From 699ca7d6df4d6aced00f8c25daa2639eb3fb1087 Mon Sep 17 00:00:00 2001 From: Michael Sitz Date: Thu, 20 Aug 2026 19:50:20 -0400 Subject: [PATCH] Added some functionality so we can modify the size of the playing video --- offlineu_core.py | 21 +++++++++++++++ templates/lesson_view.html | 55 +++++++++++++++++++++++++++++++++++++- templates/settings.html | 29 ++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/offlineu_core.py b/offlineu_core.py index b23c90c..bb7e247 100644 --- a/offlineu_core.py +++ b/offlineu_core.py @@ -47,8 +47,14 @@ DEFAULT_SETTINGS = { 'card_style': 'flat', # 'flat' | 'elevated' | 'bordered' 'corner_radius': 'rounded', # 'sharp' | 'rounded' | 'pill' 'library_path': '', # '' = use COURSES_LIBRARY_PATH/--library-path default + 'video_width': '', # '' = responsive full-width; else last dragged size, in px + 'video_height': '', } +# Bounds for the persisted video player size, to reject garbage values +# without capping how big/small someone can reasonably drag it. +VIDEO_SIZE_BOUNDS = {'video_width': (200, 4000), 'video_height': (120, 3000)} + # Allowed values for every setting except accent_color (validated separately # as a hex string). Anything outside these sets is rejected/ignored on save. SETTINGS_CHOICES = { @@ -91,6 +97,9 @@ def load_settings() -> Dict[str, Any]: # Lenient on load (directory may be transiently # unavailable at startup) - only validated on save. settings[key] = value + elif key in VIDEO_SIZE_BOUNDS: + if value == '' or (isinstance(value, int) and not isinstance(value, bool)): + settings[key] = value elif key in SETTINGS_CHOICES and value in SETTINGS_CHOICES[key]: settings[key] = value except (json.JSONDecodeError, OSError) as e: @@ -109,6 +118,18 @@ def save_settings(new_settings: Dict[str, Any]) -> Dict[str, Any]: if value and not os.path.isdir(value): raise ValueError(f"Directory not found: {value}") current[key] = value + elif key in VIDEO_SIZE_BOUNDS: + if value in (None, ''): + current[key] = '' + continue + try: + num = int(value) + except (TypeError, ValueError): + raise ValueError(f"{key} must be a number") + lo, hi = VIDEO_SIZE_BOUNDS[key] + if not (lo <= num <= hi): + raise ValueError(f"{key} must be between {lo} and {hi}") + current[key] = num elif key in SETTINGS_CHOICES and value in SETTINGS_CHOICES[key]: current[key] = value os.makedirs(DATA_DIR, exist_ok=True) diff --git a/templates/lesson_view.html b/templates/lesson_view.html index 8107607..7c15c38 100644 --- a/templates/lesson_view.html +++ b/templates/lesson_view.html @@ -130,11 +130,25 @@ } video, audio { width: 100%; - max-width: 800px; + max-width: 100%; border-radius: var(--radius); display: block; margin: 0 auto; } + video { + resize: both; + overflow: hidden; + object-fit: contain; + background: #000; + min-width: 320px; + min-height: 200px; + } + .resize-hint { + text-align: center; + font-size: 0.8em; + color: var(--text-muted); + margin-top: 6px; + } .content { margin: 20px 0; } @@ -246,6 +260,7 @@ {% endif %} Your browser does not support the video tag. +

↘ Drag the bottom-right corner to resize the player

{% endif %} {% if lesson.audio_file %} @@ -372,6 +387,44 @@ const activeMedia = video || audio; let isCompleted = false; + // Restore last-used video player size, and persist future resizes + // (dragging the native corner handle) so it sticks next time. + if (video) { + let applyingStoredSize = true; + let resizeSaveTimeout = null; + + fetch('/api/settings') + .then(r => r.json()) + .then(data => { + const s = data.settings || {}; + if (s.video_width && s.video_height) { + video.style.width = s.video_width + 'px'; + video.style.height = s.video_height + 'px'; + } + setTimeout(() => { applyingStoredSize = false; }, 300); + }) + .catch(() => { applyingStoredSize = false; }); + + if (window.ResizeObserver) { + const observer = new ResizeObserver(() => { + if (applyingStoredSize) return; + clearTimeout(resizeSaveTimeout); + resizeSaveTimeout = setTimeout(() => { + const rect = video.getBoundingClientRect(); + fetch('/api/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + video_width: Math.round(rect.width), + video_height: Math.round(rect.height) + }) + }).catch(() => {}); + }, 500); + }); + observer.observe(video); + } + } + if (activeMedia) { let lastSaveTime = 0; diff --git a/templates/settings.html b/templates/settings.html index d053622..dca0442 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -331,6 +331,22 @@ +
+

Video Player

+
+ + +
+
+
✓ Saved @@ -425,6 +441,19 @@ }); } + function resetVideoSize() { + fetch('/api/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ video_width: '', video_height: '' }) + }) + .then(r => r.json()) + .then(data => { + if (data.success) location.reload(); + }) + .catch(err => console.error('Failed to reset video size:', err)); + } + function resetSettings() { fetch('/api/settings/reset', { method: 'POST' }) .then(r => r.json())