feat(jellyfin): favorites library filter + heart favorite in the rate sheet

close #1485
This commit is contained in:
edde746
2026-07-04 18:33:58 +02:00
parent 30de8be2cd
commit 7632d4bea7
52 changed files with 328 additions and 415 deletions
+4
View File
@@ -49,6 +49,10 @@ sealed class LibraryQuery with _$LibraryQuery {
/// Whether to include items the active user has already watched.
@Default(true) bool includeWatched,
/// Restrict to items the user marked favorite (Jellyfin `Filters=IsFavorite`).
/// Plex has no equivalent; its translator ignores the flag.
@Default(false) bool favoritesOnly,
/// Restrict the result to items whose sort name starts with this string —
/// the alpha-jump bar's filter UX. The literal `#` is a sentinel for
/// "non-alphabetic" and translates to a `NameLessThan=A` query for backends
+25 -18
View File
@@ -536,7 +536,9 @@ mixin _$LibraryQuery {
int get offset; int get limit; LibrarySort? get sort; List<LibraryFilter> get filters;/// Free-text search restricted to this library. Distinct from the global
/// search endpoint.
String? get search;/// Whether to include items the active user has already watched.
bool get includeWatched;/// Restrict the result to items whose sort name starts with this string —
bool get includeWatched;/// Restrict to items the user marked favorite (Jellyfin `Filters=IsFavorite`).
/// Plex has no equivalent; its translator ignores the flag.
bool get favoritesOnly;/// Restrict the result to items whose sort name starts with this string —
/// the alpha-jump bar's filter UX. The literal `#` is a sentinel for
/// "non-alphabetic" and translates to a `NameLessThan=A` query for backends
/// that support it.
@@ -554,16 +556,16 @@ $LibraryQueryCopyWith<LibraryQuery> get copyWith => _$LibraryQueryCopyWithImpl<L
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LibraryQuery&&(identical(other.kind, kind) || other.kind == kind)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.sort, sort) || other.sort == sort)&&const DeepCollectionEquality().equals(other.filters, filters)&&(identical(other.search, search) || other.search == search)&&(identical(other.includeWatched, includeWatched) || other.includeWatched == includeWatched)&&(identical(other.nameStartsWith, nameStartsWith) || other.nameStartsWith == nameStartsWith)&&const DeepCollectionEquality().equals(other.genres, genres)&&const DeepCollectionEquality().equals(other.officialRatings, officialRatings)&&const DeepCollectionEquality().equals(other.years, years)&&const DeepCollectionEquality().equals(other.tags, tags));
return identical(this, other) || (other.runtimeType == runtimeType&&other is LibraryQuery&&(identical(other.kind, kind) || other.kind == kind)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.sort, sort) || other.sort == sort)&&const DeepCollectionEquality().equals(other.filters, filters)&&(identical(other.search, search) || other.search == search)&&(identical(other.includeWatched, includeWatched) || other.includeWatched == includeWatched)&&(identical(other.favoritesOnly, favoritesOnly) || other.favoritesOnly == favoritesOnly)&&(identical(other.nameStartsWith, nameStartsWith) || other.nameStartsWith == nameStartsWith)&&const DeepCollectionEquality().equals(other.genres, genres)&&const DeepCollectionEquality().equals(other.officialRatings, officialRatings)&&const DeepCollectionEquality().equals(other.years, years)&&const DeepCollectionEquality().equals(other.tags, tags));
}
@override
int get hashCode => Object.hash(runtimeType,kind,offset,limit,sort,const DeepCollectionEquality().hash(filters),search,includeWatched,nameStartsWith,const DeepCollectionEquality().hash(genres),const DeepCollectionEquality().hash(officialRatings),const DeepCollectionEquality().hash(years),const DeepCollectionEquality().hash(tags));
int get hashCode => Object.hash(runtimeType,kind,offset,limit,sort,const DeepCollectionEquality().hash(filters),search,includeWatched,favoritesOnly,nameStartsWith,const DeepCollectionEquality().hash(genres),const DeepCollectionEquality().hash(officialRatings),const DeepCollectionEquality().hash(years),const DeepCollectionEquality().hash(tags));
@override
String toString() {
return 'LibraryQuery(kind: $kind, offset: $offset, limit: $limit, sort: $sort, filters: $filters, search: $search, includeWatched: $includeWatched, nameStartsWith: $nameStartsWith, genres: $genres, officialRatings: $officialRatings, years: $years, tags: $tags)';
return 'LibraryQuery(kind: $kind, offset: $offset, limit: $limit, sort: $sort, filters: $filters, search: $search, includeWatched: $includeWatched, favoritesOnly: $favoritesOnly, nameStartsWith: $nameStartsWith, genres: $genres, officialRatings: $officialRatings, years: $years, tags: $tags)';
}
@@ -574,7 +576,7 @@ abstract mixin class $LibraryQueryCopyWith<$Res> {
factory $LibraryQueryCopyWith(LibraryQuery value, $Res Function(LibraryQuery) _then) = _$LibraryQueryCopyWithImpl;
@useResult
$Res call({
MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags
MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, bool favoritesOnly, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags
});
@@ -591,7 +593,7 @@ class _$LibraryQueryCopyWithImpl<$Res>
/// Create a copy of LibraryQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? kind = freezed,Object? offset = null,Object? limit = null,Object? sort = freezed,Object? filters = null,Object? search = freezed,Object? includeWatched = null,Object? nameStartsWith = freezed,Object? genres = freezed,Object? officialRatings = freezed,Object? years = freezed,Object? tags = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? kind = freezed,Object? offset = null,Object? limit = null,Object? sort = freezed,Object? filters = null,Object? search = freezed,Object? includeWatched = null,Object? favoritesOnly = null,Object? nameStartsWith = freezed,Object? genres = freezed,Object? officialRatings = freezed,Object? years = freezed,Object? tags = freezed,}) {
return _then(_self.copyWith(
kind: freezed == kind ? _self.kind : kind // ignore: cast_nullable_to_non_nullable
as MediaKind?,offset: null == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable
@@ -600,6 +602,7 @@ as int,sort: freezed == sort ? _self.sort : sort // ignore: cast_nullable_to_non
as LibrarySort?,filters: null == filters ? _self.filters : filters // ignore: cast_nullable_to_non_nullable
as List<LibraryFilter>,search: freezed == search ? _self.search : search // ignore: cast_nullable_to_non_nullable
as String?,includeWatched: null == includeWatched ? _self.includeWatched : includeWatched // ignore: cast_nullable_to_non_nullable
as bool,favoritesOnly: null == favoritesOnly ? _self.favoritesOnly : favoritesOnly // ignore: cast_nullable_to_non_nullable
as bool,nameStartsWith: freezed == nameStartsWith ? _self.nameStartsWith : nameStartsWith // ignore: cast_nullable_to_non_nullable
as String?,genres: freezed == genres ? _self.genres : genres // ignore: cast_nullable_to_non_nullable
as List<String>?,officialRatings: freezed == officialRatings ? _self.officialRatings : officialRatings // ignore: cast_nullable_to_non_nullable
@@ -699,10 +702,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, bool favoritesOnly, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LibraryQuery() when $default != null:
return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_that.search,_that.includeWatched,_that.nameStartsWith,_that.genres,_that.officialRatings,_that.years,_that.tags);case _:
return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_that.search,_that.includeWatched,_that.favoritesOnly,_that.nameStartsWith,_that.genres,_that.officialRatings,_that.years,_that.tags);case _:
return orElse();
}
@@ -720,10 +723,10 @@ return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_th
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, bool favoritesOnly, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags) $default,) {final _that = this;
switch (_that) {
case _LibraryQuery():
return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_that.search,_that.includeWatched,_that.nameStartsWith,_that.genres,_that.officialRatings,_that.years,_that.tags);}
return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_that.search,_that.includeWatched,_that.favoritesOnly,_that.nameStartsWith,_that.genres,_that.officialRatings,_that.years,_that.tags);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -737,10 +740,10 @@ return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_th
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, bool favoritesOnly, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags)? $default,) {final _that = this;
switch (_that) {
case _LibraryQuery() when $default != null:
return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_that.search,_that.includeWatched,_that.nameStartsWith,_that.genres,_that.officialRatings,_that.years,_that.tags);case _:
return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_that.search,_that.includeWatched,_that.favoritesOnly,_that.nameStartsWith,_that.genres,_that.officialRatings,_that.years,_that.tags);case _:
return null;
}
@@ -752,7 +755,7 @@ return $default(_that.kind,_that.offset,_that.limit,_that.sort,_that.filters,_th
class _LibraryQuery implements LibraryQuery {
const _LibraryQuery({this.kind, this.offset = 0, this.limit = 50, this.sort, final List<LibraryFilter> filters = const <LibraryFilter>[], this.search, this.includeWatched = true, this.nameStartsWith, final List<String>? genres, final List<String>? officialRatings, final List<int>? years, final List<String>? tags}): _filters = filters,_genres = genres,_officialRatings = officialRatings,_years = years,_tags = tags;
const _LibraryQuery({this.kind, this.offset = 0, this.limit = 50, this.sort, final List<LibraryFilter> filters = const <LibraryFilter>[], this.search, this.includeWatched = true, this.favoritesOnly = false, this.nameStartsWith, final List<String>? genres, final List<String>? officialRatings, final List<int>? years, final List<String>? tags}): _filters = filters,_genres = genres,_officialRatings = officialRatings,_years = years,_tags = tags;
/// Restrict to a single kind (e.g. `MediaKind.movie`). Null = library default.
@@ -773,6 +776,9 @@ class _LibraryQuery implements LibraryQuery {
@override final String? search;
/// Whether to include items the active user has already watched.
@override@JsonKey() final bool includeWatched;
/// Restrict to items the user marked favorite (Jellyfin `Filters=IsFavorite`).
/// Plex has no equivalent; its translator ignores the flag.
@override@JsonKey() final bool favoritesOnly;
/// Restrict the result to items whose sort name starts with this string —
/// the alpha-jump bar's filter UX. The literal `#` is a sentinel for
/// "non-alphabetic" and translates to a `NameLessThan=A` query for backends
@@ -831,16 +837,16 @@ _$LibraryQueryCopyWith<_LibraryQuery> get copyWith => __$LibraryQueryCopyWithImp
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LibraryQuery&&(identical(other.kind, kind) || other.kind == kind)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.sort, sort) || other.sort == sort)&&const DeepCollectionEquality().equals(other._filters, _filters)&&(identical(other.search, search) || other.search == search)&&(identical(other.includeWatched, includeWatched) || other.includeWatched == includeWatched)&&(identical(other.nameStartsWith, nameStartsWith) || other.nameStartsWith == nameStartsWith)&&const DeepCollectionEquality().equals(other._genres, _genres)&&const DeepCollectionEquality().equals(other._officialRatings, _officialRatings)&&const DeepCollectionEquality().equals(other._years, _years)&&const DeepCollectionEquality().equals(other._tags, _tags));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LibraryQuery&&(identical(other.kind, kind) || other.kind == kind)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.sort, sort) || other.sort == sort)&&const DeepCollectionEquality().equals(other._filters, _filters)&&(identical(other.search, search) || other.search == search)&&(identical(other.includeWatched, includeWatched) || other.includeWatched == includeWatched)&&(identical(other.favoritesOnly, favoritesOnly) || other.favoritesOnly == favoritesOnly)&&(identical(other.nameStartsWith, nameStartsWith) || other.nameStartsWith == nameStartsWith)&&const DeepCollectionEquality().equals(other._genres, _genres)&&const DeepCollectionEquality().equals(other._officialRatings, _officialRatings)&&const DeepCollectionEquality().equals(other._years, _years)&&const DeepCollectionEquality().equals(other._tags, _tags));
}
@override
int get hashCode => Object.hash(runtimeType,kind,offset,limit,sort,const DeepCollectionEquality().hash(_filters),search,includeWatched,nameStartsWith,const DeepCollectionEquality().hash(_genres),const DeepCollectionEquality().hash(_officialRatings),const DeepCollectionEquality().hash(_years),const DeepCollectionEquality().hash(_tags));
int get hashCode => Object.hash(runtimeType,kind,offset,limit,sort,const DeepCollectionEquality().hash(_filters),search,includeWatched,favoritesOnly,nameStartsWith,const DeepCollectionEquality().hash(_genres),const DeepCollectionEquality().hash(_officialRatings),const DeepCollectionEquality().hash(_years),const DeepCollectionEquality().hash(_tags));
@override
String toString() {
return 'LibraryQuery(kind: $kind, offset: $offset, limit: $limit, sort: $sort, filters: $filters, search: $search, includeWatched: $includeWatched, nameStartsWith: $nameStartsWith, genres: $genres, officialRatings: $officialRatings, years: $years, tags: $tags)';
return 'LibraryQuery(kind: $kind, offset: $offset, limit: $limit, sort: $sort, filters: $filters, search: $search, includeWatched: $includeWatched, favoritesOnly: $favoritesOnly, nameStartsWith: $nameStartsWith, genres: $genres, officialRatings: $officialRatings, years: $years, tags: $tags)';
}
@@ -851,7 +857,7 @@ abstract mixin class _$LibraryQueryCopyWith<$Res> implements $LibraryQueryCopyWi
factory _$LibraryQueryCopyWith(_LibraryQuery value, $Res Function(_LibraryQuery) _then) = __$LibraryQueryCopyWithImpl;
@override @useResult
$Res call({
MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags
MediaKind? kind, int offset, int limit, LibrarySort? sort, List<LibraryFilter> filters, String? search, bool includeWatched, bool favoritesOnly, String? nameStartsWith, List<String>? genres, List<String>? officialRatings, List<int>? years, List<String>? tags
});
@@ -868,7 +874,7 @@ class __$LibraryQueryCopyWithImpl<$Res>
/// Create a copy of LibraryQuery
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? kind = freezed,Object? offset = null,Object? limit = null,Object? sort = freezed,Object? filters = null,Object? search = freezed,Object? includeWatched = null,Object? nameStartsWith = freezed,Object? genres = freezed,Object? officialRatings = freezed,Object? years = freezed,Object? tags = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? kind = freezed,Object? offset = null,Object? limit = null,Object? sort = freezed,Object? filters = null,Object? search = freezed,Object? includeWatched = null,Object? favoritesOnly = null,Object? nameStartsWith = freezed,Object? genres = freezed,Object? officialRatings = freezed,Object? years = freezed,Object? tags = freezed,}) {
return _then(_LibraryQuery(
kind: freezed == kind ? _self.kind : kind // ignore: cast_nullable_to_non_nullable
as MediaKind?,offset: null == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable
@@ -877,6 +883,7 @@ as int,sort: freezed == sort ? _self.sort : sort // ignore: cast_nullable_to_non
as LibrarySort?,filters: null == filters ? _self._filters : filters // ignore: cast_nullable_to_non_nullable
as List<LibraryFilter>,search: freezed == search ? _self.search : search // ignore: cast_nullable_to_non_nullable
as String?,includeWatched: null == includeWatched ? _self.includeWatched : includeWatched // ignore: cast_nullable_to_non_nullable
as bool,favoritesOnly: null == favoritesOnly ? _self.favoritesOnly : favoritesOnly // ignore: cast_nullable_to_non_nullable
as bool,nameStartsWith: freezed == nameStartsWith ? _self.nameStartsWith : nameStartsWith // ignore: cast_nullable_to_non_nullable
as String?,genres: freezed == genres ? _self._genres : genres // ignore: cast_nullable_to_non_nullable
as List<String>?,officialRatings: freezed == officialRatings ? _self._officialRatings : officialRatings // ignore: cast_nullable_to_non_nullable
+5
View File
@@ -60,6 +60,7 @@ sealed class MediaItem with _$MediaItem {
int? updatedAt,
double? rating,
double? userRating,
bool? isFavorite,
List<String>? genres,
List<String>? directors,
List<String>? writers,
@@ -119,6 +120,7 @@ sealed class MediaItem with _$MediaItem {
updatedAt: updatedAt,
rating: rating,
userRating: userRating,
isFavorite: isFavorite,
genres: genres,
directors: directors,
writers: writers,
@@ -177,6 +179,7 @@ sealed class MediaItem with _$MediaItem {
updatedAt: updatedAt,
rating: rating,
userRating: userRating,
isFavorite: isFavorite,
genres: genres,
directors: directors,
writers: writers,
@@ -243,6 +246,7 @@ sealed class MediaItem with _$MediaItem {
@JsonKey(fromJson: flexibleDouble) double? rating,
@JsonKey(fromJson: flexibleDouble) double? audienceRating,
@JsonKey(fromJson: flexibleDouble) double? userRating,
bool? isFavorite,
String? ratingImage,
String? audienceRatingImage,
@JsonKey(fromJson: _mediaItemStringList) List<String>? genres,
@@ -316,6 +320,7 @@ sealed class MediaItem with _$MediaItem {
@JsonKey(fromJson: flexibleInt) int? updatedAt,
@JsonKey(fromJson: flexibleDouble) double? rating,
@JsonKey(fromJson: flexibleDouble) double? userRating,
bool? isFavorite,
@JsonKey(fromJson: _mediaItemStringList) List<String>? genres,
@JsonKey(fromJson: _mediaItemStringList) List<String>? directors,
@JsonKey(fromJson: _mediaItemStringList) List<String>? writers,
File diff suppressed because one or more lines are too long
+4
View File
@@ -46,6 +46,7 @@ PlexMediaItem _$PlexMediaItemFromJson(Map<String, dynamic> json) =>
rating: flexibleDouble(json['rating']),
audienceRating: flexibleDouble(json['audienceRating']),
userRating: flexibleDouble(json['userRating']),
isFavorite: json['isFavorite'] as bool?,
ratingImage: json['ratingImage'] as String?,
audienceRatingImage: json['audienceRatingImage'] as String?,
genres: _mediaItemStringList(json['genres']),
@@ -115,6 +116,7 @@ Map<String, dynamic> _$PlexMediaItemToJson(PlexMediaItem instance) =>
'rating': ?instance.rating,
'audienceRating': ?instance.audienceRating,
'userRating': ?instance.userRating,
'isFavorite': ?instance.isFavorite,
'ratingImage': ?instance.ratingImage,
'audienceRatingImage': ?instance.audienceRatingImage,
'genres': ?instance.genres,
@@ -182,6 +184,7 @@ JellyfinMediaItem _$JellyfinMediaItemFromJson(Map<String, dynamic> json) =>
updatedAt: flexibleInt(json['updatedAt']),
rating: flexibleDouble(json['rating']),
userRating: flexibleDouble(json['userRating']),
isFavorite: json['isFavorite'] as bool?,
genres: _mediaItemStringList(json['genres']),
directors: _mediaItemStringList(json['directors']),
writers: _mediaItemStringList(json['writers']),
@@ -241,6 +244,7 @@ Map<String, dynamic> _$JellyfinMediaItemToJson(JellyfinMediaItem instance) =>
'updatedAt': ?instance.updatedAt,
'rating': ?instance.rating,
'userRating': ?instance.userRating,
'isFavorite': ?instance.isFavorite,
'genres': ?instance.genres,
'directors': ?instance.directors,
'writers': ?instance.writers,
+5
View File
@@ -310,6 +310,11 @@ abstract class MediaServerClient {
/// awaited call in `try/catch` and surface a snackbar on the catch arm.
Future<void> rate(MediaItem item, double rating);
/// Set or clear the per-user favorite flag ("heart") for [item]. Only call
/// when [ServerCapabilities.userFavorites] is true; unsupported backends
/// throw [UnsupportedError]. Throws [MediaServerHttpException] on failure.
Future<void> setFavorite(MediaItem item, bool isFavorite);
Future<List<MediaPlaylist>> fetchPlaylists({String playlistType = 'video', bool? smart});
/// Page through server playlists. Used by the library Playlists tab so large
+11 -2
View File
@@ -56,10 +56,14 @@ class ServerCapabilities {
/// returns synthesized hubs but with sparser categorisation.
final bool richHubs;
/// Numeric ratings (Plex 010 via [Item.userRating]). Jellyfin offers
/// only a binary like/dislike, so star sliders should be hidden.
/// Numeric ratings (Plex 010 via [Item.userRating]). Jellyfin has no
/// numeric user rating, so star sliders should be hidden.
final bool numericUserRating;
/// Per-user favorite flag ("heart") on media items. Jellyfin exposes it via
/// `/Users/{userId}/FavoriteItems/{itemId}`; Plex has no equivalent.
final bool userFavorites;
/// Hide an item from Continue Watching without changing watch state or
/// playback progress. Plex exposes this directly; Jellyfin does not.
final bool continueWatchingRemoval;
@@ -115,6 +119,7 @@ class ServerCapabilities {
this.serverSideSync = false,
this.richHubs = false,
this.numericUserRating = false,
this.userFavorites = false,
this.continueWatchingRemoval = false,
this.externalSubtitleSearch = false,
this.trackPreferencePersistence = false,
@@ -138,6 +143,7 @@ class ServerCapabilities {
serverSideSync: true,
richHubs: true,
numericUserRating: true,
userFavorites: false,
continueWatchingRemoval: true,
externalSubtitleSearch: true,
trackPreferencePersistence: true,
@@ -169,6 +175,7 @@ class ServerCapabilities {
serverSideSync: false,
richHubs: false,
numericUserRating: false,
userFavorites: true,
externalSubtitleSearch: false,
trackPreferencePersistence: true,
endpointFailover: true,
@@ -190,6 +197,7 @@ class ServerCapabilities {
bool? serverSideSync,
bool? richHubs,
bool? numericUserRating,
bool? userFavorites,
bool? continueWatchingRemoval,
bool? externalSubtitleSearch,
bool? trackPreferencePersistence,
@@ -211,6 +219,7 @@ class ServerCapabilities {
serverSideSync: serverSideSync ?? this.serverSideSync,
richHubs: richHubs ?? this.richHubs,
numericUserRating: numericUserRating ?? this.numericUserRating,
userFavorites: userFavorites ?? this.userFavorites,
continueWatchingRemoval: continueWatchingRemoval ?? this.continueWatchingRemoval,
externalSubtitleSearch: externalSubtitleSearch ?? this.externalSubtitleSearch,
trackPreferencePersistence: trackPreferencePersistence ?? this.trackPreferencePersistence,