🐛 Bug fixes and optimization
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
@@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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(
|
||||||
|
@@ -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!'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -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()) {
|
@@ -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
|
Reference in New Issue
Block a user