Wire up playback reporting, fix duration flash, hide video from audio mini player
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m14s
Traceability Validation / Check Requirement Traces (push) Successful in 21s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 19m3s

Playback reporting (position sync / resume-on-another-device):
- player_configure_jellyfin now builds a PlaybackReporter sharing the player
  controller's Arc, so Start/Progress/Stopped actually reach Jellyfin on every
  auth path (login/restore/reauth); previously they never did.
- The PlaybackReporterWrapper now shares the same Arc the controller and MPV
  progress loop report through, instead of a dead parallel Option.
- Android position callbacks now emit throttled progress reports (30s/item),
  mirroring the MPV backend.

Duration flash on pause:
- resolveDuration() prefers the live store duration for the already-loaded
  track over the runTimeTicks estimate, so pausing no longer clobbers the
  slider's max to 0 when runTimeTicks is missing.

Video leaking into audio mini player:
- isVideoItem() also checks the backend PlayerMediaItem mediaType
  discriminator, so a video started via player_play_item (no Jellyfin `type`,
  mediaType "video") no longer surfaces in the audio mini player.

Middle-truncation of long media names:
- New truncateMiddle util applied to track/episode/card/mini-player titles so
  distinguishing tails (episode numbers, suffixes) stay visible.

Adds regression tests for the duration and mini-player fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 21:52:27 +02:00
co-authored by Claude Opus 4.8
parent dcee342c47
commit 342f95cac1
21 changed files with 420 additions and 28 deletions
+39
View File
@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import { truncateMiddle } from "./truncateMiddle";
describe("truncateMiddle", () => {
it("returns short strings unchanged", () => {
expect(truncateMiddle("short", 32)).toBe("short");
expect(truncateMiddle("exactly-len", 11)).toBe("exactly-len");
});
it("abbreviates in the middle keeping head and tail", () => {
const result = truncateMiddle("long_media_name_like_this", 16);
expect(result).toContain("…");
expect(result.length).toBe(16);
expect(result.startsWith("long")).toBe(true);
expect(result.endsWith("this")).toBe(true);
});
it("never exceeds maxLength", () => {
for (const len of [1, 2, 3, 5, 10, 25]) {
expect(truncateMiddle("a".repeat(100), len).length).toBeLessThanOrEqual(len);
}
});
it("handles null and undefined", () => {
expect(truncateMiddle(null)).toBe("");
expect(truncateMiddle(undefined)).toBe("");
});
it("falls back to head-truncation when there is no room for content", () => {
expect(truncateMiddle("abcdef", 1)).toBe("a");
expect(truncateMiddle("abcdef", 0)).toBe("");
});
it("supports a custom ellipsis", () => {
const result = truncateMiddle("long_media_name_like_this", 16, "...");
expect(result).toContain("...");
expect(result.length).toBe(16);
});
});
+33
View File
@@ -0,0 +1,33 @@
/**
* Abbreviate a long string in the middle so both the start and the end stay
* visible, e.g. `long_media_name_like_this` -> `long_med…ike_this`.
*
* Unlike CSS `text-overflow: ellipsis` (which only clips the end), this keeps
* the tail of a media name — often the most distinguishing part (episode
* number, disambiguating suffix, etc.).
*
* @param value The string to abbreviate.
* @param maxLength Maximum length of the returned string, including the ellipsis.
* @param ellipsis The separator inserted in the middle (default `…`).
*/
export function truncateMiddle(
value: string | null | undefined,
maxLength = 32,
ellipsis = "…",
): string {
if (value == null) return "";
if (maxLength <= 0) return "";
if (value.length <= maxLength) return value;
// Not enough room for any real content around the ellipsis: fall back to a
// plain head-truncation so we never return more than maxLength characters.
if (maxLength <= ellipsis.length) {
return value.slice(0, maxLength);
}
const keep = maxLength - ellipsis.length;
const head = Math.ceil(keep / 2);
const tail = Math.floor(keep / 2);
return value.slice(0, head) + ellipsis + (tail > 0 ? value.slice(value.length - tail) : "");
}