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
+31
View File
@@ -1,6 +1,7 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
import '../utils/media_server_http_client.dart' show AbortController;
import 'media_kind.dart';
part 'library_query.freezed.dart';
@@ -84,3 +85,33 @@ int fallbackPageTotal({required int offset, required int itemCount, int? request
final fullPage = requestedSize != null && requestedSize > 0 && itemCount >= requestedSize;
return offset + itemCount + (fullPage ? 1 : 0);
}
/// Walk every page of a paginated endpoint and concatenate the results.
///
/// [fetchPage] receives a zero-based offset and [pageSize] and is called until
/// a page comes back empty, the accumulated count reaches the page's
/// [LibraryPage.totalCount], or — when [stopOnShortPage] is set — a page comes
/// back shorter than [pageSize]. The short-page break is for backends whose
/// total is unreliable; leave it off when the total is authoritative.
///
/// [abort] is checked before and after every request. Errors propagate.
Future<List<T>> drainPages<T>(
Future<LibraryPage<T>> Function(int start, int size) fetchPage, {
required int pageSize,
AbortController? abort,
bool stopOnShortPage = false,
}) async {
final all = <T>[];
var start = 0;
while (true) {
abort?.throwIfAborted();
final page = await fetchPage(start, pageSize);
abort?.throwIfAborted();
if (page.items.isEmpty) break;
all.addAll(page.items);
start += page.items.length;
if (start >= page.totalCount) break;
if (stopOnShortPage && page.items.length < pageSize) break;
}
return all;
}
+27 -47
View File
@@ -212,55 +212,35 @@ class ServerCapabilities {
audioTranscoding: true,
);
ServerCapabilities copyWith({
bool? serverSidePlayQueue,
bool? serverSidePlaylists,
bool? liveTv,
bool? liveTvDvr,
bool? subtitleSearch,
bool? videoTranscoding,
bool? serverSideSync,
bool? richHubs,
bool? numericUserRating,
bool? userFavorites,
bool? continueWatchingRemoval,
bool? externalSubtitleSearch,
bool? trackPreferencePersistence,
bool? endpointFailover,
bool? offlineWatchQueue,
bool? discordRpc,
bool? richMetadataEdit,
AlphaBarMode? alphaBar,
bool? scrubThumbnails,
bool? folderGrouping,
bool? lyrics,
bool? instantMix,
bool? audioTranscoding,
}) {
/// Every flag here is fixed per backend *kind* except [videoTranscoding],
/// which Plex probes per server (`PlexClient.capabilities`) — so that is the
/// only override this type needs. Widen the parameter list if another flag
/// ever becomes a runtime probe.
ServerCapabilities copyWith({bool? videoTranscoding}) {
return ServerCapabilities(
serverSidePlayQueue: serverSidePlayQueue ?? this.serverSidePlayQueue,
serverSidePlaylists: serverSidePlaylists ?? this.serverSidePlaylists,
liveTv: liveTv ?? this.liveTv,
liveTvDvr: liveTvDvr ?? this.liveTvDvr,
subtitleSearch: subtitleSearch ?? this.subtitleSearch,
serverSidePlayQueue: serverSidePlayQueue,
serverSidePlaylists: serverSidePlaylists,
liveTv: liveTv,
liveTvDvr: liveTvDvr,
subtitleSearch: subtitleSearch,
videoTranscoding: videoTranscoding ?? this.videoTranscoding,
serverSideSync: serverSideSync ?? this.serverSideSync,
richHubs: richHubs ?? this.richHubs,
numericUserRating: numericUserRating ?? this.numericUserRating,
userFavorites: userFavorites ?? this.userFavorites,
continueWatchingRemoval: continueWatchingRemoval ?? this.continueWatchingRemoval,
externalSubtitleSearch: externalSubtitleSearch ?? this.externalSubtitleSearch,
trackPreferencePersistence: trackPreferencePersistence ?? this.trackPreferencePersistence,
endpointFailover: endpointFailover ?? this.endpointFailover,
offlineWatchQueue: offlineWatchQueue ?? this.offlineWatchQueue,
discordRpc: discordRpc ?? this.discordRpc,
richMetadataEdit: richMetadataEdit ?? this.richMetadataEdit,
alphaBar: alphaBar ?? this.alphaBar,
scrubThumbnails: scrubThumbnails ?? this.scrubThumbnails,
folderGrouping: folderGrouping ?? this.folderGrouping,
lyrics: lyrics ?? this.lyrics,
instantMix: instantMix ?? this.instantMix,
audioTranscoding: audioTranscoding ?? this.audioTranscoding,
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,
);
}
}