fix: eliminate cross-app consistency drift
This commit is contained in:
@@ -159,6 +159,10 @@ class _AlphaJumpBarState extends State<AlphaJumpBar> {
|
||||
}
|
||||
});
|
||||
if (selectResult != KeyEventResult.ignored) return selectResult;
|
||||
if (widget.onBack != null) {
|
||||
final backResult = handleBackKeyAction(event, widget.onBack!);
|
||||
if (backResult != KeyEventResult.ignored) return backResult;
|
||||
}
|
||||
|
||||
if (event is! KeyDownEvent && event is! KeyRepeatEvent) {
|
||||
return KeyEventResult.ignored;
|
||||
@@ -182,12 +186,6 @@ class _AlphaJumpBarState extends State<AlphaJumpBar> {
|
||||
widget.onNavigateLeft?.call();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape ||
|
||||
event.logicalKey == LogicalKeyboardKey.goBack ||
|
||||
event.logicalKey == LogicalKeyboardKey.gameButtonB) {
|
||||
widget.onBack?.call();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ class SliverErrorState extends StatelessWidget {
|
||||
final VoidCallback? onActionNavigateLeft;
|
||||
final VoidCallback? onActionNavigateRight;
|
||||
final VoidCallback? onActionBack;
|
||||
final bool actionAutofocus;
|
||||
final bool actionUseBackgroundFocus;
|
||||
|
||||
const SliverErrorState({
|
||||
super.key,
|
||||
@@ -26,6 +28,8 @@ class SliverErrorState extends StatelessWidget {
|
||||
this.onActionNavigateLeft,
|
||||
this.onActionNavigateRight,
|
||||
this.onActionBack,
|
||||
this.actionAutofocus = false,
|
||||
this.actionUseBackgroundFocus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -34,13 +38,15 @@ class SliverErrorState extends StatelessWidget {
|
||||
message: message,
|
||||
icon: Symbols.error_outline_rounded,
|
||||
onRetry: onRetry,
|
||||
retryLabel: retryLabel ?? t.common.retry,
|
||||
retryLabel: retryLabel,
|
||||
actionFocusNode: actionFocusNode,
|
||||
onActionNavigateUp: onActionNavigateUp,
|
||||
onActionNavigateDown: onActionNavigateDown,
|
||||
onActionNavigateLeft: onActionNavigateLeft,
|
||||
onActionNavigateRight: onActionNavigateRight,
|
||||
onActionBack: onActionBack,
|
||||
actionAutofocus: actionAutofocus,
|
||||
actionUseBackgroundFocus: actionUseBackgroundFocus,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import '../../media/ids.dart';
|
||||
|
||||
@@ -19,7 +18,6 @@ import '../../utils/provider_extensions.dart';
|
||||
import '../../widgets/app_menu.dart';
|
||||
import '../../widgets/media_context_menu.dart';
|
||||
import '../../widgets/optimized_media_image.dart';
|
||||
import '../../widgets/overlay_sheet.dart';
|
||||
import '../../widgets/watched_indicator.dart';
|
||||
import '../../theme/mono_tokens.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
@@ -42,7 +40,7 @@ class FolderTreeItem extends StatefulWidget {
|
||||
final FocusNode? focusNode;
|
||||
final VoidCallback? onNavigateUp;
|
||||
final VoidCallback? onNavigateLeft;
|
||||
final void Function(String itemId)? onRefresh;
|
||||
final void Function(MediaItem source)? onRefresh;
|
||||
final VoidCallback? onListRefresh;
|
||||
final String? serverId;
|
||||
|
||||
@@ -150,23 +148,13 @@ class _FolderTreeItemState extends State<FolderTreeItem> with ContextMenuTapMixi
|
||||
final previousFocus = FocusManager.instance.primaryFocus;
|
||||
final position = lastTapPosition;
|
||||
final fromKeyboard = position == null;
|
||||
final useBottomSheet = Platform.isIOS || Platform.isAndroid;
|
||||
|
||||
String? selected;
|
||||
if (useBottomSheet) {
|
||||
selected = await OverlaySheetController.showAdaptive<String>(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
builder: (context) => AppMenuSheet<String>(title: _rowTitle(), entries: entries, focusFirstItem: fromKeyboard),
|
||||
);
|
||||
} else {
|
||||
selected = await showAppMenu<String>(
|
||||
context,
|
||||
entries: entries,
|
||||
position: position ?? _rowCenter(),
|
||||
focusFirstItem: fromKeyboard,
|
||||
);
|
||||
}
|
||||
final selected = await showAdaptiveAppMenu<String>(
|
||||
context,
|
||||
title: _rowTitle(),
|
||||
entries: entries,
|
||||
position: position ?? _rowCenter(),
|
||||
focusFirstItem: fromKeyboard,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class FolderTreeView extends StatefulWidget {
|
||||
final String libraryKey;
|
||||
final String? serverId; // Server this library belongs to
|
||||
final MediaKind? libraryKind;
|
||||
final void Function(String)? onRefresh;
|
||||
final void Function(MediaItem source)? onRefresh;
|
||||
final FocusNode? firstItemFocusNode;
|
||||
final VoidCallback? onNavigateUp;
|
||||
final VoidCallback? onNavigateLeft;
|
||||
@@ -117,11 +117,12 @@ class FolderTreeViewState extends State<FolderTreeView> {
|
||||
});
|
||||
|
||||
appLogger.d('Loaded ${folders.length} root folders');
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
if (!mounted || epoch != _loadEpoch) return;
|
||||
|
||||
final message = localizedLoadErrorMessage(e, stackTrace, context: t.libraries.folders);
|
||||
setState(() {
|
||||
_errorMessage = mapUnexpectedErrorToMessage(e, context: t.libraries.folders);
|
||||
_errorMessage = message;
|
||||
_isLoadingRoot = false;
|
||||
});
|
||||
}
|
||||
@@ -181,10 +182,10 @@ class FolderTreeViewState extends State<FolderTreeView> {
|
||||
});
|
||||
|
||||
appLogger.d('Loaded ${children.length} children for folder: ${folder.title}');
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
if (!mounted || epoch != _loadEpoch) return;
|
||||
|
||||
final message = mapUnexpectedErrorToMessage(e, context: t.libraries.folders);
|
||||
final message = localizedLoadErrorMessage(e, stackTrace, context: t.libraries.folders);
|
||||
setState(() {
|
||||
_loadingFolders.remove(folderIdentity);
|
||||
// Drop partial pages so a retry refetches instead of leaving a
|
||||
@@ -228,14 +229,14 @@ class FolderTreeViewState extends State<FolderTreeView> {
|
||||
_loadFolderChildren(parent);
|
||||
}
|
||||
|
||||
Future<void> _handleItemTap(MediaItem item) async {
|
||||
Future<void> _handleItemTap(MediaItem item, MediaItem? parent) async {
|
||||
final result = await navigateToMediaItem(context, item, onRefresh: widget.onRefresh);
|
||||
if (!mounted) return;
|
||||
switch (result) {
|
||||
case MediaNavigationResult.unsupported:
|
||||
showAppSnackBar(context, t.messages.musicNotSupported);
|
||||
case MediaNavigationResult.listRefreshNeeded:
|
||||
widget.onRefresh?.call(item.id);
|
||||
_refreshAfterDeletion(parent);
|
||||
case MediaNavigationResult.navigated:
|
||||
case MediaNavigationResult.librarySelected:
|
||||
break;
|
||||
@@ -398,7 +399,7 @@ class FolderTreeViewState extends State<FolderTreeView> {
|
||||
isLoading: isLoading,
|
||||
serverId: widget.serverId,
|
||||
onExpand: isExpandable ? () => _toggleFolder(item) : null,
|
||||
onTap: !isExpandable ? () => _handleItemTap(item) : null,
|
||||
onTap: !isExpandable ? () => _handleItemTap(item, entry.parent) : null,
|
||||
onPlayAll: canPlayFolder ? () => _handleFolderPlay(item) : null,
|
||||
onShuffle: canPlayFolder ? () => _handleFolderShuffle(item) : null,
|
||||
focusNode: isFirstRootItem ? widget.firstItemFocusNode : null,
|
||||
|
||||
@@ -512,7 +512,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
}
|
||||
|
||||
@override
|
||||
void updateItemInLists(String itemId, MediaItem updatedItem) {
|
||||
void updateItemInLists(String sourceGlobalKey, MediaItem updatedItem) {
|
||||
// Delegate to the active tab — parent doesn't maintain its own item list
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
|
||||
/// Base widget for displaying state messages (empty, error, etc.)
|
||||
/// Provides a consistent UI pattern for showing icons, messages, and actions
|
||||
@@ -43,6 +44,12 @@ class StateMessageWidget extends StatelessWidget {
|
||||
final VoidCallback? onActionNavigateRight;
|
||||
final VoidCallback? onActionBack;
|
||||
|
||||
/// Whether the action button should request focus when it appears.
|
||||
final bool actionAutofocus;
|
||||
|
||||
/// Whether the action button uses a background focus indicator.
|
||||
final bool actionUseBackgroundFocus;
|
||||
|
||||
const StateMessageWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
@@ -61,6 +68,8 @@ class StateMessageWidget extends StatelessWidget {
|
||||
this.onActionNavigateRight,
|
||||
this.onActionBack,
|
||||
this.actionIcon,
|
||||
this.actionAutofocus = false,
|
||||
this.actionUseBackgroundFocus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -106,6 +115,8 @@ class StateMessageWidget extends StatelessWidget {
|
||||
onNavigateRight: onActionNavigateRight,
|
||||
onBack: onActionBack,
|
||||
onPressed: onAction,
|
||||
autofocus: actionAutofocus,
|
||||
useBackgroundFocus: actionUseBackgroundFocus,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onAction,
|
||||
icon: AppIcon(actionIcon ?? Symbols.refresh_rounded, fill: 1),
|
||||
@@ -197,6 +208,12 @@ class ErrorStateWidget extends StatelessWidget {
|
||||
/// Optional callback for retry action
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
/// Whether the retry action should request focus when it appears.
|
||||
final bool actionAutofocus;
|
||||
|
||||
/// Whether the retry action uses a background focus indicator.
|
||||
final bool actionUseBackgroundFocus;
|
||||
|
||||
/// Optional label for the retry button
|
||||
final String? retryLabel;
|
||||
final FocusNode? actionFocusNode;
|
||||
@@ -218,6 +235,8 @@ class ErrorStateWidget extends StatelessWidget {
|
||||
this.onActionNavigateLeft,
|
||||
this.onActionNavigateRight,
|
||||
this.onActionBack,
|
||||
this.actionAutofocus = false,
|
||||
this.actionUseBackgroundFocus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -228,7 +247,7 @@ class ErrorStateWidget extends StatelessWidget {
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
onAction: onRetry,
|
||||
actionLabel: retryLabel ?? 'Retry',
|
||||
actionLabel: retryLabel ?? t.common.retry,
|
||||
actionIcon: Symbols.refresh_rounded,
|
||||
actionFocusNode: actionFocusNode,
|
||||
onActionNavigateUp: onActionNavigateUp,
|
||||
@@ -236,6 +255,8 @@ class ErrorStateWidget extends StatelessWidget {
|
||||
onActionNavigateLeft: onActionNavigateLeft,
|
||||
onActionNavigateRight: onActionNavigateRight,
|
||||
onActionBack: onActionBack,
|
||||
actionAutofocus: actionAutofocus,
|
||||
actionUseBackgroundFocus: actionUseBackgroundFocus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../focus/input_mode_tracker.dart';
|
||||
import '../../../media/media_library.dart';
|
||||
import '../../../utils/app_logger.dart';
|
||||
import '../../../utils/error_message_utils.dart';
|
||||
import '../../../mixins/library_tab_state.dart';
|
||||
import '../../../mixins/refreshable.dart';
|
||||
import '../content_state_builder.dart';
|
||||
@@ -246,12 +246,12 @@ abstract class BaseLibraryTabState<T, W extends BaseLibraryTab<T>> extends State
|
||||
widget.onDataLoaded!();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
final message = localizedLoadErrorMessage(e, stackTrace, context: errorContext);
|
||||
if (!mounted) return;
|
||||
|
||||
appLogger.e('Error loading $errorContext', error: e);
|
||||
setState(() {
|
||||
_errorMessage = 'Failed to load $errorContext: ${e.toString()}';
|
||||
_errorMessage = message;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import '../../../media/ids.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:cached_network_image_ce/cached_network_image.dart';
|
||||
import '../../../media/library_first_character.dart';
|
||||
import '../../../media/library_query.dart';
|
||||
import '../../../media/media_backend.dart';
|
||||
@@ -12,13 +11,11 @@ import '../../../media/media_item.dart';
|
||||
import '../../../media/media_kind.dart';
|
||||
import '../../../providers/multi_server_provider.dart';
|
||||
import '../../../utils/media_server_http_client.dart';
|
||||
import '../../../exceptions/media_server_exceptions.dart';
|
||||
import '../../../focus/dpad_navigator.dart';
|
||||
import '../../../focus/input_mode_tracker.dart';
|
||||
import '../../../media/media_filter.dart';
|
||||
import '../../../media/media_sort.dart';
|
||||
import '../../../widgets/settings_builder.dart';
|
||||
import '../../../services/image_cache_service.dart';
|
||||
import '../../../services/library_query_translator.dart';
|
||||
import '../../../services/plex_constants.dart';
|
||||
import '../../../utils/error_message_utils.dart';
|
||||
@@ -108,9 +105,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
DeletionAware,
|
||||
PaginatedItemLoader<MediaItem, LibraryBrowseTab>,
|
||||
SkeletonUpgradeScheduler {
|
||||
@override
|
||||
String? get itemServerId => widget.library.serverId;
|
||||
|
||||
String _toGlobalKey(String ratingKey, {required ServerId serverId}) => buildGlobalKey(serverId, ratingKey);
|
||||
|
||||
@override
|
||||
@@ -161,7 +155,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
final affectedIds = {event.itemId, ...event.parentChain};
|
||||
for (final item in loadedItems.values) {
|
||||
if (affectedIds.contains(item.id)) {
|
||||
unawaited(updateItem(item.id));
|
||||
unawaited(updateItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,9 +213,9 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
int get itemCount => totalSize;
|
||||
|
||||
@override
|
||||
void updateItemInLists(String itemId, MediaItem updatedMetadata) {
|
||||
void updateItemInLists(String sourceGlobalKey, MediaItem updatedMetadata) {
|
||||
for (final entry in loadedItems.entries) {
|
||||
if (entry.value.id == itemId) {
|
||||
if (entry.value.globalKey == sourceGlobalKey) {
|
||||
loadedItems[entry.key] = updatedMetadata;
|
||||
break;
|
||||
}
|
||||
@@ -600,10 +594,11 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
// Load items and first characters in parallel
|
||||
// _loadItems manages its own requestId internally
|
||||
await Future.wait([_loadItems(), _loadFirstCharacters(requestId: firstCharactersGeneration)]);
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
if (!mounted) return;
|
||||
final message = localizedLoadErrorMessage(e, stackTrace, context: t.libraries.content);
|
||||
setState(() {
|
||||
errorMessage = _getErrorMessage(e);
|
||||
errorMessage = message;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -727,10 +722,11 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
widget.onDataLoaded!();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, stackTrace) {
|
||||
if (generation != _contentRequestId || !mounted) return;
|
||||
final message = localizedLoadErrorMessage(e, stackTrace, context: t.libraries.content);
|
||||
setState(() {
|
||||
errorMessage = _getErrorMessage(e);
|
||||
errorMessage = message;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
@@ -827,13 +823,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
}
|
||||
}
|
||||
|
||||
String _getErrorMessage(dynamic error) {
|
||||
if (error is MediaServerHttpException) {
|
||||
return mapHttpErrorToMessage(error, context: t.libraries.content);
|
||||
}
|
||||
return mapUnexpectedErrorToMessage(error, context: t.libraries.content);
|
||||
}
|
||||
|
||||
Widget _buildBrowseOptionsSheet(BuildContext sheetContext) {
|
||||
final controller = OverlaySheetController.of(sheetContext);
|
||||
return BottomSheetPageScaffold(
|
||||
@@ -1634,7 +1623,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
final item = items[i];
|
||||
final thumb = item.posterThumb(mode: episodePosterMode);
|
||||
if (thumb == null || thumb.isEmpty) continue;
|
||||
final imageType = item.usesWideAspectRatio(episodePosterMode) ? ImageType.thumb : ImageType.poster;
|
||||
final imageType = MediaImageHelper.cardImageType(item, episodePosterMode);
|
||||
|
||||
final imageUrl = MediaImageHelper.getOptimizedImageUrl(
|
||||
client: client,
|
||||
@@ -1648,19 +1637,14 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
|
||||
final scaledWidth = itemWidth * devicePixelRatio;
|
||||
final scaledHeight = itemHeight * devicePixelRatio;
|
||||
final (_, memHeight) = MediaImageHelper.getMemCacheDimensions(
|
||||
final (memWidth, memHeight) = MediaImageHelper.getMemCacheDimensions(
|
||||
displayWidth: scaledWidth.isFinite && scaledWidth > 0 ? scaledWidth.round() : 0,
|
||||
displayHeight: scaledHeight.isFinite && scaledHeight > 0 ? scaledHeight.round() : 0,
|
||||
imageType: imageType,
|
||||
);
|
||||
|
||||
precacheImage(
|
||||
CachedNetworkImageProvider(
|
||||
imageUrl,
|
||||
cacheManager: PlexImageCacheManager.instance,
|
||||
headers: const {'User-Agent': 'Plezy'},
|
||||
maxHeight: memHeight,
|
||||
),
|
||||
MediaImageHelper.serverArtworkProvider(imageUrl: imageUrl, memWidth: memWidth, memHeight: memHeight),
|
||||
context,
|
||||
).ignore();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import '../../../media/media_item.dart';
|
||||
import '../../../mixins/library_tab_focus_mixin.dart';
|
||||
import '../../../mixins/paginated_item_loader.dart';
|
||||
import '../../../services/settings_service.dart';
|
||||
import '../../../utils/app_logger.dart';
|
||||
import '../../../utils/error_message_utils.dart';
|
||||
import '../../../utils/layout_constants.dart';
|
||||
import '../../../utils/library_refresh_notifier.dart';
|
||||
import '../../../utils/media_server_http_client.dart';
|
||||
@@ -79,6 +79,7 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
||||
|
||||
@override
|
||||
Future<void> loadItems() async {
|
||||
String? loadErrorMessage;
|
||||
await loadInitialPaginatedItems(
|
||||
pageSize: _pageSize,
|
||||
resetViewState: () {
|
||||
@@ -90,8 +91,8 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
||||
items = loaded;
|
||||
isLoading = false;
|
||||
},
|
||||
applyError: (error, _) {
|
||||
errorMessage = 'Failed to load $errorContext: ${error.toString()}';
|
||||
applyError: (error, stackTrace) {
|
||||
errorMessage = loadErrorMessage ?? t.errors.unableToLoad(context: errorContext);
|
||||
isLoading = false;
|
||||
},
|
||||
onLoaded: (_, _) {
|
||||
@@ -104,7 +105,7 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
||||
}
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
appLogger.e('Error loading $errorContext', error: error, stackTrace: stackTrace);
|
||||
loadErrorMessage = localizedLoadErrorMessage(error, stackTrace, context: errorContext);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import '../../../media/media_playlist.dart';
|
||||
import '../../../mixins/library_tab_focus_mixin.dart';
|
||||
import '../../../mixins/paginated_item_loader.dart';
|
||||
import '../../../services/settings_service.dart';
|
||||
import '../../../utils/app_logger.dart';
|
||||
import '../../../utils/error_message_utils.dart';
|
||||
import '../../../utils/layout_constants.dart';
|
||||
import '../../../utils/library_refresh_notifier.dart';
|
||||
import '../../../utils/media_server_http_client.dart';
|
||||
@@ -84,6 +84,7 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
||||
|
||||
@override
|
||||
Future<void> loadItems() async {
|
||||
String? loadErrorMessage;
|
||||
await loadInitialPaginatedItems(
|
||||
pageSize: _pageSize,
|
||||
resetViewState: () {
|
||||
@@ -95,8 +96,8 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
||||
items = loaded;
|
||||
isLoading = false;
|
||||
},
|
||||
applyError: (error, _) {
|
||||
errorMessage = 'Failed to load $errorContext: ${error.toString()}';
|
||||
applyError: (error, stackTrace) {
|
||||
errorMessage = loadErrorMessage ?? t.errors.unableToLoad(context: errorContext);
|
||||
isLoading = false;
|
||||
},
|
||||
onLoaded: (_, _) {
|
||||
@@ -109,7 +110,7 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
||||
}
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
appLogger.e('Error loading $errorContext', error: error, stackTrace: stackTrace);
|
||||
loadErrorMessage = localizedLoadErrorMessage(error, stackTrace, context: errorContext);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,9 +59,6 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
String? get itemServerId => widget.library.serverId;
|
||||
|
||||
@override
|
||||
String? get watchStateServerId => widget.library.serverId;
|
||||
|
||||
@@ -106,12 +103,12 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
}
|
||||
|
||||
@override
|
||||
void updateItemInLists(String itemId, MediaItem updatedItem) {
|
||||
void updateItemInLists(String sourceGlobalKey, MediaItem updatedItem) {
|
||||
// Update the item in any hub that contains it. MediaHub items are
|
||||
// immutable lists; rebuild the affected hub in-place.
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final hub = items[i];
|
||||
final itemIndex = hub.items.indexWhere((item) => item.id == itemId);
|
||||
final itemIndex = hub.items.indexWhere((item) => item.globalKey == sourceGlobalKey);
|
||||
if (itemIndex != -1) {
|
||||
final newItems = List<MediaItem>.from(hub.items);
|
||||
newItems[itemIndex] = updatedItem;
|
||||
@@ -131,16 +128,16 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
}
|
||||
|
||||
final affectedIds = {event.itemId, ...event.parentChain};
|
||||
final refreshIds = <String>{};
|
||||
final refreshItems = <String, MediaItem>{};
|
||||
for (final hub in items) {
|
||||
for (final item in hub.items) {
|
||||
if (affectedIds.contains(item.id)) {
|
||||
refreshIds.add(item.id);
|
||||
refreshItems[item.globalKey] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final itemId in refreshIds) {
|
||||
unawaited(updateItem(itemId));
|
||||
for (final item in refreshItems.values) {
|
||||
unawaited(updateItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user