fix: reinit player safely
This commit is contained in:
@@ -6,6 +6,7 @@ import { updateQualityOptions } from '../quality';
|
|||||||
import { updateModeButtons } from '../mode';
|
import { updateModeButtons } from '../mode';
|
||||||
import { updateOverlay, isAutoplayEnabled, switchEpisodeRange } from './ui';
|
import { updateOverlay, isAutoplayEnabled, switchEpisodeRange } from './ui';
|
||||||
import { markEpisodeTransition } from '../progress';
|
import { markEpisodeTransition } from '../progress';
|
||||||
|
import { safeLocalStorage } from '../storage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles video end: either marks complete or loads next episode.
|
* Handles video end: either marks complete or loads next episode.
|
||||||
@@ -71,7 +72,7 @@ export const goToNextEpisode = async (): Promise<void> => {
|
|||||||
state.container.dataset.startTimeSeconds = String(state.startTimeSeconds);
|
state.container.dataset.startTimeSeconds = String(state.startTimeSeconds);
|
||||||
|
|
||||||
// load new video (keep preferences)
|
// load new video (keep preferences)
|
||||||
const preferredQuality = localStorage.getItem('mal:preferred-quality') || 'best';
|
const preferredQuality = safeLocalStorage.getItem('mal:preferred-quality') || 'best';
|
||||||
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(fallback)}&token=${encodeURIComponent(state.modeSources[fallback].token)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(fallback)}&token=${encodeURIComponent(state.modeSources[fallback].token)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
||||||
state.video.load();
|
state.video.load();
|
||||||
if (!state.video.paused) {
|
if (!state.video.paused) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
import { qs } from '../../q';
|
import { qs } from '../../q';
|
||||||
|
import { safeLocalStorage } from '../storage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs autoplay checkbox with localStorage on init.
|
* Syncs autoplay checkbox with localStorage on init.
|
||||||
@@ -8,11 +9,11 @@ import { qs } from '../../q';
|
|||||||
export const setupAutoplayButton = (): void => {
|
export const setupAutoplayButton = (): void => {
|
||||||
const btn = document.querySelector('[data-autoplay]') as HTMLInputElement | null;
|
const btn = document.querySelector('[data-autoplay]') as HTMLInputElement | null;
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
btn.checked = localStorage.getItem('mal:autoplay-enabled') !== 'false';
|
btn.checked = safeLocalStorage.getItem('mal:autoplay-enabled') !== 'false';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAutoplayEnabled = (): boolean =>
|
export const isAutoplayEnabled = (): boolean =>
|
||||||
localStorage.getItem('mal:autoplay-enabled') !== 'false';
|
safeLocalStorage.getItem('mal:autoplay-enabled') !== 'false';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates video overlay text (shown briefly on episode change).
|
* Updates video overlay text (shown briefly on episode change).
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { resolveActiveSegments, renderSegments } from './skip/segments';
|
|||||||
import { setupSegmentEditor } from './skip/editor';
|
import { setupSegmentEditor } from './skip/editor';
|
||||||
import { setupThumbnails } from './episodes/thumbnails';
|
import { setupThumbnails } from './episodes/thumbnails';
|
||||||
import { markEpisodeTransition, setupProgress } from './progress';
|
import { markEpisodeTransition, setupProgress } from './progress';
|
||||||
|
import { safeLocalStorage } from './storage';
|
||||||
import {
|
import {
|
||||||
absoluteTimeFromDisplay,
|
absoluteTimeFromDisplay,
|
||||||
absoluteTimeFromRatio,
|
absoluteTimeFromRatio,
|
||||||
@@ -20,7 +21,8 @@ import {
|
|||||||
} from './timeline';
|
} from './timeline';
|
||||||
import { formatTime } from './controls';
|
import { formatTime } from './controls';
|
||||||
|
|
||||||
let initialized = false; // prevent double init on htmx swaps
|
let currentContainer: HTMLElement | null = null;
|
||||||
|
let cleanup: (() => void) | null = null;
|
||||||
|
|
||||||
type ClosableDropdown = HTMLElement & { close: () => void };
|
type ClosableDropdown = HTMLElement & { close: () => void };
|
||||||
const isClosableDropdown = (el: Element | null): el is ClosableDropdown => {
|
const isClosableDropdown = (el: Element | null): el is ClosableDropdown => {
|
||||||
@@ -45,6 +47,12 @@ const showPreviewPopover = (): void => {
|
|||||||
state.previewPopover.classList.add('opacity-100');
|
state.previewPopover.classList.add('opacity-100');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const teardownPlayer = (): void => {
|
||||||
|
cleanup?.();
|
||||||
|
cleanup = null;
|
||||||
|
currentContainer = null;
|
||||||
|
};
|
||||||
|
|
||||||
// updates time preview on progress bar hover
|
// updates time preview on progress bar hover
|
||||||
const updatePreviewUI = (ratio: number): void => {
|
const updatePreviewUI = (ratio: number): void => {
|
||||||
const progressWrap = state.container.querySelector('[data-progress-wrap]') as HTMLElement | null;
|
const progressWrap = state.container.querySelector('[data-progress-wrap]') as HTMLElement | null;
|
||||||
@@ -75,20 +83,25 @@ const updatePreviewUI = (ratio: number): void => {
|
|||||||
|
|
||||||
const initPlayer = (): void => {
|
const initPlayer = (): void => {
|
||||||
const container = document.querySelector('[data-video-player]') as HTMLElement | null;
|
const container = document.querySelector('[data-video-player]') as HTMLElement | null;
|
||||||
if (!container || initialized) return;
|
if (!container) return;
|
||||||
|
if (container === currentContainer) return;
|
||||||
|
teardownPlayer();
|
||||||
|
|
||||||
if (!initState(container)) {
|
if (!initState(container)) {
|
||||||
console.error('Video player markup is missing required controls.');
|
console.error('Video player markup is missing required controls.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initialized = true;
|
currentContainer = container;
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const signal = abortController.signal;
|
||||||
|
cleanup = () => abortController.abort();
|
||||||
|
|
||||||
const loading = container.querySelector('[data-loading]') as HTMLElement | null;
|
const loading = container.querySelector('[data-loading]') as HTMLElement | null;
|
||||||
const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement | null;
|
const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement | null;
|
||||||
|
|
||||||
// build video src from mode, token, and saved quality preference
|
// build video src from mode, token, and saved quality preference
|
||||||
// Only set if not already provided by the inline script during HTML parsing
|
// Only set if not already provided by the inline script during HTML parsing
|
||||||
const preferredQuality = localStorage.getItem('mal:preferred-quality') || 'best';
|
const preferredQuality = safeLocalStorage.getItem('mal:preferred-quality') || 'best';
|
||||||
const streamToken = state.modeSources[state.currentMode]?.token;
|
const streamToken = state.modeSources[state.currentMode]?.token;
|
||||||
if (!state.video.src && streamToken) {
|
if (!state.video.src && streamToken) {
|
||||||
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(state.currentMode)}&token=${encodeURIComponent(streamToken)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(state.currentMode)}&token=${encodeURIComponent(streamToken)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
||||||
@@ -146,7 +159,7 @@ const initPlayer = (): void => {
|
|||||||
updateSkipButton(state.video.currentTime);
|
updateSkipButton(state.video.currentTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
state.video.addEventListener('loadedmetadata', onLoadedMetadata);
|
state.video.addEventListener('loadedmetadata', onLoadedMetadata, { signal });
|
||||||
// inline script runs during HTML parsing before initPlayer; if metadata
|
// inline script runs during HTML parsing before initPlayer; if metadata
|
||||||
// already loaded, fire the handler immediately
|
// already loaded, fire the handler immediately
|
||||||
if (state.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
if (state.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
||||||
@@ -157,27 +170,27 @@ const initPlayer = (): void => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
loading.style.display = 'flex';
|
loading.style.display = 'flex';
|
||||||
}
|
}
|
||||||
});
|
}, { signal });
|
||||||
state.video.addEventListener('playing', () => {
|
state.video.addEventListener('playing', () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
loading.style.display = 'none';
|
loading.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
}, { signal });
|
||||||
// update progress bar during buffering
|
// update progress bar during buffering
|
||||||
state.video.addEventListener('progress', () => {
|
state.video.addEventListener('progress', () => {
|
||||||
updateTimeline(state.video.currentTime);
|
updateTimeline(state.video.currentTime);
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// main loop: update progress, subtitles, skip buttons
|
// main loop: update progress, subtitles, skip buttons
|
||||||
state.video.addEventListener('timeupdate', () => {
|
state.video.addEventListener('timeupdate', () => {
|
||||||
updateTimeline(state.video.currentTime);
|
updateTimeline(state.video.currentTime);
|
||||||
updateSubtitleRender(displayTimeFromAbsolute(state.video.currentTime));
|
updateSubtitleRender(displayTimeFromAbsolute(state.video.currentTime));
|
||||||
updateSkipButton(state.video.currentTime);
|
updateSkipButton(state.video.currentTime);
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
state.video.addEventListener('ended', () => {
|
state.video.addEventListener('ended', () => {
|
||||||
goToNextEpisode();
|
goToNextEpisode();
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// click/drag to seek (pointer events are more consistent across fullscreen/mobile)
|
// click/drag to seek (pointer events are more consistent across fullscreen/mobile)
|
||||||
progressWrap?.addEventListener('pointerdown', e => {
|
progressWrap?.addEventListener('pointerdown', e => {
|
||||||
@@ -194,20 +207,20 @@ const initPlayer = (): void => {
|
|||||||
updateTimeline(state.video.currentTime);
|
updateTimeline(state.video.currentTime);
|
||||||
updateSkipButton(state.video.currentTime);
|
updateSkipButton(state.video.currentTime);
|
||||||
showControls();
|
showControls();
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// hover to preview time
|
// hover to preview time
|
||||||
progressWrap?.addEventListener('pointermove', e => {
|
progressWrap?.addEventListener('pointermove', e => {
|
||||||
const rect = progressWrap.getBoundingClientRect();
|
const rect = progressWrap.getBoundingClientRect();
|
||||||
updatePreviewUI(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
|
updatePreviewUI(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
progressWrap?.addEventListener('pointerleave', hidePreviewPopover);
|
progressWrap?.addEventListener('pointerleave', hidePreviewPopover, { signal });
|
||||||
progressWrap?.addEventListener('pointerup', () => {
|
progressWrap?.addEventListener('pointerup', () => {
|
||||||
// ensure we finish the seek even if no window mousemove fired
|
// ensure we finish the seek even if no window mousemove fired
|
||||||
if (!progressWrap) return;
|
if (!progressWrap) return;
|
||||||
state.isScrubbing = false;
|
state.isScrubbing = false;
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// dragging outside progress bar while scrubbing
|
// dragging outside progress bar while scrubbing
|
||||||
window.addEventListener('pointermove', e => {
|
window.addEventListener('pointermove', e => {
|
||||||
@@ -218,7 +231,7 @@ const initPlayer = (): void => {
|
|||||||
);
|
);
|
||||||
updateTimeline(state.video.currentTime);
|
updateTimeline(state.video.currentTime);
|
||||||
updateSkipButton(state.video.currentTime);
|
updateSkipButton(state.video.currentTime);
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// track next-episode links outside the player so they start fresh after finishing an episode
|
// track next-episode links outside the player so they start fresh after finishing an episode
|
||||||
document.addEventListener('click', e => {
|
document.addEventListener('click', e => {
|
||||||
@@ -234,9 +247,9 @@ const initPlayer = (): void => {
|
|||||||
const nextEpisode = Number.parseInt(url.searchParams.get('ep') ?? '1', 10);
|
const nextEpisode = Number.parseInt(url.searchParams.get('ep') ?? '1', 10);
|
||||||
const currentEpisode = Number.parseInt(state.currentEpisode, 10);
|
const currentEpisode = Number.parseInt(state.currentEpisode, 10);
|
||||||
if (nextEpisode === currentEpisode + 1) markEpisodeTransition(nextEpisode);
|
if (nextEpisode === currentEpisode + 1) markEpisodeTransition(nextEpisode);
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
state.video.addEventListener('click', showControls);
|
state.video.addEventListener('click', showControls, { signal });
|
||||||
|
|
||||||
const searchInput = document.querySelector('[data-episode-search]') as HTMLInputElement | null;
|
const searchInput = document.querySelector('[data-episode-search]') as HTMLInputElement | null;
|
||||||
const dropdown = document.querySelector('[data-episode-dropdown]') as HTMLElement | null;
|
const dropdown = document.querySelector('[data-episode-dropdown]') as HTMLElement | null;
|
||||||
@@ -265,7 +278,7 @@ const initPlayer = (): void => {
|
|||||||
updateEpisodeHighlight(clamped);
|
updateEpisodeHighlight(clamped);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
}, { signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
// range buttons (100s of episodes)
|
// range buttons (100s of episodes)
|
||||||
@@ -276,7 +289,7 @@ const initPlayer = (): void => {
|
|||||||
switchEpisodeRange(idx);
|
switchEpisodeRange(idx);
|
||||||
const dd = btn.closest('ui-dropdown');
|
const dd = btn.closest('ui-dropdown');
|
||||||
if (isClosableDropdown(dd)) dd.close();
|
if (isClosableDropdown(dd)) dd.close();
|
||||||
});
|
}, { signal });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,3 +306,10 @@ document.body.addEventListener('htmx:afterSwap', (e: Event) => {
|
|||||||
const target = (e as CustomEvent).detail?.target as HTMLElement | null;
|
const target = (e as CustomEvent).detail?.target as HTMLElement | null;
|
||||||
if (target?.querySelector('[data-video-player]')) initPlayer();
|
if (target?.querySelector('[data-video-player]')) initPlayer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('htmx:beforeSwap', (e: Event) => {
|
||||||
|
const target = (e as CustomEvent).detail?.target as HTMLElement | null;
|
||||||
|
if (target && currentContainer && target.contains(currentContainer)) {
|
||||||
|
teardownPlayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { displayTimeFromAbsolute } from './timeline';
|
|||||||
import { showControls } from './controls';
|
import { showControls } from './controls';
|
||||||
import { updateSubtitleOptions } from './subtitles';
|
import { updateSubtitleOptions } from './subtitles';
|
||||||
import { updateQualityOptions } from './quality';
|
import { updateQualityOptions } from './quality';
|
||||||
|
import { safeLocalStorage } from './storage';
|
||||||
|
|
||||||
// builds stream URL with mode, token, and optional quality param
|
// builds stream URL with mode, token, and optional quality param
|
||||||
const streamUrlForMode = (mode: string, quality?: string): string => {
|
const streamUrlForMode = (mode: string, quality?: string): string => {
|
||||||
@@ -33,7 +34,7 @@ const loadVideo = (url: string): void => {
|
|||||||
export const switchMode = (mode: string): void => {
|
export const switchMode = (mode: string): void => {
|
||||||
if (!state.availableModes.includes(mode) || mode === state.currentMode) return;
|
if (!state.availableModes.includes(mode) || mode === state.currentMode) return;
|
||||||
state.currentMode = mode;
|
state.currentMode = mode;
|
||||||
localStorage.setItem('player-audio-mode', mode);
|
safeLocalStorage.setItem('player-audio-mode', mode);
|
||||||
const qualitySelect = state.container.querySelector(
|
const qualitySelect = state.container.querySelector(
|
||||||
'[data-quality-select]'
|
'[data-quality-select]'
|
||||||
) as HTMLSelectElement | null;
|
) as HTMLSelectElement | null;
|
||||||
@@ -91,7 +92,7 @@ export const setupMode = (): void => {
|
|||||||
|
|
||||||
const autoplayBtn = document.querySelector('[data-autoplay]') as HTMLInputElement | null;
|
const autoplayBtn = document.querySelector('[data-autoplay]') as HTMLInputElement | null;
|
||||||
autoplayBtn?.addEventListener('change', e => {
|
autoplayBtn?.addEventListener('change', e => {
|
||||||
localStorage.setItem(
|
safeLocalStorage.setItem(
|
||||||
'mal:autoplay-enabled',
|
'mal:autoplay-enabled',
|
||||||
(e.target as HTMLInputElement).checked ? 'true' : 'false'
|
(e.target as HTMLInputElement).checked ? 'true' : 'false'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import { displayTimeFromAbsolute } from './timeline';
|
import { displayTimeFromAbsolute } from './timeline';
|
||||||
|
import { safeLocalStorage } from './storage';
|
||||||
|
|
||||||
// same as mode.ts - could be extracted to shared util
|
// same as mode.ts - could be extracted to shared util
|
||||||
const streamUrlForMode = (mode: string, quality?: string): string => {
|
const streamUrlForMode = (mode: string, quality?: string): string => {
|
||||||
@@ -29,7 +30,7 @@ const loadVideo = (url: string): void => {
|
|||||||
export const switchQuality = (quality: string): void => {
|
export const switchQuality = (quality: string): void => {
|
||||||
const url = streamUrlForMode(state.currentMode, quality);
|
const url = streamUrlForMode(state.currentMode, quality);
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
localStorage.setItem('mal:preferred-quality', quality);
|
safeLocalStorage.setItem('mal:preferred-quality', quality);
|
||||||
loadVideo(url);
|
loadVideo(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ export const updateQualityOptions = (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// restore saved preference
|
// restore saved preference
|
||||||
const preferred = localStorage.getItem('mal:preferred-quality') || 'best';
|
const preferred = safeLocalStorage.getItem('mal:preferred-quality') || 'best';
|
||||||
select.value = qualities.includes(preferred) ? preferred : 'best';
|
select.value = qualities.includes(preferred) ? preferred : 'best';
|
||||||
|
|
||||||
// hide if no quality options
|
// hide if no quality options
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { state } from '../state';
|
|||||||
import { displayTimeFromAbsolute, absoluteTimeFromDisplay } from '../timeline';
|
import { displayTimeFromAbsolute, absoluteTimeFromDisplay } from '../timeline';
|
||||||
import { showControls } from '../controls';
|
import { showControls } from '../controls';
|
||||||
import { saveProgress } from '../progress';
|
import { saveProgress } from '../progress';
|
||||||
|
import { safeLocalStorage } from '../storage';
|
||||||
|
|
||||||
// button label based on segment type
|
// button label based on segment type
|
||||||
const skipLabel = (type: string): string => (type === 'ed' ? 'Skip outro' : 'Skip intro');
|
const skipLabel = (type: string): string => (type === 'ed' ? 'Skip outro' : 'Skip intro');
|
||||||
@@ -27,7 +28,7 @@ export const updateSkipButton = (currentTime: number): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// auto-skip: jump to end if enabled
|
// auto-skip: jump to end if enabled
|
||||||
const autoSkip = localStorage.getItem('mal:autoskip-enabled') === 'true';
|
const autoSkip = safeLocalStorage.getItem('mal:autoskip-enabled') === 'true';
|
||||||
if (autoSkip && displayTime >= segment.start && displayTime < segment.end) {
|
if (autoSkip && displayTime >= segment.start && displayTime < segment.end) {
|
||||||
state.video.currentTime = absoluteTimeFromDisplay(segment.end + 0.01);
|
state.video.currentTime = absoluteTimeFromDisplay(segment.end + 0.01);
|
||||||
void saveProgress();
|
void saveProgress();
|
||||||
@@ -49,7 +50,7 @@ export const updateSkipButton = (currentTime: number): void => {
|
|||||||
export const updateAutoSkipButton = (): void => {
|
export const updateAutoSkipButton = (): void => {
|
||||||
const btn = document.querySelector('[data-autoskip]') as HTMLInputElement | null;
|
const btn = document.querySelector('[data-autoskip]') as HTMLInputElement | null;
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
btn.checked = localStorage.getItem('mal:autoskip-enabled') === 'true';
|
btn.checked = safeLocalStorage.getItem('mal:autoskip-enabled') === 'true';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,7 +60,7 @@ export const setupSkip = (): void => {
|
|||||||
document.addEventListener('change', e => {
|
document.addEventListener('change', e => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (target.hasAttribute('data-autoskip')) {
|
if (target.hasAttribute('data-autoskip')) {
|
||||||
localStorage.setItem(
|
safeLocalStorage.setItem(
|
||||||
'mal:autoskip-enabled',
|
'mal:autoskip-enabled',
|
||||||
(target as HTMLInputElement).checked ? 'true' : 'false'
|
(target as HTMLInputElement).checked ? 'true' : 'false'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ModeSource, SkipSegment, SubtitleCue, SubtitleTrack, ActiveSegment } from './types';
|
import type { ModeSource, SkipSegment, SubtitleCue, SubtitleTrack, ActiveSegment } from './types';
|
||||||
import { q, qs, dataset } from '../q';
|
import { q, qs, dataset } from '../q';
|
||||||
|
import { safeLocalStorage } from './storage';
|
||||||
|
|
||||||
export interface PlayerState {
|
export interface PlayerState {
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
@@ -169,7 +170,7 @@ export const initState = (c: HTMLElement): boolean => {
|
|||||||
// resolve initial mode: localStorage > backend default > first available > 'dub'
|
// resolve initial mode: localStorage > backend default > first available > 'dub'
|
||||||
const backendInitialMode = dataset(c, 'initialMode') || 'dub';
|
const backendInitialMode = dataset(c, 'initialMode') || 'dub';
|
||||||
state.modeSwitchedFrom = dataset(c, 'modeSwitchedFrom') || '';
|
state.modeSwitchedFrom = dataset(c, 'modeSwitchedFrom') || '';
|
||||||
const storedMode = localStorage.getItem('player-audio-mode');
|
const storedMode = safeLocalStorage.getItem('player-audio-mode');
|
||||||
const initialMode =
|
const initialMode =
|
||||||
storedMode && state.availableModes.includes(storedMode) ? storedMode : backendInitialMode;
|
storedMode && state.availableModes.includes(storedMode) ? storedMode : backendInitialMode;
|
||||||
const fallbackMode = Object.keys(state.modeSources).find(m => state.modeSources[m]?.token);
|
const fallbackMode = Object.keys(state.modeSources).find(m => state.modeSources[m]?.token);
|
||||||
|
|||||||
40
static/player/storage.ts
Normal file
40
static/player/storage.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
|
||||||
|
|
||||||
|
const getLocalStorage = (): StorageLike | null => {
|
||||||
|
try {
|
||||||
|
return window.localStorage;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const safeLocalStorage = {
|
||||||
|
getItem(key: string): string | null {
|
||||||
|
const storage = getLocalStorage();
|
||||||
|
if (!storage) return null;
|
||||||
|
try {
|
||||||
|
return storage.getItem(key);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setItem(key: string, value: string): void {
|
||||||
|
const storage = getLocalStorage();
|
||||||
|
if (!storage) return;
|
||||||
|
try {
|
||||||
|
storage.setItem(key, value);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeItem(key: string): void {
|
||||||
|
const storage = getLocalStorage();
|
||||||
|
if (!storage) return;
|
||||||
|
try {
|
||||||
|
storage.removeItem(key);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user