From 6840054a41400574ccf2feb8361092a1a82ffbfd Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 17 Jan 2026 15:51:34 +0800 Subject: [PATCH] :lipstick: Better dashboard customization --- assets/i18n/en-US.json | 32 +- lib/pods/config.dart | 6 + lib/screens/dashboard/dash.dart | 32 +- lib/screens/dashboard/dash_customize.dart | 434 ++++++++++++++-------- 4 files changed, 327 insertions(+), 177 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 3884f7ce..9ce24e46 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1598,5 +1598,33 @@ "sidebar": "Sidebar", "dropFilesHere": "Drop your files here", "dragAndDropToAttach": "Drag your files here to attach it", - "customize": "Customize" -} + "customize": "Customize", + "dashboardCustomizeTitle": "Customize Dashboard", + "dashboardTabVertical": "Vertical", + "dashboardTabHorizontal": "Horizontal", + "dashboardLayoutVertical": "Vertical Layout", + "dashboardLayoutHorizontal": "Horizontal Layout", + "dashboardAvailableCards": "Available Cards", + "dashboardDisplaySettings": "Display Settings", + "dashboardShowSearchBar": "Show Search Bar", + "dashboardShowClockAndCountdown": "Show Clock and Countdown", + "dashboardResetToDefaults": "Reset to Defaults", + "dashboardResetToDefaultsSubtitle": "Restore default dashboard layout and settings", + "dashboardResetConfirmMessage": "This will restore the dashboard to its default layout and settings. This action cannot be undone.", + "dashboardResetConfirmTitle": "Reset Dashboard", + "dashboardCardCheckIn": "Check In", + "dashboardCardFortuneGraph": "Fortune Graph", + "dashboardCardFortune": "Fortune", + "dashboardCardFeaturedPosts": "Featured Posts", + "dashboardCardFriends": "Friends", + "dashboardCardNotifications": "Notifications", + "dashboardCardChats": "Chats", + "dashboardCardActivityColumn": "Activity Column", + "dashboardCardPostsColumn": "Posts Column", + "dashboardCardSocialColumn": "Social Column", + "dashboardCardChatsColumn": "Chats Column", + "dashboardCardActivityColumnDescription": "Check In, Fortune Graph & Fortune", + "dashboardCardPostsColumnDescription": "Featured Posts", + "dashboardCardSocialColumnDescription": "Friends & Notifications", + "dashboardCardChatsColumnDescription": "Recent Chats" +} \ No newline at end of file diff --git a/lib/pods/config.dart b/lib/pods/config.dart index f92dcf94..46faf414 100644 --- a/lib/pods/config.dart +++ b/lib/pods/config.dart @@ -363,6 +363,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { } state = state.copyWith(dashboardConfig: value); } + + void resetDashboardConfig() { + final prefs = ref.read(sharedPreferencesProvider); + prefs.remove(kAppDashboardConfig); + state = state.copyWith(dashboardConfig: null); + } } final updateInfoProvider = diff --git a/lib/screens/dashboard/dash.dart b/lib/screens/dashboard/dash.dart index d1a0fd72..5050416e 100644 --- a/lib/screens/dashboard/dash.dart +++ b/lib/screens/dashboard/dash.dart @@ -144,8 +144,6 @@ class DashboardRenderer { } } - - class DashboardGrid extends HookConsumerWidget { const DashboardGrid({super.key}); @@ -273,7 +271,7 @@ class DashboardGrid extends HookConsumerWidget { style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.primary, ), - ), + ).tr(), style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surface, padding: const EdgeInsets.symmetric( @@ -337,17 +335,15 @@ class _DashboardGridWide extends HookConsumerWidget { } // Add configured columns in the specified order - final horizontalLayouts = appSettings.dashboardConfig?.horizontalLayouts ?? + final horizontalLayouts = + appSettings.dashboardConfig?.horizontalLayouts ?? ['activityColumn', 'postsColumn', 'socialColumn', 'chatsColumn']; for (final columnId in horizontalLayouts) { children.add(DashboardRenderer.buildColumn(columnId, ref)); } - return Row( - spacing: 16, - children: children, - ); + return Row(spacing: 16, children: children); } } @@ -367,17 +363,23 @@ class _DashboardGridNarrow extends HookConsumerWidget { } // Add configured cards in the specified order - final verticalLayouts = appSettings.dashboardConfig?.verticalLayouts ?? - ['checkIn', 'fortuneCard', 'postFeatured', 'friendsOverview', 'notifications', 'chatList', 'fortuneGraph']; + final verticalLayouts = + appSettings.dashboardConfig?.verticalLayouts ?? + [ + 'checkIn', + 'fortuneCard', + 'postFeatured', + 'friendsOverview', + 'notifications', + 'chatList', + 'fortuneGraph', + ]; for (final cardId in verticalLayouts) { children.add(DashboardRenderer.buildCard(cardId, ref)); } - return Column( - spacing: 16, - children: children, - ); + return Column(spacing: 16, children: children); } } @@ -784,4 +786,4 @@ class _UnauthorizedCard extends HookConsumerWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/dashboard/dash_customize.dart b/lib/screens/dashboard/dash_customize.dart index d9743b1f..4ca0f7d4 100644 --- a/lib/screens/dashboard/dash_customize.dart +++ b/lib/screens/dashboard/dash_customize.dart @@ -1,28 +1,69 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:island/pods/config.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:styled_widget/styled_widget.dart'; class DashboardCustomizationSheet extends HookConsumerWidget { const DashboardCustomizationSheet({super.key}); - static const Map> _cardMetadata = { - // Vertical layout cards - 'checkIn': {'name': 'Check In', 'icon': Symbols.check_circle}, - 'fortuneGraph': {'name': 'Fortune Graph', 'icon': Symbols.show_chart}, - 'fortuneCard': {'name': 'Fortune', 'icon': Symbols.lightbulb}, - 'postFeatured': {'name': 'Featured Posts', 'icon': Symbols.article}, - 'friendsOverview': {'name': 'Friends', 'icon': Symbols.group}, - 'notifications': {'name': 'Notifications', 'icon': Symbols.notifications}, - 'chatList': {'name': 'Chats', 'icon': Symbols.chat}, - // Horizontal layout columns - 'activityColumn': {'name': 'Activity Column', 'icon': Symbols.dashboard, 'description': 'Check In, Fortune Graph & Fortune'}, - 'postsColumn': {'name': 'Posts Column', 'icon': Symbols.article, 'description': 'Featured Posts'}, - 'socialColumn': {'name': 'Social Column', 'icon': Symbols.group, 'description': 'Friends & Notifications'}, - 'chatsColumn': {'name': 'Chats Column', 'icon': Symbols.chat, 'description': 'Recent Chats'}, - }; + static Map> _getCardMetadata( + BuildContext context, + ) { + return { + // Vertical layout cards + 'checkIn': { + 'name': 'dashboardCardCheckIn'.tr(), + 'icon': Symbols.check_circle, + }, + 'fortuneGraph': { + 'name': 'dashboardCardFortuneGraph'.tr(), + 'icon': Symbols.show_chart, + }, + 'fortuneCard': { + 'name': 'dashboardCardFortune'.tr(), + 'icon': Symbols.lightbulb, + }, + 'postFeatured': { + 'name': 'dashboardCardFeaturedPosts'.tr(), + 'icon': Symbols.article, + }, + 'friendsOverview': { + 'name': 'dashboardCardFriends'.tr(), + 'icon': Symbols.group, + }, + 'notifications': { + 'name': 'dashboardCardNotifications'.tr(), + 'icon': Symbols.notifications, + }, + 'chatList': {'name': 'dashboardCardChats'.tr(), 'icon': Symbols.chat}, + // Horizontal layout columns + 'activityColumn': { + 'name': 'dashboardCardActivityColumn'.tr(), + 'icon': Symbols.dashboard, + 'description': 'dashboardCardActivityColumnDescription'.tr(), + }, + 'postsColumn': { + 'name': 'dashboardCardPostsColumn'.tr(), + 'icon': Symbols.article, + 'description': 'dashboardCardPostsColumnDescription'.tr(), + }, + 'socialColumn': { + 'name': 'dashboardCardSocialColumn'.tr(), + 'icon': Symbols.group, + 'description': 'dashboardCardSocialColumnDescription'.tr(), + }, + 'chatsColumn': { + 'name': 'dashboardCardChatsColumn'.tr(), + 'icon': Symbols.chat, + 'description': 'dashboardCardChatsColumnDescription'.tr(), + }, + }; + } @override Widget build(BuildContext context, WidgetRef ref) { @@ -31,15 +72,18 @@ class DashboardCustomizationSheet extends HookConsumerWidget { // Local state for editing final verticalLayouts = useState>( - (appSettings.dashboardConfig?.verticalLayouts ?? [ - 'checkIn', - 'fortuneCard', - 'postFeatured', - 'friendsOverview', - 'notifications', - 'chatList', - 'fortuneGraph', - ]).where((id) => id != 'accountUnactivated').toList(), + (appSettings.dashboardConfig?.verticalLayouts ?? + [ + 'checkIn', + 'fortuneCard', + 'postFeatured', + 'friendsOverview', + 'notifications', + 'chatList', + 'fortuneGraph', + ]) + .where((id) => id != 'accountUnactivated') + .toList(), ); final horizontalLayouts = useState>( @@ -67,69 +111,54 @@ class DashboardCustomizationSheet extends HookConsumerWidget { } return SheetScaffold( - titleText: 'Customize Dashboard', - actions: [TextButton(onPressed: saveConfig, child: const Text('Save'))], - child: Column( - children: [ - TabBar( - controller: tabController, - tabs: const [ - Tab(text: 'Vertical'), - Tab(text: 'Horizontal'), - ], - ), - Expanded( - child: TabBarView( + titleText: 'dashboardCustomizeTitle'.tr(), + actions: [IconButton(onPressed: saveConfig, icon: Icon(Symbols.save))], + child: DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( controller: tabController, - children: [ - // Vertical layout - _buildLayoutEditor(context, 'Vertical Layout', verticalLayouts, false), - // Horizontal layout - _buildLayoutEditor( - context, - 'Horizontal Layout', - horizontalLayouts, - true, - ), + tabs: [ + Tab(text: 'dashboardTabVertical'.tr()), + Tab(text: 'dashboardTabHorizontal'.tr()), ], ), - ), - const Divider(), - // Settings checkboxes - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Display Settings', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + Expanded( + child: CustomScrollView( + slivers: [ + SliverFillRemaining( + child: TabBarView( + controller: tabController, + children: [ + // Vertical layout + _buildSliverLayoutEditor( + context, + ref, + 'dashboardLayoutVertical'.tr(), + verticalLayouts, + false, + showSearchBar, + showClockAndCountdown, + ), + // Horizontal layout + _buildSliverLayoutEditor( + context, + ref, + 'dashboardLayoutHorizontal'.tr(), + horizontalLayouts, + true, + showSearchBar, + showClockAndCountdown, + ), + ], + ), ), - ), - const SizedBox(height: 16), - CheckboxListTile( - title: const Text('Show Search Bar'), - value: showSearchBar.value, - onChanged: (value) { - if (value != null) { - showSearchBar.value = value; - } - }, - ), - CheckboxListTile( - title: const Text('Show Clock and Countdown'), - value: showClockAndCountdown.value, - onChanged: (value) { - if (value != null) { - showClockAndCountdown.value = value; - } - }, - ), - ], + ], + ), ), - ), - ], + ], + ), ), ); } @@ -150,78 +179,80 @@ class DashboardCustomizationSheet extends HookConsumerWidget { return ['activityColumn', 'postsColumn', 'socialColumn', 'chatsColumn']; } - Widget _buildLayoutEditor( + Widget _buildSliverLayoutEditor( BuildContext context, + WidgetRef ref, String title, ValueNotifier> layouts, bool isHorizontal, + ValueNotifier showSearchBar, + ValueNotifier showClockAndCountdown, ) { + final cardMetadata = _getCardMetadata(context); // Filter available cards based on layout mode final relevantCards = isHorizontal - ? _cardMetadata.entries.where((entry) => entry.key.contains('Column')).map((e) => e.key).toList() - : _cardMetadata.entries.where((entry) => !entry.key.contains('Column')).map((e) => e.key).toList(); + ? cardMetadata.entries + .where((entry) => entry.key.contains('Column')) + .map((e) => e.key) + .toList() + : cardMetadata.entries + .where((entry) => !entry.key.contains('Column')) + .map((e) => e.key) + .toList(); - final availableCards = - relevantCards.where((cardId) => !layouts.value.contains(cardId)).toList(); + final availableCards = relevantCards + .where((cardId) => !layouts.value.contains(cardId)) + .toList(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16), + return CustomScrollView( + slivers: [ + // Title + SliverToBoxAdapter( child: Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), - ), + ).padding(horizontal: 24, top: 16, bottom: 8), ), // Reorderable list for cards - Expanded( - child: ReorderableListView.builder( - buildDefaultDragHandles: false, - itemCount: layouts.value.length, - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final item = layouts.value.removeAt(oldIndex); - layouts.value.insert(newIndex, item); - // Trigger rebuild - layouts.value = List.from(layouts.value); - }, - itemBuilder: (context, index) { - final cardId = layouts.value[index]; - final metadata = - _cardMetadata[cardId] ?? - {'name': cardId, 'icon': Symbols.help}; + SliverReorderableList( + itemCount: layouts.value.length, + itemBuilder: (context, index) { + final cardId = layouts.value[index]; + final metadata = + cardMetadata[cardId] ?? {'name': cardId, 'icon': Symbols.help}; - return Card( - key: ValueKey(cardId), + return ReorderableDragStartListener( + key: ValueKey(cardId), + index: index, + child: Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile( + dense: true, leading: Icon( metadata['icon'] as IconData, color: Theme.of(context).colorScheme.primary, ), + contentPadding: const EdgeInsets.fromLTRB(16, 0, 8, 0), title: Text(metadata['name'] as String), subtitle: isHorizontal && metadata.containsKey('description') ? Text( metadata['description'] as String, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), ) : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - ReorderableDragStartListener( - index: index, - child: Icon( - Symbols.drag_handle, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + Icon( + Symbols.drag_handle, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), IconButton( icon: Icon( @@ -230,55 +261,138 @@ class DashboardCustomizationSheet extends HookConsumerWidget { color: Theme.of(context).colorScheme.error, ), onPressed: () { - layouts.value = layouts.value.where((id) => id != cardId).toList(); + layouts.value = layouts.value + .where((id) => id != cardId) + .toList(); }, ), ], ), ), - ); - }, - ), + ), + ); + }, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final item = layouts.value.removeAt(oldIndex); + layouts.value.insert(newIndex, item); + layouts.value = List.from(layouts.value); + }, ), // Available cards to add back if (availableCards.isNotEmpty) - Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Available Cards', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, + SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'dashboardAvailableCards'.tr(), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: availableCards.map((cardId) { - final metadata = - _cardMetadata[cardId] ?? - {'name': cardId, 'icon': Symbols.help}; - return ActionChip( - avatar: Icon( - metadata['icon'] as IconData, - size: 16, - color: Theme.of(context).colorScheme.primary, - ), - label: Text(metadata['name'] as String), - onPressed: () { - layouts.value = [...layouts.value, cardId]; - }, - ); - }).toList(), - ), - ], + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: availableCards.map((cardId) { + final metadata = + cardMetadata[cardId] ?? + {'name': cardId, 'icon': Symbols.help}; + return ActionChip( + avatar: Icon( + metadata['icon'] as IconData, + size: 16, + color: Theme.of(context).colorScheme.primary, + ), + label: Text(metadata['name'] as String), + onPressed: () { + layouts.value = [...layouts.value, cardId]; + }, + ); + }).toList(), + ), + ], + ), ), ), + // Divider + const SliverToBoxAdapter(child: Divider()), + // Reset tile + SliverToBoxAdapter( + child: ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: Icon( + Symbols.restore, + color: Theme.of(context).colorScheme.primary, + ), + title: Text('dashboardResetToDefaults'.tr()), + subtitle: Text('dashboardResetToDefaultsSubtitle'.tr()), + trailing: Icon( + Symbols.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onTap: () async { + final confirmed = await showConfirmAlert( + 'dashboardResetConfirmMessage'.tr(), + 'dashboardResetConfirmTitle'.tr(), + isDanger: true, + ); + + if (confirmed) { + ref.read(appSettingsProvider.notifier).resetDashboardConfig(); + if (context.mounted) { + Navigator.of(context).pop(); // Close the sheet + } + } + }, + ), + ), + // Divider + const SliverToBoxAdapter(child: Divider()), + // Settings checkboxes + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'dashboardDisplaySettings'.tr(), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ).padding(horizontal: 24, top: 12, bottom: 8), + CheckboxListTile( + dense: true, + title: Text('dashboardShowSearchBar'.tr()), + value: showSearchBar.value, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + onChanged: (value) { + if (value != null) { + showSearchBar.value = value; + } + }, + ), + CheckboxListTile( + dense: true, + title: Text('dashboardShowClockAndCountdown'.tr()), + value: showClockAndCountdown.value, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + onChanged: (value) { + if (value != null) { + showClockAndCountdown.value = value; + } + }, + ), + ], + ), + ), ], ); } -} \ No newline at end of file +}