refactor(features): consolidate shared feature primitives

This commit is contained in:
edde746
2026-07-12 17:31:13 +02:00
parent 6506f36843
commit 7ecadffdb1
103 changed files with 1718 additions and 898 deletions
+4 -3
View File
@@ -5,9 +5,10 @@ import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_item_types.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/utils/content_utils.dart';
import '../test_helpers/media_items.dart';
MediaItem _episode({int? viewOffsetMs, int? durationMs, int? viewCount, int? leafCount, int? viewedLeafCount}) {
return MediaItem(
return testMediaItem(
id: '1',
backend: MediaBackend.plex,
kind: MediaKind.episode,
@@ -20,7 +21,7 @@ MediaItem _episode({int? viewOffsetMs, int? durationMs, int? viewCount, int? lea
}
MediaItem _movie({int? viewCount}) {
return MediaItem(id: '1', backend: MediaBackend.plex, kind: MediaKind.movie, viewCount: viewCount);
return testMediaItem(id: '1', backend: MediaBackend.plex, kind: MediaKind.movie, viewCount: viewCount);
}
void main() {
@@ -58,7 +59,7 @@ void main() {
group('MediaItemTypes.shouldHideSpoiler', () {
test('false for non-episodes', () {
expect(_movie().shouldHideSpoiler, isFalse);
final show = MediaItem(id: '1', backend: MediaBackend.plex, kind: MediaKind.show);
final show = testMediaItem(id: '1', backend: MediaBackend.plex, kind: MediaKind.show);
expect(show.shouldHideSpoiler, isFalse);
});
@@ -2,9 +2,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/models/download_models.dart';
import 'package:plezy/utils/downloaded_version_match.dart';
import '../test_helpers/media_items.dart';
DownloadedMediaItem _row({int mediaIndex = 0, String? mediaSourceId}) {
return DownloadedMediaItem(
return DownloadedtestMediaItem(
id: 1,
serverId: 'srv',
ratingKey: 'movie-1',
+11 -10
View File
@@ -9,8 +9,9 @@ import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/media/media_version.dart';
import 'package:plezy/utils/download_version_utils.dart';
import 'package:plezy/media/episode_collection.dart';
import '../test_helpers/media_items.dart';
MediaItem _season(String id, {int index = 1, int? leafCount, int? viewedLeafCount}) => MediaItem(
MediaItem _season(String id, {int index = 1, int? leafCount, int? viewedLeafCount}) => testMediaItem(
id: id,
backend: MediaBackend.plex,
kind: MediaKind.season,
@@ -31,7 +32,7 @@ MediaItem _episode(
int? viewOffsetMs,
int? durationMs,
String? originallyAvailableAt,
}) => MediaItem(
}) => testMediaItem(
id: id,
backend: MediaBackend.plex,
kind: MediaKind.episode,
@@ -47,7 +48,7 @@ MediaItem _episode(
originallyAvailableAt: originallyAvailableAt,
);
MediaItem _clip(String id) => MediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.clip, title: 'Clip');
MediaItem _clip(String id) => testMediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.clip, title: 'Clip');
class _RecordingClient implements MediaServerClient {
_RecordingClient({this.childrenByParent = const {}, this.childrenPageByParent = const {}, this.itemsById = const {}});
@@ -111,7 +112,7 @@ class _LeavesClient implements MediaServerClient {
}
void main() {
test('collectEpisodesForShow drops Specials when includeSpecials is false', () async {
test('collectEpisodes drops Specials when includeSpecials is false', () async {
final client = _LeavesClient([
_episode('s1e1', parentIndex: 1, index: 1, originallyAvailableAt: '2022-10-05'),
_episode('s0e1', parentIndex: 0, index: 1, originallyAvailableAt: '2022-10-27'),
@@ -119,12 +120,12 @@ void main() {
]);
final withoutSpecials = <MediaItem>[];
await collectEpisodesForShow(client, 'show-1', unwatchedOnly: false, out: withoutSpecials, includeSpecials: false);
await collectEpisodes(client, 'show-1', unwatchedOnly: false, out: withoutSpecials, includeSpecials: false);
expect(withoutSpecials.map((e) => e.id), ['s1e1', 's1e2']);
// Default keeps Specials, interleaved into aired order.
final withSpecials = <MediaItem>[];
await collectEpisodesForShow(client, 'show-1', unwatchedOnly: false, out: withSpecials);
await collectEpisodes(client, 'show-1', unwatchedOnly: false, out: withSpecials);
expect(withSpecials.map((e) => e.id), ['s1e1', 's0e1', 's1e2']);
});
@@ -317,7 +318,7 @@ void main() {
});
test('fetchSeasonEpisodePage normalizes show and season identity', () async {
final show = MediaItem(
final show = testMediaItem(
id: 'show-1',
backend: MediaBackend.plex,
kind: MediaKind.show,
@@ -350,7 +351,7 @@ void main() {
});
test('fetchSeasonEpisodePage uses season episode paging when available', () async {
final show = MediaItem(id: 'show-1', backend: MediaBackend.plex, kind: MediaKind.show, title: 'Show');
final show = testMediaItem(id: 'show-1', backend: MediaBackend.plex, kind: MediaKind.show, title: 'Show');
final season = _season('season-1');
final row = _episode('episode-1');
final client = _SeasonPagingRecordingClient(
@@ -367,7 +368,7 @@ void main() {
});
test('normalizeSeasonEpisodes ignores non-episode rows', () {
final show = MediaItem(id: 'show-1', backend: MediaBackend.plex, kind: MediaKind.show, title: 'Show');
final show = testMediaItem(id: 'show-1', backend: MediaBackend.plex, kind: MediaKind.show, title: 'Show');
final season = _season('season-1');
final normalized = normalizeSeasonEpisodes([_clip('extra-1'), _episode('episode-1')], show: show, season: season);
@@ -395,7 +396,7 @@ void main() {
test('fetchRepresentativeVersions keeps full season lookup but pages selected season episodes', () async {
final versions = [const MediaVersion(id: '1080', videoResolution: '1080')];
final show = MediaItem(id: 'show-1', backend: MediaBackend.plex, kind: MediaKind.show, title: 'Show');
final show = testMediaItem(id: 'show-1', backend: MediaBackend.plex, kind: MediaKind.show, title: 'Show');
final special = _season('specials', index: 0);
final firstRegularSeason = _season('season-1');
final episodeRow = _episode('episode-1');
+27
View File
@@ -227,4 +227,31 @@ void main() {
expect(flexibleCsvStringList(<dynamic>[]), isNull);
});
});
group('flexible JSON objects', () {
String parseId(Map<String, dynamic> json) => json['id'] as String;
test('list parser keeps valid siblings around malformed entries', () {
final parsed = parseFlexibleJsonList([
{'id': 'first'},
{'id': 2},
'not-a-map',
{'id': 'last'},
], parseId);
expect(parsed, ['first', 'last']);
});
test('object parser finds the first map and contains parse failures', () {
expect(
parseFlexibleJsonObject([
'not-a-map',
{'id': 'value'},
], parseId),
'value',
);
expect(parseFlexibleJsonObject({'id': 2}, parseId), isNull);
expect(parseFlexibleJsonObject(null, parseId), isNull);
});
});
}
+2 -1
View File
@@ -6,6 +6,7 @@ import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_library.dart';
import 'package:plezy/utils/media_hub_ordering.dart';
import '../test_helpers/media_items.dart';
const Object _defaultServerId = Object();
@@ -20,7 +21,7 @@ MediaLibrary _library(String id, {ServerId? serverId}) {
}
MediaItem _item(String id, {String? libraryId, Object? serverId = _defaultServerId}) {
return MediaItem(
return testMediaItem(
id: id,
backend: MediaBackend.plex,
kind: MediaKind.movie,
+4 -3
View File
@@ -4,10 +4,11 @@ import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/services/settings_service.dart';
import 'package:plezy/utils/media_navigation_helper.dart';
import '../test_helpers/media_items.dart';
void main() {
test('episode detail target opens parent show and focuses season episode', () {
final episode = MediaItem(
final episode = testMediaItem(
id: 'episode-1',
backend: MediaBackend.plex,
kind: MediaKind.episode,
@@ -29,7 +30,7 @@ void main() {
});
test('season detail target opens parent show and focuses season', () {
final season = MediaItem(
final season = testMediaItem(
id: 'season-3',
backend: MediaBackend.jellyfin,
kind: MediaKind.season,
@@ -50,7 +51,7 @@ void main() {
});
test('movie detail target keeps the movie itself', () {
final movie = MediaItem(id: 'movie-1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie');
final movie = testMediaItem(id: 'movie-1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie');
final target = mediaDetailNavigationTargetFor(movie);
+2 -1
View File
@@ -9,6 +9,7 @@ import 'package:plezy/media/media_version.dart';
import 'package:plezy/services/jellyfin_mappers.dart';
import 'package:plezy/services/plex_mappers.dart';
import 'package:plezy/utils/media_quality_labels.dart';
import '../test_helpers/media_items.dart';
void main() {
group('buildMediaQualityLabels', () {
@@ -216,7 +217,7 @@ void main() {
}
MediaItem _episodeWithVersion(MediaVersion? version) {
return MediaItem(
return testMediaItem(
id: 'episode-1',
backend: MediaBackend.plex,
kind: MediaKind.episode,
+3 -2
View File
@@ -3,6 +3,7 @@ import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/utils/plex_season_display.dart';
import '../test_helpers/media_items.dart';
void main() {
group('shouldShowPlexEpisodesDirectly', () {
@@ -57,9 +58,9 @@ void main() {
}
MediaItem _show({Map<String, Object?>? raw}) {
return MediaItem(id: 'show', backend: MediaBackend.plex, kind: MediaKind.show, raw: raw);
return testMediaItem(id: 'show', backend: MediaBackend.plex, kind: MediaKind.show, raw: raw);
}
MediaItem _season(String id) {
return MediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.season);
return testMediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.season);
}
+14 -3
View File
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
@@ -9,11 +10,21 @@ import 'package:plezy/services/settings_service.dart';
import 'package:plezy/utils/video_player_navigation.dart';
import '../test_helpers/prefs.dart';
import '../test_helpers/media_items.dart';
void main() {
test('VOD and Live TV route contract is opaque, named, and transition-free', () {
final route = buildVideoPlayerRoute(builder: (_) => const SizedBox());
expect(route.settings.name, kVideoPlayerRouteName);
expect(route.opaque, isTrue);
expect(route.transitionDuration, Duration.zero);
expect(route.reverseTransitionDuration, Duration.zero);
});
test('in-flight video player navigation rejects duplicate requests', () {
final guard = VideoPlayerNavigationInFlightGuard();
final item = MediaItem(
final item = testMediaItem(
id: 'episode_1',
backend: MediaBackend.plex,
kind: MediaKind.episode,
@@ -48,7 +59,7 @@ void main() {
MediaVersion(id: '102', videoResolution: '4k', videoCodec: 'hevc', container: 'mkv'),
];
final episode = MediaItem(
final episode = testMediaItem(
id: 'ep-1',
backend: MediaBackend.plex,
kind: MediaKind.episode,
@@ -122,7 +133,7 @@ void main() {
);
SettingsService.resetForTesting();
final bare = MediaItem(
final bare = testMediaItem(
id: 'ep-2',
backend: MediaBackend.jellyfin,
kind: MediaKind.episode,