diff --git a/lib/focus/focusable_tile_mixin.dart b/lib/focus/focusable_tile_mixin.dart index 6ed39821..9b11c40d 100644 --- a/lib/focus/focusable_tile_mixin.dart +++ b/lib/focus/focusable_tile_mixin.dart @@ -7,23 +7,35 @@ import 'owned_focus_node_binding.dart'; /// auto-scrolls the tile into view when it gains focus. mixin FocusableTileStateMixin on State { 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() { diff --git a/lib/media/media_server_client.dart b/lib/media/media_server_client.dart index 161978b1..0af35131 100644 --- a/lib/media/media_server_client.dart +++ b/lib/media/media_server_client.dart @@ -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 fetchLyrics(MediaItem track); /// Free-text search across the user's libraries. diff --git a/lib/media/server_capabilities.dart b/lib/media/server_capabilities.dart index 993251a4..8dcafb82 100644 --- a/lib/media/server_capabilities.dart +++ b/lib/media/server_capabilities.dart @@ -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, ); } } diff --git a/lib/models/livetv_lineup.dart b/lib/models/livetv_lineup.dart deleted file mode 100644 index 524bf694..00000000 --- a/lib/models/livetv_lineup.dart +++ /dev/null @@ -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 _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 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 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 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 channels; - - const LiveTvLineup({ - required this.uuid, - this.type, - this.title, - this.lineupType, - this.location, - this.channels = const [], - }); - - factory LiveTvLineup.fromJson(Map json) => _$LiveTvLineupFromJson(json); -} - -class LiveTvLineupResult { - final String? lineupGroupUuid; - final List lineups; - - const LiveTvLineupResult({this.lineupGroupUuid, required this.lineups}); -} diff --git a/lib/models/livetv_lineup.g.dart b/lib/models/livetv_lineup.g.dart deleted file mode 100644 index f2c3920b..00000000 --- a/lib/models/livetv_lineup.g.dart +++ /dev/null @@ -1,42 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'livetv_lineup.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -LiveTvCountry _$LiveTvCountryFromJson(Map 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 json) => - LiveTvLanguage( - code: json['code'] as String? ?? '', - title: json['title'] as String? ?? '', - ); - -LiveTvRegion _$LiveTvRegionFromJson(Map json) => LiveTvRegion( - key: json['key'] as String? ?? '', - type: json['type'] as String?, - title: json['title'] as String? ?? '', -); - -LiveTvLineup _$LiveTvLineupFromJson(Map 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']), -); diff --git a/lib/models/livetv_server_status.dart b/lib/models/livetv_server_status.dart deleted file mode 100644 index eea63382..00000000 --- a/lib/models/livetv_server_status.dart +++ /dev/null @@ -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 json) => _$LiveTvServerStatusFromJson(json); - - Set 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'); -} diff --git a/lib/models/livetv_server_status.g.dart b/lib/models/livetv_server_status.g.dart deleted file mode 100644 index bcdd4431..00000000 --- a/lib/models/livetv_server_status.g.dart +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'livetv_server_status.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -LiveTvServerStatus _$LiveTvServerStatusFromJson(Map json) => - LiveTvServerStatus( - liveTvCount: flexibleInt(json['livetv']), - allowTuners: flexibleBoolNullable(json['allowTuners']), - ownerFeatures: json['ownerFeatures'] as String?, - ); diff --git a/lib/models/livetv_session.dart b/lib/models/livetv_session.dart deleted file mode 100644 index 05fe69d2..00000000 --- a/lib/models/livetv_session.dart +++ /dev/null @@ -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? 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 json) => _$LiveTvSessionFromJson(json); -} diff --git a/lib/models/livetv_session.g.dart b/lib/models/livetv_session.g.dart deleted file mode 100644 index 0e1f1571..00000000 --- a/lib/models/livetv_session.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'livetv_session.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -LiveTvSession _$LiveTvSessionFromJson(Map 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']), - ); diff --git a/lib/models/media_grabber_device.dart b/lib/models/media_grabber_device.dart deleted file mode 100644 index 5ab834e6..00000000 --- a/lib/models/media_grabber_device.dart +++ /dev/null @@ -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 _parseChannelMappings(Object? raw) => parseFlexibleJsonList(raw, ChannelMapping.fromJson); - -List _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 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 channelMappings; - @JsonKey(name: 'Setting', fromJson: _parseSettings) - final List 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 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 json) => _$MediaGrabberDeviceChannelFromJson(json); -} - -class MediaGrabberChannelMapRequest { - final List channelsEnabled; - final Map channelMapping; - final Map channelMappingByKey; - - const MediaGrabberChannelMapRequest({ - this.channelsEnabled = const [], - this.channelMapping = const {}, - this.channelMappingByKey = const {}, - }); -} diff --git a/lib/models/media_grabber_device.g.dart b/lib/models/media_grabber_device.g.dart deleted file mode 100644 index fff4efb0..00000000 --- a/lib/models/media_grabber_device.g.dart +++ /dev/null @@ -1,47 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'media_grabber_device.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -MediaGrabber _$MediaGrabberFromJson(Map json) => MediaGrabber( - identifier: json['identifier'] as String? ?? '', - protocol: json['protocol'] as String?, - title: json['title'] as String?, -); - -MediaGrabberDevice _$MediaGrabberDeviceFromJson(Map 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 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']), -); diff --git a/lib/screens/libraries/tabs/library_collections_tab.dart b/lib/screens/libraries/tabs/library_collections_tab.dart index 4dbd3cbc..6df6ed59 100644 --- a/lib/screens/libraries/tabs/library_collections_tab.dart +++ b/lib/screens/libraries/tabs/library_collections_tab.dart @@ -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 { State createState() => _LibraryCollectionsTabState(); } -class _LibraryCollectionsTabState extends BaseLibraryTabState - with - LibraryTabFocusMixin, - PaginatedItemLoader, - StandardPaginatedView, - 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 _cardMemo = SliverChildMemo(); +class _LibraryCollectionsTabState extends PaginatedCardGridTabState { + @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? getRefreshStream() => LibraryRefreshNotifier().collectionsStream; @override - Future> loadData() async => const []; + String idOf(MediaItem item) => item.id; @override Future> fetchPage(int start, int size, AbortController? abort) { @@ -80,42 +55,7 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState loadItems() { - return loadStandardPaginatedItems( - pageSize: _pageSize, - errorMessageFor: (error, stackTrace) => localizedLoadErrorMessage(error, stackTrace, context: errorContext), - onLoaded: (_, _) => markItemsLoaded(), - ); - } - - @override - Widget buildContent(List 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 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(); - } } diff --git a/lib/screens/libraries/tabs/library_playlists_tab.dart b/lib/screens/libraries/tabs/library_playlists_tab.dart index 12127c31..93b82cac 100644 --- a/lib/screens/libraries/tabs/library_playlists_tab.dart +++ b/lib/screens/libraries/tabs/library_playlists_tab.dart @@ -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 { State createState() => _LibraryPlaylistsTabState(); } -class _LibraryPlaylistsTabState extends BaseLibraryTabState - with - LibraryTabFocusMixin, - PaginatedItemLoader, - StandardPaginatedView, - 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 _cardMemo = SliverChildMemo(); +class _LibraryPlaylistsTabState extends PaginatedCardGridTabState { + @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? getRefreshStream() => LibraryRefreshNotifier().playlistsStream; @override - Future> loadData() async => const []; + String idOf(MediaPlaylist playlist) => playlist.id; @override Future> fetchPage(int start, int size, AbortController? abort) { @@ -86,130 +60,5 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState loadItems() { - return loadStandardPaginatedItems( - pageSize: _pageSize, - errorMessageFor: (error, stackTrace) => localizedLoadErrorMessage(error, stackTrace, context: errorContext), - onLoaded: (_, _) => markItemsLoaded(), - ); - } - - @override - Widget buildContent(List 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; } diff --git a/lib/screens/libraries/tabs/paginated_card_grid_tab.dart b/lib/screens/libraries/tabs/paginated_card_grid_tab.dart new file mode 100644 index 00000000..4686d32e --- /dev/null +++ b/lib/screens/libraries/tabs/paginated_card_grid_tab.dart @@ -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> + extends BaseLibraryTabState + with + LibraryTabFocusMixin, + PaginatedItemLoader, + StandardPaginatedView, + SkeletonUpgradeScheduler { + 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 _cardMemo = SliverChildMemo(); + + /// 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> loadData() async => []; + + @override + Future loadItems() { + return loadStandardPaginatedItems( + pageSize: pageSize, + errorMessageFor: (error, stackTrace) => localizedLoadErrorMessage(error, stackTrace, context: errorContext), + onLoaded: (_, _) => markItemsLoaded(), + ); + } + + @override + Widget buildContent(List 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(); + } +} diff --git a/lib/services/trackers/anime_lists_mapping_store.dart b/lib/services/trackers/anime_lists_mapping_store.dart index cc8f216f..6b412a60 100644 --- a/lib/services/trackers/anime_lists_mapping_store.dart +++ b/lib/services/trackers/anime_lists_mapping_store.dart @@ -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> byTvdb; final Map> 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> 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? _loading; - bool _refreshRunning = false; - - Future _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 _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 _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 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 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> 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> 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 {}; } - - Future 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 _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 _deleteDiskCopy() async { - try { - await File(await _diskPath()).delete(); - } on FileSystemException { - // Already gone. - } - } - - Future _diskPath() async { - final dir = await getApplicationSupportDirectory(); - return p.join(dir.path, _diskFileName); - } - - @visibleForTesting - void resetForTesting() { - _index = null; - _loading = null; - } } @visibleForTesting diff --git a/lib/services/trackers/etag_cached_remote_store.dart b/lib/services/trackers/etag_cached_remote_store.dart new file mode 100644 index 00000000..10db84e2 --- /dev/null +++ b/lib/services/trackers/etag_cached_remote_store.dart @@ -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 { + 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? _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 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 _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 _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 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 _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 _deleteDiskCopy() async { + try { + await File(await _diskPath()).delete(); + } on FileSystemException { + // Already gone. + } + } + + Future _diskPath() async { + final dir = await getApplicationSupportDirectory(); + return p.join(dir.path, diskFileName); + } + + @visibleForTesting + void resetForTesting() { + _index = null; + _loading = null; + } +} diff --git a/lib/services/trackers/fribb_mapping_store.dart b/lib/services/trackers/fribb_mapping_store.dart index 2e4a5917..5653e088 100644 --- a/lib/services/trackers/fribb_mapping_store.dart +++ b/lib/services/trackers/fribb_mapping_store.dart @@ -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> byTvdb; final Map> byTmdb; final Map> 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 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? _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 _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 _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 _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 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> 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 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 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 _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 _deleteDiskCopy() async { - try { - await File(await _diskPath()).delete(); - } on FileSystemException { - // Already gone. - } - } - - Future _diskPath() async { - final dir = await getApplicationSupportDirectory(); - return p.join(dir.path, _diskFileName); - } - - @visibleForTesting - void resetForTesting() { - _index = null; - _loading = null; - } + Future 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); diff --git a/lib/widgets/app_menu.dart b/lib/widgets/app_menu.dart index 05f00b45..4e93c7d9 100644 --- a/lib/widgets/app_menu.dart +++ b/lib/widgets/app_menu.dart @@ -390,16 +390,15 @@ class _AppMenuItemTileState extends State> with FocusableT @override void initState() { super.initState(); - initFocusNode(); effectiveFocusNode.addListener(_updateFocusedState); } @override void didUpdateWidget(AppMenuItemTile 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 extends State> with FocusableT @override void dispose() { effectiveFocusNode.removeListener(_updateFocusedState); - disposeFocusNode(); super.dispose(); } diff --git a/lib/widgets/focusable_list_tile.dart b/lib/widgets/focusable_list_tile.dart index 04419d3f..709234b4 100644 --- a/lib/widgets/focusable_list_tile.dart +++ b/lib/widgets/focusable_list_tile.dart @@ -82,24 +82,6 @@ class _FocusableListTileState extends State 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 extends State> @override FocusNode? get widgetFocusNode => widget.focusNode; - @override - void initState() { - super.initState(); - initFocusNode(); - } - - @override - void didUpdateWidget(FocusableRadioListTile 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 @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 @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( diff --git a/lib/widgets/music/track_row.dart b/lib/widgets/music/track_row.dart index b0e2c72b..aba4bcf5 100644 --- a/lib/widgets/music/track_row.dart +++ b/lib/widgets/music/track_row.dart @@ -123,24 +123,6 @@ class _TrackRowState extends State with ContextMenuTapMixin, @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; diff --git a/test/mixins/library_tab_state_test.dart b/test/mixins/library_tab_state_test.dart index 43a87408..1c7fdec4 100644 --- a/test/mixins/library_tab_state_test.dart +++ b/test/mixins/library_tab_state_test.dart @@ -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); diff --git a/test/navigation/profile_session_screen_test.dart b/test/navigation/profile_session_screen_test.dart index 559edc7e..aadc19c6 100644 --- a/test/navigation/profile_session_screen_test.dart +++ b/test/navigation/profile_session_screen_test.dart @@ -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); diff --git a/test/providers/libraries_provider_test.dart b/test/providers/libraries_provider_test.dart index de4793d0..d430a7af 100644 --- a/test/providers/libraries_provider_test.dart +++ b/test/providers/libraries_provider_test.dart @@ -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); diff --git a/test/providers/offline_mode_provider_test.dart b/test/providers/offline_mode_provider_test.dart index 46c7985e..12f80141 100644 --- a/test/providers/offline_mode_provider_test.dart +++ b/test/providers/offline_mode_provider_test.dart @@ -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(); diff --git a/test/screens/catalog_item_detail_screen_test.dart b/test/screens/catalog_item_detail_screen_test.dart index 4e43dad2..04678b1e 100644 --- a/test/screens/catalog_item_detail_screen_test.dart +++ b/test/screens/catalog_item_detail_screen_test.dart @@ -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 _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); diff --git a/test/screens/catalog_search_screen_test.dart b/test/screens/catalog_search_screen_test.dart index 043390e3..cad6b380 100644 --- a/test/screens/catalog_search_screen_test.dart +++ b/test/screens/catalog_search_screen_test.dart @@ -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 _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); diff --git a/test/screens/collection_detail_screen_test.dart b/test/screens/collection_detail_screen_test.dart index 4da96f39..79e2ff0a 100644 --- a/test/screens/collection_detail_screen_test.dart +++ b/test/screens/collection_detail_screen_test.dart @@ -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 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(); diff --git a/test/screens/discover_screen_test.dart b/test/screens/discover_screen_test.dart index d7c7e49e..769509f0 100644 --- a/test/screens/discover_screen_test.dart +++ b/test/screens/discover_screen_test.dart @@ -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(); diff --git a/test/screens/downloads/downloads_screen_focus_test.dart b/test/screens/downloads/downloads_screen_focus_test.dart index c9912611..4d9189be 100644 --- a/test/screens/downloads/downloads_screen_focus_test.dart +++ b/test/screens/downloads/downloads_screen_focus_test.dart @@ -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 { diff --git a/test/screens/downloads/sync_rules_screen_test.dart b/test/screens/downloads/sync_rules_screen_test.dart index 2660cc8b..e7ef8025 100644 --- a/test/screens/downloads/sync_rules_screen_test.dart +++ b/test/screens/downloads/sync_rules_screen_test.dart @@ -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); diff --git a/test/screens/hub_detail_screen_test.dart b/test/screens/hub_detail_screen_test.dart index a78966ce..a78b18ab 100644 --- a/test/screens/hub_detail_screen_test.dart +++ b/test/screens/hub_detail_screen_test.dart @@ -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 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); } diff --git a/test/screens/libraries/folder_tree_view_test.dart b/test/screens/libraries/folder_tree_view_test.dart index 491bbae7..a2a09374 100644 --- a/test/screens/libraries/folder_tree_view_test.dart +++ b/test/screens/libraries/folder_tree_view_test.dart @@ -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.value( diff --git a/test/screens/libraries/libraries_screen_test.dart b/test/screens/libraries/libraries_screen_test.dart index 2805d434..8c1da0ac 100644 --- a/test/screens/libraries/libraries_screen_test.dart +++ b/test/screens/libraries/libraries_screen_test.dart @@ -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); } diff --git a/test/screens/libraries/library_browse_music_test.dart b/test/screens/libraries/library_browse_music_test.dart index 8ce74156..63a15d71 100644 --- a/test/screens/libraries/library_browse_music_test.dart +++ b/test/screens/libraries/library_browse_music_test.dart @@ -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() { diff --git a/test/screens/libraries/library_browse_tab_test.dart b/test/screens/libraries/library_browse_tab_test.dart index 909a1dd2..58ec55b8 100644 --- a/test/screens/libraries/library_browse_tab_test.dart +++ b/test/screens/libraries/library_browse_tab_test.dart @@ -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) { diff --git a/test/screens/libraries/library_playlists_tab_test.dart b/test/screens/libraries/library_playlists_tab_test.dart index 6b15ef4c..2ecc6c5a 100644 --- a/test/screens/libraries/library_playlists_tab_test.dart +++ b/test/screens/libraries/library_playlists_tab_test.dart @@ -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 dispose() async { diff --git a/test/screens/livetv/guide_tab_test.dart b/test/screens/livetv/guide_tab_test.dart index 8789ee26..ee7cb394 100644 --- a/test/screens/livetv/guide_tab_test.dart +++ b/test/screens/livetv/guide_tab_test.dart @@ -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'), diff --git a/test/screens/livetv/live_tv_screen_test.dart b/test/screens/livetv/live_tv_screen_test.dart index 9d649c39..2b7841a9 100644 --- a/test/screens/livetv/live_tv_screen_test.dart +++ b/test/screens/livetv/live_tv_screen_test.dart @@ -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'), ]); diff --git a/test/screens/livetv/live_tv_show_schedule_screen_test.dart b/test/screens/livetv/live_tv_show_schedule_screen_test.dart index b75a8618..06151f58 100644 --- a/test/screens/livetv/live_tv_show_schedule_screen_test.dart +++ b/test/screens/livetv/live_tv_show_schedule_screen_test.dart @@ -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 _pumpScreen(WidgetTester tester, MultiServerProvider provider) async { diff --git a/test/screens/livetv/recordings_tab_test.dart b/test/screens/livetv/recordings_tab_test.dart index ed38a978..bf27bd99 100644 --- a/test/screens/livetv/recordings_tab_test.dart +++ b/test/screens/livetv/recordings_tab_test.dart @@ -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(); diff --git a/test/screens/media_detail_screen_test.dart b/test/screens/media_detail_screen_test.dart index e11dc14e..3cd95470 100644 --- a/test/screens/media_detail_screen_test.dart +++ b/test/screens/media_detail_screen_test.dart @@ -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 { diff --git a/test/screens/metadata_edit_screen_test.dart b/test/screens/metadata_edit_screen_test.dart index 3f12cb3e..82742ae4 100644 --- a/test/screens/metadata_edit_screen_test.dart +++ b/test/screens/metadata_edit_screen_test.dart @@ -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(_show()); await tester.pumpWidget( diff --git a/test/screens/music/album_detail_screen_test.dart b/test/screens/music/album_detail_screen_test.dart index 83e4480b..0d64c747 100644 --- a/test/screens/music/album_detail_screen_test.dart +++ b/test/screens/music/album_detail_screen_test.dart @@ -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 tracks) async { final client = _FakeMusicClient(tracks); final manager = MultiServerManager()..debugRegisterClientForTesting(client); - final multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager)); + final multiServerProvider = testMultiServerProvider(manager); addTearDown(multiServerProvider.dispose); diff --git a/test/screens/music/now_playing_screen_test.dart b/test/screens/music/now_playing_screen_test.dart index c3fd012a..b3b1371b 100644 --- a/test/screens/music/now_playing_screen_test.dart +++ b/test/screens/music/now_playing_screen_test.dart @@ -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( diff --git a/test/screens/music/queue_sheet_test.dart b/test/screens/music/queue_sheet_test.dart index ac548bfe..0195479b 100644 --- a/test/screens/music/queue_sheet_test.dart +++ b/test/screens/music/queue_sheet_test.dart @@ -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( diff --git a/test/screens/playlist_detail_screen_test.dart b/test/screens/playlist_detail_screen_test.dart index 69151ff1..052fc93e 100644 --- a/test/screens/playlist_detail_screen_test.dart +++ b/test/screens/playlist_detail_screen_test.dart @@ -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 { diff --git a/test/screens/profile/profile_teardown_test.dart b/test/screens/profile/profile_teardown_test.dart index 0fa02b79..8895bb1a 100644 --- a/test/screens/profile/profile_teardown_test.dart +++ b/test/screens/profile/profile_teardown_test.dart @@ -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); diff --git a/test/screens/settings/add_jellyfin_screen_test.dart b/test/screens/settings/add_jellyfin_screen_test.dart index 08ac3338..8da907d5 100644 --- a/test/screens/settings/add_jellyfin_screen_test.dart +++ b/test/screens/settings/add_jellyfin_screen_test.dart @@ -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, diff --git a/test/services/episode_navigation_service_test.dart b/test/services/episode_navigation_service_test.dart index 23dfc67b..22b9a568 100644 --- a/test/services/episode_navigation_service_test.dart +++ b/test/services/episode_navigation_service_test.dart @@ -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); diff --git a/test/services/live_tv_capability_contract_test.dart b/test/services/live_tv_capability_contract_test.dart index 6c97b18f..7ff303ea 100644 --- a/test/services/live_tv_capability_contract_test.dart +++ b/test/services/live_tv_capability_contract_test.dart @@ -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(); diff --git a/test/utils/live_tv_player_navigation_test.dart b/test/utils/live_tv_player_navigation_test.dart index 130d6233..5ccd4f11 100644 --- a/test/utils/live_tv_player_navigation_test.dart +++ b/test/utils/live_tv_player_navigation_test.dart @@ -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(() { diff --git a/test/widgets/library_management_sheet_test.dart b/test/widgets/library_management_sheet_test.dart index 2bcba7da..532ca2e2 100644 --- a/test/widgets/library_management_sheet_test.dart +++ b/test/widgets/library_management_sheet_test.dart @@ -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() { diff --git a/test/widgets/media_context_menu_test.dart b/test/widgets/media_context_menu_test.dart index 63c38449..73f40f6e 100644 --- a/test/widgets/media_context_menu_test.dart +++ b/test/widgets/media_context_menu_test.dart @@ -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> _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(); diff --git a/test/widgets/music/mini_player_test.dart b/test/widgets/music/mini_player_test.dart index d114bccc..df3b1c20 100644 --- a/test/widgets/music/mini_player_test.dart +++ b/test/widgets/music/mini_player_test.dart @@ -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( diff --git a/test/widgets/side_navigation_rail_test.dart b/test/widgets/side_navigation_rail_test.dart index 69a93be1..158f3429 100644 --- a/test/widgets/side_navigation_rail_test.dart +++ b/test/widgets/side_navigation_rail_test.dart @@ -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 _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 = []; @@ -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(); @@ -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(); diff --git a/test/widgets/tv_browse_rail_test.dart b/test/widgets/tv_browse_rail_test.dart index 00901b5d..b46dd3e6 100644 --- a/test/widgets/tv_browse_rail_test.dart +++ b/test/widgets/tv_browse_rail_test.dart @@ -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( - 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( - 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( - create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)), + create: (_) => testMultiServerProvider(serverManager), child: MaterialApp( theme: theme, home: Scaffold( @@ -513,7 +513,7 @@ void main() { await tester.pumpWidget( ChangeNotifierProvider( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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 hubs, {String? initialHubId, String? initialItemId, bool autofocus = false}) { final serverManager = MultiServerManager(); return ChangeNotifierProvider( - 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 hubs, {String? initialHubId}) { final serverManager = MultiServerManager(); return ChangeNotifierProvider( - 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 hubs, {String? initialItemId}) { final serverManager = MultiServerManager(); return ChangeNotifierProvider( - 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( - 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 hubs) { return ChangeNotifierProvider( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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 = [];