1.0 library + player settings: Jellyfin/Link/Files library with details panel, player gear, settings cards, host flag

Library: source switch, selectable cards with a details panel, one merged
Continue row, Latest grouped per series without re-releases, Link page with
previews and recent links, Files embedded. Player: gear popover in both
designs, progress line while controls hide, live hwdec. Settings rebuilt as
cards. State marks the host member (proto stays 9, unreleased); hosting
shows LAN invite addresses; window buttons in fullscreen. fadeThrough keeps
the outgoing view's state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-09 00:58:12 +03:00
1 parent 99cda7ed04
commit d092ea371a
54 files changed
+4963 -1825

No files matched your search

+19 -2
View File
@@ -30,6 +30,20 @@ class FakeJellyfin implements JellyfinHttp {
bool offline = false;
int meStatus = 200;
/// Finished episodes (`/Items?filters=IsPlayed&sortBy=DatePlayed`): JoJo
/// and Black Summoner watched on 2026-10-07, after Black Summoner's
/// resume point (10-06), so its next-up episode is what continues.
List<Object> recentlyPlayed = [
{
'Id': 'jojo-played', 'Name': 'Played', 'Type': 'Episode', 'SeriesId': '5055eea092a576ea09eee3b6527ba857',
'UserData': {'Played': true, 'LastPlayedDate': '2026-10-07T21:00:00Z'},
},
{
'Id': 'bs-played', 'Name': 'Played', 'Type': 'Episode', 'SeriesId': 'e9aec9c14a1fcab775111844112b4cf7',
'UserData': {'Played': true, 'LastPlayedDate': '2026-10-07T20:00:00Z'},
},
];
FakeJellyfin() {
fixture('GET', '/System/Info/Public', 'pub');
on('GET', '/QuickConnect/Enabled', (_, _) => const JellyfinHttpResponse(200, 'true'));
@@ -58,6 +72,7 @@ class FakeJellyfin implements JellyfinHttp {
fixture('GET', '/Items/Latest', 'latest_shows');
on('GET', '/Items', (uri, _) {
final q = uri.queryParameters;
if (q['filters'] == 'IsPlayed' && q['sortBy'] == 'DatePlayed') return _page(recentlyPlayed);
if ((q['searchTerm'] ?? '').isNotEmpty) {
return q['searchTerm']!.toLowerCase().startsWith('ari')
? JellyfinHttpResponse(200, jellyfinFixture('search'))
@@ -307,10 +322,12 @@ void main() {
c.dispose();
});
test('a saved Jellyfin pick starts on the Jellyfin source', () {
test('the library starts on Jellyfin unless told otherwise; a saved pick is selected', () {
const m = JellyfinMedia(serverId: 's', baseUrl: 'b', itemId: 'i', title: 't');
expect(MediaPickerController(jellyfin: m).source, MediaKind.jellyfin);
expect(MediaPickerController(jellyfin: m, mediaPath: '/x.mkv').source, MediaKind.file);
expect(MediaPickerController(jellyfin: m).selected!.itemId, 'i');
expect(MediaPickerController(mediaPath: '/x.mkv', linkUrl: 'https://a/b').source, MediaKind.jellyfin);
expect(MediaPickerController(source: MediaKind.link).source, MediaKind.link);
});
test('JellyfinMedia round-trips and labels', () {