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
+19
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import '../utils/deletion_notifier.dart';
import 'event_aware.dart';
import 'watch_state_aware.dart';
/// Mixin for screens that need to react to deletion events.
///
@@ -74,3 +75,21 @@ mixin DeletionAware<T extends StatefulWidget> on State<T> {
super.dispose();
}
}
/// Points [DeletionAware]'s filters at the [WatchStateAware] ones.
///
/// The usual case: a screen shows the same rows for both event families, so a
/// deleted show and a watched show affect exactly the same items. Mix this in
/// after both aware mixins instead of re-typing the three getters. A screen
/// that genuinely needs a different scope overrides the getter it cares about
/// (or skips this mixin entirely).
mixin DeletionMirrorsWatchState<T extends StatefulWidget> on WatchStateAware<T>, DeletionAware<T> {
@override
String? get deletionServerId => watchStateServerId;
@override
Set<String>? get deletionGlobalKeys => watchedGlobalKeys;
@override
Set<String>? get deletionIds => watchedIds;
}
-37
View File
@@ -104,43 +104,6 @@ mixin PaginatedItemLoader<T, W extends StatefulWidget> on State<W> {
return (page: result, applied: true);
}
/// Shared initial-load transaction for paginated consumers.
///
/// Owns reset, stale-result rejection, mounted checks, and error-state
/// application. Callers supply only their view fields, logging, and
/// post-success behavior.
Future<bool> loadInitialPaginatedItems({
required int pageSize,
required VoidCallback resetViewState,
required void Function(List<T> items) applyLoadedItems,
required void Function(Object error, StackTrace stackTrace) applyError,
void Function(int loadedCount, int totalCount)? onLoaded,
void Function(Object error, StackTrace stackTrace)? onError,
}) async {
setState(() {
resetViewState();
resetPaginationState();
});
try {
final initialPage = await loadInitialPageWithStatus(pageSize);
if (!initialPage.applied || !mounted) return false;
setState(() {
applyLoadedItems(loadedItems.values.toList());
});
onLoaded?.call(loadedItems.length, totalSize);
return true;
} catch (error, stackTrace) {
onError?.call(error, stackTrace);
if (!mounted) return false;
setState(() {
applyError(error, stackTrace);
});
return false;
}
}
/// Fetch any unloaded items inside [firstIndex, firstIndex + visibleCount)
/// with [buffer] extra indices on each side. Serialized — only one
/// range-fetch runs at a time — and re-checks after each success so a
+74
View File
@@ -0,0 +1,74 @@
import 'package:flutter/widgets.dart';
import '../media/media_item.dart';
import 'item_updatable.dart';
import 'paginated_item_loader.dart';
/// Standard view-state wiring for screens whose body is a single paginated
/// list.
///
/// [PaginatedItemLoader] owns the sparse `loadedItems` map; the hosts
/// (`BaseMediaListDetailScreen`, `BaseLibraryTabState`) additionally expose
/// `items` / `isLoading` / `errorMessage` to drive the loading, empty and
/// error chrome. This mixin owns the transitions between the two, so a
/// screen's `loadItems` supplies only the page size, the error text, and an
/// optional post-load hook.
mixin StandardPaginatedView<T, W extends StatefulWidget> on PaginatedItemLoader<T, W> {
set items(List<T> value);
set isLoading(bool value);
set errorMessage(String? value);
/// Initial-load transaction: clears the view state, fetches the first page,
/// then publishes either the loaded items or [errorMessageFor]'s text.
///
/// Stale results — a newer load started, or the screen was disposed — are
/// dropped without touching state. [errorMessageFor] runs even when
/// unmounted, so screens can log from it; [onLoaded] runs only after a
/// successful publish.
Future<void> loadStandardPaginatedItems({
required int pageSize,
required String Function(Object error, StackTrace stackTrace) errorMessageFor,
void Function(int loadedCount, int totalCount)? onLoaded,
}) async {
setState(() {
isLoading = true;
errorMessage = null;
items = [];
resetPaginationState();
});
try {
final initialPage = await loadInitialPageWithStatus(pageSize);
if (!initialPage.applied || !mounted) return;
setState(() {
items = loadedItems.values.toList();
isLoading = false;
});
onLoaded?.call(loadedItems.length, totalSize);
} catch (error, stackTrace) {
final message = errorMessageFor(error, stackTrace);
if (!mounted) return;
setState(() {
errorMessage = message;
isLoading = false;
});
}
}
}
/// [ItemUpdatable.updateItemInLists] for screens whose visible list is the
/// sparse `loadedItems` map rather than a flat `items` list — searching the
/// map is what keeps an item refreshed at a scrolled-in position, past the
/// first page.
mixin PaginatedItemUpdatable<W extends StatefulWidget> on PaginatedItemLoader<MediaItem, W>, ItemUpdatable<W> {
@override
void updateItemInLists(String sourceGlobalKey, MediaItem updatedItem) {
for (final entry in loadedItems.entries) {
if (entry.value.globalKey == sourceGlobalKey) {
loadedItems[entry.key] = updatedItem;
return;
}
}
}
}