Spaces:
Paused
Paused
| const STORAGE_KEY = 'shadowing-progress' | |
| const META_KEY = 'shadowing-video-meta' | |
| /** | |
| * @typedef {Object} ShadowingProgress | |
| * @property {string} video_id | |
| * @property {number} currentSegment | |
| * @property {number[]} dictationScores | |
| * @property {number[]} pronunciationScores | |
| * @property {number[]} flaggedSegments | |
| */ | |
| export function loadProgress(videoId) { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY) | |
| const all = raw ? JSON.parse(raw) : {} | |
| return all[videoId] || { | |
| video_id: videoId, | |
| currentSegment: 0, | |
| dictationScores: [], | |
| pronunciationScores: [], | |
| flaggedSegments: [], | |
| } | |
| } catch { | |
| return { | |
| video_id: videoId, | |
| currentSegment: 0, | |
| dictationScores: [], | |
| pronunciationScores: [], | |
| flaggedSegments: [], | |
| } | |
| } | |
| } | |
| export function saveProgress(videoId, patch) { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY) | |
| const all = raw ? JSON.parse(raw) : {} | |
| all[videoId] = { ...loadProgress(videoId), ...patch, video_id: videoId } | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(all)) | |
| } catch { /* ignore */ } | |
| } | |
| export function saveVideoMeta(videoId, { title, level, sourceUrl } = {}) { | |
| try { | |
| const raw = localStorage.getItem(META_KEY) | |
| const all = raw ? JSON.parse(raw) : {} | |
| all[videoId] = { | |
| ...all[videoId], | |
| title: title ?? all[videoId]?.title, | |
| level: level ?? all[videoId]?.level, | |
| sourceUrl: sourceUrl ?? all[videoId]?.sourceUrl, | |
| lastVisitedAt: new Date().toISOString(), | |
| } | |
| localStorage.setItem(META_KEY, JSON.stringify(all)) | |
| } catch { /* ignore */ } | |
| } | |
| /** Local fallback history from progress/meta keys. */ | |
| export function listLocalHistory() { | |
| try { | |
| const progRaw = localStorage.getItem(STORAGE_KEY) | |
| const metaRaw = localStorage.getItem(META_KEY) | |
| const prog = progRaw ? JSON.parse(progRaw) : {} | |
| const meta = metaRaw ? JSON.parse(metaRaw) : {} | |
| return Object.keys(prog) | |
| .map((videoId) => ({ | |
| video_id: videoId, | |
| title: meta[videoId]?.title || videoId, | |
| level: meta[videoId]?.level || 'Intermediate', | |
| segment_count: 0, | |
| last_viewed_at: meta[videoId]?.lastVisitedAt || null, | |
| _local: true, | |
| })) | |
| .sort((a, b) => { | |
| const ta = a.last_viewed_at ? new Date(a.last_viewed_at).getTime() : 0 | |
| const tb = b.last_viewed_at ? new Date(b.last_viewed_at).getTime() : 0 | |
| return tb - ta | |
| }) | |
| } catch { | |
| return [] | |
| } | |
| } | |
| export function updateLocalHistoryMeta(videoId, { title, level } = {}) { | |
| saveVideoMeta(videoId, { title, level }) | |
| } | |
| export function removeLocalHistory(videoId) { | |
| try { | |
| const progRaw = localStorage.getItem(STORAGE_KEY) | |
| const metaRaw = localStorage.getItem(META_KEY) | |
| const prog = progRaw ? JSON.parse(progRaw) : {} | |
| const meta = metaRaw ? JSON.parse(metaRaw) : {} | |
| delete prog[videoId] | |
| delete meta[videoId] | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(prog)) | |
| localStorage.setItem(META_KEY, JSON.stringify(meta)) | |
| } catch { /* ignore */ } | |
| } | |