💄 Better dashboard customization

This commit is contained in:
2026-01-17 15:51:34 +08:00
parent 6a0f351114
commit 6840054a41
4 changed files with 327 additions and 177 deletions

View File

@@ -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"
}

View File

@@ -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 =

View File

@@ -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 {
),
);
}
}
}

View File

@@ -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<String, Map<String, dynamic>> _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<String, Map<String, dynamic>> _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<List<String>>(
(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<List<String>>(
@@ -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<List<String>> layouts,
bool isHorizontal,
ValueNotifier<bool> showSearchBar,
ValueNotifier<bool> 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;
}
},
),
],
),
),
],
);
}
}
}