refactor: share the paginated grid tab, cached remote store, and tile focus
- PaginatedCardGridTabState: the collections and playlists tabs were 95% identical; they now supply only pageSize/fetchPage/idOf instead of each duplicating the grid, memo, inflation budget and focus wiring. - EtagCachedRemoteStore: the anime-lists and fribb mapping stores now share one download/cache/isolate-parse/conditional-GET lifecycle. - FocusableTileStateMixin manages its own initState/didUpdateWidget/dispose instead of requiring every caller to forward three lifecycle hooks. Also drops unused ServerCapabilities entries and dead code in focusable_list_tile and music/track_row.
This commit is contained in:
@@ -7,23 +7,35 @@ import 'owned_focus_node_binding.dart';
|
||||
/// auto-scrolls the tile into view when it gains focus.
|
||||
mixin FocusableTileStateMixin<T extends StatefulWidget> on State<T> {
|
||||
final _focusNodeBinding = OwnedFocusNodeBinding();
|
||||
FocusNode? _boundExternalNode;
|
||||
|
||||
FocusNode? get widgetFocusNode;
|
||||
|
||||
FocusNode get effectiveFocusNode => _focusNodeBinding.node;
|
||||
|
||||
void initFocusNode() {
|
||||
_focusNodeBinding.bind(externalNode: widgetFocusNode, listener: _onFocusChange);
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bindFocusNode();
|
||||
}
|
||||
|
||||
void updateFocusNode(FocusNode? oldFocusNode) {
|
||||
if (oldFocusNode != widgetFocusNode) {
|
||||
_focusNodeBinding.bind(externalNode: widgetFocusNode, listener: _onFocusChange);
|
||||
@override
|
||||
void didUpdateWidget(T oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (_boundExternalNode != widgetFocusNode) {
|
||||
_bindFocusNode();
|
||||
}
|
||||
}
|
||||
|
||||
void disposeFocusNode() {
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNodeBinding.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _bindFocusNode() {
|
||||
_boundExternalNode = widgetFocusNode;
|
||||
_focusNodeBinding.bind(externalNode: widgetFocusNode, listener: _onFocusChange);
|
||||
}
|
||||
|
||||
void _onFocusChange() {
|
||||
|
||||
@@ -260,7 +260,7 @@ abstract class MediaServerClient {
|
||||
/// `/Audio/{id}/Lyrics` (per-line tick offsets when synced); Plex: a
|
||||
/// sidecar-lyrics track stream (`streamType 4`) fetched from
|
||||
/// `/library/streams/{id}` and parsed from LRC. Synced-ness is per
|
||||
/// [Lyrics.synced]; gated by [ServerCapabilities.lyrics].
|
||||
/// [Lyrics.synced]; per-track absence is the runtime gate.
|
||||
Future<Lyrics?> fetchLyrics(MediaItem track);
|
||||
|
||||
/// Free-text search across the user's libraries.
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
/// How the alpha-jump bar behaves for libraries on this backend.
|
||||
enum AlphaBarMode {
|
||||
/// No alpha bar — hide entirely.
|
||||
none,
|
||||
|
||||
/// Plex: server reports per-letter cumulative offsets via `/firstCharacter`,
|
||||
/// taps scroll the grid to the offset.
|
||||
scrollSnap,
|
||||
|
||||
/// Jellyfin: bar acts as a filter button — taps set `NameStartsWith` query
|
||||
/// param, results re-fetch.
|
||||
nameStartsWithFilter,
|
||||
}
|
||||
|
||||
/// Static capability flags advertised by a [MediaServerClient]. UI consults
|
||||
/// these to gate feature affordances per server (e.g. hide Live TV when no
|
||||
/// connected server supports it).
|
||||
@@ -21,14 +7,6 @@ enum AlphaBarMode {
|
||||
/// Jellyfin features are wired in over time, the corresponding flags flip
|
||||
/// without changing call sites.
|
||||
class ServerCapabilities {
|
||||
/// Server-side `PlayQueue` resource (Plex `/playQueues`) — enables shared
|
||||
/// queue state across devices and Watch Together coordination.
|
||||
final bool serverSidePlayQueue;
|
||||
|
||||
/// Server-side editable playlists (Plex `/playlists`, Jellyfin
|
||||
/// `/Playlists`).
|
||||
final bool serverSidePlaylists;
|
||||
|
||||
/// This backend kind has a Live TV / DVR API the app can talk to. Whether
|
||||
/// a *specific* server has Live TV configured is a runtime concern —
|
||||
/// [MultiServerProvider.checkLiveTvAvailability] probes each server and
|
||||
@@ -41,17 +19,9 @@ class ServerCapabilities {
|
||||
/// when [liveTv] is true.
|
||||
final bool liveTvDvr;
|
||||
|
||||
/// Server proxies subtitle search (e.g. OpenSubtitles).
|
||||
final bool subtitleSearch;
|
||||
|
||||
/// Server can transcode video.
|
||||
final bool videoTranscoding;
|
||||
|
||||
/// Server supports server-side downloads / "sync" (the queued-from-server
|
||||
/// model). Both Plex and Jellyfin support client-driven downloads, which
|
||||
/// is a separate concept.
|
||||
final bool serverSideSync;
|
||||
|
||||
/// Server provides curated recommendation hubs (Plex Discover). Jellyfin
|
||||
/// returns synthesized hubs but with sparser categorisation.
|
||||
final bool richHubs;
|
||||
@@ -72,32 +42,9 @@ class ServerCapabilities {
|
||||
/// Hides the "Search subtitles" affordance when false.
|
||||
final bool externalSubtitleSearch;
|
||||
|
||||
/// Persisting per-track audio/subtitle preferences server-side. Plex uses
|
||||
/// `/library/metadata/{id}/prefs` + `selectStream`; Jellyfin saves selected
|
||||
/// stream indexes from `/Sessions/Playing/Progress` when the user's Jellyfin
|
||||
/// remember-selection settings are enabled. When false, in-player switching
|
||||
/// still works but choices don't follow the user across devices.
|
||||
final bool trackPreferencePersistence;
|
||||
|
||||
/// Multi-endpoint connection model with endpoint racing/failover. Plex gets
|
||||
/// local/remote/relay candidates from plex.tv; Jellyfin uses user-entered
|
||||
/// URLs for the same server.
|
||||
final bool endpointFailover;
|
||||
|
||||
/// Watch progress can be queued offline and replayed when reconnected
|
||||
/// ([OfflineWatchSyncService]). Jellyfin reports inline only today.
|
||||
final bool offlineWatchQueue;
|
||||
|
||||
/// Discord rich-presence integration. Plex-only because the RPC payload
|
||||
/// uses Plex-shaped session/metadata.
|
||||
final bool discordRpc;
|
||||
|
||||
/// Server exposes metadata edit endpoints. Hides edit affordances when false.
|
||||
final bool richMetadataEdit;
|
||||
|
||||
/// How the alpha-jump bar should behave for this backend's libraries.
|
||||
final AlphaBarMode alphaBar;
|
||||
|
||||
/// Server can supply thumbnails for the player's seek-bar scrub preview.
|
||||
/// Plex serves them as a `.bif` asset; Jellyfin uses `/Trickplay` sprite
|
||||
/// sheets. Both backends are wired through [ScrubPreviewSource]; the flag
|
||||
@@ -109,73 +56,40 @@ class ServerCapabilities {
|
||||
/// `/Items?ParentId=...&Recursive=false` queries.
|
||||
final bool folderGrouping;
|
||||
|
||||
/// Server can supply track lyrics. Jellyfin exposes `/Audio/{id}/Lyrics`;
|
||||
/// Plex surfaces sidecar `.lrc`/`.txt` files as track streams
|
||||
/// (`streamType 4`) fetched via `/library/streams/{id}`. Gates the lyrics
|
||||
/// affordance in the music player; per-track absence is the runtime gate.
|
||||
final bool lyrics;
|
||||
|
||||
/// Server can build an "instant mix" / radio track list from a seed item.
|
||||
/// Jellyfin: `/Items/{id}/InstantMix`; Plex: station play queues
|
||||
/// (`POST /playQueues?type=audio&uri=...station...`).
|
||||
final bool instantMix;
|
||||
|
||||
/// Server can transcode audio to a capped bitrate. Plex:
|
||||
/// `/music/:/transcode/universal`; Jellyfin: `PlaybackInfo` with an audio
|
||||
/// `TranscodingProfile`. Gates the music quality picker (vs original-only).
|
||||
final bool audioTranscoding;
|
||||
|
||||
const ServerCapabilities({
|
||||
this.serverSidePlayQueue = false,
|
||||
this.serverSidePlaylists = false,
|
||||
this.liveTv = false,
|
||||
this.liveTvDvr = false,
|
||||
this.subtitleSearch = false,
|
||||
this.videoTranscoding = true,
|
||||
this.serverSideSync = false,
|
||||
this.richHubs = false,
|
||||
this.numericUserRating = false,
|
||||
this.userFavorites = false,
|
||||
this.continueWatchingRemoval = false,
|
||||
this.externalSubtitleSearch = false,
|
||||
this.trackPreferencePersistence = false,
|
||||
this.endpointFailover = false,
|
||||
this.offlineWatchQueue = false,
|
||||
this.discordRpc = false,
|
||||
this.richMetadataEdit = false,
|
||||
this.alphaBar = AlphaBarMode.none,
|
||||
this.scrubThumbnails = false,
|
||||
this.folderGrouping = false,
|
||||
this.lyrics = false,
|
||||
this.instantMix = false,
|
||||
this.audioTranscoding = false,
|
||||
});
|
||||
|
||||
/// Defaults for a fully-featured Plex server.
|
||||
static const ServerCapabilities plex = ServerCapabilities(
|
||||
serverSidePlayQueue: true,
|
||||
serverSidePlaylists: true,
|
||||
liveTv: true,
|
||||
liveTvDvr: true,
|
||||
subtitleSearch: true,
|
||||
videoTranscoding: true,
|
||||
serverSideSync: true,
|
||||
richHubs: true,
|
||||
numericUserRating: true,
|
||||
userFavorites: false,
|
||||
continueWatchingRemoval: true,
|
||||
externalSubtitleSearch: true,
|
||||
trackPreferencePersistence: true,
|
||||
endpointFailover: true,
|
||||
offlineWatchQueue: true,
|
||||
discordRpc: true,
|
||||
richMetadataEdit: true,
|
||||
alphaBar: AlphaBarMode.scrollSnap,
|
||||
scrubThumbnails: true,
|
||||
folderGrouping: true,
|
||||
lyrics: true,
|
||||
instantMix: true,
|
||||
audioTranscoding: true,
|
||||
);
|
||||
|
||||
/// Defaults for a Jellyfin server.
|
||||
@@ -188,28 +102,17 @@ class ServerCapabilities {
|
||||
/// `/LiveTv/Programs`. Detection + channel listing are wired today;
|
||||
/// EPG and tuning are follow-ups.
|
||||
static const ServerCapabilities jellyfin = ServerCapabilities(
|
||||
serverSidePlayQueue: false,
|
||||
serverSidePlaylists: true,
|
||||
liveTv: true,
|
||||
liveTvDvr: false,
|
||||
subtitleSearch: false,
|
||||
videoTranscoding: true,
|
||||
serverSideSync: false,
|
||||
richHubs: false,
|
||||
numericUserRating: false,
|
||||
userFavorites: true,
|
||||
externalSubtitleSearch: false,
|
||||
trackPreferencePersistence: true,
|
||||
endpointFailover: true,
|
||||
offlineWatchQueue: false,
|
||||
discordRpc: false,
|
||||
richMetadataEdit: true,
|
||||
alphaBar: AlphaBarMode.nameStartsWithFilter,
|
||||
scrubThumbnails: true,
|
||||
folderGrouping: true,
|
||||
lyrics: true,
|
||||
instantMix: true,
|
||||
audioTranscoding: true,
|
||||
);
|
||||
|
||||
/// Every flag here is fixed per backend *kind* except [videoTranscoding],
|
||||
@@ -218,29 +121,18 @@ class ServerCapabilities {
|
||||
/// ever becomes a runtime probe.
|
||||
ServerCapabilities copyWith({bool? videoTranscoding}) {
|
||||
return ServerCapabilities(
|
||||
serverSidePlayQueue: serverSidePlayQueue,
|
||||
serverSidePlaylists: serverSidePlaylists,
|
||||
liveTv: liveTv,
|
||||
liveTvDvr: liveTvDvr,
|
||||
subtitleSearch: subtitleSearch,
|
||||
videoTranscoding: videoTranscoding ?? this.videoTranscoding,
|
||||
serverSideSync: serverSideSync,
|
||||
richHubs: richHubs,
|
||||
numericUserRating: numericUserRating,
|
||||
userFavorites: userFavorites,
|
||||
continueWatchingRemoval: continueWatchingRemoval,
|
||||
externalSubtitleSearch: externalSubtitleSearch,
|
||||
trackPreferencePersistence: trackPreferencePersistence,
|
||||
endpointFailover: endpointFailover,
|
||||
offlineWatchQueue: offlineWatchQueue,
|
||||
discordRpc: discordRpc,
|
||||
richMetadataEdit: richMetadataEdit,
|
||||
alphaBar: alphaBar,
|
||||
scrubThumbnails: scrubThumbnails,
|
||||
folderGrouping: folderGrouping,
|
||||
lyrics: lyrics,
|
||||
instantMix: instantMix,
|
||||
audioTranscoding: audioTranscoding,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../utils/json_utils.dart';
|
||||
import 'livetv_channel.dart';
|
||||
|
||||
part 'livetv_lineup.g.dart';
|
||||
|
||||
List<LiveTvChannel> _parseChannels(Object? raw) => parseFlexibleJsonList(raw, LiveTvChannel.fromJson);
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class LiveTvCountry {
|
||||
final String? key;
|
||||
final String? type;
|
||||
@JsonKey(defaultValue: '')
|
||||
final String title;
|
||||
@JsonKey(defaultValue: '')
|
||||
final String code;
|
||||
final String? language;
|
||||
final String? languageTitle;
|
||||
final String? example;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? flavor;
|
||||
|
||||
const LiveTvCountry({
|
||||
this.key,
|
||||
this.type,
|
||||
required this.title,
|
||||
required this.code,
|
||||
this.language,
|
||||
this.languageTitle,
|
||||
this.example,
|
||||
this.flavor,
|
||||
});
|
||||
|
||||
factory LiveTvCountry.fromJson(Map<String, dynamic> json) => _$LiveTvCountryFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class LiveTvLanguage {
|
||||
@JsonKey(defaultValue: '')
|
||||
final String code;
|
||||
@JsonKey(defaultValue: '')
|
||||
final String title;
|
||||
|
||||
const LiveTvLanguage({required this.code, required this.title});
|
||||
|
||||
factory LiveTvLanguage.fromJson(Map<String, dynamic> json) => _$LiveTvLanguageFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class LiveTvRegion {
|
||||
@JsonKey(defaultValue: '')
|
||||
final String key;
|
||||
final String? type;
|
||||
@JsonKey(defaultValue: '')
|
||||
final String title;
|
||||
|
||||
const LiveTvRegion({required this.key, this.type, required this.title});
|
||||
|
||||
factory LiveTvRegion.fromJson(Map<String, dynamic> json) => _$LiveTvRegionFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class LiveTvLineup {
|
||||
@JsonKey(defaultValue: '')
|
||||
final String uuid;
|
||||
final String? type;
|
||||
final String? title;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? lineupType;
|
||||
final String? location;
|
||||
@JsonKey(name: 'Channel', fromJson: _parseChannels)
|
||||
final List<LiveTvChannel> channels;
|
||||
|
||||
const LiveTvLineup({
|
||||
required this.uuid,
|
||||
this.type,
|
||||
this.title,
|
||||
this.lineupType,
|
||||
this.location,
|
||||
this.channels = const [],
|
||||
});
|
||||
|
||||
factory LiveTvLineup.fromJson(Map<String, dynamic> json) => _$LiveTvLineupFromJson(json);
|
||||
}
|
||||
|
||||
class LiveTvLineupResult {
|
||||
final String? lineupGroupUuid;
|
||||
final List<LiveTvLineup> lineups;
|
||||
|
||||
const LiveTvLineupResult({this.lineupGroupUuid, required this.lineups});
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'livetv_lineup.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LiveTvCountry _$LiveTvCountryFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvCountry(
|
||||
key: json['key'] as String?,
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
language: json['language'] as String?,
|
||||
languageTitle: json['languageTitle'] as String?,
|
||||
example: json['example'] as String?,
|
||||
flavor: flexibleInt(json['flavor']),
|
||||
);
|
||||
|
||||
LiveTvLanguage _$LiveTvLanguageFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvLanguage(
|
||||
code: json['code'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
);
|
||||
|
||||
LiveTvRegion _$LiveTvRegionFromJson(Map<String, dynamic> json) => LiveTvRegion(
|
||||
key: json['key'] as String? ?? '',
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String? ?? '',
|
||||
);
|
||||
|
||||
LiveTvLineup _$LiveTvLineupFromJson(Map<String, dynamic> json) => LiveTvLineup(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
type: json['type'] as String?,
|
||||
title: json['title'] as String?,
|
||||
lineupType: flexibleInt(json['lineupType']),
|
||||
location: json['location'] as String?,
|
||||
channels: json['Channel'] == null
|
||||
? const []
|
||||
: _parseChannels(json['Channel']),
|
||||
);
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../utils/json_utils.dart';
|
||||
|
||||
part 'livetv_server_status.g.dart';
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class LiveTvServerStatus {
|
||||
@JsonKey(name: 'livetv', fromJson: flexibleInt)
|
||||
final int? liveTvCount;
|
||||
@JsonKey(fromJson: flexibleBoolNullable)
|
||||
final bool? allowTuners;
|
||||
final String? ownerFeatures;
|
||||
|
||||
const LiveTvServerStatus({this.liveTvCount, this.allowTuners, this.ownerFeatures});
|
||||
|
||||
factory LiveTvServerStatus.fromJson(Map<String, dynamic> json) => _$LiveTvServerStatusFromJson(json);
|
||||
|
||||
Set<String> get ownerFeatureSet =>
|
||||
(ownerFeatures ?? '').split(',').map((feature) => feature.trim()).where((feature) => feature.isNotEmpty).toSet();
|
||||
|
||||
bool get hasConfiguredDvr => (liveTvCount ?? 0) > 0;
|
||||
bool get supportsTuners => allowTuners != false;
|
||||
bool get hasDvrFeature => ownerFeatureSet.contains('dvr');
|
||||
bool get hasLiveTvFeature => ownerFeatureSet.contains('livetv');
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'livetv_server_status.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LiveTvServerStatus _$LiveTvServerStatusFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvServerStatus(
|
||||
liveTvCount: flexibleInt(json['livetv']),
|
||||
allowTuners: flexibleBoolNullable(json['allowTuners']),
|
||||
ownerFeatures: json['ownerFeatures'] as String?,
|
||||
);
|
||||
@@ -1,66 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../utils/json_utils.dart';
|
||||
import 'livetv_capture_buffer.dart';
|
||||
import 'livetv_program.dart';
|
||||
import 'media_grab_operation.dart';
|
||||
|
||||
part 'livetv_session.g.dart';
|
||||
|
||||
LiveTvProgram? _programFromRaw(Object? raw) => parseFlexibleJsonObject(raw, LiveTvProgram.fromJson);
|
||||
|
||||
MediaGrabOperation? _grabOperationFromRaw(Object? raw) => parseFlexibleJsonObject(raw, MediaGrabOperation.fromJson);
|
||||
|
||||
CaptureBuffer? _captureBufferFromRaw(Object? raw) {
|
||||
final map = firstFlexibleMap(raw);
|
||||
if (map == null) return null;
|
||||
final session = firstFlexibleMap(map['TranscodeSession']) ?? map;
|
||||
return CaptureBuffer.fromTranscodeSession(session);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class LiveTvSession {
|
||||
@JsonKey(readValue: readStringField, defaultValue: '')
|
||||
final String sessionID;
|
||||
@JsonKey(readValue: readStringField)
|
||||
final String? dvrID;
|
||||
final String? channelIdentifier;
|
||||
final String? channelCallSign;
|
||||
final String? channelTitle;
|
||||
final String? activityUUID;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? currentPosition;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? nextPosition;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? startedAt;
|
||||
@JsonKey(name: 'CaptureBuffer', fromJson: _captureBufferFromRaw)
|
||||
final CaptureBuffer? captureBuffer;
|
||||
@JsonKey(name: 'MediaGrabOperation', fromJson: _grabOperationFromRaw)
|
||||
final MediaGrabOperation? grabOperation;
|
||||
@JsonKey(name: 'Timeline', fromJson: firstFlexibleMap)
|
||||
final Map<String, dynamic>? timeline;
|
||||
@JsonKey(name: 'AiringMetadataItem', fromJson: _programFromRaw)
|
||||
final LiveTvProgram? airingMetadataItem;
|
||||
@JsonKey(name: 'UpNextMetadataItem', fromJson: _programFromRaw)
|
||||
final LiveTvProgram? upNextMetadataItem;
|
||||
|
||||
const LiveTvSession({
|
||||
required this.sessionID,
|
||||
this.dvrID,
|
||||
this.channelIdentifier,
|
||||
this.channelCallSign,
|
||||
this.channelTitle,
|
||||
this.activityUUID,
|
||||
this.currentPosition,
|
||||
this.nextPosition,
|
||||
this.startedAt,
|
||||
this.captureBuffer,
|
||||
this.grabOperation,
|
||||
this.timeline,
|
||||
this.airingMetadataItem,
|
||||
this.upNextMetadataItem,
|
||||
});
|
||||
|
||||
factory LiveTvSession.fromJson(Map<String, dynamic> json) => _$LiveTvSessionFromJson(json);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'livetv_session.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LiveTvSession _$LiveTvSessionFromJson(Map<String, dynamic> json) =>
|
||||
LiveTvSession(
|
||||
sessionID: readStringField(json, 'sessionID') as String? ?? '',
|
||||
dvrID: readStringField(json, 'dvrID') as String?,
|
||||
channelIdentifier: json['channelIdentifier'] as String?,
|
||||
channelCallSign: json['channelCallSign'] as String?,
|
||||
channelTitle: json['channelTitle'] as String?,
|
||||
activityUUID: json['activityUUID'] as String?,
|
||||
currentPosition: flexibleInt(json['currentPosition']),
|
||||
nextPosition: flexibleInt(json['nextPosition']),
|
||||
startedAt: flexibleInt(json['startedAt']),
|
||||
captureBuffer: _captureBufferFromRaw(json['CaptureBuffer']),
|
||||
grabOperation: _grabOperationFromRaw(json['MediaGrabOperation']),
|
||||
timeline: firstFlexibleMap(json['Timeline']),
|
||||
airingMetadataItem: _programFromRaw(json['AiringMetadataItem']),
|
||||
upNextMetadataItem: _programFromRaw(json['UpNextMetadataItem']),
|
||||
);
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../utils/json_utils.dart';
|
||||
import 'livetv_dvr.dart';
|
||||
import 'media_subscription.dart';
|
||||
|
||||
part 'media_grabber_device.g.dart';
|
||||
|
||||
List<ChannelMapping> _parseChannelMappings(Object? raw) => parseFlexibleJsonList(raw, ChannelMapping.fromJson);
|
||||
|
||||
List<SubscriptionSetting> _parseSettings(Object? raw) => parseFlexibleJsonList(raw, SubscriptionSetting.fromJson);
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class MediaGrabber {
|
||||
@JsonKey(defaultValue: '')
|
||||
final String identifier;
|
||||
final String? protocol;
|
||||
final String? title;
|
||||
|
||||
const MediaGrabber({required this.identifier, this.protocol, this.title});
|
||||
|
||||
factory MediaGrabber.fromJson(Map<String, dynamic> json) => _$MediaGrabberFromJson(json);
|
||||
}
|
||||
|
||||
/// Tuner/grabber device known to Plex Media Server.
|
||||
@JsonSerializable(createToJson: false)
|
||||
class MediaGrabberDevice {
|
||||
@JsonKey(defaultValue: '')
|
||||
final String key;
|
||||
@JsonKey(defaultValue: '')
|
||||
final String uuid;
|
||||
final String? uri;
|
||||
final String? protocol;
|
||||
final String? title;
|
||||
final String? make;
|
||||
final String? model;
|
||||
final String? modelNumber;
|
||||
final String? firmware;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? tuners;
|
||||
final String? sources;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? status;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? state;
|
||||
@JsonKey(fromJson: flexibleInt)
|
||||
final int? lastSeenAt;
|
||||
@JsonKey(name: 'ChannelMapping', fromJson: _parseChannelMappings)
|
||||
final List<ChannelMapping> channelMappings;
|
||||
@JsonKey(name: 'Setting', fromJson: _parseSettings)
|
||||
final List<SubscriptionSetting> settings;
|
||||
|
||||
const MediaGrabberDevice({
|
||||
required this.key,
|
||||
required this.uuid,
|
||||
this.uri,
|
||||
this.protocol,
|
||||
this.title,
|
||||
this.make,
|
||||
this.model,
|
||||
this.modelNumber,
|
||||
this.firmware,
|
||||
this.tuners,
|
||||
this.sources,
|
||||
this.status,
|
||||
this.state,
|
||||
this.lastSeenAt,
|
||||
this.channelMappings = const [],
|
||||
this.settings = const [],
|
||||
});
|
||||
|
||||
factory MediaGrabberDevice.fromJson(Map<String, dynamic> json) => _$MediaGrabberDeviceFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class MediaGrabberDeviceChannel {
|
||||
@JsonKey(readValue: readStringField, defaultValue: '')
|
||||
final String identifier;
|
||||
@JsonKey(readValue: readStringField)
|
||||
final String? key;
|
||||
@JsonKey(readValue: readStringField)
|
||||
final String? name;
|
||||
@JsonKey(fromJson: flexibleBool)
|
||||
final bool drm;
|
||||
@JsonKey(fromJson: flexibleBool)
|
||||
final bool hd;
|
||||
|
||||
const MediaGrabberDeviceChannel({required this.identifier, this.key, this.name, this.drm = false, this.hd = false});
|
||||
|
||||
factory MediaGrabberDeviceChannel.fromJson(Map<String, dynamic> json) => _$MediaGrabberDeviceChannelFromJson(json);
|
||||
}
|
||||
|
||||
class MediaGrabberChannelMapRequest {
|
||||
final List<String> channelsEnabled;
|
||||
final Map<String, String> channelMapping;
|
||||
final Map<String, String> channelMappingByKey;
|
||||
|
||||
const MediaGrabberChannelMapRequest({
|
||||
this.channelsEnabled = const [],
|
||||
this.channelMapping = const {},
|
||||
this.channelMappingByKey = const {},
|
||||
});
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'media_grabber_device.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MediaGrabber _$MediaGrabberFromJson(Map<String, dynamic> json) => MediaGrabber(
|
||||
identifier: json['identifier'] as String? ?? '',
|
||||
protocol: json['protocol'] as String?,
|
||||
title: json['title'] as String?,
|
||||
);
|
||||
|
||||
MediaGrabberDevice _$MediaGrabberDeviceFromJson(Map<String, dynamic> json) =>
|
||||
MediaGrabberDevice(
|
||||
key: json['key'] as String? ?? '',
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
uri: json['uri'] as String?,
|
||||
protocol: json['protocol'] as String?,
|
||||
title: json['title'] as String?,
|
||||
make: json['make'] as String?,
|
||||
model: json['model'] as String?,
|
||||
modelNumber: json['modelNumber'] as String?,
|
||||
firmware: json['firmware'] as String?,
|
||||
tuners: flexibleInt(json['tuners']),
|
||||
sources: json['sources'] as String?,
|
||||
status: flexibleInt(json['status']),
|
||||
state: flexibleInt(json['state']),
|
||||
lastSeenAt: flexibleInt(json['lastSeenAt']),
|
||||
channelMappings: json['ChannelMapping'] == null
|
||||
? const []
|
||||
: _parseChannelMappings(json['ChannelMapping']),
|
||||
settings: json['Setting'] == null
|
||||
? const []
|
||||
: _parseSettings(json['Setting']),
|
||||
);
|
||||
|
||||
MediaGrabberDeviceChannel _$MediaGrabberDeviceChannelFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => MediaGrabberDeviceChannel(
|
||||
identifier: readStringField(json, 'identifier') as String? ?? '',
|
||||
key: readStringField(json, 'key') as String?,
|
||||
name: readStringField(json, 'name') as String?,
|
||||
drm: json['drm'] == null ? false : flexibleBool(json['drm']),
|
||||
hd: json['hd'] == null ? false : flexibleBool(json['hd']),
|
||||
);
|
||||
@@ -1,26 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../focus/input_mode_tracker.dart';
|
||||
import '../../../media/library_query.dart';
|
||||
import '../../../media/media_item.dart';
|
||||
import '../../../mixins/library_tab_focus_mixin.dart';
|
||||
import '../../../mixins/paginated_item_loader.dart';
|
||||
import '../../../mixins/standard_paginated_view.dart';
|
||||
import '../../../services/settings_service.dart';
|
||||
import '../../../utils/error_message_utils.dart';
|
||||
import '../../../utils/layout_constants.dart';
|
||||
import '../../../utils/library_refresh_notifier.dart';
|
||||
import '../../../utils/media_server_http_client.dart';
|
||||
import '../../../utils/platform_detector.dart';
|
||||
import '../../../widgets/card_inflation_budget.dart';
|
||||
import '../../../widgets/focusable_media_card.dart';
|
||||
import '../../../widgets/media_card_sliver_layout.dart';
|
||||
import '../../../widgets/settings_builder.dart';
|
||||
import '../../../widgets/skeleton_media_card.dart';
|
||||
import '../../../widgets/sliver_child_memo.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../main_screen.dart';
|
||||
import 'base_library_tab.dart';
|
||||
import 'paginated_card_grid_tab.dart';
|
||||
|
||||
/// Collections tab for library screen.
|
||||
/// Plex scopes collections to the library; Jellyfin exposes a shared BoxSets root.
|
||||
@@ -40,24 +26,13 @@ class LibraryCollectionsTab extends BaseLibraryTab<MediaItem> {
|
||||
State<LibraryCollectionsTab> createState() => _LibraryCollectionsTabState();
|
||||
}
|
||||
|
||||
class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, LibraryCollectionsTab>
|
||||
with
|
||||
LibraryTabFocusMixin<LibraryCollectionsTab>,
|
||||
PaginatedItemLoader<MediaItem, LibraryCollectionsTab>,
|
||||
StandardPaginatedView<MediaItem, LibraryCollectionsTab>,
|
||||
SkeletonUpgradeScheduler {
|
||||
static const int _pageSize = 36;
|
||||
|
||||
/// Reuses card widgets across delegate swaps so tab-level setStates
|
||||
/// (pagination, refreshes) don't rebuild every realized card inside layout.
|
||||
final SliverChildMemo<MediaItem> _cardMemo = SliverChildMemo<MediaItem>();
|
||||
class _LibraryCollectionsTabState extends PaginatedCardGridTabState<MediaItem, LibraryCollectionsTab> {
|
||||
@override
|
||||
int get pageSize => 36;
|
||||
|
||||
@override
|
||||
String get focusNodeDebugLabel => 'collections_first_item';
|
||||
|
||||
@override
|
||||
int get itemCount => totalSize;
|
||||
|
||||
@override
|
||||
IconData get emptyIcon => Symbols.collections_rounded;
|
||||
|
||||
@@ -71,7 +46,7 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
||||
Stream<void>? getRefreshStream() => LibraryRefreshNotifier().collectionsStream;
|
||||
|
||||
@override
|
||||
Future<List<MediaItem>> loadData() async => const [];
|
||||
String idOf(MediaItem item) => item.id;
|
||||
|
||||
@override
|
||||
Future<LibraryPage<MediaItem>> fetchPage(int start, int size, AbortController? abort) {
|
||||
@@ -80,42 +55,7 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> loadItems() {
|
||||
return loadStandardPaginatedItems(
|
||||
pageSize: _pageSize,
|
||||
errorMessageFor: (error, stackTrace) => localizedLoadErrorMessage(error, stackTrace, context: errorContext),
|
||||
onLoaded: (_, _) => markItemsLoaded(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildContent(List<MediaItem> items) {
|
||||
return SettingsBuilder(
|
||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||
builder: (context) {
|
||||
final settings = SettingsService.instance;
|
||||
final viewMode = settings.read(SettingsService.viewMode);
|
||||
final density = settings.read(SettingsService.libraryDensity);
|
||||
final fullCardLayout = PlatformDetector.isTV() && settings.read(SettingsService.tvFullCardLayout);
|
||||
return CustomScrollView(
|
||||
clipBehavior: Clip.none,
|
||||
slivers: [
|
||||
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
|
||||
_buildItemsSliver(viewMode, density, fullCardLayout: fullCardLayout),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static const double _focusDecorationPadding = 3.0;
|
||||
|
||||
EdgeInsets get _effectivePadding {
|
||||
final base = GridLayoutConstants.gridPadding;
|
||||
return base.copyWith(top: base.top + _focusDecorationPadding);
|
||||
}
|
||||
|
||||
bool get _usesSquareCards {
|
||||
bool get usesSquareCards {
|
||||
final loaded = loadedItems.values;
|
||||
return loaded.isNotEmpty && loaded.every(_isMusicCollection);
|
||||
}
|
||||
@@ -124,94 +64,4 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
||||
// safe fallback. Jellyfin BoxSets are server-wide and must opt in per item.
|
||||
bool _isMusicCollection(MediaItem item) =>
|
||||
item.kind.isMusic || (item is PlexMediaItem && widget.library.kind.isMusic);
|
||||
|
||||
Widget _buildItemsSliver(ViewMode viewMode, int density, {required bool fullCardLayout}) {
|
||||
final shape = _usesSquareCards ? CardShape.square : null;
|
||||
final useFullCardLayout = fullCardLayout && shape != CardShape.square;
|
||||
return MediaCardSliverLayout(
|
||||
viewMode: viewMode,
|
||||
itemCount: totalSize,
|
||||
density: density,
|
||||
padding: _effectivePadding,
|
||||
fullBleedImage: useFullCardLayout,
|
||||
shape: shape,
|
||||
listEpoch: (ViewMode.list, totalSize, density, shape),
|
||||
gridEpochBuilder: (geometry) =>
|
||||
(ViewMode.grid, geometry.columnCount, totalSize, useFullCardLayout, density, shape),
|
||||
itemBuilder: (context, position) {
|
||||
final index = position.index;
|
||||
final item = loadedItems[index];
|
||||
if (item == null) {
|
||||
ensureIndexLoaded(index, pageSize: _pageSize);
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
if (!position.isGrid) {
|
||||
return _cardMemo.widgetFor(
|
||||
index,
|
||||
item,
|
||||
epoch: position.layoutEpoch!,
|
||||
build: () =>
|
||||
_buildMediaCardItem(index, isFirstRow: position.isFirstRow, isFirstColumn: true, disableScale: true),
|
||||
);
|
||||
}
|
||||
|
||||
final cached = _cardMemo.tryGet(index, item, epoch: position.layoutEpoch!);
|
||||
if (cached != null) return cached;
|
||||
if (CardInflationBudget.isScrollingContext(context) &&
|
||||
!InputModeTracker.isKeyboardMode(context) &&
|
||||
!CardInflationBudget.tryTake()) {
|
||||
scheduleSkeletonUpgrade();
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
return _cardMemo.widgetFor(
|
||||
index,
|
||||
item,
|
||||
epoch: position.layoutEpoch!,
|
||||
build: () => _buildMediaCardItem(
|
||||
index,
|
||||
isFirstRow: position.isFirstRow,
|
||||
isFirstColumn: position.isFirstColumn,
|
||||
fullBleedImage: useFullCardLayout,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaCardItem(
|
||||
int index, {
|
||||
required bool isFirstRow,
|
||||
required bool isFirstColumn,
|
||||
bool disableScale = false,
|
||||
bool fullBleedImage = false,
|
||||
}) {
|
||||
final item = loadedItems[index];
|
||||
if (item == null) {
|
||||
ensureIndexLoaded(index, pageSize: _pageSize);
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
|
||||
return FocusableMediaCard(
|
||||
key: Key(item.id),
|
||||
item: item,
|
||||
focusNode: index == 0 ? firstItemFocusNode : null,
|
||||
disableScale: disableScale,
|
||||
fullBleedImage: fullBleedImage,
|
||||
cardShapeOverride: _usesSquareCards ? CardShape.square : null,
|
||||
onListRefresh: loadItems,
|
||||
onNavigateUp: isFirstRow ? widget.onBack : null,
|
||||
onBack: widget.onBack,
|
||||
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToSidebar() {
|
||||
MainScreenFocusScope.focusSidebarOf(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposePagination();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../focus/input_mode_tracker.dart';
|
||||
import '../../../media/library_query.dart';
|
||||
import '../../../media/media_item.dart';
|
||||
import '../../../media/media_kind.dart';
|
||||
import '../../../media/media_playlist.dart';
|
||||
import '../../../mixins/library_tab_focus_mixin.dart';
|
||||
import '../../../mixins/paginated_item_loader.dart';
|
||||
import '../../../mixins/standard_paginated_view.dart';
|
||||
import '../../../services/settings_service.dart';
|
||||
import '../../../utils/error_message_utils.dart';
|
||||
import '../../../utils/layout_constants.dart';
|
||||
import '../../../utils/library_refresh_notifier.dart';
|
||||
import '../../../utils/media_server_http_client.dart';
|
||||
import '../../../utils/platform_detector.dart';
|
||||
import '../../../widgets/card_inflation_budget.dart';
|
||||
import '../../../widgets/focusable_media_card.dart';
|
||||
import '../../../widgets/media_card_sliver_layout.dart';
|
||||
import '../../../widgets/settings_builder.dart';
|
||||
import '../../../widgets/skeleton_media_card.dart';
|
||||
import '../../../widgets/sliver_child_memo.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../main_screen.dart';
|
||||
import 'base_library_tab.dart';
|
||||
import 'paginated_card_grid_tab.dart';
|
||||
|
||||
/// Playlists tab for library screen
|
||||
/// Shows playlists that contain items from the current library
|
||||
@@ -42,24 +27,13 @@ class LibraryPlaylistsTab extends BaseLibraryTab<MediaPlaylist> {
|
||||
State<LibraryPlaylistsTab> createState() => _LibraryPlaylistsTabState();
|
||||
}
|
||||
|
||||
class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, LibraryPlaylistsTab>
|
||||
with
|
||||
LibraryTabFocusMixin<LibraryPlaylistsTab>,
|
||||
PaginatedItemLoader<MediaPlaylist, LibraryPlaylistsTab>,
|
||||
StandardPaginatedView<MediaPlaylist, LibraryPlaylistsTab>,
|
||||
SkeletonUpgradeScheduler {
|
||||
static const int _pageSize = 200;
|
||||
|
||||
/// Reuses card widgets across delegate swaps so tab-level setStates
|
||||
/// (pagination, refreshes) don't rebuild every realized card inside layout.
|
||||
final SliverChildMemo<MediaPlaylist> _cardMemo = SliverChildMemo<MediaPlaylist>();
|
||||
class _LibraryPlaylistsTabState extends PaginatedCardGridTabState<MediaPlaylist, LibraryPlaylistsTab> {
|
||||
@override
|
||||
int get pageSize => 200;
|
||||
|
||||
@override
|
||||
String get focusNodeDebugLabel => 'playlists_first_item';
|
||||
|
||||
@override
|
||||
int get itemCount => totalSize;
|
||||
|
||||
@override
|
||||
IconData get emptyIcon => Symbols.playlist_play_rounded;
|
||||
|
||||
@@ -73,7 +47,7 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
||||
Stream<void>? getRefreshStream() => LibraryRefreshNotifier().playlistsStream;
|
||||
|
||||
@override
|
||||
Future<List<MediaPlaylist>> loadData() async => const [];
|
||||
String idOf(MediaPlaylist playlist) => playlist.id;
|
||||
|
||||
@override
|
||||
Future<LibraryPage<MediaPlaylist>> fetchPage(int start, int size, AbortController? abort) {
|
||||
@@ -86,130 +60,5 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> loadItems() {
|
||||
return loadStandardPaginatedItems(
|
||||
pageSize: _pageSize,
|
||||
errorMessageFor: (error, stackTrace) => localizedLoadErrorMessage(error, stackTrace, context: errorContext),
|
||||
onLoaded: (_, _) => markItemsLoaded(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildContent(List<MediaPlaylist> items) {
|
||||
return SettingsBuilder(
|
||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||
builder: (context) {
|
||||
final settings = SettingsService.instance;
|
||||
final viewMode = settings.read(SettingsService.viewMode);
|
||||
final density = settings.read(SettingsService.libraryDensity);
|
||||
final fullCardLayout = PlatformDetector.isTV() && settings.read(SettingsService.tvFullCardLayout);
|
||||
return CustomScrollView(
|
||||
clipBehavior: Clip.none,
|
||||
slivers: [
|
||||
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
|
||||
_buildItemsSliver(viewMode, density, fullCardLayout: fullCardLayout),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static const double _focusDecorationPadding = 3.0;
|
||||
|
||||
EdgeInsets get _effectivePadding {
|
||||
final base = GridLayoutConstants.gridPadding;
|
||||
return base.copyWith(top: base.top + _focusDecorationPadding);
|
||||
}
|
||||
|
||||
bool get _usesSquareCards => widget.library.kind.isMusic;
|
||||
|
||||
Widget _buildItemsSliver(ViewMode viewMode, int density, {required bool fullCardLayout}) {
|
||||
final shape = _usesSquareCards ? CardShape.square : null;
|
||||
final useFullCardLayout = fullCardLayout && shape != CardShape.square;
|
||||
return MediaCardSliverLayout(
|
||||
viewMode: viewMode,
|
||||
itemCount: totalSize,
|
||||
density: density,
|
||||
padding: _effectivePadding,
|
||||
fullBleedImage: useFullCardLayout,
|
||||
shape: shape,
|
||||
listEpoch: (ViewMode.list, totalSize, density, shape),
|
||||
gridEpochBuilder: (geometry) =>
|
||||
(ViewMode.grid, geometry.columnCount, totalSize, useFullCardLayout, density, shape),
|
||||
itemBuilder: (context, position) {
|
||||
final index = position.index;
|
||||
final playlist = loadedItems[index];
|
||||
if (playlist == null) {
|
||||
ensureIndexLoaded(index, pageSize: _pageSize);
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
if (!position.isGrid) {
|
||||
return _cardMemo.widgetFor(
|
||||
index,
|
||||
playlist,
|
||||
epoch: position.layoutEpoch!,
|
||||
build: () =>
|
||||
_buildPlaylistCard(index, isFirstRow: position.isFirstRow, isFirstColumn: true, disableScale: true),
|
||||
);
|
||||
}
|
||||
|
||||
final cached = _cardMemo.tryGet(index, playlist, epoch: position.layoutEpoch!);
|
||||
if (cached != null) return cached;
|
||||
if (CardInflationBudget.isScrollingContext(context) &&
|
||||
!InputModeTracker.isKeyboardMode(context) &&
|
||||
!CardInflationBudget.tryTake()) {
|
||||
scheduleSkeletonUpgrade();
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
return _cardMemo.widgetFor(
|
||||
index,
|
||||
playlist,
|
||||
epoch: position.layoutEpoch!,
|
||||
build: () => _buildPlaylistCard(
|
||||
index,
|
||||
isFirstRow: position.isFirstRow,
|
||||
isFirstColumn: position.isFirstColumn,
|
||||
fullBleedImage: useFullCardLayout,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlaylistCard(
|
||||
int index, {
|
||||
required bool isFirstRow,
|
||||
required bool isFirstColumn,
|
||||
bool disableScale = false,
|
||||
bool fullBleedImage = false,
|
||||
}) {
|
||||
final playlist = loadedItems[index];
|
||||
if (playlist == null) {
|
||||
ensureIndexLoaded(index, pageSize: _pageSize);
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
|
||||
return FocusableMediaCard(
|
||||
key: Key(playlist.id),
|
||||
item: playlist,
|
||||
focusNode: index == 0 ? firstItemFocusNode : null,
|
||||
disableScale: disableScale,
|
||||
fullBleedImage: fullBleedImage,
|
||||
cardShapeOverride: _usesSquareCards ? CardShape.square : null,
|
||||
onListRefresh: loadItems,
|
||||
onNavigateUp: isFirstRow ? widget.onBack : null,
|
||||
onBack: widget.onBack,
|
||||
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToSidebar() {
|
||||
MainScreenFocusScope.focusSidebarOf(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposePagination();
|
||||
super.dispose();
|
||||
}
|
||||
bool get usesSquareCards => widget.library.kind.isMusic;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../focus/input_mode_tracker.dart';
|
||||
import '../../../media/media_item.dart';
|
||||
import '../../../mixins/library_tab_focus_mixin.dart';
|
||||
import '../../../mixins/paginated_item_loader.dart';
|
||||
import '../../../mixins/standard_paginated_view.dart';
|
||||
import '../../../services/settings_service.dart';
|
||||
import '../../../utils/error_message_utils.dart';
|
||||
import '../../../utils/layout_constants.dart';
|
||||
import '../../../utils/platform_detector.dart';
|
||||
import '../../../widgets/card_inflation_budget.dart';
|
||||
import '../../../widgets/focusable_media_card.dart';
|
||||
import '../../../widgets/media_card_sliver_layout.dart';
|
||||
import '../../../widgets/settings_builder.dart';
|
||||
import '../../../widgets/skeleton_media_card.dart';
|
||||
import '../../../widgets/sliver_child_memo.dart';
|
||||
import '../../main_screen.dart';
|
||||
import 'base_library_tab.dart';
|
||||
|
||||
/// Library tabs whose whole body is one paginated grid of media cards.
|
||||
///
|
||||
/// Owns the grid: sparse page loading, the card widget memo, the inflation
|
||||
/// budget and skeleton-upgrade handshake, and first-item/sidebar focus wiring.
|
||||
/// Subclasses supply only what differs per tab — [pageSize], [fetchPage],
|
||||
/// [usesSquareCards], [idOf], and the empty/error chrome from
|
||||
/// [BaseLibraryTabState].
|
||||
abstract class PaginatedCardGridTabState<T extends Object, W extends BaseLibraryTab<T>>
|
||||
extends BaseLibraryTabState<T, W>
|
||||
with
|
||||
LibraryTabFocusMixin<W>,
|
||||
PaginatedItemLoader<T, W>,
|
||||
StandardPaginatedView<T, W>,
|
||||
SkeletonUpgradeScheduler<W> {
|
||||
static const double _focusDecorationPadding = 3.0;
|
||||
|
||||
/// Reuses card widgets across delegate swaps so tab-level setStates
|
||||
/// (pagination, refreshes) don't rebuild every realized card inside layout.
|
||||
final SliverChildMemo<T> _cardMemo = SliverChildMemo<T>();
|
||||
|
||||
/// Items fetched per page.
|
||||
int get pageSize;
|
||||
|
||||
/// Whether cards render with the square container silhouette.
|
||||
bool get usesSquareCards;
|
||||
|
||||
/// Card key for [item]. The tabs' item types share no common supertype.
|
||||
String idOf(T item);
|
||||
|
||||
@override
|
||||
int get itemCount => totalSize;
|
||||
|
||||
@override
|
||||
Future<List<T>> loadData() async => <T>[];
|
||||
|
||||
@override
|
||||
Future<void> loadItems() {
|
||||
return loadStandardPaginatedItems(
|
||||
pageSize: pageSize,
|
||||
errorMessageFor: (error, stackTrace) => localizedLoadErrorMessage(error, stackTrace, context: errorContext),
|
||||
onLoaded: (_, _) => markItemsLoaded(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildContent(List<T> items) {
|
||||
return SettingsBuilder(
|
||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||
builder: (context) {
|
||||
final settings = SettingsService.instance;
|
||||
final viewMode = settings.read(SettingsService.viewMode);
|
||||
final density = settings.read(SettingsService.libraryDensity);
|
||||
final fullCardLayout = PlatformDetector.isTV() && settings.read(SettingsService.tvFullCardLayout);
|
||||
return CustomScrollView(
|
||||
clipBehavior: Clip.none,
|
||||
slivers: [
|
||||
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
|
||||
_buildItemsSliver(viewMode, density, fullCardLayout: fullCardLayout),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
EdgeInsets get _effectivePadding {
|
||||
final base = GridLayoutConstants.gridPadding;
|
||||
return base.copyWith(top: base.top + _focusDecorationPadding);
|
||||
}
|
||||
|
||||
Widget _buildItemsSliver(ViewMode viewMode, int density, {required bool fullCardLayout}) {
|
||||
final shape = usesSquareCards ? CardShape.square : null;
|
||||
final useFullCardLayout = fullCardLayout && shape != CardShape.square;
|
||||
return MediaCardSliverLayout(
|
||||
viewMode: viewMode,
|
||||
itemCount: totalSize,
|
||||
density: density,
|
||||
padding: _effectivePadding,
|
||||
fullBleedImage: useFullCardLayout,
|
||||
shape: shape,
|
||||
listEpoch: (ViewMode.list, totalSize, density, shape),
|
||||
gridEpochBuilder: (geometry) =>
|
||||
(ViewMode.grid, geometry.columnCount, totalSize, useFullCardLayout, density, shape),
|
||||
itemBuilder: (context, position) {
|
||||
final index = position.index;
|
||||
final item = loadedItems[index];
|
||||
if (item == null) {
|
||||
ensureIndexLoaded(index, pageSize: pageSize);
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
if (!position.isGrid) {
|
||||
return _cardMemo.widgetFor(
|
||||
index,
|
||||
item,
|
||||
epoch: position.layoutEpoch!,
|
||||
build: () => _buildCard(index, isFirstRow: position.isFirstRow, isFirstColumn: true, disableScale: true),
|
||||
);
|
||||
}
|
||||
|
||||
final cached = _cardMemo.tryGet(index, item, epoch: position.layoutEpoch!);
|
||||
if (cached != null) return cached;
|
||||
if (CardInflationBudget.isScrollingContext(context) &&
|
||||
!InputModeTracker.isKeyboardMode(context) &&
|
||||
!CardInflationBudget.tryTake()) {
|
||||
scheduleSkeletonUpgrade();
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
return _cardMemo.widgetFor(
|
||||
index,
|
||||
item,
|
||||
epoch: position.layoutEpoch!,
|
||||
build: () => _buildCard(
|
||||
index,
|
||||
isFirstRow: position.isFirstRow,
|
||||
isFirstColumn: position.isFirstColumn,
|
||||
fullBleedImage: useFullCardLayout,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard(
|
||||
int index, {
|
||||
required bool isFirstRow,
|
||||
required bool isFirstColumn,
|
||||
bool disableScale = false,
|
||||
bool fullBleedImage = false,
|
||||
}) {
|
||||
final item = loadedItems[index];
|
||||
if (item == null) {
|
||||
ensureIndexLoaded(index, pageSize: pageSize);
|
||||
return const SkeletonMediaCard();
|
||||
}
|
||||
|
||||
return FocusableMediaCard(
|
||||
key: Key(idOf(item)),
|
||||
item: item,
|
||||
focusNode: index == 0 ? firstItemFocusNode : null,
|
||||
disableScale: disableScale,
|
||||
fullBleedImage: fullBleedImage,
|
||||
cardShapeOverride: usesSquareCards ? CardShape.square : null,
|
||||
onListRefresh: loadItems,
|
||||
onNavigateUp: isFirstRow ? widget.onBack : null,
|
||||
onBack: widget.onBack,
|
||||
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToSidebar() {
|
||||
MainScreenFocusScope.focusSidebarOf(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposePagination();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,23 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../../models/trackers/anime_lists_mapping.dart';
|
||||
import '../../utils/abortable_http_request.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/json_utils.dart';
|
||||
import '../../utils/platform_http_client_stub.dart'
|
||||
if (dart.library.io) '../../utils/platform_http_client_io.dart'
|
||||
as platform;
|
||||
import '../base_shared_preferences_service.dart';
|
||||
import 'etag_cached_remote_store.dart';
|
||||
|
||||
class AnimeListsIndex {
|
||||
class AnimeListsIndex implements RemoteIndex {
|
||||
final Map<int, List<AnimeListEntry>> byTvdb;
|
||||
final Map<int, List<AnimeListEntry>> byTmdbTv;
|
||||
|
||||
const AnimeListsIndex({required this.byTvdb, required this.byTmdbTv});
|
||||
|
||||
@override
|
||||
bool get isEmpty => byTvdb.isEmpty && byTmdbTv.isEmpty;
|
||||
|
||||
@override
|
||||
String get logSummary => '${byTvdb.length} tvdb entries';
|
||||
}
|
||||
|
||||
abstract interface class AnimeListsMappingLookup {
|
||||
@@ -32,88 +28,25 @@ abstract interface class AnimeListsMappingLookup {
|
||||
Future<Set<int>> lookupAnimeIdsForShow({int? tvdbId, int? tmdbId});
|
||||
}
|
||||
|
||||
class AnimeListsMappingStore implements AnimeListsMappingLookup {
|
||||
static const String _diskFileName = 'anime-list.xml';
|
||||
static const String _prefsEtagKey = 'anime_lists_etag';
|
||||
static const String _prefsLastCheckKey = 'anime_lists_last_check';
|
||||
static const String _sourceUrl = 'https://cdn.jsdelivr.net/gh/Anime-Lists/anime-lists@master/anime-list.xml';
|
||||
|
||||
static const Duration _refreshInterval = Duration(days: 7);
|
||||
static const Duration _requestTimeout = Duration(seconds: 60);
|
||||
|
||||
AnimeListsMappingStore._();
|
||||
static final AnimeListsMappingStore instance = AnimeListsMappingStore._();
|
||||
|
||||
AnimeListsIndex? _index;
|
||||
Future<AnimeListsIndex>? _loading;
|
||||
bool _refreshRunning = false;
|
||||
|
||||
Future<AnimeListsIndex> _ensureLoaded() async {
|
||||
final existing = _index;
|
||||
if (existing != null) return existing;
|
||||
final loading = _loading;
|
||||
if (loading != null) return loading;
|
||||
|
||||
final fresh = _loadOrFetch();
|
||||
_loading = fresh;
|
||||
try {
|
||||
final idx = await fresh;
|
||||
if (!idx.isEmpty) {
|
||||
_index = idx;
|
||||
unawaited(maybeRefresh());
|
||||
}
|
||||
return idx;
|
||||
} finally {
|
||||
_loading = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<AnimeListsIndex> _loadOrFetch() async {
|
||||
final path = await _diskPath();
|
||||
try {
|
||||
return await compute(_readAndParseAnimeLists, path);
|
||||
} on FileSystemException {
|
||||
appLogger.d('Anime-Lists: no disk cache, downloading from jsDelivr');
|
||||
final raw = await _download();
|
||||
if (raw == null) return const AnimeListsIndex(byTvdb: {}, byTmdbTv: {});
|
||||
return await compute(parseAnimeListsIndex, raw);
|
||||
} catch (e) {
|
||||
appLogger.w('Anime-Lists: parse failed - deleting disk copy so next lookup re-downloads', error: e);
|
||||
await _deleteDiskCopy();
|
||||
return const AnimeListsIndex(byTvdb: {}, byTmdbTv: {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _download() async {
|
||||
final client = platform.createPlatformClient();
|
||||
try {
|
||||
final res = await sendAbortableHttpRequest(
|
||||
client,
|
||||
'GET',
|
||||
Uri.parse(_sourceUrl),
|
||||
headers: const {'Accept': 'application/xml,text/xml'},
|
||||
timeout: _requestTimeout,
|
||||
operation: 'Anime-Lists mapping download',
|
||||
class AnimeListsMappingStore extends EtagCachedRemoteStore<AnimeListsIndex> implements AnimeListsMappingLookup {
|
||||
AnimeListsMappingStore._()
|
||||
: super(
|
||||
diskFileName: 'anime-list.xml',
|
||||
prefsEtagKey: 'anime_lists_etag',
|
||||
prefsLastCheckKey: 'anime_lists_last_check',
|
||||
sourceUrl: 'https://cdn.jsdelivr.net/gh/Anime-Lists/anime-lists@master/anime-list.xml',
|
||||
acceptHeader: 'application/xml,text/xml',
|
||||
logLabel: 'Anime-Lists',
|
||||
emptyIndex: const AnimeListsIndex(byTvdb: {}, byTmdbTv: {}),
|
||||
parse: parseAnimeListsIndex,
|
||||
readAndParse: _readAndParseAnimeLists,
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
appLogger.d('Anime-Lists: download returned HTTP ${res.statusCode}');
|
||||
return null;
|
||||
}
|
||||
await _writeDiskCopy(res.body, etag: res.headers['etag']);
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
await prefs.setInt(_prefsLastCheckKey, DateTime.now().millisecondsSinceEpoch);
|
||||
return res.body;
|
||||
} catch (e) {
|
||||
appLogger.w('Anime-Lists: download failed', error: e);
|
||||
return null;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
static final AnimeListsMappingStore instance = AnimeListsMappingStore._();
|
||||
|
||||
@override
|
||||
Future<AnimeEpisodeMatch?> lookupEpisode({int? tvdbId, int? tmdbId, int? season, int? episodeNumber}) async {
|
||||
final idx = await _ensureLoaded();
|
||||
final idx = await ensureLoaded();
|
||||
return lookupAnimeListEpisodeInIndex(
|
||||
idx,
|
||||
tvdbId: tvdbId,
|
||||
@@ -125,7 +58,7 @@ class AnimeListsMappingStore implements AnimeListsMappingLookup {
|
||||
|
||||
@override
|
||||
Future<Set<int>> lookupAnimeIdsForSeason({int? tvdbId, int? tmdbId, required int season}) async {
|
||||
final idx = await _ensureLoaded();
|
||||
final idx = await ensureLoaded();
|
||||
if (tvdbId != null) {
|
||||
final ids = _seasonAnimeIds(idx.byTvdb[tvdbId], AnimeListProvider.tvdb, season);
|
||||
if (ids.isNotEmpty) return ids;
|
||||
@@ -138,7 +71,7 @@ class AnimeListsMappingStore implements AnimeListsMappingLookup {
|
||||
|
||||
@override
|
||||
Future<Set<int>> lookupAnimeIdsForShow({int? tvdbId, int? tmdbId}) async {
|
||||
final idx = await _ensureLoaded();
|
||||
final idx = await ensureLoaded();
|
||||
if (tvdbId != null) {
|
||||
final entries = idx.byTvdb[tvdbId];
|
||||
if (entries != null && entries.isNotEmpty) return {for (final entry in entries) entry.anidbId};
|
||||
@@ -149,79 +82,6 @@ class AnimeListsMappingStore implements AnimeListsMappingLookup {
|
||||
}
|
||||
return const <int>{};
|
||||
}
|
||||
|
||||
Future<void> maybeRefresh() async {
|
||||
if (_refreshRunning) return;
|
||||
if (_index == null) return;
|
||||
_refreshRunning = true;
|
||||
try {
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
final lastCheck = prefs.getInt(_prefsLastCheckKey) ?? 0;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (now - lastCheck < _refreshInterval.inMilliseconds) return;
|
||||
|
||||
final etag = prefs.getString(_prefsEtagKey);
|
||||
final client = platform.createPlatformClient();
|
||||
try {
|
||||
final res = await sendAbortableHttpRequest(
|
||||
client,
|
||||
'GET',
|
||||
Uri.parse(_sourceUrl),
|
||||
headers: {'If-None-Match': ?etag, 'Accept': 'application/xml,text/xml'},
|
||||
timeout: _requestTimeout,
|
||||
operation: 'Anime-Lists mapping refresh',
|
||||
);
|
||||
await prefs.setInt(_prefsLastCheckKey, now);
|
||||
|
||||
if (res.statusCode == 304) {
|
||||
appLogger.d('Anime-Lists: mapping unchanged (304)');
|
||||
return;
|
||||
}
|
||||
if (res.statusCode != 200) {
|
||||
appLogger.d('Anime-Lists: refresh returned HTTP ${res.statusCode}');
|
||||
return;
|
||||
}
|
||||
|
||||
await _writeDiskCopy(res.body, etag: res.headers['etag']);
|
||||
final fresh = await compute(parseAnimeListsIndex, res.body);
|
||||
_index = fresh;
|
||||
appLogger.d('Anime-Lists: mapping refreshed (${fresh.byTvdb.length} tvdb entries)');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.d('Anime-Lists: refresh failed (non-fatal)', error: e);
|
||||
} finally {
|
||||
_refreshRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeDiskCopy(String body, {String? etag}) async {
|
||||
await File(await _diskPath()).writeAsString(body, flush: true);
|
||||
if (etag != null) {
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
await prefs.setString(_prefsEtagKey, etag);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteDiskCopy() async {
|
||||
try {
|
||||
await File(await _diskPath()).delete();
|
||||
} on FileSystemException {
|
||||
// Already gone.
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _diskPath() async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
return p.join(dir.path, _diskFileName);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void resetForTesting() {
|
||||
_index = null;
|
||||
_loading = null;
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../utils/abortable_http_request.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/platform_http_client_stub.dart'
|
||||
if (dart.library.io) '../../utils/platform_http_client_io.dart'
|
||||
as platform;
|
||||
import '../base_shared_preferences_service.dart';
|
||||
|
||||
/// An index parsed out of a cached remote file.
|
||||
abstract interface class RemoteIndex {
|
||||
bool get isEmpty;
|
||||
|
||||
/// Reported after a successful refresh, e.g. `'1234 tvdb entries'`.
|
||||
String get logSummary;
|
||||
}
|
||||
|
||||
/// Base for the mapping stores backed by a static file on jsDelivr.
|
||||
///
|
||||
/// Owns the whole lifecycle: lazy download on first use, a disk copy in the
|
||||
/// app-support directory, parsing in a background isolate, and a weekly
|
||||
/// conditional-GET ([maybeRefresh], If-None-Match) to pick up upstream changes.
|
||||
/// Subclasses supply the source, the cache keys and the two isolate entry
|
||||
/// points, and build their lookups on top of [ensureLoaded].
|
||||
abstract class EtagCachedRemoteStore<T extends RemoteIndex> {
|
||||
static const Duration _refreshInterval = Duration(days: 7);
|
||||
static const Duration _requestTimeout = Duration(seconds: 60);
|
||||
|
||||
final String diskFileName;
|
||||
final String prefsEtagKey;
|
||||
final String prefsLastCheckKey;
|
||||
final String sourceUrl;
|
||||
final String acceptHeader;
|
||||
|
||||
/// Prefixes log lines and the abortable-request operation names.
|
||||
final String logLabel;
|
||||
|
||||
/// Returned when nothing could be loaded; never cached.
|
||||
final T emptyIndex;
|
||||
|
||||
/// Parses a raw body. Top-level so it can run in a `compute` isolate.
|
||||
final T Function(String raw) parse;
|
||||
|
||||
/// Reads the disk copy and parses it inside the isolate. Halves peak memory
|
||||
/// vs. reading the string on the main isolate and shipping it across.
|
||||
final T Function(String path) readAndParse;
|
||||
|
||||
EtagCachedRemoteStore({
|
||||
required this.diskFileName,
|
||||
required this.prefsEtagKey,
|
||||
required this.prefsLastCheckKey,
|
||||
required this.sourceUrl,
|
||||
required this.acceptHeader,
|
||||
required this.logLabel,
|
||||
required this.emptyIndex,
|
||||
required this.parse,
|
||||
required this.readAndParse,
|
||||
});
|
||||
|
||||
T? _index;
|
||||
Future<T>? _loading;
|
||||
bool _refreshRunning = false;
|
||||
|
||||
/// Lazily load, downloading on first use. Subsequent calls return the
|
||||
/// cached index in O(1). Concurrent callers share the same Future.
|
||||
/// Schedules a background refresh after the first successful load.
|
||||
@protected
|
||||
Future<T> ensureLoaded() async {
|
||||
final existing = _index;
|
||||
if (existing != null) return existing;
|
||||
final loading = _loading;
|
||||
if (loading != null) return loading;
|
||||
|
||||
final fresh = _loadOrFetch();
|
||||
_loading = fresh;
|
||||
try {
|
||||
final idx = await fresh;
|
||||
// Don't cache an empty index (network failure, no disk copy) — let the
|
||||
// next lookup retry so transient offline periods self-heal.
|
||||
if (!idx.isEmpty) {
|
||||
_index = idx;
|
||||
unawaited(maybeRefresh());
|
||||
}
|
||||
return idx;
|
||||
} finally {
|
||||
_loading = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> _loadOrFetch() async {
|
||||
final path = await _diskPath();
|
||||
try {
|
||||
return await compute(readAndParse, path);
|
||||
} on FileSystemException {
|
||||
appLogger.d('$logLabel: no disk cache, downloading from jsDelivr');
|
||||
final raw = await _download();
|
||||
if (raw == null) return emptyIndex;
|
||||
return await compute(parse, raw);
|
||||
} catch (e) {
|
||||
appLogger.w('$logLabel: parse failed — deleting disk copy so next lookup re-downloads', error: e);
|
||||
await _deleteDiskCopy();
|
||||
return emptyIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// GET the mapping, save it to disk, and return the body. Returns `null`
|
||||
/// on any failure (offline, 4xx/5xx, timeout).
|
||||
Future<String?> _download() async {
|
||||
final client = platform.createPlatformClient();
|
||||
try {
|
||||
final res = await sendAbortableHttpRequest(
|
||||
client,
|
||||
'GET',
|
||||
Uri.parse(sourceUrl),
|
||||
headers: {'Accept': acceptHeader},
|
||||
timeout: _requestTimeout,
|
||||
operation: '$logLabel mapping download',
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
appLogger.d('$logLabel: download returned HTTP ${res.statusCode}');
|
||||
return null;
|
||||
}
|
||||
await _writeDiskCopy(res.body, etag: res.headers['etag']);
|
||||
// Seed the weekly throttle so a same-week relaunch skips the refresh.
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
await prefs.setInt(prefsLastCheckKey, DateTime.now().millisecondsSinceEpoch);
|
||||
return res.body;
|
||||
} catch (e) {
|
||||
appLogger.w('$logLabel: download failed', error: e);
|
||||
return null;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Conditional-GET the mapping if the last check was >[_refreshInterval] ago
|
||||
/// and we already have an index loaded. No-op when nothing is loaded — the
|
||||
/// first lookup handles the initial download.
|
||||
Future<void> maybeRefresh() async {
|
||||
if (_refreshRunning) return;
|
||||
if (_index == null) return;
|
||||
_refreshRunning = true;
|
||||
try {
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
final lastCheck = prefs.getInt(prefsLastCheckKey) ?? 0;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (now - lastCheck < _refreshInterval.inMilliseconds) return;
|
||||
|
||||
final etag = prefs.getString(prefsEtagKey);
|
||||
final client = platform.createPlatformClient();
|
||||
try {
|
||||
final res = await sendAbortableHttpRequest(
|
||||
client,
|
||||
'GET',
|
||||
Uri.parse(sourceUrl),
|
||||
headers: {'If-None-Match': ?etag, 'Accept': acceptHeader},
|
||||
timeout: _requestTimeout,
|
||||
operation: '$logLabel mapping refresh',
|
||||
);
|
||||
await prefs.setInt(prefsLastCheckKey, now);
|
||||
|
||||
if (res.statusCode == 304) {
|
||||
appLogger.d('$logLabel: mapping unchanged (304)');
|
||||
return;
|
||||
}
|
||||
if (res.statusCode != 200) {
|
||||
appLogger.d('$logLabel: refresh returned HTTP ${res.statusCode}');
|
||||
return;
|
||||
}
|
||||
|
||||
await _writeDiskCopy(res.body, etag: res.headers['etag']);
|
||||
final fresh = await compute(parse, res.body);
|
||||
_index = fresh;
|
||||
appLogger.d('$logLabel: mapping refreshed (${fresh.logSummary})');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.d('$logLabel: refresh failed (non-fatal)', error: e);
|
||||
} finally {
|
||||
_refreshRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeDiskCopy(String body, {String? etag}) async {
|
||||
await File(await _diskPath()).writeAsString(body, flush: true);
|
||||
if (etag != null) {
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
await prefs.setString(prefsEtagKey, etag);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteDiskCopy() async {
|
||||
try {
|
||||
await File(await _diskPath()).delete();
|
||||
} on FileSystemException {
|
||||
// Already gone.
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _diskPath() async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
return p.join(dir.path, diskFileName);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void resetForTesting() {
|
||||
_index = null;
|
||||
_loading = null;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../../models/trackers/fribb_mapping_row.dart';
|
||||
import '../base_shared_preferences_service.dart';
|
||||
import '../../utils/abortable_http_request.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/platform_http_client_stub.dart'
|
||||
if (dart.library.io) '../../utils/platform_http_client_io.dart'
|
||||
as platform;
|
||||
import 'etag_cached_remote_store.dart';
|
||||
|
||||
/// Indexed view of the Fribb mapping database, queried by external ID.
|
||||
///
|
||||
@@ -19,7 +12,7 @@ import '../../utils/platform_http_client_stub.dart'
|
||||
/// A single tvdb_id may map to multiple rows (split-cour anime → one per
|
||||
/// season); callers that have a Plex season number should filter by
|
||||
/// [FribbMappingRow.tvdbSeason] or [FribbMappingRow.tmdbSeason].
|
||||
class FribbIndex {
|
||||
class FribbIndex implements RemoteIndex {
|
||||
final Map<int, List<FribbMappingRow>> byTvdb;
|
||||
final Map<int, List<FribbMappingRow>> byTmdb;
|
||||
final Map<String, List<FribbMappingRow>> byImdb;
|
||||
@@ -30,7 +23,11 @@ class FribbIndex {
|
||||
|
||||
const FribbIndex({required this.byTvdb, required this.byTmdb, required this.byImdb, this.byMal = const {}});
|
||||
|
||||
@override
|
||||
bool get isEmpty => byTvdb.isEmpty && byTmdb.isEmpty && byImdb.isEmpty && byMal.isEmpty;
|
||||
|
||||
@override
|
||||
String get logSummary => '${byTvdb.length} tvdb entries';
|
||||
}
|
||||
|
||||
abstract interface class FribbMappingLookup {
|
||||
@@ -39,107 +36,31 @@ abstract interface class FribbMappingLookup {
|
||||
Future<FribbMappingRow?> lookupByMal(int malId);
|
||||
}
|
||||
|
||||
/// Loads and refreshes the Fribb anime-lists mapping on demand.
|
||||
///
|
||||
/// On first lookup the ~5 MB JSON is downloaded from jsDelivr and cached to
|
||||
/// the app-support directory. Subsequent lookups read from the cache. Parsing
|
||||
/// runs in a background isolate. [maybeRefresh] does a weekly conditional-GET
|
||||
/// (If-None-Match) to pick up upstream changes.
|
||||
class FribbMappingStore implements FribbMappingLookup {
|
||||
static const String _diskFileName = 'anime-list-mini.json';
|
||||
static const String _prefsEtagKey = 'fribb_anime_list_etag';
|
||||
static const String _prefsLastCheckKey = 'fribb_anime_list_last_check';
|
||||
|
||||
/// jsDelivr (CDN-backed). `raw.githubusercontent.com` rate-limits
|
||||
/// aggressively on shared IPs and returns 429 mid-refresh.
|
||||
static const String _sourceUrl = 'https://cdn.jsdelivr.net/gh/Fribb/anime-lists@master/anime-list-mini.json';
|
||||
|
||||
static const Duration _refreshInterval = Duration(days: 7);
|
||||
static const Duration _requestTimeout = Duration(seconds: 60);
|
||||
|
||||
FribbMappingStore._();
|
||||
static final FribbMappingStore instance = FribbMappingStore._();
|
||||
|
||||
FribbIndex? _index;
|
||||
Future<FribbIndex>? _loading;
|
||||
bool _refreshRunning = false;
|
||||
|
||||
/// Lazily load, downloading on first use. Subsequent calls return the
|
||||
/// cached index in O(1). Concurrent callers share the same Future.
|
||||
/// Schedules a background refresh after the first successful load.
|
||||
Future<FribbIndex> _ensureLoaded() async {
|
||||
final existing = _index;
|
||||
if (existing != null) return existing;
|
||||
final loading = _loading;
|
||||
if (loading != null) return loading;
|
||||
|
||||
final fresh = _loadOrFetch();
|
||||
_loading = fresh;
|
||||
try {
|
||||
final idx = await fresh;
|
||||
// Don't cache an empty index (network failure, no disk copy) — let the
|
||||
// next lookup retry so transient offline periods self-heal.
|
||||
if (!idx.isEmpty) {
|
||||
_index = idx;
|
||||
unawaited(maybeRefresh());
|
||||
}
|
||||
return idx;
|
||||
} finally {
|
||||
_loading = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<FribbIndex> _loadOrFetch() async {
|
||||
final path = await _diskPath();
|
||||
try {
|
||||
return await compute(_readAndParse, path);
|
||||
} on FileSystemException {
|
||||
appLogger.d('Fribb: no disk cache, downloading from jsDelivr');
|
||||
final raw = await _download();
|
||||
if (raw == null) return const FribbIndex(byTvdb: {}, byTmdb: {}, byImdb: {});
|
||||
return await compute(parseFribbIndex, raw);
|
||||
} catch (e) {
|
||||
appLogger.w('Fribb: parse failed — deleting disk copy so next lookup re-downloads', error: e);
|
||||
await _deleteDiskCopy();
|
||||
return const FribbIndex(byTvdb: {}, byTmdb: {}, byImdb: {});
|
||||
}
|
||||
}
|
||||
|
||||
/// GET the mapping, save it to disk, and return the body. Returns `null`
|
||||
/// on any failure (offline, 4xx/5xx, timeout).
|
||||
Future<String?> _download() async {
|
||||
final client = platform.createPlatformClient();
|
||||
try {
|
||||
final res = await sendAbortableHttpRequest(
|
||||
client,
|
||||
'GET',
|
||||
Uri.parse(_sourceUrl),
|
||||
headers: const {'Accept': 'application/json'},
|
||||
timeout: _requestTimeout,
|
||||
operation: 'Fribb mapping download',
|
||||
/// Loads and refreshes the Fribb anime-lists mapping on demand — the ~5 MB
|
||||
/// JSON, indexed by external ID.
|
||||
class FribbMappingStore extends EtagCachedRemoteStore<FribbIndex> implements FribbMappingLookup {
|
||||
FribbMappingStore._()
|
||||
: super(
|
||||
diskFileName: 'anime-list-mini.json',
|
||||
prefsEtagKey: 'fribb_anime_list_etag',
|
||||
prefsLastCheckKey: 'fribb_anime_list_last_check',
|
||||
// jsDelivr (CDN-backed). `raw.githubusercontent.com` rate-limits
|
||||
// aggressively on shared IPs and returns 429 mid-refresh.
|
||||
sourceUrl: 'https://cdn.jsdelivr.net/gh/Fribb/anime-lists@master/anime-list-mini.json',
|
||||
acceptHeader: 'application/json',
|
||||
logLabel: 'Fribb',
|
||||
emptyIndex: const FribbIndex(byTvdb: {}, byTmdb: {}, byImdb: {}),
|
||||
parse: parseFribbIndex,
|
||||
readAndParse: _readAndParse,
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
appLogger.d('Fribb: download returned HTTP ${res.statusCode}');
|
||||
return null;
|
||||
}
|
||||
await _writeDiskCopy(res.body, etag: res.headers['etag']);
|
||||
// Seed the weekly throttle so a same-week relaunch skips the refresh.
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
await prefs.setInt(_prefsLastCheckKey, DateTime.now().millisecondsSinceEpoch);
|
||||
return res.body;
|
||||
} catch (e) {
|
||||
appLogger.w('Fribb: download failed', error: e);
|
||||
return null;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
static final FribbMappingStore instance = FribbMappingStore._();
|
||||
|
||||
/// Look up rows by Plex external IDs. Returns the first non-empty candidate
|
||||
/// list in preference order: tvdb → tmdb → imdb.
|
||||
@override
|
||||
Future<List<FribbMappingRow>> lookup({int? tvdbId, int? tmdbId, String? imdbId}) async {
|
||||
final idx = await _ensureLoaded();
|
||||
final idx = await ensureLoaded();
|
||||
if (tvdbId != null) {
|
||||
final hits = idx.byTvdb[tvdbId];
|
||||
if (hits != null && hits.isNotEmpty) return hits;
|
||||
@@ -156,87 +77,9 @@ class FribbMappingStore implements FribbMappingLookup {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FribbMappingRow?> lookupByMal(int malId) async => (await _ensureLoaded()).byMal[malId];
|
||||
|
||||
/// Conditional-GET the mapping if the last check was >[_refreshInterval] ago
|
||||
/// and we already have an index loaded. No-op when nothing is loaded — the
|
||||
/// first lookup handles the initial download.
|
||||
Future<void> maybeRefresh() async {
|
||||
if (_refreshRunning) return;
|
||||
if (_index == null) return;
|
||||
_refreshRunning = true;
|
||||
try {
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
final lastCheck = prefs.getInt(_prefsLastCheckKey) ?? 0;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (now - lastCheck < _refreshInterval.inMilliseconds) return;
|
||||
|
||||
final etag = prefs.getString(_prefsEtagKey);
|
||||
final client = platform.createPlatformClient();
|
||||
try {
|
||||
final res = await sendAbortableHttpRequest(
|
||||
client,
|
||||
'GET',
|
||||
Uri.parse(_sourceUrl),
|
||||
headers: {'If-None-Match': ?etag, 'Accept': 'application/json'},
|
||||
timeout: _requestTimeout,
|
||||
operation: 'Fribb mapping refresh',
|
||||
);
|
||||
await prefs.setInt(_prefsLastCheckKey, now);
|
||||
|
||||
if (res.statusCode == 304) {
|
||||
appLogger.d('Fribb: mapping unchanged (304)');
|
||||
return;
|
||||
}
|
||||
if (res.statusCode != 200) {
|
||||
appLogger.d('Fribb: refresh returned HTTP ${res.statusCode}');
|
||||
return;
|
||||
}
|
||||
|
||||
await _writeDiskCopy(res.body, etag: res.headers['etag']);
|
||||
final fresh = await compute(parseFribbIndex, res.body);
|
||||
_index = fresh;
|
||||
appLogger.d('Fribb: mapping refreshed (${fresh.byTvdb.length} tvdb entries)');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.d('Fribb: refresh failed (non-fatal)', error: e);
|
||||
} finally {
|
||||
_refreshRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeDiskCopy(String body, {String? etag}) async {
|
||||
await File(await _diskPath()).writeAsString(body, flush: true);
|
||||
if (etag != null) {
|
||||
final prefs = await BaseSharedPreferencesService.sharedCache();
|
||||
await prefs.setString(_prefsEtagKey, etag);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteDiskCopy() async {
|
||||
try {
|
||||
await File(await _diskPath()).delete();
|
||||
} on FileSystemException {
|
||||
// Already gone.
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _diskPath() async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
return p.join(dir.path, _diskFileName);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void resetForTesting() {
|
||||
_index = null;
|
||||
_loading = null;
|
||||
}
|
||||
Future<FribbMappingRow?> lookupByMal(int malId) async => (await ensureLoaded()).byMal[malId];
|
||||
}
|
||||
|
||||
/// Read the JSON from disk and parse it inside the isolate. Halves peak
|
||||
/// memory vs. reading the string on the main isolate and shipping it across.
|
||||
FribbIndex _readAndParse(String path) {
|
||||
final raw = File(path).readAsStringSync();
|
||||
return parseFribbIndex(raw);
|
||||
|
||||
@@ -390,16 +390,15 @@ class _AppMenuItemTileState<T> extends State<AppMenuItemTile<T>> with FocusableT
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initFocusNode();
|
||||
effectiveFocusNode.addListener(_updateFocusedState);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AppMenuItemTile<T> oldWidget) {
|
||||
final rebinds = oldWidget.focusNode != widget.focusNode;
|
||||
if (rebinds) effectiveFocusNode.removeListener(_updateFocusedState);
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.focusNode != widget.focusNode) {
|
||||
effectiveFocusNode.removeListener(_updateFocusedState);
|
||||
updateFocusNode(oldWidget.focusNode);
|
||||
if (rebinds) {
|
||||
effectiveFocusNode.addListener(_updateFocusedState);
|
||||
_isFocused = effectiveFocusNode.hasFocus;
|
||||
}
|
||||
@@ -408,7 +407,6 @@ class _AppMenuItemTileState<T> extends State<AppMenuItemTile<T>> with FocusableT
|
||||
@override
|
||||
void dispose() {
|
||||
effectiveFocusNode.removeListener(_updateFocusedState);
|
||||
disposeFocusNode();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,24 +82,6 @@ class _FocusableListTileState extends State<FocusableListTile> with FocusableTil
|
||||
@override
|
||||
FocusNode? get widgetFocusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initFocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FocusableListTile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
updateFocusNode(oldWidget.focusNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeFocusNode();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// When hovered/focused with a custom hoverColor, use onError-style foreground
|
||||
@@ -212,24 +194,6 @@ class _FocusableRadioListTileState<T> extends State<FocusableRadioListTile<T>>
|
||||
@override
|
||||
FocusNode? get widgetFocusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initFocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FocusableRadioListTile<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
updateFocusNode(oldWidget.focusNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeFocusNode();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClickableCursor(
|
||||
@@ -316,24 +280,6 @@ class _FocusableSwitchListTileState extends State<FocusableSwitchListTile>
|
||||
@override
|
||||
FocusNode? get widgetFocusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initFocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FocusableSwitchListTile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
updateFocusNode(oldWidget.focusNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeFocusNode();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClickableCursor(
|
||||
@@ -398,24 +344,6 @@ class _FocusableCheckboxListTileState extends State<FocusableCheckboxListTile>
|
||||
@override
|
||||
FocusNode? get widgetFocusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initFocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FocusableCheckboxListTile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
updateFocusNode(oldWidget.focusNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeFocusNode();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClickableCursor(
|
||||
|
||||
@@ -123,24 +123,6 @@ class _TrackRowState extends State<TrackRow> with ContextMenuTapMixin<TrackRow>,
|
||||
@override
|
||||
FocusNode? get widgetFocusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initFocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TrackRow oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
updateFocusNode(oldWidget.focusNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeFocusNode();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleFocusChange(bool hasFocus) {
|
||||
setState(() {
|
||||
_hasFocus = hasFocus;
|
||||
|
||||
@@ -7,9 +7,10 @@ import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/mixins/library_tab_state.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
class _Probe extends StatefulWidget {
|
||||
const _Probe({required this.library, required this.onState});
|
||||
|
||||
@@ -45,8 +46,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final provider = MultiServerProvider(manager, aggregation);
|
||||
final provider = testMultiServerProvider(manager);
|
||||
// provider.dispose() cascades to manager.dispose() — only register
|
||||
// the outer teardown to avoid a double-close on the manager's stream.
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
@@ -16,13 +16,13 @@ import 'package:plezy/providers/discover_provider.dart';
|
||||
import 'package:plezy/providers/hidden_libraries_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/providers/trackers_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/offline_watch_sync_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
import 'package:plezy/services/system_shelf_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
void main() {
|
||||
@@ -51,7 +51,7 @@ void main() {
|
||||
storage: storage,
|
||||
);
|
||||
final serverManager = MultiServerManager();
|
||||
final multiServer = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
final multiServer = testMultiServerProvider(serverManager);
|
||||
// The session tree instantiates MusicPlaybackServiceImpl (the mini-player
|
||||
// overlay watches it), which needs the database + offline watch service.
|
||||
final offlineWatch = OfflineWatchSyncService(database: db, serverManager: serverManager);
|
||||
|
||||
@@ -8,11 +8,11 @@ import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/providers/libraries_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
MediaLibrary _lib(String key, {String type = 'movie', ServerId? serverId, String title = 'L'}) => MediaLibrary(
|
||||
@@ -563,7 +563,7 @@ void main() {
|
||||
|
||||
test('online-servers listener is removed on dispose', () {
|
||||
final manager = MultiServerManager();
|
||||
final multiServer = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServer = testMultiServerProvider(manager);
|
||||
|
||||
final before = multiServer.onlineServersListenerCount;
|
||||
final scoped = LibrariesProvider(multiServer: multiServer);
|
||||
|
||||
@@ -4,12 +4,11 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/connection/connection.dart';
|
||||
import 'package:plezy/providers/offline_mode_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_auth_service.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
void main() {
|
||||
@@ -105,7 +104,7 @@ void main() {
|
||||
httpClient: MockClient((_) async => http.Response('', 401)),
|
||||
);
|
||||
manager.debugRegisterJellyfinClientForTesting(client, online: false);
|
||||
final multi = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multi = testMultiServerProvider(manager);
|
||||
final p = OfflineModeProvider(manager, multiServerProvider: multi);
|
||||
await p.initialize();
|
||||
|
||||
@@ -122,7 +121,7 @@ void main() {
|
||||
|
||||
test('expected but unreachable visible servers enter offline without live clients', () async {
|
||||
final manager = MultiServerManager();
|
||||
final multi = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multi = testMultiServerProvider(manager);
|
||||
final p = OfflineModeProvider(manager, multiServerProvider: multi);
|
||||
await p.initialize();
|
||||
manager.updateServerStatus(ServerId('plex-server'), false);
|
||||
@@ -146,7 +145,7 @@ void main() {
|
||||
|
||||
test('expected but unreachable profile servers enter offline once visibility settles', () async {
|
||||
final manager = MultiServerManager();
|
||||
final multi = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multi = testMultiServerProvider(manager);
|
||||
final p = OfflineModeProvider(manager, multiServerProvider: multi);
|
||||
|
||||
expect(p.isOffline, isFalse);
|
||||
@@ -173,7 +172,7 @@ void main() {
|
||||
|
||||
test('Plex auth errors without live clients stay out of generic offline', () async {
|
||||
final manager = MultiServerManager();
|
||||
final multi = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multi = testMultiServerProvider(manager);
|
||||
final p = OfflineModeProvider(manager, multiServerProvider: multi);
|
||||
await p.initialize();
|
||||
|
||||
|
||||
@@ -10,11 +10,9 @@ import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/models/catalog/catalog_cast_member.dart';
|
||||
import 'package:plezy/models/catalog/catalog_item.dart';
|
||||
import 'package:plezy/providers/catalog_sources_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/catalog_item_detail_screen.dart';
|
||||
import 'package:plezy/services/catalog/catalog_source.dart';
|
||||
import 'package:plezy/services/catalog/catalog_library_matcher.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
@@ -24,6 +22,7 @@ import 'package:plezy/widgets/media_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
class _FakeCatalogSource implements CatalogSource {
|
||||
@@ -127,7 +126,7 @@ Future<void> _pumpDetail(
|
||||
}) async {
|
||||
final sources = _FakeCatalogSourcesProvider(source);
|
||||
final serverManager = MultiServerManager();
|
||||
final multiServer = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
final multiServer = testMultiServerProvider(serverManager);
|
||||
final matcher = _FakeCatalogLibraryMatcher(multiServer, matches);
|
||||
addTearDown(sources.dispose);
|
||||
addTearDown(source.dispose);
|
||||
|
||||
@@ -6,12 +6,10 @@ import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/models/catalog/catalog_item.dart';
|
||||
import 'package:plezy/models/catalog/catalog_cast_member.dart';
|
||||
import 'package:plezy/providers/catalog_sources_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/catalog_item_detail_screen.dart';
|
||||
import 'package:plezy/screens/catalog_search_screen.dart';
|
||||
import 'package:plezy/services/catalog/catalog_source.dart';
|
||||
import 'package:plezy/services/catalog/catalog_library_matcher.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
@@ -19,6 +17,7 @@ import 'package:plezy/widgets/app_menu.dart';
|
||||
import 'package:plezy/widgets/media_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
/// Only the members the search screen touches; everything else throws.
|
||||
@@ -197,7 +196,7 @@ Future<void> _pumpMenuSearch(WidgetTester tester, _FakeSearchSource source, {req
|
||||
|
||||
final sources = _FakeCatalogSourcesProvider(source);
|
||||
final manager = MultiServerManager();
|
||||
final multiServer = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServer = testMultiServerProvider(manager);
|
||||
final matcher = _FakeCatalogLibraryMatcher(multiServer);
|
||||
addTearDown(manager.dispose);
|
||||
addTearDown(multiServer.dispose);
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/providers/download_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/collection_detail_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/download_manager_service.dart';
|
||||
import 'package:plezy/services/download_storage_service.dart';
|
||||
import 'package:plezy/services/jellyfin_api_cache.dart';
|
||||
@@ -29,6 +28,7 @@ import 'package:plezy/widgets/media_card_sliver_layout.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/paged_fakes.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
@@ -133,7 +133,7 @@ Future<_CollectionHarness> _createHarness(List<MediaItem> items) async {
|
||||
|
||||
final client = _CollectionClient(items);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
|
||||
addTearDown(() async {
|
||||
downloadProvider.dispose();
|
||||
|
||||
@@ -31,7 +31,6 @@ import 'package:plezy/providers/libraries_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/discover_screen.dart';
|
||||
import 'package:plezy/screens/main_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
@@ -46,6 +45,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -82,7 +82,7 @@ void main() {
|
||||
final hub = MediaHub(id: 'hub_1', title: 'Recommended', type: 'movie', items: [item], size: 1);
|
||||
final client = _FakeMediaServerClient(hubs: [hub]);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final hiddenLibrariesProvider = HiddenLibrariesProvider();
|
||||
final librariesProvider = LibrariesProvider();
|
||||
final watchTogetherProvider = WatchTogetherProvider();
|
||||
@@ -262,7 +262,7 @@ void main() {
|
||||
];
|
||||
final client = _FakeMediaServerClient(hubs: const [], continueWatching: onDeck);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final hiddenLibrariesProvider = HiddenLibrariesProvider();
|
||||
final librariesProvider = LibrariesProvider();
|
||||
final watchTogetherProvider = WatchTogetherProvider();
|
||||
|
||||
@@ -18,7 +18,6 @@ import 'package:plezy/navigation/main_screen_scope.dart';
|
||||
import 'package:plezy/providers/download_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/downloads/downloads_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/download_manager_service.dart';
|
||||
import 'package:plezy/services/download_storage_service.dart';
|
||||
import 'package:plezy/services/jellyfin_api_cache.dart';
|
||||
@@ -33,6 +32,7 @@ import 'package:provider/provider.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/io_fakes.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
class _FakeConnectionRegistry extends ConnectionRegistry {
|
||||
_FakeConnectionRegistry(super.db);
|
||||
@@ -77,7 +77,7 @@ void main() {
|
||||
await downloadProvider.ensureInitialized();
|
||||
|
||||
serverManager = MultiServerManager();
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
multiServerProvider = testMultiServerProvider(serverManager);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
|
||||
@@ -15,7 +15,6 @@ import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/providers/download_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/downloads/sync_rules_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/download_manager_service.dart';
|
||||
import 'package:plezy/services/download_storage_service.dart';
|
||||
import 'package:plezy/services/jellyfin_api_cache.dart';
|
||||
@@ -27,6 +26,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/prefs.dart';
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
PlexConnection _plexConnection() {
|
||||
return PlexConnection(
|
||||
@@ -213,7 +213,7 @@ void main() {
|
||||
addTearDown(authClient.close);
|
||||
serverManager.debugRegisterJellyfinClientForTesting(authClient, online: false);
|
||||
serverManager.debugMarkAuthErrorForTesting(ServerId('auth-jf'));
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
multiServerProvider = testMultiServerProvider(serverManager);
|
||||
|
||||
await insertRule(ServerId('plex-srv'), 'show-1');
|
||||
await insertRule(ServerId('jf-machine'), 'show-2');
|
||||
@@ -233,7 +233,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('removes orphaned sync rules from the sync rules screen', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
multiServerProvider = testMultiServerProvider(serverManager);
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
|
||||
await pumpScreen(tester);
|
||||
@@ -256,7 +256,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('provider rebuilds reuse the connection stream subscription', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
multiServerProvider = testMultiServerProvider(serverManager);
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
await pumpScreen(tester);
|
||||
|
||||
@@ -268,7 +268,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('does not autofocus the first sync rule in pointer mode', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
multiServerProvider = testMultiServerProvider(serverManager);
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
@@ -279,7 +279,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('keyboard navigation reaches and toggles the sync rule switch', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
multiServerProvider = testMultiServerProvider(serverManager);
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
|
||||
await pumpScreen(tester, keyboardMode: true);
|
||||
@@ -297,7 +297,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('setting sync rule count to zero removes the rule', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
multiServerProvider = testMultiServerProvider(serverManager);
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
|
||||
await pumpScreen(tester, keyboardMode: true);
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/hub_detail_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
@@ -21,6 +20,7 @@ import 'package:provider/provider.dart';
|
||||
import '../test_helpers/paged_fakes.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -144,7 +144,7 @@ Future<_HubHarness> _createHarness(List<MediaItem> items, {required MediaBackend
|
||||
await SettingsService.getInstance();
|
||||
final client = _PagedHubClient(items, backend: backend);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(provider.dispose);
|
||||
return _HubHarness(client: client, provider: provider);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ import 'package:plezy/focus/input_mode_tracker.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/folder_tree_view.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/backend_client_fixtures.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
void main() {
|
||||
@@ -62,12 +61,7 @@ void main() {
|
||||
);
|
||||
}),
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(() {
|
||||
provider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
final provider = testMultiServer(clients: [client]).provider;
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:plezy/providers/hidden_libraries_provider.dart';
|
||||
import 'package:plezy/providers/libraries_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/libraries_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
@@ -22,6 +21,7 @@ import 'package:shared_preferences_platform_interface/in_memory_shared_preferenc
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
import 'package:shared_preferences_platform_interface/types.dart';
|
||||
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
const _libraryA = MediaLibrary(
|
||||
@@ -135,7 +135,7 @@ final class _Harness {
|
||||
final hiddenLibraries = HiddenLibrariesProvider();
|
||||
await hiddenLibraries.ensureInitialized();
|
||||
final manager = MultiServerManager();
|
||||
final multiServer = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServer = testMultiServerProvider(manager);
|
||||
return _Harness(libraries: libraries, hiddenLibraries: hiddenLibraries, multiServer: multiServer);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/state_messages.dart';
|
||||
import 'package:plezy/screens/libraries/tabs/library_browse_tab.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
@@ -24,6 +23,7 @@ import 'package:plezy/widgets/media_card.dart';
|
||||
|
||||
import '../../test_helpers/backend_client_fixtures.dart';
|
||||
import '../../test_helpers/library_tab_scaffold.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
final _musicLibrary = MediaLibrary(
|
||||
@@ -165,7 +165,7 @@ class _MusicBrowseHarness {
|
||||
}),
|
||||
);
|
||||
manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
provider = testMultiServerProvider(manager);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@@ -17,7 +17,6 @@ import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/state_messages.dart';
|
||||
import 'package:plezy/screens/libraries/tabs/library_browse_tab.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
@@ -27,6 +26,7 @@ import 'package:plezy/widgets/focusable_filter_chip.dart';
|
||||
|
||||
import '../../test_helpers/library_tab_scaffold.dart';
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
void main() {
|
||||
@@ -191,7 +191,7 @@ class _BrowseHarness {
|
||||
manager = MultiServerManager()
|
||||
..debugRegisterClientForTesting(clientA)
|
||||
..debugRegisterClientForTesting(clientB);
|
||||
provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
provider = testMultiServerProvider(manager);
|
||||
}
|
||||
|
||||
MediaLibrary _libraryFor(_BrowseClient client) {
|
||||
|
||||
@@ -16,7 +16,6 @@ import 'package:plezy/media/media_playlist.dart';
|
||||
import 'package:plezy/models/plex/plex_config.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/libraries/tabs/library_playlists_tab.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_client.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
@@ -28,6 +27,7 @@ import 'package:plezy/widgets/media_card_sliver_layout.dart';
|
||||
|
||||
import '../../test_helpers/backend_client_fixtures.dart';
|
||||
import '../../test_helpers/library_tab_scaffold.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
final _serverId = ServerId('playlist-server');
|
||||
@@ -246,7 +246,7 @@ class _PlaylistHarness {
|
||||
}),
|
||||
);
|
||||
manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
provider = testMultiServerProvider(manager);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
|
||||
@@ -19,13 +19,14 @@ import 'package:plezy/models/livetv_channel.dart';
|
||||
import 'package:plezy/models/livetv_program.dart';
|
||||
import 'package:plezy/screens/livetv/tabs/guide_tab.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
const _selectDown = KeyDownEvent(
|
||||
physicalKey: PhysicalKeyboardKey.enter,
|
||||
logicalKey: LogicalKeyboardKey.enter,
|
||||
@@ -202,7 +203,7 @@ final class _GuideHarness {
|
||||
final serverB = includeServerB ? _FakeMediaServerClient(serverId: 'server-b', stationId: 'station-b') : null;
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(serverA);
|
||||
if (serverB != null) manager.debugRegisterClientForTesting(serverB);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager))
|
||||
final provider = testMultiServerProvider(manager)
|
||||
..debugSetLiveTvServersForTesting([
|
||||
LiveTvServerInfo(serverId: 'server-a', dvrKey: 'dvr-a'),
|
||||
if (serverB != null) LiveTvServerInfo(serverId: 'server-b', dvrKey: 'dvr-b'),
|
||||
|
||||
@@ -16,12 +16,12 @@ import 'package:plezy/models/livetv_program.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/livetv/live_tv_screen.dart';
|
||||
import 'package:plezy/screens/livetv/tabs/guide_tab.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
void main() {
|
||||
@@ -75,7 +75,7 @@ void main() {
|
||||
final manager = MultiServerManager()
|
||||
..debugRegisterClientForTesting(failedClient)
|
||||
..debugRegisterClientForTesting(healthyClient);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
provider.debugSetLiveTvServersForTesting([
|
||||
LiveTvServerInfo(serverId: 'server-a', dvrKey: 'dvr-a', lineup: 'provider-a'),
|
||||
LiveTvServerInfo(serverId: 'server-b', dvrKey: 'dvr-b', lineup: 'provider-b'),
|
||||
@@ -148,7 +148,7 @@ Future<_LiveTvHarness> _pumpLiveTvScreen(WidgetTester tester) async {
|
||||
final liveTv = _FakeLiveTvSupport();
|
||||
final client = _FakeMediaServerClient(liveTv);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
provider.debugSetLiveTvServersForTesting([
|
||||
LiveTvServerInfo(serverId: client.serverId.value, dvrKey: 'dvr-a', lineup: 'provider-a'),
|
||||
]);
|
||||
|
||||
@@ -14,12 +14,13 @@ import 'package:plezy/models/livetv_program.dart';
|
||||
import 'package:plezy/models/media_subscription.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/livetv/live_tv_show_schedule_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() => initializeDateFormatting('en'));
|
||||
@@ -113,7 +114,7 @@ LiveTvProgram _program({String? guid}) => LiveTvProgram(
|
||||
|
||||
MultiServerProvider _providerFor(MediaServerClient client) {
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
return MultiServerProvider(manager, DataAggregationService(manager));
|
||||
return testMultiServerProvider(manager);
|
||||
}
|
||||
|
||||
Future<void> _pumpScreen(WidgetTester tester, MultiServerProvider provider) async {
|
||||
|
||||
@@ -13,11 +13,12 @@ import 'package:plezy/models/media_grab_operation.dart';
|
||||
import 'package:plezy/models/media_subscription.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/livetv/tabs/recordings_tab.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -27,7 +28,7 @@ void main() {
|
||||
final dvr = _ControllableDvr();
|
||||
final client = _FakeMediaServerClient(dvr);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager))
|
||||
final provider = testMultiServerProvider(manager)
|
||||
..debugSetLiveTvServersForTesting([LiveTvServerInfo(serverId: client.serverId.value, dvrKey: 'dvr')]);
|
||||
addTearDown(provider.dispose);
|
||||
final tabKey = GlobalKey<RecordingsTabState>();
|
||||
|
||||
@@ -22,7 +22,6 @@ import 'package:plezy/providers/download_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/providers/watch_state_store.dart';
|
||||
import 'package:plezy/screens/media_detail_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
|
||||
import '../test_helpers/paged_fakes.dart';
|
||||
import 'package:plezy/services/download_manager_service.dart';
|
||||
@@ -46,6 +45,7 @@ import 'package:provider/provider.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/profile_navigation.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -308,7 +308,7 @@ void main() {
|
||||
pendingPlayableDescendants: descendantsCompleter.future,
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -434,7 +434,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -531,7 +531,7 @@ void main() {
|
||||
childrenPageErrors: {season1.id: Exception('season cache failed')},
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -622,7 +622,7 @@ void main() {
|
||||
childrenPageFutures: {season2.id: season2Completer.future},
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -708,7 +708,7 @@ void main() {
|
||||
PlexApiCache.initialize(database);
|
||||
JellyfinApiCache.initialize(database);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final offlineWatch = OfflineWatchSyncService(database: database, serverManager: manager);
|
||||
final downloadManager = DownloadManagerService(
|
||||
database: database,
|
||||
@@ -832,7 +832,7 @@ void main() {
|
||||
await downloadProvider.ensureInitialized();
|
||||
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final watchStateOverlay = WatchStateStore();
|
||||
|
||||
addTearDown(() async {
|
||||
|
||||
@@ -19,7 +19,6 @@ import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/metadata_edit/metadata_edit_models.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/metadata_edit_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/file_picker_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
@@ -32,6 +31,7 @@ import 'package:provider/provider.dart';
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/http_fixtures.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
@@ -327,7 +327,7 @@ Future<_EditorHarness> _pumpEditor(WidgetTester tester, _PlexMetadataRequests re
|
||||
PlexApiCache.initialize(database);
|
||||
final client = testPlexClient(serverId: ServerId('server-1'), handler: requests.handle);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
final metadata = ValueNotifier<MediaItem>(_show());
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/music/album_detail_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/music/music_playback_service.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
@@ -19,6 +18,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/prefs.dart';
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -109,7 +109,7 @@ Future<_AlbumHarness> _createHarness(List<MediaItem> tracks) async {
|
||||
|
||||
final client = _FakeMusicClient(tracks);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
|
||||
@@ -10,14 +10,13 @@ import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/music/now_playing_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/music/music_playback_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
MediaItem _track({required String id, required String title, required String album, required int year}) {
|
||||
return testMediaItem(
|
||||
@@ -118,13 +117,8 @@ void main() {
|
||||
TvDetectionService.debugSetAppleTVOverride(isTv);
|
||||
PlatformDetector.debugSetIsDesktopOSOverride(!isTv);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(service.dispose);
|
||||
addTearDown(() {
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
final multiServerProvider = testMultiServer().provider;
|
||||
|
||||
await tester.pumpWidget(
|
||||
InputModeTracker(
|
||||
|
||||
@@ -7,8 +7,6 @@ import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/music/queue_sheet.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/music/music_playback_service.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
@@ -17,6 +15,7 @@ import 'package:plezy/widgets/music/track_row.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
MediaItem _track(String id, String title) => testMediaItem(
|
||||
@@ -73,13 +72,8 @@ void main() {
|
||||
});
|
||||
|
||||
Widget wrap(MusicPlaybackService service) {
|
||||
final manager = MultiServerManager();
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(service.dispose);
|
||||
addTearDown(() {
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
final multiServerProvider = testMultiServer().provider;
|
||||
|
||||
return TranslationProvider(
|
||||
child: MultiProvider(
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/screens/playlist/playlist_detail_screen.dart';
|
||||
import 'package:plezy/screens/playlist/playlist_item_card.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/download_manager_service.dart';
|
||||
import 'package:plezy/services/download_storage_service.dart';
|
||||
import 'package:plezy/services/jellyfin_api_cache.dart';
|
||||
@@ -41,6 +40,7 @@ import 'package:plezy/utils/media_image_helper.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/paged_fakes.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
@@ -671,7 +671,7 @@ Future<_PlaylistHarness> _createHarness(
|
||||
|
||||
final client = _PagedPlaylistClient(items, failOnceAt: failOnceAt, deleteResult: deleteResult, backend: backend);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final playbackState = PlaybackStateProvider();
|
||||
|
||||
addTearDown(() async {
|
||||
|
||||
@@ -20,12 +20,12 @@ import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/providers/user_profile_provider.dart';
|
||||
import 'package:plezy/screens/profile/profile_teardown.dart';
|
||||
import 'package:plezy/services/plex_auth_service.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
import 'package:plezy/services/system_shelf_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
class _PlexHome extends PlexHomeService {
|
||||
@@ -275,7 +275,7 @@ Future<_Harness> _pumpHarness(
|
||||
await storage.setActiveProfileId(profile.id);
|
||||
await active.initialize();
|
||||
final manager = MultiServerManager();
|
||||
final multiServer = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServer = testMultiServerProvider(manager);
|
||||
final shelf = SystemShelfService.forTesting(channel: channel, isSupported: () async => true);
|
||||
shelf.beginProfileSession(profile.id);
|
||||
SystemShelfService.debugOverrideInstance(shelf);
|
||||
|
||||
@@ -22,7 +22,6 @@ import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/settings/add_jellyfin_screen.dart';
|
||||
import 'package:plezy/services/jellyfin_auth_service.dart';
|
||||
import 'package:plezy/services/credential_vault.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/jellyfin_lan_discovery_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/storage_service.dart';
|
||||
@@ -30,6 +29,7 @@ import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
|
||||
Profile _profile(String id) =>
|
||||
@@ -244,7 +244,7 @@ class _RouteHarness {
|
||||
storage: storage,
|
||||
);
|
||||
final manager = _CountingJellyfinManager();
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final binder = _CountingActiveProfileBinder(
|
||||
activeProfile: activeProfiles,
|
||||
connections: connections,
|
||||
|
||||
@@ -8,11 +8,11 @@ import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/play_queue.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/episode_navigation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
MediaItem _meta(String id, {String? title}) =>
|
||||
testMediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.episode, title: title ?? 'Episode $id');
|
||||
@@ -106,7 +106,7 @@ void main() {
|
||||
final playback = PlaybackStateProvider();
|
||||
addTearDown(playback.dispose);
|
||||
final manager = _StubManager(null);
|
||||
final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final serverProvider = testMultiServerProvider(manager);
|
||||
addTearDown(serverProvider.dispose);
|
||||
|
||||
AdjacentEpisodes? result;
|
||||
@@ -176,8 +176,7 @@ void main() {
|
||||
],
|
||||
);
|
||||
final manager = _StubManager(client);
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final serverProvider = MultiServerProvider(manager, aggregation);
|
||||
final serverProvider = testMultiServerProvider(manager);
|
||||
addTearDown(serverProvider.dispose);
|
||||
|
||||
AdjacentEpisodes? result;
|
||||
@@ -211,7 +210,7 @@ void main() {
|
||||
addTearDown(playback.dispose);
|
||||
final client = _RecordingClient(seriesEpisodes: [ep1, ep2, ep3], clientBackend: MediaBackend.plex);
|
||||
final manager = _StubManager(client);
|
||||
final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final serverProvider = testMultiServerProvider(manager);
|
||||
addTearDown(serverProvider.dispose);
|
||||
|
||||
AdjacentEpisodes? result;
|
||||
@@ -244,7 +243,7 @@ void main() {
|
||||
fetchError: StateError('network unavailable'),
|
||||
);
|
||||
final manager = _StubManager(client);
|
||||
final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final serverProvider = testMultiServerProvider(manager);
|
||||
addTearDown(serverProvider.dispose);
|
||||
|
||||
AdjacentEpisodes? result;
|
||||
@@ -272,7 +271,7 @@ void main() {
|
||||
addTearDown(playback.dispose);
|
||||
final client = _RecordingClient(seriesEpisodes: [ep1, ep2], clientBackend: MediaBackend.plex);
|
||||
final manager = _StubManager(client);
|
||||
final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final serverProvider = testMultiServerProvider(manager);
|
||||
addTearDown(serverProvider.dispose);
|
||||
|
||||
AdjacentEpisodes? result;
|
||||
@@ -330,7 +329,7 @@ void main() {
|
||||
);
|
||||
final client = _RecordingClient(seriesEpisodes: [ep1, ep2, ep3, ep4, ep5]);
|
||||
final manager = _StubManager(client);
|
||||
final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final serverProvider = testMultiServerProvider(manager);
|
||||
addTearDown(serverProvider.dispose);
|
||||
return (playback, client, serverProvider);
|
||||
}
|
||||
@@ -403,7 +402,7 @@ void main() {
|
||||
addTearDown(playback.dispose);
|
||||
final client = _RecordingClient(seriesEpisodes: [ep1, ep2, ep3, ep4, ep5]);
|
||||
final manager = _StubManager(client);
|
||||
final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final serverProvider = testMultiServerProvider(manager);
|
||||
addTearDown(serverProvider.dispose);
|
||||
|
||||
final result = await probe(tester, playback, serverProvider, ep3);
|
||||
|
||||
@@ -8,14 +8,13 @@ import 'package:http/testing.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
import 'package:plezy/services/plex_client.dart';
|
||||
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
@@ -107,7 +106,7 @@ void main() {
|
||||
final manager = MultiServerManager();
|
||||
manager.debugRegisterClientForTesting(plex);
|
||||
manager.debugRegisterJellyfinClientForTesting(jellyfin);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final provider = testMultiServerProvider(manager);
|
||||
addTearDown(() {
|
||||
provider.dispose();
|
||||
manager.dispose();
|
||||
|
||||
@@ -8,10 +8,11 @@ import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/video_player_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/utils/live_tv_player_navigation.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
late MultiServerManager manager;
|
||||
late MultiServerProvider multiServer;
|
||||
@@ -19,7 +20,7 @@ void main() {
|
||||
setUp(() async {
|
||||
await LocaleSettings.setLocale(AppLocale.bg);
|
||||
manager = MultiServerManager();
|
||||
multiServer = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
multiServer = testMultiServerProvider(manager);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
|
||||
@@ -15,7 +15,6 @@ import 'package:plezy/providers/hidden_libraries_provider.dart';
|
||||
import 'package:plezy/providers/libraries_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
@@ -24,6 +23,7 @@ import 'package:plezy/widgets/overlay_sheet.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
@@ -49,8 +49,7 @@ Future<({int Function() selects, int Function() backs})> _pumpLibraryManagementL
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final fallbackManager = multiServerProvider == null ? MultiServerManager() : null;
|
||||
final effectiveMultiServerProvider =
|
||||
multiServerProvider ?? MultiServerProvider(fallbackManager!, DataAggregationService(fallbackManager));
|
||||
final effectiveMultiServerProvider = multiServerProvider ?? testMultiServerProvider(fallbackManager!);
|
||||
if (fallbackManager != null) {
|
||||
addTearDown(() {
|
||||
effectiveMultiServerProvider.dispose();
|
||||
@@ -268,7 +267,7 @@ class _LibraryActionHarness {
|
||||
);
|
||||
manager.debugRegisterClientForTesting(owner);
|
||||
}
|
||||
provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
provider = testMultiServerProvider(manager);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@@ -30,7 +30,6 @@ import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/screens/music/album_detail_screen.dart';
|
||||
import 'package:plezy/screens/music/artist_detail_screen.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/download_manager_service.dart';
|
||||
import 'package:plezy/services/download_storage_service.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
@@ -46,6 +45,7 @@ import 'package:plezy/widgets/media_context_menu.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/profile_stack.dart';
|
||||
|
||||
@@ -124,7 +124,7 @@ void main() {
|
||||
final client = _AudioPlaylistClient(tracks);
|
||||
final music = _RecordingMusicPlaybackService();
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final stack = await ProfileStack.create(withStorage: false);
|
||||
addTearDown(() async {
|
||||
await stack.dispose();
|
||||
@@ -228,7 +228,7 @@ void main() {
|
||||
])..blockWithAbort = true;
|
||||
final playback = PlaybackStateProvider();
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final stack = await ProfileStack.create(withStorage: false);
|
||||
addTearDown(() async {
|
||||
playback.dispose();
|
||||
@@ -293,7 +293,7 @@ void main() {
|
||||
addTearDown(() => TvDetectionService.debugSetAppleTVOverride(null));
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final stack = await ProfileStack.create(withStorage: false);
|
||||
addTearDown(() async {
|
||||
await stack.dispose();
|
||||
@@ -569,7 +569,7 @@ Future<GlobalKey<MediaContextMenuState>> _pumpPlexMovieMenu(
|
||||
}),
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final stack = await ProfileStack.create(db: db, withStorage: false);
|
||||
addTearDown(() async {
|
||||
await stack.dispose();
|
||||
@@ -771,7 +771,7 @@ Future<_SiblingMusicMenuHarness> _pumpSiblingMusicMenu(
|
||||
await downloadProvider.ensureInitialized();
|
||||
final client = _RelatedMusicClient(relatedItems);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
final stack = await ProfileStack.create(db: db, withStorage: false);
|
||||
final music = _RecordingMusicPlaybackService();
|
||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@@ -14,8 +14,6 @@ import 'package:plezy/models/download_models.dart';
|
||||
import 'package:plezy/profiles/active_profile_provider.dart';
|
||||
import 'package:plezy/providers/download_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/music/music_playback_service.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
@@ -26,6 +24,7 @@ import 'package:plezy/widgets/music/mini_player.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../test_helpers/media_items.dart';
|
||||
import '../../test_helpers/multi_server_fixtures.dart';
|
||||
import '../../test_helpers/prefs.dart';
|
||||
import '../../test_helpers/profile_stack.dart';
|
||||
|
||||
@@ -159,14 +158,9 @@ void main() {
|
||||
ActiveProfileProvider? activeProfileProvider,
|
||||
DownloadProvider? downloadProvider,
|
||||
}) {
|
||||
final manager = MultiServerManager();
|
||||
final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(service.dispose);
|
||||
addTearDown(observer.suppress.dispose);
|
||||
addTearDown(() {
|
||||
multiServerProvider.dispose();
|
||||
manager.dispose();
|
||||
});
|
||||
final multiServerProvider = testMultiServer().provider;
|
||||
|
||||
return TranslationProvider(
|
||||
child: MultiProvider(
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:plezy/navigation/navigation_tabs.dart';
|
||||
import 'package:plezy/providers/hidden_libraries_provider.dart';
|
||||
import 'package:plezy/providers/libraries_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
@@ -22,6 +21,7 @@ import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:plezy/widgets/side_navigation_rail.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/theme.dart';
|
||||
|
||||
@@ -82,8 +82,7 @@ Future<void> _pumpBasicRail(
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final multiServerProvider = MultiServerProvider(manager, aggregation);
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
final rail = SideNavigationRail(
|
||||
@@ -140,8 +139,7 @@ void main() {
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final multiServerProvider = MultiServerProvider(manager, aggregation);
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -205,8 +203,7 @@ void main() {
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final multiServerProvider = MultiServerProvider(manager, aggregation);
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -309,8 +306,7 @@ void main() {
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final multiServerProvider = MultiServerProvider(manager, aggregation);
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
final reports = <bool>[];
|
||||
@@ -373,8 +369,7 @@ void main() {
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final multiServerProvider = MultiServerProvider(manager, aggregation);
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
final sideNavKey = GlobalKey<SideNavigationRailState>();
|
||||
@@ -450,8 +445,7 @@ void main() {
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final multiServerProvider = MultiServerProvider(manager, aggregation);
|
||||
final multiServerProvider = testMultiServerProvider(manager);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
final sideNavKey = GlobalKey<SideNavigationRailState>();
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:plezy/media/media_hub.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/device_performance.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
@@ -25,6 +24,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -346,7 +346,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -418,7 +418,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -473,7 +473,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: theme,
|
||||
home: Scaffold(
|
||||
@@ -513,7 +513,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -557,7 +557,7 @@ void main() {
|
||||
|
||||
Widget rail(MediaHub hub) {
|
||||
return ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -599,7 +599,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -689,7 +689,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -762,7 +762,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -822,7 +822,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
@@ -884,7 +884,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -939,7 +939,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1001,7 +1001,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1039,7 +1039,7 @@ void main() {
|
||||
Widget buildRail(List<MediaHub> hubs, {String? initialHubId, String? initialItemId, bool autofocus = false}) {
|
||||
final serverManager = MultiServerManager();
|
||||
return ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1081,7 +1081,7 @@ void main() {
|
||||
Widget buildRail(List<MediaHub> hubs, {String? initialHubId}) {
|
||||
final serverManager = MultiServerManager();
|
||||
return ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1124,7 +1124,7 @@ void main() {
|
||||
Widget buildRail(List<MediaHub> hubs, {String? initialItemId}) {
|
||||
final serverManager = MultiServerManager();
|
||||
return ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1204,7 +1204,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1318,7 +1318,7 @@ void main() {
|
||||
|
||||
Widget buildRail(List<MediaHub> hubs) {
|
||||
return ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1401,7 +1401,7 @@ void main() {
|
||||
|
||||
Widget buildRail({required bool backgroundLoaded}) {
|
||||
return ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1477,7 +1477,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1579,7 +1579,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1655,7 +1655,7 @@ void main() {
|
||||
final serverManager = MultiServerManager();
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1744,7 +1744,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1835,7 +1835,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1907,7 +1907,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -1972,7 +1972,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -2023,7 +2023,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -2071,7 +2071,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -2127,7 +2127,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -2178,7 +2178,7 @@ void main() {
|
||||
final item = testMediaItem(id: 'item_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie');
|
||||
final hub = MediaHub(id: 'hub_1', title: 'Hub', type: 'movie', items: [item], size: 1);
|
||||
return ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -2213,7 +2213,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -2258,7 +2258,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
create: (_) => testMultiServerProvider(serverManager),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
@@ -2289,7 +2289,7 @@ void main() {
|
||||
|
||||
testWidgets('background bleed updates do not renotify rail focus', (tester) async {
|
||||
final serverManager = MultiServerManager();
|
||||
final multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
final multiServerProvider = testMultiServerProvider(serverManager);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
final focusedItemIds = <String>[];
|
||||
|
||||
Reference in New Issue
Block a user