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 '../../media/media_server_client.dart';
|
||||||
import '../../providers/hidden_libraries_provider.dart';
|
import '../../providers/hidden_libraries_provider.dart';
|
||||||
import '../../providers/libraries_provider.dart';
|
import '../../providers/libraries_provider.dart';
|
||||||
|
import '../../providers/settings_provider.dart';
|
||||||
import '../../utils/app_logger.dart';
|
import '../../utils/app_logger.dart';
|
||||||
import '../../utils/dialogs.dart';
|
import '../../utils/dialogs.dart';
|
||||||
|
import '../../utils/library_grouping.dart';
|
||||||
import '../../utils/platform_detector.dart';
|
import '../../utils/platform_detector.dart';
|
||||||
import '../../utils/provider_extensions.dart';
|
import '../../utils/provider_extensions.dart';
|
||||||
import '../../utils/snackbar_helper.dart';
|
import '../../utils/snackbar_helper.dart';
|
||||||
import '../../utils/content_utils.dart';
|
import '../../utils/content_utils.dart';
|
||||||
|
import '../../widgets/backend_badge.dart';
|
||||||
import '../../widgets/desktop_app_bar.dart';
|
import '../../widgets/desktop_app_bar.dart';
|
||||||
import '../../widgets/overlay_sheet.dart';
|
import '../../widgets/overlay_sheet.dart';
|
||||||
import '../../services/storage_service.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();
|
return nameCounts.entries.where((e) => e.value > 1).map((e) => e.key).toSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build dropdown menu items with server subtitle for non-unique names
|
Widget _buildLibraryServerLabel(
|
||||||
List<PopupMenuEntry<String>> _buildGroupedLibraryMenuItems(List<MediaLibrary> visibleLibraries) {
|
MediaLibrary library,
|
||||||
// Find which library names are not unique
|
TextStyle? style, {
|
||||||
final nonUniqueNames = _getNonUniqueLibraryNames(visibleLibraries);
|
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 isSelected = library.globalKey == _selectedLibraryGlobalKey;
|
||||||
final showServerName = nonUniqueNames.contains(library.title) && library.serverName != null;
|
|
||||||
|
|
||||||
return PopupMenuItem<String>(
|
return PopupMenuItem<String>(
|
||||||
value: library.globalKey,
|
value: library.globalKey,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -795,12 +831,14 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showServerName)
|
if (showServerName)
|
||||||
Text(
|
_buildLibraryServerLabel(
|
||||||
library.serverName!,
|
library,
|
||||||
style: TextStyle(
|
TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
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();
|
}).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
|
/// Build the app bar title - either dropdown on mobile or simple title on desktop
|
||||||
Widget _buildAppBarTitle(List<MediaLibrary> visibleLibraries, MediaLibrary? selectedLibrary) {
|
Widget _buildAppBarTitle(List<MediaLibrary> visibleLibraries, MediaLibrary? selectedLibrary) {
|
||||||
// No selection at all, or visible list is empty AND we're not browsing a hidden library
|
// 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.where((lib) => lib.globalKey == _selectedLibraryGlobalKey).firstOrNull ??
|
||||||
visibleLibraries.firstOrNull;
|
visibleLibraries.firstOrNull;
|
||||||
if (selectedLibrary == null) return Text(t.libraries.title);
|
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>(
|
return PopupMenuButton<String>(
|
||||||
key: _libraryDropdownKey,
|
key: _libraryDropdownKey,
|
||||||
@@ -854,7 +920,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
|||||||
onSelected: (libraryGlobalKey) {
|
onSelected: (libraryGlobalKey) {
|
||||||
_loadLibraryContent(libraryGlobalKey);
|
_loadLibraryContent(libraryGlobalKey);
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => _buildGroupedLibraryMenuItems(visibleLibraries),
|
itemBuilder: (context) => _buildGroupedLibraryMenuItems(visibleLibraries, showServerHeaders: showServerHeaders),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -868,11 +934,12 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(selectedLibrary.title, style: Theme.of(context).textTheme.titleMedium),
|
Text(selectedLibrary.title, style: Theme.of(context).textTheme.titleMedium),
|
||||||
Text(
|
_buildLibraryServerLabel(
|
||||||
selectedLibrary.serverName!,
|
selectedLibrary,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
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/libraries_provider.dart';
|
||||||
import '../providers/settings_provider.dart';
|
import '../providers/settings_provider.dart';
|
||||||
import '../utils/platform_detector.dart';
|
import '../utils/platform_detector.dart';
|
||||||
|
import '../utils/library_grouping.dart';
|
||||||
import '../providers/multi_server_provider.dart';
|
import '../providers/multi_server_provider.dart';
|
||||||
import '../services/fullscreen_state_manager.dart';
|
import '../services/fullscreen_state_manager.dart';
|
||||||
import '../theme/mono_tokens.dart';
|
import '../theme/mono_tokens.dart';
|
||||||
@@ -284,7 +285,7 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
|||||||
if (!showServerHeaders) {
|
if (!showServerHeaders) {
|
||||||
return libs.map((lib) => lib.globalKey).toList();
|
return libs.map((lib) => lib.globalKey).toList();
|
||||||
}
|
}
|
||||||
final grouped = _groupByFirstAppearance(libs);
|
final grouped = groupLibrariesByFirstAppearance(libs);
|
||||||
final result = <String>[];
|
final result = <String>[];
|
||||||
for (final serverKey in grouped.serverOrder) {
|
for (final serverKey in grouped.serverOrder) {
|
||||||
if (serverKey.isNotEmpty) {
|
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();
|
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}) {
|
Widget _buildLibraryGroupedColumn(List<MediaLibrary> libraries, dynamic t, {required bool showServerHeaders}) {
|
||||||
if (!showServerHeaders) {
|
if (!showServerHeaders) {
|
||||||
final nonUniqueNames = _getNonUniqueLibraryNames(libraries);
|
final nonUniqueNames = _getNonUniqueLibraryNames(libraries);
|
||||||
@@ -876,7 +857,7 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final grouped = _groupByFirstAppearance(libraries);
|
final grouped = groupLibrariesByFirstAppearance(libraries);
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
for (final serverKey in grouped.serverOrder) {
|
for (final serverKey in grouped.serverOrder) {
|
||||||
final bucket = grouped.byServer[serverKey]!;
|
final bucket = grouped.byServer[serverKey]!;
|
||||||
|
|||||||
Reference in New Issue
Block a user