♻️ Refactored heatmap
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fl_heatmap/fl_heatmap.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -20,6 +19,7 @@ import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:island/widgets/activity_heatmap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@@ -36,11 +36,11 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnPublisherHeatmap?> publisherHeatmap(Ref ref, String? uname) async {
|
||||
Future<SnHeatmap?> publisherHeatmap(Ref ref, String? uname) async {
|
||||
if (uname == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/publishers/$uname/heatmap');
|
||||
return SnPublisherHeatmap.fromJson(resp.data);
|
||||
return SnHeatmap.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
@@ -601,7 +601,7 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
|
||||
class _PublisherStatsWidget extends StatelessWidget {
|
||||
final SnPublisherStats stats;
|
||||
final SnPublisherHeatmap? heatmap;
|
||||
final SnHeatmap? heatmap;
|
||||
const _PublisherStatsWidget({required this.stats, this.heatmap});
|
||||
|
||||
@override
|
||||
@@ -655,7 +655,7 @@ class _PublisherStatsWidget extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (heatmap != null) _PublisherHeatmapWidget(heatmap: heatmap!),
|
||||
if (heatmap != null) ActivityHeatmapWidget(heatmap: heatmap!),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1197,231 +1197,3 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublisherHeatmapWidget extends StatelessWidget {
|
||||
final SnPublisherHeatmap heatmap;
|
||||
const _PublisherHeatmapWidget({required this.heatmap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Generate exactly 365 days ending at current date
|
||||
final now = DateTime.now();
|
||||
|
||||
// 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 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 weeks to cover exactly 365 days
|
||||
final weeks = <DateTime>[];
|
||||
var current = startMonday;
|
||||
while (current.isBefore(endSunday) || current.isAtSameMomentAs(endSunday)) {
|
||||
weeks.add(current);
|
||||
current = current.add(const Duration(days: 7));
|
||||
}
|
||||
|
||||
// Create data map for all dates in the range
|
||||
final dataMap = <DateTime, double>{};
|
||||
for (final week in weeks) {
|
||||
for (var i = 0; i < 7; 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(
|
||||
(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 = <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(
|
||||
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(), // Weeks horizontally
|
||||
items: [
|
||||
for (int day = 0; day < 7; day++) // For each day of week (Mon-Sun)
|
||||
for (final week in weeks) // For each week
|
||||
HeatmapItem(
|
||||
value: dataMap[week.add(Duration(days: day))] ?? 0.0,
|
||||
unit: heatmap.unit,
|
||||
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',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'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),
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
|
@@ -156,7 +156,7 @@ String _$publisherHeatmapHash() => r'780dfb05b8610a37cfcd937fd04cf5bbe9b298c9';
|
||||
const publisherHeatmapProvider = PublisherHeatmapFamily();
|
||||
|
||||
/// See also [publisherHeatmap].
|
||||
class PublisherHeatmapFamily extends Family<AsyncValue<SnPublisherHeatmap?>> {
|
||||
class PublisherHeatmapFamily extends Family<AsyncValue<SnHeatmap?>> {
|
||||
/// See also [publisherHeatmap].
|
||||
const PublisherHeatmapFamily();
|
||||
|
||||
@@ -188,8 +188,7 @@ class PublisherHeatmapFamily extends Family<AsyncValue<SnPublisherHeatmap?>> {
|
||||
}
|
||||
|
||||
/// See also [publisherHeatmap].
|
||||
class PublisherHeatmapProvider
|
||||
extends AutoDisposeFutureProvider<SnPublisherHeatmap?> {
|
||||
class PublisherHeatmapProvider extends AutoDisposeFutureProvider<SnHeatmap?> {
|
||||
/// See also [publisherHeatmap].
|
||||
PublisherHeatmapProvider(String? uname)
|
||||
: this._internal(
|
||||
@@ -220,7 +219,7 @@ class PublisherHeatmapProvider
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnPublisherHeatmap?> Function(PublisherHeatmapRef provider) create,
|
||||
FutureOr<SnHeatmap?> Function(PublisherHeatmapRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
@@ -237,7 +236,7 @@ class PublisherHeatmapProvider
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnPublisherHeatmap?> createElement() {
|
||||
AutoDisposeFutureProviderElement<SnHeatmap?> createElement() {
|
||||
return _PublisherHeatmapProviderElement(this);
|
||||
}
|
||||
|
||||
@@ -257,13 +256,13 @@ class PublisherHeatmapProvider
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PublisherHeatmapRef on AutoDisposeFutureProviderRef<SnPublisherHeatmap?> {
|
||||
mixin PublisherHeatmapRef on AutoDisposeFutureProviderRef<SnHeatmap?> {
|
||||
/// The parameter `uname` of this provider.
|
||||
String? get uname;
|
||||
}
|
||||
|
||||
class _PublisherHeatmapProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnPublisherHeatmap?>
|
||||
extends AutoDisposeFutureProviderElement<SnHeatmap?>
|
||||
with PublisherHeatmapRef {
|
||||
_PublisherHeatmapProviderElement(super.provider);
|
||||
|
||||
|
Reference in New Issue
Block a user