Added some functionality so we can modify the size of the playing video
This commit is contained in:
@@ -47,8 +47,14 @@ DEFAULT_SETTINGS = {
|
|||||||
'card_style': 'flat', # 'flat' | 'elevated' | 'bordered'
|
'card_style': 'flat', # 'flat' | 'elevated' | 'bordered'
|
||||||
'corner_radius': 'rounded', # 'sharp' | 'rounded' | 'pill'
|
'corner_radius': 'rounded', # 'sharp' | 'rounded' | 'pill'
|
||||||
'library_path': '', # '' = use COURSES_LIBRARY_PATH/--library-path default
|
'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
|
# Allowed values for every setting except accent_color (validated separately
|
||||||
# as a hex string). Anything outside these sets is rejected/ignored on save.
|
# as a hex string). Anything outside these sets is rejected/ignored on save.
|
||||||
SETTINGS_CHOICES = {
|
SETTINGS_CHOICES = {
|
||||||
@@ -91,6 +97,9 @@ def load_settings() -> Dict[str, Any]:
|
|||||||
# Lenient on load (directory may be transiently
|
# Lenient on load (directory may be transiently
|
||||||
# unavailable at startup) - only validated on save.
|
# unavailable at startup) - only validated on save.
|
||||||
settings[key] = value
|
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]:
|
elif key in SETTINGS_CHOICES and value in SETTINGS_CHOICES[key]:
|
||||||
settings[key] = value
|
settings[key] = value
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
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):
|
if value and not os.path.isdir(value):
|
||||||
raise ValueError(f"Directory not found: {value}")
|
raise ValueError(f"Directory not found: {value}")
|
||||||
current[key] = 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]:
|
elif key in SETTINGS_CHOICES and value in SETTINGS_CHOICES[key]:
|
||||||
current[key] = value
|
current[key] = value
|
||||||
os.makedirs(DATA_DIR, exist_ok=True)
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
|||||||
@@ -130,11 +130,25 @@
|
|||||||
}
|
}
|
||||||
video, audio {
|
video, audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 100%;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
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 {
|
.content {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
@@ -246,6 +260,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
|
<p class="resize-hint">↘ Drag the bottom-right corner to resize the player</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if lesson.audio_file %}
|
{% if lesson.audio_file %}
|
||||||
@@ -372,6 +387,44 @@
|
|||||||
const activeMedia = video || audio;
|
const activeMedia = video || audio;
|
||||||
let isCompleted = false;
|
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) {
|
if (activeMedia) {
|
||||||
let lastSaveTime = 0;
|
let lastSaveTime = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Video Player</h2>
|
||||||
|
<div class="setting-row">
|
||||||
|
<label>Player size
|
||||||
|
<span class="setting-desc">
|
||||||
|
{% if settings.video_width and settings.video_height %}
|
||||||
|
Remembered at {{ settings.video_width }}×{{ settings.video_height }}px from your last resize
|
||||||
|
{% else %}
|
||||||
|
Using default responsive full-width sizing
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-secondary" onclick="resetVideoSize()">Reset size</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
|
<button class="btn btn-secondary" onclick="resetSettings()">Reset to Defaults</button>
|
||||||
<span id="save-status">✓ Saved</span>
|
<span id="save-status">✓ Saved</span>
|
||||||
@@ -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() {
|
function resetSettings() {
|
||||||
fetch('/api/settings/reset', { method: 'POST' })
|
fetch('/api/settings/reset', { method: 'POST' })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
|
|||||||
Reference in New Issue
Block a user