commit 1383fc56bf05995733f620ee76a3dec35beb0b41 truex <truex@equwal.com> 2026-09-26 17:48:03 -0700 Merge the reading view fixes: top bar, status strip, start card, fit width, page turns, follow hold, crossing cues, drawer # Conflicts: # src/subtitles.ts # test/subtitles.test.ts
README.md | 136 +++++++++---- index.html | 95 +++++++-- src/cue-pages.ts | 131 ++++++++++++ src/drawer.ts | 58 ++++++ src/follower.ts | 95 ++++----- src/hit-test.ts | 12 ++ src/line-boxes.ts | 75 +++++-- src/main.ts | 517 +++++++++++++++++++++++++++++------------------- src/messages.ts | 86 ++++++++ src/nav.ts | 210 ++++++++++++++++++++ src/paging.ts | 86 ++++++++ src/scroll.ts | 28 +++ src/status.ts | 69 +++++++ src/style.css | 261 +++++++++++++++++++----- src/subtitles.ts | 13 +- src/view.ts | 187 ++++++++++++++++++ test/cue-pages.test.ts | 219 ++++++++++++++++++++ test/follower.test.ts | 220 ++++++++++----------- test/hit-test.test.ts | 31 ++- test/line-boxes.test.ts | 131 +++++++++++- test/messages.test.ts | 100 ++++++++++ test/paging.test.ts | 149 ++++++++++++++ test/scroll.test.ts | 80 ++++++++ test/subtitles.test.ts | 34 +++- 24 files changed, 2541 insertions(+), 482 deletions(-)
diff --cc src/subtitles.ts index bf1a658,fc677ab..4af23d2 --- a/src/subtitles.ts +++ b/src/subtitles.ts @@@ -49,29 -49,11 +49,32 @@@ export function parseSubtitles(input: s return cues; } +/** + * Decodes the bytes of a subtitle file to text, and removes the BOM. + * + * - A UTF-8 BOM, or bytes that are valid UTF-8: UTF-8. + * - A UTF-16 BOM: UTF-16, little endian or big endian as the BOM tells. + * - Other bytes: Shift_JIS, the usual encoding of old Japanese files. + * Japanese text in Shift_JIS is almost never valid UTF-8. + */ +export function decodeSubtitles(bytes: ArrayBuffer): string { + const b = new Uint8Array(bytes); + // TextDecoder removes the BOM of its own encoding. + if (b[0] === 0xef && b[1] === 0xbb && b[2] === 0xbf) return new TextDecoder('utf-8').decode(b); + if (b[0] === 0xff && b[1] === 0xfe) return new TextDecoder('utf-16le').decode(b); + if (b[0] === 0xfe && b[1] === 0xff) return new TextDecoder('utf-16be').decode(b); + try { + return new TextDecoder('utf-8', { fatal: true }).decode(b); + } catch { + return new TextDecoder('shift_jis').decode(b); + } +} + - /** Index of the cue active at time t, or -1. Cues must be sorted by start. */ - export function cueIndexAt(cues: readonly Cue[], t: number): number { + /** + * Index of the last cue that starts at or before time t, or -1. In the + * silence after a cue, this is still that cue. Cues must be sorted by start. + */ + export function lastCueAt(cues: readonly Pick<Cue, 'start'>[], t: number): number { let lo = 0; let hi = cues.length - 1; let found = -1; diff --cc test/subtitles.test.ts index bd12de2,e25e4ca..31d7259 --- a/test/subtitles.test.ts +++ b/test/subtitles.test.ts @@@ -1,6 -1,6 +1,6 @@@ import fc from 'fast-check'; import { describe, expect, it } from 'vitest'; - import { cueIndexAt, decodeSubtitles, parseSubtitles } from '../src/subtitles'; -import { cueIndexAt, lastCueAt, parseSubtitles } from '../src/subtitles'; ++import { cueIndexAt, decodeSubtitles, lastCueAt, parseSubtitles } from '../src/subtitles'; const SRT = `1 00:00:01,000 --> 00:00:03,500