Merge the deduplication and dead-code removal pass
Consolidates duplicated logic behind shared implementations — paginated grid tabs, focus chrome, cached remote stores, sheet selection columns, the server artifact store and a test fixture layer — and removes code that had become unreachable. Net reduction of about 5,500 lines with no behaviour change. Where a fix had landed separately in code that moved into a shared helper, the fix was re-applied inside the helper rather than left behind in the copy that went away.
This commit is contained in:
@@ -171,6 +171,22 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The results list both screens render: padded, without keep-alives or
|
||||
/// semantic indexes, one child per entry of [searchResults].
|
||||
Widget buildResultsSliver(NullableIndexedWidgetBuilder itemBuilder) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
childCount: searchResults.length,
|
||||
addAutomaticKeepAlives: false,
|
||||
addSemanticIndexes: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// OSK "Search" / hardware Enter on TV: jump to results, or force the
|
||||
/// pending search to run now.
|
||||
void handleSearchSubmit() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user