diff --git a/CHANGELOG.md b/CHANGELOG.md index c570d08..039f910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,22 +6,30 @@ This project follows [Semantic Versioning](https://semver.org/). --- -## v4.2.1 - 2025-11-10 +## v4.3.0 - 2025-11-14 ### Added -- Added horizontal scroll/refresh as time moves forward -- Added API dynamic guide timing refresh +- New mobile UI assets including mobile.css, mobile-header.css, mobile-popup.css, and mobile-submenu.css to improve layout and usability on mobile devices. +- New mobile-related JavaScript files including mobile-nav.js, mobile-scroll-fix.js, and mobile-player-adapt.js. +- Added tuner management enhancements through the new tuner-settings.js script. +- New templates added: _header.html, about.html, change_tuner.html, manage_users.html, and logs.html to support expanded UI functionality. + +### Changed +- Updated app.py to support the expanded template set, updated tuner handling, and new mobile behavior. +- Updated bump_version.py and bump_version.sh for compatibility with the current file layout and new release processes. +- Updated installation scripts: retroiptv_linux.sh, retroiptv_rpi.sh, retroiptv_windows.bat, and retroiptv_windows.ps1 to align with the new release structure. --- -## [Unreleased] +## v4.2.1 - 2025-11-10 -- Planned: add `.m3u8` tuner support. -- Planned: move logs to SQLite DB. -- Planned: log filtering and pagination. +### Added +- Added horizontal scroll/refresh as time moves forward +- Added API dynamic guide timing refresh --- + ## v4.2.0 - 2025-11-06 This version introduces mobile responsiveness, a new theme, refinements to auto-scroll, and backend API structures. @@ -40,6 +48,7 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Resolved layout inconsistencies across themes and display sizes. - General code cleanup and alignment for CI/CD consistency. +--- ## v4.1.0 - 2025-10-25 ### New Features @@ -71,6 +80,8 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Improved guide performance and browser compatibility with the new auto-scroll implementation. - Minor visual and layout corrections across settings and guide pages. +--- + ## v4.0.0 — 2025-10-19 **Status:** Public Release (Feature Complete) @@ -100,6 +111,8 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - PlutoTV / custom tuner aggregation features - Enhanced guide refresh logic for long-running sessions +--- + ## v3.3.0 - 2025-10-15 ### Added @@ -118,7 +131,6 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- --- - ## [v3.2.0] - 2025-10-11 ### Added - **Containerization & TrueNAS Deployment Support** @@ -137,6 +149,7 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- ### Fixed - Corrected GHCR tag formatting for TrueNAS (eliminated `:latest:latest` errors). - Fixed workflow permissions with explicit `packages: write` and PAT authentication. + --- ## v3.1.0 - 2025-10-09 @@ -188,10 +201,8 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Prevented guide from displaying outdated EPG after tuner change. - Corrected case where missing XML data produced empty grid. - --- - ## [3.0.0] - 2025-10-03 ### Added - **Windows Support**: @@ -220,6 +231,8 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Consistent logging of user agreement and installer actions. - Ensured firewall rule removal on Windows during uninstall. +--- + ## [2.3.2] - 2025-09-26 ### Added - Introduced unified **Themes submenu** (Light, Dark, AOL/CompuServe, TV Guide Magazine) across all admin and user pages. @@ -238,10 +251,8 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Theme persistence issues: selected theme now applies instantly and consistently on every page. - AOL and Magazine themes now update **immediately** on About and other pages (previously only visible after navigating away). - --- - ## v2.3.1 - 2025-09-26 ### Added @@ -295,9 +306,9 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- - Fixed alignment of tuner forms with consistent dropdowns and validation. - Ensured flash messages and logging work consistently across all tuner operations. +--- - -## [v2.0.0] – 2025-09-24 +## [v2.0.0] 2025-09-24 ### Added - Tuner URL validation: new validate_tuner_url() function checks XML/M3U inputs before saving. - Detects invalid/empty URLs, unresolvable hostnames, and distinguishes between public vs. private IPs. @@ -335,7 +346,7 @@ This version introduces mobile responsiveness, a new theme, refinements to auto- --- -## [v1.x.x] – 2025-09-01 → 2025-09-23 +## [v1.x.x] 2025-09-01 - 2025-09-23 ### Added - Initial IPTV Flask application with: - User authentication (login/logout, password change). diff --git a/INSTALL.md b/INSTALL.md index baad1a2..e3ded14 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,7 +1,7 @@ # Installation Guide -**Version:** v4.2.1 -**Last Updated:** 2025-10-25 +**Version:** v4.3.0 +**Last Updated:** 2025-11-14 --- @@ -20,7 +20,6 @@ ## 🐳 Quick Docker Run -The fastest way to launch **RetroIPTVGuide v3.2.0**: ```bash docker pull ghcr.io/thehack904/retroiptvguide:latest @@ -110,7 +109,7 @@ sudo retroiptv_rpi.sh update ``` ### 🪟 Windows -**Alignment with Linux/Pi currently on track for v4.0.1 release** + ```powershell git fetch --all ; git reset --hard origin/main ; Restart-Service RetroIPTVGuide ``` diff --git a/README.md b/README.md index 63191cd..d14cfee 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# 📺 RetroIPTVGuide v4.2.1 +# 📺 RetroIPTVGuide vv4.3.0

- Version + Version GHCR diff --git a/ROADMAP.md b/ROADMAP.md index 4644d2c..7c92c9a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,22 +1,21 @@ # 📌 RetroIPTVGuide — Roadmap This document tracks **planned upgrades** and ideas for improving the IPTV Flask server. -These are **not yet implemented**, but provide a development path for future releases. +These are **not yet implemented**, partially implemented, or completed in previous releases. --- -# Current Version: **v4.2.1 (2025-11-10)** -This version adds horizontal scroll/refresh as time moves forward, API dynamic guide timing refresh +# Current Version: **v4.3.0 (2025-11-14)** ## 🔮 Feature Upgrades ### 1. Tuner Management - [x] Add ability to **add/remove tuners** from the UI (v2.3.0). - [x] Add ability to rename tuners via the UI (v2.3.0). -- [ ] Support for **.m3u8 single-channel playlists** as tuner sources (planned v3.2.0). +- [ ] Support for **.m3u8 single-channel playlists** as tuner sources. - [x] Validate tuner URLs (ping/check format before saving) (v2.0.0). -- [ ] Optional auto-refresh of tuner lineup on a schedule. -- [ ] Add per-user tuner assignment and default tuner preferences. *(v4.3.x planned)* -- [ ] Introduce combined tuner builder (custom tuner aggregation). *(v5.x.x planned)* +- [x] Optional auto-refresh of tuner lineup on a schedule. *(v4.3.0)* +- [ ] Add per-user tuner assignment and default tuner preferences. +- [ ] Introduce combined tuner builder (custom tuner aggregation). --- @@ -24,107 +23,98 @@ This version adds horizontal scroll/refresh as time moves forward, API dynamic g - [ ] Move logs from flat file (`activity.log`) into **SQLite DB** for better querying. - [ ] Add filtering and pagination in logs view. - [ ] Add system health checks (tuner reachability, XMLTV freshness). -- [x] **Admin log management**: clear logs + file size indicator (v2.3.1). +- [x] Admin log management: clear logs + size indicator (v2.3.1). - [x] Post-install HTTP service verification in Pi installer (v3.1.0). -- [x] Unified “Refresh Guide” scheduler. *(v4.2.0)* +- [x] Unified “Refresh Guide” scheduler. (v4.2.0) --- ### 3. Guide & Playback -- [x] **Auto-Scroll feature** added for the Live Guide (v4.1.0). - - Uses `requestAnimationFrame` for smooth scroll with fallback watchdog. - - Deterministic looping and localStorage preference tracking. +- [x] Auto-Scroll feature added for the Live Guide (v4.1.0). - [x] Improved auto-scroll performance and modular handling (v4.2.0). - [x] Added responsive layout for mobile devices (v4.2.0). - [ ] Add search/filter box to guide. - [ ] Add ability to set favorites. - [x] Fallback message for missing EPG info (v3.0.1). - [ ] Add reminders/notifications for upcoming programs. -- [ ] Add EPG caching for faster guide reloads. *(v5.x.x planned)* +- [ ] Add EPG caching for faster guide reloads. --- ### 4. User Management -- [x] Add **manage_users.html** for integrated user control panel. *(v4.0.0)* -- [ ] Role-based access control (admin/user/read-only). +- [x] Add manage_users.html (v4.0.0) +- [-] Role-based access control (basic admin-only gates exist, no RBAC roles). - [ ] Add email or 2FA support for login. - [ ] Show last login time in admin panel. -- [ ] User role/channel restrictions. *(v5.x.x planned)* +- [ ] User role/channel restrictions. --- ### 5. UI/UX Improvements - [x] Unified theming across all templates (v2.3.2). -- [x] Android / Fire / Google TV optimized. *(v4.0.0)* -- [x] Consolidated UI templates (`guide.html`, `login.html`, etc.). *(v4.0.0)* -- [x] **Refactored UI templates into shared `base.html` and `_header.html` (v4.1.0)**. -- [x] **Modular CSS and JS added (v4.1.0)** – per-page styling and script loading. +- [x] Android / Fire / Google TV optimized (v4.0.0). +- [x] Consolidated UI templates (`guide.html`, `login.html`, etc.). (v4.0.0) +- [x] Refactored UI templates into shared `base.html` and `_header.html` (v4.1.0). +- [x] Modular CSS and JS added (v4.1.0). - [x] Introduced new JS modules: `auto-scroll.js`, `tuner-settings.js`. -- [x] **Mobile responsive layout and navigation (v4.2.0)**. +- [x] Mobile responsive layout and navigation (v4.2.0). - [ ] Add dark/light theme auto-detect. -- [ ] Frozen header timeline to prevent scrolling with channel listing. +- [x] Frozen header timeline to prevent scrolling with channel listing (v4.3.0). - [x] About page under Settings menu (v2.3.1). +- [x] Added new mobile-specific CSS and JS (v4.3.0). +- [x] Added new templates: change_tuner.html, manage_users.html, logs.html. (v4.3.0) --- ### 6. Cross-Platform -- [x] Unified Linux, Windows, and Raspberry Pi installers. *(v4.0.0)* -- [x] Windows update/uninstall parity implemented. *(v4.1.0)* +- [x] Unified Linux, Windows, and Raspberry Pi installers. (v4.0.0) +- [x] Windows update/uninstall parity implemented. (v4.1.0) - [ ] Create MacOS install/executable. - [x] Validate/test installers on all Windows environments. -- [ ] Explore TrueNAS SCALE App Catalog certification. *(v5.x.x planned)* +- [ ] Explore TrueNAS SCALE App Catalog certification. --- ### 7. New Features -- [ ] Add auto-play stream on login (ErsatzTV integration). -- [ ] Default auto-play source selection. -- [ ] Begin integration path for **PlutoTV / external IPTV services**. *(v5.x.x)* +- [-] Add auto-play stream on login. *(partial JS scaffolding in tuner-settings.js)* +- [-] Default auto-play source selection. *(partial JS only, not wired to UI)* +- [ ] Begin integration path for PlutoTV / external IPTV services. --- ### 8. Planned Enhancements -- [ ] Add safety checks in `add_tuner()` (duplicate prevention + URL validation). +- [ ] Add safety checks in add_tuner() (duplicate prevention + URL validation). - [x] GPU verification after `raspi-config` (v3.1.0). - [x] Suppress `rfkill` Wi-Fi message during GPU config (v3.1.0). - [x] Adaptive HTTP check loop (v3.1.0). -- [x] **Project structure and documentation reorganized** *(v4.0.0–4.2.0)* +- [x] Project structure and documentation reorganized (v4.0.0–4.2.0). --- ## ⚙️ Technical Improvements - [x] Add uninstall.sh (v2.3.0). - [ ] Validate/test uninstall script fully on Windows. -- [ ] Add HTTPS + optional token-based authentication. *(v4.5.x)* -- [x] Refactor tuner handling for unified DB. *(v4.0.0)* -- [x] **Updated bump_version and installer scripts to auto-track new version (v4.1.0)** +- [ ] Add HTTPS + optional token-based authentication. +- [x] Refactor tuner handling for unified DB. (v4.0.0) +- [x] Updated bump_version and installer scripts for new structure. (v4.3.0) - [x] Containerize app (Dockerfile + Compose). - [ ] Add migrations for DB schema changes. -- [ ] CI/CD automation for official builds. *(v5.x.x)* -- [ ] Add test suite for tuner parsing, authentication, and logging. +- [ ] CI/CD automation for official builds. +- [-] Add test suite for tuner parsing, authentication, and logging. *(placeholder file only)* --- ## 🍓 Installer Enhancements -- [x] Unified installer architecture. *(v4.0.0)* -- [x] Windows update/uninstall parity complete. *(v4.1.0)* +- [x] Unified installer architecture. (v4.0.0) +- [x] Windows update/uninstall parity complete. (v4.1.0) - [ ] Add kiosk/headless mode selector. - [ ] Add `--mode kiosk` flag for non-interactive installs. - [ ] Validate update/uninstall paths on all OSes. --- + ## User Submitted Enhancements -- [ ] Casting Support - (Chromecast Support) -- [ ] Resize Pop Out Video - (pop out the video player resize) -- [ ] Resize video on page (able to resize or change the layout of the video on the program guide page) +- [ ] Casting Support (Chromecast) +- [ ] Resize Pop Out Video +- [ ] Resize video on page - [ ] Auto load Channel from Guide / Hidden Channel / Sizzle Reels - - -## ✅ Completed (v4.2.0) -- [x] Added mobile responsive layout. -- [x] Improved auto-scroll handling and modular JS design. -- [x] Updated documentation (CHANGELOG, README, INSTALL, ROADMAP). -- [x] Added RetroIPTV Theme (default). -- [x] Release tagged as **v4.2.0** - - diff --git a/app.py b/app.py index ae27266..1123e9e 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ # app.py — merged version (features from both sources) -APP_VERSION = "v4.2.1" -APP_RELEASE_DATE = "2025-11-10" +APP_VERSION = "v4.3.0" +APP_RELEASE_DATE = "2025-11-14" from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, abort from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user @@ -12,6 +12,7 @@ import os import datetime import requests +import time import xml.etree.ElementTree as ET from urllib.parse import urlparse, urljoin import socket @@ -19,6 +20,7 @@ import logging import subprocess from datetime import datetime, timezone, timedelta +import threading # New import: vlc control helper (optional - keep existing integration compatibility) try: @@ -212,6 +214,21 @@ def rename_tuner(old_name, new_name): c.execute("UPDATE tuners SET name=? WHERE name=?", (new_name, old_name)) conn.commit() + +# ------------------- Template context helpers ------------------- +@app.context_processor +def inject_tuner_context(): + """Inject tuner info into all templates (for header fly-outs).""" + try: + tuners = get_tuners() + tuner_names = list(tuners.keys()) + except Exception: + tuner_names = [] + return { + "current_tuner": get_current_tuner(), + "tuner_names": tuner_names + } + # ------------------- Global cache ------------------- cached_channels = [] cached_epg = {} @@ -588,6 +605,48 @@ def about(): return render_template("about.html", info=info) + +@app.route('/set_tuner/') +@login_required +def set_tuner(name): + """Quick-switch the active tuner from the header fly-out. + Admin-only, mirrors the behaviour of the 'switch_tuner' action in /change_tuner. + """ + if current_user.username != 'admin': + log_event(current_user.username, f"Unauthorized quick tuner switch attempt to {name}") + flash("Unauthorized access.", "warning") + return redirect(url_for('guide')) + + tuners = get_tuners() + if name not in tuners: + flash(f"Tuner '{name}' does not exist.", "warning") + return redirect(request.referrer or url_for('change_tuner')) + + # Update current tuner + set_current_tuner(name) + + # Refresh cached guide data + global cached_channels, cached_epg + m3u_url = tuners[name].get("m3u") + xml_url = tuners[name].get("xml") + + cached_channels = parse_m3u(m3u_url) if m3u_url else [] + cached_epg = parse_epg(xml_url) if xml_url else {} + cached_epg = apply_epg_fallback(cached_channels, cached_epg) + + log_event(current_user.username, f"Quick switched active tuner to {name}") + flash(f"Active tuner switched to {name}", "success") + + # Try to redirect back to where the user came from, falling back to guide + dest = request.referrer or url_for('guide') + try: + if not is_safe_url(dest): + dest = url_for('guide') + except Exception: + dest = url_for('guide') + return redirect(dest) + + @app.route('/change_tuner', methods=['GET', 'POST']) @login_required def change_tuner(): @@ -664,20 +723,83 @@ def change_tuner(): log_event(current_user.username, f"Added tuner {name}") flash(f"Tuner {name} added successfully.") + elif action == "update_auto_refresh": + # Expect form fields: auto_refresh_enabled ('0' or '1') and auto_refresh_interval_hours (2/4/6/12/24) + enabled = request.form.get("auto_refresh_enabled", "0") + interval = request.form.get("auto_refresh_interval_hours", "") + if enabled not in ("0", "1"): + enabled = "0" + if interval: + try: + intval = int(interval) + except: + intval = None + else: + intval = None + + # only allow preset intervals + AUTO_REFRESH_PRESETS = [2, 4, 6, 12, 24] + if intval and intval not in AUTO_REFRESH_PRESETS: + flash(f"Invalid interval. Allowed: {AUTO_REFRESH_PRESETS}", "warning") + else: + # persist using existing settings table + try: + with sqlite3.connect(TUNER_DB, timeout=10) as conn: + c = conn.cursor() + c.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ("auto_refresh_enabled", "1" if enabled == "1" else "0")) + if intval: + c.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ("auto_refresh_interval_hours", str(intval))) + else: + c.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ("auto_refresh_interval_hours", "")) + conn.commit() + except Exception: + logging.exception("Failed to persist auto-refresh settings") + flash("Failed to save auto-refresh settings.", "warning") + else: + log_event(current_user.username, f"Updated auto-refresh: enabled={enabled} interval={interval}") + flash("Auto-refresh settings updated.", "success") + tuners = get_tuners() current_tuner = get_current_tuner() + + # read auto-refresh status for template display + def _get_setting_inline(key, default=None): + try: + with sqlite3.connect(TUNER_DB, timeout=10) as conn: + c = conn.cursor() + c.execute("SELECT value FROM settings WHERE key=?", (key,)) + row = c.fetchone() + return row[0] if row else default + except Exception: + return default + + auto_refresh_enabled = _get_setting_inline("auto_refresh_enabled", "0") + auto_refresh_interval_hours = _get_setting_inline("auto_refresh_interval_hours", "") + last_auto_refresh = None + if current_tuner: + last_auto_refresh = _get_setting_inline(f"last_auto_refresh:{current_tuner}", None) + return render_template( "change_tuner.html", tuners=tuners.keys(), current_tuner=current_tuner, current_urls=tuners[current_tuner], - TUNERS=tuners + TUNERS=tuners, + auto_refresh_enabled=auto_refresh_enabled, + auto_refresh_interval_hours=auto_refresh_interval_hours, + last_auto_refresh=last_auto_refresh ) @app.route('/guide') @login_required def guide(): log_event(current_user.username, "Loaded guide page") + # Check and run auto-refresh if due (minimal preset-based approach) + try: + refresh_if_due() + except Exception: + logging.exception("refresh_if_due from guide() failed") + now = datetime.now(timezone.utc) grid_start = now.replace(minute=(0 if now.minute < 30 else 30), second=0, microsecond=0) slots = int((HOURS_SPAN * 60) / SLOT_MINUTES) @@ -832,6 +954,29 @@ def api_start_stream(): log_event(current_user.username, f"Requested start_stream {url} (id={instance}, hide_cursor={hide_cursor})") return jsonify({"ok": True, "message": "started", "id": instance}) +@app.route('/api/auto_refresh/status', methods=['GET']) +@login_required +def api_auto_refresh_status(): + """ + Return auto-refresh status for the current tuner: + { tuner, enabled (bool), interval_hours (int|null), last_run (string|null) } + """ + try: + tuner = get_current_tuner() + enabled = get_setting("auto_refresh_enabled", "0") + interval = get_setting("auto_refresh_interval_hours", "") + last = get_setting(f"last_auto_refresh:{tuner}", None) if tuner else None + + return jsonify({ + "tuner": tuner, + "enabled": bool(str(enabled) in ("1", "true", "True")), + "interval_hours": int(interval) if interval not in (None, "") else None, + "last_run": last + }) + except Exception as e: + logging.exception("api_auto_refresh_status failed: %s", e) + return jsonify({"error": str(e)}), 500 + @app.route('/api/stop_stream', methods=['POST']) @login_required def api_stop_stream(): @@ -1287,6 +1432,12 @@ def api_guide_snapshot(): Supports ?hours=N (0.5–8) to control window size. """ try: + # Run auto-refresh if due so CRT clients get fresh data when they poll + try: + refresh_if_due() + except Exception: + logging.exception("refresh_if_due from api_guide_snapshot failed") + now = datetime.now(timezone.utc) # Read optional hours parameter @@ -1458,6 +1609,228 @@ def api_qr_show(): return jsonify({"status": "visible"}) +def check_url_reachable(url, timeout=5): + try: + r = requests.head(url, timeout=timeout) + return r.status_code < 400 + except: + return False + +def check_xmltv_freshness(xml_url, max_age_hours=6): + try: + r = requests.get(xml_url, timeout=10) + r.raise_for_status() + + root = ET.fromstring(r.content) + + now = datetime.now(timezone.utc) + past_starts = [] + + for prog in root.findall(".//programme"): + start = prog.get("start") + if not start: + continue + + # example format: "20251115051031 +0000" + parts = start.split() + ts = parts[0] # 20251115051031 + if len(ts) >= 14: + try: + dt = datetime.strptime(ts[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc) + if dt <= now: + past_starts.append(dt) + except: + pass + + if not past_starts: + # If no past events, assume fresh + return (True, 0.0) + + latest_past = max(past_starts) + age_hours = (now - latest_past).total_seconds() / 3600.0 + + return (age_hours <= max_age_hours, age_hours) + + except Exception as e: + print("XMLTV freshness error:", e) + return (False, None) + +# ------------------- Minimal Auto-refresh (preset-based, no scheduler) ------------------- +AUTO_REFRESH_PRESETS = [2, 4, 6, 12, 24] # allowed hours +_auto_refresh_locks = {} # in-memory locks (OK for single-process) + +def get_setting(key, default=None): + """Read key from tuners.settings table (existing settings table).""" + try: + with sqlite3.connect(TUNER_DB, timeout=10) as conn: + c = conn.cursor() + c.execute("SELECT value FROM settings WHERE key=?", (key,)) + row = c.fetchone() + return row[0] if row else default + except Exception: + return default + +def set_setting(key, value): + try: + with sqlite3.connect(TUNER_DB, timeout=10) as conn: + c = conn.cursor() + c.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", (key, str(value))) + conn.commit() + except Exception: + logging.exception("set_setting failed for %s", key) + +def _acquire_lock(name): + lock = _auto_refresh_locks.setdefault(name, threading.Lock()) + return lock.acquire(blocking=False) + +def _release_lock(name): + lock = _auto_refresh_locks.get(name) + try: + if lock and lock.locked(): + lock.release() + except RuntimeError: + # already released or not owned + pass + +def refresh_current_tuner(tuner_name=None): + """Perform the same refresh logic you already run on login/tuner switch. + Returns True on success, False on failure/skip. + """ + try: + if not tuner_name: + tuner_name = get_current_tuner() + if not tuner_name: + logging.info("refresh_current_tuner: no current tuner set") + return False + + if not _acquire_lock(tuner_name): + logging.info("refresh_current_tuner: lock busy for %s", tuner_name) + return False + + logging.info("refresh_current_tuner: refreshing tuner %s", tuner_name) + tuners = get_tuners() + info = tuners.get(tuner_name) + if not info: + logging.warning("refresh_current_tuner: tuner %s not found", tuner_name) + return False + + m3u_url = info.get('m3u') + xml_url = info.get('xml') + + new_channels = parse_m3u(m3u_url) if m3u_url else [] + new_epg = parse_epg(xml_url) if xml_url else {} + new_epg = apply_epg_fallback(new_channels, new_epg) + + # atomic swap + global cached_channels, cached_epg + cached_channels = new_channels + cached_epg = new_epg + + now_iso = datetime.now(timezone.utc).isoformat() + set_setting(f"last_auto_refresh:{tuner_name}", f"success|{now_iso}") + + logging.info("refresh_current_tuner: finished %s", tuner_name) + return True + + except Exception as e: + logging.exception("refresh_current_tuner error for %s: %s", tuner_name, e) + now_iso = datetime.now(timezone.utc).isoformat() + set_setting(f"last_auto_refresh:{tuner_name}", f"failed|{now_iso}|{str(e)[:200]}") + return False + finally: + try: + _release_lock(tuner_name) + except Exception: + pass + +def refresh_if_due(tuner_name=None): + """Check settings and last-run timestamp; refresh if interval elapsed.""" + try: + # global enabling (simple): stored as 'auto_refresh_enabled' = "1" or "0" + enabled = get_setting("auto_refresh_enabled", "0") + if str(enabled) not in ("1", "true", "True"): + return False + + interval_value = get_setting("auto_refresh_interval_hours", None) + try: + interval_hours = int(interval_value) if interval_value is not None and interval_value != "" else None + except: + interval_hours = None + + # Only allow preset intervals for simplicity/safety + if interval_hours not in AUTO_REFRESH_PRESETS: + logging.debug("refresh_if_due: interval %s not in presets %s", interval_hours, AUTO_REFRESH_PRESETS) + return False + + if not tuner_name: + tuner_name = get_current_tuner() + if not tuner_name: + return False + + last_raw = get_setting(f"last_auto_refresh:{tuner_name}", None) + if last_raw: + # stored as "success|{ISO}" or "failed|{ISO}|msg" + try: + last_iso = last_raw.split("|")[1] + last_dt = datetime.fromisoformat(last_iso) + except Exception: + last_dt = None + else: + last_dt = None + + now = datetime.now(timezone.utc) + if last_dt is None: + due = True + else: + elapsed_hours = (now - last_dt).total_seconds() / 3600.0 + due = (elapsed_hours >= interval_hours) + + if due: + logging.info("refresh_if_due: due for tuner %s (interval=%s)", tuner_name, interval_hours) + return refresh_current_tuner(tuner_name) + return False + + except Exception: + logging.exception("refresh_if_due unexpected error") + return False + +# ------------------- QR Visibility Control (with auto-restore) ------------------- + +# (rest of the file unchanged) +# ------------------- Health endpoint and the remainder of the script ------------------- + +@app.route('/api/health') +@login_required +def api_health(): + tuners = get_tuners() + curr = get_current_tuner() + t = tuners.get(curr, {}) + + m3u_url = t.get("m3u", "") + xml_url = t.get("xml", "") + + # Reachability checks + m3u_ok = check_url_reachable(m3u_url) if m3u_url else False + xml_ok = check_url_reachable(xml_url) if xml_url else False + + # Freshness check (keep your current function) + # If you don't compute age yet, return None so "Unknown" shows + xml_fresh, xml_age_hours = check_xmltv_freshness(xml_url) + + + return jsonify({ + "tuner": curr, + "m3u_reachable": m3u_ok, + "xml_reachable": xml_ok, + "xmltv_fresh": xml_fresh, + + # ADD THESE: + "tuner_m3u": m3u_url, + "tuner_xml": xml_url, + "xmltv_age_hours": xml_age_hours + }) + + if __name__ == '__main__': init_db() add_user('admin', 'strongpassword123') @@ -1474,4 +1847,5 @@ def api_qr_show(): cached_channels = parse_m3u(tuners[current_tuner]["m3u"]) cached_epg = parse_epg(tuners[current_tuner]["xml"]) + # No background scheduler — auto-refresh is triggered lazily on page/API hits. app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/bump_version.py b/bump_version.py index 83b3f40..fa92daf 100644 --- a/bump_version.py +++ b/bump_version.py @@ -3,150 +3,316 @@ bump_version.py - automate version bumps for RetroIPTVGuide Usage: - python bump_version.py 2.4.0 - python bump_version.py 2.4.0 --commit + python bump_version.py 4.3.0 + python bump_version.py v4.3.0 --commit """ import sys import re from datetime import datetime -import subprocess from pathlib import Path +import subprocess -# --- FILE TARGETS --- +# Target files APP_FILE = Path("app.py") CHANGELOG_FILE = Path("CHANGELOG.md") -INSTALL_WIN = Path("install_windows.ps1") -UNINSTALL_WIN = Path("uninstall_windows.ps1") -UNINSTALL_SH = Path("uninstall.sh") -INSTALL_SH = Path("install.sh") -RPI_INSTALL_SH = Path("retroiptv_rpi.sh") - -# ------------------------------------------------------- -def update_app_py(new_version: str): - """Update APP_VERSION in app.py, add if missing""" - content = APP_FILE.read_text().splitlines() - updated = [] - found = False - for line in content: - if line.strip().startswith("APP_VERSION"): - updated.append(f'APP_VERSION = "v{new_version}"') - found = True - else: - updated.append(line) - if not found: - updated.insert(0, f'APP_VERSION = "v{new_version}"') - print("ℹ️ APP_VERSION not found in app.py, added at top") - APP_FILE.write_text("\n".join(updated) + "\n") - print(f"✅ Updated app.py to v{new_version}") - -def update_changelog(new_version: str): - """Insert new version section after the Unreleased block""" - changelog = CHANGELOG_FILE.read_text().splitlines() - today = datetime.today().strftime("%Y-%m-%d") +INSTALL_MD = Path("INSTALL.md") +README_MD = Path("README.md") +ROADMAP_MD = Path("ROADMAP.md") +LINUX_SH = Path("retroiptv_linux.sh") +RPI_SH = Path("retroiptv_rpi.sh") +WIN_BAT = Path("retroiptv_windows.bat") +WIN_PS1 = Path("retroiptv_windows.ps1") + + +def normalize_version(raw: str) -> tuple[str, str]: + """ + Returns (base_version, v_version) + raw may be "4.3.0" or "v4.3.0" + """ + base = raw.lstrip("vV").strip() + if not re.fullmatch(r"\d+\.\d+\.\d+", base): + raise SystemExit(f"Invalid version '{raw}'. Expected format: 4.3.0 or v4.3.0") + return base, f"v{base}" + + +def update_app_py(base_version: str, v_version: str, date_str: str) -> None: + if not APP_FILE.exists(): + print(f"[-] {APP_FILE} not found, skipping") + return + + text = APP_FILE.read_text(encoding="utf-8") + # APP_VERSION line + if "APP_VERSION" in text: + text, n = re.subn( + r'^APP_VERSION\s*=.*$', + f'APP_VERSION = "{v_version}"', + text, + flags=re.MULTILINE, + ) + else: + text = f'APP_VERSION = "{v_version}"\n' + text + n = 1 + print(f"[+] app.py: set APP_VERSION = \"{v_version}\" (updated {n} line{'s' if n != 1 else ''})") + + # APP_RELEASE_DATE line + if "APP_RELEASE_DATE" in text: + text, n2 = re.subn( + r'^APP_RELEASE_DATE\s*=.*$', + f'APP_RELEASE_DATE = "{date_str}"', + text, + flags=re.MULTILINE, + ) + else: + # insert just after APP_VERSION + text = re.sub( + r'^(APP_VERSION\s*=.*\n)', + rf'\1APP_RELEASE_DATE = "{date_str}"\n', + text, + count=1, + flags=re.MULTILINE, + ) + n2 = 1 + print(f"[+] app.py: set APP_RELEASE_DATE = \"{date_str}\" (updated {n2} line{'s' if n2 != 1 else ''})") + + APP_FILE.write_text(text, encoding="utf-8") + + +def update_changelog(base_version: str, v_version: str, date_str: str) -> None: + if not CHANGELOG_FILE.exists(): + print(f"[-] {CHANGELOG_FILE} not found, skipping") + return + + lines = CHANGELOG_FILE.read_text(encoding="utf-8").splitlines() + try: + idx = next(i for i, line in enumerate(lines) if line.strip() == "---") + except StopIteration: + raise SystemExit("Could not find top '---' separator in CHANGELOG.md") new_block = [ - f"## v{new_version} - {today}", + "", + f"## {v_version} - {date_str}", + "", "### Added", "- (empty)", "", + "### Changed", + "- (empty)", + "", "### Fixed", "- (empty)", "", "---", - "", ] - updated = [] - inserted = False - for i, line in enumerate(changelog): - updated.append(line) - if line.strip() == "---" and not inserted: - if any("## [Unreleased]" in l for l in changelog[:i]): - updated.append("") # spacing - updated.extend(new_block) - inserted = True - - if not inserted: - print("⚠️ Could not find end of [Unreleased] section in CHANGELOG.md") - sys.exit(1) - - CHANGELOG_FILE.write_text("\n".join(updated) + "\n") - print(f"✅ Inserted v{new_version} section into CHANGELOG.md") - -def update_script_version(file: Path, new_version: str, is_bash: bool): - """Update or insert version string in shell or PowerShell scripts""" - if not file.exists(): - print(f"⚠️ {file} not found, skipping") + # Insert immediately after the first '---' + updated = lines[: idx + 1] + new_block + lines[idx + 1 :] + CHANGELOG_FILE.write_text("\n".join(updated) + "\n", encoding="utf-8") + print(f"[+] CHANGELOG.md: inserted section for {v_version} - {date_str}") + + +def update_install_md(v_version: str) -> None: + if not INSTALL_MD.exists(): + print(f"[-] {INSTALL_MD} not found, skipping") + return + + text = INSTALL_MD.read_text(encoding="utf-8") + new_text, n = re.subn( + r'^\*\*Version:\*\* v\d+\.\d+\.\d+', + f"**Version:** {v_version}", + text, + flags=re.MULTILINE, + ) + if n == 0: + print("[!] INSTALL.md: no '**Version:** vX.Y.Z' line found (no change)") + else: + INSTALL_MD.write_text(new_text, encoding="utf-8") + print(f"[+] INSTALL.md: updated version line to {v_version}") + + +def update_readme_md(v_version: str) -> None: + if not README_MD.exists(): + print(f"[-] {README_MD} not found, skipping") + return + + text = README_MD.read_text(encoding="utf-8") + + # Header line + text, n1 = re.subn( + r'^(# 📺 RetroIPTVGuide v)\d+\.\d+\.\d+', + rf"\g<1>{v_version}", + text, + flags=re.MULTILINE, + ) + + # Version badge + text, n2 = re.subn( + r'(version-v)\d+\.\d+\.\d+(-blue)', + rf"\1{v_version}\2", + text, + ) + + README_MD.write_text(text, encoding="utf-8") + print(f"[+] README.md: updated header ({n1} line) and badge ({n2} match{'es' if n2 != 1 else ''})") + + +def update_roadmap_md(v_version: str, date_str: str) -> None: + if not ROADMAP_MD.exists(): + print(f"[-] {ROADMAP_MD} not found, skipping") + return + + text = ROADMAP_MD.read_text(encoding="utf-8") + + # Current Version line + text, n1 = re.subn( + r'^# Current Version: \*\*v\d+\.\d+\.\d+ \(\d{4}-\d{2}-\d{2}\)\*\*', + f"# Current Version: **{v_version} ({date_str})**", + text, + flags=re.MULTILINE, + ) + + # "Release tagged as" line + text, n2 = re.subn( + r'Release tagged as \*\*v\d+\.\d+\.\d+\*\*', + f"Release tagged as **{v_version}**", + text, + count=1, + ) + + ROADMAP_MD.write_text(text, encoding="utf-8") + print(f"[+] ROADMAP.md: updated current version line ({n1}) and release tag ({n2})") + + +def update_shell_script(path: Path, base_version: str) -> None: + if not path.exists(): + print(f"[-] {path} not found, skipping") + return + + text = path.read_text(encoding="utf-8") + new_text, n = re.subn( + r'^VERSION="\d+\.\d+\.\d+"', + f'VERSION="{base_version}"', + text, + flags=re.MULTILINE, + ) + if n == 0: + print(f"[!] {path}: no VERSION=\"X.Y.Z\" line found (no change)") + else: + path.write_text(new_text, encoding="utf-8") + print(f"[+] {path}: set VERSION=\"{base_version}\"") + + +def update_win_ps1(base_version: str) -> None: + if not WIN_PS1.exists(): + print(f"[-] {WIN_PS1} not found, skipping") + return + + # Windows script may not be UTF-8; use latin-1 to be safe + text = WIN_PS1.read_text(encoding="latin-1") + + # Header Version: X.Y.Z + text, n1 = re.subn( + r'^(Version:\s*)\d+\.\d+\.\d+', + rf"\g<1>{base_version}", + text, + flags=re.MULTILINE, + ) + + # $VERSION = "X.Y.Z" + text, n2 = re.subn( + r'^\$VERSION\s*=\s*"\d+\.\d+\.\d+"', + f'$VERSION = "{base_version}"', + text, + flags=re.MULTILINE, + ) + + WIN_PS1.write_text(text, encoding="latin-1") + print(f"[+] {WIN_PS1}: updated header Version ({n1}) and $VERSION ({n2})") + + +def update_win_bat(base_version: str, v_version: str) -> None: + if not WIN_BAT.exists(): + print(f"[-] {WIN_BAT} not found, skipping") + return + + # Same encoding concern as PS1 + text = WIN_BAT.read_text(encoding="latin-1") + + # REM Version: vX.Y.Z + text, n1 = re.subn( + r'^(REM Version:\s*)v\d+\.\d+\.\d+', + rf"\g<1>{v_version}", + text, + flags=re.MULTILINE, + ) + + # set "VERSION=X.Y.Z" + text, n2 = re.subn( + r'^(set\s+"VERSION=)\d+\.\d+\.\d+"', + rf'\g<1>{base_version}"', + text, + flags=re.MULTILINE, + ) + + WIN_BAT.write_text(text, encoding="latin-1") + print(f"[+] {WIN_BAT}: updated REM Version ({n1}) and set VERSION ({n2})") + + +def git_commit(v_version: str) -> None: + files = [ + APP_FILE, + CHANGELOG_FILE, + INSTALL_MD, + README_MD, + ROADMAP_MD, + LINUX_SH, + RPI_SH, + WIN_BAT, + WIN_PS1, + ] + existing = [str(f) for f in files if f.exists()] + if not existing: + print("[!] No files to add to git") return - content = file.read_text().splitlines() - updated = [] - found = False - - if is_bash: - pattern = re.compile(r'^\s*VERSION\s*=\s*".*"') - replacement = f'VERSION="{new_version}"' - else: # PowerShell - pattern = re.compile(r'^\s*\$?VERSION\s*=\s*".*"') - replacement = f'$VERSION = "{new_version}"' - - for line in content: - if pattern.match(line): - updated.append(replacement) - found = True - else: - updated.append(line) - - if not found: - updated.insert(0, replacement) - print(f"ℹ️ VERSION not found in {file}, added at top") - - file.write_text("\n".join(updated) + "\n") - print(f"✅ Updated {file} to v{new_version}") - -def git_commit(new_version: str): - """Commit changes with git""" try: - subprocess.run( - ["git", "add", - str(APP_FILE), - str(CHANGELOG_FILE), - str(INSTALL_WIN), - str(UNINSTALL_WIN), - str(UNINSTALL_SH), - str(INSTALL_SH), - str(RPI_INSTALL_SH)], - check=True - ) - subprocess.run( - ["git", "commit", "-m", f"Bump version to v{new_version}"], - check=True - ) - print("✅ Changes committed to git") + subprocess.run(["git", "add", *existing], check=True) + subprocess.run(["git", "commit", "-m", f"Bump version to {v_version}"], check=True) + print("[+] Git commit created") except subprocess.CalledProcessError: - print("⚠️ Git commit failed (maybe no repo?)") + print("[!] Git commit failed (is this a git repo?)") -def main(): - if len(sys.argv) < 2: + +def main(argv: list[str]) -> None: + if len(argv) < 2: print("Usage: python bump_version.py [--commit]") - sys.exit(1) + raise SystemExit(1) + + raw_version = argv[1] + do_commit = "--commit" in argv[2:] - new_version = sys.argv[1].lstrip("v") - do_commit = "--commit" in sys.argv + base_version, v_version = normalize_version(raw_version) + date_str = datetime.today().strftime("%Y-%m-%d") - update_app_py(new_version) - update_changelog(new_version) + print("== RetroIPTVGuide version bump ==") + print(f" New version: {base_version} ({v_version})") + print(f" Release date: {date_str}") + print("") - # Update all installers/uninstallers - update_script_version(INSTALL_WIN, new_version, is_bash=False) - update_script_version(UNINSTALL_WIN, new_version, is_bash=False) - update_script_version(UNINSTALL_SH, new_version, is_bash=True) - update_script_version(INSTALL_SH, new_version, is_bash=True) - update_script_version(RPI_INSTALL_SH, new_version, is_bash=True) + update_app_py(base_version, v_version, date_str) + update_changelog(base_version, v_version, date_str) + update_install_md(v_version) + update_readme_md(v_version) + update_roadmap_md(v_version, date_str) + update_shell_script(LINUX_SH, base_version) + update_shell_script(RPI_SH, base_version) + update_win_ps1(base_version) + update_win_bat(base_version, v_version) if do_commit: - git_commit(new_version) + git_commit(v_version) + if __name__ == "__main__": - main() + main(sys.argv) + diff --git a/bump_version.sh b/bump_version.sh deleted file mode 100644 index 1a3dc93..0000000 --- a/bump_version.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -# ──────────────────────────────────────────────────────────────── -# bump_version.sh — Version bump utility for RetroIPTVGuide -# Author: RetroIPTVGuide Dev Team -# License: CC BY-NC-SA 4.0 -# Usage: -# ./bump_version.sh 4.0.0 -# ./bump_version.sh 4.0.0 --commit -# ──────────────────────────────────────────────────────────────── - -set -euo pipefail -# macOS locale workaround for sed "illegal byte sequence" -export LC_CTYPE=C -export LANG=C - -# --- Color setup --- -RED=$(tput setaf 1) -GREEN=$(tput setaf 2) -YELLOW=$(tput setaf 3) -RESET=$(tput sgr0) - -# --- Argument parsing --- -NEW_VERSION="${1:-}" -DO_COMMIT="${2:-}" -TODAY=$(date +%Y-%m-%d) - -if [[ -z "$NEW_VERSION" ]]; then - echo "${YELLOW}Usage:${RESET} $0 [--commit]" - exit 1 -fi - -# Strip leading v -NEW_VERSION="${NEW_VERSION#v}" - -# --- File targets --- -FILES=( - "retroiptv_linux.sh" - "retroiptv_rpi.sh" - "retroiptv_windows.ps1" - "app.py" -) - -CHANGELOG="CHANGELOG.md" - -# --- Detect sed flavor --- -if sed --version >/dev/null 2>&1; then - # GNU sed - SED_INPLACE=(-i) -else - # BSD/macOS sed - SED_INPLACE=(-i "") -fi - -echo "🔢 Bumping version to v${NEW_VERSION}" -echo "───────────────────────────────────────────────" - -# --- Update app.py --- -if [[ -f "app.py" ]]; then - if grep -q '^APP_VERSION' app.py; then - sed "${SED_INPLACE[@]}" "s/^APP_VERSION.*/APP_VERSION = \"v${NEW_VERSION}\"/" app.py - else - sed "${SED_INPLACE[@]}" "1iAPP_VERSION = \"v${NEW_VERSION}\"" app.py - fi - echo "${GREEN}✔ Updated app.py${RESET}" -fi - -# --- Update version in scripts --- -for FILE in "${FILES[@]}"; do - [[ ! -f "$FILE" ]] && continue - - case "$FILE" in - *.sh) - if grep -q '^VERSION=' "$FILE"; then - sed "${SED_INPLACE[@]}" "s/^VERSION=.*/VERSION=\"${NEW_VERSION}\"/" "$FILE" - else - sed "${SED_INPLACE[@]}" "1iVERSION=\"${NEW_VERSION}\"" "$FILE" - fi - ;; - *.ps1|*.bat) - if grep -q 'VERSION' "$FILE"; then - sed "${SED_INPLACE[@]}" "s/^[\$]*VERSION.*/\$VERSION = \"${NEW_VERSION}\"/" "$FILE" - else - sed "${SED_INPLACE[@]}" "1i\$VERSION = \"${NEW_VERSION}\"" "$FILE" - fi - ;; - esac - echo "${GREEN}✔ Updated ${FILE}${RESET}" -done - -# --- Update CHANGELOG.md --- -if [[ -f "$CHANGELOG" ]]; then - TMP_FILE=$(mktemp) - awk -v ver="v${NEW_VERSION}" -v date="$TODAY" ' - /^---$/ && !inserted { - print "" - print "## " ver " - " date - print "### Added" - print "- (empty)\n" - print "### Fixed" - print "- (empty)\n" - print "---\n" - inserted=1 - } - {print} - ' "$CHANGELOG" > "$TMP_FILE" - mv "$TMP_FILE" "$CHANGELOG" - echo "${GREEN}✔ Updated CHANGELOG.md${RESET}" -fi - -# --- Optional Git commit --- -if [[ "$DO_COMMIT" == "--commit" ]]; then - git add app.py "$CHANGELOG" "${FILES[@]}" || true - if git diff --cached --quiet; then - echo "${YELLOW}⚠ No changes to commit.${RESET}" - else - git commit -m "Bump version to v${NEW_VERSION}" || true - echo "${GREEN}✔ Git commit created${RESET}" - fi -fi - -echo "───────────────────────────────────────────────" -echo "${GREEN}🎉 Version bump complete → v${NEW_VERSION}${RESET}" - diff --git a/retroiptv_linux.sh b/retroiptv_linux.sh index 225f961..a1720aa 100644 --- a/retroiptv_linux.sh +++ b/retroiptv_linux.sh @@ -3,7 +3,7 @@ # License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) set -euo pipefail -VERSION="4.2.0" +VERSION="4.3.0" TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") LOGFILE="retroiptv_${TIMESTAMP}.log" exec > >(tee -a "$LOGFILE") 2>&1 diff --git a/retroiptv_rpi.sh b/retroiptv_rpi.sh index 3a3084c..d06dbce 100644 --- a/retroiptv_rpi.sh +++ b/retroiptv_rpi.sh @@ -1,5 +1,5 @@ #!/bin/bash -VERSION="4.2.0" +VERSION="4.3.0" # RetroIPTVGuide Raspberry Pi Installer (Headless, Pi3/4/5) # Installs to /home/iptv/iptv-server for consistency with Debian/Windows # Logs to /var/log/retroiptvguide/install-YYYYMMDD-HHMMSS.log diff --git a/retroiptv_windows.bat b/retroiptv_windows.bat index 06cad5b..edf890b 100644 --- a/retroiptv_windows.bat +++ b/retroiptv_windows.bat @@ -3,7 +3,7 @@ mode con: cols=160 lines=50 REM ============================================================ REM RetroIPTVGuide Windows Unified Installer / Uninstaller -REM Version: v4.0.0 +REM Version: v4.3.0 REM License: Creative Commons BY-NC-SA 4.0 REM ============================================================ @@ -31,7 +31,7 @@ if /i "%choice%"=="Y" ( :continue setlocal -set "VERSION=4.0.0" +set "VERSION=4.3.0" set "REPO_URL=https://github.com/thehack904/RetroIPTVGuide.git" set "ZIP_URL=https://github.com/thehack904/RetroIPTVGuide/archive/refs/heads/main.zip" set "INSTALL_DIR=%~dp0RetroIPTVGuide" diff --git a/retroiptv_windows.ps1 b/retroiptv_windows.ps1 index 7e49187..d6fc090 100644 --- a/retroiptv_windows.ps1 +++ b/retroiptv_windows.ps1 @@ -1,7 +1,7 @@ <# RetroIPTVGuide Windows Installer/Uninstaller Filename: retroiptv_windows.ps1 -Version: 4.1.0 +Version: 4.3.0 License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International https://creativecommons.org/licenses/by-nc-sa/4.0/ @@ -55,7 +55,7 @@ if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdent $ErrorActionPreference = 'Stop' $PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' -$VERSION = "4.2.0" +$VERSION = "4.3.0" $ScriptDir = Split-Path -Parent -Path $MyInvocation.MyCommand.Path Set-Location $ScriptDir diff --git a/static/css/about.css b/static/css/about.css index 9a99a65..57dc62f 100644 --- a/static/css/about.css +++ b/static/css/about.css @@ -65,3 +65,104 @@ body.retro-magazine .about-box { background: #fff; border: 1px solid #000; } .about-box li { padding: 8px 6px; display:block; } .about-box li strong { display:block; margin-bottom:6px; } } + +/* --- Collapsible System Health Drawer --- */ + +.health-header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + user-select: none; + margin-bottom: 10px; +} + +.health-header .arrow { + font-size: 1.1rem; + transition: transform 0.25s ease; +} + +/* Hidden drawer area */ +.diag-box { + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 0.35s ease, opacity 0.25s ease; + margin-top: 10px; + padding-top: 0; /* padding removed when collapsed */ +} + +/* Expanded drawer */ +.diag-box.open { + max-height: 600px; /* large enough to hold content */ + opacity: 1; + padding-top: 10px; /* restore padding */ + border-top: 1px solid rgba(255,255,255,0.04); +} + +/* Light theme version */ +body.light .diag-box.open { + border-top: 1px solid rgba(0,0,0,0.08); +} + +:root { + --arrow-collapsed: "▶"; + --arrow-expanded: "▼"; +} + +/* Use the themed arrow automatically */ +.arrow::after { + content: var(--arrow-collapsed); +} + +/* When open */ +.health-header.open .arrow::after { + content: var(--arrow-expanded); +} + +/* Collapsible drawer repeated styles de-duplicated (kept for compatibility) */ +.health-header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + user-select: none; + margin-bottom: 10px; +} +.diag-box { + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 0.35s ease, opacity 0.25s ease; + margin-top: 10px; + padding-top: 0; +} +.diag-box.open { + max-height: 600px; + opacity: 1; + padding-top: 10px; + border-top: 1px solid rgba(255,255,255,0.04); +} +body.light .diag-box.open { + border-top: 1px solid rgba(0,0,0,0.08); +} +:root{--arrow-collapsed:"▶";--arrow-expanded:"▼";} +.arrow::after{content:var(--arrow-collapsed);transition:transform .2s ease;} +.health-header.open .arrow::after{content:var(--arrow-expanded);} + +body.retroiptv{--arrow-collapsed:"►";--arrow-expanded:"▼";} +body.directv{--arrow-collapsed:"▸";--arrow-expanded:"▾";} +body.comcast{--arrow-collapsed:"›";--arrow-expanded:"⌄";} +body.retro-aol{--arrow-collapsed:"+";--arrow-expanded:"-";} +body.retro-magazine{--arrow-collapsed:"▶";--arrow-expanded:"▼";} +body.dark{--arrow-collapsed:"▶";--arrow-expanded:"▼";} +body.light{--arrow-collapsed:"▶";--arrow-expanded:"▼";} + +/* Styles for the auto-refresh list to visually match the diagnostics rows */ +.auto-refresh-list { margin: 8px 0 0 0; padding: 0; list-style: none; } +.auto-refresh-list li { padding: 6px 0; display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,0.03); } +.auto-refresh-list li:last-child { border-bottom: none; } +.diag-auto-refresh p { margin: 8px 0 6px 0; font-weight: 600; } + +/* small helper to hide an element if needed */ +.hidden { display: none !important; } diff --git a/static/css/base.css b/static/css/base.css index 9cfc668..2a28899 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -258,6 +258,8 @@ body.light .program { background:#ddd; border-color:#aaa; color:#000; } body.light .program.now { background:#cfe8cf; border-color:#090; } body.light .now-line { background:#090; } body.light .header #clock { color: #ffffff !important; } +/* compact: make hamburger rects white in Light theme */ +body.light .hamburger svg rect,body.light #mobileHamburger svg rect{fill:#fff!important;stroke:#fff!important} /* Retro-AOL */ body.retro-aol { --timebar-bg:#004466; --timebar-border:#33cccc; --timebar-color:#33cccc; --chan-col-bg:#004466; background:#2f4f4f; color:#f0f0f0; font-family:"Tahoma","Arial",sans-serif; } @@ -497,4 +499,29 @@ body.retroiptv a{color:var(--timebar-border)}body.retroiptv a:hover{color:var(-- body { transition: background-color .3s ease,color .3s ease; } body.fade-switch { opacity:0; transition:opacity .25s ease; } + +:root{--arrow-collapsed:"▶";--arrow-expanded:"▼";} +.arrow::after{content:var(--arrow-collapsed);transition:transform .2s ease;} +.health-header.open .arrow::after{content:var(--arrow-expanded);} + +body.retroiptv{--arrow-collapsed:"►";--arrow-expanded:"▼";} +body.directv{--arrow-collapsed:"▸";--arrow-expanded:"▾";} +body.comcast{--arrow-collapsed:"›";--arrow-expanded:"⌄";} +body.retro-aol{--arrow-collapsed:"+";--arrow-expanded:"-";} +body.retro-magazine{--arrow-collapsed:"▶";--arrow-expanded:"▼";} +body.dark{--arrow-collapsed:"▶";--arrow-expanded:"▼";} +body.light{--arrow-collapsed:"▶";--arrow-expanded:"▼";} + + /* End of base.css */ + + +/* Theme-aware separator for tuner fly-outs */ +.separator { + border-top: 1px solid currentColor; + opacity: 0.25; + margin: 6px 0; + padding: 0; + list-style: none; + height: 0; +} diff --git a/static/css/change_password.css b/static/css/change_password.css index f489616..06942ef 100644 --- a/static/css/change_password.css +++ b/static/css/change_password.css @@ -1,61 +1,125 @@ -/* Per-page styles for change_password (keeps the original compact centered form look) */ +/* Full replacement: Change Password page stylesheet + * + * Goal: exactly match the Change Tuner high-contrast surface treatment, + * cover both .box and .card markup, and robustly override problematic theme rules. + * + * IMPORTANT: + * - Replace the existing static/css/change_password.css with this file. + * - Hard-refresh the browser (Ctrl+F5) to ensure no cached CSS remains. + */ -/* Container centers the single panel */ +/* ====================================================================== */ +/* Page-scoped CSS variables (mirror change_tuner) */ +/* ====================================================================== */ +.container-change-password { + --panel-bg: rgba(10,12,14,0.72); + --panel-text: #e8f6f6; + --panel-border: rgba(255,255,255,0.06); + --card-head-bg: rgba(255,255,255,0.03); + --heading-color: var(--panel-text); + --heading-weight: 800; + + --label-color: rgba(240,240,240,0.92); + --muted-color: rgba(220,220,220,0.65); + + --input-bg: rgba(255,255,255,0.04); + --input-border: rgba(160,200,200,0.18); + --input-color: #e9f7f7; + --placeholder-color: rgba(230,230,230,0.42); + + --primary-color: #1ed3ce; + --primary-contrast: #ffffff; + + --focus-border: rgba(30,211,206,0.95); + --focus-ring: rgba(30,211,206,0.12); + + --box-shadow: 0 8px 22px rgba(0,0,0,0.40); + --card-radius: 8px; + + color: var(--panel-text); + background: transparent; +} + +/* ====================================================================== */ +/* Light-theme page-scoped overrides (only this page) */ +/* ====================================================================== */ +body.light .container-change-password { + --panel-bg: #f3f3f3; + --panel-text: #111; + --panel-border: rgba(0,0,0,0.06); + --card-head-bg: rgba(0,0,0,0.02); + --heading-color: #111; + --heading-weight: 700; + + --label-color: #333; + --muted-color: rgba(0,0,0,0.56); + + --input-bg: #fff; + --input-border: #aaa; + --input-color: #111; + --placeholder-color: rgba(0,0,0,0.36); + + --primary-color: #06c; + --primary-contrast: #fff; + --box-shadow: 0 3px 12px rgba(0,0,0,0.04); +} + +/* ====================================================================== */ +/* Layout */ +/* ====================================================================== */ .container-change-password { max-width: 360px; margin: 60px auto 80px; - padding: 0 12px; + padding: 12px; box-sizing: border-box; } -/* Panel (card) */ -.container-change-password .box { +/* Map both .box and .card so markup variants are covered */ +.container-change-password .box, +.container-change-password .card { margin: 0; - padding: 18px 18px; - border-radius: 8px; + padding: 18px; + border-radius: var(--card-radius); width: 100%; box-sizing: border-box; - background: rgba(0,0,0,0.12); - border: 1px solid rgba(255,255,255,0.03); - box-shadow: 0 2px 0 rgba(255,255,255,0.02) inset, 0 6px 12px rgba(0,0,0,0.12); + background: var(--panel-bg); + color: var(--panel-text); + border: 1px solid var(--panel-border); + box-shadow: var(--box-shadow); } /* Heading */ -.container-change-password .box h2 { +.container-change-password .box h2, +.container-change-password .card h2 { margin: 0 0 12px; font-size: 1.25rem; font-weight: 700; - color: inherit; + color: var(--heading-color); } /* Inputs */ -.container-change-password input[type="password"] { +.container-change-password input[type="password"], +.container-change-password input[type="text"] { width: 100%; padding: 8px 10px; margin: 6px 0; - border-radius: 4px; - border: 1px solid rgba(255,255,255,0.08); - background: rgba(255,255,255,0.03); - color: inherit; + border-radius: 6px; + border: 1px solid var(--input-border); + background: var(--input-bg); + color: var(--input-color); box-sizing: border-box; outline: none; + transition: box-shadow .12s ease, border-color .12s ease; + font-size: 0.96rem; } -/* Light-theme overrides */ -body.light .container-change-password .box { - background: #f3f3f3; - border-color: rgba(0,0,0,0.06); -} -body.light .container-change-password input { - background: #fff; - color: #000; - border-color: #aaa; -} +/* Placeholder color */ +.container-change-password ::placeholder { color: var(--placeholder-color); } /* Focus */ .container-change-password input:focus { - border-color: rgba(30,211,206,0.95); - box-shadow: 0 0 0 4px rgba(30,211,206,0.06); + border-color: var(--focus-border); + box-shadow: 0 0 0 5px var(--focus-ring); } /* Button */ @@ -63,27 +127,75 @@ body.light .container-change-password input { width: 100%; padding: 10px; margin-top: 10px; - border-radius: 4px; + border-radius: 6px; border: none; - background: #1ed3ce; - color: #002e2c; + background: var(--primary-color); + color: var(--primary-contrast); font-weight: 700; cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.12); } - -/* Button hover */ .container-change-password button.primary:hover { transform: translateY(-1px); - background: #16cfc9; + filter: brightness(0.96); } /* Flash messages */ .flash-messages { margin-top: 12px; } -.flash { color: #ffd; background: transparent; margin: 6px 0; } +.flash { color: var(--panel-text); background: transparent; margin: 6px 0; } + +/* ====================================================================== */ +/* Theme-specific overrides: explicit, robust and page-scoped */ +/* The user requested exact colors for the .box area in each theme. */ +/* We apply them to both .box and .card and include !important to beat */ +/* theme CSS that previously forced blending. */ +/* ====================================================================== */ + +/* RetroIPTV: box background #48494a, text white */ +body.retroiptv .container-change-password .box, +body.retroiptv .container-change-password .card { + background: #48494a !important; + color: #ffffff !important; + border-color: rgba(59,15,26,0.15) !important; +} +body.retroiptv .container-change-password .box h2, +body.retroiptv .container-change-password .card h2 { + color: #ffffff !important; +} + +/* Retro Magazine: box background #48494a, text white */ +body.retro-magazine .container-change-password .box, +body.retro-magazine .container-change-password .card { + background: #48494a !important; + color: #ffffff !important; + border-color: rgba(59,15,26,0.15) !important; +} +body.retro-magazine .container-change-password .box h2, +body.retro-magazine .container-change-password .card h2 { + color: #ffffff !important; +} -/* Accessibility / small screens */ +/* DirecTV: box background #0c3451, text white */ +body.directv .container-change-password .box, +body.directv .container-change-password .card { + background: #0c3451 !important; + color: #ffffff !important; + border-color: rgba(0,0,0,0.12) !important; +} +body.directv .container-change-password .box h2, +body.directv .container-change-password .card h2 { + color: #ffffff !important; +} + +/* ====================================================================== */ +/* Misc responsive & accessibility */ +/* ====================================================================== */ @media (max-width: 480px) { .container-change-password { padding: 0 14px; } .container-change-password .box { padding: 14px; } } + +/* Smooth transitions but non-invasive */ +.container-change-password * { + transition: background-color .12s ease, color .12s ease, border-color .12s ease !important; +} diff --git a/static/css/change_tuner.css b/static/css/change_tuner.css index f6c99cc..033b956 100644 --- a/static/css/change_tuner.css +++ b/static/css/change_tuner.css @@ -1,153 +1,301 @@ -/* change_tuner page stylesheet — theme-aware via CSS variables - Falls back to original values if variables are not provided by theme CSS. -*/ +/* Full replacement: change_tuner page stylesheet + * + * Goals: + * - Scoped to .container-change-tuner so changes don't ripple site-wide. + * - Strong, readable defaults for cards, headings, labels, inputs, buttons. + * - Explicit per-theme overrides for the lead text you requested. + * - Accessible focus outlines and improved contrast for danger area. + * + * Replace the existing static/css/change_tuner.css with this file and hard-refresh (Ctrl+F5). + */ -/* Layout: center column and stack boxes */ +/* ====================================================================== */ +/* Page-scoped CSS variables (sane, higher-contrast defaults) */ +/* All variables are scoped to .container-change-tuner to avoid globals. */ +/* ====================================================================== */ .container-change-tuner { - max-width: 520px; - margin: 28px auto 80px; - padding: 0 12px; - box-sizing: border-box; + /* surface & text */ + --panel-bg: rgba(10,12,14,0.72); /* strong dark panel fallback */ + --panel-text: #e8f6f6; /* readable pale text on dark panels */ + --panel-border: rgba(255,255,255,0.06); + --card-head-bg: rgba(255,255,255,0.03); + --heading-color: #ffffff; + --heading-weight: 800; + + /* labels & muted text */ + --label-color: rgba(240,240,240,0.92); + --muted-color: rgba(220,220,220,0.65); + + /* inputs */ + --input-bg: rgba(255,255,255,0.04); + --input-border: rgba(160,200,200,0.18); + --input-color: #e9f7f7; + --placeholder-color: rgba(230,230,230,0.42); + + /* primary / danger */ + --primary-color: #1ed3ce; + --primary-hover: #16cfc9; + --primary-contrast: #ffffff; /* force white for reliable contrast */ + --danger-color: #ff4d4f; + + /* focus & visual */ + --focus-border: rgba(30,211,206,0.95); + --focus-ring: rgba(30,211,206,0.12); + --box-shadow: 0 8px 22px rgba(0,0,0,0.40); + --card-border-radius: 10px; + + /* dropdown / submenu defaults */ + --dropdown-bg: rgba(22,22,22,0.95); + --dropdown-color: #edf7f7; + + /* page defaults */ + color: var(--panel-text); + background: transparent; } -/* Panel (card) used for each section */ -.box { - margin: 28px 0; - padding: 18px 20px; - border-radius: 8px; +/* ====================================================================== */ +/* Light-theme page-scoped overrides (only affect this page) */ +/* ====================================================================== */ +body.light .container-change-tuner { + --panel-bg: #fff; + --panel-text: #111; + --panel-border: rgba(0,0,0,0.08); + --card-head-bg: rgba(0,0,0,0.02); + --heading-color: #111; + --heading-weight: 700; + + --label-color: #333; + --muted-color: rgba(0,0,0,0.56); + + --input-bg: #fff; + --input-border: rgba(0,0,0,0.12); + --input-color: #111; + --placeholder-color: rgba(0,0,0,0.38); - /* Use CSS variables so theme files can override these. - Fallback to previous values when variables are not defined. */ - background: var(--panel-bg, rgba(0,0,0,0.18)); - box-shadow: 0 2px 0 rgba(255,255,255,0.02) inset, 0 6px 18px rgba(0,0,0,0.18); - border: 1px solid var(--panel-border, rgba(255,255,255,0.03)); - color: var(--text-color, #eaf6f6); + --primary-color: #06c; + --primary-hover: #058; + --primary-contrast: #ffffff; + --box-shadow: 0 6px 18px rgba(0,0,0,0.06); + + --dropdown-bg: #fff; + --dropdown-color: #111; } -/* Headings */ -.box h2 { - margin: 0 0 12px; - font-size: 1.35rem; - font-weight: 700; - color: var(--heading-color, #fff); - text-shadow: 0 1px 0 rgba(0,0,0,0.25); +/* ====================================================================== */ +/* Layout */ +/* ====================================================================== */ +.container-change-tuner { + max-width: 1100px; + margin: 28px auto 80px; + padding: 18px; + box-sizing: border-box; } -/* Labels */ -.box label { - display: block; - font-size: 0.9rem; - color: var(--label-color, rgba(255,255,255,0.9)); - margin: 8px 0 6px; +/* Header */ +.page-header h1 { + margin: 0 0 6px; + font-size: 1.6rem; + color: var(--panel-text); + font-weight: var(--heading-weight); +} +.page-header .lead { + margin: 0 0 18px; + color: var(--muted-color); + font-size: 0.98rem; + line-height: 1.45; } -/* Form rows - stack fields with gap */ -.form-row { - display: block; - margin-bottom: 10px; +/* ====================================================================== */ +/* Grid & Cards */ +/* ====================================================================== */ +.card-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; } +.card-danger { grid-column: 1 / -1; } -/* Inputs & selects: full width, use themed borders/background/text */ -select, input[type="text"], input[type="url"], input[type="email"], input[type="password"], textarea { - width: 100%; - padding: 10px 12px; - border-radius: 6px; - border: 1px solid var(--input-border, rgba(100,200,200,0.25)); - background: var(--input-bg, rgba(255,255,255,0.03)); - color: var(--input-color, #eaf6f6); - box-sizing: border-box; - outline: none; +.card { + background: var(--panel-bg); + color: var(--panel-text); + border: 1px solid var(--panel-border); + border-radius: var(--card-border-radius); + overflow: hidden; + box-shadow: var(--box-shadow); +} +.card-head { + padding: 12px 16px; + background: var(--card-head-bg); + border-bottom: 1px solid color-mix(in srgb, var(--panel-border) 85%, transparent 15%); +} +.card-head h2 { + margin: 0; + font-size: 1.05rem; + color: var(--heading-color); + font-weight: var(--heading-weight); + text-shadow: none; +} +.card-body { padding: 14px 16px; } + +/* ====================================================================== */ +/* Form controls & readability */ +/* ====================================================================== */ +.compact-form label { + display:block; + margin:8px 0 6px; + font-size:.95rem; + color: var(--label-color); + font-weight:600; +} +.compact-form input[type="text"], +.compact-form select, +.compact-form textarea { + width:100%; + padding:9px 11px; + border-radius:6px; + border:1px solid var(--input-border); + background: var(--input-bg); + color: var(--input-color); + box-sizing:border-box; transition: box-shadow .12s ease, border-color .12s ease, background-color .12s ease; + font-size:0.96rem; } -/* slightly lighter placeholder text so it reads on dark backgrounds */ -::placeholder { color: var(--placeholder-color, rgba(255,255,255,0.35)); } +/* placeholder color */ +.container-change-tuner ::placeholder { color: var(--placeholder-color); } -/* Focus styles */ -select:focus, input:focus, textarea:focus { - border-color: var(--focus-border, rgba(36,220,210,0.95)); - box-shadow: 0 0 0 4px var(--focus-ring, rgba(36,220,210,0.06)); - background: var(--input-bg-focus, rgba(255,255,255,0.02)); - color: var(--input-color-focus, #fff); +/* focus */ +.compact-form input:focus, +.compact-form select:focus, +.compact-form textarea:focus { + border-color: var(--focus-border); + box-shadow: 0 0 0 5px var(--focus-ring); + outline: none; } -/* Primary button — full width pill */ -button.primary, input[type="submit"].primary { - display: block; - width: 100%; - padding: 10px 12px; - border-radius: 6px; - background: var(--primary-color, #1ed3ce); - color: var(--primary-text-color, #002e2c); - font-weight: 700; +/* ====================================================================== */ +/* Buttons */ +/* ====================================================================== */ +button.primary { + background: var(--primary-color); + color: var(--primary-contrast); border: none; - cursor: pointer; + padding: 8px 12px; + border-radius: 6px; + font-weight:700; + cursor:pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.12); - transition: transform .06s ease, box-shadow .06s ease, background .08s ease; } +button.primary:hover { background: var(--primary-hover); transform: translateY(-1px); } -/* Hover / active */ -button.primary:hover { transform: translateY(-1px); background: var(--primary-color-hover, #16cfc9); } -button.primary:active { transform: translateY(0); box-shadow: 0 1px 0 rgba(0,0,0,0.12); } - -/* Secondary small inline buttons (e.g., Rename button row) */ -.btn-inline { - display: inline-block; - padding: 6px 10px; - border-radius: 4px; - background: var(--btn-inline-bg, rgba(255,255,255,0.92)); - color: var(--btn-inline-color, #002e2c); - border: 1px solid var(--btn-inline-border, rgba(0,0,0,0.06)); - cursor: pointer; - font-weight: 600; - margin-left: 8px; +button.secondary { + background: transparent; + border: 1px solid color-mix(in srgb, var(--panel-border) 70%, transparent 30%); + padding: 8px 12px; + border-radius: 6px; + color: var(--panel-text); + cursor:pointer; +} +button.danger { + background: var(--danger-color); + color: #fff; + border:none; + padding:8px 12px; + border-radius:6px; + cursor:pointer; } -/* Tuning select & inputs layout in a row for small screens only: vertical stack on mobile */ -.row-inline { - display: flex; - gap: 10px; - align-items: center; +/* ====================================================================== */ +/* Rows, subgrid, divider */ +/* ====================================================================== */ +.form-row-inline { display:flex; gap:8px; align-items:center; margin-top:12px; } +.row-inline { display:flex; gap:12px; align-items:flex-start; } +.row-inline .col { flex:1 1 0; } +.subgrid { display:grid; grid-template-columns: 1fr 1fr; gap:12px; } +.divider { border: none; height: 1px; background: color-mix(in srgb, var(--panel-border) 85%, transparent 15%); margin: 12px 0; } + +/* Muted helper text */ +.muted { color: var(--muted-color); font-size:0.92rem; } +.small { font-size:0.88rem; } + +/* ====================================================================== */ +/* Danger card improvements */ +/* ====================================================================== */ +.card-danger { + border-left: 4px solid var(--danger-color); + background: color-mix(in srgb, var(--panel-bg) 80%, var(--danger-color) 6%); } -.row-inline .row-item { - flex: 1 1 auto; +.card-danger .card-head { + border-bottom-color: color-mix(in srgb, var(--danger-color) 12%, transparent 88%); } +.confirm-label { font-size:.9rem; margin-top:8px; color: var(--label-color); } +#confirm_delete { letter-spacing: 0.02em; } -/* Small screens: keep stacked */ -@media (max-width: 640px) { - .container-change-tuner { padding: 0 18px; max-width: 100%; } - .row-inline { flex-direction: column; align-items: stretch; } +/* ====================================================================== */ +/* Dropdowns & flyouts (scoped to this page) */ +/* ====================================================================== */ +.container-change-tuner .dropdown-content, +.container-change-tuner .submenu-content { + background: var(--dropdown-bg); + color: var(--dropdown-color, var(--panel-text)); + border: 1px solid color-mix(in srgb, var(--panel-border) 85%, transparent 15%); } -/* Additional spacing for the delete / rename sections to match original spacing */ -.box .sub-heading { - margin-top: 12px; - margin-bottom: 8px; - color: var(--sub-heading-color, rgba(255,255,255,0.9)); - font-weight: 600; +/* ====================================================================== */ +/* Table header contrasts (if table used) */ +/* ====================================================================== */ +.container-change-tuner th, +.container-change-tuner .table thead th { + color: var(--panel-text); + background: color-mix(in srgb, var(--panel-bg) 90%, transparent 10%); } -/* Theme-aware light adjustments: - Match how the theme script applies themes: html[data-theme="light"] */ -html[data-theme="light"] .box { - background: var(--panel-bg-light, rgba(255,255,255,0.95)); - color: var(--text-color-light, #111); +/* ====================================================================== */ +/* Accessibility focus outlines */ +/* ====================================================================== */ +button:focus, select:focus, input:focus, textarea:focus { + outline: 3px solid color-mix(in srgb, var(--primary-color) 30%, transparent 70%); + outline-offset: 2px; } -html[data-theme="light"] .box h2 { - color: var(--heading-color-light, #111); - text-shadow: none; + +/* ====================================================================== */ +/* Responsive */ +/* ====================================================================== */ +@media (max-width: 920px) { + .card-grid { grid-template-columns: 1fr; } + .subgrid { grid-template-columns: 1fr; } + .row-inline { flex-direction: column; } +} + +/* ====================================================================== */ +/* Theme-specific, exact overrides you asked for */ +/* - RetroIPTV and TV Guide: lead text color #48494a */ +/* - DirecTV: lead text color #153a56 */ +/* These are intentionally page-scoped and use !important to override */ +/* theme rules that previously caused blending. */ +/* ====================================================================== */ + +/* RetroIPTV */ +body.retroiptv .container-change-tuner .page-header h1, +body.retroiptv .container-change-tuner .page-header .lead { + color: #48494a !important; } -html[data-theme="light"] select, -html[data-theme="light"] input, -html[data-theme="light"] textarea { - color: var(--input-color-light, #111); - background: var(--input-bg-light, #fff); - border-color: var(--input-border-light, rgba(0,0,0,0.12)); + +/* TV Guide (retro-magazine) */ +body.retro-magazine .container-change-tuner .page-header h1, +body.retro-magazine .container-change-tuner .page-header .lead { + color: #48494a !important; } -html[data-theme="light"] button.primary { - background: var(--primary-color-light, #06c); - color: var(--primary-text-color-light, #fff); + +/* DirecTV */ +body.directv .container-change-tuner .page-header h1, +body.directv .container-change-tuner .page-header .lead { + color: #153a56 !important; } -/* small helper to hide an element if needed */ -.hidden { display: none !important; } +/* ====================================================================== */ +/* Smooth, conservative transitions */ +/* ====================================================================== */ +.container-change-tuner * { + transition: background-color .12s ease, color .12s ease, border-color .12s ease !important; +} diff --git a/static/css/manage_users.css b/static/css/manage_users.css index 062b4af..ae92e9d 100644 --- a/static/css/manage_users.css +++ b/static/css/manage_users.css @@ -1,6 +1,74 @@ -/* Per-page styles for Manage Users page — scoped to this page */ +/* Full replacement: Manage Users page stylesheet + * + * Purpose: + * - Match the Change Tuner visual system (same scoped variables) so cards/boxes, + * inputs, select, buttons, table headers, and focus states are consistent. + * - Scoped to .container-manage-users so site-wide CSS is unaffected. + * - Theme-specific header overrides for RetroIPTV, Retro Magazine, and DirecTV. + */ + +/* Page-scoped variables (same tokens as change_tuner.css) */ +.container-manage-users { + --panel-bg: rgba(10,12,14,0.72); + --panel-text: #e8f6f6; + --panel-border: rgba(255,255,255,0.06); + --card-head-bg: rgba(255,255,255,0.03); + --heading-color: #ffffff; + --heading-weight: 800; + + --label-color: rgba(240,240,240,0.92); + --muted-color: rgba(220,220,220,0.65); + + --input-bg: rgba(255,255,255,0.04); + --input-border: rgba(160,200,200,0.18); + --input-color: #e9f7f7; + --placeholder-color: rgba(230,230,230,0.42); + + --primary-color: #1ed3ce; + --primary-hover: #16cfc9; + --primary-contrast: #ffffff; + + --danger-color: #c00; + + --focus-border: rgba(30,211,206,0.95); + --focus-ring: rgba(30,211,206,0.12); + --box-shadow: 0 8px 22px rgba(0,0,0,0.40); + --card-border-radius: 10px; -/* Outer container to center content */ + --dropdown-bg: rgba(22,22,22,0.95); + --dropdown-color: #edf7f7; + + color: var(--panel-text); + background: transparent; +} + +/* Light-theme page-scoped overrides */ +body.light .container-manage-users { + --panel-bg: #f3f3f3; + --panel-text: #111; + --panel-border: rgba(0,0,0,0.06); + --card-head-bg: rgba(0,0,0,0.02); + --heading-color: #111; + --heading-weight: 700; + + --label-color: #333; + --muted-color: rgba(0,0,0,0.56); + + --input-bg: #fff; + --input-border: rgba(0,0,0,0.08); + --input-color: #111; + --placeholder-color: rgba(0,0,0,0.36); + + --primary-color: #06c; + --primary-hover: #058; + --primary-contrast: #ffffff; + --box-shadow: 0 6px 18px rgba(0,0,0,0.06); + + --dropdown-bg: #fff; + --dropdown-color: #111; +} + +/* Layout */ .container-manage-users { max-width: 720px; margin: 60px auto; @@ -8,21 +76,32 @@ box-sizing: border-box; } -/* Panel */ -.container-manage-users .box { +/* Panel / Card surface */ +.container-manage-users .box, +.container-manage-users .card { padding: 18px; - border-radius: 8px; - background: rgba(0,0,0,0.06); - border: 1px solid rgba(255,255,255,0.03); - box-shadow: 0 2px 0 rgba(255,255,255,0.02) inset; + border-radius: var(--card-border-radius); + background: var(--panel-bg); + border: 1px solid var(--panel-border); + box-shadow: var(--box-shadow); + color: var(--panel-text); } /* Headings */ -.container-manage-users h2 { margin: 0 0 12px; font-size:1.25rem; font-weight:700; color:inherit; } -.container-manage-users h3 { margin: 10px 0; font-size:1rem; color:inherit; } +.container-manage-users h2 { + margin: 0 0 12px; + font-size: 1.25rem; + font-weight: 700; + color: var(--heading-color); +} +.container-manage-users h3 { + margin: 10px 0; + font-size: 1rem; + color: var(--heading-color); +} /* Form rows */ -.form-row { margin-bottom: 8px; } +.container-manage-users .form-row { margin-bottom: 8px; } /* Inputs / selects */ .container-manage-users input[type="text"], @@ -30,68 +109,78 @@ .container-manage-users select { width: 100%; padding: 8px 10px; - border-radius: 4px; - border: 1px solid rgba(255,255,255,0.06); - background: rgba(255,255,255,0.03); - color: inherit; + border-radius: 6px; + border: 1px solid var(--input-border); + background: var(--input-bg); + color: var(--input-color); box-sizing: border-box; outline: none; + font-size: 0.96rem; } -.container-manage-users input:focus { - border-color: rgba(30,211,206,0.95); - box-shadow: 0 0 0 4px rgba(30,211,206,0.06); + +/* Placeholder and focus */ +.container-manage-users ::placeholder { color: var(--placeholder-color); } +.container-manage-users input:focus, +.container-manage-users select:focus { + border-color: var(--focus-border); + box-shadow: 0 0 0 5px var(--focus-ring); } /* Buttons */ -.btn { padding: 8px 12px; border-radius:4px; border:none; cursor:pointer; font-weight:700; } -.btn.add { background: #0a0; color: #fff; display: inline-block; } -.btn.add:hover { background: #070; } -.btn.danger { background: #c00; color: #fff; } -.btn.danger:hover { background: #900; } +.btn { padding: 8px 12px; border-radius:6px; border:none; cursor:pointer; font-weight:700; } +.btn.add { background: var(--primary-color); color: var(--primary-contrast); display: inline-block; } +.btn.add:hover { background: var(--primary-hover); } +.btn.danger { background: var(--danger-color); color: #fff; } .btn.warn { background: #d2691e; color: #fff; } -.btn.warn:hover { background: #a0522d; } -/* For the add user button make it full width in the form */ +/* Make add-user button full width in add-user form */ .add-user-form .btn.add { width: 100%; } /* Table */ .table-wrap { overflow-x: auto; margin-top: 12px; } .users-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; } -.users-table thead th { background: rgba(0,0,0,0.6); color: #fff; padding: 8px; text-align: left; position: sticky; top: 0; z-index: 5; } -.users-table tbody td { padding: 10px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); vertical-align: middle; } +.users-table thead th { + background: color-mix(in srgb, var(--panel-bg) 85%, rgba(0,0,0,0.06) 15%); + color: var(--panel-text); + padding: 8px; + text-align: left; + position: sticky; + top: 0; + z-index: 5; +} +.users-table tbody td { + padding: 10px 8px; + border-bottom: 1px solid color-mix(in srgb, var(--panel-border) 75%, transparent 25%); + vertical-align: middle; + color: var(--panel-text); +} -/* Actions cell: inline forms */ +/* Actions & inline forms */ .actions { display:flex; gap:8px; } - -/* Inline form reset so buttons sit next to each other */ .inline-form { display:inline; margin:0; padding:0; } /* Flash messages */ .flash-messages { margin-top: 12px; } -.flash { color: #ffd; background: transparent; margin: 6px 0; } - -/* Theme overrides */ -body.dark .container-manage-users .box { background: #222; border-color: rgba(255,255,255,0.03); } -body.dark .users-table thead th { background: #333; color: #fff; } -body.dark .users-table tbody tr:nth-child(even) { background: #222; color: #fff; } +.flash { color: var(--panel-text); background: transparent; margin: 6px 0; } -body.light .container-manage-users .box { background: #f3f3f3; border-color: rgba(0,0,0,0.06); color: #000; } -body.light .users-table thead th { background: #ddd; color: #000; } -body.light .users-table tbody tr:nth-child(even) { background: #fff; color: #000; } - -body.retro-aol .container-manage-users .box { - padding: 18px; - border-radius: 8px; - background: #004466; - border:2px solid #33cccc; - box-shadow: 0 2px 0 rgba(255,255,255,0.02) inset; +/* Theme-specific header overrides to match change_tuner exact colors */ +/* RetroIPTV */ +body.retroiptv .container-manage-users h2, +body.retroiptv .container-manage-users h3 { + color: #ffffff !important; } -body.retro-magazine .container-manage-users .box { border:2px solid #000000; } - -body.directv .container-manage-users .box { background:linear-gradient(to bottom,#e7e4ff,#d2f3ff); border-bottom:2px solid #003090; } +/* Retro Magazine (tv guide look) */ +body.retro-magazine .container-manage-users h2, +body.retro-magazine .container-manage-users h3 { + color: #ffffff !important; +} -body.comcast .container-manage-users .box { background:linear-gradient(to bottom,#0055cc,#001b50); border-bottom:2px solid #003090; } +/* DirecTV */ +body.directv .container-manage-users h2, +body.directv .container-manage-users h3 { + color: #ffffff !important; +} /* Responsive */ @media (max-width: 640px) { diff --git a/static/css/mobile.css b/static/css/mobile.css index a0aab31..390314e 100644 --- a/static/css/mobile.css +++ b/static/css/mobile.css @@ -162,3 +162,14 @@ html, body { /* Ensure grid-content transform is GPU-friendly */ .grid-content { will-change: transform; -webkit-transform-origin: left top; transform-origin: left top; } } + + +/* Theme-aware separator for tuner fly-outs */ +.separator { + border-top: 1px solid currentColor; + opacity: 0.25; + margin: 6px 0; + padding: 0; + list-style: none; + height: 0; +} diff --git a/templates/_header.html b/templates/_header.html index f7fe684..57e7253 100644 --- a/templates/_header.html +++ b/templates/_header.html @@ -8,15 +8,23 @@ Active Tuner: {{ current_tuner }} HOME - {% if current_user is defined and current_user.username == 'admin' %} - MANAGE USERS - {% endif %} -