vidply 1.2.0 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +974 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-JYYLK5KB.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-JYYLK5KB.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-47RCKQF5.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-VEJ4HDHO.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-VEJ4HDHO.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-C2UVRPZY.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-C2UVRPZY.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZVBK2TNQ.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZVBK2TNQ.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-HLRQUOCU.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-FLUCBW5H.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-FLUCBW5H.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-MGMAJ7NK.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-MGMAJ7NK.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-Y2RYXHJW.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-Y2RYXHJW.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-RQFQD7OE.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-RQFQD7OE.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-EU2BARZF.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-EU2BARZF.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-6LYZYI5G.js} +3 -3
- package/dist/dev/vidply.chunk-6LYZYI5G.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-A4ESEOQ2.js} +6 -6
- package/dist/dev/vidply.chunk-A4ESEOQ2.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-CGYIED2I.js} +2 -2
- package/dist/dev/vidply.chunk-CGYIED2I.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-HZ7UOKOU.js} +3 -3
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-I6EDRYGJ.js} +33 -6
- package/dist/dev/vidply.chunk-I6EDRYGJ.js.map +7 -0
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-JKMQGLCJ.js} +5 -5
- package/dist/dev/vidply.chunk-JKMQGLCJ.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-JZNZX32Z.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-P25MRJ6O.js} +2 -2
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-PRFNESYA.js} +2 -2
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-UI3RYCVF.js} +4 -3
- package/dist/dev/vidply.chunk-UI3RYCVF.js.map +7 -0
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-UNFGMABP.js} +2 -2
- package/dist/dev/vidply.chunk-UNFGMABP.js.map +7 -0
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-VAUVSQXC.js} +29 -2
- package/dist/dev/vidply.de-VAUVSQXC.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-G65HFWO6.js} +29 -2
- package/dist/dev/vidply.es-G65HFWO6.js.map +7 -0
- package/dist/dev/vidply.esm.js +648 -35
- package/dist/dev/vidply.esm.js.map +3 -3
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-MJNOZELM.js} +29 -2
- package/dist/dev/vidply.fr-MJNOZELM.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-INMDHIYJ.js} +29 -2
- package/dist/dev/vidply.ja-INMDHIYJ.js.map +7 -0
- package/dist/legacy/vidply.js +1062 -34
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +127 -55
- package/dist/prod/vidply.AudioDescriptionManager-EKWUNOPE.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-JTWSNHRQ.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-CIDAHFWM.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-VZXHPCPM.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-Q6EFYKID.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-XRDJUTOO.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-JUOSA5AM.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-7ODD46PO.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-4B2QKYGQ.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-MR3UYLY7.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-ITRTC7NR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-3EEDXU4T.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-5HTX5QXW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-6OGHHIYP.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-GUPGMFUZ.min.js} +2 -2
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-MWEBITCP.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-PL3P3JHR.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-QQZGV7TD.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-U5IRRYG5.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-VKHV4QHA.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-XA4M574A.min.js} +1 -1
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-ZPTV4JEP.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-UZHFCEZU.min.js} +2 -2
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-BOISUV2S.min.js} +2 -2
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-ICSMKPFM.min.js} +2 -2
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-VQ2HKGAY.min.js} +2 -2
- package/dist/vidply.css +5852 -5796
- package/dist/vidply.esm.min.meta.json +204 -149
- package/dist/vidply.min.css +1 -1
- package/package.json +11 -11
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4191 -4159
- package/src/controls/KeyboardHelp.ts +326 -0
- package/src/controls/KeyboardManager.ts +387 -383
- package/src/controls/SettingsDialog.ts +488 -488
- package/src/controls/TranscriptManager.ts +2426 -2426
- package/src/core/AudioDescriptionManager.ts +863 -774
- package/src/core/DescriptionSpeechManager.ts +232 -0
- package/src/core/FloatingPlayerManager.ts +674 -674
- package/src/core/LazyInit.ts +80 -80
- package/src/core/MediaSessionManager.ts +341 -0
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2973 -2872
- package/src/core/PosterManager.ts +169 -169
- package/src/core/PseudoFullscreen.ts +179 -179
- package/src/core/ResponsiveManager.ts +231 -231
- package/src/core/ResumeManager.ts +236 -236
- package/src/core/SignLanguageManager.ts +1308 -1308
- package/src/core/ThemeManager.ts +157 -157
- package/src/features/PlaylistManager.ts +1838 -1827
- package/src/i18n/i18n.ts +223 -223
- package/src/i18n/languages/de.ts +244 -217
- package/src/i18n/languages/en.ts +245 -218
- package/src/i18n/languages/es.ts +241 -214
- package/src/i18n/languages/fr.ts +241 -214
- package/src/i18n/languages/ja.ts +241 -214
- package/src/icons/Icons.ts +168 -167
- package/src/index.ts +196 -193
- package/src/renderers/DASHRenderer.ts +1077 -1077
- package/src/renderers/HLSRenderer.ts +944 -944
- package/src/renderers/HTML5Renderer.ts +389 -389
- package/src/renderers/SoundCloudRenderer.ts +437 -437
- package/src/renderers/VimeoRenderer.ts +298 -298
- package/src/renderers/YouTubeRenderer.ts +290 -290
- package/src/styles/vidply.css +5852 -5796
- package/src/types/events.ts +128 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +199 -182
- package/src/types/renderer.ts +58 -58
- package/src/utils/DOMUtils.ts +281 -281
- package/src/utils/DownloadInfo.ts +171 -171
- package/src/utils/DraggablePanel.ts +539 -539
- package/src/utils/DraggablePanelMenu.ts +195 -195
- package/src/utils/DraggableResizable.ts +997 -997
- package/src/utils/EventEmitter.ts +59 -59
- package/src/utils/FormUtils.ts +95 -95
- package/src/utils/MenuFactory.ts +483 -483
- package/src/utils/Sanitize.ts +70 -70
- package/src/utils/SettingsMenuFactory.ts +289 -289
- package/src/utils/StorageManager.ts +287 -287
- package/src/utils/TrackLabelUtils.ts +52 -52
- package/src/utils/UrlSafe.ts +69 -69
- package/src/utils/VideoFrameCapture.ts +114 -114
- package/src/utils/WindowComponents.ts +250 -250
- package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
- package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
- package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
- package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
- package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
- package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
- package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
- package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
- package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
- package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
- package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
- package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
- package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
- package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
- package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
- package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
- package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
- package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
- package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
- package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
- package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
- package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
- package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
- /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-47RCKQF5.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-HLRQUOCU.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-HZ7UOKOU.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-JZNZX32Z.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-P25MRJ6O.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-PRFNESYA.js.map} +0 -0
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
import { shallowSanitize } from './Sanitize.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Type guard helpers used by the validated `get()` overload below.
|
|
5
|
-
* Each guard accepts an unknown JSON value and tells TypeScript whether
|
|
6
|
-
* it matches the expected runtime shape.
|
|
7
|
-
*/
|
|
8
|
-
export type Validator<T> = (value: unknown) => value is T;
|
|
9
|
-
|
|
10
|
-
interface WatchProgressEntry {
|
|
11
|
-
currentTime: number;
|
|
12
|
-
duration: number;
|
|
13
|
-
percentage: number;
|
|
14
|
-
updatedAt: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Loose alias for stored preference payloads. Numeric/string-typed fields
|
|
19
|
-
* are validated at the boundary (`isPlainObject`) and clamped where it
|
|
20
|
-
* matters (volume, playbackSpeed). Individual properties remain `unknown`
|
|
21
|
-
* so callers must narrow before use.
|
|
22
|
-
*/
|
|
23
|
-
export type StoredPreferences = Record<string, unknown>;
|
|
24
|
-
|
|
25
|
-
function clamp(n: number, min: number, max: number): number {
|
|
26
|
-
if (!Number.isFinite(n)) return min;
|
|
27
|
-
if (n < min) return min;
|
|
28
|
-
if (n > max) return max;
|
|
29
|
-
return n;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isFiniteNonNegative(value: unknown): value is number {
|
|
33
|
-
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isWatchProgressEntry(value: unknown): value is WatchProgressEntry {
|
|
37
|
-
if (!value || typeof value !== 'object') return false;
|
|
38
|
-
const v = value as Record<string, unknown>;
|
|
39
|
-
return (
|
|
40
|
-
isFiniteNonNegative(v.currentTime) &&
|
|
41
|
-
isFiniteNonNegative(v.duration) &&
|
|
42
|
-
typeof v.percentage === 'number' &&
|
|
43
|
-
Number.isFinite(v.percentage) &&
|
|
44
|
-
typeof v.updatedAt === 'number' &&
|
|
45
|
-
Number.isFinite(v.updatedAt)
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export class StorageManager {
|
|
50
|
-
private namespace: string;
|
|
51
|
-
private storage: Storage | null;
|
|
52
|
-
|
|
53
|
-
static MAX_WATCH_PROGRESS_ENTRIES = 100;
|
|
54
|
-
|
|
55
|
-
constructor(namespace = 'vidply') {
|
|
56
|
-
this.namespace = namespace;
|
|
57
|
-
this.storage = this.isStorageAvailable() ? localStorage : null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* `localStorage` access can throw in private-browsing modes (Safari) and
|
|
62
|
-
* is undefined in non-DOM environments. Both are tolerated here so the
|
|
63
|
-
* Player still works (without persistence) when storage is unavailable.
|
|
64
|
-
*/
|
|
65
|
-
isStorageAvailable(): boolean {
|
|
66
|
-
try {
|
|
67
|
-
if (typeof localStorage === 'undefined') return false;
|
|
68
|
-
const test = '__storage_test__';
|
|
69
|
-
localStorage.setItem(test, test);
|
|
70
|
-
localStorage.removeItem(test);
|
|
71
|
-
return true;
|
|
72
|
-
} catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
getKey(key: string): string {
|
|
78
|
-
return `${this.namespace}_${key}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
set(key: string, value: unknown): boolean {
|
|
82
|
-
if (!this.storage) return false;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const namespacedKey = this.getKey(key);
|
|
86
|
-
this.storage.setItem(namespacedKey, JSON.stringify(value));
|
|
87
|
-
return true;
|
|
88
|
-
} catch (e) {
|
|
89
|
-
console.warn('Failed to save to localStorage:', e);
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Generic get. Accepts an optional `validator` so callers can assert the
|
|
96
|
-
* runtime shape of the parsed JSON before trusting it. Falls back to
|
|
97
|
-
* `defaultValue` if the payload fails validation.
|
|
98
|
-
*/
|
|
99
|
-
get<T = unknown>(key: string, defaultValue: T | null = null, validator?: Validator<T>): T | null {
|
|
100
|
-
if (!this.storage) return defaultValue;
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const namespacedKey = this.getKey(key);
|
|
104
|
-
const raw = this.storage.getItem(namespacedKey);
|
|
105
|
-
if (raw === null) return defaultValue;
|
|
106
|
-
const parsed: unknown = JSON.parse(raw);
|
|
107
|
-
if (validator && !validator(parsed)) {
|
|
108
|
-
console.warn(`[VidPly] Discarding malformed localStorage payload for "${key}"`);
|
|
109
|
-
return defaultValue;
|
|
110
|
-
}
|
|
111
|
-
return parsed as T;
|
|
112
|
-
} catch (e) {
|
|
113
|
-
console.warn('Failed to read from localStorage:', e);
|
|
114
|
-
return defaultValue;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
remove(key: string): boolean {
|
|
119
|
-
if (!this.storage) return false;
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const namespacedKey = this.getKey(key);
|
|
123
|
-
this.storage.removeItem(namespacedKey);
|
|
124
|
-
return true;
|
|
125
|
-
} catch (e) {
|
|
126
|
-
console.warn('Failed to remove from localStorage:', e);
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
clear(): boolean {
|
|
132
|
-
if (!this.storage) return false;
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const storage = this.storage;
|
|
136
|
-
const keys = Object.keys(storage);
|
|
137
|
-
keys.forEach(key => {
|
|
138
|
-
if (key.startsWith(this.namespace)) {
|
|
139
|
-
storage.removeItem(key);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
return true;
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.warn('Failed to clear localStorage:', e);
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
saveTranscriptPreferences(preferences: StoredPreferences): boolean {
|
|
150
|
-
return this.set('transcript_preferences', shallowSanitize(preferences));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
getTranscriptPreferences(): StoredPreferences | null {
|
|
154
|
-
return this.get<StoredPreferences>(
|
|
155
|
-
'transcript_preferences',
|
|
156
|
-
null,
|
|
157
|
-
isPlainObject as Validator<StoredPreferences>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
saveCaptionPreferences(preferences: StoredPreferences): boolean {
|
|
162
|
-
return this.set('caption_preferences', shallowSanitize(preferences));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
getCaptionPreferences(): StoredPreferences | null {
|
|
166
|
-
return this.get<StoredPreferences>('caption_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
savePlayerPreferences(preferences: StoredPreferences): boolean {
|
|
170
|
-
// Numeric ranges are clamped here so a tampered storage payload cannot
|
|
171
|
-
// ship a `volume: 1e9` straight to a renderer.
|
|
172
|
-
const sanitized = shallowSanitize(preferences);
|
|
173
|
-
if (typeof sanitized.volume === 'number') {
|
|
174
|
-
sanitized.volume = clamp(sanitized.volume, 0, 1);
|
|
175
|
-
}
|
|
176
|
-
if (typeof sanitized.playbackSpeed === 'number') {
|
|
177
|
-
sanitized.playbackSpeed = clamp(sanitized.playbackSpeed, 0.1, 4);
|
|
178
|
-
}
|
|
179
|
-
return this.set('player_preferences', sanitized);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
getPlayerPreferences(): StoredPreferences | null {
|
|
183
|
-
const value = this.get<StoredPreferences>('player_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
184
|
-
if (!value) return null;
|
|
185
|
-
if (typeof value.volume === 'number') {
|
|
186
|
-
value.volume = clamp(value.volume, 0, 1);
|
|
187
|
-
}
|
|
188
|
-
if (typeof value.playbackSpeed === 'number') {
|
|
189
|
-
value.playbackSpeed = clamp(value.playbackSpeed, 0.1, 4);
|
|
190
|
-
}
|
|
191
|
-
return value;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
saveSignLanguagePreferences(preferences: StoredPreferences): boolean {
|
|
195
|
-
return this.set('sign_language_preferences', shallowSanitize(preferences));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
getSignLanguagePreferences(): StoredPreferences | null {
|
|
199
|
-
return this.get<StoredPreferences>('sign_language_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
saveFloatingPreferences(preferences: StoredPreferences): boolean {
|
|
203
|
-
return this.set('floating_preferences', shallowSanitize(preferences));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
getFloatingPreferences(): StoredPreferences | null {
|
|
207
|
-
return this.get<StoredPreferences>('floating_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Persist watch progress for a video id. Numeric inputs are validated +
|
|
212
|
-
* clamped so a caller cannot poison the store with `Infinity`/negatives.
|
|
213
|
-
*/
|
|
214
|
-
saveWatchProgress(videoId: string, currentTime: number, duration: number): boolean {
|
|
215
|
-
if (typeof videoId !== 'string' || !videoId) return false;
|
|
216
|
-
if (!Number.isFinite(currentTime) || !Number.isFinite(duration) || duration <= 0) return false;
|
|
217
|
-
|
|
218
|
-
const safeDuration = clamp(duration, 0.001, 24 * 60 * 60); // cap to 24h to neutralize gigantic numbers
|
|
219
|
-
const safeCurrent = clamp(currentTime, 0, safeDuration);
|
|
220
|
-
|
|
221
|
-
const allProgress = this.get<Record<string, WatchProgressEntry>>(
|
|
222
|
-
'watch_progress',
|
|
223
|
-
Object.create(null),
|
|
224
|
-
isWatchProgressMap
|
|
225
|
-
) ?? Object.create(null) as Record<string, WatchProgressEntry>;
|
|
226
|
-
|
|
227
|
-
const percentage = (safeCurrent / safeDuration) * 100;
|
|
228
|
-
|
|
229
|
-
allProgress[videoId] = {
|
|
230
|
-
currentTime: safeCurrent,
|
|
231
|
-
duration: safeDuration,
|
|
232
|
-
percentage: clamp(percentage, 0, 100),
|
|
233
|
-
updatedAt: Date.now()
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const entries = Object.entries(allProgress);
|
|
237
|
-
if (entries.length > StorageManager.MAX_WATCH_PROGRESS_ENTRIES) {
|
|
238
|
-
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
|
239
|
-
const toRemove = entries.length - StorageManager.MAX_WATCH_PROGRESS_ENTRIES;
|
|
240
|
-
for (let i = 0; i < toRemove; i++) {
|
|
241
|
-
const entry = entries[i];
|
|
242
|
-
if (entry) {
|
|
243
|
-
delete allProgress[entry[0]];
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return this.set('watch_progress', allProgress);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
getWatchProgress(videoId: string): WatchProgressEntry | null {
|
|
252
|
-
if (!videoId) return null;
|
|
253
|
-
const allProgress = this.get<Record<string, WatchProgressEntry>>(
|
|
254
|
-
'watch_progress',
|
|
255
|
-
Object.create(null),
|
|
256
|
-
isWatchProgressMap
|
|
257
|
-
) ?? Object.create(null) as Record<string, WatchProgressEntry>;
|
|
258
|
-
const entry = allProgress[videoId];
|
|
259
|
-
return entry && isWatchProgressEntry(entry) ? entry : null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
clearWatchProgress(videoId: string): boolean {
|
|
263
|
-
if (!videoId) return false;
|
|
264
|
-
const allProgress = this.get<Record<string, WatchProgressEntry>>(
|
|
265
|
-
'watch_progress',
|
|
266
|
-
Object.create(null),
|
|
267
|
-
isWatchProgressMap
|
|
268
|
-
) ?? Object.create(null) as Record<string, WatchProgressEntry>;
|
|
269
|
-
if (allProgress[videoId]) {
|
|
270
|
-
delete allProgress[videoId];
|
|
271
|
-
return this.set('watch_progress', allProgress);
|
|
272
|
-
}
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
278
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function isWatchProgressMap(value: unknown): value is Record<string, WatchProgressEntry> {
|
|
282
|
-
if (!isPlainObject(value)) return false;
|
|
283
|
-
for (const entry of Object.values(value)) {
|
|
284
|
-
if (!isWatchProgressEntry(entry)) return false;
|
|
285
|
-
}
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
1
|
+
import { shallowSanitize } from './Sanitize.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard helpers used by the validated `get()` overload below.
|
|
5
|
+
* Each guard accepts an unknown JSON value and tells TypeScript whether
|
|
6
|
+
* it matches the expected runtime shape.
|
|
7
|
+
*/
|
|
8
|
+
export type Validator<T> = (value: unknown) => value is T;
|
|
9
|
+
|
|
10
|
+
interface WatchProgressEntry {
|
|
11
|
+
currentTime: number;
|
|
12
|
+
duration: number;
|
|
13
|
+
percentage: number;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Loose alias for stored preference payloads. Numeric/string-typed fields
|
|
19
|
+
* are validated at the boundary (`isPlainObject`) and clamped where it
|
|
20
|
+
* matters (volume, playbackSpeed). Individual properties remain `unknown`
|
|
21
|
+
* so callers must narrow before use.
|
|
22
|
+
*/
|
|
23
|
+
export type StoredPreferences = Record<string, unknown>;
|
|
24
|
+
|
|
25
|
+
function clamp(n: number, min: number, max: number): number {
|
|
26
|
+
if (!Number.isFinite(n)) return min;
|
|
27
|
+
if (n < min) return min;
|
|
28
|
+
if (n > max) return max;
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isFiniteNonNegative(value: unknown): value is number {
|
|
33
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isWatchProgressEntry(value: unknown): value is WatchProgressEntry {
|
|
37
|
+
if (!value || typeof value !== 'object') return false;
|
|
38
|
+
const v = value as Record<string, unknown>;
|
|
39
|
+
return (
|
|
40
|
+
isFiniteNonNegative(v.currentTime) &&
|
|
41
|
+
isFiniteNonNegative(v.duration) &&
|
|
42
|
+
typeof v.percentage === 'number' &&
|
|
43
|
+
Number.isFinite(v.percentage) &&
|
|
44
|
+
typeof v.updatedAt === 'number' &&
|
|
45
|
+
Number.isFinite(v.updatedAt)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class StorageManager {
|
|
50
|
+
private namespace: string;
|
|
51
|
+
private storage: Storage | null;
|
|
52
|
+
|
|
53
|
+
static MAX_WATCH_PROGRESS_ENTRIES = 100;
|
|
54
|
+
|
|
55
|
+
constructor(namespace = 'vidply') {
|
|
56
|
+
this.namespace = namespace;
|
|
57
|
+
this.storage = this.isStorageAvailable() ? localStorage : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `localStorage` access can throw in private-browsing modes (Safari) and
|
|
62
|
+
* is undefined in non-DOM environments. Both are tolerated here so the
|
|
63
|
+
* Player still works (without persistence) when storage is unavailable.
|
|
64
|
+
*/
|
|
65
|
+
isStorageAvailable(): boolean {
|
|
66
|
+
try {
|
|
67
|
+
if (typeof localStorage === 'undefined') return false;
|
|
68
|
+
const test = '__storage_test__';
|
|
69
|
+
localStorage.setItem(test, test);
|
|
70
|
+
localStorage.removeItem(test);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getKey(key: string): string {
|
|
78
|
+
return `${this.namespace}_${key}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set(key: string, value: unknown): boolean {
|
|
82
|
+
if (!this.storage) return false;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const namespacedKey = this.getKey(key);
|
|
86
|
+
this.storage.setItem(namespacedKey, JSON.stringify(value));
|
|
87
|
+
return true;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.warn('Failed to save to localStorage:', e);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generic get. Accepts an optional `validator` so callers can assert the
|
|
96
|
+
* runtime shape of the parsed JSON before trusting it. Falls back to
|
|
97
|
+
* `defaultValue` if the payload fails validation.
|
|
98
|
+
*/
|
|
99
|
+
get<T = unknown>(key: string, defaultValue: T | null = null, validator?: Validator<T>): T | null {
|
|
100
|
+
if (!this.storage) return defaultValue;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const namespacedKey = this.getKey(key);
|
|
104
|
+
const raw = this.storage.getItem(namespacedKey);
|
|
105
|
+
if (raw === null) return defaultValue;
|
|
106
|
+
const parsed: unknown = JSON.parse(raw);
|
|
107
|
+
if (validator && !validator(parsed)) {
|
|
108
|
+
console.warn(`[VidPly] Discarding malformed localStorage payload for "${key}"`);
|
|
109
|
+
return defaultValue;
|
|
110
|
+
}
|
|
111
|
+
return parsed as T;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.warn('Failed to read from localStorage:', e);
|
|
114
|
+
return defaultValue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
remove(key: string): boolean {
|
|
119
|
+
if (!this.storage) return false;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const namespacedKey = this.getKey(key);
|
|
123
|
+
this.storage.removeItem(namespacedKey);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.warn('Failed to remove from localStorage:', e);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
clear(): boolean {
|
|
132
|
+
if (!this.storage) return false;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const storage = this.storage;
|
|
136
|
+
const keys = Object.keys(storage);
|
|
137
|
+
keys.forEach(key => {
|
|
138
|
+
if (key.startsWith(this.namespace)) {
|
|
139
|
+
storage.removeItem(key);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return true;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.warn('Failed to clear localStorage:', e);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
saveTranscriptPreferences(preferences: StoredPreferences): boolean {
|
|
150
|
+
return this.set('transcript_preferences', shallowSanitize(preferences));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getTranscriptPreferences(): StoredPreferences | null {
|
|
154
|
+
return this.get<StoredPreferences>(
|
|
155
|
+
'transcript_preferences',
|
|
156
|
+
null,
|
|
157
|
+
isPlainObject as Validator<StoredPreferences>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
saveCaptionPreferences(preferences: StoredPreferences): boolean {
|
|
162
|
+
return this.set('caption_preferences', shallowSanitize(preferences));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getCaptionPreferences(): StoredPreferences | null {
|
|
166
|
+
return this.get<StoredPreferences>('caption_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
savePlayerPreferences(preferences: StoredPreferences): boolean {
|
|
170
|
+
// Numeric ranges are clamped here so a tampered storage payload cannot
|
|
171
|
+
// ship a `volume: 1e9` straight to a renderer.
|
|
172
|
+
const sanitized = shallowSanitize(preferences);
|
|
173
|
+
if (typeof sanitized.volume === 'number') {
|
|
174
|
+
sanitized.volume = clamp(sanitized.volume, 0, 1);
|
|
175
|
+
}
|
|
176
|
+
if (typeof sanitized.playbackSpeed === 'number') {
|
|
177
|
+
sanitized.playbackSpeed = clamp(sanitized.playbackSpeed, 0.1, 4);
|
|
178
|
+
}
|
|
179
|
+
return this.set('player_preferences', sanitized);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getPlayerPreferences(): StoredPreferences | null {
|
|
183
|
+
const value = this.get<StoredPreferences>('player_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
184
|
+
if (!value) return null;
|
|
185
|
+
if (typeof value.volume === 'number') {
|
|
186
|
+
value.volume = clamp(value.volume, 0, 1);
|
|
187
|
+
}
|
|
188
|
+
if (typeof value.playbackSpeed === 'number') {
|
|
189
|
+
value.playbackSpeed = clamp(value.playbackSpeed, 0.1, 4);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
saveSignLanguagePreferences(preferences: StoredPreferences): boolean {
|
|
195
|
+
return this.set('sign_language_preferences', shallowSanitize(preferences));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getSignLanguagePreferences(): StoredPreferences | null {
|
|
199
|
+
return this.get<StoredPreferences>('sign_language_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
saveFloatingPreferences(preferences: StoredPreferences): boolean {
|
|
203
|
+
return this.set('floating_preferences', shallowSanitize(preferences));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getFloatingPreferences(): StoredPreferences | null {
|
|
207
|
+
return this.get<StoredPreferences>('floating_preferences', null, isPlainObject as Validator<StoredPreferences>);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Persist watch progress for a video id. Numeric inputs are validated +
|
|
212
|
+
* clamped so a caller cannot poison the store with `Infinity`/negatives.
|
|
213
|
+
*/
|
|
214
|
+
saveWatchProgress(videoId: string, currentTime: number, duration: number): boolean {
|
|
215
|
+
if (typeof videoId !== 'string' || !videoId) return false;
|
|
216
|
+
if (!Number.isFinite(currentTime) || !Number.isFinite(duration) || duration <= 0) return false;
|
|
217
|
+
|
|
218
|
+
const safeDuration = clamp(duration, 0.001, 24 * 60 * 60); // cap to 24h to neutralize gigantic numbers
|
|
219
|
+
const safeCurrent = clamp(currentTime, 0, safeDuration);
|
|
220
|
+
|
|
221
|
+
const allProgress = this.get<Record<string, WatchProgressEntry>>(
|
|
222
|
+
'watch_progress',
|
|
223
|
+
Object.create(null),
|
|
224
|
+
isWatchProgressMap
|
|
225
|
+
) ?? Object.create(null) as Record<string, WatchProgressEntry>;
|
|
226
|
+
|
|
227
|
+
const percentage = (safeCurrent / safeDuration) * 100;
|
|
228
|
+
|
|
229
|
+
allProgress[videoId] = {
|
|
230
|
+
currentTime: safeCurrent,
|
|
231
|
+
duration: safeDuration,
|
|
232
|
+
percentage: clamp(percentage, 0, 100),
|
|
233
|
+
updatedAt: Date.now()
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const entries = Object.entries(allProgress);
|
|
237
|
+
if (entries.length > StorageManager.MAX_WATCH_PROGRESS_ENTRIES) {
|
|
238
|
+
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
|
239
|
+
const toRemove = entries.length - StorageManager.MAX_WATCH_PROGRESS_ENTRIES;
|
|
240
|
+
for (let i = 0; i < toRemove; i++) {
|
|
241
|
+
const entry = entries[i];
|
|
242
|
+
if (entry) {
|
|
243
|
+
delete allProgress[entry[0]];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return this.set('watch_progress', allProgress);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getWatchProgress(videoId: string): WatchProgressEntry | null {
|
|
252
|
+
if (!videoId) return null;
|
|
253
|
+
const allProgress = this.get<Record<string, WatchProgressEntry>>(
|
|
254
|
+
'watch_progress',
|
|
255
|
+
Object.create(null),
|
|
256
|
+
isWatchProgressMap
|
|
257
|
+
) ?? Object.create(null) as Record<string, WatchProgressEntry>;
|
|
258
|
+
const entry = allProgress[videoId];
|
|
259
|
+
return entry && isWatchProgressEntry(entry) ? entry : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
clearWatchProgress(videoId: string): boolean {
|
|
263
|
+
if (!videoId) return false;
|
|
264
|
+
const allProgress = this.get<Record<string, WatchProgressEntry>>(
|
|
265
|
+
'watch_progress',
|
|
266
|
+
Object.create(null),
|
|
267
|
+
isWatchProgressMap
|
|
268
|
+
) ?? Object.create(null) as Record<string, WatchProgressEntry>;
|
|
269
|
+
if (allProgress[videoId]) {
|
|
270
|
+
delete allProgress[videoId];
|
|
271
|
+
return this.set('watch_progress', allProgress);
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
278
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isWatchProgressMap(value: unknown): value is Record<string, WatchProgressEntry> {
|
|
282
|
+
if (!isPlainObject(value)) return false;
|
|
283
|
+
for (const entry of Object.values(value)) {
|
|
284
|
+
if (!isWatchProgressEntry(entry)) return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|