refactor: extract shared mixins and helpers, drop dead abstractions

Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.

Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
This commit is contained in:
edde746
2026-07-26 06:09:48 +02:00
parent 61344f7862
commit 352b88109b
217 changed files with 6813 additions and 8773 deletions
+3 -2
View File
@@ -20,6 +20,7 @@ import 'package:plezy/utils/active_client_scope.dart';
import 'package:plezy/utils/media_server_http_client.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import '../test_helpers/download_fixtures.dart';
import '../test_helpers/prefs.dart';
void main() {
@@ -68,8 +69,8 @@ class _AppDatabaseTestSuite {
// v20 dropped the profile_id FK so virtual Plex Home profiles can
// persist join rows without a parent `profiles` row. Profile deletion
// instead cleans up join rows explicitly (via the teardown flow's
// removeAllProfileConnectionsAndCleanup) before deleting the profile,
// so the cascade isn't needed.
// ProfileConnectionCleanup.removeAllProfileConnections) before deleting
// the profile, so the cascade isn't needed.
final now = DateTime.now().millisecondsSinceEpoch;
await db
.into(db.connections)
+2 -109
View File
@@ -8,6 +8,8 @@ import 'package:plezy/database/app_database.dart';
import 'package:plezy/database/download_operations.dart';
import 'package:plezy/models/download_models.dart';
import '../test_helpers/download_fixtures.dart';
void main() {
late AppDatabase db;
@@ -19,115 +21,6 @@ void main() {
await db.close();
});
// ============================================================
// insertDownload
// ============================================================
group('insertDownload', () {
test('inserts a movie row with defaults', () async {
await db.insertDownload(
serverId: ServerId('srv'),
ratingKey: '100',
globalKey: 'srv:100',
type: 'movie',
status: DownloadStatus.queued.index,
);
final rows = await db.select(db.downloadedMedia).get();
expect(rows, hasLength(1));
final r = rows.first;
expect(r.serverId, 'srv');
expect(r.ratingKey, '100');
expect(r.globalKey, 'srv:100');
expect(r.type, 'movie');
expect(r.status, DownloadStatus.queued.index);
expect(r.parentRatingKey, isNull);
expect(r.grandparentRatingKey, isNull);
expect(r.mediaIndex, 0);
});
test('inserts an episode with parent and grandparent keys', () async {
await db.insertDownload(
serverId: ServerId('srv'),
ratingKey: 'ep1',
globalKey: 'srv:ep1',
type: 'episode',
parentRatingKey: 'season1',
grandparentRatingKey: 'show1',
status: DownloadStatus.queued.index,
mediaIndex: 7,
);
final row = (await db.select(db.downloadedMedia).get()).single;
expect(row.parentRatingKey, 'season1');
expect(row.grandparentRatingKey, 'show1');
expect(row.mediaIndex, 7);
});
test('atomically updates metadata and attempt state while preserving the row and physical fields', () async {
await db.insertDownload(
serverId: ServerId('srv'),
clientScopeId: 'scope-old',
ratingKey: '100',
globalKey: 'srv:100',
type: 'movie',
status: DownloadStatus.queued.index,
mediaIndex: 1,
mediaSourceId: 'source-old',
);
final original = (await db.getDownloadedMedia('srv:100'))!;
await (db.update(db.downloadedMedia)..where((row) => row.globalKey.equals('srv:100'))).write(
const DownloadedMediaCompanion(
progress: Value(50),
downloadedBytes: Value(500),
totalBytes: Value(1000),
videoFilePath: Value('downloads/video.mkv'),
safRootUri: Value('content://downloads'),
thumbPath: Value('downloads/thumb.jpg'),
downloadedAt: Value(1234),
errorMessage: Value('old error'),
retryCount: Value(2),
bgTaskId: Value('current-task'),
),
);
await db.insertDownload(
serverId: ServerId('srv-new'),
clientScopeId: 'scope-new',
ratingKey: '100-new',
globalKey: 'srv:100',
type: 'episode',
parentRatingKey: 'season-new',
grandparentRatingKey: 'show-new',
status: DownloadStatus.failed.index,
mediaIndex: 3,
mediaSourceId: 'source-new',
);
final row = (await db.select(db.downloadedMedia).get()).single;
expect(row.id, original.id);
expect(row.serverId, 'srv-new');
expect(row.clientScopeId, 'scope-new');
expect(row.ratingKey, '100-new');
expect(row.type, 'episode');
expect(row.parentRatingKey, 'season-new');
expect(row.grandparentRatingKey, 'show-new');
expect(row.status, DownloadStatus.failed.index);
expect(row.mediaIndex, 3);
expect(row.mediaSourceId, 'source-new');
expect(row.progress, 0);
expect(row.downloadedBytes, 0);
expect(row.totalBytes, isNull);
expect(row.errorMessage, isNull);
expect(row.retryCount, 0);
expect(row.videoFilePath, 'downloads/video.mkv');
expect(row.safRootUri, 'content://downloads');
expect(row.thumbPath, 'downloads/thumb.jpg');
expect(row.downloadedAt, 1234);
expect(row.bgTaskId, 'current-task');
});
});
group('insertQueuedDownload', () {
test('atomically persists media identity, scope, policy, and queue state', () async {
final tempDir = await Directory.systemTemp.createTemp('plezy_atomic_queue_');
+34 -22
View File
@@ -7,6 +7,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/mixins/paginated_item_loader.dart';
import 'package:plezy/mixins/standard_paginated_view.dart';
import 'package:plezy/utils/media_server_http_client.dart';
import 'package:plezy/exceptions/media_server_exceptions.dart';
import '../test_helpers/media_items.dart';
@@ -27,10 +28,20 @@ class _PaginatedProbe extends StatefulWidget {
State<_PaginatedProbe> createState() => _PaginatedProbeState();
}
class _PaginatedProbeState extends State<_PaginatedProbe> with PaginatedItemLoader<MediaItem, _PaginatedProbe> {
class _PaginatedProbeState extends State<_PaginatedProbe>
with PaginatedItemLoader<MediaItem, _PaginatedProbe>, StandardPaginatedView<MediaItem, _PaginatedProbe> {
int fetchCalls = 0;
final List<({int start, int size})> fetchArgs = [];
/// View fields required by [StandardPaginatedView], mirroring the
/// `items`/`isLoading`/`errorMessage` trio the real screens expose.
@override
List<MediaItem> items = [];
@override
bool isLoading = false;
@override
String? errorMessage;
@override
Future<LibraryPage<MediaItem>> fetchPage(int start, int size, AbortController? abort) {
fetchCalls++;
@@ -109,10 +120,8 @@ void main() {
expect(hooked, [(0, 5)]);
});
testWidgets('loadInitialPaginatedItems applies reset, data, and success callback', (tester) async {
testWidgets('loadStandardPaginatedItems resets view state, publishes items, and fires onLoaded', (tester) async {
late _PaginatedProbeState state;
var reset = false;
List<MediaItem>? applied;
(int, int)? counts;
await tester.pumpWidget(
_PaginatedProbe(
@@ -121,25 +130,26 @@ void main() {
),
);
final succeeded = await state.loadInitialPaginatedItems(
// Stale view state from a previous load must be cleared by the reset.
state.items = [_meta(99)];
state.errorMessage = 'stale error';
await state.loadStandardPaginatedItems(
pageSize: 3,
resetViewState: () => reset = true,
applyLoadedItems: (items) => applied = items,
applyError: (error, stackTrace) => fail('unexpected error: $error'),
errorMessageFor: (error, stackTrace) => fail('unexpected error: $error'),
onLoaded: (loaded, total) => counts = (loaded, total),
);
await tester.pump();
expect(succeeded, isTrue);
expect(reset, isTrue);
expect(applied?.map((item) => item.id), ['k0', 'k1', 'k2']);
expect(state.items.map((item) => item.id), ['k0', 'k1', 'k2']);
expect(state.isLoading, isFalse);
expect(state.errorMessage, isNull);
expect(counts, (3, 7));
});
testWidgets('loadInitialPaginatedItems applies one error transaction', (tester) async {
testWidgets('loadStandardPaginatedItems applies one error transaction', (tester) async {
late _PaginatedProbeState state;
Object? appliedError;
Object? loggedError;
Object? reportedError;
await tester.pumpWidget(
_PaginatedProbe(
onState: (s) => state = s,
@@ -147,18 +157,20 @@ void main() {
),
);
final succeeded = await state.loadInitialPaginatedItems(
await state.loadStandardPaginatedItems(
pageSize: 3,
resetViewState: () {},
applyLoadedItems: (_) => fail('items must not be applied'),
applyError: (error, stackTrace) => appliedError = error,
onError: (error, stackTrace) => loggedError = error,
errorMessageFor: (error, stackTrace) {
reportedError = error;
return 'could not load';
},
onLoaded: (_, _) => fail('items must not be applied'),
);
await tester.pump();
expect(succeeded, isFalse);
expect(appliedError, isA<StateError>());
expect(loggedError, same(appliedError));
expect(reportedError, isA<StateError>());
expect(state.errorMessage, 'could not load');
expect(state.isLoading, isFalse);
expect(state.items, isEmpty);
});
testWidgets('totalSize == 0 means no more pages — ensureRangeLoaded is a no-op', (tester) async {
@@ -83,6 +83,7 @@ void main() {
late ConnectionRegistry connections;
late ProfileConnectionRegistry profileConnections;
late StorageService storage;
late ProfileConnectionCleanup cleanup;
setUp(() async {
resetSharedPreferencesForTest();
@@ -90,6 +91,11 @@ void main() {
connections = ConnectionRegistry(db);
profileConnections = ProfileConnectionRegistry(db);
storage = await StorageService.getInstance();
cleanup = ProfileConnectionCleanup(
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
});
tearDown(() async {
@@ -112,13 +118,7 @@ void main() {
await storage.saveHiddenLibraries({'jf-machine:movies'});
await storage.saveLibraryOrder(['jf-machine:movies']);
await removeProfileConnectionAndCleanup(
profileId: 'p1',
connection: conn,
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
await cleanup.removeProfileConnection(profileId: 'p1', connection: conn);
expect(await profileConnections.listForConnection(conn.id), isEmpty);
expect(await connections.get(conn.id), isNull);
@@ -151,13 +151,7 @@ void main() {
await storage.setActiveProfileId('p2');
await storage.saveHiddenLibraries({'jf-machine:movies'});
await removeProfileConnectionAndCleanup(
profileId: 'p1',
connection: conn,
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
await cleanup.removeProfileConnection(profileId: 'p1', connection: conn);
expect(await connections.get(conn.id), isNotNull);
final remaining = await profileConnections.listForConnection(conn.id);
@@ -177,11 +171,7 @@ void main() {
await storage.saveHiddenLibraries({'jf-machine:movies'});
await storage.saveLibrarySort('jf-machine:movies', 'titleSort');
final removed = await pruneUnreferencedJellyfinConnections(
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
final removed = await cleanup.pruneUnreferencedJellyfinConnections();
expect(removed, 1);
expect(await connections.get(conn.id), isNull);
@@ -205,11 +195,7 @@ void main() {
await storage.setActiveProfileId('p2');
await storage.saveHiddenLibraries({'jf-machine:movies'});
final removed = await pruneUnreferencedJellyfinConnections(
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
final removed = await cleanup.pruneUnreferencedJellyfinConnections();
expect(removed, 1);
expect(await connections.get(orphan.id), isNull);
@@ -241,13 +227,7 @@ void main() {
expect(await connections.get(acct.id), isNotNull);
expect(await profileConnections.listAll(), hasLength(2));
final removal = await removePlexAccountConnectionAndCleanup(
account: acct,
profileConnections: profileConnections,
connections: connections,
storage: storage,
plannedRemoval: plannedRemoval,
);
final removal = await cleanup.removePlexAccountConnection(acct, plannedRemoval: plannedRemoval);
expect(removal.removedVirtualProfileIds, {vProfile});
expect(removal.borrowerProfileIds, isEmpty);
@@ -270,12 +250,7 @@ void main() {
await profileConnections.upsert(_row(vProfile, jf));
await profileConnections.upsert(_row('local-1', jf));
await removePlexAccountConnectionAndCleanup(
account: acct,
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
await cleanup.removePlexAccountConnection(acct);
expect(await connections.get(jf.id), isNotNull);
final remaining = await profileConnections.listAll();
@@ -293,12 +268,7 @@ void main() {
await profileConnections.upsert(_row('local-1', acct));
await profileConnections.upsert(_row('local-1', jf));
final removal = await removePlexAccountConnectionAndCleanup(
account: acct,
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
final removal = await cleanup.removePlexAccountConnection(acct);
expect(removal.removedVirtualProfileIds, isEmpty);
expect(removal.borrowerProfileIds, {'local-1'});
@@ -323,12 +293,7 @@ void main() {
await profileConnections.upsert(_row(v2, acct2, userIdentifier: uuid2));
await storage.savePlexHomeUsersCache(acct2.id, [_homeUser(uuid2).toJson()]);
final removal = await removePlexAccountConnectionAndCleanup(
account: acct1,
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
final removal = await cleanup.removePlexAccountConnection(acct1);
expect(removal.removedVirtualProfileIds, {v1});
expect(await connections.get(acct2.id), isNotNull);
@@ -348,12 +313,7 @@ void main() {
await profileConnections.upsert(_row(vProfile, acct, userIdentifier: uuid));
await profileConnections.upsert(_row(vProfile, jf));
Future<PlexAccountRemoval> run() => removePlexAccountConnectionAndCleanup(
account: acct,
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
Future<PlexAccountRemoval> run() => cleanup.removePlexAccountConnection(acct);
await run();
final second = await run();
@@ -375,13 +335,7 @@ void main() {
await storage.setActiveProfileId('p2');
await storage.saveHiddenLibraries({'plex-machine:movies'});
await removeProfileConnectionAndCleanup(
profileId: 'p1',
connection: conn,
profileConnections: profileConnections,
connections: connections,
storage: storage,
);
await cleanup.removeProfileConnection(profileId: 'p1', connection: conn);
expect(await connections.get(conn.id), isNotNull);
expect(await profileConnections.listForConnection(conn.id), isEmpty);
@@ -402,13 +356,7 @@ void main() {
Future<({PostRemovalRoute route, List<Profile> profiles})> resolve({
Map<String, List<PlexHomeUser>> plexHomeUsers = const {},
}) {
return resolvePostRemovalState(
profileRegistry: profileRegistry,
profileConnections: profileConnections,
connections: connections,
plexHomeUsers: plexHomeUsers,
storage: storage,
);
return cleanup.resolvePostRemovalState(profileRegistry: profileRegistry, plexHomeUsers: plexHomeUsers);
}
Profile local(String id) =>
@@ -21,6 +21,7 @@ import 'package:plezy/services/plex_api_cache.dart';
import 'package:plezy/utils/deletion_notifier.dart';
import 'package:plezy/utils/watch_state_notifier.dart';
import 'package:plezy/utils/active_client_scope.dart';
import '../test_helpers/download_fixtures.dart';
import '../test_helpers/media_items.dart';
/// Implements only [fetchPlayableDescendants], the surface [collectEpisodes]
+5 -4
View File
@@ -4,6 +4,7 @@ import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/providers/watch_state_store.dart';
import 'package:plezy/services/watch_state_resolver.dart';
import 'package:plezy/utils/watch_state_notifier.dart';
import '../test_helpers/media_items.dart';
@@ -190,13 +191,13 @@ void main() {
store.setHydratedPatches(const [
HydratedWatchStatePatch(
globalKey: 'jf-machine:show-1',
patch: WatchStatePatch(isWatched: true, hasViewOffsetMs: true, viewOffsetMs: 0),
patch: WatchStateSnapshot(isWatched: true, hasViewOffsetMs: true, viewOffsetMs: 0),
updatedAt: 100,
order: 1,
),
HydratedWatchStatePatch(
globalKey: 'jf-machine:episode-1',
patch: WatchStatePatch(isWatched: false, hasViewOffsetMs: true, viewOffsetMs: 0),
patch: WatchStateSnapshot(isWatched: false, hasViewOffsetMs: true, viewOffsetMs: 0),
updatedAt: 200,
order: 2,
),
@@ -213,13 +214,13 @@ void main() {
store.setHydratedPatches(const [
HydratedWatchStatePatch(
globalKey: 'jf-machine/user-a:show-1',
patch: WatchStatePatch(isWatched: true),
patch: WatchStateSnapshot(isWatched: true),
updatedAt: 100,
order: 1,
),
HydratedWatchStatePatch(
globalKey: 'jf-machine/user-b:show-1',
patch: WatchStatePatch(isWatched: false),
patch: WatchStateSnapshot(isWatched: false),
updatedAt: 100,
order: 2,
),
@@ -33,6 +33,7 @@ import 'package:plezy/utils/media_server_http_client.dart';
import 'package:plezy/utils/active_client_scope.dart';
import 'package:saf_util/saf_util_platform_interface.dart';
import '../test_helpers/download_fixtures.dart';
import '../test_helpers/io_fakes.dart';
import '../test_helpers/prefs.dart';
import '../test_helpers/media_items.dart';
@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:os_media_controls/os_media_controls.dart';
import 'package:plezy/screens/video_player/media_control_router.dart';
import 'package:plezy/services/media_control_router.dart';
void main() {
test('denied playback and media-item commands are consumed without mutation', () {
@@ -47,12 +47,12 @@ void main() {
});
}
VideoPlayerMediaControlRouter _router({
MediaControlRouter _router({
required bool Function() canControl,
required bool Function() canNavigate,
required List<String> calls,
}) {
return VideoPlayerMediaControlRouter(
return MediaControlRouter(
canControlPlayback: canControl,
canNavigateMediaItems: canNavigate,
onPlay: () => calls.add('play'),
@@ -22,6 +22,7 @@ import 'package:plezy/utils/active_client_scope.dart';
import 'package:plezy/utils/watch_state_notifier.dart';
import '../test_helpers/backend_client_fixtures.dart';
import '../test_helpers/download_fixtures.dart';
import '../test_helpers/playback_report_fakes.dart';
import '../test_helpers/prefs.dart';
import '../test_helpers/media_items.dart';
+2
View File
@@ -11,6 +11,8 @@ import 'package:plezy/main.dart';
import 'package:plezy/media/ids.dart';
import 'package:plezy/models/download_models.dart';
import 'test_helpers/download_fixtures.dart';
void main() {
testWidgets('renders a Flutter frame before starting the initialization gate', (tester) async {
final completion = Completer<int>();
+70
View File
@@ -0,0 +1,70 @@
import 'package:drift/drift.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/media/ids.dart';
/// Seeds `downloaded_media` rows at an arbitrary [status] so tests can start
/// from completed, downloading, or paused state.
///
/// Production never writes rows this way — it goes through
/// `insertQueuedDownload`, which only ever admits `queued` rows and guards on
/// the existing status. This fixture deliberately keeps neither restriction,
/// which is why it lives in `test/` instead of `lib/`.
extension DownloadFixtures on AppDatabase {
Future<void> insertDownload({
required ServerId serverId,
String? clientScopeId,
required String ratingKey,
required String globalKey,
required String type,
String? parentRatingKey,
String? grandparentRatingKey,
required int status,
int mediaIndex = 0,
String? mediaSourceId,
}) async {
await customUpdate(
'''
INSERT INTO downloaded_media (
server_id,
client_scope_id,
rating_key,
global_key,
type,
parent_rating_key,
grandparent_rating_key,
status,
media_index,
media_source_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(global_key) DO UPDATE SET
server_id = excluded.server_id,
client_scope_id = excluded.client_scope_id,
rating_key = excluded.rating_key,
type = excluded.type,
parent_rating_key = excluded.parent_rating_key,
grandparent_rating_key = excluded.grandparent_rating_key,
status = excluded.status,
progress = 0,
total_bytes = NULL,
downloaded_bytes = 0,
error_message = NULL,
retry_count = 0,
media_index = excluded.media_index,
media_source_id = excluded.media_source_id
''',
variables: [
Variable<String>(serverId),
Variable<String>(clientScopeId),
Variable<String>(ratingKey),
Variable<String>(globalKey),
Variable<String>(type),
Variable<String>(parentRatingKey),
Variable<String>(grandparentRatingKey),
Variable<int>(status),
Variable<int>(mediaIndex),
Variable<String>(mediaSourceId),
],
updates: {downloadedMedia},
);
}
}
@@ -0,0 +1,126 @@
import 'package:drift/drift.dart' hide isNull, isNotNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/database/download_operations.dart';
import 'package:plezy/media/ids.dart';
import 'package:plezy/models/download_models.dart';
import 'download_fixtures.dart';
void main() {
late AppDatabase db;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory());
});
tearDown(() async {
await db.close();
});
group('insertDownload', () {
test('inserts a movie row with defaults', () async {
await db.insertDownload(
serverId: ServerId('srv'),
ratingKey: '100',
globalKey: 'srv:100',
type: 'movie',
status: DownloadStatus.queued.index,
);
final rows = await db.select(db.downloadedMedia).get();
expect(rows, hasLength(1));
final r = rows.first;
expect(r.serverId, 'srv');
expect(r.ratingKey, '100');
expect(r.globalKey, 'srv:100');
expect(r.type, 'movie');
expect(r.status, DownloadStatus.queued.index);
expect(r.parentRatingKey, isNull);
expect(r.grandparentRatingKey, isNull);
expect(r.mediaIndex, 0);
});
test('inserts an episode with parent and grandparent keys', () async {
await db.insertDownload(
serverId: ServerId('srv'),
ratingKey: 'ep1',
globalKey: 'srv:ep1',
type: 'episode',
parentRatingKey: 'season1',
grandparentRatingKey: 'show1',
status: DownloadStatus.queued.index,
mediaIndex: 7,
);
final row = (await db.select(db.downloadedMedia).get()).single;
expect(row.parentRatingKey, 'season1');
expect(row.grandparentRatingKey, 'show1');
expect(row.mediaIndex, 7);
});
test('atomically updates metadata and attempt state while preserving the row and physical fields', () async {
await db.insertDownload(
serverId: ServerId('srv'),
clientScopeId: 'scope-old',
ratingKey: '100',
globalKey: 'srv:100',
type: 'movie',
status: DownloadStatus.queued.index,
mediaIndex: 1,
mediaSourceId: 'source-old',
);
final original = (await db.getDownloadedMedia('srv:100'))!;
await (db.update(db.downloadedMedia)..where((row) => row.globalKey.equals('srv:100'))).write(
const DownloadedMediaCompanion(
progress: Value(50),
downloadedBytes: Value(500),
totalBytes: Value(1000),
videoFilePath: Value('downloads/video.mkv'),
safRootUri: Value('content://downloads'),
thumbPath: Value('downloads/thumb.jpg'),
downloadedAt: Value(1234),
errorMessage: Value('old error'),
retryCount: Value(2),
bgTaskId: Value('current-task'),
),
);
await db.insertDownload(
serverId: ServerId('srv-new'),
clientScopeId: 'scope-new',
ratingKey: '100-new',
globalKey: 'srv:100',
type: 'episode',
parentRatingKey: 'season-new',
grandparentRatingKey: 'show-new',
status: DownloadStatus.failed.index,
mediaIndex: 3,
mediaSourceId: 'source-new',
);
final row = (await db.select(db.downloadedMedia).get()).single;
expect(row.id, original.id);
expect(row.serverId, 'srv-new');
expect(row.clientScopeId, 'scope-new');
expect(row.ratingKey, '100-new');
expect(row.type, 'episode');
expect(row.parentRatingKey, 'season-new');
expect(row.grandparentRatingKey, 'show-new');
expect(row.status, DownloadStatus.failed.index);
expect(row.mediaIndex, 3);
expect(row.mediaSourceId, 'source-new');
expect(row.progress, 0);
expect(row.downloadedBytes, 0);
expect(row.totalBytes, isNull);
expect(row.errorMessage, isNull);
expect(row.retryCount, 0);
expect(row.videoFilePath, 'downloads/video.mkv');
expect(row.safRootUri, 'content://downloads');
expect(row.thumbPath, 'downloads/thumb.jpg');
expect(row.downloadedAt, 1234);
expect(row.bgTaskId, 'current-task');
});
});
}
+2 -3
View File
@@ -10,6 +10,7 @@ import 'package:plezy/mpv/player/player_streams.dart';
import 'package:plezy/screens/settings/subtitle_styling_screen.dart';
import 'package:plezy/services/sleep_timer_service.dart';
import 'package:plezy/services/settings_service.dart';
import 'package:plezy/widgets/video_controls/models/track_controls_state.dart';
import 'package:plezy/widgets/video_controls/sheets/video_settings_sheet.dart';
import '../test_helpers/prefs.dart';
@@ -147,10 +148,8 @@ Future<void> _pumpSheet(
height: 700,
child: VideoSettingsSheet(
player: player ?? _FakeSettingsPlayer(),
audioSyncOffset: 0,
subtitleSyncOffset: 0,
canControl: canControl,
supportsHdrControl: supportsHdrControl,
trackControlsState: TrackControlsState(canControl: canControl),
),
),
),