feat(video, settings, docs): add muted playback, nested Settings tabs, merge holidays tab; bump 2025.1.0-alpha.11

API/DB: add Event.muted with full CRUD wiring (Alembic migration), persist/return with autoplay/loop/volume
Dashboard: per‑event video options (autoplay/loop/volume/muted) with system defaults; Settings → Events → Videos defaults
Settings UX: nested tabs with controlled selection; Academic Calendar: merge “Schulferien Import”+“Liste” into “📥 Import & Liste”
Docs: update README and copilot-instructions (video payload, streaming 206, defaults keys); update program-info.json changelog; bump version to 2025.1.0‑alpha.11
This commit is contained in:
RobbStarkAustria
2025-11-05 19:30:10 +00:00
parent 38800cec68
commit 452ba3033b
12 changed files with 793 additions and 381 deletions

View File

@@ -28,6 +28,7 @@ type CustomEventData = {
autoplay?: boolean;
loop?: boolean;
volume?: number;
muted?: boolean;
};
// Typ für initialData erweitern, damit Id unterstützt wird
@@ -117,13 +118,50 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
);
const [websiteUrl, setWebsiteUrl] = React.useState<string>(initialData.websiteUrl ?? '');
// Video-specific state
// Video-specific state with system defaults loading
const [autoplay, setAutoplay] = React.useState<boolean>(initialData.autoplay ?? true);
const [loop, setLoop] = React.useState<boolean>(initialData.loop ?? false);
const [loop, setLoop] = React.useState<boolean>(initialData.loop ?? true);
const [volume, setVolume] = React.useState<number>(initialData.volume ?? 0.8);
const [muted, setMuted] = React.useState<boolean>(initialData.muted ?? false);
const [videoDefaultsLoaded, setVideoDefaultsLoaded] = React.useState<boolean>(false);
const [mediaModalOpen, setMediaModalOpen] = React.useState(false);
// Load system video defaults once when opening for a new video event
React.useEffect(() => {
if (open && !editMode && !videoDefaultsLoaded) {
(async () => {
try {
const api = await import('../apiSystemSettings');
const keys = ['video_autoplay', 'video_loop', 'video_volume', 'video_muted'] as const;
const [autoplayRes, loopRes, volumeRes, mutedRes] = await Promise.all(
keys.map(k => api.getSetting(k).catch(() => ({ value: null } as { value: string | null })))
);
// Only apply defaults if not already set from initialData
if (initialData.autoplay === undefined) {
setAutoplay(autoplayRes.value == null ? true : autoplayRes.value === 'true');
}
if (initialData.loop === undefined) {
setLoop(loopRes.value == null ? true : loopRes.value === 'true');
}
if (initialData.volume === undefined) {
const volParsed = volumeRes.value == null ? 0.8 : parseFloat(String(volumeRes.value));
setVolume(Number.isFinite(volParsed) ? volParsed : 0.8);
}
if (initialData.muted === undefined) {
setMuted(mutedRes.value == null ? false : mutedRes.value === 'true');
}
setVideoDefaultsLoaded(true);
} catch {
// Silently fall back to hard-coded defaults
setVideoDefaultsLoaded(true);
}
})();
}
}, [open, editMode, videoDefaultsLoaded, initialData]);
React.useEffect(() => {
if (open) {
const isSingleOccurrence = initialData.isSingleOccurrence || false;
@@ -154,12 +192,16 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
setPageProgress(initialData.pageProgress ?? true);
setAutoProgress(initialData.autoProgress ?? true);
setWebsiteUrl(initialData.websiteUrl ?? '');
// Video fields
setAutoplay(initialData.autoplay ?? true);
setLoop(initialData.loop ?? false);
setVolume(initialData.volume ?? 0.8);
// Video fields - use initialData values when editing
if (editMode) {
setAutoplay(initialData.autoplay ?? true);
setLoop(initialData.loop ?? true);
setVolume(initialData.volume ?? 0.8);
setMuted(initialData.muted ?? false);
}
}
}, [open, initialData]);
}, [open, initialData, editMode]);
React.useEffect(() => {
if (!mediaModalOpen && pendingMedia) {
@@ -296,6 +338,7 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
payload.autoplay = autoplay;
payload.loop = loop;
payload.volume = volume;
payload.muted = muted;
}
try {
@@ -664,13 +707,24 @@ const CustomEventModal: React.FC<CustomEventModalProps> = ({
/>
</div>
<div style={{ marginTop: 8 }}>
<TextBoxComponent
placeholder="Lautstärke (0.0 - 1.0)"
floatLabelType="Auto"
type="number"
value={String(volume)}
change={e => setVolume(Math.max(0, Math.min(1, Number(e.value))))}
/>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500, fontSize: '14px' }}>
Lautstärke
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<TextBoxComponent
placeholder="0.0 - 1.0"
floatLabelType="Never"
type="number"
value={String(volume)}
change={e => setVolume(Math.max(0, Math.min(1, Number(e.value))))}
style={{ flex: 1 }}
/>
<CheckBoxComponent
label="Ton aus"
checked={muted}
change={e => setMuted(e.checked || false)}
/>
</div>
</div>
</div>
)}