diff --git a/.github/workflows/FlutterBuild.yml b/.github/workflows/FlutterBuild.yml index cfe3e86..2978f6c 100644 --- a/.github/workflows/FlutterBuild.yml +++ b/.github/workflows/FlutterBuild.yml @@ -18,7 +18,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: '3.41.0' + flutter-version: '3.44.0' - name: Decode keystore run: | echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/release.jks diff --git a/.github/workflows/FlutterBuildTest.yml b/.github/workflows/FlutterBuildTest.yml index 6085823..b1afa17 100644 --- a/.github/workflows/FlutterBuildTest.yml +++ b/.github/workflows/FlutterBuildTest.yml @@ -15,7 +15,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: '3.41.0' + flutter-version: '3.44.0' - run: flutter pub get - run: flutter build windows @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v6 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.41.0' + flutter-version: '3.44.0' - run: flutter pub get - run: flutter build web --release - uses: actions/upload-artifact@v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index b143771..75179c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,22 @@ - 复习模块添加了强化学习选项 - 添加了学习中单词自动播放发音的功能 +- 复习阶段添加了进度指示 ### Improvement - 优化了FSRS学习机制 - 听写界面不熄屏 - 优化解压逻辑,避免主线程卡死 +- 复习改为自动乱序 +- 更新Flutter框架至3.44.0 ### Fix - 修复了可能的复习列表重复问题 - 补全控制器销毁逻辑,避免内存泄露 - 修复了网页端中无法通过文件导入数据的问题 +- 修复了单词推送不截止的问题 ## v1.0.0 - 2026-3-18 - (100000) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a919e92..44445a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,8 @@ 待审查通过后,你的提交就会被合并。 +--- + ## 你必须做到的 请在你进行提交和PR时关注以下的问题: @@ -44,7 +46,7 @@ 这个项目设置了必须要有GPG验证了是你的更改后才能进行合并。 -> 如果你不知道GPG是什么,请参照[这篇文章](https://docs.github.com/zh/authentication/managing-commit-signature-verification/about-commit-signature-verification) +> 如果你不知道GPG是什么,请参照[这篇文章](https://docs.github.com/authentication/managing-commit-signature-verification/about-commit-signature-verification) ### DCO签名 @@ -54,6 +56,8 @@ 你可以通过 `git commit -s` 对提交进行签署。 +--- + ## 其他的事项 ### 代码规范 @@ -67,18 +71,17 @@ - 对于一个类,请使用大写字母或者下划线开头 - 对于一个函数,请使用小写字母开头 -违反此规定的PR会要求进行相应修改后即可 +--- + +### 关于AI辅助生成代码 + +> 该定义仅对此项目生效 -#### 存储单词数据的变量 +定义: 通过 **自然语言** 请求或指导 **大语言模型(LLM)** 生成贡献中的 **编程语言代码** ,并且 **由人工的编写代码** 占贡献中 **40%** 以下的,即认为是 **AI辅助生成代码** 。 -在应用中,存储单个阿拉伯语单词数据的变量使用`word`为变量名`Map`为其类型,内部详细结构如下 +要求: 我并不反对通过AI辅助生成代码,但**十分希望**通过AI辅助生成的贡献能做到以下几点: -``` dart -Map word = {"arabic": "{Arabic}", - "chinese": "{Chinese}", - "explanation": "{explanation}", - "subClass": "{className}", - "learningProgress": "{times}", //int - "id": "{id}" // int getSelectedWords函数结果中会有,并且所有面向用户的界面中调用的数据都应具有此键值对。 - }; -``` +1. 尊重他人,避免出现有侮辱性的部分。(要是出现了这类问题,**请不要拿AI做挡箭牌**) +2. 注意不要在代码中泄露你的Key。(当然,如果你想让别人也免费用的话,那就另说了) +3. 先在你自己的环境下确保你的代码能正常运行。 +4. AI也会犯错,在你请求核查之前请先自己理解并排查可能的问题。 diff --git a/README.md b/README.md index 0c71772..6ad3391 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,105 @@ + # arabic_learning | Ar 学 -> 该软件目前还在开发阶段,很多功能尚未完善,使用时请倍加小心。 +

+ Flutter Build + License: AGPL-3.0 +

-一个用于记背阿拉伯语单词的学习软件。 +一个跨平台的阿拉伯语 ~~背单词~~ 单词学习软件,支持 **Android / Windows / Linux / macOS / Web**。 + +## 功能 + +### 学习 + +- 选择课程进行单词学习 +- 多种题型可自由配置与排序:单词卡片、中译阿选择题、阿译中选择题、拼写题、听力题 +- 题型内/题型间/全局三级乱序 +- 偏好易混词或同课词 + +### 规律复习(FSRS) + +> [!TIP] +> 你需要手动启用复习功能才会生效 + +- 基于 [FSRS](https://github.com/open-spaced-repetition/fsrs4anki) 算法的间隔重复复习 +- 可自由配置期望提取率、评分限时(优秀/良好) +- 自我评级模式 +- 每日单词推送,学习即加入复习计划 +- 强化记忆循环:同一到期卡片在队列中交错出现多次 + +### 测试 + +- **自主听写**:配置播放速度/次数/间隔,听写期间不熄屏 +- **局域网联机对战**:通过 WebRTC 实现,支持扫码/口令连接,双方实时 PK + +### 词库管理 + +- 从Github[此仓库](https://github.com/JYinherit/Arabiclearning)线上下载词库 +- 本地 JSON 文件导入 +- 词汇总览:按词库-课程分级展示,支持网格列数自定义 +- 词汇查找:阿语/中文双向搜索,支持 BK 树模糊匹配及编辑距离容错 + +### 数据同步 + +- **WebDAV** 远程备份与恢复 +- 本地数据导出/导入(JSON 文件) + +### 音频与 TTS + +- 系统 TTS / 在线 TTS/ 神经网络语音合成(sherpa-onnx / VITS) +- 播放速度可调(0.5x – 1.5x) +- 自动播放发音(学习中进入阿译中选择题时自动朗读) + +### 个性化 + +- 多种主题色 +- Material UI +- 深色模式 +- 备用字体(阿语 Vazirmatn / 中文 NotoSansSC) + +### 统计 + +- 连胜天数 +- 已学词汇 +- 待复习数量 +- 词库单词总数 + +## 平台 + +| 平台 | 支持 | +| ------ | ------ | +| Android | ✅ | +| Windows | ✅ | +| Linux | ✅ (请自行构建) | +| macOS | ✅ | +| iOS | 已在v0.1.12终止支持 | +| Web | ✅ [在线体验](https://octagonalstar.github.io/arabic_learning/) | + +> 网页版为最新构建版,相对于软件版本可能有更多功能和更多 bug。 + +## 技术栈 + +- **框架**:Flutter/Dart +- **状态管理**:Provider +- **复习算法**:FSRS +- **音频**:flutter_tts / just_audio / sherpa_onnx +- **联机**:flutter_webrtc +- **同步**:webdav_client +- **搜索**:BK 树 +- **存储**:shared_preferences / idb_shim(Web IndexedDB) + +## 构建 + +```bash +flutter pub get +flutter run +``` + +各平台构建产物参见 GitHub Releases 和 Github Action。 + +## 许可证 + +[AGPL-3.0](LICENSE) + +Copyright (C) 2025 OctagonalStar diff --git a/lib/funcs/fsrs_func.dart b/lib/funcs/fsrs_func.dart index b7e75e8..3e41570 100644 --- a/lib/funcs/fsrs_func.dart +++ b/lib/funcs/fsrs_func.dart @@ -149,6 +149,15 @@ class FSRS { logger.fine("计算得分: hard"); return Rating.hard; } + + DateTime? getCardBirthday(int wordId) { + try { + return config.reviewLogs.firstWhere((ReviewLog rvl) => rvl.cardId == wordId).reviewDateTime; + } catch (e) { + logger.severe("wordID: $wordId card not found or has more than one"); + return null; + } + } } @immutable diff --git a/lib/package_replacement/fake_dart_io.dart b/lib/package_replacement/fake_dart_io.dart index 078bb17..d416a7c 100644 --- a/lib/package_replacement/fake_dart_io.dart +++ b/lib/package_replacement/fake_dart_io.dart @@ -5,7 +5,7 @@ class File { // nothing return ""; } - bool existsSync() {return true;} + bool existsSync() {return false;} void deleteSync() {} diff --git a/lib/pages/learning_page.dart b/lib/pages/learning_page.dart index 6b125f0..27c5f6e 100644 --- a/lib/pages/learning_page.dart +++ b/lib/pages/learning_page.dart @@ -113,11 +113,13 @@ class LearningPage extends StatelessWidget { int tries = 0; while(pushWords.length < FSRS().config.pushAmount && tries < FSRS().config.pushAmount * 10){ int chosen = rnd.nextInt(AppData().wordData.words.length); - if(!FSRS().isContained(chosen)) { + DateTime? cardBirthday = FSRS().getCardBirthday(chosen); + if(cardBirthday == null || cardBirthday.difference(DateTime.now()).inDays == 0) { pushWords.add(AppData().wordData.words.elementAt(chosen)); } tries++; } + pushWords.removeWhere((WordItem item) => FSRS().isContained(item.id)); if(pushWords.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("今日的推送已完成"), duration: Duration(seconds: 1),), diff --git a/lib/pages/setting_page.dart b/lib/pages/setting_page.dart index 4162968..20ca8b3 100644 --- a/lib/pages/setting_page.dart +++ b/lib/pages/setting_page.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:arabic_learning/vars/statics_var.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -25,6 +24,8 @@ import 'package:arabic_learning/sub_pages_builder/setting_pages/questions_settin show QuestionsSettingPage; import 'package:arabic_learning/sub_pages_builder/setting_pages/sync_page.dart' show DataSyncPage; +import 'package:arabic_learning/sub_pages_builder/learning_pages/fsrs_pages.dart' + show ForeFSRSSettingPage; import 'package:arabic_learning/package_replacement/fake_dart_io.dart' if (dart.library.io) 'dart:io' as io; @@ -49,147 +50,111 @@ class _SettingPage extends State { SettingItem( title: "帮助", children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: Size.fromHeight(mediaQuery.size.height * 0.08), - backgroundColor: Theme.of( - context, - ).colorScheme.onPrimary.withAlpha(150), - shape: RoundedRectangleBorder(borderRadius: StaticsVar.br), - ), - onPressed: () { - context.read().uiLogger.info( - "跳转: SettingPage => HelpPage", - ); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => HelpPage()), - ); - }, - child: Row( - children: [ - Icon(Icons.help, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("常见问题", textAlign: TextAlign.start)), - Icon(Icons.arrow_forward_ios), - ], - ), - ), + SettingRedirctButton(title: "常见问题", icon: Icons.help, target: HelpPage()) ], ), SettingItem( title: "常规设置", padding: EdgeInsets.all(8.0), children: [ - Row( - children: [ - Icon(Icons.color_lens, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("主题颜色")), - DropdownButton( - value: appData.config.regular.theme, - items: const [ - DropdownMenuItem(value: 0, child: Text('樱粉')), - DropdownMenuItem(value: 1, child: Text('海蓝')), - DropdownMenuItem(value: 2, child: Text('草绿')), - DropdownMenuItem(value: 3, child: Text('金黄')), - DropdownMenuItem(value: 4, child: Text('柑橘')), - DropdownMenuItem(value: 5, child: Text('雅紫')), - DropdownMenuItem(value: 6, child: Text('木棕')), - DropdownMenuItem(value: 7, child: Text('冷灰')), - DropdownMenuItem(value: 8, child: Text('茶香')), - DropdownMenuItem(value: 9, child: Text('烟蓝')), - DropdownMenuItem(value: 10, child: Text('星青')), - ], - onChanged: (value) async { - context.read().uiLogger.info("更新主题颜色: $value"); - AppData().config = AppData().config.copyWith( - regular: AppData().config.regular.copyWith( - theme: value, + SettingRow( + leading: "主题颜色", + icon: Icons.color_lens, + end: DropdownButton( + value: appData.config.regular.theme, + items: const [ + DropdownMenuItem(value: 0, child: Text('樱粉')), + DropdownMenuItem(value: 1, child: Text('海蓝')), + DropdownMenuItem(value: 2, child: Text('草绿')), + DropdownMenuItem(value: 3, child: Text('金黄')), + DropdownMenuItem(value: 4, child: Text('柑橘')), + DropdownMenuItem(value: 5, child: Text('雅紫')), + DropdownMenuItem(value: 6, child: Text('木棕')), + DropdownMenuItem(value: 7, child: Text('冷灰')), + DropdownMenuItem(value: 8, child: Text('茶香')), + DropdownMenuItem(value: 9, child: Text('烟蓝')), + DropdownMenuItem(value: 10, child: Text('星青')), + ], + onChanged: (value) async { + context.read().uiLogger.info("更新主题颜色: $value"); + AppData().config = AppData().config.copyWith( + regular: AppData().config.regular.copyWith( + theme: value, + ), + ); + context.read().updateSetting(); + }, + ) + ), + SettingRow( + leading: "深色模式", + icon: Icons.brightness_4, + end: Switch( + value: appData.config.regular.darkMode, + onChanged: (value) { + context.read().uiLogger.info( + "更新深色模式设置: $value", + ); + AppData().config = AppData().config.copyWith( + regular: AppData().config.regular.copyWith( + darkMode: value, + ), + ); + context.read().updateSetting(); + }, + ), + ), + SettingRow( + leading: "字体设置", + icon: Icons.font_download, + note: "若你的系统上阿语/中文字体异常,可在此更换备用", + end: DropdownButton( + value: appData.config.regular.font, + items: [ + DropdownMenuItem(value: 0, child: Text('默认字体')), + DropdownMenuItem(value: 1, child: Text('仅阿语使用备用字体')), + DropdownMenuItem(value: 2, child: Text('中阿均使用备用字体')), + ], + onChanged: (value) { + context.read().uiLogger.info("更新字体设置: $value"); + if (value == 2 && kIsWeb) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("网页版加载中文字体需要较长时间,请先耐心等待"), + duration: Duration(seconds: 3), ), ); - context.read().updateSetting(); - }, - ), - ], + } + AppData().config = AppData().config.copyWith( + regular: AppData().config.regular.copyWith( + font: value, + ), + ); + Provider.of( + context, + listen: false, + ).updateSetting(); + }, + ), ), - Row( - children: [ - Icon(Icons.brightness_4, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("深色模式")), - Switch( - value: appData.config.regular.darkMode, + if (kIsWeb) + SettingRow( + leading: "隐藏网页版右上角APP下载按钮", + icon: Icons.hide_source, + end: Switch( + value: AppData().config.regular.hideAppDownloadButton, onChanged: (value) { context.read().uiLogger.info( - "更新深色模式设置: $value", + "更新网页端APP下载按钮隐藏设置: $value", ); AppData().config = AppData().config.copyWith( regular: AppData().config.regular.copyWith( - darkMode: value, + hideAppDownloadButton: value, ), ); context.read().updateSetting(); }, ), - ], - ), - Row( - children: [ - Icon(Icons.font_download, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("字体设置")), - DropdownButton( - value: appData.config.regular.font, - items: [ - DropdownMenuItem(value: 0, child: Text('默认字体')), - DropdownMenuItem(value: 1, child: Text('仅阿语使用备用字体')), - DropdownMenuItem(value: 2, child: Text('中阿均使用备用字体')), - ], - onChanged: (value) { - context.read().uiLogger.info("更新字体设置: $value"); - if (value == 2 && kIsWeb) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("网页版加载中文字体需要较长时间,请先耐心等待"), - duration: Duration(seconds: 3), - ), - ); - } - AppData().config = AppData().config.copyWith( - regular: AppData().config.regular.copyWith( - font: value, - ), - ); - Provider.of( - context, - listen: false, - ).updateSetting(); - }, - ), - ], - ), - if (kIsWeb) - Row( - children: [ - Icon(Icons.hide_source, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("隐藏网页版右上角APP下载按钮")), - Switch( - value: AppData().config.regular.hideAppDownloadButton, - onChanged: (value) { - context.read().uiLogger.info( - "更新网页端APP下载按钮隐藏设置: $value", - ); - AppData().config = AppData().config.copyWith( - regular: AppData().config.regular.copyWith( - hideAppDownloadButton: value, - ), - ); - context.read().updateSetting(); - }, - ), - ], ), ], ), @@ -318,60 +283,9 @@ class _SettingPage extends State { ), ], ), - ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: Size.fromHeight(mediaQuery.size.height * 0.08), - backgroundColor: Theme.of( - context, - ).colorScheme.onPrimary.withAlpha(150), - shape: BeveledRectangleBorder(), - ), - onPressed: () { - context.read().uiLogger.info( - "跳转: SettingPage => QuestionsSettingPage", - ); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => QuestionsSettingPage(), - ), - ); - }, - child: Row( - children: [ - Icon(Icons.quiz), - Expanded(child: Text("题型配置")), - Icon(Icons.arrow_forward_ios), - ], - ), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: Size.fromHeight(mediaQuery.size.height * 0.08), - backgroundColor: Theme.of( - context, - ).colorScheme.onPrimary.withAlpha(150), - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusGeometry.vertical( - bottom: Radius.circular(25.0), - ), - ), - ), - onPressed: () { - context.read().uiLogger.info( - "跳转: SettingPage => DataSyncPage", - ); - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => DataSyncPage()), - ); - }, - child: Row( - children: [ - Icon(Icons.sync), - Expanded(child: Text("数据备份及同步")), - Icon(Icons.arrow_forward_ios), - ], - ), - ), + SettingRedirctButton(title: "题型配置",icon: Icons.quiz, target: QuestionsSettingPage()), + SettingRedirctButton(title: "数据备份及同步", icon: Icons.sync, target: DataSyncPage()), + SettingRedirctButton(title: "复习配置", icon: Icons.bookmark, target: ForeFSRSSettingPage(forceChoosing: true)), ], ), SettingItem( @@ -379,31 +293,11 @@ class _SettingPage extends State { children: [ Column( children: [ - Row( - children: [ - SizedBox(width: mediaQuery.size.width * 0.02), - Icon(Icons.api, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentGeometry.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("选择文本转语音接口"), - Text( - "默认使用系统自带的文本转语音接口,但有些厂商可能没有阿拉伯语支持\n若使用\"神经网络合成语音\"你必须使用APP端并下载模型。", - style: TextStyle( - fontSize: 8.0, - color: Colors.grey, - ), - ), - ], - ), - ), - ), - ], + SettingRow( + leading: "选择文本转语音接口", + icon: Icons.api, + note: "默认使用系统自带的文本转语音接口,但有些厂商可能没有阿拉伯语支持\n若使用\"神经网络合成语音\"你必须使用APP端并下载模型。", + end: SizedBox() ), DropdownButton( value: AppData().config.audio.audioSource, @@ -439,18 +333,12 @@ class _SettingPage extends State { ), DropdownMenuItem( value: 2, - enabled: kIsWeb - ? false - : (AppData().modelTTSDownloaded ? true : false), + enabled: !kIsWeb && AppData().modelTTSDownloaded ? true : false, child: Text( "神经网络合成语音", overflow: TextOverflow.ellipsis, style: TextStyle( - color: kIsWeb - ? Colors.grey - : (AppData().modelTTSDownloaded - ? null - : Colors.grey), + color: !kIsWeb && AppData().modelTTSDownloaded ? null : Colors.grey, ), ), ), @@ -460,153 +348,59 @@ class _SettingPage extends State { SizedBox(width: mediaQuery.size.width * 0.02), ], ), - Row( - children: [ - SizedBox(width: mediaQuery.size.width * 0.02), - Icon(Icons.speed, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("设置播放速度"), - Text( - "默认为1.0,即正常播放速度。", - style: TextStyle(fontSize: 8.0, color: Colors.grey), - ), - ], - ), - ), - Slider( - value: AppData().config.audio.playRate, - min: 0.5, - max: 1.5, - divisions: 10, - label: "${AppData().config.audio.playRate}", - onChanged: (value) { - setState(() { - AppData().config = AppData().config.copyWith( - audio: AppData().config.audio.copyWith( - playRate: value, - ), - ); - }); - }, - onChangeEnd: (value) { - context.read().uiLogger.info( - "更新音频速度设置: $value", - ); - context.read().updateSetting(); - }, - ), - SizedBox(width: mediaQuery.size.width * 0.02), - ], - ), - Row( - children: [ - SizedBox(width: mediaQuery.size.width * 0.02), - Icon(Icons.play_arrow, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("自动播放发音"), - Text( - "开启后将会在<学习>模块中,每进入阿译中选择题时阅读当前单词发音。", - style: TextStyle(fontSize: 8.0, color: Colors.grey), - ), - ], - ), - ), - Switch( - value: appData.config.audio.autoPlay, - onChanged: (value) { - context.read().uiLogger.info( - "更新自动发音设置: $value", - ); + SettingRow( + leading: "设置播放速度", + icon: Icons.speed, + note: "默认为1.0,即正常播放速度。", + end: Slider( + value: AppData().config.audio.playRate, + min: 0.5, + max: 1.5, + divisions: 10, + label: "${AppData().config.audio.playRate}", + onChanged: (value) { + setState(() { AppData().config = AppData().config.copyWith( audio: AppData().config.audio.copyWith( - autoPlay: value + playRate: value, ), ); - context.read().updateSetting(); - }, - ), - ], + }); + }, + onChangeEnd: (value) { + context.read().uiLogger.info( + "更新音频速度设置: $value", + ); + context.read().updateSetting(); + }, + ) ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.onPrimary.withAlpha(150), - minimumSize: Size.fromHeight(mediaQuery.size.height * 0.08), - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusGeometry.vertical( - bottom: Radius.circular(25.0), - ), - ), - ), - onPressed: () { - if (kIsWeb) { - alart(context, "此功能仅支持APP端。\n要是你是果儿的话...那我没招了,抱歉"); - } else { + SettingRow( + leading: "自动播放发音", + icon: Icons.play_arrow, + note: "开启后将会在<学习>模块中,每进入阿译中选择题时阅读当前单词发音。", + end: Switch( + value: appData.config.audio.autoPlay, + onChanged: (value) { context.read().uiLogger.info( - "跳转: SettingPage => ModelDownload", + "更新自动发音设置: $value", ); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ModelDownload(), + AppData().config = AppData().config.copyWith( + audio: AppData().config.audio.copyWith( + autoPlay: value ), ); - } - }, - child: Row( - children: [ - Icon(Icons.model_training, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("下载神经网络文本转语音模型")), - Icon(Icons.arrow_forward_ios, size: 28.0), - ], - ), + context.read().updateSetting(); + }, + ) ), + if(!kIsWeb) SettingRedirctButton(title: "下载神经网络文本转语音模型", icon: Icons.model_training, target: ModelDownload()) ], ), SettingItem( title: "关于", children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.onPrimary.withAlpha(150), - minimumSize: Size.fromHeight(mediaQuery.size.height * 0.08), - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusGeometry.vertical( - top: Radius.circular(25.0), - ), - ), - ), - onPressed: () { - context.read().uiLogger.info( - "跳转: SettingPage => DebugPage", - ); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => DebugPage()), - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon(Icons.bug_report, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("调试信息")), - Icon(Icons.arrow_forward_ios), - ], - ), - ), + SettingRedirctButton(title: "调试信息", icon: Icons.bug_report, target: DebugPage()), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Theme.of( @@ -648,35 +442,7 @@ class _SettingPage extends State { ], ), ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.onPrimary.withAlpha(150), - minimumSize: Size.fromHeight(mediaQuery.size.height * 0.08), - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusGeometry.vertical( - bottom: Radius.circular(25.0), - ), - ), - ), - onPressed: () { - context.read().uiLogger.info( - "跳转: SettingPage => AboutPage", - ); - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => AboutPage()), - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon(Icons.adb, size: 24.0), - SizedBox(width: mediaQuery.size.width * 0.01), - Expanded(child: Text("关于该软件")), - ], - ), - ), + SettingRedirctButton(title: "关于本软件", icon: Icons.adb, target: AboutPage()) ], ), ], @@ -685,3 +451,88 @@ class _SettingPage extends State { ); } } + +class SettingRedirctButton extends StatelessWidget { + const SettingRedirctButton({ + super.key, + required this.title, + required this.target, + this.icon = Icons.settings + }); + + final String title; + final IconData icon; + final Widget target; + + @override + Widget build(BuildContext context) { + MediaQueryData mediaQuery = MediaQuery.of(context); + return ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: Size.fromHeight(mediaQuery.size.height * 0.08), + backgroundColor: Theme.of( + context, + ).colorScheme.onPrimary.withAlpha(150), + shape: BeveledRectangleBorder(), + ), + onPressed: () { + context.read().uiLogger.info( + "跳转: SettingPage => ${target.toString()}", + ); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => target, + ), + ); + }, + child: Row( + children: [ + Icon(icon), + Expanded(child: Text(title)), + Icon(Icons.arrow_forward_ios), + ], + ), + ); + } +} + +class SettingRow extends StatelessWidget { + const SettingRow({ + super.key, + required this.leading, + required this.end, + this.icon = Icons.settings, + this.note, + }); + + final String leading; + final String? note; + final IconData icon; + final Widget end; + + @override + Widget build(BuildContext context) { + MediaQueryData mediaQuery = MediaQuery.of(context); + return Row( + children: [ + SizedBox(width: mediaQuery.size.width * 0.02), + Icon(icon, size: 24.0), + SizedBox(width: mediaQuery.size.width * 0.01), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(leading), + if(note != null) Text( + note!, + style: TextStyle(fontSize: 12.0, color: Colors.grey), + ), + ], + ), + ), + end, + SizedBox(width: mediaQuery.size.width * 0.02) + ], + ); + } +} \ No newline at end of file diff --git a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart index 4b16bbd..d6c68b3 100644 --- a/lib/sub_pages_builder/learning_pages/fsrs_pages.dart +++ b/lib/sub_pages_builder/learning_pages/fsrs_pages.dart @@ -323,6 +323,7 @@ class _MainFSRSPageState extends State { List queue = []; final PageController controller = PageController(); final Random sharedRnd = Random(); + final int initalReviewNum = FSRS().getWillDueCount(); @override void initState() { @@ -341,9 +342,9 @@ class _MainFSRSPageState extends State { if (widget.fsrs.config.reinforceMemory) { queue.addAll([ for (int i = 0; i < 3; i++) ...uniqueIds - ]); + ]..shuffle()); } else { - queue.addAll(uniqueIds); + queue.addAll(uniqueIds..shuffle()); } } @@ -367,10 +368,35 @@ class _MainFSRSPageState extends State { Widget build(BuildContext context) { context.read().uiLogger.info("构建 MainFSRSPage, 动态队列长度: ${queue.length}"); MediaQueryData mediaQuery = MediaQuery.of(context); + void Function(void Function()) refreshProgress =(_)=>(); return Scaffold( appBar: AppBar( - title: const Text("规律学习"), + title: Column( + children: [ + Text("复习"), + if(queue.isNotEmpty) StatefulBuilder( + builder: (context, setLocalState) { + refreshProgress = setLocalState; + context.read().uiLogger.fine("刷新复习进度条"); + return TweenAnimationBuilder( + tween: Tween( + begin: 0, + end: (initalReviewNum-widget.fsrs.getWillDueCount())/initalReviewNum + ), + duration: Durations.medium2, + builder: (context, value, child) { + return LinearProgressIndicator( + value: value, + minHeight: 15, + borderRadius: StaticsVar.br, + ); + } + ); + } + ) + ], + ), actions: [ IconButton( icon: Icon(Icons.refresh), @@ -394,6 +420,9 @@ class _MainFSRSPageState extends State { scrollDirection: Axis.vertical, controller: controller, physics: const PageScrollPhysics(), + onPageChanged: (value) { + refreshProgress(()=>()); + }, itemBuilder: (context, index) { // Page 0: 引导页 if (index == 0) { diff --git a/lib/sub_pages_builder/setting_pages/item_widget.dart b/lib/sub_pages_builder/setting_pages/item_widget.dart index 985e006..7098605 100644 --- a/lib/sub_pages_builder/setting_pages/item_widget.dart +++ b/lib/sub_pages_builder/setting_pages/item_widget.dart @@ -14,59 +14,20 @@ class SettingItem extends StatelessWidget { Widget build(BuildContext context) { context.read().uiLogger.info("构建 SettingItem: $title"); MediaQueryData mediaQuery = MediaQuery.of(context); - List decoratedContainers = children.map((widget) { + List decoratedContainers = List.generate(children.length, (int index) { return Container( width: mediaQuery.size.width * 0.90, padding: padding, + margin: EdgeInsets.all(4.0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.onPrimary.withAlpha(150), - borderRadius: BorderRadius.all(Radius.circular(5.0)), + borderRadius: BorderRadius.vertical(top: Radius.circular(index == 0 ? 25.0 : 5.0), bottom: Radius.circular(index == children.length-1 ? 25.0 : 5.0)), ), - child: widget, + clipBehavior: Clip.antiAlias, + child: children[index], ); - }).toList(); + }); - if(decoratedContainers.length > 1){ - decoratedContainers[0] = Container( - width: mediaQuery.size.width * 0.90, - margin: decoratedContainers[0].margin, - padding: decoratedContainers[0].padding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onPrimary.withAlpha(150), - borderRadius: BorderRadius.vertical(top: Radius.circular(25.0), bottom: Radius.circular(5.0)), - ), - child: decoratedContainers[0].child, - ); - decoratedContainers[decoratedContainers.length - 1] = Container( - width: mediaQuery.size.width * 0.90, - margin: decoratedContainers[decoratedContainers.length - 1].margin, - padding: decoratedContainers[decoratedContainers.length - 1].padding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onPrimary.withAlpha(150), - borderRadius: BorderRadius.vertical(bottom: Radius.circular(25.0), top: Radius.circular(5.0)), - ), - child: decoratedContainers[decoratedContainers.length - 1].child, - ); - } else { - decoratedContainers[0] = Container( - width: mediaQuery.size.width * 0.90, - margin: decoratedContainers[0].margin, - padding: decoratedContainers[0].padding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onPrimary.withAlpha(150), - borderRadius: BorderRadius.all(Radius.circular(25.0)), - ), - child: decoratedContainers[0].child, - ); - } - //Add Sizedbox between each item in list - List newList = []; - for (var i = 0; i < decoratedContainers.length; i++) { - newList.add(decoratedContainers[i]); - if (i != decoratedContainers.length - 1) { - newList.add(SizedBox(height: mediaQuery.size.height * 0.005)); - } - } return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -74,7 +35,7 @@ class SettingItem extends StatelessWidget { TextContainer(text: title), Center( child: Column( - children: newList, + children: decoratedContainers, ), ), ] diff --git a/lib/sub_pages_builder/setting_pages/questions_setting_page.dart b/lib/sub_pages_builder/setting_pages/questions_setting_page.dart index c2b40f0..051e724 100644 --- a/lib/sub_pages_builder/setting_pages/questions_setting_page.dart +++ b/lib/sub_pages_builder/setting_pages/questions_setting_page.dart @@ -83,7 +83,7 @@ class _QuestionsSettingPage extends State { if(!AppData().isWideScreen) TextContainer(text: "长按可拖动排序", style: TextStyle(color: Colors.grey), animated: true), Expanded( child: ReorderableListView( - onReorder: (oldIndex, newIndex) { + onReorderItem: (oldIndex, newIndex) { context.read().uiLogger.info("重排题型项目: $oldIndex => $newIndex"); setState(() { if(oldIndex < newIndex) newIndex--; // 修正索引 diff --git a/pubspec.lock b/pubspec.lock index 81b20e0..b36d8ca 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -580,10 +580,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1105,26 +1105,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" timezone: dependency: transitive description: