🐛 Bug fixes and optimization

This commit is contained in:
2025-10-12 20:45:10 +08:00
parent e2d315afd4
commit ad9fb0719a
7 changed files with 186 additions and 57 deletions

View File

@@ -1220,5 +1220,6 @@
"noStickersInPack": "This pack does not contains stickers", "noStickersInPack": "This pack does not contains stickers",
"noStickerPacks": "No Sticker Packs", "noStickerPacks": "No Sticker Packs",
"refresh": "Refresh", "refresh": "Refresh",
"spoiler": "Spoiler" "spoiler": "Spoiler",
"activityHeatmap": "Activity Heatmap"
} }

View File

@@ -1204,19 +1204,22 @@ class _PublisherHeatmapWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Find min and max dates // Generate exactly 365 days ending at current date
final dates = heatmap.items.map((e) => e.date).toList(); final now = DateTime.now();
if (dates.isEmpty) return const SizedBox.shrink();
final minDate = dates.reduce((a, b) => a.isBefore(b) ? a : b); // Start from exactly 365 days ago
final maxDate = dates.reduce((a, b) => a.isAfter(b) ? a : b); final startDate = now.subtract(const Duration(days: 365));
// End at current date
final endDate = now;
// Find monday of the week containing minDate // Find monday of the week containing start date
final startMonday = minDate.subtract(Duration(days: minDate.weekday - 1)); final startMonday = startDate.subtract(
// Find sunday of the week containing maxDate Duration(days: startDate.weekday - 1),
final endSunday = maxDate.add(Duration(days: 7 - maxDate.weekday)); );
// 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 = <DateTime>[]; final weeks = <DateTime>[];
var current = startMonday; var current = startMonday;
while (current.isBefore(endSunday) || current.isAtSameMomentAs(endSunday)) { while (current.isBefore(endSunday) || current.isAtSameMomentAs(endSunday)) {
@@ -1224,17 +1227,14 @@ class _PublisherHeatmapWidget extends StatelessWidget {
current = current.add(const Duration(days: 7)); current = current.add(const Duration(days: 7));
} }
// Columns: Mon to Sun // Create data map for all dates in the range
const columns = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; final dataMap = <DateTime, double>{};
// Create data map
final dataMap = <String, Map<String, double>>{};
for (final week in weeks) { 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++) { for (var i = 0; i < 7; i++) {
final date = week.add(Duration(days: i)); final date = week.add(Duration(days: i));
// 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( final item = heatmap.items.firstWhere(
(e) => (e) =>
e.date.year == date.year && e.date.year == date.year &&
@@ -1242,27 +1242,83 @@ class _PublisherHeatmapWidget extends StatelessWidget {
e.date.day == date.day, e.date.day == date.day,
orElse: () => SnPublisherHeatmapItem(date: date, count: 0), orElse: () => SnPublisherHeatmapItem(date: date, count: 0),
); );
dataMap[weekKey]![columns[i]] = item.count.toDouble(); dataMap[date] = item.count.toDouble();
}
}
}
// Generate month labels for the top
final monthLabels = <String>[];
final monthPositions = <int>[];
final processedMonths =
<String>{}; // 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),
);
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( final heatmapData = HeatmapData(
rows: rows: [
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun',
], // Days of week vertically
columns:
weeks weeks
.map( .map(
(w) => (w) =>
'${w.year}-${w.month.toString().padLeft(2, '0')}-${w.day.toString().padLeft(2, '0')}', '${w.year}-${w.month.toString().padLeft(2, '0')}-${w.day.toString().padLeft(2, '0')}',
) )
.toList(), .toList(), // Weeks horizontally
columns: columns,
items: [ items: [
for (final row in dataMap.entries) for (int day = 0; day < 7; day++) // For each day of week (Mon-Sun)
for (final col in row.value.entries) for (final week in weeks) // For each week
HeatmapItem( HeatmapItem(
value: col.value, value: dataMap[week.add(Duration(days: day))] ?? 0.0,
unit: heatmap.unit, unit: heatmap.unit,
xAxisLabel: col.key, xAxisLabel:
yAxisLabel: row.key, '${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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Activity Heatmap', 'activityHeatmap',
style: Theme.of(context).textTheme.titleMedium, 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), const Gap(8),
Heatmap( // Legend
showXAxisLabels: false, Row(
showYAxisLabels: false, mainAxisAlignment: MainAxisAlignment.end,
heatmapData: heatmapData, children: [
rowsVisible: 5, 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];
}
} }

View File

@@ -97,7 +97,7 @@ class WindowScaffold extends HookConsumerWidget {
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(), LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
}, },
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(context)}, actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
child: Material( child: Material(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack( child: Stack(
@@ -206,7 +206,7 @@ class WindowScaffold extends HookConsumerWidget {
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(), LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
}, },
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(context)}, actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [Positioned.fill(child: child), _WebSocketIndicator()], children: [Positioned.fill(child: child), _WebSocketIndicator()],
@@ -351,14 +351,14 @@ class PopIntent extends Intent {
} }
class PopAction extends Action<PopIntent> { class PopAction extends Action<PopIntent> {
final BuildContext context; final WidgetRef ref;
PopAction(this.context); PopAction(this.ref);
@override @override
void invoke(PopIntent intent) { void invoke(PopIntent intent) {
if (context.canPop()) { if (ref.watch(routerProvider).canPop()) {
context.pop(); ref.read(routerProvider).pop();
} }
} }
} }

View File

@@ -24,7 +24,7 @@ import "package:material_symbols_icons/material_symbols_icons.dart";
import "package:pasteboard/pasteboard.dart"; import "package:pasteboard/pasteboard.dart";
import "package:styled_widget/styled_widget.dart"; import "package:styled_widget/styled_widget.dart";
import "package:material_symbols_icons/symbols.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"; import "package:island/pods/chat/chat_subscribe.dart";
class ChatInput extends HookConsumerWidget { class ChatInput extends HookConsumerWidget {
@@ -524,9 +524,6 @@ class ChatInput extends HookConsumerWidget {
hideOnEmpty: true, hideOnEmpty: true,
hideOnLoading: true, hideOnLoading: true,
debounceDuration: const Duration(milliseconds: 500), debounceDuration: const Duration(milliseconds: 500),
loadingBuilder: (context) => const Text('Loading...'),
errorBuilder: (context, error) => const Text('Error!'),
emptyBuilder: (context) => const Text('No items found!'),
), ),
), ),
IconButton( IconButton(

View File

@@ -236,9 +236,6 @@ class ComposeFormFields extends HookConsumerWidget {
hideOnEmpty: true, hideOnEmpty: true,
hideOnLoading: true, hideOnLoading: true,
debounceDuration: const Duration(milliseconds: 500), debounceDuration: const Duration(milliseconds: 500),
loadingBuilder: (context) => const Text('Loading...'),
errorBuilder: (context, error) => const Text('Error!'),
emptyBuilder: (context) => const Text('No items found!'),
), ),
], ],
), ),

View File

@@ -14,7 +14,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart'; import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:island/widgets/extended_refresh_indicator.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: /// Fetch user-added sticker packs (with stickers) from API:
/// GET /sphere/stickers/me /// GET /sphere/stickers/me
@@ -34,7 +34,7 @@ Future<List<SnStickerPack>> myStickerPacks(Ref ref) async {
/// Sticker Picker popover dialog /// Sticker Picker popover dialog
/// - Displays user-owned sticker packs as tabs (chips) /// - Displays user-owned sticker packs as tabs (chips)
/// - Shows grid of stickers in selected pack /// - 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 { class StickerPicker extends HookConsumerWidget {
final void Function(String placeholder) onPick; final void Function(String placeholder) onPick;
@@ -63,7 +63,7 @@ class StickerPicker extends HookConsumerWidget {
return _PackSwitcher( return _PackSwitcher(
packs: packs, packs: packs,
onPick: (pack, sticker) { onPick: (pack, sticker) {
final placeholder = ':${pack.prefix}${sticker.slug}:'; final placeholder = ':${pack.prefix}+${sticker.slug}:';
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
onPick(placeholder); onPick(placeholder);
if (Navigator.of(context).canPop()) { if (Navigator.of(context).canPop()) {

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'picker.dart'; part of 'sticker_picker.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator