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
2 changes: 1 addition & 1 deletion lib/config/storage_keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class StorageKeys {
static const String themeMode = 'theme_mode'; // 主题模式:light, dark, system

// 默认值
static const String defaultTimerMode = 'random_break';
static const String defaultTimerMode = 'pomodoro';
static const int defaultFocusTimeMinutes = 90;
static const int defaultBigBreakTimeMinutes = 20;
static const bool defaultMicroBreakEnabled = true; // 默认启用微休息
Expand Down
12 changes: 6 additions & 6 deletions lib/page/home/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,17 @@ class HomePage extends StatelessWidget {
children: [
_buildModeChip(
context: context,
label: '随机提示音',
isSelected: !isPomodoro,
label: '番茄时钟',
isSelected: isPomodoro,
enabled: isStopped,
onTap: () => controller.switchTimerMode('random_break'),
onTap: () => controller.switchTimerMode('pomodoro'),
),
_buildModeChip(
context: context,
label: '番茄时钟',
isSelected: isPomodoro,
label: '随机提示音',
isSelected: !isPomodoro,
enabled: isStopped,
onTap: () => controller.switchTimerMode('pomodoro'),
onTap: () => controller.switchTimerMode('random_break'),
),
],
),
Expand Down
10 changes: 2 additions & 8 deletions lib/page/main/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:get/get.dart';
import 'package:time_machine/page/main/state.dart';
import 'package:time_machine/utils/toast_util.dart';

class MainController extends GetxController {
final MainState state = MainState();
Expand Down Expand Up @@ -44,14 +45,7 @@ class MainController extends GetxController {
state.lastBackPressTime = now;

// 显示Toast提示
Get.snackbar(
'提示',
'再按一次返回键退出应用',
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 2),
margin: const EdgeInsets.all(16),
barBlur: 100,
);
ToastUtil.show('提示', '再按一次返回键退出应用');

return false; // 不退出应用
} else {
Expand Down
77 changes: 23 additions & 54 deletions lib/page/setting/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mmkv/mmkv.dart';
import 'package:time_machine/route/route_name.dart';
import 'package:time_machine/utils/toast_util.dart';

import '../../service/app_storage_service.dart';
import '../../service/permission_service.dart';
Expand Down Expand Up @@ -107,13 +108,7 @@ class SettingController extends GetxController {
/// 保存设置到存储
void saveSettings() {
if (!state.validateAll()) {
Get.snackbar(
'验证失败',
'请检查输入的设置值',
snackPosition: SnackPosition.TOP,
barBlur: 100,
duration: Duration(seconds: 2)
);
ToastUtil.show('验证失败', '请检查输入的设置值');
return;
}

Expand All @@ -131,13 +126,7 @@ class SettingController extends GetxController {
_storage.encodeInt(StorageKeys.pomodoroLongBreakMinutes, state.pomodoroLongBreakMinutes.value);
_storage.encodeInt(StorageKeys.pomodoroLongBreakInterval, state.pomodoroLongBreakInterval.value);

Get.snackbar(
'设置已保存',
'所有设置已成功保存',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: Duration(seconds: 1),
);
ToastUtil.show('设置已保存', '所有设置已成功保存');

// 通知 HomeController 重新加载设置
try {
Expand Down Expand Up @@ -167,13 +156,7 @@ class SettingController extends GetxController {
state.updateControllers();
state.clearErrors();

Get.snackbar(
'已重置',
'所有设置已重置为默认值',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: Duration(seconds: 1),
);
ToastUtil.show('已重置', '所有设置已重置为默认值');
}

/// 更新专注时间
Expand Down Expand Up @@ -268,12 +251,10 @@ class SettingController extends GetxController {
// 申请存储权限
final perm = Get.find<PermissionService>();
if (!await perm.requestStorage()) {
Get.snackbar(
ToastUtil.show(
'权限不足',
'备份需要存储权限, 请在「权限管理→存储权限」中授予后再试',
snackPosition: SnackPosition.TOP,
barBlur: 100,
duration: const Duration(seconds: 3),
toastLength: Toast.LENGTH_LONG,
);
return;
}
Expand All @@ -288,30 +269,26 @@ class SettingController extends GetxController {
final backupPath = await backupRestoreDBService.backupData();

if (backupPath != null) {
Get.snackbar(
// 原生 Toast 对超长无空格路径会截断,只展示外部存储根(/0/)之后的关键路径
final shownPath = backupPath.split('/0/').last;
Comment on lines +272 to +273
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

使用 split('/0/').last 来截断备份路径可能不够健壮。如果路径中包含其他带有 /0/ 的片段(例如 Android 应用私有存储路径 /data/user/0/... 或用户创建的名为 0 的文件夹),它会进行错误的分割,从而导致显示不完整或具有误导性的路径。

更健壮的方法是专门检查并移除标准的 Android 外部存储根路径前缀(/storage/emulated/0/)。

Suggested change
// 原生 Toast 对超长无空格路径会截断,只展示外部存储根(/0/)之后的关键路径
final shownPath = backupPath.split('/0/').last;
// 原生 Toast 对超长无空格路径会截断,只展示外部存储根(/storage/emulated/0/)之后的关键路径
final shownPath = backupPath.startsWith('/storage/emulated/0/')
? backupPath.replaceFirst('/storage/emulated/0/', '')
: backupPath;

ToastUtil.show(
'备份成功',
'数据已备份到: $backupPath',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: const Duration(seconds: 3),
'数据已备份到: $shownPath',
toastLength: Toast.LENGTH_LONG,
);
} else {
Get.snackbar(
ToastUtil.show(
'备份失败',
'无法创建备份文件, 请检查存储权限',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: const Duration(seconds: 3),
toastLength: Toast.LENGTH_LONG,
);
}
} catch (e) {
Get.log('备份过程中发生错误: $e');
Get.snackbar(
ToastUtil.show(
'备份失败',
'备份过程中发生错误: ${e.toString()}',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: const Duration(seconds: 3),
toastLength: Toast.LENGTH_LONG,
);
} finally {
state.isBackupInProgress.value = false;
Expand All @@ -325,12 +302,10 @@ class SettingController extends GetxController {
// 申请存储权限
final perm = Get.find<PermissionService>();
if (!await perm.requestStorage()) {
Get.snackbar(
ToastUtil.show(
'权限不足',
'恢复需要存储权限, 请在「权限管理→存储权限」中授予后再试',
snackPosition: SnackPosition.TOP,
barBlur: 100,
duration: const Duration(seconds: 3),
toastLength: Toast.LENGTH_LONG,
);
return;
}
Expand Down Expand Up @@ -364,12 +339,10 @@ class SettingController extends GetxController {
final success = await backupRestoreDBService.restoreData();

if (success) {
Get.snackbar(
ToastUtil.show(
'恢复成功',
'数据已成功恢复, 请重启应用以生效',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: const Duration(seconds: 3),
toastLength: Toast.LENGTH_LONG,
);

// 重新加载设置
Expand All @@ -383,22 +356,18 @@ class SettingController extends GetxController {
// HomeController 可能还没有初始化, 忽略错误
}
} else {
Get.snackbar(
ToastUtil.show(
'恢复失败',
'无法恢复数据, 请检查备份文件是否有效',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: const Duration(seconds: 3),
toastLength: Toast.LENGTH_LONG,
);
}
} catch (e) {
Get.log('恢复过程中发生错误: $e');
Get.snackbar(
ToastUtil.show(
'恢复失败',
'恢复过程中发生错误: ${e.toString()}',
snackPosition: SnackPosition.TOP,
barBlur: 100.0,
duration: const Duration(seconds: 3),
toastLength: Toast.LENGTH_LONG,
);
} finally {
state.isRestoreInProgress.value = false;
Expand Down
9 changes: 2 additions & 7 deletions lib/page/setting/state.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:time_machine/utils/toast_util.dart';
import '../../config/storage_keys.dart';

class SettingState {
Expand Down Expand Up @@ -137,13 +138,7 @@ class SettingState {
if (microBreakIntervalMinMinutes.value > microBreakIntervalMaxMinutes.value) {
microBreakIntervalMinMinutes.value = microBreakIntervalMaxMinutes.value;
microBreakIntervalMinController.text = microBreakIntervalMinMinutes.value.toString();
Get.snackbar(
'设置已调整',
'最小间隔不能大于最大间隔,已调整为相同值',
snackPosition: SnackPosition.TOP,
barBlur: 100,
duration: Duration(seconds: 2),
);
ToastUtil.show('设置已调整', '最小间隔不能大于最大间隔,已调整为相同值');
}
Comment on lines 138 to 142
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在失去焦点时静默覆盖用户的输入并弹出 Toast 提示,可能会给用户带来非常挫败的体验。例如,当用户想要同时调大最小和最大间隔(例如在当前最大间隔为 5 的情况下,将最小间隔改为 10,最大间隔改为 15),一旦他们编辑完最小间隔并切换焦点到最大间隔输入框时,他们的输入就会被强制重置回 5

相比于自动调整数值并弹出 Toast,更好的做法是设置一个验证错误信息(microBreakIntervalMinError)并将表单标记为无效(isValid = false)。这样可以允许用户在保存前自然地编辑这两个字段并解决冲突。

      if (microBreakIntervalMinMinutes.value > microBreakIntervalMaxMinutes.value) {
        microBreakIntervalMinError.value = '最小间隔不能大于最大间隔';
        isValid = false;
      }

}

Expand Down
60 changes: 26 additions & 34 deletions lib/page/setting/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,19 @@ class SettingPage extends StatelessWidget {
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
children: [
// 专注设置组
// 番茄时钟设置组(始终显示,供用户预先配置)
SettingSection(
title: '专注设置',
title: '番茄时钟设置',
children: [
PomodoroSettings(controller: controller),
],
),

const SizedBox(height: 24),

// 随机提示音设置组
SettingSection(
title: '随机提示音设置',
children: [
SettingTile(
title: '专注时间',
Expand Down Expand Up @@ -125,34 +135,7 @@ class SettingPage extends StatelessWidget {
),
),
const SettingDivider(),
SettingTile(
title: '自动开始下一个',
subtitle: '休息结束后自动开始下一个专注',
trailing: Obx(() => SettingSwitch(
value: state.autoStartNextFocus.value,
onChanged: controller.toggleAutoStartNextFocus,
)),
),
],
),

const SizedBox(height: 24),

// 微休息设置组
SettingSection(
title: '微休息设置',
children: [
MicroBreakSettings(controller: controller),
],
),

const SizedBox(height: 24),

// 番茄时钟设置组(始终显示,供用户预先配置)
SettingSection(
title: '番茄时钟设置',
children: [
PomodoroSettings(controller: controller),
MicroBreakSettings(controller: controller)
],
),

Expand All @@ -175,12 +158,15 @@ class SettingPage extends StatelessWidget {

// 显示设置组
SettingSection(
title: '显示设置',
title: '显示与行为',
children: [
SettingTile(
title: '主题模式',
subtitle: '选择应用的外观主题',
trailing: const ThemeSelector(),
title: '自动开始下一个',
subtitle: '休息结束后自动开始下一个专注',
trailing: Obx(() => SettingSwitch(
value: state.autoStartNextFocus.value,
onChanged: controller.toggleAutoStartNextFocus,
)),
),
const SettingDivider(),
SettingTile(
Expand All @@ -200,6 +186,12 @@ class SettingPage extends StatelessWidget {
onChanged: controller.toggleProgressDirection,
)),
),
const SettingDivider(),
SettingTile(
title: '主题模式',
subtitle: '选择应用的外观主题',
trailing: const ThemeSelector(),
),
],
),

Expand Down
6 changes: 2 additions & 4 deletions lib/page/setting/widgets/data_settings.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:time_machine/utils/toast_util.dart';

import '../../../service/database_service.dart';
import '../controller.dart';
Expand Down Expand Up @@ -102,10 +103,7 @@ class DataSettings extends StatelessWidget {
if (confirmed == true) {
final databaseService = Get.find<DatabaseService>();
await databaseService.resetDatabase();
Get.snackbar('成功', '数据库已重置',
duration: Duration(seconds: 1),
barBlur: 100,
);
ToastUtil.show('成功', '数据库已重置');
}
},
child: const Text('重置'),
Expand Down
3 changes: 2 additions & 1 deletion lib/page/setting/widgets/developer_settings.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:time_machine/utils/toast_util.dart';
import 'package:time_machine/page/setting/widgets/setting_divider.dart';
import 'package:time_machine/page/setting/widgets/setting_section.dart';
import 'package:time_machine/page/setting/widgets/setting_tile.dart';
Expand All @@ -24,7 +25,7 @@ class DeveloperSettings extends StatelessWidget {
onPressed: () async {
final testDataController = Get.put(TestDataController());
await testDataController.generateTestData();
Get.snackbar('成功', '测试数据已生成', duration: Duration(seconds: 1));
ToastUtil.show('成功', '测试数据已生成');
},
child: const Text('生成'),
),
Expand Down
9 changes: 2 additions & 7 deletions lib/page/setting/widgets/permission_settings.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:time_machine/utils/toast_util.dart';

import '../../../service/permission_service.dart';
import 'setting_tile.dart';
Expand Down Expand Up @@ -75,13 +76,7 @@ class PermissionSettings extends StatelessWidget {
}

void _showOpenSettingsHint() {
Get.snackbar(
'需要在系统设置中手动开启',
'权限已被永久拒绝, 即将跳转到应用设置',
snackPosition: SnackPosition.TOP,
barBlur: 100,
duration: const Duration(seconds: 2),
);
ToastUtil.show('需要在系统设置中手动开启', '权限已被永久拒绝, 即将跳转到应用设置');
}
}

Expand Down
Loading
Loading