Added manifest.json, made the site more mobile-friendly
This commit is contained in:
parent
361f09c728
commit
7832f6e6b8
224
main.py
224
main.py
@ -9,8 +9,7 @@ import os
|
||||
import time
|
||||
import multiprocessing
|
||||
|
||||
from flask import request
|
||||
from flask import jsonify
|
||||
from flask import request, jsonify, send_from_directory
|
||||
|
||||
from PyQt5.QtWidgets import *
|
||||
from PyQt5.QtCore import *
|
||||
@ -486,199 +485,32 @@ def index():
|
||||
grouped_channels.setdefault(channel["group"], []).append(channel)
|
||||
|
||||
flat_channel_list = [channel for channel in channels]
|
||||
|
||||
html = f"""
|
||||
<html>
|
||||
<head>
|
||||
<title>IPMPV</title>
|
||||
<style>
|
||||
body {{ background-color: #111111; font-family: Fira Sans Regular, Arial, sans-serif; color: white; text-align: center; }}
|
||||
.channel {{ display: flex; align-items: center; padding: 5px; }}
|
||||
.channel img {{ width: 50px; height: auto; margin-right: 10px; }}
|
||||
.group-container {{ display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; }}
|
||||
.group {{ display: flex; flex-direction: column; margin-top: 20px; font-size: 20px; font-weight: bold; }}
|
||||
button {{
|
||||
padding: 10px;
|
||||
min-width: 200px;
|
||||
margin: 5px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
border: none;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
background-color: #222222;
|
||||
border-radius: 15px;
|
||||
}}
|
||||
button.input-btn {{
|
||||
padding: 10px;
|
||||
min-width: 200px;
|
||||
margin: 5px 5px 5px 0;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
border: none;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
background-color: #222222;
|
||||
border-radius: 0;
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}}
|
||||
#latency-btn {{
|
||||
padding: 10px;
|
||||
min-width: 50px;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
border: none;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
border-radius: 0;
|
||||
}}
|
||||
input {{
|
||||
padding: 10px;
|
||||
min-width: 500px;
|
||||
margin: 5px 0 5px 5px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
border: none;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
background-color: #303030;
|
||||
border-top-left-radius: 15px;
|
||||
border-bottom-left-radius: 15px;
|
||||
}}
|
||||
button:hover {{
|
||||
background-color: #444444;
|
||||
}}
|
||||
button:active {{
|
||||
background-color: #666666;
|
||||
transform: scale(0.95);
|
||||
}}
|
||||
button.input-btn:active {{
|
||||
background-color: #666666;
|
||||
transform: none;
|
||||
}}
|
||||
button.OFF {{
|
||||
background-color: #770000; /* Default OFF color */
|
||||
}}
|
||||
button.ON {{
|
||||
background-color: #007700; /* Default OFF color */
|
||||
}}
|
||||
#osd-on-btn {{
|
||||
min-width: 100px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 15px;
|
||||
border-bottom-left-radius: 15px;
|
||||
}}
|
||||
#osd-off-btn {{
|
||||
min-width: 100px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to IPMPV</h1>
|
||||
<p>Current Channel: {channels[current_index]['name'] if current_index is not None else "None"}</p>
|
||||
<button onclick="stopPlayer()">Stop</button>
|
||||
"""
|
||||
#<button onclick="changeChannel({(current_index - 1) % len(channels)})">Previous</button>
|
||||
#<button onclick="changeChannel({(current_index + 1) % len(channels)})">Next</button>
|
||||
|
||||
global deinterlace
|
||||
global low_latency
|
||||
deinterlace_state = "ON" if deinterlace else "OFF"
|
||||
retroarch_state = "ON" if retroarch_p and retroarch_p.poll() is None else "OFF"
|
||||
html += f"""
|
||||
<button id="retroarch-btn" class="{retroarch_state}" onclick="toggleRetroArch()"><span id="retroarch-state">{"Stop RetroArch" if retroarch_p and retroarch_p.poll() is None else "Start RetroArch"}</span></button>
|
||||
<button id="deinterlace-btn" class="{deinterlace_state}" onclick="toggleDeinterlace()">Deinterlacing: <span id="deinterlace-state">{deinterlace_state}</span></button>
|
||||
<button id="resolution-btn" onclick="toggleResolution()">Resolution: <span id="resolution-state">{resolution}</span></button>
|
||||
<h2>Toggle OSD</h2>
|
||||
<button id="osd-on-btn" onclick="showOSD()">on</button><button id="osd-off-btn" onclick="hideOSD()">off</button>
|
||||
"""
|
||||
html += f"""
|
||||
<h2>Play Custom URL</h2>
|
||||
<input type="text" id="custom-url" placeholder="Enter stream URL"><button id="latency-btn" class="{'ON' if low_latency else 'OFF'}" onclick="toggleLatency()"><span id="latency-state">{'Lo' if low_latency else 'Hi'}</span></button><button class="input-btn" onclick="playCustomURL()">Play</button>
|
||||
<h2>All Channels</h2>
|
||||
<div class="group-container">
|
||||
"""
|
||||
|
||||
|
||||
# Create the channel groups HTML
|
||||
channel_groups_html = ""
|
||||
for group, ch_list in grouped_channels.items():
|
||||
html += f'<div class="group">{group}'
|
||||
channel_groups_html += f'<div class="group">{group}'
|
||||
for channel in ch_list:
|
||||
index = flat_channel_list.index(channel) # Get correct global index
|
||||
html += f'''
|
||||
channel_groups_html += f'''
|
||||
<div class="channel">
|
||||
<img src="{channel['logo']}" onerror="this.style.display='none'">
|
||||
<button onclick="changeChannel({index})">{channel['name']}</button>
|
||||
</div>
|
||||
'''
|
||||
html += '</div>'
|
||||
|
||||
|
||||
html += """
|
||||
</div>
|
||||
<script>
|
||||
function playCustomURL() {
|
||||
const url = document.getElementById("custom-url").value;
|
||||
if (!url.trim()) return; // Ignore empty input
|
||||
fetch(`/play_custom?url=${encodeURIComponent(url)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Now playing: " + url);
|
||||
} else {
|
||||
alert("Error: " + data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
function toggleLatency() {
|
||||
fetch(`/toggle_latency`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("latency-state").textContent = data.state ? "Lo" : "Hi";
|
||||
document.getElementById("latency-btn").style.backgroundColor = data.state ? "#007700" : "#770000";
|
||||
});
|
||||
}
|
||||
function toggleRetroArch() {
|
||||
fetch(`/toggle_retroarch`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("retroarch-state").textContent = data.state ? "Stop RetroArch" : "Start RetroArch";
|
||||
document.getElementById("retroarch-btn").style.backgroundColor = data.state ? "#007700" : "#770000";
|
||||
});
|
||||
}
|
||||
function toggleResolution() {
|
||||
fetch(`/toggle_resolution`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("resolution-state").textContent = data.res;
|
||||
});
|
||||
}
|
||||
function stopPlayer() {
|
||||
fetch(`/stop_player`).then(() => window.location.reload());
|
||||
}
|
||||
function changeChannel(index) {
|
||||
fetch(`/channel?index=${index}`).then(() => window.location.reload());
|
||||
}
|
||||
function toggleDeinterlace() {
|
||||
fetch(`/toggle_deinterlace`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("deinterlace-state").textContent = data.state ? "ON" : "OFF";
|
||||
document.getElementById("deinterlace-btn").style.backgroundColor = data.state ? "#007700" : "#770000";
|
||||
});
|
||||
}
|
||||
function showOSD() {
|
||||
fetch(`/show_osd`).then(response => response.json())
|
||||
}
|
||||
function hideOSD() {
|
||||
fetch(`/hide_osd`).then(response => response.json())
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
channel_groups_html += '</div>'
|
||||
|
||||
# Replace placeholders with actual values
|
||||
html = open("templates/index.html").read()
|
||||
html = html.replace("%CURRENT_CHANNEL%", channels[current_index]['name'] if current_index is not None else "None")
|
||||
html = html.replace("%RETROARCH_STATE%", "ON" if retroarch_p and retroarch_p.poll() is None else "OFF")
|
||||
html = html.replace("%RETROARCH_LABEL%", "Stop RetroArch" if retroarch_p and retroarch_p.poll() is None else "Start RetroArch")
|
||||
html = html.replace("%DEINTERLACE_STATE%", "ON" if deinterlace else "OFF")
|
||||
html = html.replace("%RESOLUTION%", resolution)
|
||||
html = html.replace("%LATENCY_STATE%", "ON" if low_latency else "OFF")
|
||||
html = html.replace("%LATENCY_LABEL%", "Lo" if low_latency else "Hi")
|
||||
html = html.replace("%CHANNEL_GROUPS%", channel_groups_html)
|
||||
|
||||
return html
|
||||
|
||||
def is_valid_url(url):
|
||||
@ -765,7 +597,7 @@ def toggle_retroarch():
|
||||
print("Launching RetroArch")
|
||||
retroarch_env = os.environ.copy()
|
||||
retroarch_env["MESA_GL_VERSION_OVERRIDE"] = "3.3"
|
||||
retroarch_p = subprocess.Popen(re.split('\s', ipmpv_retroarch_cmd if ipmpv_retroarch_cmd is not None else 'retroarch'), env=retroarch_env)
|
||||
retroarch_p = subprocess.Popen(re.split("\\s", ipmpv_retroarch_cmd if ipmpv_retroarch_cmd is not None else 'retroarch'), env=retroarch_env)
|
||||
return jsonify(state=True)
|
||||
|
||||
@app.route("/toggle_latency")
|
||||
@ -823,6 +655,22 @@ def toggle_resolution():
|
||||
|
||||
return jsonify(res=get_current_resolution())
|
||||
|
||||
@app.route('/manifest.json')
|
||||
def serve_manifest():
|
||||
return send_from_directory("static", 'manifest.json',
|
||||
mimetype='application/manifest+json')
|
||||
|
||||
# Serve icon files from root
|
||||
@app.route('/icon512_rounded.png')
|
||||
def serve_rounded_icon():
|
||||
return send_from_directory("static", 'icon512_rounded.png',
|
||||
mimetype='image/png')
|
||||
|
||||
@app.route('/icon512_maskable.png')
|
||||
def serve_maskable_icon():
|
||||
return send_from_directory("static", 'icon512_maskable.png',
|
||||
mimetype='image/png')
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start Qt process
|
||||
qt_proc = multiprocessing.Process(target=qt_process)
|
||||
|
||||
BIN
static/icon512_maskable.png
Normal file
BIN
static/icon512_maskable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
static/icon512_rounded.png
Normal file
BIN
static/icon512_rounded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
27
static/manifest.json
Normal file
27
static/manifest.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"theme_color":"#222222",
|
||||
"background_color":"#222222",
|
||||
"start_url": "/",
|
||||
"icons":
|
||||
[
|
||||
{
|
||||
"purpose":"maskable",
|
||||
"sizes":"512x512",
|
||||
"src":"icon512_maskable.png",
|
||||
"type":"image/png"
|
||||
},{
|
||||
"purpose":"any",
|
||||
"sizes":"512x512",
|
||||
"src":"icon512_rounded.png",
|
||||
"type":"image/png"
|
||||
}
|
||||
],
|
||||
"orientation":"any",
|
||||
"display":"standalone",
|
||||
"dir":"auto",
|
||||
"lang":"en",
|
||||
"name":"IPMPV",
|
||||
"short_name":"IPMPV",
|
||||
"description":"Remote control for your IPMPV instance",
|
||||
"id":"cc.netpaws.ipmpv"
|
||||
}
|
||||
474
templates/index.html
Normal file
474
templates/index.html
Normal file
@ -0,0 +1,474 @@
|
||||
<!-- Updated HTML/CSS for the index() function in Flask -->
|
||||
<html>
|
||||
<head>
|
||||
<title>IPMPV</title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<style>
|
||||
:root {
|
||||
--primary-bg: #111111;
|
||||
--secondary-bg: #282828;
|
||||
--input-bg: #303030;
|
||||
--button-hover: #444444;
|
||||
--button-active: #666666;
|
||||
--text-color: white;
|
||||
--on-state: #007700;
|
||||
--off-state: #770000;
|
||||
--border-radius: 15px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--primary-bg);
|
||||
font-family: Fira Sans Regular, Arial, sans-serif;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
max-width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.channel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.channel button {
|
||||
width: 100%;
|
||||
box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.channel img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.group-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 0 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 20px 8px 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border: 1px solid #333;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px;
|
||||
min-width: 120px;
|
||||
margin: 5px;
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Control buttons layout */
|
||||
.control-button {
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.url-input-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.url-input-group {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px;
|
||||
flex-grow: 1;
|
||||
margin: 5px 0;
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.osd-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#osd-on-btn {
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
min-width: 80px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#osd-off-btn {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
min-width: 80px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#latency-btn {
|
||||
min-width: 60px;
|
||||
width: 60px;
|
||||
margin: 5px 0;
|
||||
border-radius: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.custom-url-input {
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.input-btn {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--button-hover);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: var(--button-active);
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
button.OFF {
|
||||
background-color: var(--off-state);
|
||||
}
|
||||
|
||||
button.ON {
|
||||
background-color: var(--on-state);
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
margin: 15px 0 10px 0;
|
||||
}
|
||||
|
||||
/* For tablets and larger screens */
|
||||
@media (min-width: 768px) {
|
||||
.channel button {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.url-input-group {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.custom-url-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.input-btn {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* For larger desktops - maintain original spacing */
|
||||
@media (min-width: 1200px) {
|
||||
.channel img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile touch improvements */
|
||||
@media (max-width: 767px) {
|
||||
button, input {
|
||||
padding: 14px; /* Larger touch targets */
|
||||
margin: 8px 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.group-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.url-input-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-url-input {
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
#latency-btn {
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.input-btn {
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to IPMPV</h1>
|
||||
<p>Current Channel: <span id="current-channel">%CURRENT_CHANNEL%</span></p>
|
||||
|
||||
<div class="controls">
|
||||
<button class="control-button" onclick="stopPlayer()">Stop</button>
|
||||
<button id="retroarch-btn" class="%RETROARCH_STATE%" onclick="toggleRetroArch()">
|
||||
<span id="retroarch-state">%RETROARCH_LABEL%</span>
|
||||
</button>
|
||||
<button id="deinterlace-btn" class="%DEINTERLACE_STATE%" onclick="toggleDeinterlace()">
|
||||
Deinterlacing: <span id="deinterlace-state">%DEINTERLACE_STATE%</span>
|
||||
</button>
|
||||
<button id="resolution-btn" class="control-button" onclick="toggleResolution()">
|
||||
Resolution: <span id="resolution-state">%RESOLUTION%</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Toggle OSD</h2>
|
||||
<div class="osd-toggle">
|
||||
<button id="osd-on-btn" onclick="showOSD()">on</button>
|
||||
<button id="osd-off-btn" onclick="hideOSD()">off</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Play Custom URL</h2>
|
||||
<div class="url-input-container">
|
||||
<div class="url-input-group">
|
||||
<input type="text" id="custom-url" class="custom-url-input" placeholder="Enter stream URL">
|
||||
<button id="latency-btn" class="%LATENCY_STATE%" onclick="toggleLatency()">
|
||||
<span id="latency-state">%LATENCY_LABEL%</span>
|
||||
</button>
|
||||
<button class="input-btn" onclick="playCustomURL()">Play</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>All Channels</h2>
|
||||
<div class="group-container">
|
||||
<!-- Channel groups will be inserted here -->
|
||||
%CHANNEL_GROUPS%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Improve mobile touch experience
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add active/touch state for all buttons
|
||||
const buttons = document.querySelectorAll('button');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('touchstart', function() {
|
||||
this.classList.add('touching');
|
||||
});
|
||||
button.addEventListener('touchend', function() {
|
||||
this.classList.remove('touching');
|
||||
});
|
||||
});
|
||||
|
||||
// Auto hide address bar on mobile
|
||||
window.scrollTo(0, 1);
|
||||
});
|
||||
|
||||
function playCustomURL() {
|
||||
const url = document.getElementById("custom-url").value;
|
||||
if (!url.trim()) return; // Ignore empty input
|
||||
|
||||
// Show loading indicator
|
||||
const playButton = document.querySelector('.input-btn');
|
||||
const originalText = playButton.textContent;
|
||||
playButton.textContent = "Loading...";
|
||||
playButton.disabled = true;
|
||||
|
||||
fetch(`/play_custom?url=${encodeURIComponent(url)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
playButton.textContent = originalText;
|
||||
playButton.disabled = false;
|
||||
|
||||
if (data.success) {
|
||||
// Show toast instead of alert on mobile
|
||||
showToast("Now playing: " + url);
|
||||
} else {
|
||||
showToast("Error: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
playButton.textContent = originalText;
|
||||
playButton.disabled = false;
|
||||
showToast("Connection error. Please try again.");
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLatency() {
|
||||
fetch(`/toggle_latency`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("latency-state").textContent = data.state ? "Lo" : "Hi";
|
||||
document.getElementById("latency-btn").className = data.state ? "ON" : "OFF";
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRetroArch() {
|
||||
fetch(`/toggle_retroarch`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("retroarch-state").textContent = data.state ? "Stop RetroArch" : "Start RetroArch";
|
||||
document.getElementById("retroarch-btn").className = data.state ? "ON" : "OFF";
|
||||
});
|
||||
}
|
||||
|
||||
function toggleResolution() {
|
||||
fetch(`/toggle_resolution`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("resolution-state").textContent = data.res;
|
||||
});
|
||||
}
|
||||
|
||||
function stopPlayer() {
|
||||
fetch(`/stop_player`).then(() => window.location.reload());
|
||||
}
|
||||
|
||||
function changeChannel(index) {
|
||||
// Show loading indicator
|
||||
const channelButtons = document.querySelectorAll('.channel button');
|
||||
channelButtons.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
});
|
||||
|
||||
showToast("Loading channel...");
|
||||
|
||||
fetch(`/channel?index=${index}`)
|
||||
.then(() => window.location.reload())
|
||||
.catch(() => {
|
||||
channelButtons.forEach(btn => {
|
||||
btn.disabled = false;
|
||||
});
|
||||
showToast("Error loading channel. Try again.");
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDeinterlace() {
|
||||
fetch(`/toggle_deinterlace`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("deinterlace-state").textContent = data.state ? "ON" : "OFF";
|
||||
document.getElementById("deinterlace-btn").className = data.state ? "ON" : "OFF";
|
||||
});
|
||||
}
|
||||
|
||||
function showOSD() {
|
||||
fetch(`/show_osd`).then(response => response.json());
|
||||
}
|
||||
|
||||
function hideOSD() {
|
||||
fetch(`/hide_osd`).then(response => response.json());
|
||||
}
|
||||
|
||||
// Mobile-friendly toast notification
|
||||
function showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast';
|
||||
toast.textContent = message;
|
||||
toast.style.position = 'fixed';
|
||||
toast.style.bottom = '20px';
|
||||
toast.style.left = '50%';
|
||||
toast.style.transform = 'translateX(-50%)';
|
||||
toast.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||
toast.style.color = 'white';
|
||||
toast.style.padding = '12px 20px';
|
||||
toast.style.borderRadius = '25px';
|
||||
toast.style.zIndex = '1000';
|
||||
toast.style.maxWidth = '80%';
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transition = 'opacity 0.5s ease';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(toast);
|
||||
}, 500);
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user