refactor: deduplicate logic
This commit is contained in:
@@ -106,6 +106,22 @@ class OfflineWatchProvider extends ChangeNotifier {
|
|||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Batch resolve watch statuses for a list of episodes.
|
||||||
|
///
|
||||||
|
/// Returns a map of globalKey -> isWatched for each episode.
|
||||||
|
Future<Map<String, bool>> _resolveEpisodeWatchStatuses(List<PlexMetadata> episodes) async {
|
||||||
|
if (episodes.isEmpty) return {};
|
||||||
|
|
||||||
|
final globalKeys = episodes.map((e) => e.globalKey).toSet();
|
||||||
|
final localStatuses = await _syncService.getLocalWatchStatusesBatched(globalKeys);
|
||||||
|
|
||||||
|
return {
|
||||||
|
for (final episode in episodes)
|
||||||
|
episode.globalKey:
|
||||||
|
localStatuses[episode.globalKey] ?? _downloadProvider.getMetadata(episode.globalKey)?.isWatched ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the next unwatched downloaded episode for a show.
|
/// Find the next unwatched downloaded episode for a show.
|
||||||
///
|
///
|
||||||
/// This is the "offline OnDeck" calculation - finds the first
|
/// This is the "offline OnDeck" calculation - finds the first
|
||||||
@@ -118,15 +134,11 @@ class OfflineWatchProvider extends ChangeNotifier {
|
|||||||
final episodes = _getSortedEpisodes(showRatingKey);
|
final episodes = _getSortedEpisodes(showRatingKey);
|
||||||
if (episodes.isEmpty) return null;
|
if (episodes.isEmpty) return null;
|
||||||
|
|
||||||
// Batch fetch all watch statuses in a single query
|
final watchStatuses = await _resolveEpisodeWatchStatuses(episodes);
|
||||||
final globalKeys = episodes.map((e) => e.globalKey).toSet();
|
|
||||||
final localStatuses = await _syncService.getLocalWatchStatusesBatched(globalKeys);
|
|
||||||
|
|
||||||
// Find first unwatched episode
|
// Find first unwatched episode
|
||||||
for (final episode in episodes) {
|
for (final episode in episodes) {
|
||||||
final localStatus = localStatuses[episode.globalKey];
|
if (!watchStatuses[episode.globalKey]!) {
|
||||||
final watched = localStatus ?? _downloadProvider.getMetadata(episode.globalKey)?.isWatched ?? false;
|
|
||||||
if (!watched) {
|
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,21 +188,11 @@ class OfflineWatchProvider extends ChangeNotifier {
|
|||||||
/// Uses batched database query for efficiency.
|
/// Uses batched database query for efficiency.
|
||||||
Future<List<(PlexMetadata episode, bool isWatched)>> getEpisodesWithWatchStatus(String showRatingKey) async {
|
Future<List<(PlexMetadata episode, bool isWatched)>> getEpisodesWithWatchStatus(String showRatingKey) async {
|
||||||
final episodes = _downloadProvider.getDownloadedEpisodesForShow(showRatingKey);
|
final episodes = _downloadProvider.getDownloadedEpisodesForShow(showRatingKey);
|
||||||
|
|
||||||
if (episodes.isEmpty) return [];
|
if (episodes.isEmpty) return [];
|
||||||
|
|
||||||
// Batch fetch all watch statuses in a single query
|
final watchStatuses = await _resolveEpisodeWatchStatuses(episodes);
|
||||||
final globalKeys = episodes.map((e) => e.globalKey).toSet();
|
|
||||||
final localStatuses = await _syncService.getLocalWatchStatusesBatched(globalKeys);
|
|
||||||
|
|
||||||
final results = <(PlexMetadata, bool)>[];
|
return [for (final episode in episodes) (episode, watchStatuses[episode.globalKey]!)];
|
||||||
for (final episode in episodes) {
|
|
||||||
final localStatus = localStatuses[episode.globalKey];
|
|
||||||
final watched = localStatus ?? _downloadProvider.getMetadata(episode.globalKey)?.isWatched ?? false;
|
|
||||||
results.add((episode, watched));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger a manual sync of pending items.
|
/// Trigger a manual sync of pending items.
|
||||||
|
|||||||
@@ -129,20 +129,27 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
|
|||||||
valueController.dispose();
|
valueController.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDeleteEntryDialog(int index) async {
|
Future<bool> _showConfirmDeleteDialog({required String title, required String content}) async {
|
||||||
final result = await showDialog<bool>(
|
final result = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(t.mpvConfig.deleteProperty),
|
title: Text(title),
|
||||||
content: Text(t.mpvConfig.confirmDeleteProperty),
|
content: Text(content),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.delete)),
|
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.delete)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return result == true;
|
||||||
|
}
|
||||||
|
|
||||||
if (result == true) {
|
Future<void> _showDeleteEntryDialog(int index) async {
|
||||||
|
final confirmed = await _showConfirmDeleteDialog(
|
||||||
|
title: t.mpvConfig.deleteProperty,
|
||||||
|
content: t.mpvConfig.confirmDeleteProperty,
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
_deleteEntry(index);
|
_deleteEntry(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,19 +208,12 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deletePreset(MpvPreset preset) async {
|
Future<void> _deletePreset(MpvPreset preset) async {
|
||||||
final result = await showDialog<bool>(
|
final confirmed = await _showConfirmDeleteDialog(
|
||||||
context: context,
|
title: t.mpvConfig.deletePreset,
|
||||||
builder: (context) => AlertDialog(
|
content: t.mpvConfig.confirmDeletePreset,
|
||||||
title: Text(t.mpvConfig.deletePreset),
|
|
||||||
content: Text(t.mpvConfig.confirmDeletePreset),
|
|
||||||
actions: [
|
|
||||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
|
||||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.delete)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == true) {
|
if (confirmed) {
|
||||||
await _settingsService.deleteMpvPreset(preset.name);
|
await _settingsService.deleteMpvPreset(preset.name);
|
||||||
setState(() {
|
setState(() {
|
||||||
_presets = _settingsService.getMpvPresets();
|
_presets = _settingsService.getMpvPresets();
|
||||||
|
|||||||
@@ -247,15 +247,11 @@ class DataAggregationService {
|
|||||||
|
|
||||||
// Private helper methods
|
// Private helper methods
|
||||||
|
|
||||||
/// Higher-order helper for per-server fan-out operations
|
/// Base helper for per-server fan-out operations
|
||||||
///
|
///
|
||||||
/// Iterates over all online clients, executes the operation for each server,
|
/// Returns raw results as (serverId, result) tuples.
|
||||||
/// handles errors, updates server status, and aggregates results.
|
/// Used by [_perServer] and [_perServerGrouped] for different aggregation strategies.
|
||||||
///
|
Future<List<(String serverId, List<T> result)>> _perServerRaw<T>({
|
||||||
/// Type parameter `T` is the item type returned by the operation
|
|
||||||
/// [operationName] is used for logging (e.g., "fetching libraries")
|
|
||||||
/// [operation] is the async function to run per server, returning `List<T>`
|
|
||||||
Future<List<T>> _perServer<T>({
|
|
||||||
required String operationName,
|
required String operationName,
|
||||||
required Future<List<T>> Function(String serverId, PlexClient client, PlexServer? server) operation,
|
required Future<List<T>> Function(String serverId, PlexClient client, PlexServer? server) operation,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -268,61 +264,6 @@ class DataAggregationService {
|
|||||||
|
|
||||||
appLogger.d('$operationName from ${clients.length} servers');
|
appLogger.d('$operationName from ${clients.length} servers');
|
||||||
|
|
||||||
final allResults = <T>[];
|
|
||||||
|
|
||||||
// Execute operation on all servers in parallel
|
|
||||||
final Iterable<Future<List<T>>> futures = clients.entries.map((entry) async {
|
|
||||||
final serverId = entry.key;
|
|
||||||
final client = entry.value;
|
|
||||||
final server = _serverManager.getServer(serverId);
|
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final result = await operation(serverId, client, server);
|
|
||||||
appLogger.d(
|
|
||||||
'$operationName for server $serverId completed in ${sw.elapsedMilliseconds}ms with ${result.length} items',
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
appLogger.e('Failed $operationName from server $serverId', error: e, stackTrace: stackTrace);
|
|
||||||
_serverManager.updateServerStatus(serverId, false);
|
|
||||||
appLogger.d('$operationName for server $serverId failed after ${sw.elapsedMilliseconds}ms');
|
|
||||||
return <T>[];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<List<T>> results = await Future.wait<List<T>>(futures);
|
|
||||||
|
|
||||||
// Flatten results
|
|
||||||
for (final items in results) {
|
|
||||||
allResults.addAll(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
return allResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Higher-order helper for per-server fan-out operations that groups results by server
|
|
||||||
///
|
|
||||||
/// Similar to [_perServer] but returns a Map with results grouped by serverId
|
|
||||||
/// instead of flattening into a single list.
|
|
||||||
///
|
|
||||||
/// Type parameter `T` is the item type returned by the operation
|
|
||||||
/// [operationName] is used for logging (e.g., "fetching libraries")
|
|
||||||
/// [operation] is the async function to run per server, returning `List<T>`
|
|
||||||
Future<Map<String, List<T>>> _perServerGrouped<T>({
|
|
||||||
required String operationName,
|
|
||||||
required Future<List<T>> Function(String serverId, PlexClient client, PlexServer? server) operation,
|
|
||||||
}) async {
|
|
||||||
final clients = _serverManager.onlineClients;
|
|
||||||
|
|
||||||
if (clients.isEmpty) {
|
|
||||||
appLogger.w('No online servers available for $operationName');
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
appLogger.d('$operationName from ${clients.length} servers');
|
|
||||||
|
|
||||||
// Execute operation on all servers in parallel
|
|
||||||
final futures = clients.entries.map((entry) async {
|
final futures = clients.entries.map((entry) async {
|
||||||
final serverId = entry.key;
|
final serverId = entry.key;
|
||||||
final client = entry.value;
|
final client = entry.value;
|
||||||
@@ -334,17 +275,38 @@ class DataAggregationService {
|
|||||||
appLogger.d(
|
appLogger.d(
|
||||||
'$operationName for server $serverId completed in ${sw.elapsedMilliseconds}ms with ${result.length} items',
|
'$operationName for server $serverId completed in ${sw.elapsedMilliseconds}ms with ${result.length} items',
|
||||||
);
|
);
|
||||||
return MapEntry(serverId, result);
|
return (serverId, result);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
appLogger.e('Failed $operationName from server $serverId', error: e, stackTrace: stackTrace);
|
appLogger.e('Failed $operationName from server $serverId', error: e, stackTrace: stackTrace);
|
||||||
_serverManager.updateServerStatus(serverId, false);
|
_serverManager.updateServerStatus(serverId, false);
|
||||||
appLogger.d('$operationName for server $serverId failed after ${sw.elapsedMilliseconds}ms');
|
appLogger.d('$operationName for server $serverId failed after ${sw.elapsedMilliseconds}ms');
|
||||||
return MapEntry(serverId, <T>[]);
|
return (serverId, <T>[]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final results = await Future.wait(futures);
|
return await Future.wait(futures);
|
||||||
|
}
|
||||||
|
|
||||||
return Map.fromEntries(results);
|
/// Higher-order helper for per-server fan-out operations
|
||||||
|
///
|
||||||
|
/// Iterates over all online clients, executes the operation for each server,
|
||||||
|
/// handles errors, updates server status, and flattens results into a single list.
|
||||||
|
Future<List<T>> _perServer<T>({
|
||||||
|
required String operationName,
|
||||||
|
required Future<List<T>> Function(String serverId, PlexClient client, PlexServer? server) operation,
|
||||||
|
}) async {
|
||||||
|
final results = await _perServerRaw(operationName: operationName, operation: operation);
|
||||||
|
return [for (final (_, items) in results) ...items];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Higher-order helper for per-server fan-out operations that groups results by server
|
||||||
|
///
|
||||||
|
/// Similar to [_perServer] but returns a Map with results grouped by serverId.
|
||||||
|
Future<Map<String, List<T>>> _perServerGrouped<T>({
|
||||||
|
required String operationName,
|
||||||
|
required Future<List<T>> Function(String serverId, PlexClient client, PlexServer? server) operation,
|
||||||
|
}) async {
|
||||||
|
final results = await _perServerRaw(operationName: operationName, operation: operation);
|
||||||
|
return {for (final (id, items) in results) id: items};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,7 @@ class TrackSelectionService {
|
|||||||
final PlexMetadata metadata;
|
final PlexMetadata metadata;
|
||||||
final PlexMediaInfo? plexMediaInfo;
|
final PlexMediaInfo? plexMediaInfo;
|
||||||
|
|
||||||
TrackSelectionService({
|
TrackSelectionService({required this.player, this.profileSettings, required this.metadata, this.plexMediaInfo});
|
||||||
required this.player,
|
|
||||||
this.profileSettings,
|
|
||||||
required this.metadata,
|
|
||||||
this.plexMediaInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Build list of preferred languages from a user profile
|
/// Build list of preferred languages from a user profile
|
||||||
List<String> _buildPreferredLanguages(PlexUserProfile profile, {required bool isAudio}) {
|
List<String> _buildPreferredLanguages(PlexUserProfile profile, {required bool isAudio}) {
|
||||||
@@ -324,7 +319,10 @@ class TrackSelectionService {
|
|||||||
/// Priority 3: Per-media language preference
|
/// Priority 3: Per-media language preference
|
||||||
/// Priority 4: User profile preferences
|
/// Priority 4: User profile preferences
|
||||||
/// Priority 5: Default or first track
|
/// Priority 5: Default or first track
|
||||||
TrackSelectionResult<AudioTrack>? selectAudioTrack(List<AudioTrack> availableTracks, AudioTrack? preferredAudioTrack) {
|
TrackSelectionResult<AudioTrack>? selectAudioTrack(
|
||||||
|
List<AudioTrack> availableTracks,
|
||||||
|
AudioTrack? preferredAudioTrack,
|
||||||
|
) {
|
||||||
if (availableTracks.isEmpty) return null;
|
if (availableTracks.isEmpty) return null;
|
||||||
|
|
||||||
AudioTrack? trackToSelect;
|
AudioTrack? trackToSelect;
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
_displayName = name;
|
_displayName = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wire up sync manager's state change callback to update provider state
|
||||||
|
void _wireSyncStateChanges() {
|
||||||
|
_syncManager!.onSyncStateChanged = (isSyncing) {
|
||||||
|
_isSyncing = isSyncing;
|
||||||
|
notifyListeners();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new watch together session as host
|
/// Create a new watch together session as host
|
||||||
Future<String> createSession({
|
Future<String> createSession({
|
||||||
required ControlMode controlMode,
|
required ControlMode controlMode,
|
||||||
@@ -113,10 +121,7 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
displayName: _displayName,
|
displayName: _displayName,
|
||||||
);
|
);
|
||||||
|
|
||||||
_syncManager!.onSyncStateChanged = (isSyncing) {
|
_wireSyncStateChanges();
|
||||||
_isSyncing = isSyncing;
|
|
||||||
notifyListeners();
|
|
||||||
};
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
appLogger.d('WatchTogether: Session created: $sessionId');
|
appLogger.d('WatchTogether: Session created: $sessionId');
|
||||||
@@ -164,10 +169,7 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
};
|
};
|
||||||
|
|
||||||
_syncManager!.onSyncStateChanged = (isSyncing) {
|
_wireSyncStateChanges();
|
||||||
_isSyncing = isSyncing;
|
|
||||||
notifyListeners();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add self to participants
|
// Add self to participants
|
||||||
_participants.add(Participant(peerId: _peerService!.myPeerId!, displayName: _displayName, isHost: false));
|
_participants.add(Participant(peerId: _peerService!.myPeerId!, displayName: _displayName, isHost: false));
|
||||||
|
|||||||
@@ -83,6 +83,31 @@ class WatchTogetherPeerService {
|
|||||||
return const Uuid().v4().substring(0, 8).toUpperCase();
|
return const Uuid().v4().substring(0, 8).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attach common peer event listeners for disconnected/close/error events
|
||||||
|
void _attachCommonPeerListeners({
|
||||||
|
required Completer completer,
|
||||||
|
required PeerErrorType errorType,
|
||||||
|
required String errorMessage,
|
||||||
|
}) {
|
||||||
|
_peer!.on('disconnected').listen((_) {
|
||||||
|
appLogger.w('WatchTogether: Peer disconnected from server');
|
||||||
|
_handleDisconnectedFromServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
_peer!.on('close').listen((_) {
|
||||||
|
appLogger.d('WatchTogether: Peer closed');
|
||||||
|
_connectionStateController.add(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
_peer!.on('error').listen((error) {
|
||||||
|
appLogger.e('WatchTogether: Peer error', error: error);
|
||||||
|
_errorController.add(PeerError(type: errorType, message: '$errorMessage: $error', originalError: error));
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new session as host
|
/// Create a new session as host
|
||||||
///
|
///
|
||||||
/// Returns the session ID that others can use to join
|
/// Returns the session ID that others can use to join
|
||||||
@@ -115,25 +140,11 @@ class WatchTogetherPeerService {
|
|||||||
_handleNewConnection(dataConn);
|
_handleNewConnection(dataConn);
|
||||||
});
|
});
|
||||||
|
|
||||||
_peer!.on('error').listen((error) {
|
_attachCommonPeerListeners(
|
||||||
appLogger.e('WatchTogether: Peer error', error: error);
|
completer: completer,
|
||||||
_errorController.add(
|
errorType: PeerErrorType.serverError,
|
||||||
PeerError(type: PeerErrorType.serverError, message: error.toString(), originalError: error),
|
errorMessage: 'Server error',
|
||||||
);
|
);
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.completeError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_peer!.on('disconnected').listen((_) {
|
|
||||||
appLogger.w('WatchTogether: Peer disconnected from server');
|
|
||||||
_handleDisconnectedFromServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
_peer!.on('close').listen((_) {
|
|
||||||
appLogger.d('WatchTogether: Peer closed');
|
|
||||||
_connectionStateController.add(false);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('WatchTogether: Failed to create peer', error: e);
|
appLogger.e('WatchTogether: Failed to create peer', error: e);
|
||||||
if (!completer.isCompleted) {
|
if (!completer.isCompleted) {
|
||||||
@@ -178,29 +189,11 @@ class WatchTogetherPeerService {
|
|||||||
_handleNewConnection(conn, isOutgoing: true, completer: completer);
|
_handleNewConnection(conn, isOutgoing: true, completer: completer);
|
||||||
});
|
});
|
||||||
|
|
||||||
_peer!.on('error').listen((error) {
|
_attachCommonPeerListeners(
|
||||||
appLogger.e('WatchTogether: Peer error', error: error);
|
completer: completer,
|
||||||
_errorController.add(
|
errorType: PeerErrorType.connectionFailed,
|
||||||
PeerError(
|
errorMessage: 'Failed to connect to session',
|
||||||
type: PeerErrorType.connectionFailed,
|
);
|
||||||
message: 'Failed to connect to session: $error',
|
|
||||||
originalError: error,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.completeError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_peer!.on('disconnected').listen((_) {
|
|
||||||
appLogger.w('WatchTogether: Peer disconnected from server');
|
|
||||||
_handleDisconnectedFromServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
_peer!.on('close').listen((_) {
|
|
||||||
appLogger.d('WatchTogether: Peer closed');
|
|
||||||
_connectionStateController.add(false);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('WatchTogether: Failed to create peer for joining', error: e);
|
appLogger.e('WatchTogether: Failed to create peer for joining', error: e);
|
||||||
if (!completer.isCompleted) {
|
if (!completer.isCompleted) {
|
||||||
|
|||||||
@@ -241,19 +241,9 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort shows by status and title
|
// Sort shows and movies by status and title
|
||||||
shows.sort((a, b) {
|
_sortNodesByStatusAndTitle(shows);
|
||||||
final statusCompare = _compareByStatus(a.status, b.status);
|
_sortNodesByStatusAndTitle(movies);
|
||||||
if (statusCompare != 0) return statusCompare;
|
|
||||||
return a.title.compareTo(b.title);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort movies by status and title
|
|
||||||
movies.sort((a, b) {
|
|
||||||
final statusCompare = _compareByStatus(a.status, b.status);
|
|
||||||
if (statusCompare != 0) return statusCompare;
|
|
||||||
return a.title.compareTo(b.title);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Combine movies and shows
|
// Combine movies and shows
|
||||||
return [...movies, ...shows];
|
return [...movies, ...shows];
|
||||||
@@ -292,6 +282,15 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
|||||||
return (statusOrder[a] ?? 99).compareTo(statusOrder[b] ?? 99);
|
return (statusOrder[a] ?? 99).compareTo(statusOrder[b] ?? 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sort nodes by status (downloading first) then by title
|
||||||
|
void _sortNodesByStatusAndTitle(List<DownloadTreeNode> nodes) {
|
||||||
|
nodes.sort((a, b) {
|
||||||
|
final statusCompare = _compareByStatus(a.status, b.status);
|
||||||
|
if (statusCompare != 0) return statusCompare;
|
||||||
|
return a.title.compareTo(b.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Flatten the tree into a list of visible nodes with their depths
|
/// Flatten the tree into a list of visible nodes with their depths
|
||||||
List<_FlatNode> _flattenTree(List<DownloadTreeNode> nodes, [int depth = 0]) {
|
List<_FlatNode> _flattenTree(List<DownloadTreeNode> nodes, [int depth = 0]) {
|
||||||
final List<_FlatNode> result = [];
|
final List<_FlatNode> result = [];
|
||||||
|
|||||||
@@ -61,25 +61,16 @@ class _HorizontalScrollWithArrowsState extends State<HorizontalScrollWithArrows>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollLeft() {
|
void _animateScroll(double direction) {
|
||||||
final position = _scrollController.position;
|
final position = _scrollController.position;
|
||||||
final targetScroll = (position.pixels - (position.viewportDimension * widget.scrollAmount)).clamp(
|
final delta = direction * position.viewportDimension * widget.scrollAmount;
|
||||||
0.0,
|
final targetScroll = (position.pixels + delta).clamp(0.0, position.maxScrollExtent);
|
||||||
position.maxScrollExtent,
|
|
||||||
);
|
|
||||||
|
|
||||||
_scrollController.animateTo(targetScroll, duration: tokens(context).slow, curve: Curves.easeInOut);
|
_scrollController.animateTo(targetScroll, duration: tokens(context).slow, curve: Curves.easeInOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollRight() {
|
void _scrollLeft() => _animateScroll(-1);
|
||||||
final position = _scrollController.position;
|
|
||||||
final targetScroll = (position.pixels + (position.viewportDimension * widget.scrollAmount)).clamp(
|
|
||||||
0.0,
|
|
||||||
position.maxScrollExtent,
|
|
||||||
);
|
|
||||||
|
|
||||||
_scrollController.animateTo(targetScroll, duration: tokens(context).slow, curve: Curves.easeInOut);
|
void _scrollRight() => _animateScroll(1);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildArrowButton({
|
Widget _buildArrowButton({
|
||||||
required double position,
|
required double position,
|
||||||
|
|||||||
Reference in New Issue
Block a user