File size: 2,682 Bytes
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// ─── Video Engine V1 β€” on-device progress (resume) ───────────────────────────
// Saves playback position on the student's device so they can resume later.
// Bounded to the 50 most-recent videos (LRU) to stay light.

const KEY_PREFIX = "docdoe_video_progress_";
const INDEX_KEY = "docdoe_video_progress_index";
const MAX_ENTRIES = 50;

export type SavedVideoProgress = {
  video_id: string;
  current_segment_index: number;
  current_segment_elapsed: number; // seconds into the current segment
  completed_segment_ids: string[];
  updated_at: string;
};

function canUseStorage(): boolean {
  return typeof window !== "undefined" && !!window.localStorage;
}

function readIndex(): string[] {
  if (!canUseStorage()) return [];
  try {
    const raw = window.localStorage.getItem(INDEX_KEY);
    const parsed = raw ? (JSON.parse(raw) as unknown) : [];
    return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === "string") : [];
  } catch {
    return [];
  }
}

function writeIndex(ids: string[]): void {
  if (!canUseStorage()) return;
  try {
    window.localStorage.setItem(INDEX_KEY, JSON.stringify(ids));
  } catch {
    /* storage full / blocked β€” ignore */
  }
}

function touch(videoId: string): void {
  let index = readIndex().filter((id) => id !== videoId);
  index.unshift(videoId);
  // Evict least-recently-used beyond the cap.
  if (index.length > MAX_ENTRIES) {
    const evicted = index.slice(MAX_ENTRIES);
    index = index.slice(0, MAX_ENTRIES);
    for (const id of evicted) {
      try {
        window.localStorage.removeItem(`${KEY_PREFIX}${id}`);
      } catch {
        /* ignore */
      }
    }
  }
  writeIndex(index);
}

export function loadVideoProgress(videoId: string): SavedVideoProgress | null {
  if (!canUseStorage()) return null;
  try {
    const raw = window.localStorage.getItem(`${KEY_PREFIX}${videoId}`);
    if (!raw) return null;
    return JSON.parse(raw) as SavedVideoProgress;
  } catch {
    return null;
  }
}

export function saveVideoProgress(progress: SavedVideoProgress): void {
  if (!canUseStorage()) return;
  try {
    window.localStorage.setItem(
      `${KEY_PREFIX}${progress.video_id}`,
      JSON.stringify({ ...progress, updated_at: new Date().toISOString() }),
    );
    touch(progress.video_id);
  } catch {
    /* storage full / blocked β€” degrade silently */
  }
}

export function clearVideoProgress(videoId: string): void {
  if (!canUseStorage()) return;
  try {
    window.localStorage.removeItem(`${KEY_PREFIX}${videoId}`);
    writeIndex(readIndex().filter((id) => id !== videoId));
  } catch {
    /* ignore */
  }
}