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

+18 -1
View File
@@ -180,7 +180,8 @@ class JellyfinClient {
final JellyfinHttp _http;
/// Fields requested for list items: enough for cards and detail headers.
static const listFields = 'PrimaryImageAspectRatio,Overview,ChildCount,RecursiveItemCount';
static const listFields =
'PrimaryImageAspectRatio,Overview,ChildCount,RecursiveItemCount,DateCreated';
static const _imageParams = {
'enableImageTypes': 'Primary,Backdrop,Thumb,Logo',
'imageTypeLimit': '1',
@@ -350,6 +351,22 @@ class JellyfinClient {
.toList();
}
/// Episodes and movies the user finished, most recently played first
/// ([JellyfinUserData.lastPlayedDate] set): when each series was last
/// watched, which [resume] and [nextUp] do not tell for fully played
/// episodes.
Future<List<JellyfinItem>> recentlyPlayed({int limit = 100}) async => (await items(
JellyfinItemQuery(
includeItemTypes: const [JellyfinItemType.episode, JellyfinItemType.movie],
recursive: true,
filters: const [JellyfinFilter.isPlayed],
sortBy: const [JellyfinSortBy.datePlayed],
descending: true,
limit: limit,
),
))
.items;
/// A page of library items; see [JellyfinItemQuery].
Future<JellyfinItemPage> items(JellyfinItemQuery q) async {
final page = JellyfinItemPage.fromJson(_map(await _json('GET', '/Items', query: {
@@ -198,6 +198,9 @@ class JellyfinItem {
final String? overview;
final int? productionYear;
final DateTime? premiereDate;
/// When the item was added to the library (needs `DateCreated` in
/// `fields`; [JellyfinClient.listFields] has it).
final DateTime? dateCreated;
final double? communityRating;
final String? officialRating;
final int? runTimeTicks;
@@ -244,6 +247,7 @@ class JellyfinItem {
this.overview,
this.productionYear,
this.premiereDate,
this.dateCreated,
this.communityRating,
this.officialRating,
this.runTimeTicks,
@@ -282,6 +286,7 @@ class JellyfinItem {
overview: _str(j['Overview']),
productionYear: _int(j['ProductionYear']),
premiereDate: _date(j['PremiereDate']),
dateCreated: _date(j['DateCreated']),
communityRating: _double(j['CommunityRating']),
officialRating: _str(j['OfficialRating']),
runTimeTicks: _int(j['RunTimeTicks']),
+5
View File
@@ -490,6 +490,8 @@ class MemberView {
final bool stale;
/// Last `report.jf` of this member; empty when never reported.
final Map<String, bool> jf;
/// The hosting app's own member (sent the server's host key).
final bool host;
const MemberView({
required this.id,
required this.name,
@@ -499,6 +501,7 @@ class MemberView {
required this.buffered,
required this.stale,
this.jf = const {},
this.host = false,
});
Map<String, Object?> toJson() => {
@@ -510,6 +513,7 @@ class MemberView {
'buffered': buffered,
'stale': stale,
if (jf.isNotEmpty) 'jf': jf,
if (host) 'host': true,
};
factory MemberView.fromJson(Map<String, Object?> json) => MemberView(
@@ -521,6 +525,7 @@ class MemberView {
buffered: finiteDouble(json['buffered'], 'buffered'),
stale: (json['stale'] as bool?) ?? false,
jf: jfAccessFromJson(json['jf']) ?? const {},
host: json['host'] == true,
);
}
+3
View File
@@ -52,6 +52,8 @@ class EngineMember {
double buffered = 0;
/// Last reported Jellyfin access by ServerId (`report.jf`).
Map<String, bool> jf = const {};
/// The hosting app's own member (set by the server from the host key).
bool host = false;
DateTime lastSeen;
EngineMember({
required this.id,
@@ -91,6 +93,7 @@ class EngineMember {
buffered: buffered,
stale: stale,
jf: jf,
host: host,
);
}