From ad9fb0719a75abacd7419cbd126dc4652005433f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 12 Oct 2025 20:45:10 +0800 Subject: [PATCH] :bug: Bug fixes and optimization --- assets/i18n/en-US.json | 3 +- lib/screens/creators/hub.dart | 212 ++++++++++++++---- lib/widgets/app_scaffold.dart | 12 +- lib/widgets/chat/chat_input.dart | 5 +- lib/widgets/post/compose_form_fields.dart | 3 - .../{picker.dart => sticker_picker.dart} | 6 +- .../{picker.g.dart => sticker_picker.g.dart} | 2 +- 7 files changed, 186 insertions(+), 57 deletions(-) rename lib/widgets/stickers/{picker.dart => sticker_picker.dart} (98%) rename lib/widgets/stickers/{picker.g.dart => sticker_picker.g.dart} (97%) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index b3cce145..65731860 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1220,5 +1220,6 @@ "noStickersInPack": "This pack does not contains stickers", "noStickerPacks": "No Sticker Packs", "refresh": "Refresh", - "spoiler": "Spoiler" + "spoiler": "Spoiler", + "activityHeatmap": "Activity Heatmap" } diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart index 552f40c9..32ac30dd 100644 --- a/lib/screens/creators/hub.dart +++ b/lib/screens/creators/hub.dart @@ -1204,19 +1204,22 @@ class _PublisherHeatmapWidget extends StatelessWidget { @override Widget build(BuildContext context) { - // Find min and max dates - final dates = heatmap.items.map((e) => e.date).toList(); - if (dates.isEmpty) return const SizedBox.shrink(); + // Generate exactly 365 days ending at current date + final now = DateTime.now(); - final minDate = dates.reduce((a, b) => a.isBefore(b) ? a : b); - final maxDate = dates.reduce((a, b) => a.isAfter(b) ? a : b); + // Start from exactly 365 days ago + final startDate = now.subtract(const Duration(days: 365)); + // End at current date + final endDate = now; - // Find monday of the week containing minDate - final startMonday = minDate.subtract(Duration(days: minDate.weekday - 1)); - // Find sunday of the week containing maxDate - final endSunday = maxDate.add(Duration(days: 7 - maxDate.weekday)); + // Find monday of the week containing start date + final startMonday = startDate.subtract( + Duration(days: startDate.weekday - 1), + ); + // Find sunday of the week containing end date + final endSunday = endDate.add(Duration(days: 7 - endDate.weekday)); - // Generate all weeks + // Generate weeks to cover exactly 365 days final weeks = []; var current = startMonday; while (current.isBefore(endSunday) || current.isAtSameMomentAs(endSunday)) { @@ -1224,45 +1227,98 @@ class _PublisherHeatmapWidget extends StatelessWidget { current = current.add(const Duration(days: 7)); } - // Columns: Mon to Sun - const columns = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - - // Create data map - final dataMap = >{}; + // Create data map for all dates in the range + final dataMap = {}; for (final week in weeks) { - final weekKey = - '${week.year}-${week.month.toString().padLeft(2, '0')}-${week.day.toString().padLeft(2, '0')}'; - dataMap[weekKey] = {}; for (var i = 0; i < 7; i++) { final date = week.add(Duration(days: i)); - final item = heatmap.items.firstWhere( - (e) => - e.date.year == date.year && - e.date.month == date.month && - e.date.day == date.day, - orElse: () => SnPublisherHeatmapItem(date: date, count: 0), + // Only include dates within our 365-day range + if (date.isAfter(startDate.subtract(const Duration(days: 1))) && + date.isBefore(endDate.add(const Duration(days: 1)))) { + final item = heatmap.items.firstWhere( + (e) => + e.date.year == date.year && + e.date.month == date.month && + e.date.day == date.day, + orElse: () => SnPublisherHeatmapItem(date: date, count: 0), + ); + dataMap[date] = item.count.toDouble(); + } + } + } + + // Generate month labels for the top + final monthLabels = []; + final monthPositions = []; + final processedMonths = + {}; // Track processed months to avoid duplicates + + for (final week in weeks) { + final monthKey = '${week.year}-${week.month.toString().padLeft(2, '0')}'; + + // Only process each month once + if (!processedMonths.contains(monthKey)) { + processedMonths.add(monthKey); + + // Find which week this month starts in + final firstDayOfMonth = DateTime(week.year, week.month, 1); + final monthStartMonday = firstDayOfMonth.subtract( + Duration(days: firstDayOfMonth.weekday - 1), ); - dataMap[weekKey]![columns[i]] = item.count.toDouble(); + + final monthStartWeekIndex = weeks.indexWhere( + (w) => + w.year == monthStartMonday.year && + w.month == monthStartMonday.month && + w.day == monthStartMonday.day, + ); + + if (monthStartWeekIndex != -1) { + monthLabels.add(_getMonthAbbreviation(week.month)); + monthPositions.add(monthStartWeekIndex); + } } } final heatmapData = HeatmapData( - rows: + rows: [ + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + 'Sun', + ], // Days of week vertically + columns: weeks .map( (w) => '${w.year}-${w.month.toString().padLeft(2, '0')}-${w.day.toString().padLeft(2, '0')}', ) - .toList(), - columns: columns, + .toList(), // Weeks horizontally items: [ - for (final row in dataMap.entries) - for (final col in row.value.entries) + for (int day = 0; day < 7; day++) // For each day of week (Mon-Sun) + for (final week in weeks) // For each week HeatmapItem( - value: col.value, + value: dataMap[week.add(Duration(days: day))] ?? 0.0, unit: heatmap.unit, - xAxisLabel: col.key, - yAxisLabel: row.key, + xAxisLabel: + '${week.year}-${week.month.toString().padLeft(2, '0')}-${week.day.toString().padLeft(2, '0')}', + yAxisLabel: + day == 0 + ? 'Mon' + : day == 1 + ? 'Tue' + : day == 2 + ? 'Wed' + : day == 3 + ? 'Thu' + : day == 4 + ? 'Fri' + : day == 5 + ? 'Sat' + : 'Sun', ), ], ); @@ -1275,19 +1331,97 @@ class _PublisherHeatmapWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Activity Heatmap', + 'activityHeatmap', style: Theme.of(context).textTheme.titleMedium, + ).tr(), + const Gap(8), + // Month labels row + Row( + children: [ + const SizedBox(width: 30), // Space for day labels + ...monthLabels.asMap().entries.map((entry) { + final monthIndex = entry.key; + final month = entry.value; + + return Expanded( + child: Container( + alignment: Alignment.center, + child: Text( + month, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ), + ); + }), + ], + ), + const Gap(4), + Heatmap( + heatmapData: heatmapData, + rowsVisible: 7, + showXAxisLabels: false, ), const Gap(8), - Heatmap( - showXAxisLabels: false, - showYAxisLabels: false, - heatmapData: heatmapData, - rowsVisible: 5, + // Legend + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'Less', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const Gap(4), + // Color indicators (light to dark green) + ...[ + Colors.green.withOpacity(0.2), + Colors.green.withOpacity(0.4), + Colors.green.withOpacity(0.6), + Colors.green.withOpacity(0.8), + Colors.green, + ].map( + (color) => Container( + width: 8, + height: 8, + margin: const EdgeInsets.symmetric(horizontal: 1), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const Gap(4), + Text( + 'More', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], ), ], ), ), ); } + + String _getMonthAbbreviation(int month) { + const monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + return monthNames[month - 1]; + } } diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 97a1ae78..608ab21e 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -97,7 +97,7 @@ class WindowScaffold extends HookConsumerWidget { LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(), }, child: Actions( - actions: >{PopIntent: PopAction(context)}, + actions: >{PopIntent: PopAction(ref)}, child: Material( color: Theme.of(context).colorScheme.surfaceContainer, child: Stack( @@ -206,7 +206,7 @@ class WindowScaffold extends HookConsumerWidget { LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(), }, child: Actions( - actions: >{PopIntent: PopAction(context)}, + actions: >{PopIntent: PopAction(ref)}, child: Stack( fit: StackFit.expand, children: [Positioned.fill(child: child), _WebSocketIndicator()], @@ -351,14 +351,14 @@ class PopIntent extends Intent { } class PopAction extends Action { - final BuildContext context; + final WidgetRef ref; - PopAction(this.context); + PopAction(this.ref); @override void invoke(PopIntent intent) { - if (context.canPop()) { - context.pop(); + if (ref.watch(routerProvider).canPop()) { + ref.read(routerProvider).pop(); } } } diff --git a/lib/widgets/chat/chat_input.dart b/lib/widgets/chat/chat_input.dart index fa17a5da..9f40add7 100644 --- a/lib/widgets/chat/chat_input.dart +++ b/lib/widgets/chat/chat_input.dart @@ -24,7 +24,7 @@ import "package:material_symbols_icons/material_symbols_icons.dart"; import "package:pasteboard/pasteboard.dart"; import "package:styled_widget/styled_widget.dart"; import "package:material_symbols_icons/symbols.dart"; -import "package:island/widgets/stickers/picker.dart"; +import "package:island/widgets/stickers/sticker_picker.dart"; import "package:island/pods/chat/chat_subscribe.dart"; class ChatInput extends HookConsumerWidget { @@ -524,9 +524,6 @@ class ChatInput extends HookConsumerWidget { hideOnEmpty: true, hideOnLoading: true, debounceDuration: const Duration(milliseconds: 500), - loadingBuilder: (context) => const Text('Loading...'), - errorBuilder: (context, error) => const Text('Error!'), - emptyBuilder: (context) => const Text('No items found!'), ), ), IconButton( diff --git a/lib/widgets/post/compose_form_fields.dart b/lib/widgets/post/compose_form_fields.dart index 41f78c1a..ccf11b94 100644 --- a/lib/widgets/post/compose_form_fields.dart +++ b/lib/widgets/post/compose_form_fields.dart @@ -236,9 +236,6 @@ class ComposeFormFields extends HookConsumerWidget { hideOnEmpty: true, hideOnLoading: true, debounceDuration: const Duration(milliseconds: 500), - loadingBuilder: (context) => const Text('Loading...'), - errorBuilder: (context, error) => const Text('Error!'), - emptyBuilder: (context) => const Text('No items found!'), ), ], ), diff --git a/lib/widgets/stickers/picker.dart b/lib/widgets/stickers/sticker_picker.dart similarity index 98% rename from lib/widgets/stickers/picker.dart rename to lib/widgets/stickers/sticker_picker.dart index 6d235415..b8900071 100644 --- a/lib/widgets/stickers/picker.dart +++ b/lib/widgets/stickers/sticker_picker.dart @@ -14,7 +14,7 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:flutter_popup_card/flutter_popup_card.dart'; import 'package:island/widgets/extended_refresh_indicator.dart'; -part 'picker.g.dart'; +part 'sticker_picker.g.dart'; /// Fetch user-added sticker packs (with stickers) from API: /// GET /sphere/stickers/me @@ -34,7 +34,7 @@ Future> myStickerPacks(Ref ref) async { /// Sticker Picker popover dialog /// - Displays user-owned sticker packs as tabs (chips) /// - Shows grid of stickers in selected pack -/// - On tap, returns placeholder string :{prefix}{slug}: via onPick callback +/// - On tap, returns placeholder string :{prefix}+{slug}: via onPick callback class StickerPicker extends HookConsumerWidget { final void Function(String placeholder) onPick; @@ -63,7 +63,7 @@ class StickerPicker extends HookConsumerWidget { return _PackSwitcher( packs: packs, onPick: (pack, sticker) { - final placeholder = ':${pack.prefix}${sticker.slug}:'; + final placeholder = ':${pack.prefix}+${sticker.slug}:'; HapticFeedback.selectionClick(); onPick(placeholder); if (Navigator.of(context).canPop()) { diff --git a/lib/widgets/stickers/picker.g.dart b/lib/widgets/stickers/sticker_picker.g.dart similarity index 97% rename from lib/widgets/stickers/picker.g.dart rename to lib/widgets/stickers/sticker_picker.g.dart index 6b67928a..ec430323 100644 --- a/lib/widgets/stickers/picker.g.dart +++ b/lib/widgets/stickers/sticker_picker.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'picker.dart'; +part of 'sticker_picker.dart'; // ************************************************************************** // RiverpodGenerator