Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/domain/dashboard_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class DashboardPreferences {
this.enableGlassEffect = false,
this.weekStartMode = WeekStartMode.monday,
this.selectedYear,
this.focusMonth,
});

final HeatmapRangeMode heatmapRangeMode;
Expand All @@ -29,6 +30,9 @@ class DashboardPreferences {
/// 选择的年份(用于日历年份模式)。如果为 null,则使用当前年份。
final int? selectedYear;

/// 焦点月份(用于滚动12个月模式)。如果为 null,则使用当前月份。
final DateTime? focusMonth;

/// 当前平台是否实际使用毛玻璃效果。
/// 只有 macOS / Windows 且 enableGlassEffect 为 true 时才生效。
bool get useGlassEffect =>
Expand All @@ -39,12 +43,14 @@ class DashboardPreferences {
bool? enableGlassEffect,
WeekStartMode? weekStartMode,
int? selectedYear,
DateTime? focusMonth,
}) {
return DashboardPreferences(
heatmapRangeMode: heatmapRangeMode ?? this.heatmapRangeMode,
enableGlassEffect: enableGlassEffect ?? this.enableGlassEffect,
weekStartMode: weekStartMode ?? this.weekStartMode,
selectedYear: selectedYear ?? this.selectedYear,
focusMonth: focusMonth ?? this.focusMonth,
);
}
}
Expand All @@ -58,6 +64,7 @@ class DashboardPreferencesRepository {

static const _keyWeekStartMode = 'ringotrack.dashboard.weekStartMode';
static const _keySelectedYear = 'ringotrack.dashboard.selectedYear';
static const _keyFocusMonth = 'ringotrack.dashboard.focusMonth';

Future<DashboardPreferences> load() async {
final sp = await SharedPreferences.getInstance();
Expand All @@ -73,11 +80,21 @@ class DashboardPreferencesRepository {
orElse: () => const DashboardPreferences().weekStartMode,
);
final selectedYear = sp.getInt(_keySelectedYear);
final focusMonthString = sp.getString(_keyFocusMonth);
DateTime? focusMonth;
if (focusMonthString != null) {
try {
focusMonth = DateTime.parse(focusMonthString);
} catch (_) {
// 解析失败则忽略
}
}
return DashboardPreferences(
heatmapRangeMode: mode,
enableGlassEffect: enableGlass,
weekStartMode: weekStartMode,
selectedYear: selectedYear,
focusMonth: focusMonth,
);
}

Expand All @@ -91,6 +108,11 @@ class DashboardPreferencesRepository {
} else {
await sp.remove(_keySelectedYear);
}
if (prefs.focusMonth != null) {
await sp.setString(_keyFocusMonth, prefs.focusMonth!.toIso8601String());
} else {
await sp.remove(_keyFocusMonth);
}
}

/// Windows 平台:设置待生效的玻璃效果值(下次启动时应用)
Expand Down
7 changes: 7 additions & 0 deletions lib/domain/dashboard_preferences_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ class DashboardPreferencesController
state = AsyncData(next);
await _repository.save(next);
}

Future<void> setFocusMonth(DateTime? month) async {
final current = state.value ?? const DashboardPreferences();
final next = current.copyWith(focusMonth: month);
state = AsyncData(next);
await _repository.save(next);
}
}

final dashboardPreferencesRepositoryProvider =
Expand Down
215 changes: 2 additions & 213 deletions lib/pages/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:ringotrack/domain/drawing_app_preferences_controller.dart';
import 'package:ringotrack/domain/dashboard_preferences_controller.dart';
import 'package:ringotrack/domain/dashboard_preferences.dart';
import 'package:ringotrack/providers.dart';
import 'package:ringotrack/widgets/ringo_heatmap.dart';
import 'package:ringotrack/domain/usage_analysis.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:ringotrack/widgets/ringo_hourly_line_heatmap.dart';
import 'package:ringotrack/widgets/heatmap_color_scale.dart';
import 'package:ringotrack/widgets/year_selector.dart';
import 'dart:io' show Platform;
import 'package:ringotrack/platform/glass_tint_controller.dart';
import 'package:url_launcher/url_launcher.dart';
Expand Down Expand Up @@ -336,7 +336,7 @@ class _DashboardPageState extends ConsumerState<DashboardPage> {
Widget _buildYearSelectorAndTabs(ThemeData theme) {
return _InlineTabsAndYear(
tabsBuilder: () => _buildTabs(theme),
yearSelectorBuilder: () => _buildYearSelector(theme),
yearSelectorBuilder: () => YearSelector(),
);
}

Expand Down Expand Up @@ -1504,154 +1504,6 @@ class _DashboardPageState extends ConsumerState<DashboardPage> {
String twoDigits(int v) => v.toString().padLeft(2, '0');
return '${timestamp.month}月${timestamp.day}日 ${twoDigits(timestamp.hour)}:${twoDigits(timestamp.minute)}';
}

Widget _buildYearSelector(ThemeData theme) {
final prefs =
ref.watch(dashboardPreferencesControllerProvider).value ??
const DashboardPreferences();
final currentYear = DateTime.now().year;
final selectedYear = prefs.selectedYear ?? currentYear;

final availableYears =
List.generate(
13,
(index) => currentYear - 10 + index,
).where((y) => y >= 2020 && y <= currentYear + 2).toList()
..sort((a, b) => b.compareTo(a));

Future<void> showPicker() async {
final picked = await showModalBottomSheet<int>(
context: context,
isScrollControlled: false,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 12.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0),
borderRadius: BorderRadius.circular(999),
),
),
),
SizedBox(height: 12.h),
Text(
'选择年份',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
SizedBox(height: 4.h),
Text(
'切换热力图和分析展示的年份',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.black54,
),
),
SizedBox(height: 12.h),
ConstrainedBox(
constraints: BoxConstraints(maxHeight: 420.h),
child: ListView.separated(
shrinkWrap: true,
itemCount: availableYears.length,
separatorBuilder: (_, __) =>
Divider(height: 1, color: const Color(0xFFF0F0F0)),
itemBuilder: (ctx, i) {
final year = availableYears[i];
final active = year == selectedYear;
final isCurrent = year == currentYear;
return ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 4.w),
onTap: () => Navigator.of(ctx).pop(year),
title: Row(
children: [
Text(
'$year年',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: active
? FontWeight.w700
: FontWeight.w500,
color: active
? theme.colorScheme.primary
: Colors.black87,
),
),
if (isCurrent) ...[
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withAlpha(
24,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'今年',
style: theme.textTheme.labelMedium
?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w700,
),
),
),
],
const Spacer(),
if (active)
Icon(
Icons.check_circle_rounded,
size: 18.r,
color: theme.colorScheme.primary,
),
],
),
);
},
),
),
SizedBox(height: 8.h),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
),
],
),
),
);
},
);
if (picked != null) {
ref
.read(dashboardPreferencesControllerProvider.notifier)
.setSelectedYear(picked);
}
}

return _InlineYearSelector(
selectedYear: selectedYear,
currentYear: currentYear,
onOpenPicker: showPicker,
onSelectThisYear: null,
theme: theme,
);
}
}

class _TabButton extends StatefulWidget {
Expand Down Expand Up @@ -1929,69 +1781,6 @@ class _InlineTabsAndYear extends StatelessWidget {
}
}

class _InlineYearSelector extends StatelessWidget {
const _InlineYearSelector({
required this.selectedYear,
required this.currentYear,
required this.onOpenPicker,
required this.onSelectThisYear,
required this.theme,
});

final int selectedYear;
final int currentYear;
final VoidCallback onOpenPicker;
final VoidCallback? onSelectThisYear;
final ThemeData theme;

@override
Widget build(BuildContext context) {
final pillColor = theme.colorScheme.primary;

return Container(
height: 40.h,
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE4E4E4)),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(8),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$selectedYear年',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
SizedBox(width: 12.w),
GestureDetector(
onTap: onOpenPicker,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h),
decoration: BoxDecoration(
color: pillColor.withAlpha(24),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: pillColor.withAlpha(90)),
),
child: Icon(Icons.calendar_today, size: 12, color: pillColor),
),
),
],
),
);
}
}

class _ModernTabContainer extends StatefulWidget {
const _ModernTabContainer({
required this.child,
Expand Down
9 changes: 6 additions & 3 deletions lib/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,24 @@ final useGlassEffectProvider = Provider<bool>((ref) {

/// 根据偏好计算当前热力图的时间窗口。
/// - calendarYear: 当年 1/1 ~ 12/31
/// - rolling12Months: 最近 12 个月,右侧对齐当前月
/// - rolling12Months: 以焦点月份为结束点的 12 个月窗口
final heatmapRangeProvider = Provider<({DateTime start, DateTime end})>((ref) {
final prefsAsync = ref.watch(dashboardPreferencesControllerProvider);
final mode =
prefsAsync.value?.heatmapRangeMode ??
const DashboardPreferences().heatmapRangeMode;

final today = DateTime.now();
final normalizedToday = _normalizeDay(today);

DateTime start;
DateTime end;

if (mode == HeatmapRangeMode.rolling12Months) {
end = DateTime(normalizedToday.year, normalizedToday.month + 1, 0);
// 使用焦点月份,默认当前月份
final focusMonth =
prefsAsync.value?.focusMonth ?? DateTime(today.year, today.month, 1);
// 以焦点月份为结束点,往前推11个月
end = DateTime(focusMonth.year, focusMonth.month + 1, 0);
start = DateTime(end.year, end.month - 11, 1);
} else {
final selectedYear = prefsAsync.value?.selectedYear ?? today.year;
Expand Down
Loading