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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user