fix: group mobile library dropdown by server
This commit is contained in:
@@ -19,12 +19,15 @@ import '../../media/media_library.dart';
|
||||
import '../../media/media_server_client.dart';
|
||||
import '../../providers/hidden_libraries_provider.dart';
|
||||
import '../../providers/libraries_provider.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/library_grouping.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../utils/provider_extensions.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../utils/content_utils.dart';
|
||||
import '../../widgets/backend_badge.dart';
|
||||
import '../../widgets/desktop_app_bar.dart';
|
||||
import '../../widgets/overlay_sheet.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
@@ -762,15 +765,48 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
return nameCounts.entries.where((e) => e.value > 1).map((e) => e.key).toSet();
|
||||
}
|
||||
|
||||
/// Build dropdown menu items with server subtitle for non-unique names
|
||||
List<PopupMenuEntry<String>> _buildGroupedLibraryMenuItems(List<MediaLibrary> visibleLibraries) {
|
||||
// Find which library names are not unique
|
||||
final nonUniqueNames = _getNonUniqueLibraryNames(visibleLibraries);
|
||||
Widget _buildLibraryServerLabel(
|
||||
MediaLibrary library,
|
||||
TextStyle? style, {
|
||||
double badgeSize = 11,
|
||||
bool constrainText = false,
|
||||
String? fallbackServerName,
|
||||
}) {
|
||||
final serverName = library.serverName ?? fallbackServerName;
|
||||
if (serverName == null || serverName.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return visibleLibraries.map((library) {
|
||||
final text = Text(serverName, style: style, overflow: TextOverflow.ellipsis);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BackendBadge(backend: library.backend, size: badgeSize, color: style?.color),
|
||||
const SizedBox(width: 4),
|
||||
if (constrainText) Flexible(child: text) else text,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<String> _buildLibraryServerHeaderMenuItem(MediaLibrary library, String serverKey) {
|
||||
final style = Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.4,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.65),
|
||||
);
|
||||
return PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
height: 32,
|
||||
child: _buildLibraryServerLabel(
|
||||
library,
|
||||
style,
|
||||
badgeSize: 12,
|
||||
constrainText: true,
|
||||
fallbackServerName: serverKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<String> _buildLibraryMenuItem(MediaLibrary library, {required bool showServerName}) {
|
||||
final isSelected = library.globalKey == _selectedLibraryGlobalKey;
|
||||
final showServerName = nonUniqueNames.contains(library.title) && library.serverName != null;
|
||||
|
||||
return PopupMenuItem<String>(
|
||||
value: library.globalKey,
|
||||
child: Row(
|
||||
@@ -795,12 +831,14 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
),
|
||||
),
|
||||
if (showServerName)
|
||||
Text(
|
||||
library.serverName!,
|
||||
style: TextStyle(
|
||||
_buildLibraryServerLabel(
|
||||
library,
|
||||
TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
badgeSize: 10,
|
||||
constrainText: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -808,9 +846,35 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build dropdown menu items with server subtitle when needed for clarity.
|
||||
List<PopupMenuEntry<String>> _buildGroupedLibraryMenuItems(
|
||||
List<MediaLibrary> visibleLibraries, {
|
||||
required bool showServerHeaders,
|
||||
}) {
|
||||
if (!showServerHeaders) {
|
||||
final nonUniqueNames = _getNonUniqueLibraryNames(visibleLibraries);
|
||||
return visibleLibraries.map((library) {
|
||||
final showServerName = library.serverName != null && nonUniqueNames.contains(library.title);
|
||||
return _buildLibraryMenuItem(library, showServerName: showServerName);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final grouped = groupLibrariesByFirstAppearance(visibleLibraries);
|
||||
final menuItems = <PopupMenuEntry<String>>[];
|
||||
for (final serverKey in grouped.serverOrder) {
|
||||
final bucket = grouped.byServer[serverKey]!;
|
||||
if (serverKey.isNotEmpty) {
|
||||
menuItems.add(_buildLibraryServerHeaderMenuItem(bucket.first, serverKey));
|
||||
}
|
||||
for (final library in bucket) {
|
||||
menuItems.add(_buildLibraryMenuItem(library, showServerName: false));
|
||||
}
|
||||
}
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
/// Build the app bar title - either dropdown on mobile or simple title on desktop
|
||||
Widget _buildAppBarTitle(List<MediaLibrary> visibleLibraries, MediaLibrary? selectedLibrary) {
|
||||
// No selection at all, or visible list is empty AND we're not browsing a hidden library
|
||||
@@ -846,6 +910,8 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
visibleLibraries.where((lib) => lib.globalKey == _selectedLibraryGlobalKey).firstOrNull ??
|
||||
visibleLibraries.firstOrNull;
|
||||
if (selectedLibrary == null) return Text(t.libraries.title);
|
||||
final groupByServerSetting = context.select<SettingsProvider, bool>((p) => p.groupLibrariesByServer);
|
||||
final showServerHeaders = _hasMultipleServers(visibleLibraries) && groupByServerSetting;
|
||||
|
||||
return PopupMenuButton<String>(
|
||||
key: _libraryDropdownKey,
|
||||
@@ -854,7 +920,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
onSelected: (libraryGlobalKey) {
|
||||
_loadLibraryContent(libraryGlobalKey);
|
||||
},
|
||||
itemBuilder: (context) => _buildGroupedLibraryMenuItems(visibleLibraries),
|
||||
itemBuilder: (context) => _buildGroupedLibraryMenuItems(visibleLibraries, showServerHeaders: showServerHeaders),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
@@ -868,11 +934,12 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(selectedLibrary.title, style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(
|
||||
selectedLibrary.serverName!,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
_buildLibraryServerLabel(
|
||||
selectedLibrary,
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
badgeSize: 10,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import '../media/media_library.dart';
|
||||
|
||||
typedef LibraryServerGroups = ({List<String> serverOrder, Map<String, List<MediaLibrary>> byServer});
|
||||
|
||||
/// Groups libraries by server while preserving each server's first appearance in
|
||||
/// the provided list. Libraries without a server id are placed in the empty-key
|
||||
/// bucket at their first occurrence.
|
||||
LibraryServerGroups groupLibrariesByFirstAppearance(List<MediaLibrary> libraries) {
|
||||
final order = <String>[];
|
||||
final byServer = <String, List<MediaLibrary>>{};
|
||||
for (final lib in libraries) {
|
||||
final key = lib.serverId ?? '';
|
||||
if (!byServer.containsKey(key)) {
|
||||
order.add(key);
|
||||
byServer[key] = [];
|
||||
}
|
||||
byServer[key]!.add(lib);
|
||||
}
|
||||
return (serverOrder: order, byServer: byServer);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import '../providers/hidden_libraries_provider.dart';
|
||||
import '../providers/libraries_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import '../utils/library_grouping.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../services/fullscreen_state_manager.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
@@ -284,7 +285,7 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
if (!showServerHeaders) {
|
||||
return libs.map((lib) => lib.globalKey).toList();
|
||||
}
|
||||
final grouped = _groupByFirstAppearance(libs);
|
||||
final grouped = groupLibrariesByFirstAppearance(libs);
|
||||
final result = <String>[];
|
||||
for (final serverKey in grouped.serverOrder) {
|
||||
if (serverKey.isNotEmpty) {
|
||||
@@ -844,26 +845,6 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
return nameCounts.entries.where((e) => e.value > 1).map((e) => e.key).toSet();
|
||||
}
|
||||
|
||||
/// First-appearance grouping: walk libraries once, recording each serverId's
|
||||
/// first-seen position and bucketing libraries underneath. Returns the server
|
||||
/// order plus a per-server library list. Libraries without a serverId end up
|
||||
/// in a synthetic '' bucket appearing at their first occurrence.
|
||||
({List<String> serverOrder, Map<String, List<MediaLibrary>> byServer}) _groupByFirstAppearance(
|
||||
List<MediaLibrary> libs,
|
||||
) {
|
||||
final order = <String>[];
|
||||
final byServer = <String, List<MediaLibrary>>{};
|
||||
for (final lib in libs) {
|
||||
final key = lib.serverId ?? '';
|
||||
if (!byServer.containsKey(key)) {
|
||||
order.add(key);
|
||||
byServer[key] = [];
|
||||
}
|
||||
byServer[key]!.add(lib);
|
||||
}
|
||||
return (serverOrder: order, byServer: byServer);
|
||||
}
|
||||
|
||||
Widget _buildLibraryGroupedColumn(List<MediaLibrary> libraries, dynamic t, {required bool showServerHeaders}) {
|
||||
if (!showServerHeaders) {
|
||||
final nonUniqueNames = _getNonUniqueLibraryNames(libraries);
|
||||
@@ -876,7 +857,7 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
);
|
||||
}
|
||||
|
||||
final grouped = _groupByFirstAppearance(libraries);
|
||||
final grouped = groupLibrariesByFirstAppearance(libraries);
|
||||
final children = <Widget>[];
|
||||
for (final serverKey in grouped.serverOrder) {
|
||||
final bucket = grouped.byServer[serverKey]!;
|
||||
|
||||
Reference in New Issue
Block a user