refactor: fold single-use helpers into their call sites
Collapses indirection layers and one-caller abstractions across the video player, shortcut dispatch, shader loading and context-menu code, including the VideoPIPManager pass-through over PipService.
This commit is contained in:
@@ -25,6 +25,7 @@ typedef LibraryAggregationResult = ({
|
||||
Set<String> succeededServerIds,
|
||||
Set<String> cancelledServerIds,
|
||||
});
|
||||
typedef _FanOutResult<T> = ({List<T> items, Set<String> succeededServerIds, Set<String> cancelledServerIds});
|
||||
|
||||
/// Whether [error] is a client-side abort (client teardown mid-request)
|
||||
/// rather than a genuine server failure. Aggregation reports these servers
|
||||
@@ -54,6 +55,37 @@ class DataAggregationService {
|
||||
};
|
||||
}
|
||||
|
||||
/// Run [fetch] against every client in [clients] and concatenate the results
|
||||
/// in client order. A per-server failure is swallowed — logged with
|
||||
/// [failureMessage] and contributing nothing — so one bad server cannot sink
|
||||
/// the pass; that server is simply absent from `succeededServerIds`, and also
|
||||
/// lands in `cancelledServerIds` when the failure was a client-side abort.
|
||||
Future<_FanOutResult<T>> _fanOut<T>(
|
||||
Map<String, MediaServerClient> clients, {
|
||||
required String Function(String serverId) failureMessage,
|
||||
required Future<List<T>> Function(String serverId, MediaServerClient client) fetch,
|
||||
}) async {
|
||||
final cancelledServerIds = <String>{};
|
||||
final futures = clients.entries.map((entry) async {
|
||||
try {
|
||||
return (serverId: entry.key, items: await fetch(entry.key, entry.value));
|
||||
} catch (e, stackTrace) {
|
||||
if (_isCancellation(e)) cancelledServerIds.add(entry.key);
|
||||
appLogger.e(failureMessage(entry.key), error: e, stackTrace: stackTrace);
|
||||
return (serverId: null, items: <T>[]);
|
||||
}
|
||||
});
|
||||
final results = await Future.wait(futures);
|
||||
return (
|
||||
items: [for (final result in results) ...result.items],
|
||||
succeededServerIds: {
|
||||
for (final result in results)
|
||||
if (result.serverId != null) result.serverId!,
|
||||
},
|
||||
cancelledServerIds: cancelledServerIds,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch libraries from all online clients regardless of backend, returning
|
||||
/// the merged neutral [MediaLibrary]s alongside the ids of the servers whose
|
||||
/// fetch actually succeeded. [serverIds] restricts the fan-out to those
|
||||
@@ -77,24 +109,15 @@ class DataAggregationService {
|
||||
cancelledServerIds: const <String>{},
|
||||
);
|
||||
}
|
||||
final succeededServerIds = <String>{};
|
||||
final cancelledServerIds = <String>{};
|
||||
final futures = clients.entries.map((entry) async {
|
||||
try {
|
||||
final libraries = await entry.value.fetchLibraries();
|
||||
succeededServerIds.add(entry.key);
|
||||
return libraries;
|
||||
} catch (e, stackTrace) {
|
||||
if (_isCancellation(e)) cancelledServerIds.add(entry.key);
|
||||
appLogger.e('Failed neutral library fetch from ${entry.key}', error: e, stackTrace: stackTrace);
|
||||
return <MediaLibrary>[];
|
||||
}
|
||||
});
|
||||
final results = await Future.wait(futures);
|
||||
final fetched = await _fanOut<MediaLibrary>(
|
||||
clients,
|
||||
failureMessage: (serverId) => 'Failed neutral library fetch from $serverId',
|
||||
fetch: (_, client) => client.fetchLibraries(),
|
||||
);
|
||||
return (
|
||||
libraries: [for (final list in results) ...list],
|
||||
succeededServerIds: succeededServerIds,
|
||||
cancelledServerIds: cancelledServerIds,
|
||||
libraries: fetched.items,
|
||||
succeededServerIds: fetched.succeededServerIds,
|
||||
cancelledServerIds: fetched.cancelledServerIds,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,24 +136,12 @@ class DataAggregationService {
|
||||
return (items: const <MediaItem>[], succeededServerIds: const <String>{}, cancelledServerIds: const <String>{});
|
||||
}
|
||||
|
||||
final cancelledServerIds = <String>{};
|
||||
final futures = clients.entries.map((entry) async {
|
||||
final client = entry.value;
|
||||
try {
|
||||
final items = await client.fetchContinueWatching(count: limit);
|
||||
return (serverId: entry.key, items: items);
|
||||
} catch (e, st) {
|
||||
if (_isCancellation(e)) cancelledServerIds.add(entry.key);
|
||||
appLogger.e('Failed on-deck fetch from ${entry.key}', error: e, stackTrace: st);
|
||||
return (serverId: null, items: <MediaItem>[]);
|
||||
}
|
||||
});
|
||||
final results = await Future.wait(futures);
|
||||
final succeededServerIds = {
|
||||
for (final result in results)
|
||||
if (result.serverId != null) result.serverId!,
|
||||
};
|
||||
final allOnDeck = results.expand((result) => result.items).toList();
|
||||
final fetched = await _fanOut<MediaItem>(
|
||||
clients,
|
||||
failureMessage: (serverId) => 'Failed on-deck fetch from $serverId',
|
||||
fetch: (_, client) => client.fetchContinueWatching(count: limit),
|
||||
);
|
||||
final allOnDeck = fetched.items;
|
||||
|
||||
// Filter out items from hidden libraries
|
||||
List<MediaItem> filteredOnDeck = allOnDeck;
|
||||
@@ -154,7 +165,11 @@ class DataAggregationService {
|
||||
|
||||
appLogger.i('Fetched ${items.length} on deck items from all servers');
|
||||
|
||||
return (items: items, succeededServerIds: succeededServerIds, cancelledServerIds: cancelledServerIds);
|
||||
return (
|
||||
items: items,
|
||||
succeededServerIds: fetched.succeededServerIds,
|
||||
cancelledServerIds: fetched.cancelledServerIds,
|
||||
);
|
||||
}
|
||||
|
||||
/// Merge an [existing] Continue Watching list with [fresh] rows from
|
||||
@@ -382,11 +397,10 @@ class DataAggregationService {
|
||||
? _groupLibrariesByServer((await getMediaLibrariesFromAllServers(serverIds: serverIds)).libraries)
|
||||
: null;
|
||||
|
||||
final cancelledServerIds = <String>{};
|
||||
final futures = clients.entries.map((entry) async {
|
||||
final serverId = entry.key;
|
||||
final client = entry.value;
|
||||
try {
|
||||
final fetched = await _fanOut<MediaHub>(
|
||||
clients,
|
||||
failureMessage: (serverId) => 'Failed to fetch hubs from server $serverId',
|
||||
fetch: (serverId, client) async {
|
||||
final serverLibraries = libraries?[serverId];
|
||||
final shouldUseGlobalHubs = useGlobalHubs && client.capabilities.richHubs;
|
||||
final hubItemLimit = limit ?? defaultHubPreviewLimit;
|
||||
@@ -413,28 +427,13 @@ class DataAggregationService {
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
libraries: useGlobalHubs ? serverLibraries : null,
|
||||
);
|
||||
return (
|
||||
serverId: serverId,
|
||||
hubs: _postProcessHubs(hubs, serverId: ServerId(serverId), hiddenLibraryKeys: hiddenLibraryKeys),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
if (_isCancellation(e)) cancelledServerIds.add(serverId);
|
||||
appLogger.e('Failed to fetch hubs from server $serverId', error: e, stackTrace: stackTrace);
|
||||
return (serverId: null, hubs: <MediaHub>[]);
|
||||
}
|
||||
});
|
||||
return _postProcessHubs(hubs, serverId: ServerId(serverId), hiddenLibraryKeys: hiddenLibraryKeys);
|
||||
},
|
||||
);
|
||||
|
||||
final results = await Future.wait(futures);
|
||||
final succeededServerIds = {
|
||||
for (final result in results)
|
||||
if (result.serverId != null) result.serverId!,
|
||||
};
|
||||
final all = <MediaHub>[];
|
||||
for (final result in results) {
|
||||
all.addAll(result.hubs);
|
||||
}
|
||||
final all = fetched.items;
|
||||
final hubs = limit != null && limit < all.length ? all.sublist(0, limit) : all;
|
||||
return (hubs: hubs, succeededServerIds: succeededServerIds, cancelledServerIds: cancelledServerIds);
|
||||
return (hubs: hubs, succeededServerIds: fetched.succeededServerIds, cancelledServerIds: fetched.cancelledServerIds);
|
||||
}
|
||||
|
||||
/// Per-library hub fetch for a single client. Filters to visible libraries
|
||||
@@ -519,18 +518,12 @@ class DataAggregationService {
|
||||
final resultLimit = limit ?? defaultMediaSearchLimit;
|
||||
final fetchLimit = resultLimit < defaultMediaSearchLimit ? defaultMediaSearchLimit : resultLimit;
|
||||
|
||||
final futures = clients.entries.map((entry) async {
|
||||
final client = entry.value;
|
||||
try {
|
||||
return await client.searchItems(query, limit: fetchLimit);
|
||||
} catch (e, st) {
|
||||
appLogger.e('Search failed on ${entry.key}', error: e, stackTrace: st);
|
||||
return <MediaItem>[];
|
||||
}
|
||||
});
|
||||
|
||||
final allResults = (await Future.wait(futures)).expand((l) => l).toList();
|
||||
final result = rankMediaSearchResults(allResults, query, limit: resultLimit);
|
||||
final fetched = await _fanOut<MediaItem>(
|
||||
clients,
|
||||
failureMessage: (serverId) => 'Search failed on $serverId',
|
||||
fetch: (_, client) => client.searchItems(query, limit: fetchLimit),
|
||||
);
|
||||
final result = rankMediaSearchResults(fetched.items, query, limit: resultLimit);
|
||||
|
||||
appLogger.i('Found ${result.length} search results across all servers');
|
||||
|
||||
|
||||
@@ -202,8 +202,6 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
VoidCallback? onVolumeUp,
|
||||
VoidCallback? onVolumeDown,
|
||||
VoidCallback? onToggleMute,
|
||||
int? currentPositionEpoch,
|
||||
ValueChanged<int>? onLiveSeek,
|
||||
ValueChanged<int>? onLiveSeekBy,
|
||||
Future<void> Function(Duration position)? onSeekRequested,
|
||||
}) {
|
||||
@@ -277,32 +275,78 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
_executeAction(
|
||||
action,
|
||||
player,
|
||||
onToggleFullscreen,
|
||||
onToggleSubtitles,
|
||||
onNextAudioTrack,
|
||||
onNextSubtitleTrack,
|
||||
onNextChapter,
|
||||
onPreviousChapter,
|
||||
onPlayPause: onPlayPause,
|
||||
onToggleShader: onToggleShader,
|
||||
onSkipMarker: onSkipMarker,
|
||||
onNextEpisode: onNextEpisode,
|
||||
onPreviousEpisode: onPreviousEpisode,
|
||||
onScreenshot: onScreenshot,
|
||||
onZoomIn: onZoomIn,
|
||||
onZoomOut: onZoomOut,
|
||||
onZoomReset: onZoomReset,
|
||||
onVolumeUp: onVolumeUp,
|
||||
onVolumeDown: onVolumeDown,
|
||||
onToggleMute: onToggleMute,
|
||||
currentPositionEpoch: currentPositionEpoch,
|
||||
onLiveSeek: onLiveSeek,
|
||||
onLiveSeekBy: onLiveSeekBy,
|
||||
onSeekRequested: onSeekRequested,
|
||||
);
|
||||
void performSeek(int offsetSeconds) {
|
||||
// Relative live-TV skip: route through the parent accumulator, which
|
||||
// coalesces a rapid burst into one transcode re-open (#1253).
|
||||
if (onLiveSeekBy != null) {
|
||||
onLiveSeekBy(offsetSeconds);
|
||||
} else {
|
||||
final target = clampSeekPosition(player, player.state.position + Duration(seconds: offsetSeconds));
|
||||
unawaited((onSeekRequested ?? player.seek)(target));
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case ShortcutAction.playPause:
|
||||
(onPlayPause ?? player.playOrPause).call();
|
||||
case ShortcutAction.volumeUp:
|
||||
onVolumeUp?.call();
|
||||
case ShortcutAction.volumeDown:
|
||||
onVolumeDown?.call();
|
||||
case ShortcutAction.seekForward:
|
||||
performSeek(_seekTimeSmall);
|
||||
case ShortcutAction.seekBackward:
|
||||
performSeek(-_seekTimeSmall);
|
||||
case ShortcutAction.seekForwardLarge:
|
||||
performSeek(_seekTimeLarge);
|
||||
case ShortcutAction.seekBackwardLarge:
|
||||
performSeek(-_seekTimeLarge);
|
||||
case ShortcutAction.fullscreenToggle:
|
||||
onToggleFullscreen?.call();
|
||||
case ShortcutAction.muteToggle:
|
||||
onToggleMute?.call();
|
||||
case ShortcutAction.subtitleToggle:
|
||||
onToggleSubtitles?.call();
|
||||
case ShortcutAction.audioTrackNext:
|
||||
onNextAudioTrack?.call();
|
||||
case ShortcutAction.subtitleTrackNext:
|
||||
onNextSubtitleTrack?.call();
|
||||
case ShortcutAction.chapterNext:
|
||||
onNextChapter?.call();
|
||||
case ShortcutAction.chapterPrevious:
|
||||
onPreviousChapter?.call();
|
||||
case ShortcutAction.episodeNext:
|
||||
onNextEpisode?.call();
|
||||
case ShortcutAction.episodePrevious:
|
||||
onPreviousEpisode?.call();
|
||||
case ShortcutAction.speedIncrease:
|
||||
final newRateUp = (player.state.rate + 0.25).clamp(0.25, 3.0);
|
||||
player.setRate(newRateUp);
|
||||
_settingsService.write(SettingsService.defaultPlaybackSpeed, newRateUp);
|
||||
case ShortcutAction.speedDecrease:
|
||||
final newRateDown = (player.state.rate - 0.25).clamp(0.25, 3.0);
|
||||
player.setRate(newRateDown);
|
||||
_settingsService.write(SettingsService.defaultPlaybackSpeed, newRateDown);
|
||||
case ShortcutAction.speedReset:
|
||||
player.setRate(1.0);
|
||||
_settingsService.write(SettingsService.defaultPlaybackSpeed, 1.0);
|
||||
case ShortcutAction.subSeekNext:
|
||||
player.command(['sub-seek', '1']);
|
||||
case ShortcutAction.subSeekPrev:
|
||||
player.command(['sub-seek', '-1']);
|
||||
case ShortcutAction.shaderToggle:
|
||||
onToggleShader?.call();
|
||||
case ShortcutAction.skipMarker:
|
||||
onSkipMarker?.call();
|
||||
case ShortcutAction.screenshot:
|
||||
unawaited(player.command(['screenshot', 'subtitles']).then((_) => onScreenshot?.call()));
|
||||
case ShortcutAction.zoomIn:
|
||||
onZoomIn?.call();
|
||||
case ShortcutAction.zoomOut:
|
||||
onZoomOut?.call();
|
||||
case ShortcutAction.zoomReset:
|
||||
onZoomReset?.call();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
@@ -310,106 +354,6 @@ class KeyboardShortcutsService extends ChangeNotifier {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
void _executeAction(
|
||||
ShortcutAction action,
|
||||
Player player,
|
||||
VoidCallback? onToggleFullscreen,
|
||||
VoidCallback? onToggleSubtitles,
|
||||
VoidCallback? onNextAudioTrack,
|
||||
VoidCallback? onNextSubtitleTrack,
|
||||
VoidCallback? onNextChapter,
|
||||
VoidCallback? onPreviousChapter, {
|
||||
VoidCallback? onPlayPause,
|
||||
VoidCallback? onToggleShader,
|
||||
VoidCallback? onSkipMarker,
|
||||
VoidCallback? onNextEpisode,
|
||||
VoidCallback? onPreviousEpisode,
|
||||
VoidCallback? onScreenshot,
|
||||
VoidCallback? onZoomIn,
|
||||
VoidCallback? onZoomOut,
|
||||
VoidCallback? onZoomReset,
|
||||
VoidCallback? onVolumeUp,
|
||||
VoidCallback? onVolumeDown,
|
||||
VoidCallback? onToggleMute,
|
||||
int? currentPositionEpoch,
|
||||
ValueChanged<int>? onLiveSeek,
|
||||
ValueChanged<int>? onLiveSeekBy,
|
||||
Future<void> Function(Duration position)? onSeekRequested,
|
||||
}) {
|
||||
void performSeek(int offsetSeconds) {
|
||||
// Relative live-TV skip: route through the parent accumulator, which
|
||||
// coalesces a rapid burst into one transcode re-open (#1253).
|
||||
if (onLiveSeekBy != null) {
|
||||
onLiveSeekBy(offsetSeconds);
|
||||
} else {
|
||||
final target = clampSeekPosition(player, player.state.position + Duration(seconds: offsetSeconds));
|
||||
unawaited((onSeekRequested ?? player.seek)(target));
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case ShortcutAction.playPause:
|
||||
(onPlayPause ?? player.playOrPause).call();
|
||||
case ShortcutAction.volumeUp:
|
||||
onVolumeUp?.call();
|
||||
case ShortcutAction.volumeDown:
|
||||
onVolumeDown?.call();
|
||||
case ShortcutAction.seekForward:
|
||||
performSeek(_seekTimeSmall);
|
||||
case ShortcutAction.seekBackward:
|
||||
performSeek(-_seekTimeSmall);
|
||||
case ShortcutAction.seekForwardLarge:
|
||||
performSeek(_seekTimeLarge);
|
||||
case ShortcutAction.seekBackwardLarge:
|
||||
performSeek(-_seekTimeLarge);
|
||||
case ShortcutAction.fullscreenToggle:
|
||||
onToggleFullscreen?.call();
|
||||
case ShortcutAction.muteToggle:
|
||||
onToggleMute?.call();
|
||||
case ShortcutAction.subtitleToggle:
|
||||
onToggleSubtitles?.call();
|
||||
case ShortcutAction.audioTrackNext:
|
||||
onNextAudioTrack?.call();
|
||||
case ShortcutAction.subtitleTrackNext:
|
||||
onNextSubtitleTrack?.call();
|
||||
case ShortcutAction.chapterNext:
|
||||
onNextChapter?.call();
|
||||
case ShortcutAction.chapterPrevious:
|
||||
onPreviousChapter?.call();
|
||||
case ShortcutAction.episodeNext:
|
||||
onNextEpisode?.call();
|
||||
case ShortcutAction.episodePrevious:
|
||||
onPreviousEpisode?.call();
|
||||
case ShortcutAction.speedIncrease:
|
||||
final newRateUp = (player.state.rate + 0.25).clamp(0.25, 3.0);
|
||||
player.setRate(newRateUp);
|
||||
_settingsService.write(SettingsService.defaultPlaybackSpeed, newRateUp);
|
||||
case ShortcutAction.speedDecrease:
|
||||
final newRateDown = (player.state.rate - 0.25).clamp(0.25, 3.0);
|
||||
player.setRate(newRateDown);
|
||||
_settingsService.write(SettingsService.defaultPlaybackSpeed, newRateDown);
|
||||
case ShortcutAction.speedReset:
|
||||
player.setRate(1.0);
|
||||
_settingsService.write(SettingsService.defaultPlaybackSpeed, 1.0);
|
||||
case ShortcutAction.subSeekNext:
|
||||
player.command(['sub-seek', '1']);
|
||||
case ShortcutAction.subSeekPrev:
|
||||
player.command(['sub-seek', '-1']);
|
||||
case ShortcutAction.shaderToggle:
|
||||
onToggleShader?.call();
|
||||
case ShortcutAction.skipMarker:
|
||||
onSkipMarker?.call();
|
||||
case ShortcutAction.screenshot:
|
||||
unawaited(player.command(['screenshot', 'subtitles']).then((_) => onScreenshot?.call()));
|
||||
case ShortcutAction.zoomIn:
|
||||
onZoomIn?.call();
|
||||
case ShortcutAction.zoomOut:
|
||||
onZoomOut?.call();
|
||||
case ShortcutAction.zoomReset:
|
||||
onZoomReset?.call();
|
||||
}
|
||||
}
|
||||
|
||||
String getActionDisplayName(String action) {
|
||||
final shortcut = ShortcutAction.fromId(action);
|
||||
if (shortcut == null) return action;
|
||||
|
||||
@@ -182,85 +182,32 @@ class ShaderAssetLoader {
|
||||
/// Get the shader file paths for an Anime4K preset.
|
||||
/// Returns a list of shader paths in the correct order for MPV.
|
||||
static Future<List<String>> getAnime4KShaders(Anime4KConfig config) async {
|
||||
final (restoreVariant, upscaleVariant) = switch (config.quality) {
|
||||
Anime4KQuality.fast => ('restore_m', 'upscale_m'),
|
||||
Anime4KQuality.hq => ('restore_vl', 'upscale_vl'),
|
||||
};
|
||||
|
||||
// All modes start with Clamp, then apply their own ordered chain.
|
||||
final chain = <String>[
|
||||
'clamp',
|
||||
...switch (config.mode) {
|
||||
Anime4KMode.modeA => [restoreVariant],
|
||||
Anime4KMode.modeB => [restoreVariant, upscaleVariant, 'downscale'],
|
||||
Anime4KMode.modeC => [upscaleVariant, 'downscale'],
|
||||
Anime4KMode.modeAA => [restoreVariant, restoreVariant],
|
||||
Anime4KMode.modeBB => [restoreVariant, restoreVariant, upscaleVariant, 'downscale'],
|
||||
Anime4KMode.modeCA => [upscaleVariant, restoreVariant, 'downscale'],
|
||||
},
|
||||
];
|
||||
|
||||
final shaders = <String>[];
|
||||
final quality = config.quality;
|
||||
final mode = config.mode;
|
||||
|
||||
String restoreVariant;
|
||||
String upscaleVariant;
|
||||
|
||||
switch (quality) {
|
||||
case Anime4KQuality.fast:
|
||||
restoreVariant = 'restore_m';
|
||||
upscaleVariant = 'upscale_m';
|
||||
break;
|
||||
case Anime4KQuality.hq:
|
||||
restoreVariant = 'restore_vl';
|
||||
upscaleVariant = 'upscale_vl';
|
||||
break;
|
||||
}
|
||||
|
||||
// Build shader chain based on mode
|
||||
// All modes start with Clamp
|
||||
final clampPath = await _extractShader(_anime4kShaders['clamp']!);
|
||||
if (clampPath != null) shaders.add(clampPath);
|
||||
|
||||
switch (mode) {
|
||||
case Anime4KMode.modeA:
|
||||
// A: Clamp + Restore
|
||||
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
||||
if (restorePath != null) shaders.add(restorePath);
|
||||
break;
|
||||
|
||||
case Anime4KMode.modeB:
|
||||
// B: Clamp + Restore + Upscale + Downscale
|
||||
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
||||
if (restorePath != null) shaders.add(restorePath);
|
||||
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
||||
if (upscalePath != null) shaders.add(upscalePath);
|
||||
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
||||
if (downscalePath != null) shaders.add(downscalePath);
|
||||
break;
|
||||
|
||||
case Anime4KMode.modeC:
|
||||
// C: Clamp + Upscale + Downscale
|
||||
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
||||
if (upscalePath != null) shaders.add(upscalePath);
|
||||
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
||||
if (downscalePath != null) shaders.add(downscalePath);
|
||||
break;
|
||||
|
||||
case Anime4KMode.modeAA:
|
||||
// A+A: Clamp + Restore + Restore
|
||||
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
||||
if (restorePath != null) {
|
||||
shaders.add(restorePath);
|
||||
shaders.add(restorePath); // Second restore pass
|
||||
}
|
||||
break;
|
||||
|
||||
case Anime4KMode.modeBB:
|
||||
// B+B: Clamp + Restore + Restore + Upscale + Downscale
|
||||
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
||||
if (restorePath != null) {
|
||||
shaders.add(restorePath);
|
||||
shaders.add(restorePath); // Second restore pass
|
||||
}
|
||||
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
||||
if (upscalePath != null) shaders.add(upscalePath);
|
||||
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
||||
if (downscalePath != null) shaders.add(downscalePath);
|
||||
break;
|
||||
|
||||
case Anime4KMode.modeCA:
|
||||
// C+A: Clamp + Upscale + Restore + Downscale
|
||||
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
|
||||
if (upscalePath != null) shaders.add(upscalePath);
|
||||
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
|
||||
if (restorePath != null) shaders.add(restorePath);
|
||||
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
|
||||
if (downscalePath != null) shaders.add(downscalePath);
|
||||
break;
|
||||
final extracted = <String, String?>{};
|
||||
for (final key in chain) {
|
||||
if (!extracted.containsKey(key)) {
|
||||
extracted[key] = await _extractShader(_anime4kShaders[key]!);
|
||||
}
|
||||
final shaderPath = extracted[key];
|
||||
if (shaderPath != null) shaders.add(shaderPath);
|
||||
}
|
||||
|
||||
return shaders;
|
||||
|
||||
@@ -9,8 +9,9 @@ import 'shader_service.dart';
|
||||
/// One row per action carries everything about it except the behaviour: the
|
||||
/// persisted [id], the [defaultHotKey] shipped with the app, the localized
|
||||
/// [label], and the capability flags that gate dispatch. Adding a shortcut is
|
||||
/// one entry here plus a case in `KeyboardShortcutsService._executeAction`,
|
||||
/// which the analyzer demands because that switch is exhaustive over this enum.
|
||||
/// one entry here plus a case in
|
||||
/// `KeyboardShortcutsService.handleVideoPlayerKeyEvent`, which the analyzer
|
||||
/// demands because that switch is exhaustive over this enum.
|
||||
///
|
||||
/// Declaration order is the order shortcuts are listed in settings, and [id] is
|
||||
/// persisted in preferences — do not reorder or rename existing entries.
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../mpv/mpv.dart';
|
||||
import '../services/pip_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
class VideoPIPManager {
|
||||
final Player player;
|
||||
|
||||
/// Current viewport size, used as the PiP aspect ratio fallback.
|
||||
final Size? Function() playerSize;
|
||||
|
||||
VideoPIPManager({required this.player, required this.playerSize});
|
||||
|
||||
/// Callback to prepare video filter before entering PiP
|
||||
VoidCallback? onBeforeEnterPip;
|
||||
|
||||
/// Get current video dimensions (display or storage or fallback to viewport)
|
||||
Future<(int? width, int? height)> _getVideoDimensions() async {
|
||||
int? width;
|
||||
int? height;
|
||||
|
||||
try {
|
||||
final dwidth = await player.getProperty('dwidth');
|
||||
final dheight = await player.getProperty('dheight');
|
||||
if (dwidth != null && dheight != null) {
|
||||
width = int.tryParse(dwidth);
|
||||
height = int.tryParse(dheight);
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.d('VideoPipManager: dwidth/dheight unavailable', error: e);
|
||||
}
|
||||
|
||||
if (width == null || height == null) {
|
||||
try {
|
||||
final videoWidth = await player.getProperty('width');
|
||||
final videoHeight = await player.getProperty('height');
|
||||
if (videoWidth != null && videoHeight != null) {
|
||||
width = int.tryParse(videoWidth);
|
||||
height = int.tryParse(videoHeight);
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.d('VideoPipManager: width/height unavailable', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
final viewport = playerSize();
|
||||
width ??= viewport?.width.toInt();
|
||||
height ??= viewport?.height.toInt();
|
||||
|
||||
return (width, height);
|
||||
}
|
||||
|
||||
Future<(bool success, String? error)> togglePIP() async {
|
||||
final supported = await PipService.isSupported();
|
||||
if (!supported) return (false, 'PiP not supported on this device');
|
||||
|
||||
// If PiP is already active, exit it
|
||||
if (PipService().isPipActive.value) {
|
||||
await PipService.exit();
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
// Reset video filter to contain mode before entering PiP. Android, iOS,
|
||||
// and macOS all reuse the inline video surface/layer for PiP.
|
||||
if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
|
||||
onBeforeEnterPip?.call();
|
||||
// Wait a frame for the filter change to take effect
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
final dims = await _getVideoDimensions();
|
||||
return await PipService.enter(width: dims.$1, height: dims.$2);
|
||||
}
|
||||
|
||||
Future<void> updateAutoPipState({required bool isPlaying}) async {
|
||||
if (!isPlaying) {
|
||||
await PipService.setAutoPipReady(ready: false);
|
||||
return;
|
||||
}
|
||||
|
||||
final dims = await _getVideoDimensions();
|
||||
await PipService.setAutoPipReady(ready: true, width: dims.$1, height: dims.$2);
|
||||
}
|
||||
|
||||
/// Disable auto-PiP (called on dispose or when leaving player)
|
||||
Future<void> disableAutoPip() async {
|
||||
await PipService.setAutoPipReady(ready: false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user