Version 3.6 mit URL Zoom Funktion
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
161
app.py
161
app.py
@@ -23,7 +23,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||||||
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
||||||
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
||||||
|
|
||||||
APP_VERSION = "3.5.0"
|
APP_VERSION = "3.6.0"
|
||||||
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -50,10 +50,21 @@ def save_config(cfg):
|
|||||||
|
|
||||||
|
|
||||||
def is_url(value):
|
def is_url(value):
|
||||||
return (
|
"""Check if value is a URL (string or dict with 'url' key)"""
|
||||||
isinstance(value, str)
|
if isinstance(value, str):
|
||||||
and value.lower().startswith(("http://", "https://"))
|
return value.lower().startswith(("http://", "https://"))
|
||||||
)
|
if isinstance(value, dict) and "url" in value:
|
||||||
|
url = value.get("url", "")
|
||||||
|
return isinstance(url, str) and url.lower().startswith(("http://", "https://"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def normalize_url(value):
|
||||||
|
"""Convert URL string to dict with zoom factor, or return existing dict"""
|
||||||
|
if isinstance(value, dict) and "url" in value:
|
||||||
|
return value
|
||||||
|
if isinstance(value, str) and value.lower().startswith(("http://", "https://")):
|
||||||
|
return {"url": value, "zoom": 1.0}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@@ -133,13 +144,15 @@ def player(screen):
|
|||||||
normal_files = []
|
normal_files = []
|
||||||
|
|
||||||
# 1. Playlist-Reihenfolge
|
# 1. Playlist-Reihenfolge
|
||||||
for name in playlist:
|
for item in playlist:
|
||||||
if is_url(name):
|
if is_url(item):
|
||||||
normal_files.append({"kind": "url", "url": name})
|
url_data = normalize_url(item)
|
||||||
|
if url_data:
|
||||||
|
normal_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0)})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if os.path.exists(os.path.join(folder, name)) and allowed_file(name):
|
if os.path.exists(os.path.join(folder, item)) and allowed_file(item):
|
||||||
normal_files.append({"kind": "file", "name": name})
|
normal_files.append({"kind": "file", "name": item})
|
||||||
|
|
||||||
existing_file_names = {
|
existing_file_names = {
|
||||||
item["name"] for item in normal_files if item["kind"] == "file"
|
item["name"] for item in normal_files if item["kind"] == "file"
|
||||||
@@ -160,14 +173,16 @@ def player(screen):
|
|||||||
|
|
||||||
if priority_cfg.get("enabled", False):
|
if priority_cfg.get("enabled", False):
|
||||||
prio_files = []
|
prio_files = []
|
||||||
for name in priority_cfg.get("playlist", []):
|
for item in priority_cfg.get("playlist", []):
|
||||||
if is_url(name):
|
if is_url(item):
|
||||||
prio_files.append({"kind": "url", "url": name})
|
url_data = normalize_url(item)
|
||||||
|
if url_data:
|
||||||
|
prio_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0)})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(MEDIA_DIR, "priority", name)
|
file_path = os.path.join(MEDIA_DIR, "priority", item)
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
prio_files.append({"kind": "file", "name": name})
|
prio_files.append({"kind": "file", "name": item})
|
||||||
else:
|
else:
|
||||||
prio_files = []
|
prio_files = []
|
||||||
|
|
||||||
@@ -234,25 +249,28 @@ def admin():
|
|||||||
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
|
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
|
||||||
|
|
||||||
# 1️⃣ Playlist-Reihenfolge
|
# 1️⃣ Playlist-Reihenfolge
|
||||||
for name in playlist:
|
for item in playlist:
|
||||||
if is_url(name):
|
if is_url(item):
|
||||||
files.append({
|
url_data = normalize_url(item)
|
||||||
"name": name,
|
if url_data:
|
||||||
"type": "url",
|
files.append({
|
||||||
"size": "URL"
|
"name": url_data["url"],
|
||||||
})
|
"type": "url",
|
||||||
|
"size": "URL",
|
||||||
|
"zoom": url_data.get("zoom", 1.0)
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(path, name)
|
file_path = os.path.join(path, item)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ext = os.path.splitext(name)[1].lower()
|
ext = os.path.splitext(item)[1].lower()
|
||||||
ftype = "video" if ext == ".mp4" else "image"
|
ftype = "video" if ext == ".mp4" else "image"
|
||||||
size = os.path.getsize(file_path) // 1024
|
size = os.path.getsize(file_path) // 1024
|
||||||
|
|
||||||
files.append({
|
files.append({
|
||||||
"name": name,
|
"name": item,
|
||||||
"type": ftype,
|
"type": ftype,
|
||||||
"size": size
|
"size": size
|
||||||
})
|
})
|
||||||
@@ -286,25 +304,28 @@ def admin():
|
|||||||
prio_playlist = cfg["priority"].get("playlist", [])
|
prio_playlist = cfg["priority"].get("playlist", [])
|
||||||
|
|
||||||
# 1️⃣ Playlist-Reihenfolge
|
# 1️⃣ Playlist-Reihenfolge
|
||||||
for name in prio_playlist:
|
for item in prio_playlist:
|
||||||
if is_url(name):
|
if is_url(item):
|
||||||
priority_files.append({
|
url_data = normalize_url(item)
|
||||||
"name": name,
|
if url_data:
|
||||||
"type": "url",
|
priority_files.append({
|
||||||
"size": "URL"
|
"name": url_data["url"],
|
||||||
})
|
"type": "url",
|
||||||
|
"size": "URL",
|
||||||
|
"zoom": url_data.get("zoom", 1.0)
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(prio_dir, name)
|
file_path = os.path.join(prio_dir, item)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ext = os.path.splitext(name)[1].lower()
|
ext = os.path.splitext(item)[1].lower()
|
||||||
ftype = "video" if ext == ".mp4" else "image"
|
ftype = "video" if ext == ".mp4" else "image"
|
||||||
size = os.path.getsize(file_path) // 1024
|
size = os.path.getsize(file_path) // 1024
|
||||||
|
|
||||||
priority_files.append({
|
priority_files.append({
|
||||||
"name": name,
|
"name": item,
|
||||||
"type": ftype,
|
"type": ftype,
|
||||||
"size": size
|
"size": size
|
||||||
})
|
})
|
||||||
@@ -388,12 +409,24 @@ def upload(screen):
|
|||||||
@login_required
|
@login_required
|
||||||
def add_url(screen):
|
def add_url(screen):
|
||||||
url = request.form.get("url", "").strip()
|
url = request.form.get("url", "").strip()
|
||||||
|
zoom = request.form.get("zoom", "1.0").strip()
|
||||||
|
|
||||||
if not is_url(url):
|
if not is_url(url):
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
|
|
||||||
|
try:
|
||||||
|
zoom_factor = float(zoom)
|
||||||
|
if zoom_factor <= 0:
|
||||||
|
zoom_factor = 1.0
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
zoom_factor = 1.0
|
||||||
|
|
||||||
|
# Store as object with zoom factor
|
||||||
|
url_data = {"url": url, "zoom": zoom_factor}
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
if screen == "priority":
|
if screen == "priority":
|
||||||
config.setdefault("priority", {}).setdefault("playlist", []).append(url)
|
config.setdefault("priority", {}).setdefault("playlist", []).append(url_data)
|
||||||
else:
|
else:
|
||||||
screen_cfg = config.setdefault("screens", {}).setdefault(screen, {
|
screen_cfg = config.setdefault("screens", {}).setdefault(screen, {
|
||||||
"interval": 10,
|
"interval": 10,
|
||||||
@@ -401,7 +434,7 @@ def add_url(screen):
|
|||||||
"show_videos": True,
|
"show_videos": True,
|
||||||
"playlist": []
|
"playlist": []
|
||||||
})
|
})
|
||||||
screen_cfg.setdefault("playlist", []).append(url)
|
screen_cfg.setdefault("playlist", []).append(url_data)
|
||||||
|
|
||||||
save_config(config)
|
save_config(config)
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
@@ -424,12 +457,18 @@ def delete_file(screen, filename):
|
|||||||
# --- Playlist bereinigen ---
|
# --- Playlist bereinigen ---
|
||||||
if screen == "priority":
|
if screen == "priority":
|
||||||
playlist = cfg.get("priority", {}).get("playlist", [])
|
playlist = cfg.get("priority", {}).get("playlist", [])
|
||||||
if filename in playlist:
|
# Remove both string URLs and dict-based URLs
|
||||||
playlist.remove(filename)
|
playlist[:] = [item for item in playlist if not (
|
||||||
|
(isinstance(item, str) and item == filename) or
|
||||||
|
(isinstance(item, dict) and item.get("url") == filename)
|
||||||
|
)]
|
||||||
else:
|
else:
|
||||||
playlist = cfg["screens"].get(screen, {}).get("playlist", [])
|
playlist = cfg["screens"].get(screen, {}).get("playlist", [])
|
||||||
if filename in playlist:
|
# Remove both string URLs and dict-based URLs
|
||||||
playlist.remove(filename)
|
playlist[:] = [item for item in playlist if not (
|
||||||
|
(isinstance(item, str) and item == filename) or
|
||||||
|
(isinstance(item, dict) and item.get("url") == filename)
|
||||||
|
)]
|
||||||
|
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
@@ -444,20 +483,36 @@ def delete_file(screen, filename):
|
|||||||
def save_playlist(screen):
|
def save_playlist(screen):
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
new_order = data["playlist"] # List of filenames/URLs as strings
|
||||||
|
|
||||||
|
# Get the current playlist with all data (including zoom factors)
|
||||||
if screen == "priority":
|
if screen == "priority":
|
||||||
cfg.setdefault("priority", {}).setdefault("playlist", [])
|
old_playlist = cfg.get("priority", {}).get("playlist", [])
|
||||||
cfg["priority"]["playlist"] = data["playlist"]
|
|
||||||
else:
|
else:
|
||||||
screen_cfg = cfg.setdefault("screens", {}).setdefault(screen, {
|
old_playlist = cfg.get("screens", {}).get(screen, {}).get("playlist", [])
|
||||||
"interval": 10,
|
|
||||||
"show_images": True,
|
# Create a mapping: filename/url -> full item (preserves zoom factor)
|
||||||
"show_videos": True,
|
item_map = {}
|
||||||
"playlist": [],
|
for item in old_playlist:
|
||||||
"newsticker_text": "",
|
if isinstance(item, dict) and "url" in item:
|
||||||
"newsticker_enabled": False
|
key = item["url"] # URL as key
|
||||||
})
|
else:
|
||||||
screen_cfg["playlist"] = data["playlist"]
|
key = item # Filename as key
|
||||||
|
item_map[key] = item
|
||||||
|
|
||||||
|
# Build new playlist: use the mapping to preserve zoom factors
|
||||||
|
new_playlist = []
|
||||||
|
for key in new_order:
|
||||||
|
if key in item_map:
|
||||||
|
new_playlist.append(item_map[key])
|
||||||
|
else:
|
||||||
|
# Item not found in mapping - add as is
|
||||||
|
new_playlist.append(key)
|
||||||
|
|
||||||
|
if screen == "priority":
|
||||||
|
cfg.setdefault("priority", {})["playlist"] = new_playlist
|
||||||
|
else:
|
||||||
|
cfg.setdefault("screens", {}).setdefault(screen, {})["playlist"] = new_playlist
|
||||||
|
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|||||||
16
config.json
16
config.json
@@ -2,7 +2,10 @@
|
|||||||
"priority": {
|
"priority": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"playlist": [
|
"playlist": [
|
||||||
"https://www.meteoblue.com/en/meteotv/d7b0fd"
|
{
|
||||||
|
"url": "https://www.meteoblue.com/en/meteotv/d7b0fd",
|
||||||
|
"zoom": 1.0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"screens": {
|
"screens": {
|
||||||
@@ -11,13 +14,16 @@
|
|||||||
"show_images": true,
|
"show_images": true,
|
||||||
"show_videos": false,
|
"show_videos": false,
|
||||||
"playlist": [
|
"playlist": [
|
||||||
"https://wbxroompresence.cancom.io/standort?find=Stuttgart",
|
|
||||||
"Hilfe_KI.jpg",
|
"Hilfe_KI.jpg",
|
||||||
"e00a687f-d82a-446d-b8f3-07895fbc7309.png",
|
"e00a687f-d82a-446d-b8f3-07895fbc7309.png",
|
||||||
"Video_CANCOM_LIVE_2025_Stuttgart.MP4"
|
"Video_CANCOM_LIVE_2025_Stuttgart.MP4",
|
||||||
|
{
|
||||||
|
"url": "https://wbxroompresence.cancom.io/standort?find=Stuttgart",
|
||||||
|
"zoom": 0.8
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"newsticker_text": "Hallo dies ist der Newsticker",
|
"newsticker_text": "Herzlich willkommen bei der CANCOM - wir begr\u00fc\u00dfen unsere G\u00e4ste - wir w\u00fcnschen ihnen einen sch\u00f6nen Tag",
|
||||||
"newsticker_enabled": false
|
"newsticker_enabled": true
|
||||||
},
|
},
|
||||||
"casino": {
|
"casino": {
|
||||||
"interval": 10,
|
"interval": 10,
|
||||||
|
|||||||
@@ -177,11 +177,16 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form action="/admin/add-url/priority" method="post" class="mb-3">
|
<form action="/admin/add-url/priority" method="post" class="mb-3">
|
||||||
<div class="input-group">
|
<div class="mb-2">
|
||||||
|
<label class="form-label">URL</label>
|
||||||
<input type="url" name="url" class="form-control"
|
<input type="url" name="url" class="form-control"
|
||||||
placeholder="https://example.com" required>
|
placeholder="https://example.com" required>
|
||||||
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Zoom Faktor (z.B. 1.0, 1.5, 2.0)</label>
|
||||||
|
<input type="number" name="zoom" class="form-control" step="0.1" value="1.0" min="0.1">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Playlist -->
|
<!-- Playlist -->
|
||||||
@@ -199,9 +204,9 @@
|
|||||||
title="<img src='/media/priority/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
|
title="<img src='/media/priority/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
|
||||||
{% elif file.type == "url" %}
|
{% elif file.type == "url" %}
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
title="URL: {{ file.name }}"
|
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
{{ file.name }}
|
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{% if file.type == "image" %}
|
{% if file.type == "image" %}
|
||||||
@@ -326,11 +331,16 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form action="/admin/add-url/{{ screen }}" method="post" class="mb-4">
|
<form action="/admin/add-url/{{ screen }}" method="post" class="mb-4">
|
||||||
<div class="input-group">
|
<div class="mb-2">
|
||||||
|
<label class="form-label">URL</label>
|
||||||
<input type="url" name="url" class="form-control"
|
<input type="url" name="url" class="form-control"
|
||||||
placeholder="https://example.com" required>
|
placeholder="https://example.com" required>
|
||||||
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Zoom Faktor (z.B. 1.0, 1.5, 2.0)</label>
|
||||||
|
<input type="number" name="zoom" class="form-control" step="0.1" value="1.0" min="0.1">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
@@ -350,9 +360,9 @@
|
|||||||
title="<img src='/media/{{ screen }}/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
|
title="<img src='/media/{{ screen }}/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
|
||||||
{% elif file.type == "url" %}
|
{% elif file.type == "url" %}
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
title="URL: {{ file.name }}"
|
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
{{ file.name }}
|
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{% if file.type == "image" %}
|
{% if file.type == "image" %}
|
||||||
|
|||||||
@@ -22,8 +22,9 @@
|
|||||||
background: black;
|
background: black;
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 50%;
|
||||||
left: 0;
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +123,14 @@ function normalizeItem(item) {
|
|||||||
if (typeof item === "string") {
|
if (typeof item === "string") {
|
||||||
const lower = item.toLowerCase();
|
const lower = item.toLowerCase();
|
||||||
if (lower.startsWith("http://") || lower.startsWith("https://")) {
|
if (lower.startsWith("http://") || lower.startsWith("https://")) {
|
||||||
return { kind: "url", url: item };
|
return { kind: "url", url: item, zoom: 1.0 };
|
||||||
}
|
}
|
||||||
return { kind: "file", name: item };
|
return { kind: "file", name: item };
|
||||||
}
|
}
|
||||||
|
// Item is already an object (from backend) - ensure zoom is set
|
||||||
|
if (item.kind === "url" && !("zoom" in item)) {
|
||||||
|
item.zoom = 1.0;
|
||||||
|
}
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,10 +203,27 @@ function playNext() {
|
|||||||
vid.pause();
|
vid.pause();
|
||||||
vid.src = "";
|
vid.src = "";
|
||||||
iframe.src = "";
|
iframe.src = "";
|
||||||
|
iframe.style.transform = "scale(1)"; // Reset zoom
|
||||||
|
|
||||||
if (item.kind === "url") {
|
if (item.kind === "url") {
|
||||||
iframe.style.display = "block";
|
iframe.style.display = "block";
|
||||||
iframe.src = item.url;
|
iframe.src = item.url;
|
||||||
|
// Apply zoom factor with browser-like behavior
|
||||||
|
// Zoom < 1.0: mehr Inhalt sichtbar (wie Browser herauszoomen)
|
||||||
|
// Zoom > 1.0: weniger Inhalt sichtbar (wie Browser reinzoomen)
|
||||||
|
const zoom = item.zoom || 1.0;
|
||||||
|
|
||||||
|
if (zoom !== 1.0) {
|
||||||
|
// Adjust iframe size and scale to simulate real browser zoom
|
||||||
|
const scale = 1 / zoom; // Inverted for proper zoom behavior
|
||||||
|
iframe.style.width = `${100 * scale}vw`;
|
||||||
|
iframe.style.height = `${100 * scale}vh`;
|
||||||
|
iframe.style.transform = `translate(-50%, -50%) scale(${zoom})`;
|
||||||
|
} else {
|
||||||
|
iframe.style.width = `100vw`;
|
||||||
|
iframe.style.height = `100vh`;
|
||||||
|
iframe.style.transform = `translate(-50%, -50%)`;
|
||||||
|
}
|
||||||
setTimeout(playNext, interval);
|
setTimeout(playNext, interval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user