From f67238fd3d4c1dccc6f8b7970b893dc2eec5ac6c Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Sat, 30 May 2026 23:38:04 +0000 Subject: [PATCH 1/4] Translate app UI from Chinese to English - Replace all hardcoded Chinese UI string literals with English across lib/ - Set MaterialApp locale and supportedLocales to English (en) - Translate in-app disclaimer (assets/statements/statements.txt) - Keep Chinese strings that are matched against Bangumi API data / used as regex parsing tokens (character relation keys, infobox keys, tag query, episode-number regex) to preserve functionality Co-authored-by: DeadEnd Dev <283450271+Deadendev@users.noreply.github.com> --- assets/statements/statements.txt | 22 +-- lib/app_widget.dart | 24 ++-- lib/bean/card/bangumi_card.dart | 2 +- lib/bean/card/bangumi_history_card.dart | 12 +- lib/bean/card/bangumi_info_card.dart | 10 +- lib/bean/card/comments_card.dart | 2 +- lib/bean/card/episode_comments_card.dart | 2 +- lib/bean/settings/color_type.dart | 18 +-- .../widget/bangumi_mirror_error_widget.dart | 6 +- lib/bean/widget/collect_button.dart | 12 +- lib/bean/widget/image_preview.dart | 2 +- lib/main.dart | 10 +- .../bangumi/bangumi_collection_type.dart | 12 +- lib/modules/bangumi/sync_priority.dart | 4 +- lib/modules/characters/character_item.dart | 2 +- lib/modules/collect/collect_type.dart | 12 +- lib/pages/about/about_module.dart | 2 +- lib/pages/about/about_page.dart | 54 +++---- lib/pages/bangumi/bangumi_setting.dart | 36 ++--- lib/pages/collect/collect_controller.dart | 52 +++---- lib/pages/collect/collect_page.dart | 34 ++--- lib/pages/download/download_controller.dart | 8 +- .../download/download_episode_sheet.dart | 14 +- lib/pages/download/download_page.dart | 56 ++++---- lib/pages/error/storage_error_page.dart | 8 +- lib/pages/history/history_page.dart | 16 +-- lib/pages/index_module.dart | 2 +- lib/pages/info/character_page.dart | 18 +-- lib/pages/info/info_page.dart | 22 +-- lib/pages/info/info_tabview.dart | 38 ++--- lib/pages/info/rating_review_dialog.dart | 68 ++++----- lib/pages/info/source_sheet.dart | 76 +++++----- lib/pages/init_page.dart | 44 +++--- lib/pages/logs/logs_page.dart | 16 +-- lib/pages/menu/menu.dart | 16 +-- lib/pages/my/my_controller.dart | 8 +- lib/pages/my/my_page.dart | 56 ++++---- .../player_playback_controller.dart | 6 +- .../player_syncplay_controller.dart | 20 +-- lib/pages/player/episode_comments_sheet.dart | 20 +-- lib/pages/player/player_adjustment_hud.dart | 2 +- lib/pages/player/player_item.dart | 122 ++++++++-------- lib/pages/player/player_item_panel.dart | 98 ++++++------- .../player/smallest_player_item_panel.dart | 78 +++++----- .../plugin_editor/plugin_editor_page.dart | 72 +++++----- lib/pages/plugin_editor/plugin_shop_page.dart | 36 ++--- lib/pages/plugin_editor/plugin_test_page.dart | 78 +++++----- lib/pages/plugin_editor/plugin_view_page.dart | 72 +++++----- lib/pages/popular/popular_page.dart | 12 +- lib/pages/search/image_search_page.dart | 70 ++++----- lib/pages/search/search_controller.dart | 8 +- lib/pages/search/search_page.dart | 24 ++-- .../settings/danmaku/danmaku_settings.dart | 48 +++---- .../danmaku/danmaku_settings_sheet.dart | 36 ++--- .../danmaku/danmaku_shield_settings.dart | 10 +- .../danmaku_shield_settings_sheet.dart | 10 +- lib/pages/settings/decoder_settings.dart | 4 +- lib/pages/settings/displaymode_settings.dart | 8 +- lib/pages/settings/download_settings.dart | 30 ++-- lib/pages/settings/interface_settings.dart | 20 +-- lib/pages/settings/keyboard_settings.dart | 6 +- lib/pages/settings/player_settings.dart | 98 ++++++------- .../settings/proxy/proxy_editor_page.dart | 18 +-- .../settings/proxy/proxy_settings_page.dart | 14 +- lib/pages/settings/renderer_settings.dart | 4 +- .../settings/super_resolution_settings.dart | 16 +-- lib/pages/settings/theme_settings_page.dart | 38 ++--- lib/pages/timeline/timeline_controller.dart | 2 +- lib/pages/timeline/timeline_page.dart | 86 +++++------ lib/pages/video/video_controller.dart | 16 +-- lib/pages/video/video_page.dart | 42 +++--- .../webdav_editor/webdav_editor_page.dart | 12 +- lib/pages/webdav_editor/webdav_setting.dart | 52 +++---- lib/plugins/plugins.dart | 2 +- lib/request/apis/bangumi_api.dart | 10 +- lib/request/core/network_config.dart | 2 +- lib/request/core/network_error_mapper.dart | 30 ++-- .../download/background_download_service.dart | 16 +-- lib/services/download/download_manager.dart | 26 ++-- .../proxy_aware_image_cache_manager.dart | 2 +- lib/services/network/proxy_manager.dart | 4 +- .../player/external_playback_launcher.dart | 16 +-- lib/services/player/remote.dart | 12 +- .../player/timed_shutdown_service.dart | 30 ++-- lib/services/sync/bangumi_sync_service.dart | 38 ++--- lib/services/sync/webdav.dart | 2 +- lib/services/update/auto_updater.dart | 94 ++++++------ lib/utils/anime_season.dart | 12 +- lib/utils/constants.dart | 134 +++++++++--------- lib/utils/date_time.dart | 24 ++-- .../impl/captcha_webview_linux_impl.dart | 2 +- .../impl/captcha_webview_windows_impl.dart | 4 +- .../impl/video_webview_android_impl.dart | 6 +- .../video/impl/video_webview_impl.dart | 6 +- .../video/impl/video_webview_linux_impl.dart | 2 +- .../impl/video_webview_windows_impl.dart | 4 +- 96 files changed, 1277 insertions(+), 1285 deletions(-) diff --git a/assets/statements/statements.txt b/assets/statements/statements.txt index 182232d54..3b893c238 100644 --- a/assets/statements/statements.txt +++ b/assets/statements/statements.txt @@ -1,11 +1,11 @@ -在使用本软件之前,请您仔细阅读以下内容,并确保您充分理解并同意以下条款: -1、本软件为开源软件,您应该免费获取和使用。如果您是从第三方付费获取,建议您向其索取赔偿。 -2、本软件完全基于您个人意愿使用,您应该对自己的使用行为和所有结果承担全部责任。 -3、本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。 -4、本软件并不保证与所有操作系统或硬件设备兼容。本软件作者或贡献者不对因使用本软件而产生的任何技术或安全问题承担责任。 -5、本软件作者或贡献者不承担因使用本软件而造成的任何直接、间接、特殊或后果性的损失或损害的责任,包括但不限于财产损失、商业利润损失、信息或数据丢失或损坏等。 -6、本软件使用者应遵守国家相关法律法规和使用规范,不得利用本软件从事任何违法违规行为。如因使用本软件而导致的违法行为,使用者应承担相应的法律责任。 -7、本软件不会收集、存储、使用任何用户的个人信息,包括但不限于姓名、地址、电子邮件地址、电话号码等。在使用本软件过程中,不会进行任何形式的个人信息采集。 -8、本软件作者或贡献者保留随时修改、增加、删除本免责声明中的内容而不另行通知的权利。 -9、如果本软件存在侵犯您的合法权益的情况,请及时与作者联系,作者将会及时删除有关内容。 -如您不同意本免责声明中的任何内容,请勿使用本软件。使用本软件即代表您已完全理解并同意上述内容。 \ No newline at end of file +Before using this software, please read the following carefully and make sure you fully understand and agree to the following terms: +1. This software is open source and you should obtain and use it for free. If you obtained it from a third party for a fee, you are advised to seek a refund from them. +2. You use this software entirely of your own free will, and you are solely responsible for your usage and all of its consequences. +3. This software is intended only for non-commercial purposes such as learning, communication, and research. Using this software for commercial purposes is strictly prohibited. Any commercial activity is unrelated to this software. +4. This software does not guarantee compatibility with all operating systems or hardware devices. The authors or contributors of this software are not responsible for any technical or security issues arising from its use. +5. The authors or contributors of this software are not liable for any direct, indirect, special, or consequential loss or damage caused by using this software, including but not limited to property loss, loss of business profits, and loss or corruption of information or data. +6. Users of this software must comply with relevant national laws, regulations, and usage standards, and must not use this software for any illegal or non-compliant activity. Users shall bear the corresponding legal liability for any illegal acts resulting from the use of this software. +7. This software does not collect, store, or use any user personal information, including but not limited to name, address, email address, or phone number. No personal information is collected in any form while using this software. +8. The authors or contributors of this software reserve the right to modify, add to, or remove the contents of this disclaimer at any time without further notice. +9. If this software infringes upon your legitimate rights and interests, please contact the author promptly, and the author will remove the relevant content in a timely manner. +If you do not agree with any part of this disclaimer, please do not use this software. Using this software means you have fully understood and agreed to the above. diff --git a/lib/app_widget.dart b/lib/app_widget.dart index f785b1580..534a5873e 100644 --- a/lib/app_widget.dart +++ b/lib/app_widget.dart @@ -215,12 +215,12 @@ class _AppWidgetState extends State bool saveExitBehavior = false; // 下次不再询问? return AlertDialog( - title: const Text('退出确认'), + title: const Text('Exit confirmation'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text('您想要退出 Kazumi 吗?'), + const Text('Do you want to exit Kazumi?'), const SizedBox(height: 24), StatefulBuilder(builder: (context, setState) { onChanged(value) { @@ -233,7 +233,7 @@ class _AppWidgetState extends State spacing: 8, children: [ Checkbox(value: saveExitBehavior, onChanged: onChanged), - const Text('下次不再询问'), + const Text('Do not ask again'), ], ); }), @@ -247,7 +247,7 @@ class _AppWidgetState extends State } exit(0); }, - child: const Text('退出 Kazumi')), + child: const Text('Exit Kazumi')), TextButton( onPressed: () async { if (saveExitBehavior) { @@ -256,9 +256,9 @@ class _AppWidgetState extends State KazumiDialog.dismiss(); windowManager.hide(); }, - child: const Text('最小化至托盘')), + child: const Text('Minimize to tray')), const TextButton( - onPressed: KazumiDialog.dismiss, child: Text('取消')), + onPressed: KazumiDialog.dismiss, child: Text('Cancel')), ], ); }); @@ -308,9 +308,9 @@ class _AppWidgetState extends State } Menu trayMenu = Menu(items: [ - MenuItem(key: 'show_window', label: '显示窗口'), + MenuItem(key: 'show_window', label: 'Show window'), MenuItem.separator(), - MenuItem(key: 'exit', label: '退出 Kazumi') + MenuItem(key: 'exit', label: 'Exit Kazumi') ]); await trayManager.setContextMenu(trayMenu); } @@ -346,12 +346,8 @@ class _AppWidgetState extends State return MaterialApp.router( title: "Kazumi", localizationsDelegates: GlobalMaterialLocalizations.delegates, - supportedLocales: const [ - Locale.fromSubtags( - languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN") - ], - locale: const Locale.fromSubtags( - languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN"), + supportedLocales: const [Locale('en')], + locale: const Locale('en'), theme: lightTheme, darkTheme: effectiveDarkTheme, themeMode: themeProvider.themeMode, diff --git a/lib/bean/card/bangumi_card.dart b/lib/bean/card/bangumi_card.dart index dabe803a6..83fde595f 100644 --- a/lib/bean/card/bangumi_card.dart +++ b/lib/bean/card/bangumi_card.dart @@ -29,7 +29,7 @@ class BangumiCardV extends StatelessWidget { onTap: () { if (!canTap) { KazumiDialog.showToast( - message: '编辑模式', + message: 'Edit mode', ); return; } diff --git a/lib/bean/card/bangumi_history_card.dart b/lib/bean/card/bangumi_history_card.dart index ae3763a6a..004c77342 100644 --- a/lib/bean/card/bangumi_history_card.dart +++ b/lib/bean/card/bangumi_history_card.dart @@ -40,11 +40,11 @@ class _BangumiHistoryCardVState extends State { Future _onTap() async { if (widget.showDelete) { - KazumiDialog.showToast(message: '编辑模式'); + KazumiDialog.showToast(message: 'Edit mode'); return; } KazumiDialog.showLoading( - msg: '获取中', + msg: 'Loading', barrierDismissible: isDesktop(), onDismiss: () { videoPageController.cancelQueryRoads(); @@ -60,7 +60,7 @@ class _BangumiHistoryCardVState extends State { } if (!flag) { KazumiDialog.dismiss(); - KazumiDialog.showToast(message: '未找到关联番剧源'); + KazumiDialog.showToast(message: 'No associated anime source found'); return; } videoPageController.bangumiItem = widget.historyItem.bangumiItem; @@ -89,7 +89,7 @@ class _BangumiHistoryCardVState extends State { ? widget.historyItem.bangumiItem.name : widget.historyItem.bangumiItem.nameCn; final String episodeText = widget.historyItem.lastWatchEpisodeName.isEmpty - ? '第${widget.historyItem.lastWatchEpisode}话' + ? 'Ep ${widget.historyItem.lastWatchEpisode}' : widget.historyItem.lastWatchEpisodeName; return Dismissible( @@ -237,7 +237,7 @@ class _BangumiHistoryCardVState extends State { size: 20, color: colorScheme.onSurfaceVariant, ), - tooltip: '番剧详情', + tooltip: 'Anime details', onPressed: () { Modular.to.pushNamed( '/info/', @@ -252,7 +252,7 @@ class _BangumiHistoryCardVState extends State { Icons.delete_outline, color: colorScheme.error, ), - tooltip: '删除记录', + tooltip: 'Delete record', onPressed: () { widget.onDeleted?.call(); }, diff --git a/lib/bean/card/bangumi_info_card.dart b/lib/bean/card/bangumi_info_card.dart index 22d3dda80..139ca06ff 100644 --- a/lib/bean/card/bangumi_info_card.dart +++ b/lib/bean/card/bangumi_info_card.dart @@ -33,7 +33,7 @@ class _BangumiInfoCardVState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - ' 评分透视:', + ' Rating breakdown:', ), SizedBox(height: 16), AspectRatio( @@ -66,7 +66,7 @@ class _BangumiInfoCardVState extends State { widget.bangumiItem.votes * 100; return BarTooltipItem( - '${percentage.toStringAsFixed(2)}% (${widget.bangumiItem.votesCount[groupIndex]}人)', + '${percentage.toStringAsFixed(2)}% (${widget.bangumiItem.votesCount[groupIndex]} votes)', TextStyle( color: Theme.of(context).colorScheme.onInverseSurface), @@ -171,7 +171,7 @@ class _BangumiInfoCardVState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '放送开始:', + 'Air date:', ), Text( widget.bangumiItem.airDate == '' @@ -186,8 +186,8 @@ class _BangumiInfoCardVState extends State { SizedBox(height: 8), Text( widget.showRating - ? '${widget.bangumiItem.votes} 人评分:' - : '*** 人评分:', + ? '${widget.bangumiItem.votes} ratings:' + : '*** ratings:', ), if (widget.isLoading) // Skeleton Loader 占位符 diff --git a/lib/bean/card/comments_card.dart b/lib/bean/card/comments_card.dart index 81f308a0e..c628d4e25 100644 --- a/lib/bean/card/comments_card.dart +++ b/lib/bean/card/comments_card.dart @@ -90,7 +90,7 @@ class CommentsCard extends StatelessWidget { color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(4), ), - child: Text('我的吐槽', + child: Text('My comment', style: TextStyle(fontSize: 12,color: Theme.of(context).colorScheme.primaryContainer), )) ], diff --git a/lib/bean/card/episode_comments_card.dart b/lib/bean/card/episode_comments_card.dart index 10f0e0d43..080787606 100644 --- a/lib/bean/card/episode_comments_card.dart +++ b/lib/bean/card/episode_comments_card.dart @@ -16,7 +16,7 @@ class EpisodeCommentsCard extends StatelessWidget { // 对 用户评论 做判空操作,如果为空则显示“用户已删除” String userComment = commentItem.comment.comment; if (userComment.isEmpty) { - userComment = "<用户已删除>"; + userComment = ""; } return Card( diff --git a/lib/bean/settings/color_type.dart b/lib/bean/settings/color_type.dart index 373eca1cb..0cd4be013 100644 --- a/lib/bean/settings/color_type.dart +++ b/lib/bean/settings/color_type.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; final List> colorThemeTypes = [ - {'color': Colors.green, 'label': '默认'}, - {'color': Colors.teal, 'label': '青色'}, - {'color': Colors.blue, 'label': '蓝色'}, - {'color': Colors.indigo, 'label': '靛蓝色'}, - {'color': const Color(0xff6750a4), 'label': '紫罗兰色'}, - {'color': Colors.pink, 'label': '粉红色'}, - {'color': Colors.yellow, 'label': '黄色'}, - {'color': Colors.orange, 'label': '橙色'}, - {'color': Colors.deepOrange, 'label': '深橙色'}, + {'color': Colors.green, 'label': 'Default'}, + {'color': Colors.teal, 'label': 'Teal'}, + {'color': Colors.blue, 'label': 'Blue'}, + {'color': Colors.indigo, 'label': 'Indigo'}, + {'color': const Color(0xff6750a4), 'label': 'Violet'}, + {'color': Colors.pink, 'label': 'Pink'}, + {'color': Colors.yellow, 'label': 'Yellow'}, + {'color': Colors.orange, 'label': 'Orange'}, + {'color': Colors.deepOrange, 'label': 'Deep Orange'}, ]; diff --git a/lib/bean/widget/bangumi_mirror_error_widget.dart b/lib/bean/widget/bangumi_mirror_error_widget.dart index 09b7bd845..4fb783a41 100644 --- a/lib/bean/widget/bangumi_mirror_error_widget.dart +++ b/lib/bean/widget/bangumi_mirror_error_widget.dart @@ -20,18 +20,18 @@ class BangumiMirrorErrorWidget extends StatelessWidget { return GeneralErrorWidget( errMsg: - '啊咧(⊙.⊙) 无法加载数据\nBangumi 镜像${mirrorEnabled ? '已启用' : '已禁用'}', + 'Oh no (⊙.⊙) Failed to load data\nBangumi mirror ${mirrorEnabled ? 'enabled' : 'disabled'}', actions: [ GeneralErrorButton( onPressed: () async { await Modular.to.pushNamed('/settings/webdav/'); onSettingsReturned?.call(); }, - text: '镜像开关', + text: 'Mirror toggle', ), GeneralErrorButton( onPressed: onRetry, - text: '点击重试', + text: 'Tap to retry', ), ], ); diff --git a/lib/bean/widget/collect_button.dart b/lib/bean/widget/collect_button.dart index e6a13cca3..ea4915a60 100644 --- a/lib/bean/widget/collect_button.dart +++ b/lib/bean/widget/collect_button.dart @@ -51,17 +51,17 @@ class _CollectButtonState extends State { String getTypeStringByInt(int collectType) { switch (collectType) { case 1: - return "在看"; + return "Watching"; case 2: - return "想看"; + return "Plan to watch"; case 3: - return "搁置"; + return "On hold"; case 4: - return "看过"; + return "Watched"; case 5: - return "抛弃"; + return "Dropped"; default: - return "未追"; + return "Not tracked"; } } diff --git a/lib/bean/widget/image_preview.dart b/lib/bean/widget/image_preview.dart index dcdeef6cf..22777e71f 100644 --- a/lib/bean/widget/image_preview.dart +++ b/lib/bean/widget/image_preview.dart @@ -121,7 +121,7 @@ class _ImageViewerState extends State { ), const SizedBox(height: 8), Text( - '图片加载失败', + 'Failed to load image', style: TextStyle( color: Theme.of(context).colorScheme.onErrorContainer, ), diff --git a/lib/main.dart b/lib/main.dart index 5af15c549..067eab5fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,14 +50,10 @@ void main() async { }); } runApp(MaterialApp( - title: '初始化失败', + title: 'Initialization failed', localizationsDelegates: GlobalMaterialLocalizations.delegates, - supportedLocales: const [ - Locale.fromSubtags( - languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN") - ], - locale: const Locale.fromSubtags( - languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN"), + supportedLocales: const [Locale('en')], + locale: const Locale('en'), builder: (context, child) { return const StorageErrorPage(); })); diff --git a/lib/modules/bangumi/bangumi_collection_type.dart b/lib/modules/bangumi/bangumi_collection_type.dart index 4365d8529..7ec6bad7c 100644 --- a/lib/modules/bangumi/bangumi_collection_type.dart +++ b/lib/modules/bangumi/bangumi_collection_type.dart @@ -1,17 +1,17 @@ /// Bangumi 收藏类型枚举 /// via: https://bangumi.github.io/api/#/model-CollectionType enum BangumiCollectionType { - unknown(0, '未知'), + unknown(0, 'Unknown'), - planToWatch(1, '想看'), + planToWatch(1, 'Plan to watch'), - watched(2, '看过'), + watched(2, 'Watched'), - watching(3, '在看'), + watching(3, 'Watching'), - onHold(4, '搁置'), + onHold(4, 'On hold'), - abandoned(5, '抛弃'); + abandoned(5, 'Dropped'); const BangumiCollectionType(this.value, this.label); diff --git a/lib/modules/bangumi/sync_priority.dart b/lib/modules/bangumi/sync_priority.dart index ed7f07f36..0cd47cb44 100644 --- a/lib/modules/bangumi/sync_priority.dart +++ b/lib/modules/bangumi/sync_priority.dart @@ -1,6 +1,6 @@ enum BangumiSyncPriority { - localFirst(0, '本地优先'), - bangumiFirst(1, 'Bangumi优先'); + localFirst(0, 'Local first'), + bangumiFirst(1, 'Bangumi first'); const BangumiSyncPriority(this.value, this.label); diff --git a/lib/modules/characters/character_item.dart b/lib/modules/characters/character_item.dart index bd0eb7760..8509dc535 100644 --- a/lib/modules/characters/character_item.dart +++ b/lib/modules/characters/character_item.dart @@ -74,7 +74,7 @@ class CharacterItem { id: json['id'] ?? 0, type: json['type'] ?? 0, name: json['name'] ?? '', - relation: json['relation'] ?? '未知', + relation: json['relation'] ?? 'Unknown', avator: CharacterAvator.fromJson(json['images'] as Map), actorList: resActorList, diff --git a/lib/modules/collect/collect_type.dart b/lib/modules/collect/collect_type.dart index 1ed4b3c4e..be99d5dad 100644 --- a/lib/modules/collect/collect_type.dart +++ b/lib/modules/collect/collect_type.dart @@ -3,22 +3,22 @@ /// 用于标识番剧的收藏状态 enum CollectType { /// 未收藏 - none(0, '未收藏'), + none(0, 'Not collected'), /// 在看 - watching(1, '在看'), + watching(1, 'Watching'), /// 想看 - planToWatch(2, '想看'), + planToWatch(2, 'Plan to watch'), /// 搁置 - onHold(3, '搁置'), + onHold(3, 'On hold'), /// 看过 - watched(4, '看过'), + watched(4, 'Watched'), /// 抛弃 - abandoned(5, '抛弃'); + abandoned(5, 'Dropped'); const CollectType(this.value, this.label); diff --git a/lib/pages/about/about_module.dart b/lib/pages/about/about_module.dart index 27d1fd5f8..d4de79b07 100644 --- a/lib/pages/about/about_module.dart +++ b/lib/pages/about/about_module.dart @@ -17,7 +17,7 @@ class AboutModule extends Module { child: (_) => const LicensePage( applicationName: 'Kazumi', applicationVersion: ApiEndpoints.version, - applicationLegalese: '开源许可证', + applicationLegalese: 'Open source licenses', ), ); } diff --git a/lib/pages/about/about_page.dart b/lib/pages/about/about_page.dart index 61debe3f6..8df104316 100644 --- a/lib/pages/about/about_page.dart +++ b/lib/pages/about/about_page.dart @@ -22,7 +22,7 @@ class AboutPage extends StatefulWidget { } class _AboutPageState extends State { - final exitBehaviorTitles = ['退出 Kazumi', '最小化至托盘', '每次都询问']; + final exitBehaviorTitles = ['Exit Kazumi', 'Minimize to tray', 'Always ask']; late dynamic defaultDanmakuArea; late dynamic defaultThemeMode; late dynamic defaultThemeColor; @@ -101,15 +101,15 @@ class _AboutPageState extends State { KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('缓存管理'), - content: const Text('缓存为番剧封面, 清除后加载时需要重新下载,确认要清除缓存吗?'), + title: const Text('Cache management'), + content: const Text('The cache stores anime cover images. After clearing, they will be downloaded again when loading. Clear the cache?'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -120,7 +120,7 @@ class _AboutPageState extends State { } catch (_) {} KazumiDialog.dismiss(); }, - child: const Text('确认'), + child: const Text('Confirm'), ), ], ); @@ -137,7 +137,7 @@ class _AboutPageState extends State { onBackPressed(context); }, child: Scaffold( - appBar: const SysAppBar(title: Text('关于')), + appBar: const SysAppBar(title: Text('About')), // backgroundColor: Colors.transparent, body: SettingsList( maxWidth: 1000, @@ -149,28 +149,28 @@ class _AboutPageState extends State { Modular.to.pushNamed('/settings/about/license'); }, title: - Text('开源许可证', style: TextStyle(fontFamily: fontFamily)), - description: Text('查看所有开源许可证', + Text('Open source licenses', style: TextStyle(fontFamily: fontFamily)), + description: Text('View all open source licenses', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( - title: Text('外部链接', style: TextStyle(fontFamily: fontFamily)), + title: Text('External links', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { launchUrl(Uri.parse(ApiEndpoints.projectUrl), mode: LaunchMode.externalApplication); }, - title: Text('项目主页', style: TextStyle(fontFamily: fontFamily)), + title: Text('Project homepage', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) { launchUrl(Uri.parse(ApiEndpoints.sourceUrl), mode: LaunchMode.externalApplication); }, - title: Text('代码仓库', style: TextStyle(fontFamily: fontFamily)), + title: Text('Code repository', style: TextStyle(fontFamily: fontFamily)), value: Text('Github', style: TextStyle(fontFamily: fontFamily)), ), @@ -179,7 +179,7 @@ class _AboutPageState extends State { launchUrl(Uri.parse(ApiEndpoints.iconUrl), mode: LaunchMode.externalApplication); }, - title: Text('图标创作', style: TextStyle(fontFamily: fontFamily)), + title: Text('Icon design', style: TextStyle(fontFamily: fontFamily)), value: Text('Pixiv', style: TextStyle(fontFamily: fontFamily)), ), @@ -188,7 +188,7 @@ class _AboutPageState extends State { launchUrl(Uri.parse(ApiEndpoints.bangumiIndex), mode: LaunchMode.externalApplication); }, - title: Text('番剧索引', style: TextStyle(fontFamily: fontFamily)), + title: Text('Anime index', style: TextStyle(fontFamily: fontFamily)), value: Text('Bangumi', style: TextStyle(fontFamily: fontFamily)), ), @@ -197,7 +197,7 @@ class _AboutPageState extends State { launchUrl(Uri.parse('https://trace.moe'), mode: LaunchMode.externalApplication); }, - title: Text('以图搜番', style: TextStyle(fontFamily: fontFamily)), + title: Text('Search anime by image', style: TextStyle(fontFamily: fontFamily)), value: Text('trace.moe', style: TextStyle(fontFamily: fontFamily)), ), @@ -206,7 +206,7 @@ class _AboutPageState extends State { launchUrl(Uri.parse(ApiEndpoints.dandanIndex), mode: LaunchMode.externalApplication); }, - title: Text('弹幕来源', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku source', style: TextStyle(fontFamily: fontFamily)), description: Text('ID: ${dandanCredentials['id']}', style: TextStyle(fontFamily: fontFamily)), value: Text('DanDanPlay', @@ -215,7 +215,7 @@ class _AboutPageState extends State { ], ), SettingsSection( - title: Text('社区', style: TextStyle(fontFamily: fontFamily)), + title: Text('Community', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { @@ -224,15 +224,15 @@ class _AboutPageState extends State { }, title: Text('Telegram', style: TextStyle(fontFamily: fontFamily)), - description: Text('Kazumi 官方 Telegram 群组', + description: Text('Kazumi official Telegram group', style: TextStyle(fontFamily: fontFamily)), - value: Text('点击加入', style: TextStyle(fontFamily: fontFamily)), + value: Text('Tap to join', style: TextStyle(fontFamily: fontFamily)), ), ], ), if (isDesktop()) // 之后如果有非桌面平台的新选项可以移除 SettingsSection( - title: Text('默认行为', style: TextStyle(fontFamily: fontFamily)), + title: Text('Default behavior', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { @@ -243,7 +243,7 @@ class _AboutPageState extends State { } }, title: - Text('关闭时', style: TextStyle(fontFamily: fontFamily)), + Text('On close', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: menuController, @@ -286,7 +286,7 @@ class _AboutPageState extends State { onPressed: (_) { Modular.to.pushNamed('/settings/about/logs'); }, - title: Text('错误日志', style: TextStyle(fontFamily: fontFamily)), + title: Text('Error logs', style: TextStyle(fontFamily: fontFamily)), ), ], ), @@ -296,16 +296,16 @@ class _AboutPageState extends State { onPressed: (_) { _showCacheDialog(); }, - title: Text('清除缓存', style: TextStyle(fontFamily: fontFamily)), + title: Text('Clear cache', style: TextStyle(fontFamily: fontFamily)), value: _cacheSizeMB == -1 - ? Text('统计中...', style: TextStyle(fontFamily: fontFamily)) + ? Text('Calculating...', style: TextStyle(fontFamily: fontFamily)) : Text('${_cacheSizeMB.toStringAsFixed(2)}MB', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( - title: Text('应用更新', style: TextStyle(fontFamily: fontFamily)), + title: Text('App updates', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { @@ -313,15 +313,15 @@ class _AboutPageState extends State { await setting.put(SettingBoxKey.autoUpdate, autoUpdate); setState(() {}); }, - title: Text('自动更新', style: TextStyle(fontFamily: fontFamily)), + title: Text('Auto update', style: TextStyle(fontFamily: fontFamily)), initialValue: autoUpdate, ), SettingsTile.navigation( onPressed: (_) { myController.checkUpdate(); }, - title: Text('检查更新', style: TextStyle(fontFamily: fontFamily)), - value: Text('当前版本 ${ApiEndpoints.version}', + title: Text('Check for updates', style: TextStyle(fontFamily: fontFamily)), + value: Text('Current version ${ApiEndpoints.version}', style: TextStyle(fontFamily: fontFamily)), ), ], diff --git a/lib/pages/bangumi/bangumi_setting.dart b/lib/pages/bangumi/bangumi_setting.dart index 86745e522..540c3251c 100644 --- a/lib/pages/bangumi/bangumi_setting.dart +++ b/lib/pages/bangumi/bangumi_setting.dart @@ -56,7 +56,7 @@ class _BangumiEditorPageState extends State { final syncEnable = setting.get(SettingBoxKey.bangumiSyncEnable, defaultValue: false); if (!syncEnable) { - KazumiDialog.showToast(message: '请先开启 Bangumi 同步'); + KazumiDialog.showToast(message: 'Please enable Bangumi sync first'); return; } @@ -84,7 +84,7 @@ class _BangumiEditorPageState extends State { }, ); } catch (e) { - KazumiDialog.showToast(message: 'Bangumi同步失败 $e'); + KazumiDialog.showToast(message: 'Bangumi sync failed $e'); } finally { if (KazumiDialog.observer.hasKazumiDialog) { KazumiDialog.dismiss(); @@ -103,7 +103,7 @@ class _BangumiEditorPageState extends State { return PopScope( canPop: !syncCollectiblesing, child: Scaffold( - appBar: const SysAppBar(title: Text('Bangumi 配置')), + appBar: const SysAppBar(title: Text('Bangumi configuration')), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Center( @@ -145,9 +145,9 @@ class _BangumiEditorPageState extends State { setState(() {}); } }, - title: Text('即时同步提示', + title: Text('Instant sync prompt', style: TextStyle(fontFamily: fontFamily)), - description: Text('点击追番按钮触发即时同步时显示提示框', + description: Text('Show a prompt when tapping the track button triggers instant sync', style: TextStyle(fontFamily: fontFamily)), initialValue: bangumiImmediateSyncToastEnable, ), @@ -159,9 +159,9 @@ class _BangumiEditorPageState extends State { syncPriorityMenuController.open(); } }, - title: Text('同步优先级', + title: Text('Sync priority', style: TextStyle(fontFamily: fontFamily)), - description: Text('当本地与 Bangumi 状态不一致时优先使用哪个状态', + description: Text('Which state takes priority when the local and Bangumi states differ', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, @@ -208,9 +208,9 @@ class _BangumiEditorPageState extends State { onPressed: (_) async { await syncWithProgress(); }, - title: Text("立即同步状态", + title: Text("Sync status now", style: TextStyle(fontFamily: fontFamily)), - description: Text('同步状态不一致或仅存在于本地/远端的条目', + description: Text('Entries whose status differs or that exist only locally or remotely', style: TextStyle(fontFamily: fontFamily)), ), ], @@ -224,11 +224,11 @@ class _BangumiEditorPageState extends State { await launchUrl(url, mode: LaunchMode.externalApplication); } else { - KazumiDialog.showToast(message: '无法打开链接'); + KazumiDialog.showToast(message: 'Cannot open link'); } }, child: Text( - '你可以点击此处前往 Bangumi 生成 Access Token', + 'You can tap here to go to Bangumi and generate an Access Token', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.primary, @@ -252,7 +252,7 @@ class _BangumiEditorPageState extends State { defaultValue: false); if (token.isEmpty && bangumiSyncEnable) { - KazumiDialog.showToast(message: 'Access Token 不能为空'); + KazumiDialog.showToast(message: 'Access Token cannot be empty'); return; } setState(() { @@ -263,7 +263,7 @@ class _BangumiEditorPageState extends State { if (token.isEmpty) { bangumi.reset(); - KazumiDialog.showToast(message: 'Bangumi Token 为空,请检查'); + KazumiDialog.showToast(message: 'Bangumi Token is empty, please check'); if (!mounted) return; setState(() { isVerifying = false; @@ -271,11 +271,11 @@ class _BangumiEditorPageState extends State { return; } - KazumiDialog.showToast(message: '正在测试 Bangumi Token...'); + KazumiDialog.showToast(message: 'Testing Bangumi Token...'); try { await bangumi.init(); } catch (e) { - KazumiDialog.showToast(message: '验证失败:${e.toString()}'); + KazumiDialog.showToast(message: 'Verification failed: ${e.toString()}'); await setting.put(SettingBoxKey.bangumiSyncEnable, false); if (!mounted) return; setState(() { @@ -285,7 +285,7 @@ class _BangumiEditorPageState extends State { } KazumiDialog.showToast( - message: '测试成功,用户名:${bangumi.username}'); + message: 'Test succeeded, username: ${bangumi.username}'); if (!mounted) return; setState(() { isVerifying = false; @@ -308,7 +308,7 @@ class _BangumiSyncProgressDialog extends StatefulWidget { class _BangumiSyncProgressDialogState extends State<_BangumiSyncProgressDialog> { - String _progressText = '准备同步 Bangumi 状态...'; + String _progressText = 'Preparing to sync Bangumi status...'; double? _progressValue; void update(String text, double? value) { @@ -333,7 +333,7 @@ class _BangumiSyncProgressDialogState crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Bangumi 同步进行中', + 'Bangumi sync in progress', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, diff --git a/lib/pages/collect/collect_controller.dart b/lib/pages/collect/collect_controller.dart index c6a16e355..b0be6e739 100644 --- a/lib/pages/collect/collect_controller.dart +++ b/lib/pages/collect/collect_controller.dart @@ -132,28 +132,28 @@ abstract class _CollectController with Store { return KazumiDialog.show<_BangumiDeleteSyncAction>( clickMaskDismiss: true, builder: (context) => AlertDialog( - title: const Text('Bangumi 不支持删除收藏'), + title: const Text('Bangumi does not support deleting collections'), content: const Text( - '因为安全考虑,Bangumi 未提供删除接口,您可以选择把本地和远端标记为“抛弃”,或者选择仅删除本地收藏并打开网页后手动删除 Bangumi 数据。', + 'For security reasons, Bangumi does not provide a delete interface. You can mark both local and remote as “Dropped”, or delete only the local collection and remove the Bangumi data manually after opening the web page.', ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(_BangumiDeleteSyncAction.cancel); }, - child: const Text('取消'), + child: const Text('Cancel'), ), TextButton( onPressed: () { Navigator.of(context).pop(_BangumiDeleteSyncAction.openWeb); }, - child: const Text('打开网页'), + child: const Text('Open web page'), ), FilledButton( onPressed: () { Navigator.of(context).pop(_BangumiDeleteSyncAction.markAbandoned); }, - child: const Text('标记为抛弃'), + child: const Text('Mark as dropped'), ), ], ), @@ -166,7 +166,7 @@ abstract class _CollectController with Store { await launchUrl(url, mode: LaunchMode.externalApplication); return; } - KazumiDialog.showToast(message: '无法打开 Bangumi 网页'); + KazumiDialog.showToast(message: 'Cannot open the Bangumi web page'); } Future _syncBangumiCollectIfEnabled( @@ -184,7 +184,7 @@ abstract class _CollectController with Store { final bangumi = BangumiSyncService(); if (!bangumi.initialized) { - KazumiDialog.showToast(message: 'Bangumi 未初始化,同步失败,已取消本次状态修改'); + KazumiDialog.showToast(message: 'Bangumi is not initialized, sync failed, this status change has been canceled'); KazumiLogger().w( 'Bangumi: immediate collect sync skipped because Bangumi is not initialized. ' 'bangumiId=$bangumiId, type=$localType', @@ -193,15 +193,15 @@ abstract class _CollectController with Store { } try { if (showImmediateSyncToast) { - KazumiDialog.showToast(message: '正在同步到 Bangumi...'); + KazumiDialog.showToast(message: 'Syncing to Bangumi...'); } final bool synced = await bangumi.syncCollectibleWhenIdle(bangumiId, localType); if (synced && showImmediateSyncToast) { - KazumiDialog.showToast(message: '已同步到 Bangumi'); + KazumiDialog.showToast(message: 'Synced to Bangumi'); return true; } else if (!synced) { - KazumiDialog.showToast(message: '同步到 Bangumi 失败,已取消本次状态修改'); + KazumiDialog.showToast(message: 'Failed to sync to Bangumi, this status change has been canceled'); KazumiLogger().w( 'Bangumi: immediate collect sync did not complete. bangumiId=$bangumiId, type=$localType', ); @@ -209,7 +209,7 @@ abstract class _CollectController with Store { } return true; } catch (e, stackTrace) { - KazumiDialog.showToast(message: '同步到 Bangumi 失败,已取消本次状态修改: $e'); + KazumiDialog.showToast(message: 'Failed to sync to Bangumi, this status change has been canceled: $e'); KazumiLogger().e( 'Bangumi: immediate collect sync failed. bangumiId=$bangumiId, type=$localType', error: e, @@ -228,11 +228,11 @@ abstract class _CollectController with Store { final bool webDavCollectEnable = setting.get(SettingBoxKey.webDavEnableCollect, defaultValue: false); if (!webDavCollectEnable) { - KazumiDialog.showToast(message: '未开启WebDav收藏同步'); + KazumiDialog.showToast(message: 'WebDav collection sync is not enabled'); return false; } if (!WebDav().initialized) { - KazumiDialog.showToast(message: '未开启WebDav同步或配置无效'); + KazumiDialog.showToast(message: 'WebDav sync is not enabled or the configuration is invalid'); return false; } bool flag = true; @@ -240,7 +240,7 @@ abstract class _CollectController with Store { await WebDav().ping(); } catch (e) { KazumiLogger().e('WebDav: WebDav connection failed', error: e); - KazumiDialog.showToast(message: 'WebDav连接失败: $e'); + KazumiDialog.showToast(message: 'WebDav connection failed: $e'); flag = false; } if (!flag) { @@ -249,10 +249,10 @@ abstract class _CollectController with Store { try { await WebDav().syncCollectibles(); if (showSuccessToast) { - KazumiDialog.showToast(message: 'WebDav同步完成'); + KazumiDialog.showToast(message: 'WebDav sync complete'); } } catch (e) { - KazumiDialog.showToast(message: 'WebDav同步失败 $e'); + KazumiDialog.showToast(message: 'WebDav sync failed $e'); return false; } loadCollectibles(); @@ -266,11 +266,11 @@ abstract class _CollectController with Store { final bool webDavCollectEnable = setting.get(SettingBoxKey.webDavEnableCollect, defaultValue: false); if (!webDavCollectEnable) { - KazumiDialog.showToast(message: '未开启WebDav收藏同步'); + KazumiDialog.showToast(message: 'WebDav collection sync is not enabled'); return false; } if (!WebDav().initialized) { - KazumiDialog.showToast(message: '未开启WebDav同步或配置无效'); + KazumiDialog.showToast(message: 'WebDav sync is not enabled or the configuration is invalid'); return false; } bool flag = true; @@ -278,7 +278,7 @@ abstract class _CollectController with Store { await WebDav().ping(); } catch (e) { KazumiLogger().e('WebDav: WebDav connection failed', error: e); - KazumiDialog.showToast(message: 'WebDav连接失败: $e'); + KazumiDialog.showToast(message: 'WebDav connection failed: $e'); flag = false; } if (!flag) { @@ -287,10 +287,10 @@ abstract class _CollectController with Store { try { await WebDav().updateCollectibles(); if (showSuccessToast) { - KazumiDialog.showToast(message: 'WebDav上传完成'); + KazumiDialog.showToast(message: 'WebDav upload complete'); } } catch (e) { - KazumiDialog.showToast(message: 'WebDav上传失败 $e'); + KazumiDialog.showToast(message: 'WebDav upload failed $e'); return false; } return true; @@ -346,12 +346,12 @@ abstract class _CollectController with Store { final bool syncEnable = setting.get(SettingBoxKey.bangumiSyncEnable, defaultValue: false); if (!syncEnable) { - KazumiDialog.showToast(message: '未开启Bangumi同步,请先在设置中启用'); + KazumiDialog.showToast(message: 'Bangumi sync is not enabled, please enable it in settings first'); return false; } if (!BangumiSyncService().initialized) { - KazumiDialog.showToast(message: 'Bangumi同步已开启但未初始化,请检查Token后重试'); + KazumiDialog.showToast(message: 'Bangumi sync is enabled but not initialized, please check the Token and retry'); return false; } try { @@ -361,16 +361,16 @@ abstract class _CollectController with Store { await BangumiSyncService().syncCollectibles(onProgress: onProgress); if (showSuccessToast) { KazumiDialog.showToast( - message: hasChanges ? 'Bangumi同步完成' : '未发现状态差异,无需同步', + message: hasChanges ? 'Bangumi sync complete' : 'No status differences found, no sync needed', ); } } catch (e) { - KazumiDialog.showToast(message: 'Bangumi同步失败 $e'); + KazumiDialog.showToast(message: 'Bangumi sync failed $e'); return false; } } catch (e) { KazumiLogger().e('Bangumi: Bangumi connection failed', error: e); - KazumiDialog.showToast(message: 'Bangumi访问失败: $e'); + KazumiDialog.showToast(message: 'Bangumi access failed: $e'); return false; } loadCollectibles(); diff --git a/lib/pages/collect/collect_page.dart b/lib/pages/collect/collect_page.dart index 2238d3445..7280d5b92 100644 --- a/lib/pages/collect/collect_page.dart +++ b/lib/pages/collect/collect_page.dart @@ -35,7 +35,7 @@ class _CollectPageState extends State Future _syncBangumiWithProgress({ required GlobalKey<_FullSyncProgressDialogState> progressDialogKey, }) async { - progressDialogKey.currentState?.update('准备同步 Bangumi 收藏...', null); + progressDialogKey.currentState?.update('Preparing to sync Bangumi collection...', null); await Future.delayed(const Duration(milliseconds: 80)); @@ -67,16 +67,16 @@ class _CollectPageState extends State }) { final List states = []; if (plan.shouldSyncWebDavCollectibles) { - states.add(webDavSynced ? 'WebDav 已同步' : 'WebDav 未完成'); + states.add(webDavSynced ? 'WebDav synced' : 'WebDav incomplete'); } if (plan.shouldSyncBangumi) { - states.add(bangumiSynced ? 'Bangumi 已同步' : 'Bangumi 未完成'); + states.add(bangumiSynced ? 'Bangumi synced' : 'Bangumi incomplete'); } if (plan.shouldSyncWebDavCollectibles && plan.shouldSyncBangumi && webDavSynced && bangumiSynced) { - states.add(webDavUploaded ? 'WebDav 已回传最新数据' : 'WebDav 未回传最新数据'); + states.add(webDavUploaded ? 'WebDav uploaded latest data' : 'WebDav did not upload latest data'); } return states.join(','); } @@ -98,7 +98,7 @@ class _CollectPageState extends State try { if (plan.shouldSyncWebDavCollectibles) { - progressDialogKey.currentState?.update('正在同步 WebDav 收藏...', null); + progressDialogKey.currentState?.update('Syncing WebDav collection...', null); webDavSynced = await collectController.syncCollectibles(showSuccessToast: false); } @@ -113,7 +113,7 @@ class _CollectPageState extends State webDavSynced: webDavSynced, bangumiSynced: bangumiSynced, )) { - progressDialogKey.currentState?.update('正在回传最新收藏到 WebDav...', null); + progressDialogKey.currentState?.update('Uploading latest collection to WebDav...', null); webDavUploaded = await collectController.uploadCollectiblesToWebDav( showSuccessToast: false, ); @@ -162,11 +162,11 @@ class _CollectPageState extends State } final List tabs = const [ - Tab(text: '在看'), - Tab(text: '想看'), - Tab(text: '搁置'), - Tab(text: '看过'), - Tab(text: '抛弃'), + Tab(text: 'Watching'), + Tab(text: 'Plan to watch'), + Tab(text: 'On hold'), + Tab(text: 'Watched'), + Tab(text: 'Dropped'), ]; @override @@ -191,7 +191,7 @@ class _CollectPageState extends State tabs: tabs, indicatorColor: Theme.of(context).colorScheme.primary, ), - title: const Text('追番'), + title: const Text('Tracking'), actions: [ IconButton( onPressed: () { @@ -218,11 +218,11 @@ class _CollectPageState extends State bangumiEnabled: bgmSyncEnable, ); if (!syncPlan.canSync) { - KazumiDialog.showToast(message: '同步功能不可用,请至少开启一个同步功能'); + KazumiDialog.showToast(message: 'Sync is unavailable, please enable at least one sync option'); return; } if (showDelete) { - KazumiDialog.showToast(message: '编辑模式无法执行同步'); + KazumiDialog.showToast(message: 'Cannot sync while in edit mode'); return; } if (syncCollectiblesing) { @@ -263,7 +263,7 @@ class _CollectPageState extends State ); } else { return const Center( - child: Text('啊嘞, 没有追番的说 (´;ω;`)'), + child: Text('Oops, nothing tracked yet (´;ω;`)'), ); } } @@ -364,7 +364,7 @@ class _FullSyncProgressDialog extends StatefulWidget { } class _FullSyncProgressDialogState extends State<_FullSyncProgressDialog> { - String _progressText = '准备开始同步收藏...'; + String _progressText = 'Preparing to start collection sync...'; double? _progressValue; void update(String text, double? value) { @@ -389,7 +389,7 @@ class _FullSyncProgressDialogState extends State<_FullSyncProgressDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - '收藏全量同步中', + 'Full collection sync in progress', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, diff --git a/lib/pages/download/download_controller.dart b/lib/pages/download/download_controller.dart index ca5bdbe5c..b6e392aeb 100644 --- a/lib/pages/download/download_controller.dart +++ b/lib/pages/download/download_controller.dart @@ -510,7 +510,7 @@ abstract class _DownloadController with Store { final plugin = _findPlugin(request.pluginName); if (plugin == null) { _failEpisode(request.recordKey, request.episodeNumber, - '找不到插件 ${request.pluginName}'); + 'Plugin not found ${request.pluginName}'); return; } @@ -547,7 +547,7 @@ abstract class _DownloadController with Store { } if (m3u8Url == null || m3u8Url.isEmpty) { - _failEpisode(request.recordKey, request.episodeNumber, '解析视频源超时'); + _failEpisode(request.recordKey, request.episodeNumber, 'Video source parsing timed out'); return; } @@ -726,7 +726,7 @@ abstract class _DownloadController with Store { final plugin = _findPlugin(pluginName); if (plugin == null) { - _failEpisode(recordKey, episodeNumber, '找不到插件 $pluginName'); + _failEpisode(recordKey, episodeNumber, 'Plugin not found $pluginName'); return; } @@ -830,7 +830,7 @@ abstract class _DownloadController with Store { final plugin = _findPlugin(pluginName); if (plugin == null) { - _failEpisode(recordKey, episodeNumber, '找不到插件 $pluginName'); + _failEpisode(recordKey, episodeNumber, 'Plugin not found $pluginName'); return; } diff --git a/lib/pages/download/download_episode_sheet.dart b/lib/pages/download/download_episode_sheet.dart index b53569587..5ea131986 100644 --- a/lib/pages/download/download_episode_sheet.dart +++ b/lib/pages/download/download_episode_sheet.dart @@ -60,11 +60,11 @@ class _DownloadEpisodeSheetState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( - '选择要下载的集数', + 'Select episodes to download', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Text( - '已选 ${_selectedEpisodes.length} 集', + '${_selectedEpisodes.length} selected', style: TextStyle( fontSize: 14, color: Theme.of(context).colorScheme.primary, @@ -90,7 +90,7 @@ class _DownloadEpisodeSheetState extends State { } }); }, - child: const Text('全选'), + child: const Text('Select all'), ), TextButton( onPressed: () { @@ -98,7 +98,7 @@ class _DownloadEpisodeSheetState extends State { _selectedEpisodes.clear(); }); }, - child: const Text('取消全选'), + child: const Text('Deselect all'), ), ], ), @@ -202,7 +202,7 @@ class _DownloadEpisodeSheetState extends State { children: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: const Text('Cancel'), ), const SizedBox(width: 12), SizedBox( @@ -211,7 +211,7 @@ class _DownloadEpisodeSheetState extends State { onPressed: _selectedEpisodes.isEmpty ? null : () => _startBatchDownload(context), - child: Text('开始下载(${_selectedEpisodes.length})'), + child: Text('Start download (${_selectedEpisodes.length})'), ), ), ], @@ -251,7 +251,7 @@ class _DownloadEpisodeSheetState extends State { } KazumiDialog.showToast( - message: '已添加 ${sortedEpisodes.length} 集到下载队列,可在下载管理中查看', + message: 'Added ${sortedEpisodes.length} episodes to the download queue, view them in Download Manager', ); } } diff --git a/lib/pages/download/download_page.dart b/lib/pages/download/download_page.dart index 1857d79bf..4680be401 100644 --- a/lib/pages/download/download_page.dart +++ b/lib/pages/download/download_page.dart @@ -29,7 +29,7 @@ class _DownloadPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: const SysAppBar(title: Text('下载管理')), + appBar: const SysAppBar(title: Text('Download manager')), body: Observer(builder: (context) { final recordKeys = downloadController.recordKeys.toList(); if (recordKeys.isEmpty) { @@ -47,7 +47,7 @@ class _DownloadPageState extends State { ), const SizedBox(height: 16), Text( - '暂无离线下载', + 'No offline downloads yet', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.outline, ), @@ -107,7 +107,7 @@ class _DownloadPageState extends State { overflow: TextOverflow.ellipsis, ), subtitle: Text( - '来源: ${record.pluginName} · $completedCount/${episodes.length} 已完成', + 'Source: ${record.pluginName} · $completedCount/${episodes.length} completed', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.outline, @@ -122,17 +122,17 @@ class _DownloadPageState extends State { record.bangumiId, record.pluginName, ); - KazumiDialog.showToast(message: '已开始恢复下载'); + KazumiDialog.showToast(message: 'Resuming downloads'); } }, itemBuilder: (context) => [ const PopupMenuItem( value: 'resume_all', - child: Text('开始全部'), + child: Text('Start all'), ), const PopupMenuItem( value: 'delete', - child: Text('删除全部'), + child: Text('Delete all'), ), ], ), @@ -161,7 +161,7 @@ class _DownloadPageState extends State { Text( episode.episodeName.isNotEmpty ? episode.episodeName - : '第${episode.episodeNumber}集', + : 'Ep ${episode.episodeNumber}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14), @@ -232,7 +232,7 @@ class _DownloadPageState extends State { String _getStatusText(DownloadRecord record, DownloadEpisode episode) { switch (episode.status) { case DownloadStatus.completed: - return '已完成 ${formatBytes(episode.totalBytes)}'; + return 'Completed ${formatBytes(episode.totalBytes)}'; case DownloadStatus.downloading: final speed = downloadController.getSpeed( record.bangumiId, @@ -243,13 +243,13 @@ class _DownloadPageState extends State { return '${(episode.progressPercent * 100).toStringAsFixed(0)}% ' '${episode.downloadedSegments}/${episode.totalSegments}$speedText'; case DownloadStatus.failed: - return episode.errorMessage.isNotEmpty ? episode.errorMessage : '下载失败'; + return episode.errorMessage.isNotEmpty ? episode.errorMessage : 'Download failed'; case DownloadStatus.paused: - return '已暂停 ${(episode.progressPercent * 100).toStringAsFixed(0)}%'; + return 'Paused ${(episode.progressPercent * 100).toStringAsFixed(0)}%'; case DownloadStatus.pending: - return '等待中'; + return 'Waiting'; case DownloadStatus.resolving: - return '解析视频源中'; + return 'Parsing video source'; default: return ''; } @@ -265,7 +265,7 @@ class _DownloadPageState extends State { icon: Icon(Icons.play_circle_outline, size: 20, color: Theme.of(context).colorScheme.primary), onPressed: () => _playEpisode(record, episode), - tooltip: '播放', + tooltip: 'Play', visualDensity: VisualDensity.compact, )); break; @@ -277,7 +277,7 @@ class _DownloadPageState extends State { record.pluginName, episode.episodeNumber, ), - tooltip: '暂停', + tooltip: 'Pause', visualDensity: VisualDensity.compact, )); break; @@ -289,7 +289,7 @@ class _DownloadPageState extends State { pluginName: record.pluginName, episodeNumber: episode.episodeNumber, ), - tooltip: '继续', + tooltip: 'Resume', visualDensity: VisualDensity.compact, )); break; @@ -301,7 +301,7 @@ class _DownloadPageState extends State { pluginName: record.pluginName, episodeNumber: episode.episodeNumber, ), - tooltip: '重试', + tooltip: 'Retry', visualDensity: VisualDensity.compact, )); break; @@ -315,9 +315,9 @@ class _DownloadPageState extends State { pluginName: record.pluginName, episodeNumber: episode.episodeNumber, ); - KazumiDialog.showToast(message: '已插队优先下载'); + KazumiDialog.showToast(message: 'Moved to the front of the download queue'); }, - tooltip: '优先下载', + tooltip: 'Prioritize download', visualDensity: VisualDensity.compact, )); break; @@ -328,7 +328,7 @@ class _DownloadPageState extends State { buttons.add(IconButton( icon: const Icon(Icons.delete_outline, size: 20), onPressed: () => _confirmDeleteEpisode(record, episode), - tooltip: '删除', + tooltip: 'Delete', visualDensity: VisualDensity.compact, )); @@ -342,7 +342,7 @@ class _DownloadPageState extends State { episode.episodeNumber, ); if (localPath == null) { - KazumiDialog.showToast(message: '本地文件不存在'); + KazumiDialog.showToast(message: 'Local file does not exist'); return; } @@ -384,14 +384,14 @@ class _DownloadPageState extends State { void _confirmDeleteEpisode(DownloadRecord record, DownloadEpisode episode) { KazumiDialog.show( builder: (context) => AlertDialog( - title: const Text('删除下载'), + title: const Text('Delete download'), content: Text( - '确定要删除「${episode.episodeName.isNotEmpty ? episode.episodeName : '第${episode.episodeNumber}集'}」的下载文件吗?'), + 'Delete the download file for “${episode.episodeName.isNotEmpty ? episode.episodeName : 'Ep ${episode.episodeNumber}'}”?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -404,7 +404,7 @@ class _DownloadPageState extends State { ); KazumiDialog.dismiss(); }, - child: const Text('删除'), + child: const Text('Delete'), ), ], ), @@ -414,13 +414,13 @@ class _DownloadPageState extends State { void _confirmDeleteRecord(DownloadRecord record) { KazumiDialog.show( builder: (context) => AlertDialog( - title: const Text('删除全部下载'), - content: Text('确定要删除「${record.bangumiName}」的所有下载文件吗?'), + title: const Text('Delete all downloads'), + content: Text('Delete all downloaded files for “${record.bangumiName}”?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -432,7 +432,7 @@ class _DownloadPageState extends State { ); KazumiDialog.dismiss(); }, - child: const Text('删除'), + child: const Text('Delete'), ), ], ), diff --git a/lib/pages/error/storage_error_page.dart b/lib/pages/error/storage_error_page.dart index 4f51c2182..87bb97b25 100644 --- a/lib/pages/error/storage_error_page.dart +++ b/lib/pages/error/storage_error_page.dart @@ -10,7 +10,7 @@ class StorageErrorPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('内部错误'), + title: const Text('Internal error'), ), body: Center( child: FutureBuilder( @@ -18,15 +18,15 @@ class StorageErrorPage extends StatelessWidget { builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { final supportDir = snapshot.data; - final path = supportDir != null ? '$supportDir' : '未知路径'; + final path = supportDir != null ? '$supportDir' : 'Unknown path'; return GeneralErrorWidget( - errMsg: '存储初始化错误 \n 当前储存位置 $path \n 尝试删除该目录以重置本地存储', + errMsg: 'Storage initialization error \n Current storage location $path \n Try deleting this directory to reset local storage', actions: [ GeneralErrorButton( onPressed: () { exit(0); }, - text: '退出程序', + text: 'Exit app', ), ], ); diff --git a/lib/pages/history/history_page.dart b/lib/pages/history/history_page.dart index c026117bd..e94f67d1c 100644 --- a/lib/pages/history/history_page.dart +++ b/lib/pages/history/history_page.dart @@ -36,15 +36,15 @@ class _HistoryPageState extends State { KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('记录管理'), - content: const Text('确认要清除所有历史记录吗?'), + title: const Text('Record management'), + content: const Text('Clear all history records?'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -55,7 +55,7 @@ class _HistoryPageState extends State { historyController.clearAll(); } catch (_) {} }, - child: const Text('确认'), + child: const Text('Confirm'), ), ], ); @@ -73,7 +73,7 @@ class _HistoryPageState extends State { }, child: Scaffold( appBar: SysAppBar( - title: const Text('历史记录'), + title: const Text('History'), actions: [ if (historyController.histories.isNotEmpty) ...[ IconButton( @@ -85,14 +85,14 @@ class _HistoryPageState extends State { icon: showDelete ? const Icon(Icons.edit_off_outlined) : const Icon(Icons.edit_outlined), - tooltip: showDelete ? '退出编辑' : '编辑', + tooltip: showDelete ? 'Exit editing' : 'Edit', ), IconButton( onPressed: () { showHistoryClearDialog(); }, icon: const Icon(Icons.delete_sweep_outlined), - tooltip: '清除全部', + tooltip: 'Clear all', ), ], ], @@ -119,7 +119,7 @@ class _HistoryPageState extends State { ), const SizedBox(height: 16), Text( - '没有找到历史记录', + 'No history records found', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.outline, ), diff --git a/lib/pages/index_module.dart b/lib/pages/index_module.dart index 2818f936b..235d67df3 100644 --- a/lib/pages/index_module.dart +++ b/lib/pages/index_module.dart @@ -61,7 +61,7 @@ class IndexModule extends Module { "/error", child: (_) => Scaffold( appBar: AppBar(title: const Text("Kazumi")), - body: const Center(child: Text("初始化失败")), + body: const Center(child: Text("Initialization failed")), ), ), ], diff --git a/lib/pages/info/character_page.dart b/lib/pages/info/character_page.dart index fd06ca5ce..939df0263 100644 --- a/lib/pages/info/character_page.dart +++ b/lib/pages/info/character_page.dart @@ -83,8 +83,8 @@ class _CharacterPageState extends State { child: Material( child: TabBar( tabs: [ - Tab(text: '人物资料'), - Tab(text: '吐槽箱'), + Tab(text: 'Character profile'), + Tab(text: 'Comment box'), ], ), ), @@ -111,13 +111,13 @@ class _CharacterPageState extends State { ? const Center(child: CircularProgressIndicator()) : (characterFullItem.id == 0 ? GeneralErrorWidget( - errMsg: '什么都没有找到 (´;ω;`)', + errMsg: 'Nothing found (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { loadCharacter(); }, - text: '点击重试', + text: 'Tap to retry', ), ], ) @@ -185,7 +185,7 @@ class _CharacterPageState extends State { padding: const EdgeInsets.symmetric( vertical: 8.0), child: Text( - '基本信息', + 'Basic info', style: Theme.of(context) .textTheme .titleSmall @@ -206,7 +206,7 @@ class _CharacterPageState extends State { padding: const EdgeInsets.symmetric( vertical: 8.0), child: Text( - '角色简介', + 'Character introduction', style: Theme.of(context) .textTheme .titleSmall @@ -262,13 +262,13 @@ class _CharacterPageState extends State { if (commentsError) { return SliverFillRemaining( child: GeneralErrorWidget( - errMsg: '什么都没有找到 (´;ω;`)', + errMsg: 'Nothing found (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { loadComments(); }, - text: '点击重试', + text: 'Tap to retry', ), ], ), @@ -277,7 +277,7 @@ class _CharacterPageState extends State { if (commentsList.isEmpty) { return const SliverFillRemaining( child: Center( - child: Text('什么都没有找到 (´;ω;`)'), + child: Text('Nothing found (´;ω;`)'), ), ); } diff --git a/lib/pages/info/info_page.dart b/lib/pages/info/info_page.dart index 297c73717..c808bca08 100644 --- a/lib/pages/info/info_page.dart +++ b/lib/pages/info/info_page.dart @@ -32,11 +32,11 @@ class InfoPage extends StatefulWidget { class _InfoPageState extends State with TickerProviderStateMixin { static const List _infoTabs = [ - '概览', - '吐槽', - '角色', - '评论', - '制作人员', + 'Overview', + 'Comments', + 'Characters', + 'Reviews', + 'Staff', ]; static const int _commentsTabIndex = 1; static const Duration _minimumBangumiInfoLoadingDuration = @@ -172,13 +172,13 @@ class _InfoPageState extends State with TickerProviderStateMixin { .toString() .trim(); if (token.isEmpty) { - KazumiDialog.showToast(message: '请先在同步设置中绑定你的 Bangumi 配置以发表吐槽'); + KazumiDialog.showToast(message: 'Please bind your Bangumi configuration in sync settings before posting a comment'); return; } final localType = infoController.collectController .getCollectType(infoController.bangumiItem); if (localType == 0) { - KazumiDialog.showToast(message: '请先追番后再发表评价'); + KazumiDialog.showToast(message: 'Please track the anime before posting a review'); return; } KazumiDialog.show( @@ -489,13 +489,13 @@ class _InfoPageState extends State with TickerProviderStateMixin { ), floatingActionButton: showRatingFab ? FloatingActionButton.extended( - tooltip: '吐槽', + tooltip: 'Comments', onPressed: onBangumiRatingTap, - label: const Text('发表吐槽'), + label: const Text('Post comment'), icon: const Icon(Icons.rate_review_rounded), ) : FloatingActionButton.extended( - tooltip: '开始观看', + tooltip: 'Start watching', onPressed: () async { showModalBottomSheet( isScrollControlled: true, @@ -521,7 +521,7 @@ class _InfoPageState extends State with TickerProviderStateMixin { }, ); }, - label: const Text('开始观看'), + label: const Text('Start watching'), icon: const Icon(Icons.play_arrow_rounded), ), ), diff --git a/lib/pages/info/info_tabview.dart b/lib/pages/info/info_tabview.dart index 57e849f3a..f24d23c6f 100644 --- a/lib/pages/info/info_tabview.dart +++ b/lib/pages/info/info_tabview.dart @@ -94,7 +94,7 @@ class _InfoTabViewState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('简介', style: TextStyle(fontSize: 18)), + Text('Introduction', style: TextStyle(fontSize: 18)), const SizedBox(height: 8), // https://stackoverflow.com/questions/54091055/flutter-how-to-get-the-number-of-text-lines // only show expand button when line > 7 @@ -130,7 +130,7 @@ class _InfoTabViewState extends State fullIntro = !fullIntro; }); }, - child: Text(fullIntro ? '加载更少' : '加载更多'), + child: Text(fullIntro ? 'Show less' : 'Show more'), ), ], ); @@ -144,7 +144,7 @@ class _InfoTabViewState extends State } }), const SizedBox(height: 16), - Text('标签', style: TextStyle(fontSize: 18)), + Text('Tags', style: TextStyle(fontSize: 18)), const SizedBox(height: 8), Wrap( spacing: 8.0, @@ -157,7 +157,7 @@ class _InfoTabViewState extends State // make tag expandable return ActionChip( label: Text( - '更多 +', + 'More +', style: TextStyle( color: Theme.of(context).colorScheme.primary), ), @@ -244,7 +244,7 @@ class _InfoTabViewState extends State scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), - key: PageStorageKey('吐槽'), + key: PageStorageKey('Comments'), slivers: [ SliverOverlapInjector( handle: @@ -321,14 +321,14 @@ class _InfoTabViewState extends State if (widget.commentsQueryTimeout) { return SliverFillRemaining( child: GeneralErrorWidget( - errMsg: '获取失败,请重试', + errMsg: 'Failed to load, please retry', actions: [ GeneralErrorButton( onPressed: () { widget.loadMoreComments( loadMore: widget.commentsList.isNotEmpty); }, - text: '重试', + text: 'Retry', ), ], ), @@ -337,7 +337,7 @@ class _InfoTabViewState extends State if (widget.commentsIsEmpty) { return const SliverFillRemaining( child: Center( - child: Text('什么都没有找到 (´;ω;`)'), + child: Text('Nothing found (´;ω;`)'), ), ); } @@ -376,7 +376,7 @@ class _InfoTabViewState extends State scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), - key: PageStorageKey('制作人员'), + key: PageStorageKey('Staff'), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), @@ -405,13 +405,13 @@ class _InfoTabViewState extends State if (widget.staffQueryTimeout) { return SliverFillRemaining( child: GeneralErrorWidget( - errMsg: '获取失败,请重试', + errMsg: 'Failed to load, please retry', actions: [ GeneralErrorButton( onPressed: () { widget.loadStaff(); }, - text: '重试', + text: 'Retry', ), ], ), @@ -420,7 +420,7 @@ class _InfoTabViewState extends State if (widget.staffIsEmpty) { return const SliverFillRemaining( child: Center( - child: Text('什么都没有找到 (´;ω;`)'), + child: Text('Nothing found (´;ω;`)'), ), ); } @@ -458,7 +458,7 @@ class _InfoTabViewState extends State scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), - key: PageStorageKey('角色'), + key: PageStorageKey('Characters'), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), @@ -487,13 +487,13 @@ class _InfoTabViewState extends State if (widget.charactersQueryTimeout) { return SliverFillRemaining( child: GeneralErrorWidget( - errMsg: '获取失败,请重试', + errMsg: 'Failed to load, please retry', actions: [ GeneralErrorButton( onPressed: () { widget.loadCharacters(); }, - text: '重试', + text: 'Retry', ), ], ), @@ -502,7 +502,7 @@ class _InfoTabViewState extends State if (widget.charactersIsEmpty) { return const SliverFillRemaining( child: Center( - child: Text('什么都没有找到 (´;ω;`)'), + child: Text('Nothing found (´;ω;`)'), ), ); } @@ -551,7 +551,7 @@ class _InfoTabViewState extends State // The PageStorageKey should be unique to this ScrollView; // it allows the list to remember its scroll position when // the tab view is not on the screen. - key: PageStorageKey('概览'), + key: PageStorageKey('Overview'), slivers: [ SliverOverlapInjector( handle: @@ -576,7 +576,7 @@ class _InfoTabViewState extends State scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, ), - key: PageStorageKey('评论'), + key: PageStorageKey('Reviews'), slivers: [ SliverOverlapInjector( handle: @@ -584,7 +584,7 @@ class _InfoTabViewState extends State ), // TODO: 评论区 SliverFillRemaining( - child: Center(child: Text('施工中')), + child: Center(child: Text('Under construction')), ), ], ); diff --git a/lib/pages/info/rating_review_dialog.dart b/lib/pages/info/rating_review_dialog.dart index 392b68480..1d9d6974a 100644 --- a/lib/pages/info/rating_review_dialog.dart +++ b/lib/pages/info/rating_review_dialog.dart @@ -39,17 +39,17 @@ class RatingReviewDialog extends StatefulWidget { class _RatingReviewDialogState extends State { static const List scoreLabels = [ - '未评分', - '不忍直视', - '很差', - '差', - '较差', - '不过不失', - '还行', - '推荐', - '力荐', - '神作', - '超神作', + 'No rating', + 'Unwatchable', + 'Very bad', + 'Bad', + 'Poor', + 'Mediocre', + 'Okay', + 'Recommended', + 'Highly recommended', + 'Masterpiece', + 'Ultimate masterpiece', ]; /// Maximum number of tags that can be submitted. @@ -117,7 +117,7 @@ class _RatingReviewDialogState extends State { return; } if (selectedTags.length >= _maxSelectedTags) { - _tagErrorText = '最多选择 $_maxSelectedTags 个标签'; + _tagErrorText = 'Select up to $_maxSelectedTags tags'; return; } selectedTags.add(tag); @@ -129,19 +129,19 @@ class _RatingReviewDialogState extends State { if (_isSubmitting) return; final text = tagInputController.text.trim(); if (text.isEmpty) { - _setTagError('请输入标签内容'); + _setTagError('Please enter tag content'); return; } if (text.length > _maxTagLength) { - _setTagError('单个标签不能超过 $_maxTagLength 个字'); + _setTagError('A single tag cannot exceed $_maxTagLength characters'); return; } if (selectedTags.length >= _maxSelectedTags) { - _setTagError('最多选择 $_maxSelectedTags 个标签'); + _setTagError('Select up to $_maxSelectedTags tags'); return; } if (selectedTags.contains(text)) { - _setTagError('这个标签已经添加过了'); + _setTagError('This tag has already been added'); return; } setState(() { @@ -358,7 +358,7 @@ class _RatingReviewDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('发表吐槽', style: theme.textTheme.headlineSmall), + Text('Post comment', style: theme.textTheme.headlineSmall), const SizedBox(height: 4), Text( displayName, @@ -372,7 +372,7 @@ class _RatingReviewDialogState extends State { ), ), IconButton( - tooltip: '关闭', + tooltip: 'Close', onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(), icon: const Icon(Icons.close_rounded), ), @@ -418,7 +418,7 @@ class _RatingReviewDialogState extends State { minLines: 5, maxLines: 9, decoration: InputDecoration( - hintText: '写下你对这部番剧的看法', + hintText: 'Write your thoughts about this anime', filled: true, fillColor: colorScheme.surfaceContainer, hoverColor: colorScheme.surfaceContainer, @@ -444,7 +444,7 @@ class _RatingReviewDialogState extends State { Row( children: [ Expanded( - child: Text('我的评分', style: theme.textTheme.titleMedium), + child: Text('My rating', style: theme.textTheme.titleMedium), ), AnimatedSwitcher( duration: const Duration(milliseconds: 160), @@ -509,7 +509,7 @@ class _RatingReviewDialogState extends State { Row( children: [ Expanded( - child: Text('标签', style: theme.textTheme.titleMedium), + child: Text('Tags', style: theme.textTheme.titleMedium), ), Text( '${selectedTags.length} / $_maxSelectedTags', @@ -521,14 +521,14 @@ class _RatingReviewDialogState extends State { FilledButton.tonalIcon( onPressed: _isSubmitting ? null : _openTagSelection, icon: const Icon(Icons.edit_outlined), - label: const Text('编辑'), + label: const Text('Edit'), ), ], ), const SizedBox(height: 12), if (selectedTags.isEmpty) Text( - '还没有添加标签', + 'No tags added yet', style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -612,7 +612,7 @@ class _RatingReviewDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('编辑标签', style: theme.textTheme.titleLarge), + Text('Edit tags', style: theme.textTheme.titleLarge), Text( '${selectedTags.length} / $_maxSelectedTags', style: theme.textTheme.bodySmall?.copyWith( @@ -623,7 +623,7 @@ class _RatingReviewDialogState extends State { ), ), IconButton( - tooltip: '完成', + tooltip: 'Done', onPressed: _isSubmitting ? null : _closeTagSelection, icon: const Icon(Icons.done_rounded), ), @@ -657,10 +657,10 @@ class _RatingReviewDialogState extends State { onSubmitted: (_) => _addCustomTag(), enabled: !_isSubmitting, decoration: InputDecoration( - labelText: '自定义标签', - hintText: '例如:治愈', + labelText: 'Custom tag', + hintText: 'e.g. Healing', helperText: _tagErrorText == null - ? '最多 $_maxSelectedTags 个标签' + ? 'Up to $_maxSelectedTags tags' : null, errorText: _tagErrorText, filled: true, @@ -678,14 +678,14 @@ class _RatingReviewDialogState extends State { child: FilledButton.tonalIcon( onPressed: _isSubmitting ? null : _addCustomTag, icon: const Icon(Icons.add_rounded), - label: const Text('添加'), + label: const Text('Add'), ), ), ], ), const SizedBox(height: 12), Text( - '已选标签', + 'Selected tags', style: theme.textTheme.labelLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -695,7 +695,7 @@ class _RatingReviewDialogState extends State { const SizedBox(height: 18), if (popularTags.isNotEmpty) ...[ Text( - '热门标签', + 'Popular tags', style: theme.textTheme.labelLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -730,7 +730,7 @@ class _RatingReviewDialogState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - '还没有添加标签', + 'No tags added yet', style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -767,7 +767,7 @@ class _RatingReviewDialogState extends State { children: [ TextButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(), - child: const Text('取消'), + child: const Text('Cancel'), ), const SizedBox(width: 8), FilledButton( @@ -785,7 +785,7 @@ class _RatingReviewDialogState extends State { color: theme.colorScheme.onPrimary, ), ) - : const Text('提交'), + : const Text('Submit'), ), ), ), diff --git a/lib/pages/info/source_sheet.dart b/lib/pages/info/source_sheet.dart index 6aa885091..77dbfae19 100644 --- a/lib/pages/info/source_sheet.dart +++ b/lib/pages/info/source_sheet.dart @@ -114,8 +114,8 @@ class _SourceSheetState extends State // show a 3s countdown progress dialog before re-querying, // to avoid triggering rate limits immediately after verification. KazumiDialog.showTimedSuccessDialog( - title: '验证成功', - message: '正在重新检索,请稍候…', + title: 'Verification succeeded', + message: 'Searching again, please wait…', onComplete: () => pluginSearchService?.querySource(keyword, plugin.name), ); @@ -163,8 +163,8 @@ class _SourceSheetState extends State void showButtonClickDialog(Plugin plugin) { showAutomatedVerifyDialog( plugin, - statusText: '${plugin.name} 正在自动完成验证,请稍候', - detailText: '已检测到验证按钮并模拟点击,等待验证通过…', + statusText: '${plugin.name} is completing verification automatically, please wait', + detailText: 'Verification button detected and clicked, waiting for verification to pass…', startVerification: (captchaService, searchUrl, onVerified) { return captchaService.loadForButtonClick( url: searchUrl, @@ -179,8 +179,8 @@ class _SourceSheetState extends State void showCustomScriptDialog(Plugin plugin) { showAutomatedVerifyDialog( plugin, - statusText: '${plugin.name} 正在执行验证脚本,请稍候', - detailText: '已加载验证页面并执行自定义脚本,等待验证通过…', + statusText: '${plugin.name} is running the verification script, please wait', + detailText: 'Verification page loaded and custom script executed, waiting for verification to pass…', startVerification: (captchaService, searchUrl, onVerified) { return captchaService.loadForCustomScript( url: searchUrl, @@ -216,8 +216,8 @@ class _SourceSheetState extends State verified = true; KazumiDialog.dismiss(); KazumiDialog.showTimedSuccessDialog( - title: '验证成功', - message: '正在重新检索,请稍候…', + title: 'Verification succeeded', + message: 'Searching again, please wait…', onComplete: () => pluginSearchService?.querySource(keyword, plugin.name), ); @@ -248,7 +248,7 @@ class _SourceSheetState extends State crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - '自动验证中', + 'Auto verifying', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 4), @@ -270,7 +270,7 @@ class _SourceSheetState extends State child: TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), @@ -291,43 +291,43 @@ class _SourceSheetState extends State } if (status == 'captcha') { return GeneralErrorWidget( - errMsg: '${plugin.name} 需要验证码验证', + errMsg: '${plugin.name} requires captcha verification', actions: [ GeneralErrorButton( onPressed: () => showAntiCrawlerDialog(plugin), - text: '进行验证', + text: 'Verify', ), GeneralErrorButton( onPressed: () => pluginSearchService?.querySource(keyword, plugin.name), - text: '重试', + text: 'Retry', ), ], ); } if (status == 'noResult') { return GeneralErrorWidget( - errMsg: '${plugin.name} 无结果 使用别名或左右滑动以切换到其他视频来源', + errMsg: '${plugin.name} returned no results. Use an alias or swipe left or right to switch to another video source', actions: [ GeneralErrorButton( onPressed: () => showAliasSearchDialog(plugin.name), - text: '别名检索', + text: 'Search by alias', ), GeneralErrorButton( onPressed: () => showCustomSearchDialog(plugin.name), - text: '手动检索', + text: 'Manual search', ), ], ); } if (status == 'error') { return GeneralErrorWidget( - errMsg: '${plugin.name} 检索失败 重试或左右滑动以切换到其他视频来源', + errMsg: '${plugin.name} search failed. Retry or swipe left or right to switch to another video source', actions: [ GeneralErrorButton( onPressed: () => pluginSearchService?.querySource(keyword, plugin.name), - text: '重试', + text: 'Retry', ), ], ); @@ -359,7 +359,7 @@ class _SourceSheetState extends State runSpacing: 4, children: [ Text( - '结果不准确?', + 'Results inaccurate?', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context) .textTheme @@ -378,7 +378,7 @@ class _SourceSheetState extends State textStyle: Theme.of(context).textTheme.bodySmall, ), onPressed: () => showAliasSearchDialog(pluginName), - child: const Text('别名检索'), + child: const Text('Search by alias'), ), TextButton( style: TextButton.styleFrom( @@ -390,7 +390,7 @@ class _SourceSheetState extends State textStyle: Theme.of(context).textTheme.bodySmall, ), onPressed: () => showCustomSearchDialog(pluginName), - child: const Text('手动检索'), + child: const Text('Manual search'), ), ], ), @@ -402,7 +402,7 @@ class _SourceSheetState extends State void showAliasSearchDialog(String pluginName) { if (widget.infoController.bangumiItem.alias.isEmpty) { - KazumiDialog.showToast(message: '无可用别名,试试手动检索'); + KazumiDialog.showToast(message: 'No aliases available, try a manual search'); return; } KazumiDialog.show( @@ -439,7 +439,7 @@ class _SourceSheetState extends State KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('输入别名'), + title: const Text('Enter alias'), content: TextField( onChanged: (value) => customKeyword = value, onSubmitted: (keyword) { @@ -452,7 +452,7 @@ class _SourceSheetState extends State KazumiDialog.dismiss(); }, child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -461,7 +461,7 @@ class _SourceSheetState extends State submit(customKeyword); }, child: const Text( - '确认', + 'Confirm', ), ), ], @@ -567,7 +567,7 @@ class _SourceSheetState extends State borderRadius: BorderRadius.circular(12), onTap: () async { KazumiDialog.showLoading( - msg: '获取中', + msg: 'Loading', barrierDismissible: isDesktop(), onDismiss: () { videoPageController.cancelQueryRoads(); @@ -654,7 +654,7 @@ class _CaptchaDialogState extends State<_CaptchaDialog> { if (_submittingNotifier.value) return; final captchaCode = _captchaCode.trim(); if (captchaCode.isEmpty) { - KazumiDialog.showToast(message: '请输入验证码'); + KazumiDialog.showToast(message: 'Please enter the captcha'); return; } _submittingNotifier.value = true; @@ -674,12 +674,12 @@ class _CaptchaDialogState extends State<_CaptchaDialog> { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - '验证码验证', + 'Captcha verification', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 4), Text( - '${widget.pluginName} 需要验证码验证', + '${widget.pluginName} requires captcha verification', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 20), @@ -691,7 +691,7 @@ class _CaptchaDialogState extends State<_CaptchaDialog> { children: [ CircularProgressIndicator(), SizedBox(height: 12), - Text('正在加载验证码图片...'), + Text('Loading captcha image...'), ], ); } @@ -707,7 +707,7 @@ class _CaptchaDialogState extends State<_CaptchaDialog> { height: 80, fit: BoxFit.contain, errorBuilder: (context, error, _) => - const Text('图片解码失败'), + const Text('Failed to decode image'), ), ), const SizedBox(height: 16), @@ -716,7 +716,7 @@ class _CaptchaDialogState extends State<_CaptchaDialog> { enabled: !isSubmitting, onChanged: (value) => _captchaCode = value, decoration: const InputDecoration( - labelText: '请输入验证码', + labelText: 'Please enter the captcha', border: OutlineInputBorder(), ), onSubmitted: isSubmitting ? null : (_) => _submit(), @@ -743,7 +743,7 @@ class _CaptchaDialogState extends State<_CaptchaDialog> { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), @@ -760,7 +760,7 @@ class _CaptchaDialogState extends State<_CaptchaDialog> { strokeWidth: 2, ), ) - : const Text('提交'), + : const Text('Submit'), ), ], ); @@ -820,15 +820,15 @@ class _AliasDialogState extends State<_AliasDialog> { KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('删除确认'), - content: const Text('删除后无法恢复,确认要永久删除这个别名吗?'), + title: const Text('Delete confirmation'), + content: const Text('This cannot be undone. Permanently delete this alias?'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, child: Text( - '取消', + 'Cancel', style: TextStyle( color: Theme.of(context) .colorScheme @@ -846,7 +846,7 @@ class _AliasDialogState extends State<_AliasDialog> { Navigator.of(this.context).pop(); } }, - child: const Text('确认'), + child: const Text('Confirm'), ), ], ); diff --git a/lib/pages/init_page.dart b/lib/pages/init_page.dart index 9901de504..153e95980 100644 --- a/lib/pages/init_page.dart +++ b/lib/pages/init_page.dart @@ -87,23 +87,23 @@ class _InitPageState extends State { clickMaskDismiss: false, builder: (context) { return AlertDialog( - title: const Text('需要通知权限'), + title: const Text('Notification permission required'), content: const Text( - '开启通知权限后,可以在后台下载时显示进度,并防止系统终止下载任务。\n\n' - '如果拒绝,下载功能仍可使用,但在后台时可能被系统中断。', + 'Granting notification permission lets downloads show progress in the background and prevents the system from terminating download tasks.\n\n' + 'If denied, downloads still work but may be interrupted by the system when in the background.', ), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(popWith: false), child: Text( - '稍后再说', + 'Maybe later', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () => KazumiDialog.dismiss(popWith: true), - child: const Text('允许'), + child: const Text('Allow'), ), ], ); @@ -184,7 +184,7 @@ class _InitPageState extends State { error: e, ); KazumiDialog.showToast( - message: '初始化Bangumi失败,已关闭 Bangumi 同步: ${e.toString()}', + message: 'Failed to initialize Bangumi, Bangumi sync has been disabled: ${e.toString()}', ); } } @@ -202,16 +202,16 @@ class _InitPageState extends State { return PopScope( canPop: false, child: AlertDialog( - title: const Text('X11环境检测'), + title: const Text('X11 environment detected'), content: const Text( - '检测到您当前运行在X11环境下,Kazumi在X11环境下可能出现性能问题或界面异常,建议切换到Wayland以获得更好的体验。您是否希望在X11下继续使用Kazumi?'), + 'You are currently running under X11. Kazumi may have performance issues or display glitches under X11, and switching to Wayland is recommended for a better experience. Do you want to continue using Kazumi under X11?'), actions: [ TextButton( onPressed: () { exit(0); }, child: Text( - '退出', + 'Exit', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), @@ -220,7 +220,7 @@ class _InitPageState extends State { onPressed: () { KazumiDialog.dismiss(); }, - child: const Text('继续'), + child: const Text('Resume'), ), ], ), @@ -239,17 +239,17 @@ class _InitPageState extends State { final create = await KazumiDialog.show( clickMaskDismiss: false, builder: (context) => AlertDialog( - title: const Text('创建桌面快捷方式'), - content: const Text('是否在桌面创建 Kazumi 的快捷方式?'), + title: const Text('Create desktop shortcut'), + content: const Text('Create a Kazumi shortcut on the desktop?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(popWith: false), - child: Text('暂不创建', + child: Text('Not now', style: TextStyle(color: Theme.of(context).colorScheme.outline)), ), TextButton( onPressed: () => KazumiDialog.dismiss(popWith: true), - child: const Text('创建'), + child: const Text('Create'), ), ], ), @@ -258,7 +258,7 @@ class _InitPageState extends State { await setting.put(SettingBoxKey.shortcutDialogShown, true); if (create ?? false) { final success = await WindowsShortcut.createDesktopShortcut(); - KazumiDialog.showToast(message: success ? '桌面快捷方式已创建' : '桌面快捷方式创建失败'); + KazumiDialog.showToast(message: success ? 'Desktop shortcut created' : 'Failed to create desktop shortcut'); } } @@ -277,7 +277,7 @@ class _InitPageState extends State { return PopScope( canPop: false, child: AlertDialog( - title: const Text('免责声明'), + title: const Text('Disclaimer'), scrollable: true, content: Text(statementsText), actions: [ @@ -286,7 +286,7 @@ class _InitPageState extends State { exit(0); }, child: Text( - '退出', + 'Exit', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), @@ -302,7 +302,7 @@ class _InitPageState extends State { } await _switchUpdateMirror(); }, - child: const Text('已阅读并同意'), + child: const Text('I have read and agree'), ), ], ), @@ -323,7 +323,7 @@ class _InitPageState extends State { return PopScope( canPop: false, child: AlertDialog( - title: const Text('更新镜像'), + title: const Text('Update mirror'), content: const Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -331,14 +331,14 @@ class _InitPageState extends State { Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Text( - '您希望从哪里获取应用更新?', + 'Where would you like to get app updates from?', textAlign: TextAlign.left, ), ), Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Text( - 'Github镜像为大多数情况下的最佳选择。如果您使用F-Droid应用商店, 请选择F-Droid镜像。', + 'The Github mirror is the best choice in most cases. If you use the F-Droid app store, please choose the F-Droid mirror.', textAlign: TextAlign.left, ), ), @@ -389,7 +389,7 @@ class _InitPageState extends State { } } if (count != 0) { - KazumiDialog.showToast(message: '检测到 $count 条规则可以更新'); + KazumiDialog.showToast(message: '$count rules can be updated'); } } diff --git a/lib/pages/logs/logs_page.dart b/lib/pages/logs/logs_page.dart index a3681c187..37b71f889 100644 --- a/lib/pages/logs/logs_page.dart +++ b/lib/pages/logs/logs_page.dart @@ -137,7 +137,7 @@ class _LogsPageState extends State { }); } catch (e) { if (!mounted) return; - KazumiDialog.showToast(message: '清空失败: $e'); + KazumiDialog.showToast(message: 'Failed to clear: $e'); } } @@ -145,10 +145,10 @@ class _LogsPageState extends State { try { await Clipboard.setData(ClipboardData(text: _fullContent)); if (!mounted) return; - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); } catch (e) { if (!mounted) return; - KazumiDialog.showToast(message: '复制失败: $e'); + KazumiDialog.showToast(message: 'Copy failed: $e'); } } @@ -156,7 +156,7 @@ class _LogsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: const SysAppBar( - title: Text('日志'), + title: Text('Logs'), ), body: buildBody, floatingActionButton: buildFloatingButtons, @@ -172,13 +172,13 @@ class _LogsPageState extends State { if (_hasError) { return const Center( - child: Text('加载日志失败'), + child: Text('Failed to load logs'), ); } if (_logLines.isEmpty) { return const Center( - child: Text('没有数据'), + child: Text('No data'), ); } @@ -219,14 +219,14 @@ class _LogsPageState extends State { FloatingActionButton( heroTag: null, onPressed: _clearLogs, - tooltip: '清空日志', + tooltip: 'Clear logs', child: const Icon(Icons.clear_all), ), const SizedBox(width: 15), FloatingActionButton( heroTag: null, onPressed: _copyLogs, - tooltip: '复制日志', + tooltip: 'Copy logs', child: const Icon(Icons.copy), ), ], diff --git a/lib/pages/menu/menu.dart b/lib/pages/menu/menu.dart index 047718d24..6121e24f7 100644 --- a/lib/pages/menu/menu.dart +++ b/lib/pages/menu/menu.dart @@ -98,22 +98,22 @@ class _ScaffoldMenu extends State { NavigationDestination( selectedIcon: Icon(Icons.home), icon: Icon(Icons.home_outlined), - label: '推荐', + label: 'Recommended', ), NavigationDestination( selectedIcon: Icon(Icons.timeline), icon: Icon(Icons.timeline_outlined), - label: '时间表', + label: 'Schedule', ), NavigationDestination( selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_outlined), - label: '追番', + label: 'Tracking', ), NavigationDestination( selectedIcon: Icon(Icons.settings), icon: Icon(Icons.settings), - label: '我的', + label: 'Me', ), ], selectedIndex: state.selectedIndex, @@ -148,22 +148,22 @@ class _ScaffoldMenu extends State { NavigationRailDestination( selectedIcon: Icon(Icons.home), icon: Icon(Icons.home_outlined), - label: Text('推荐'), + label: Text('Recommended'), ), NavigationRailDestination( selectedIcon: Icon(Icons.timeline), icon: Icon(Icons.timeline_outlined), - label: Text('时间表'), + label: Text('Schedule'), ), NavigationRailDestination( selectedIcon: Icon(Icons.favorite), icon: Icon(Icons.favorite_border), - label: Text('追番'), + label: Text('Tracking'), ), NavigationRailDestination( selectedIcon: Icon(Icons.settings), icon: Icon(Icons.settings_outlined), - label: Text('我的'), + label: Text('Me'), ), ], selectedIndex: state.selectedIndex, diff --git a/lib/pages/my/my_controller.dart b/lib/pages/my/my_controller.dart index df92ed090..174c5d905 100644 --- a/lib/pages/my/my_controller.dart +++ b/lib/pages/my/my_controller.dart @@ -43,15 +43,15 @@ abstract class _MyController with Store { void addShieldList(String item) { if (item.isEmpty) { - KazumiDialog.showToast(message: '请输入关键词'); + KazumiDialog.showToast(message: 'Please enter a keyword'); return; } if (item.length > 64) { - KazumiDialog.showToast(message: '关键词过长'); + KazumiDialog.showToast(message: 'Keyword too long'); return; } if (shieldList.contains(item)) { - KazumiDialog.showToast(message: '已存在该关键词'); + KazumiDialog.showToast(message: 'This keyword already exists'); return; } shieldList.add(item); @@ -79,7 +79,7 @@ abstract class _MyController with Store { } catch (err) { KazumiLogger().e('Update: check update failed', error: err); if (type == 'manual') { - KazumiDialog.showToast(message: '检查更新失败,请稍后重试'); + KazumiDialog.showToast(message: 'Failed to check for updates, please try again later'); } return false; } diff --git a/lib/pages/my/my_page.dart b/lib/pages/my/my_page.dart index 5c00349e8..b8b1a96e1 100644 --- a/lib/pages/my/my_page.dart +++ b/lib/pages/my/my_page.dart @@ -44,20 +44,20 @@ class _MyPageState extends State { onBackPressed(context); }, child: Scaffold( - appBar: const SysAppBar(title: Text('我的'), needTopOffset: false), + appBar: const SysAppBar(title: Text('Me'), needTopOffset: false), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('播放历史与视频源', style: TextStyle(fontFamily: fontFamily)), + title: Text('Watch history and video sources', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/history/'); }, leading: const Icon(Icons.history_rounded), - title: Text('历史记录', style: TextStyle(fontFamily: fontFamily)), - description: Text('查看播放历史记录', + title: Text('History', style: TextStyle(fontFamily: fontFamily)), + description: Text('View watch history', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -65,8 +65,8 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/download/'); }, leading: const Icon(Icons.download_rounded), - title: Text('下载管理', style: TextStyle(fontFamily: fontFamily)), - description: Text('查看和管理离线下载', + title: Text('Download manager', style: TextStyle(fontFamily: fontFamily)), + description: Text('View and manage offline downloads', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -74,8 +74,8 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/download-settings'); }, leading: const Icon(Icons.settings_rounded), - title: Text('下载设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('配置下载并发数等参数', + title: Text('Download settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure download concurrency and other parameters', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -83,22 +83,22 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/plugin/'); }, leading: const Icon(Icons.extension), - title: Text('规则管理', style: TextStyle(fontFamily: fontFamily)), - description: Text('管理番剧资源规则', + title: Text('Rule management', style: TextStyle(fontFamily: fontFamily)), + description: Text('Manage anime source rules', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( - title: Text('播放器设置', style: TextStyle(fontFamily: fontFamily)), + title: Text('Player settings', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/player'); }, leading: const Icon(Icons.display_settings_rounded), - title: Text('播放设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('设置播放器相关参数', + title: Text('Playback settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure player-related parameters', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -106,8 +106,8 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/danmaku/'); }, leading: const Icon(Icons.subtitles_rounded), - title: Text('弹幕设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('设置弹幕相关参数', + title: Text('Danmaku settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure danmaku-related parameters', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -115,8 +115,8 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/keyboard'); }, leading: const Icon(Icons.keyboard_rounded), - title: Text('操作设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('设置播放器按键映射', + title: Text('Control settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure player key bindings', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -124,22 +124,22 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/proxy'); }, leading: const Icon(Icons.vpn_key_rounded), - title: Text('代理设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('配置HTTP代理', + title: Text('Proxy settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure HTTP proxy', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( - title: Text('应用与外观', style: TextStyle(fontFamily: fontFamily)), + title: Text('App and appearance', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/theme'); }, leading: const Icon(Icons.palette_rounded), - title: Text('外观设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('设置应用主题和刷新率', + title: Text('Appearance settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure app theme and refresh rate', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -147,8 +147,8 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/interface'); }, leading: const Icon(Icons.pages_rounded), - title: Text('界面设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('设置应用界面样式', + title: Text('Interface settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure app interface style', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -156,21 +156,21 @@ class _MyPageState extends State { Modular.to.pushNamed('/settings/webdav/'); }, leading: const Icon(Icons.cloud), - title: Text('同步设置', style: TextStyle(fontFamily: fontFamily)), + title: Text('Sync settings', style: TextStyle(fontFamily: fontFamily)), description: - Text('设置同步参数', style: TextStyle(fontFamily: fontFamily)), + Text('Configure sync parameters', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( - title: Text('其他', style: TextStyle(fontFamily: fontFamily)), + title: Text('Other', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/about/'); }, leading: const Icon(Icons.info_outline_rounded), - title: Text('关于', style: TextStyle(fontFamily: fontFamily)), + title: Text('About', style: TextStyle(fontFamily: fontFamily)), ), ], ), diff --git a/lib/pages/player/controller/player_playback_controller.dart b/lib/pages/player/controller/player_playback_controller.dart index 0f5238465..b40fb053f 100644 --- a/lib/pages/player/controller/player_playback_controller.dart +++ b/lib/pages/player/controller/player_playback_controller.dart @@ -239,7 +239,7 @@ abstract class _PlayerPlaybackController with Store { if (!isCurrentPlayer(player)) { return await _discardIfNotCurrent(player); } - KazumiLogger().i('Player: HTTP 代理设置成功 $formattedProxy'); + KazumiLogger().i('Player: HTTP proxy set successfully $formattedProxy'); } } @@ -303,10 +303,10 @@ abstract class _PlayerPlaybackController with Store { } if (event.toString().contains('Failed to open') && playerBuffering) { KazumiDialog.showToast( - message: '加载失败, 请尝试更换其他视频来源', showActionButton: true); + message: 'Loading failed, please try a different video source', showActionButton: true); } else { KazumiDialog.showToast( - message: '播放器内部错误 ${event.toString()} ${videoUrl()}', + message: 'Player internal error ${event.toString()} ${videoUrl()}', duration: const Duration(seconds: 5), showActionButton: true); } diff --git a/lib/pages/player/controller/player_syncplay_controller.dart b/lib/pages/player/controller/player_syncplay_controller.dart index e936b7ab0..6949b9e17 100644 --- a/lib/pages/player/controller/player_syncplay_controller.dart +++ b/lib/pages/player/controller/player_syncplay_controller.dart @@ -91,7 +91,7 @@ abstract class _PlayerSyncPlayController with Store { } catch (_) {} if (syncPlayEndPointHost == '' || syncPlayEndPointPort == 0) { KazumiDialog.showToast( - message: 'SyncPlay: 服务器地址不合法 $syncPlayEndPoint', + message: 'SyncPlay: invalid server address $syncPlayEndPoint', ); KazumiLogger().e('SyncPlay: invalid server address $syncPlayEndPoint'); return; @@ -111,10 +111,10 @@ abstract class _PlayerSyncPlayController with Store { if (error is SyncplayConnectionException) { exitRoom(); KazumiDialog.showToast( - message: 'SyncPlay: 同步中断 ${error.message}', + message: 'SyncPlay: sync interrupted ${error.message}', duration: const Duration(seconds: 5), showActionButton: true, - actionLabel: '重新连接', + actionLabel: 'Reconnect', onActionPressed: () => createRoom(room, username, changeEpisode), ); } @@ -125,23 +125,23 @@ abstract class _PlayerSyncPlayController with Store { if (message['type'] == 'init') { if (message['username'] == '') { KazumiDialog.showToast( - message: 'SyncPlay: 您是当前房间中的唯一用户', + message: 'SyncPlay: you are the only user in the current room', duration: const Duration(seconds: 5)); setPlayingBangumi(); } else { KazumiDialog.showToast( message: - 'SyncPlay: 您不是当前房间中的唯一用户, 当前以用户 ${message['username']} 进度为准'); + 'SyncPlay: you are not the only user in the current room, now following the progress of user ${message['username']}'); } } if (message['type'] == 'left') { KazumiDialog.showToast( - message: 'SyncPlay: ${message['username']} 离开了房间', + message: 'SyncPlay: ${message['username']} left the room', duration: const Duration(seconds: 5)); } if (message['type'] == 'joined') { KazumiDialog.showToast( - message: 'SyncPlay: ${message['username']} 加入了房间', + message: 'SyncPlay: ${message['username']} joined the room', duration: const Duration(seconds: 5)); } }, @@ -158,7 +158,7 @@ abstract class _PlayerSyncPlayController with Store { if (bangumiID != 0 && episode != 0 && episode != currentEpisode()) { KazumiDialog.showToast( message: - 'SyncPlay: ${message['setBy'] ?? 'unknown'} 切换到第 $episode 话', + 'SyncPlay: ${message['setBy'] ?? 'unknown'} switched to episode $episode', duration: const Duration(seconds: 3)); changeEpisode(episode, currentRoad: currentRoad()); } @@ -190,14 +190,14 @@ abstract class _PlayerSyncPlayController with Store { if (message['paused']) { if (message['position'] != 0) { KazumiDialog.showToast( - message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} 暂停了播放', + message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} paused playback', duration: const Duration(seconds: 3)); pause(enableSync: false); } } else { if (message['position'] != 0) { KazumiDialog.showToast( - message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} 开始了播放', + message: 'SyncPlay: ${message['setBy'] ?? 'unknown'} started playback', duration: const Duration(seconds: 3)); play(enableSync: false); } diff --git a/lib/pages/player/episode_comments_sheet.dart b/lib/pages/player/episode_comments_sheet.dart index 328433bb3..24221e651 100644 --- a/lib/pages/player/episode_comments_sheet.dart +++ b/lib/pages/player/episode_comments_sheet.dart @@ -101,13 +101,13 @@ class _EpisodeCommentsSheetState extends State { if (commentsQueryTimeout) { return SliverFillRemaining( child: GeneralErrorWidget( - errMsg: '评论获取失败', + errMsg: 'Failed to load comments', actions: [ GeneralErrorButton( onPressed: () { _refreshIndicatorKey.currentState?.show(); }, - text: '重试', + text: 'Retry', ), ], ), @@ -116,7 +116,7 @@ class _EpisodeCommentsSheetState extends State { if (commentsIsEmpty) { return const SliverFillRemaining( child: Center( - child: Text('什么都没有找到 (´;ω;`)'), + child: Text('Nothing found (´;ω;`)'), ), ); } @@ -154,7 +154,7 @@ class _EpisodeCommentsSheetState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text(' 本集标题 '), + const Text(' Episode title '), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -188,7 +188,7 @@ class _EpisodeCommentsSheetState extends State { showEpisodeSelection(); }, child: const Text( - '手动切换', + 'Switch manually', style: TextStyle(fontSize: 13), ), ), @@ -203,7 +203,7 @@ class _EpisodeCommentsSheetState extends State { onPressed: toggleSortOrder, child: Observer(builder: (context) { return Text( - videoPageController.isCommentsAscending ? '倒序' : '正序', + videoPageController.isCommentsAscending ? 'Descending' : 'Ascending', style: const TextStyle(fontSize: 13), ); }), @@ -217,7 +217,7 @@ class _EpisodeCommentsSheetState extends State { void showEpisodeSelection() async { final int selectedEpisode = ep == 0 ? EpisodeInfoWidget.of(context)!.episode : ep; - KazumiDialog.showLoading(msg: '分集列表加载中'); + KazumiDialog.showLoading(msg: 'Loading episode list'); final List episodeList = await BangumiApi.getBangumiEpisodesByID( videoPageController.bangumiItem.id); @@ -226,7 +226,7 @@ class _EpisodeCommentsSheetState extends State { return; } if (episodeList.isEmpty) { - KazumiDialog.showToast(message: '未找到分集列表'); + KazumiDialog.showToast(message: 'Episode list not found'); return; } KazumiDialog.show( @@ -240,7 +240,7 @@ class _EpisodeCommentsSheetState extends State { children: [ const Padding( padding: EdgeInsets.fromLTRB(24, 20, 24, 8), - child: Text('分集列表', style: TextStyle(fontSize: 20)), + child: Text('Episode list', style: TextStyle(fontSize: 20)), ), Flexible( child: ListView.builder( @@ -278,7 +278,7 @@ class _EpisodeCommentsSheetState extends State { child: TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), diff --git a/lib/pages/player/player_adjustment_hud.dart b/lib/pages/player/player_adjustment_hud.dart index b8fb89ca5..01ce987e4 100644 --- a/lib/pages/player/player_adjustment_hud.dart +++ b/lib/pages/player/player_adjustment_hud.dart @@ -927,7 +927,7 @@ class _PlayerSpeedHudState extends State { ), const SizedBox(height: 1), Text( - '倍速播放', + 'Playback speed', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) diff --git a/lib/pages/player/player_item.dart b/lib/pages/player/player_item.dart index e484b65bf..72daa630c 100644 --- a/lib/pages/player/player_item.dart +++ b/lib/pages/player/player_item.dart @@ -328,17 +328,17 @@ class _PlayerItemState extends State } if (targetEpisode > episodes.length) { - KazumiDialog.showToast(message: '已经是最新一集'); + KazumiDialog.showToast(message: 'Already the latest episode'); return; } if (targetEpisode <= 0) { - KazumiDialog.showToast(message: '已经是第一集'); + KazumiDialog.showToast(message: 'Already the first episode'); return; } final identifier = videoPageController.roadList[currentRoad].identifier[targetEpisode - 1]; - KazumiDialog.showToast(message: '正在加载$identifier'); + KazumiDialog.showToast(message: 'Loading $identifier'); widget.changeEpisode(targetEpisode, currentRoad: currentRoad); } @@ -630,12 +630,12 @@ class _PlayerItemState extends State Uint8List? screenshot = await playerController.screenshotPng(); if (screenshot == null) { - KazumiDialog.showToast(message: '截图失败:未获取到图像'); + KazumiDialog.showToast(message: 'Screenshot failed: no image captured'); return; } if (isDesktop()) { - KazumiDialog.showToast(message: '桌面端暂未支持保存截图'); + KazumiDialog.showToast(message: 'Saving screenshots is not yet supported on desktop'); return; } final result = await SaverGallery.saveImage( @@ -644,10 +644,10 @@ class _PlayerItemState extends State skipIfExists: false, ); if (!result.isSuccess) { - KazumiDialog.showToast(message: '截图保存失败:${result.errorMessage}'); + KazumiDialog.showToast(message: 'Failed to save screenshot: ${result.errorMessage}'); } } catch (e) { - KazumiDialog.showToast(message: '截图失败:$e'); + KazumiDialog.showToast(message: 'Screenshot failed: $e'); } } @@ -670,15 +670,15 @@ class _PlayerItemState extends State if (androidVideoRenderer == 'mediacodec_embed') { await KazumiDialog.show(builder: (context) { return AlertDialog( - title: const Text('兼容性提示'), - content: const Text('MediaCodec 渲染器不支持超分辨率功能。\n\n' - '如需使用超分辨率,请在播放设置中将视频渲染器切换为 gpu 或 gpu-next。'), + title: const Text('Compatibility notice'), + content: const Text('The MediaCodec renderer does not support super resolution.\n\n' + 'To use super resolution, switch the video renderer to gpu or gpu-next in playback settings.'), actions: [ TextButton( onPressed: () { KazumiDialog.dismiss(); }, - child: const Text('确定'), + child: const Text('OK'), ), ], ); @@ -699,12 +699,12 @@ class _PlayerItemState extends State return StatefulBuilder(builder: (context, setState) { return AlertDialog( - title: const Text('性能提示'), + title: const Text('Performance notice'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('启用超分辨率(质量档)可能会造成设备卡顿,是否继续?'), + const Text('Enabling super resolution (quality mode) may cause stuttering on your device. Continue?'), const SizedBox(height: 12), Row( mainAxisSize: MainAxisSize.min, @@ -714,7 +714,7 @@ class _PlayerItemState extends State onChanged: (value) => setState(() => dontAskAgain = value ?? false), ), - const Text('下次不再询问'), + const Text('Do not ask again'), ], ), ], @@ -727,7 +727,7 @@ class _PlayerItemState extends State } KazumiDialog.dismiss(); }, - child: const Text('取消'), + child: const Text('Cancel'), ), TextButton( onPressed: () async { @@ -737,7 +737,7 @@ class _PlayerItemState extends State } KazumiDialog.dismiss(); }, - child: const Text('确认'), + child: const Text('Confirm'), ), ], ); @@ -835,14 +835,14 @@ class _PlayerItemState extends State index++; setPlaybackSpeed(defaultPlaySpeedList[index]); } else { - KazumiDialog.showToast(message: '已达倍速上限'); + KazumiDialog.showToast(message: 'Maximum playback speed reached'); } } else if (type == "down") { if (index > 0) { index--; setPlaybackSpeed(defaultPlaySpeedList[index]); } else { - KazumiDialog.showToast(message: '已达倍速下限'); + KazumiDialog.showToast(message: 'Minimum playback speed reached'); } } } catch (e) { @@ -1024,7 +1024,7 @@ class _PlayerItemState extends State autoPlayNext) { KazumiDialog.showToast( message: - '正在加载${videoPageController.roadList[playingSelection.road].identifier[playingSelection.episode]}'); + 'Loading ${videoPageController.roadList[playingSelection.road].identifier[playingSelection.episode]}'); try { playerTimer!.cancel(); } catch (_) {} @@ -1038,7 +1038,7 @@ class _PlayerItemState extends State void showDanmakuSearchDialog(String keyword) async { KazumiDialog.dismiss(); - KazumiDialog.showLoading(msg: '弹幕检索中'); + KazumiDialog.showLoading(msg: 'Searching danmaku'); DanmakuSearchResponse danmakuSearchResponse; DanmakuEpisodeResponse danmakuEpisodeResponse; try { @@ -1046,12 +1046,12 @@ class _PlayerItemState extends State await DanmakuApi.getDanmakuSearchResponse(keyword); } catch (e) { KazumiDialog.dismiss(); - KazumiDialog.showToast(message: '弹幕检索错误: ${e.toString()}'); + KazumiDialog.showToast(message: 'Danmaku search error: ${e.toString()}'); return; } KazumiDialog.dismiss(); if (danmakuSearchResponse.animes.isEmpty) { - KazumiDialog.showToast(message: '未找到匹配结果'); + KazumiDialog.showToast(message: 'No matching results found'); return; } await KazumiDialog.show(builder: (context) { @@ -1065,19 +1065,19 @@ class _PlayerItemState extends State title: Text(danmakuInfo.animeTitle), onTap: () async { KazumiDialog.dismiss(); - KazumiDialog.showLoading(msg: '弹幕检索中'); + KazumiDialog.showLoading(msg: 'Searching danmaku'); try { danmakuEpisodeResponse = await DanmakuApi.getDanDanEpisodesByDanDanBangumiID( danmakuInfo.animeId); } catch (e) { KazumiDialog.dismiss(); - KazumiDialog.showToast(message: '弹幕检索错误: ${e.toString()}'); + KazumiDialog.showToast(message: 'Danmaku search error: ${e.toString()}'); return; } KazumiDialog.dismiss(); if (danmakuEpisodeResponse.episodes.isEmpty) { - KazumiDialog.showToast(message: '未找到匹配结果'); + KazumiDialog.showToast(message: 'No matching results found'); return; } KazumiDialog.show(builder: (context) { @@ -1104,13 +1104,13 @@ class _PlayerItemState extends State } if (hasDanmakus) { playerController.danmaku.danmakuOn = true; - KazumiDialog.showToast(message: '弹幕切换成功'); + KazumiDialog.showToast(message: 'Danmaku switched successfully'); } else { playerController.danmaku.danmakuOn = false; - KazumiDialog.showToast(message: '未找到弹幕内容'); + KazumiDialog.showToast(message: 'No danmaku content found'); } } catch (e) { - KazumiDialog.showToast(message: '弹幕切换失败'); + KazumiDialog.showToast(message: 'Failed to switch danmaku'); } }, ); @@ -1134,11 +1134,11 @@ class _PlayerItemState extends State KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('弹幕检索'), + title: const Text('Danmaku search'), content: TextFormField( initialValue: searchKeyword, decoration: const InputDecoration( - hintText: '番剧名', + hintText: 'Anime name', ), onChanged: (value) => searchKeyword = value, onFieldSubmitted: (keyword) { @@ -1152,7 +1152,7 @@ class _PlayerItemState extends State widget.keyboardFocus.requestFocus(); }, child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -1161,7 +1161,7 @@ class _PlayerItemState extends State showDanmakuSearchDialog(searchKeyword); }, child: const Text( - '提交', + 'Submit', ), ), ], @@ -1178,7 +1178,7 @@ class _PlayerItemState extends State title: const Text("Source"), subtitle: Text(playerController.videoUrl), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData(text: playerController.videoUrl), ); @@ -1189,7 +1189,7 @@ class _PlayerItemState extends State subtitle: Text( '${playerController.debug.playerWidth}x${playerController.debug.playerHeight}'), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData( text: @@ -1202,7 +1202,7 @@ class _PlayerItemState extends State title: const Text("VideoParams"), subtitle: Text(playerController.debug.playerVideoParams.toString()), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData( text: @@ -1215,7 +1215,7 @@ class _PlayerItemState extends State title: const Text("AudioParams"), subtitle: Text(playerController.debug.playerAudioParams.toString()), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData( text: @@ -1228,7 +1228,7 @@ class _PlayerItemState extends State title: const Text("Media"), subtitle: Text(playerController.debug.playerPlaylist.toString()), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData( text: @@ -1241,7 +1241,7 @@ class _PlayerItemState extends State title: const Text("AudioTrack"), subtitle: Text(playerController.debug.playerAudioTracks.toString()), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData( text: @@ -1254,7 +1254,7 @@ class _PlayerItemState extends State title: const Text("VideoTrack"), subtitle: Text(playerController.debug.playerVideoTracks.toString()), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData( text: @@ -1268,7 +1268,7 @@ class _PlayerItemState extends State subtitle: Text(playerController.debug.playerAudioBitrate.toString()), onTap: () { - KazumiDialog.showToast(message: '已复制到剪贴板'); + KazumiDialog.showToast(message: 'Copied to clipboard'); Clipboard.setData( ClipboardData( text: @@ -1326,8 +1326,8 @@ class _PlayerItemState extends State child: Material( child: TabBar( tabs: [ - Tab(text: '状态'), - Tab(text: '日志'), + Tab(text: 'Status'), + Tab(text: 'Logs'), ], ), ), @@ -1349,11 +1349,11 @@ class _PlayerItemState extends State void showSyncPlayEndPointSwitchDialog() { if (playerController.syncplay.syncplayController != null) { - KazumiDialog.showToast(message: 'SyncPlay: 请先退出当前房间再切换服务器'); + KazumiDialog.showToast(message: 'SyncPlay: please leave the current room before switching servers'); return; } - final String defaultCustomSyncPlayEndPoint = '自定义服务器'; + final String defaultCustomSyncPlayEndPoint = 'Custom server'; String customSyncPlayEndPoint = defaultCustomSyncPlayEndPoint; String selectedSyncPlayEndPoint = setting.get( SettingBoxKey.syncPlayEndPoint, @@ -1369,7 +1369,7 @@ class _PlayerItemState extends State syncPlayEndPoints.add(selectedSyncPlayEndPoint); } return AlertDialog( - title: const Text('选择服务器'), + title: const Text('Select server'), content: SingleChildScrollView( child: ListBody( children: [ @@ -1404,22 +1404,22 @@ class _PlayerItemState extends State KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('自定义服务器'), + title: const Text('Custom server'), content: TextField( decoration: const InputDecoration( - hintText: '请输入服务器地址', + hintText: 'Please enter the server address', ), onChanged: (value) => serverText = value, ), actions: [ TextButton( - child: const Text('取消'), + child: const Text('Cancel'), onPressed: () { KazumiDialog.dismiss(); }, ), TextButton( - child: const Text('确认'), + child: const Text('Confirm'), onPressed: () { if (serverText.isNotEmpty && !syncPlayEndPoints @@ -1431,7 +1431,7 @@ class _PlayerItemState extends State }); } else { KazumiDialog.showToast( - message: '服务器地址不能重复或为空'); + message: 'Server address cannot be duplicate or empty'); } }, ), @@ -1452,13 +1452,13 @@ class _PlayerItemState extends State ), actions: [ TextButton( - child: const Text('取消'), + child: const Text('Cancel'), onPressed: () { KazumiDialog.dismiss(); }, ), TextButton( - child: const Text('确认'), + child: const Text('Confirm'), onPressed: () { setting.put( SettingBoxKey.syncPlayEndPoint, @@ -1480,7 +1480,7 @@ class _PlayerItemState extends State String username = ''; KazumiDialog.show(builder: (BuildContext context) { return AlertDialog( - title: const Text('加入房间'), + title: const Text('Join room'), content: Form( key: formKey, child: Column( @@ -1489,16 +1489,16 @@ class _PlayerItemState extends State TextFormField( keyboardType: TextInputType.number, decoration: const InputDecoration( - labelText: '房间号', + labelText: 'Room number', ), onChanged: (value) => room = value, validator: (value) { if (value == null || value.isEmpty) { - return '请输入房间号'; + return 'Please enter a room number'; } final regex = RegExp(r'^[0-9]{6,10}$'); if (!regex.hasMatch(value)) { - return '房间号需要6到10位数字'; + return 'Room number must be 6 to 10 digits'; } return null; }, @@ -1506,16 +1506,16 @@ class _PlayerItemState extends State const SizedBox(height: 16), TextFormField( decoration: const InputDecoration( - labelText: '用户名', + labelText: 'Username', ), onChanged: (value) => username = value, validator: (value) { if (value == null || value.isEmpty) { - return '请输入用户名'; + return 'Please enter a username'; } final regex = RegExp(r'^[a-zA-Z]{4,12}$'); if (!regex.hasMatch(value)) { - return '用户名必须为4到12位英文字符'; + return 'Username must be 4 to 12 English characters'; } return null; }, @@ -1528,7 +1528,7 @@ class _PlayerItemState extends State onPressed: () { KazumiDialog.dismiss(); }, - child: const Text('取消'), + child: const Text('Cancel'), ), TextButton( onPressed: () { @@ -1538,7 +1538,7 @@ class _PlayerItemState extends State room, username, widget.changeEpisode); } }, - child: const Text('确定'), + child: const Text('OK'), ), ], ); diff --git a/lib/pages/player/player_item_panel.dart b/lib/pages/player/player_item_panel.dart index 57bbf0f87..e2828250d 100644 --- a/lib/pages/player/player_item_panel.dart +++ b/lib/pages/player/player_item_panel.dart @@ -141,7 +141,7 @@ class _PlayerItemPanelState extends State { fillColor: Colors.white38, floatingLabelBehavior: FloatingLabelBehavior.never, hintText: - playerController.danmaku.danmakuOn ? '发个友善的弹幕见证当下' : '已关闭弹幕', + playerController.danmaku.danmakuOn ? 'Send a friendly danmaku to mark the moment' : 'Danmaku is off', hintStyle: TextStyle(fontSize: isDesktop() ? 15 : 13, color: Colors.white60), alignLabelWithHint: true, @@ -174,7 +174,7 @@ class _PlayerItemPanelState extends State { borderRadius: BorderRadius.circular(isDesktop() ? 8 : 20), ), ), - child: const Text('发送'), + child: const Text('Send'), ), ], ), @@ -203,7 +203,7 @@ class _PlayerItemPanelState extends State { final double currentSpeed = playerController.playback.playerSpeed; KazumiDialog.show(builder: (context) { return AlertDialog( - title: const Text('播放速度'), + title: const Text('Playback speed'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Wrap( @@ -235,7 +235,7 @@ class _PlayerItemPanelState extends State { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -244,7 +244,7 @@ class _PlayerItemPanelState extends State { await widget.setPlaybackSpeed(1.0); KazumiDialog.dismiss(); }, - child: const Text('默认速度'), + child: const Text('Default speed'), ), ], ); @@ -255,7 +255,7 @@ class _PlayerItemPanelState extends State { KazumiDialog.show(builder: (context) { String input = ""; return AlertDialog( - title: const Text('跳过秒数'), + title: const Text('Skip seconds'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( @@ -276,7 +276,7 @@ class _PlayerItemPanelState extends State { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -289,7 +289,7 @@ class _PlayerItemPanelState extends State { KazumiDialog.dismiss(); } }, - child: const Text('确定'), + child: const Text('OK'), ), ], ); @@ -383,14 +383,14 @@ class _PlayerItemPanelState extends State { widget.handleDanmaku(); }, tooltip: playerController.danmaku.danmakuLoading - ? '弹幕加载中...' - : (playerController.danmaku.danmakuOn ? '关闭弹幕' : '打开弹幕'), + ? 'Loading danmaku...' + : (playerController.danmaku.danmakuOn ? 'Turn off danmaku' : 'Turn on danmaku'), ); } Widget forwardIcon() { return Tooltip( - message: '快进${playerController.playback.buttonSkipTime}秒,长按修改时间', + message: 'Skip forward ${playerController.playback.buttonSkipTime}s, long press to change', child: GestureDetector( onLongPress: () => showForwardChange(), child: IconButton( @@ -670,7 +670,7 @@ class _PlayerItemPanelState extends State { icon: Icon(playerController.playback.playing ? Icons.pause_rounded : Icons.play_arrow_rounded), - tooltip: playerController.playback.playing ? '暂停' : '播放', + tooltip: playerController.playback.playing ? 'Pause' : 'Play', onPressed: () { playerController.playOrPause(); }, @@ -682,7 +682,7 @@ class _PlayerItemPanelState extends State { IconButton( color: Colors.white, icon: const Icon(Icons.skip_next_rounded), - tooltip: '下一集', + tooltip: 'Next episode', onPressed: () => widget.handlePreNextEpisode('next'), ), if (isDesktop()) @@ -748,7 +748,7 @@ class _PlayerItemPanelState extends State { }, color: Colors.white, icon: cachedDanmakuSettingIcon!, - tooltip: '弹幕设置', + tooltip: 'Danmaku settings', ), if (isSpaceEnough) danmakuTextField, ], @@ -767,8 +767,8 @@ class _PlayerItemPanelState extends State { widget.handleDanmaku(); }, tooltip: playerController.danmaku.danmakuOn - ? '关闭弹幕' - : '打开弹幕', + ? 'Turn off danmaku' + : 'Turn on danmaku', ), if (playerController.danmaku.danmakuOn) ...[ IconButton( @@ -801,7 +801,7 @@ class _PlayerItemPanelState extends State { }, color: Colors.white, icon: cachedDanmakuSettingIcon!, - tooltip: '弹幕设置', + tooltip: 'Danmaku settings', ), Expanded(child: danmakuTextField), ], @@ -822,7 +822,7 @@ class _PlayerItemPanelState extends State { } }, child: const Text( - '超分辨率', + 'Super resolution', style: TextStyle(color: Colors.white), ), ); @@ -839,10 +839,10 @@ class _PlayerItemPanelState extends State { alignment: Alignment.centerLeft, child: Text( index + 1 == 1 - ? '关闭' + ? 'Close' : index + 1 == 2 - ? '效率档' - : '质量档', + ? 'Performance mode' + : 'Quality mode', style: TextStyle( color: playerController .playback.superResolutionType == @@ -872,7 +872,7 @@ class _PlayerItemPanelState extends State { }, child: Text( playerController.playback.playerSpeed == 1.0 - ? '倍速' + ? 'Speed' : '${playerController.playback.playerSpeed}x', style: const TextStyle(color: Colors.white), ), @@ -923,7 +923,7 @@ class _PlayerItemPanelState extends State { Icons.aspect_ratio_rounded, color: Colors.white, ), - tooltip: '视频比例', + tooltip: 'Aspect ratio', ); }, menuChildren: [ @@ -958,7 +958,7 @@ class _PlayerItemPanelState extends State { : IconButton( color: Colors.white, icon: const Icon(Icons.menu_open_rounded), - tooltip: '选集面板', + tooltip: 'Episode panel', onPressed: () { widget.toggleMenu(); }, @@ -974,8 +974,8 @@ class _PlayerItemPanelState extends State { ? Icons.fullscreen_exit_rounded : Icons.fullscreen_rounded), tooltip: videoPageController.isFullscreen - ? '退出全屏' - : '全屏', + ? 'Exit fullscreen' + : 'Fullscreen', onPressed: () { widget.handleFullscreen(); }, @@ -1011,7 +1011,7 @@ class _PlayerItemPanelState extends State { IconButton( color: Colors.white, icon: const Icon(Icons.arrow_back_rounded), - tooltip: '返回', + tooltip: 'Back', onPressed: () { widget.onBackPressed(context); }, @@ -1051,7 +1051,7 @@ class _PlayerItemPanelState extends State { final bool supported = await PipUtils.isAndroidPIPSupported(); if (!supported) { - KazumiDialog.showToast(message: '当前设备不支持画中画'); + KazumiDialog.showToast(message: 'This device does not support picture-in-picture'); return; } await PipUtils.updateAndroidPIPActions( @@ -1065,10 +1065,10 @@ class _PlayerItemPanelState extends State { height: playerController.debug.playerHeight, ); if (!entered) { - KazumiDialog.showToast(message: '进入画中画失败'); + KazumiDialog.showToast(message: 'Failed to enter picture-in-picture'); } }, - tooltip: '画中画', + tooltip: 'Picture-in-picture', icon: const Icon( Icons.picture_in_picture, color: Colors.white, @@ -1092,7 +1092,7 @@ class _PlayerItemPanelState extends State { controller.open(); } }, - tooltip: '更多选项', + tooltip: 'More options', icon: const Icon( Icons.more_vert, color: Colors.white, @@ -1109,7 +1109,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("弹幕切换"), + child: Text("Switch danmaku"), ), ), ), @@ -1122,7 +1122,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("视频详情"), + child: Text("Video details"), ), ), ), @@ -1144,7 +1144,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("远程投屏"), + child: Text("Remote casting"), ), ), ), @@ -1157,7 +1157,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("外部播放"), + child: Text("External player"), ), ), ), @@ -1174,7 +1174,7 @@ class _PlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "不开启", + "Off", style: TextStyle( color: !TimedShutdownService().isActive ? Theme.of(context).colorScheme.primary @@ -1191,7 +1191,7 @@ class _PlayerItemPanelState extends State { onExpired: widget.pauseForTimedShutdown); KazumiDialog.showToast( message: - '已设置 ${TimedShutdownService().formatMinutesToDisplay(minutes)} 后定时关闭'); + 'Sleep timer set for ${TimedShutdownService().formatMinutesToDisplay(minutes)}'); }, child: Container( height: 48, @@ -1199,7 +1199,7 @@ class _PlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "$minutes 分钟", + "$minutes min", style: TextStyle( color: TimedShutdownService().setMinutes == minutes @@ -1221,7 +1221,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("自定义"), + child: Text("Custom"), ), ), ), @@ -1237,8 +1237,8 @@ class _PlayerItemPanelState extends State { builder: (context, remainingSeconds, child) { return Text( remainingSeconds > 0 - ? "定时关闭 (${TimedShutdownService().formatRemainingTime()})" - : "定时关闭", + ? "Sleep timer (${TimedShutdownService().formatRemainingTime()})" + : "Sleep timer", ); }, ), @@ -1254,7 +1254,7 @@ class _PlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "当前房间: ${playerController.syncplay.syncplayRoom == '' ? '未加入' : playerController.syncplay.syncplayRoom}"), + "Current room: ${playerController.syncplay.syncplayRoom == '' ? 'Not joined' : playerController.syncplay.syncplayRoom}"), ), ), ), @@ -1265,7 +1265,7 @@ class _PlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "网络延时: ${playerController.syncplay.syncplayClientRtt}ms"), + "Network latency: ${playerController.syncplay.syncplayClientRtt}ms"), ), ), ), @@ -1278,7 +1278,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("加入房间"), + child: Text("Join room"), ), ), ), @@ -1291,7 +1291,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("切换服务器"), + child: Text("Switch server"), ), ), ), @@ -1304,7 +1304,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("断开连接"), + child: Text("Disconnect"), ), ), ), @@ -1314,7 +1314,7 @@ class _PlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("一起看"), + child: Text("Watch together"), ), ), ), @@ -1345,7 +1345,7 @@ class _PlayerItemPanelState extends State { Icons.photo_camera_outlined, color: Colors.white, ), - tooltip: '截图', + tooltip: 'Screenshot', onPressed: () { widget.handleScreenShot(); }, @@ -1357,7 +1357,7 @@ class _PlayerItemPanelState extends State { : Icons.lock_open, color: Colors.white, ), - tooltip: playerController.panel.lockPanel ? '解锁面板' : '锁定面板', + tooltip: playerController.panel.lockPanel ? 'Unlock panel' : 'Lock panel', onPressed: () { playerController.panel.lockPanel = !playerController.panel.lockPanel; diff --git a/lib/pages/player/smallest_player_item_panel.dart b/lib/pages/player/smallest_player_item_panel.dart index db299caf9..ab0225280 100644 --- a/lib/pages/player/smallest_player_item_panel.dart +++ b/lib/pages/player/smallest_player_item_panel.dart @@ -98,7 +98,7 @@ class _SmallestPlayerItemPanelState extends State { KazumiDialog.show(builder: (context) { String input = ""; return AlertDialog( - title: const Text('跳过秒数'), + title: const Text('Skip seconds'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( @@ -119,7 +119,7 @@ class _SmallestPlayerItemPanelState extends State { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -132,7 +132,7 @@ class _SmallestPlayerItemPanelState extends State { KazumiDialog.dismiss(); } }, - child: const Text('确定'), + child: const Text('OK'), ), ], ); @@ -219,14 +219,14 @@ class _SmallestPlayerItemPanelState extends State { widget.handleDanmaku(); }, tooltip: playerController.danmaku.danmakuLoading - ? '弹幕加载中...' - : (playerController.danmaku.danmakuOn ? '关闭弹幕' : '打开弹幕'), + ? 'Loading danmaku...' + : (playerController.danmaku.danmakuOn ? 'Turn off danmaku' : 'Turn on danmaku'), ); } Widget forwardIcon() { return Tooltip( - message: '快进${playerController.playback.buttonSkipTime}秒,长按修改时间', + message: 'Skip forward ${playerController.playback.buttonSkipTime}s, long press to change', child: GestureDetector( onLongPress: () => showForwardChange(), child: IconButton( @@ -424,7 +424,7 @@ class _SmallestPlayerItemPanelState extends State { icon: Icon(playerController.playback.playing ? Icons.pause_rounded : Icons.play_arrow_rounded), - tooltip: playerController.playback.playing ? '暂停' : '播放', + tooltip: playerController.playback.playing ? 'Pause' : 'Play', onPressed: () { playerController.playOrPause(); }, @@ -467,7 +467,7 @@ class _SmallestPlayerItemPanelState extends State { icon: Icon(videoPageController.isFullscreen ? Icons.fullscreen_exit_rounded : Icons.fullscreen_rounded), - tooltip: videoPageController.isFullscreen ? '退出全屏' : '全屏', + tooltip: videoPageController.isFullscreen ? 'Exit fullscreen' : 'Fullscreen', onPressed: () { widget.handleFullscreen(); }, @@ -486,7 +486,7 @@ class _SmallestPlayerItemPanelState extends State { IconButton( color: Colors.white, icon: const Icon(Icons.arrow_back_rounded), - tooltip: '返回', + tooltip: 'Back', onPressed: () { widget.onBackPressed(context); }, @@ -516,7 +516,7 @@ class _SmallestPlayerItemPanelState extends State { final bool supported = await PipUtils.isAndroidPIPSupported(); if (!supported) { - KazumiDialog.showToast(message: '当前设备不支持画中画'); + KazumiDialog.showToast(message: 'This device does not support picture-in-picture'); return; } await PipUtils.updateAndroidPIPActions( @@ -530,10 +530,10 @@ class _SmallestPlayerItemPanelState extends State { height: playerController.debug.playerHeight, ); if (!entered) { - KazumiDialog.showToast(message: '进入画中画失败'); + KazumiDialog.showToast(message: 'Failed to enter picture-in-picture'); } }, - tooltip: '画中画', + tooltip: 'Picture-in-picture', icon: const Icon(Icons.picture_in_picture, color: Colors.white)), // 弹幕开关 @@ -556,7 +556,7 @@ class _SmallestPlayerItemPanelState extends State { controller.open(); } }, - tooltip: '更多选项', + tooltip: 'More options', icon: const Icon( Icons.more_vert, color: Colors.white, @@ -577,10 +577,10 @@ class _SmallestPlayerItemPanelState extends State { alignment: Alignment.centerLeft, child: Text( index + 1 == 1 - ? '自动' + ? 'Auto' : index + 1 == 2 - ? '裁切填充' - : '拉伸填充', + ? 'Crop to fill' + : 'Stretch to fill', style: TextStyle( color: index + 1 == playerController.panel.aspectRatioType @@ -596,7 +596,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("视频比例"), + child: Text("Aspect ratio"), ), ), ), @@ -631,7 +631,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("倍速"), + child: Text("Speed"), ), ), ), @@ -648,10 +648,10 @@ class _SmallestPlayerItemPanelState extends State { alignment: Alignment.centerLeft, child: Text( index + 1 == 1 - ? '关闭' + ? 'Close' : index + 1 == 2 - ? '效率档' - : '质量档', + ? 'Performance mode' + : 'Quality mode', style: TextStyle( color: playerController .playback.superResolutionType == @@ -669,7 +669,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("超分辨率"), + child: Text("Super resolution"), ), ), ), @@ -682,7 +682,7 @@ class _SmallestPlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "当前房间: ${playerController.syncplay.syncplayRoom == '' ? '未加入' : playerController.syncplay.syncplayRoom}"), + "Current room: ${playerController.syncplay.syncplayRoom == '' ? 'Not joined' : playerController.syncplay.syncplayRoom}"), ), ), ), @@ -693,7 +693,7 @@ class _SmallestPlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "网络延时: ${playerController.syncplay.syncplayClientRtt}ms"), + "Network latency: ${playerController.syncplay.syncplayClientRtt}ms"), ), ), ), @@ -706,7 +706,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("加入房间"), + child: Text("Join room"), ), ), ), @@ -719,7 +719,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("切换服务器"), + child: Text("Switch server"), ), ), ), @@ -732,7 +732,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("断开连接"), + child: Text("Disconnect"), ), ), ), @@ -742,7 +742,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("一起看"), + child: Text("Watch together"), ), ), ), @@ -755,7 +755,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("弹幕切换"), + child: Text("Switch danmaku"), ), ), ), @@ -787,7 +787,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("弹幕设置"), + child: Text("Danmaku settings"), ), ), ), @@ -800,7 +800,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("视频详情"), + child: Text("Video details"), ), ), ), @@ -822,7 +822,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("远程投屏"), + child: Text("Remote casting"), ), ), ), @@ -835,7 +835,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("外部播放"), + child: Text("External player"), ), ), ), @@ -852,7 +852,7 @@ class _SmallestPlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "不开启", + "Off", style: TextStyle( color: !TimedShutdownService().isActive ? Theme.of(context).colorScheme.primary @@ -869,7 +869,7 @@ class _SmallestPlayerItemPanelState extends State { onExpired: widget.pauseForTimedShutdown); KazumiDialog.showToast( message: - '已设置 ${TimedShutdownService().formatMinutesToDisplay(minutes)} 后定时关闭'); + 'Sleep timer set for ${TimedShutdownService().formatMinutesToDisplay(minutes)}'); }, child: Container( height: 48, @@ -877,7 +877,7 @@ class _SmallestPlayerItemPanelState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - "$minutes 分钟", + "$minutes min", style: TextStyle( color: TimedShutdownService().setMinutes == minutes @@ -899,7 +899,7 @@ class _SmallestPlayerItemPanelState extends State { constraints: BoxConstraints(minWidth: 112), child: Align( alignment: Alignment.centerLeft, - child: Text("自定义"), + child: Text("Custom"), ), ), ), @@ -915,8 +915,8 @@ class _SmallestPlayerItemPanelState extends State { builder: (context, remainingSeconds, child) { return Text( remainingSeconds > 0 - ? "定时关闭 (${TimedShutdownService().formatRemainingTime()})" - : "定时关闭", + ? "Sleep timer (${TimedShutdownService().formatRemainingTime()})" + : "Sleep timer", ); }, ), diff --git a/lib/pages/plugin_editor/plugin_editor_page.dart b/lib/pages/plugin_editor/plugin_editor_page.dart index 90b4655eb..b623e25f9 100644 --- a/lib/pages/plugin_editor/plugin_editor_page.dart +++ b/lib/pages/plugin_editor/plugin_editor_page.dart @@ -52,15 +52,15 @@ class _PluginEditorPageState extends State { final MenuController captchaDetectTypeMenuController = MenuController(); static const Map _captchaTypeMap = { - CaptchaType.imageCaptcha: '图片验证码', - CaptchaType.autoClickButton: '自动点击按钮', - CaptchaType.customJavaScript: '自定义 JS 验证', + CaptchaType.imageCaptcha: 'Image captcha', + CaptchaType.autoClickButton: 'Auto-click button', + CaptchaType.customJavaScript: 'Custom JS verification', }; static const Map _captchaDetectTypeMap = { CaptchaDetectType.xpath: 'XPath', - CaptchaDetectType.text: '文本', - CaptchaDetectType.regex: '正则', + CaptchaDetectType.text: 'Text', + CaptchaDetectType.regex: 'Regex', }; @override @@ -127,7 +127,7 @@ class _PluginEditorPageState extends State { return Scaffold( appBar: const SysAppBar( - title: Text('规则编辑器'), + title: Text('Rule editor'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), @@ -191,18 +191,18 @@ class _PluginEditorPageState extends State { ), const SizedBox(height: 20), ExpansionTile( - title: const Text('高级选项'), + title: const Text('Advanced options'), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.zero), children: [ SettingsSection( - title: Text('行为设置', + title: Text('Behavior settings', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( - title: Text('简易解析', + title: Text('Simple parsing', style: TextStyle(fontFamily: fontFamily)), - description: Text('使用简易解析器而不是现代解析器', + description: Text('Use the simple parser instead of the modern parser', style: TextStyle(fontFamily: fontFamily)), initialValue: useLegacyParser, onToggle: (v) => setState( @@ -211,25 +211,25 @@ class _PluginEditorPageState extends State { SettingsTile.switchTile( title: Text('POST', style: TextStyle(fontFamily: fontFamily)), - description: Text('使用 POST 而不是 GET 进行检索', + description: Text('Use POST instead of GET for searching', style: TextStyle(fontFamily: fontFamily)), initialValue: usePost, onToggle: (v) => setState(() => usePost = v ?? !usePost), ), SettingsTile.switchTile( - title: Text('内置播放器', + title: Text('Built-in player', style: TextStyle(fontFamily: fontFamily)), - description: Text('使用内置播放器播放视频', + description: Text('Play videos with the built-in player', style: TextStyle(fontFamily: fontFamily)), initialValue: useNativePlayer, onToggle: (v) => setState( () => useNativePlayer = v ?? !useNativePlayer), ), SettingsTile.switchTile( - title: Text('广告过滤', + title: Text('Ad filtering', style: TextStyle(fontFamily: fontFamily)), - description: Text('启用 HLS 广告过滤', + description: Text('Enable HLS ad filtering', style: TextStyle(fontFamily: fontFamily)), initialValue: adBlocker, onToggle: (v) => @@ -238,7 +238,7 @@ class _PluginEditorPageState extends State { ], ), SettingsSection( - title: Text('网络设置', + title: Text('Network settings', style: TextStyle(fontFamily: fontFamily)), tiles: [ CustomSettingsTile( @@ -260,13 +260,13 @@ class _PluginEditorPageState extends State { ], ), SettingsSection( - title: Text('反反爬虫配置', + title: Text('Anti-anti-crawler configuration', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( - title: Text('启用反反爬虫', + title: Text('Enable anti-anti-crawler', style: TextStyle(fontFamily: fontFamily)), - description: Text('检索失败时显示验证码验证按钮而非重试', + description: Text('Show a captcha verification button instead of retry when search fails', style: TextStyle(fontFamily: fontFamily)), initialValue: antiCrawlerEnabled, onToggle: (v) => setState(() => @@ -281,17 +281,17 @@ class _PluginEditorPageState extends State { captchaTypeMenuController.open(); } }, - title: Text('验证类型', + title: Text('Verification type', style: TextStyle(fontFamily: fontFamily)), description: Text( switch (captchaType) { CaptchaType.imageCaptcha => - '图片验证码(展示验证码图片,用户手动输入)', + 'Image captcha (shows a captcha image for manual input)', CaptchaType.autoClickButton => - '自动点击验证按钮(检测到按钮后自动模拟点击)', + 'Auto-click verification button (clicks automatically when the button is detected)', CaptchaType.customJavaScript => - '自定义 JS 验证(加载页面后执行规则脚本)', - _ => '未知验证类型', + 'Custom JS verification (runs the rule script after the page loads)', + _ => 'Unknown verification type', }, style: TextStyle(fontFamily: fontFamily), ), @@ -299,7 +299,7 @@ class _PluginEditorPageState extends State { consumeOutsideTap: true, controller: captchaTypeMenuController, builder: (_, __, ___) => Text( - _captchaTypeMap[captchaType] ?? '未知', + _captchaTypeMap[captchaType] ?? 'Unknown', style: TextStyle(fontFamily: fontFamily), ), menuChildren: [ @@ -339,16 +339,16 @@ class _PluginEditorPageState extends State { captchaDetectTypeMenuController.open(); } }, - title: Text('验证页检测方式', + title: Text('Verification page detection method', style: TextStyle(fontFamily: fontFamily)), - description: Text('优先使用该标记判断搜索响应是否为验证页', + description: Text('Prefer this marker to determine whether the search response is a verification page', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: captchaDetectTypeMenuController, builder: (_, __, ___) => Text( _captchaDetectTypeMap[captchaDetectType] ?? - '未知', + 'Unknown', style: TextStyle(fontFamily: fontFamily), ), menuChildren: [ @@ -389,11 +389,11 @@ class _PluginEditorPageState extends State { controller: captchaDetectValueController, label: 'CaptchaDetectValue', hint: captchaDetectType == CaptchaDetectType.text - ? '身份验证' + ? 'Authentication' : captchaDetectType == CaptchaDetectType.regex - ? '身份验证|smart_verify' + ? 'Authentication|smart_verify' : '//button[@id="verify"]', - helper: '留空时回退到旧的图片/按钮 XPath 检测', + helper: 'When left empty, fall back to the old image or button XPath detection', ), ), if (captchaType == CaptchaType.imageCaptcha) ...[ @@ -404,7 +404,7 @@ class _PluginEditorPageState extends State { controller: captchaImageController, label: 'CaptchaImage (XPath)', hint: '//img[@class="captcha"]', - helper: '验证码图片元素的 XPath', + helper: 'XPath of the captcha image element', ), ), CustomSettingsTile( @@ -414,7 +414,7 @@ class _PluginEditorPageState extends State { controller: captchaInputController, label: 'CaptchaInput (XPath)', hint: '//input[@name="captcha"]', - helper: '验证码输入框元素的 XPath', + helper: 'XPath of the captcha input element', ), ), ], @@ -429,8 +429,8 @@ class _PluginEditorPageState extends State { : 'VerifyButton (XPath)', hint: '//button[@type="submit"]', helper: captchaType == CaptchaType.imageCaptcha - ? '验证提交按钮元素的 XPath' - : '验证按钮元素的 XPath,检测到后自动点击', + ? 'XPath of the verification submit button element' + : 'XPath of the verification button element, clicked automatically when detected', ), ), if (captchaType == CaptchaType.customJavaScript) @@ -443,7 +443,7 @@ class _PluginEditorPageState extends State { hint: 'KazumiCaptcha.log("ready"); KazumiCaptcha.done();', helper: - '可调用 KazumiCaptcha.log/clicked/done/fail', + 'You can call KazumiCaptcha.log/clicked/done/fail', maxLines: 8, ), ), diff --git a/lib/pages/plugin_editor/plugin_shop_page.dart b/lib/pages/plugin_editor/plugin_shop_page.dart index 6ce7a5b47..7437c3ef6 100644 --- a/lib/pages/plugin_editor/plugin_shop_page.dart +++ b/lib/pages/plugin_editor/plugin_shop_page.dart @@ -153,7 +153,7 @@ class _PluginShopPageState extends State { if (sortedList[index].lastUpdate > 0) ...[ const SizedBox(height: 4), Text( - '更新时间: ${DateTime.fromMillisecondsSinceEpoch(sortedList[index].lastUpdate).toString().split('.')[0]}', + 'Updated: ${DateTime.fromMillisecondsSinceEpoch(sortedList[index].lastUpdate).toString().split('.')[0]}', style: const TextStyle(color: Colors.grey), ), ], @@ -163,43 +163,43 @@ class _PluginShopPageState extends State { onPressed: () async { if (pluginsController.pluginStatus(sortedList[index]) == 'install') { - KazumiDialog.showToast(message: '导入中'); + KazumiDialog.showToast(message: 'Importing'); int res = await pluginsController .tryUpdatePluginByName(sortedList[index].name); if (res == 0) { - KazumiDialog.showToast(message: '导入成功'); + KazumiDialog.showToast(message: 'Import succeeded'); setState(() {}); } else if (res == 1) { KazumiDialog.showToast( - message: 'kazumi版本过低, 此规则不兼容当前版本'); + message: 'Kazumi version is too low, this rule is not compatible with the current version'); } else if (res == 2) { - KazumiDialog.showToast(message: '导入规则失败'); + KazumiDialog.showToast(message: 'Failed to import rule'); } } if (pluginsController.pluginStatus(sortedList[index]) == 'update') { - KazumiDialog.showToast(message: '更新中'); + KazumiDialog.showToast(message: 'Updating'); int res = await pluginsController .tryUpdatePluginByName(sortedList[index].name); if (res == 0) { - KazumiDialog.showToast(message: '更新成功'); + KazumiDialog.showToast(message: 'Update succeeded'); setState(() {}); } else if (res == 1) { KazumiDialog.showToast( - message: 'kazumi版本过低, 此规则不兼容当前版本'); + message: 'Kazumi version is too low, this rule is not compatible with the current version'); } else if (res == 2) { - KazumiDialog.showToast(message: '更新规则失败'); + KazumiDialog.showToast(message: 'Failed to update rule'); } } }, child: Text(pluginsController .pluginStatus(sortedList[index]) == 'install' - ? '安装' + ? 'Install' : (pluginsController.pluginStatus(sortedList[index]) == 'installed') - ? '已安装' - : '更新'), + ? 'Installed' + : 'Update'), )), ); }, @@ -210,19 +210,19 @@ class _PluginShopPageState extends State { Widget get timeoutWidget { return Center( child: GeneralErrorWidget( - errMsg: '啊咧(⊙.⊙) 无法访问远程仓库\n${enableGitProxy ? '镜像已启用' : '镜像已禁用'}', + errMsg: 'Oh no (⊙.⊙) Cannot access the remote repository\n${enableGitProxy ? 'Mirror enabled' : 'Mirror disabled'}', actions: [ GeneralErrorButton( onPressed: () { Modular.to.pushNamed('/settings/webdav/'); }, - text: enableGitProxy ? '禁用镜像' : '启用镜像', + text: enableGitProxy ? 'Disable mirror' : 'Enable mirror', ), GeneralErrorButton( onPressed: () { _handleRefresh(); }, - text: '刷新', + text: 'Refresh', ), ], ), @@ -242,18 +242,18 @@ class _PluginShopPageState extends State { }, child: Scaffold( appBar: SysAppBar( - title: const Text('规则仓库'), + title: const Text('Rule repository'), actions: [ IconButton( onPressed: _toggleSort, - tooltip: sortByName ? '按名称排序' : '按更新时间排序', + tooltip: sortByName ? 'Sort by name' : 'Sort by update time', icon: Icon(sortByName ? Icons.sort_by_alpha : Icons.access_time)), IconButton( onPressed: () { _handleRefresh(); }, - tooltip: '刷新规则列表', + tooltip: 'Refresh rule list', icon: const Icon(Icons.refresh)) ], ), diff --git a/lib/pages/plugin_editor/plugin_test_page.dart b/lib/pages/plugin_editor/plugin_test_page.dart index 6ca227f1c..b2f6ea07c 100644 --- a/lib/pages/plugin_editor/plugin_test_page.dart +++ b/lib/pages/plugin_editor/plugin_test_page.dart @@ -114,7 +114,7 @@ class _PluginTestPageState extends State { } catch (e) { KazumiLogger() .e('PluginTest: failed to parse HTML item ${index + 1}', error: e); - return "解析失败:$e"; + return "Parse failed: $e"; } } @@ -163,17 +163,17 @@ class _PluginTestPageState extends State { onPopInvokedWithResult: (didPop, _) => !didPop ? onBackPressed() : null, child: Scaffold( appBar: SysAppBar( - title: Text('${plugin.name} 测试'), + title: Text('${plugin.name} test'), actions: [ IconButton( onPressed: isTesting ? null : startTest, icon: const Icon(Icons.bug_report_outlined), - tooltip: '开始测试', + tooltip: 'Start test', ), IconButton( onPressed: resetState, icon: const Icon(Icons.refresh), - tooltip: '重置', + tooltip: 'Reset', ), ], ), @@ -190,7 +190,7 @@ class _PluginTestPageState extends State { _buildErrorWidget(theme), _buildExpansionTile( theme: theme, - title: '1. 搜索请求测试', + title: '1. Search request test', subtitle: _getSearchSubtitle(), expanded: false, child: _buildSearchContent(theme), @@ -198,7 +198,7 @@ class _PluginTestPageState extends State { _h12, _buildExpansionTile( theme: theme, - title: '2. 搜索解析测试', + title: '2. Search parse test', subtitle: _getParseSubtitle(), expanded: false, child: _buildParseContent(theme), @@ -206,7 +206,7 @@ class _PluginTestPageState extends State { _h12, _buildExpansionTile( theme: theme, - title: '3. 章节列表测试', + title: '3. Chapter list test', subtitle: _getChapterSubtitle(), expanded: _hasSearchData, child: _buildChapterContent(theme), @@ -242,7 +242,7 @@ class _PluginTestPageState extends State { Widget _buildKeywordInput(ThemeData theme) => TextField( controller: testKeywordController, decoration: InputDecoration( - labelText: '测试关键词', + labelText: 'Test keyword', border: OutlineInputBorder( borderSide: BorderSide(color: theme.getCoreColor(CoreColorType.waiting))), @@ -284,7 +284,7 @@ class _PluginTestPageState extends State { backgroundColor: theme .getCoreColor(CoreColorType.error) .withValues(alpha: 0.1)), - child: Text('重试测试', + child: Text('Retry test', style: TextStyle( color: theme.colorScheme.onErrorContainer)), ), @@ -316,21 +316,21 @@ class _PluginTestPageState extends State { ); String _getSearchSubtitle() { - if (isTesting) return '测试中...'; - if (!_hasSearchHtml) return '未执行测试'; - return 'HTML长度:${searchHtml.length} 字符'; + if (isTesting) return 'Testing...'; + if (!_hasSearchHtml) return 'Test not run'; + return 'HTML length: ${searchHtml.length} chars'; } // 简化副标题颜色逻辑:仅三类 Color _getSubtitleColor(String subtitle, ThemeData theme) { - if (subtitle.contains('测试中') || - subtitle.contains('获取中') || - subtitle.contains('解析中')) { + if (subtitle.contains('Testing') || + subtitle.contains('Loading') || + subtitle.contains('Parsing')) { return theme.getCoreColor(CoreColorType.waiting); } - if (subtitle.contains('失败') || - subtitle.contains('无可用') || - subtitle.contains('无有效')) { + if (subtitle.contains('failed') || + subtitle.contains('No available') || + subtitle.contains('No valid')) { return theme.getCoreColor(CoreColorType.error); } return theme.getCoreColor(CoreColorType.success); @@ -338,7 +338,7 @@ class _PluginTestPageState extends State { Widget _buildSearchContent(ThemeData theme) { if (isTesting) return _buildLoading(theme); - if (!_hasSearchHtml) return _buildEmpty('点击顶部「开始测试」按钮执行', theme); + if (!_hasSearchHtml) return _buildEmpty('Tap the “Start test” button at the top to run', theme); return Container( margin: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.all(8.0), @@ -360,16 +360,16 @@ class _PluginTestPageState extends State { } String _getParseSubtitle() { - if (isTesting && _showItemHtmlIdx == null) return '解析中...'; - if (!_hasSearchHtml) return '未执行解析'; - if (!_hasSearchData) return '未解析到结果'; - return '解析到 ${searchRes?.data.length ?? 0} 条结果'; + if (isTesting && _showItemHtmlIdx == null) return 'Parsing...'; + if (!_hasSearchHtml) return 'Parsing not run'; + if (!_hasSearchData) return 'No results parsed'; + return 'Parsed ${searchRes?.data.length ?? 0} results'; } Widget _buildParseContent(ThemeData theme) { if (isTesting && _showItemHtmlIdx == null) return _buildLoading(theme); - if (!_hasSearchHtml) return _buildEmpty('请先完成搜索请求测试', theme); - if (!_hasSearchData) return _buildEmpty('未解析到搜索结果', theme, isError: true); + if (!_hasSearchHtml) return _buildEmpty('Please complete the search request test first', theme); + if (!_hasSearchData) return _buildEmpty('No valid search results', theme, isError: true); return Column(children: [ ListView.builder( @@ -385,7 +385,7 @@ class _PluginTestPageState extends State { Widget _buildSearchItemCard(SearchItem item, int i, ThemeData theme) { final isShowHtml = _showItemHtmlIdx == i; - final itemHtml = _itemHtmlMap[i] ?? '加载中...'; + final itemHtml = _itemHtmlMap[i] ?? 'Loading...'; return Column(children: [ Card( @@ -412,11 +412,11 @@ class _PluginTestPageState extends State { size: 18, color: theme.getCoreColor(CoreColorType.success), ), - tooltip: isShowHtml ? '隐藏HTML' : '查看HTML', + tooltip: isShowHtml ? 'Hide HTML' : 'View HTML', ), ]), _h8, - Text('链接:${item.src}', + Text('Link: ${item.src}', style: theme.textTheme.bodySmall?.copyWith( color: theme.getCoreColor(CoreColorType.waiting))), ]), @@ -447,19 +447,19 @@ class _PluginTestPageState extends State { } String _getChapterSubtitle() { - if (isTesting) return '获取中...'; - if (!_hasSearchData) return '无有效搜索结果'; - if (!_needChapterParse) return '无需解析章节'; - if (chapters == null) return '未获取章节数据'; - return '获取到 ${chapters?.length ?? 0} 个播放列表'; + if (isTesting) return 'Loading...'; + if (!_hasSearchData) return 'No valid search results'; + if (!_needChapterParse) return 'No chapter parsing needed'; + if (chapters == null) return 'No chapter data retrieved'; + return 'Retrieved ${chapters?.length ?? 0} playlists'; } Widget _buildChapterContent(ThemeData theme) { - if (!_needChapterParse) return _buildEmpty('未填写章节规则', theme); + if (!_needChapterParse) return _buildEmpty('No chapter rule provided', theme); if (isTesting) return _buildLoading(theme); - if (!_hasSearchData) return _buildEmpty('请先解析到有效结果', theme); - if (chapters == null) return _buildEmpty('未获取章节数据', theme, isError: true); - if (!_hasChapters) return _buildEmpty('无可用章节', theme, isError: true); + if (!_hasSearchData) return _buildEmpty('Please parse a valid result first', theme); + if (chapters == null) return _buildEmpty('No chapter data retrieved', theme, isError: true); + if (!_hasChapters) return _buildEmpty('No available chapters', theme, isError: true); return Container( padding: const EdgeInsets.all(8.0), @@ -482,12 +482,12 @@ class _PluginTestPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - '播放列表 ${i + 1}:${road.name}', + 'Playlist ${i + 1}: ${road.name}', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w500), ), _h8, - Text('章节数量:${road.data.length}', + Text('Chapter count: ${road.data.length}', style: theme.textTheme.bodySmall?.copyWith( color: theme.getCoreColor(CoreColorType.waiting))), _h8, diff --git a/lib/pages/plugin_editor/plugin_view_page.dart b/lib/pages/plugin_editor/plugin_view_page.dart index 03531d42a..ff986c6bd 100644 --- a/lib/pages/plugin_editor/plugin_view_page.dart +++ b/lib/pages/plugin_editor/plugin_view_page.dart @@ -26,13 +26,13 @@ class _PluginViewPageState extends State { final Set selectedNames = {}; Future _handleUpdate() async { - KazumiDialog.showLoading(msg: '更新中'); + KazumiDialog.showLoading(msg: 'Updating'); int count = await pluginsController.tryUpdateAllPlugin(); KazumiDialog.dismiss(); if (count == 0) { - KazumiDialog.showToast(message: '所有规则已是最新'); + KazumiDialog.showToast(message: 'All rules are up to date'); } else { - KazumiDialog.showToast(message: '更新成功 $count 条'); + KazumiDialog.showToast(message: 'Updated $count rules'); } } @@ -46,7 +46,7 @@ class _PluginViewPageState extends State { mainAxisSize: MainAxisSize.min, // 设置为MainAxisSize.min以减小高度 children: [ ListTile( - title: const Text('新建规则'), + title: const Text('New rule'), onTap: () { KazumiDialog.dismiss(); Modular.to.pushNamed('/settings/plugin/editor', @@ -55,7 +55,7 @@ class _PluginViewPageState extends State { ), const SizedBox(height: 10), ListTile( - title: const Text('从规则仓库导入'), + title: const Text('Import from rule repository'), onTap: () { KazumiDialog.dismiss(); Modular.to.pushNamed('/settings/plugin/shop', @@ -64,7 +64,7 @@ class _PluginViewPageState extends State { ), const SizedBox(height: 10), ListTile( - title: const Text('从剪贴板导入'), + title: const Text('Import from clipboard'), onTap: () { KazumiDialog.dismiss(); _showInputDialog(); @@ -82,7 +82,7 @@ class _PluginViewPageState extends State { KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('导入规则'), + title: const Text('Import rule'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return TextField( @@ -93,7 +93,7 @@ class _PluginViewPageState extends State { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -104,14 +104,14 @@ class _PluginViewPageState extends State { try { pluginsController.updatePlugin(Plugin.fromJson( json.decode(kazumiBase64ToJson(pluginText)))); - KazumiDialog.showToast(message: '导入成功'); + KazumiDialog.showToast(message: 'Import succeeded'); } catch (e) { KazumiDialog.dismiss(); - KazumiDialog.showToast(message: '导入失败 ${e.toString()}'); + KazumiDialog.showToast(message: 'Import failed ${e.toString()}'); } KazumiDialog.dismiss(); }, - child: const Text('导入'), + child: const Text('Import'), ); }) ], @@ -150,8 +150,8 @@ class _PluginViewPageState extends State { child: Scaffold( appBar: SysAppBar( title: isMultiSelectMode - ? Text('已选择 ${selectedNames.length} 项') - : const Text('规则管理'), + ? Text('${selectedNames.length} selected') + : const Text('Rule management'), leading: isMultiSelectMode ? IconButton( icon: const Icon(Icons.close), @@ -171,14 +171,14 @@ class _PluginViewPageState extends State { : () { KazumiDialog.show( builder: (context) => AlertDialog( - title: const Text('删除规则'), + title: const Text('Delete rule'), content: - Text('确定要删除选中的 ${selectedNames.length} 条规则吗?'), + Text('Delete the ${selectedNames.length} selected rules?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle( color: Theme.of(context) .colorScheme @@ -195,7 +195,7 @@ class _PluginViewPageState extends State { }); KazumiDialog.dismiss(); }, - child: const Text('删除'), + child: const Text('Delete'), ), ], ), @@ -208,14 +208,14 @@ class _PluginViewPageState extends State { onPressed: () { _handleUpdate(); }, - tooltip: '更新全部', + tooltip: 'Update all', icon: const Icon(Icons.update), ), IconButton( onPressed: () { _handleAdd(); }, - tooltip: '添加规则', + tooltip: 'Add rule', icon: const Icon(Icons.add), ) ], @@ -224,7 +224,7 @@ class _PluginViewPageState extends State { body: Observer(builder: (context) { return pluginsController.pluginList.isEmpty ? const Center( - child: Text('啊咧(⊙.⊙) 没有可用规则的说'), + child: Text('Oops (⊙.⊙) no rules available'), ) : Builder(builder: (context) { return ReorderableListView.builder( @@ -306,7 +306,7 @@ class _PluginViewPageState extends State { BorderRadius.circular(4), ), child: Text( - '可更新', + 'Update available', style: TextStyle( fontSize: 12, color: Theme.of(context) @@ -330,7 +330,7 @@ class _PluginViewPageState extends State { BorderRadius.circular(4), ), child: Text( - '搜索有效', + 'Search works', style: TextStyle( fontSize: 12, color: Theme.of(context) @@ -402,19 +402,19 @@ class _PluginViewPageState extends State { onPressed: () async { var state = pluginsController.pluginUpdateStatus(plugin); if (state == "nonexistent") { - KazumiDialog.showToast(message: '规则仓库中没有当前规则'); + KazumiDialog.showToast(message: 'This rule is not in the rule repository'); } else if (state == "latest") { - KazumiDialog.showToast(message: '规则已是最新'); + KazumiDialog.showToast(message: 'Rule is up to date'); } else if (state == "updatable") { - KazumiDialog.showLoading(msg: '更新中'); + KazumiDialog.showLoading(msg: 'Updating'); int res = await pluginsController.tryUpdatePlugin(plugin); KazumiDialog.dismiss(); if (res == 0) { - KazumiDialog.showToast(message: '更新成功'); + KazumiDialog.showToast(message: 'Update succeeded'); } else if (res == 1) { - KazumiDialog.showToast(message: 'kazumi版本过低, 此规则不兼容当前版本'); + KazumiDialog.showToast(message: 'Kazumi version is too low, this rule is not compatible with the current version'); } else if (res == 2) { - KazumiDialog.showToast(message: '更新规则失败'); + KazumiDialog.showToast(message: 'Failed to update rule'); } } }, @@ -427,7 +427,7 @@ class _PluginViewPageState extends State { children: [ Icon(Icons.update_rounded), SizedBox(width: 8), - Text('更新'), + Text('Update'), ], ), ), @@ -447,7 +447,7 @@ class _PluginViewPageState extends State { children: [ Icon(Icons.edit), SizedBox(width: 8), - Text('编辑'), + Text('Edit'), ], ), ), @@ -467,7 +467,7 @@ class _PluginViewPageState extends State { children: [ Icon(Icons.bug_report_outlined), SizedBox(width: 8), - Text('测试'), + Text('Test'), ], ), ), @@ -478,7 +478,7 @@ class _PluginViewPageState extends State { onPressed: () { KazumiDialog.show(builder: (context) { return AlertDialog( - title: const Text('规则链接'), + title: const Text('Rule link'), content: SelectableText( jsonToKazumiBase64(json .encode(pluginsController.pluginList[index].toJson())), @@ -489,7 +489,7 @@ class _PluginViewPageState extends State { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), @@ -505,7 +505,7 @@ class _PluginViewPageState extends State { )); KazumiDialog.dismiss(); }, - child: const Text('复制到剪贴板'), + child: const Text('Copy to clipboard'), ), ], ); @@ -520,7 +520,7 @@ class _PluginViewPageState extends State { children: [ Icon(Icons.share), SizedBox(width: 8), - Text('分享'), + Text('Share'), ], ), ), @@ -542,7 +542,7 @@ class _PluginViewPageState extends State { children: [ Icon(Icons.delete), SizedBox(width: 8), - Text('删除'), + Text('Delete'), ], ), ), diff --git a/lib/pages/popular/popular_page.dart b/lib/pages/popular/popular_page.dart index 45b4ad28c..3e1afe963 100644 --- a/lib/pages/popular/popular_page.dart +++ b/lib/pages/popular/popular_page.dart @@ -88,7 +88,7 @@ class _PopularPageState extends State DateTime.now().difference(_lastPressedAt!) > const Duration(seconds: 2)) { _lastPressedAt = DateTime.now(); - KazumiDialog.showToast(message: "再按一次退出应用", context: context); + KazumiDialog.showToast(message: "Press again to exit the app", context: context); return; } SystemNavigator.pop(); @@ -240,7 +240,7 @@ class _PopularPageState extends State mainAxisSize: MainAxisSize.min, children: [ Text( - isTrend ? '热门番组' : popularController.currentTag, + isTrend ? 'Trending anime' : popularController.currentTag, style: theme.textTheme.headlineMedium!.copyWith( fontWeight: fontWeight, fontSize: fontSize, @@ -268,14 +268,14 @@ class _PopularPageState extends State final actions = [ if (MediaQuery.of(context).orientation == Orientation.portrait) IconButton( - tooltip: '搜索', + tooltip: 'Search', onPressed: () => Modular.to.pushNamed('/search/'), icon: const Icon(Icons.search), ), ]; actions.add( IconButton( - tooltip: '历史记录', + tooltip: 'History', onPressed: () => Modular.to.pushNamed('/settings/history/'), icon: const Icon(Icons.history), ), @@ -284,7 +284,7 @@ class _PopularPageState extends State if (!showWindowButton()) { actions.add( IconButton( - tooltip: '退出', + tooltip: 'Exit', onPressed: () => windowManager.close(), icon: const Icon(Icons.close), ), @@ -319,7 +319,7 @@ class _PopularPageState extends State '', ...defaultAnimeTags, ], - itemBuilder: (item) => item.isEmpty ? '热门番组' : item, + itemBuilder: (item) => item.isEmpty ? 'Trending anime' : item, ); }, transitionDuration: const Duration(milliseconds: 200), diff --git a/lib/pages/search/image_search_page.dart b/lib/pages/search/image_search_page.dart index 7daaa36fd..dd0659ce7 100644 --- a/lib/pages/search/image_search_page.dart +++ b/lib/pages/search/image_search_page.dart @@ -71,7 +71,7 @@ class _ImageSearchPageState extends State { if (imageBytes > maxImageBytes) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('图片大小不能超过 25MB')), + const SnackBar(content: Text('Image size cannot exceed 25MB')), ); return; } @@ -89,14 +89,14 @@ class _ImageSearchPageState extends State { final imageUrl = _urlController.text.trim(); final uri = Uri.tryParse(imageUrl); if (imageUrl.isEmpty || uri == null || !uri.hasScheme) { - KazumiDialog.showToast(message: '请输入有效的图片链接'); + KazumiDialog.showToast(message: 'Please enter a valid image URL'); return; } await _searchPageController.searchImageByUrl(imageUrl); } else { final imageFile = _selectedImageFile; if (imageFile == null) { - KazumiDialog.showToast(message: '请先选择图片文件'); + KazumiDialog.showToast(message: 'Please select an image file first'); return; } await _searchPageController.searchImageByFile(imageFile); @@ -125,7 +125,7 @@ class _ImageSearchPageState extends State { title?.romaji ?? title?.english ?? result.filename ?? - '未知番剧'; + 'Unknown anime'; } static String _formatTraceEpisode(dynamic episode) { @@ -134,15 +134,15 @@ class _ImageSearchPageState extends State { } if (episode is num) { - return '第 ${formatEpisodeValue(episode)} 集'; + return 'Episode ${formatEpisodeValue(episode)}'; } if (episode is List && episode.isNotEmpty) { final episodes = episode.whereType().map(formatEpisodeValue); if (episodes.isNotEmpty) { - return '剧集: ${episodes.join(' / ')}'; + return 'Episodes: ${episodes.join(' / ')}'; } } - return '剧集未知'; + return 'Episode unknown'; } int _resolveCrossAxisCount(double width) { @@ -166,7 +166,7 @@ class _ImageSearchPageState extends State { return Scaffold( appBar: SysAppBar( backgroundColor: Colors.transparent, - title: const Text('图片搜索'), + title: const Text('Image search'), ), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), @@ -190,7 +190,7 @@ class _ImageSearchPageState extends State { _isUrlMode ? Icons.upload_file : Icons.link, size: 18, ), - label: Text(_isUrlMode ? '改为上传图片文件' : '改为输入图片 URL'), + label: Text(_isUrlMode ? 'Switch to uploading an image file' : 'Switch to entering an image URL'), ), ), Observer( @@ -213,8 +213,8 @@ class _ImageSearchPageState extends State { : const Icon(Icons.image_search_rounded), label: Text( _searchPageController.isImageSearching - ? '搜索中...' - : '开始搜索', + ? 'Searching...' + : 'Start search', style: const TextStyle(fontSize: 16), ), ), @@ -265,14 +265,14 @@ class _ImageSearchPageState extends State { ), const SizedBox(height: 16), Text( - '点击选择图片', + 'Tap to select an image', style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), Text( - '支持 JPG、PNG、WEBP 格式', + 'Supports JPG, PNG, WEBP formats', style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -287,7 +287,7 @@ class _ImageSearchPageState extends State { fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Center( child: Text( - '图片预览失败', + 'Image preview failed', style: textTheme.bodyMedium?.copyWith( color: colorScheme.error, ), @@ -318,7 +318,7 @@ class _ImageSearchPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - _selectedImageName ?? '已选择图片', + _selectedImageName ?? 'Image selected', maxLines: 1, overflow: TextOverflow.ellipsis, style: textTheme.titleMedium?.copyWith( @@ -328,7 +328,7 @@ class _ImageSearchPageState extends State { ), const SizedBox(height: 4), Text( - '点击可重新选择图片', + 'Tap to reselect an image', style: textTheme.bodySmall?.copyWith( color: Colors.white.withValues(alpha: 0.78), ), @@ -339,7 +339,7 @@ class _ImageSearchPageState extends State { IconButton.filledTonal( onPressed: _pickImageFile, icon: const Icon(Icons.edit_outlined), - tooltip: '重新选择', + tooltip: 'Reselect', ), ], ), @@ -359,7 +359,7 @@ class _ImageSearchPageState extends State { TextField( controller: _urlController, decoration: InputDecoration( - hintText: '请输入图片链接', + hintText: 'Please enter an image URL', hintStyle: TextStyle( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), fontSize: 13, @@ -368,7 +368,7 @@ class _ImageSearchPageState extends State { suffixIcon: IconButton( icon: const Icon(Icons.clear), onPressed: _urlController.clear, - tooltip: '清除', + tooltip: 'Clear', ), filled: true, fillColor: @@ -423,7 +423,7 @@ class _ImageSearchPageState extends State { ), const SizedBox(height: 8), Text( - '输入图片链接后预览', + 'Preview after entering an image URL', style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4), @@ -451,7 +451,7 @@ class _ImageSearchPageState extends State { ), const SizedBox(height: 12), Text( - '加载中...', + 'Loading...', style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -471,14 +471,14 @@ class _ImageSearchPageState extends State { ), const SizedBox(height: 8), Text( - '图片加载失败', + 'Failed to load image', style: textTheme.bodySmall?.copyWith( color: colorScheme.error, ), ), const SizedBox(height: 4), Text( - '请检查链接是否有效', + 'Please check whether the URL is valid', style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -505,8 +505,8 @@ class _ImageSearchPageState extends State { height: 28, child: CircularProgressIndicator(strokeWidth: 2.4), ), - title: '正在识别图片', - description: '请稍候,正在从截图中匹配番剧信息', + title: 'Recognizing image', + description: 'Please wait, matching anime info from the screenshot', ); } @@ -523,9 +523,9 @@ class _ImageSearchPageState extends State { ? colorScheme.primary : colorScheme.error, ), - title: errorMessage.isEmpty ? '搜索结果将在这里展示' : '未获取到搜索结果', + title: errorMessage.isEmpty ? 'Search results will appear here' : 'No search results found', description: - errorMessage.isEmpty ? '选择图片文件或输入图片链接后开始搜索' : errorMessage, + errorMessage.isEmpty ? 'Select an image file or enter an image URL to start searching' : errorMessage, ); } @@ -533,7 +533,7 @@ class _ImageSearchPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '识别结果', + 'Recognition results', style: textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), @@ -675,12 +675,12 @@ class _ImageSearchPageState extends State { _buildInfoLine( textTheme, colorScheme, - '相似度: ${formatTraceSimilarity(result.similarity)}', + 'Similarity: ${formatTraceSimilarity(result.similarity)}', ), _buildInfoLine( textTheme, colorScheme, - '时间: ${durationToString(Duration(seconds: (result.from ?? 0).floor()))} - ${durationToString(Duration(seconds: (result.to ?? 0).floor()))}', + 'Time: ${durationToString(Duration(seconds: (result.from ?? 0).floor()))} - ${durationToString(Duration(seconds: (result.to ?? 0).floor()))}', ), ], ), @@ -722,13 +722,13 @@ class _ImageSearchPageState extends State { final dotColor = colorScheme.onSurfaceVariant.withValues(alpha: 0.6); final tips = [ - Text('仅支持使用原始比例番剧截图搜索结果', style: baseStyle), - Text('截图应清晰,避免过度压缩或添加水印', style: baseStyle), + Text('Only original aspect ratio anime screenshots are supported for search', style: baseStyle), + Text('Screenshots should be clear, avoid heavy compression or watermarks', style: baseStyle), RichText( text: TextSpan( style: baseStyle, children: [ - const TextSpan(text: '搜索引擎由 '), + const TextSpan(text: 'Search powered by '), WidgetSpan( alignment: PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic, @@ -740,7 +740,7 @@ class _ImageSearchPageState extends State { child: Text('trace.moe', style: linkStyle), ), ), - const TextSpan(text: ' 提供支持'), + const TextSpan(text: ''), ], ), ), @@ -758,7 +758,7 @@ class _ImageSearchPageState extends State { ), const SizedBox(width: 6), Text( - '以图搜番', + 'Search anime by image', style: textTheme.labelLarge?.copyWith( color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, diff --git a/lib/pages/search/search_controller.dart b/lib/pages/search/search_controller.dart index 5919324d0..3a1216798 100644 --- a/lib/pages/search/search_controller.dart +++ b/lib/pages/search/search_controller.dart @@ -141,10 +141,10 @@ abstract class _SearchPageController with Store { if (result.error != null && result.error!.isNotEmpty) { imageSearchError = result.error!; } else if (imageSearchResults.isEmpty) { - imageSearchError = '未找到匹配结果'; + imageSearchError = 'No matching results found'; } } catch (e) { - imageSearchError = '图片搜索失败,请稍后重试'; + imageSearchError = 'Image search failed, please try again later'; } finally { isImageSearching = false; } @@ -161,10 +161,10 @@ abstract class _SearchPageController with Store { if (result.error != null && result.error!.isNotEmpty) { imageSearchError = result.error!; } else if (imageSearchResults.isEmpty) { - imageSearchError = '未找到匹配结果'; + imageSearchError = 'No matching results found'; } } catch (e) { - imageSearchError = '图片搜索失败,请检查图片地址或稍后重试'; + imageSearchError = 'Image search failed, please check the image URL or try again later'; } finally { isImageSearching = false; } diff --git a/lib/pages/search/search_page.dart b/lib/pages/search/search_page.dart index 40450901e..38d717e9b 100644 --- a/lib/pages/search/search_page.dart +++ b/lib/pages/search/search_page.dart @@ -27,8 +27,8 @@ class _SearchPageState extends State { final ScrollController scrollController = ScrollController(); final List tabs = [ - Tab(text: "排序方式"), - Tab(text: "过滤器"), + Tab(text: "Sort by"), + Tab(text: "Filters"), ]; @override @@ -75,7 +75,7 @@ class _SearchPageState extends State { !searchPageController.notShowWatchedBangumis); }, child: ListTile( - title: const Text('不显示已看过的番剧'), + title: const Text('Hide watched anime'), trailing: Switch( value: searchPageController.notShowWatchedBangumis, onChanged: (value) { @@ -92,7 +92,7 @@ class _SearchPageState extends State { !searchPageController.notShowAbandonedBangumis); }, child: ListTile( - title: const Text('不显示已抛弃的番剧'), + title: const Text('Hide dropped anime'), trailing: Switch( value: searchPageController.notShowAbandonedBangumis, onChanged: (value) { @@ -113,7 +113,7 @@ class _SearchPageState extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: const Text('按热度排序'), + title: const Text('Sort by popularity'), onTap: () { Navigator.pop(context); searchController.text = searchPageController.attachSortParams( @@ -123,7 +123,7 @@ class _SearchPageState extends State { }, ), ListTile( - title: const Text('按评分排序'), + title: const Text('Sort by rating'), onTap: () { Navigator.pop(context); searchController.text = searchPageController.attachSortParams( @@ -133,7 +133,7 @@ class _SearchPageState extends State { }, ), ListTile( - title: const Text('按匹配程度排序'), + title: const Text('Sort by relevance'), onTap: () { Navigator.pop(context); searchController.text = searchPageController.attachSortParams( @@ -175,7 +175,7 @@ class _SearchPageState extends State { return Scaffold( appBar: SysAppBar( backgroundColor: Colors.transparent, - title: const Text("搜索"), + title: const Text("Search"), ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { @@ -201,7 +201,7 @@ class _SearchPageState extends State { ); }, icon: const Icon(Icons.sort), - label: const Text("搜索设置"), + label: const Text("Search settings"), ), body: Column( children: [ @@ -244,7 +244,7 @@ class _SearchPageState extends State { return Container( height: 400, alignment: Alignment.center, - child: Text("无可用搜索建议,回车以直接检索"), + child: Text("No search suggestions, press Enter to search directly"), ); } else { return Column( @@ -294,7 +294,7 @@ class _SearchPageState extends State { child: SizedBox( height: 400, child: GeneralErrorWidget( - errMsg: '什么都没有找到 (´;ω;`)', + errMsg: 'Nothing found (´;ω;`)', actions: [ GeneralErrorButton( onPressed: () { @@ -302,7 +302,7 @@ class _SearchPageState extends State { searchController.text, type: 'init'); }, - text: '点击重试', + text: 'Tap to retry', ), ], ), diff --git a/lib/pages/settings/danmaku/danmaku_settings.dart b/lib/pages/settings/danmaku/danmaku_settings.dart index 92b519595..2ff820f54 100644 --- a/lib/pages/settings/danmaku/danmaku_settings.dart +++ b/lib/pages/settings/danmaku/danmaku_settings.dart @@ -141,12 +141,12 @@ class _DanmakuSettingsPageState extends State { onBackPressed(context); }, child: Scaffold( - appBar: const SysAppBar(title: Text('弹幕设置')), + appBar: const SysAppBar(title: Text('Danmaku settings')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('弹幕来源', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku source', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { @@ -184,22 +184,22 @@ class _DanmakuSettingsPageState extends State { ], ), SettingsSection( - title: Text('弹幕屏蔽', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku blocking', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { Modular.to.pushNamed('/settings/danmaku/shield'); }, title: - Text('关键词屏蔽', style: TextStyle(fontFamily: fontFamily)), + Text('Keyword blocking', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( - title: Text('弹幕显示', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku display', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( - title: Text('弹幕区域', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku area', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuArea, min: 0, @@ -213,7 +213,7 @@ class _DanmakuSettingsPageState extends State { ), SettingsTile( title: - Text('弹幕持续时间', style: TextStyle(fontFamily: fontFamily)), + Text('Danmaku duration', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuDuration, min: 2, @@ -226,7 +226,7 @@ class _DanmakuSettingsPageState extends State { ), ), SettingsTile( - title: Text('弹幕行高', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku line height', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuLineHeight, min: 0, @@ -246,9 +246,9 @@ class _DanmakuSettingsPageState extends State { SettingBoxKey.danmakuFollowSpeed, danmakuFollowSpeed); setState(() {}); }, - title: Text('弹幕跟随视频倍速', + title: Text('Danmaku follows playback speed', style: TextStyle(fontFamily: fontFamily)), - description: Text('开启后弹幕速度会随视频倍速而改变', + description: Text('When enabled, danmaku speed changes with playback speed', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuFollowSpeed, ), @@ -258,7 +258,7 @@ class _DanmakuSettingsPageState extends State { await setting.put(SettingBoxKey.danmakuTop, danmakuTop); setState(() {}); }, - title: Text('顶部弹幕', style: TextStyle(fontFamily: fontFamily)), + title: Text('Top danmaku', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuTop, ), SettingsTile.switchTile( @@ -268,7 +268,7 @@ class _DanmakuSettingsPageState extends State { SettingBoxKey.danmakuBottom, danmakuBottom); setState(() {}); }, - title: Text('底部弹幕', style: TextStyle(fontFamily: fontFamily)), + title: Text('Bottom danmaku', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuBottom, ), SettingsTile.switchTile( @@ -278,7 +278,7 @@ class _DanmakuSettingsPageState extends State { SettingBoxKey.danmakuScroll, danmakuScroll); setState(() {}); }, - title: Text('滚动弹幕', style: TextStyle(fontFamily: fontFamily)), + title: Text('Scrolling danmaku', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuScroll, ), SettingsTile.switchTile( @@ -288,8 +288,8 @@ class _DanmakuSettingsPageState extends State { SettingBoxKey.danmakuMassive, danmakuMassive); setState(() {}); }, - title: Text('海量弹幕', style: TextStyle(fontFamily: fontFamily)), - description: Text('弹幕过多时进行叠加绘制', + title: Text('Massive danmaku', style: TextStyle(fontFamily: fontFamily)), + description: Text('Overlap danmaku when there are too many', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuMassive, ), @@ -300,15 +300,15 @@ class _DanmakuSettingsPageState extends State { danmakuDeduplication); setState(() {}); }, - title: Text('弹幕去重', style: TextStyle(fontFamily: fontFamily)), - description: Text('相同内容弹幕过多时合并为一条弹幕', + title: Text('Danmaku deduplication', style: TextStyle(fontFamily: fontFamily)), + description: Text('Merge identical danmaku into one when there are too many', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuDeduplication, ), ], ), SettingsSection( - title: Text('弹幕样式', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku style', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { @@ -317,12 +317,12 @@ class _DanmakuSettingsPageState extends State { SettingBoxKey.danmakuBorder, danmakuBorder); setState(() {}); }, - title: Text('弹幕描边', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku outline', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuBorder, ), SettingsTile( title: - Text('弹幕描边粗细', style: TextStyle(fontFamily: fontFamily)), + Text('Danmaku outline thickness', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultdanmakuBorderSize, min: 0.1, @@ -341,11 +341,11 @@ class _DanmakuSettingsPageState extends State { await setting.put(SettingBoxKey.danmakuColor, danmakuColor); setState(() {}); }, - title: Text('弹幕颜色', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku color', style: TextStyle(fontFamily: fontFamily)), initialValue: danmakuColor, ), SettingsTile( - title: Text('字体大小', style: TextStyle(fontFamily: fontFamily)), + title: Text('Font size', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuFontSize, min: 10, @@ -357,7 +357,7 @@ class _DanmakuSettingsPageState extends State { ), ), SettingsTile( - title: Text('字体字重', style: TextStyle(fontFamily: fontFamily)), + title: Text('Font weight', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuFontWeight.toDouble(), min: 1, @@ -371,7 +371,7 @@ class _DanmakuSettingsPageState extends State { ), SettingsTile( title: - Text('弹幕不透明度', style: TextStyle(fontFamily: fontFamily)), + Text('Danmaku opacity', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultDanmakuOpacity, min: 0.1, diff --git a/lib/pages/settings/danmaku/danmaku_settings_sheet.dart b/lib/pages/settings/danmaku/danmaku_settings_sheet.dart index 3f6e32a83..b7c9ab3fc 100644 --- a/lib/pages/settings/danmaku/danmaku_settings_sheet.dart +++ b/lib/pages/settings/danmaku/danmaku_settings_sheet.dart @@ -37,10 +37,10 @@ class _DanmakuSettingsSheetState extends State { String _formatDanmakuTimeOffset(double value) { if (value == 0) { - return '无偏移'; + return 'No offset'; } - final direction = value > 0 ? '延后' : '提前'; - return '$direction ${value.abs().toStringAsFixed(1)} 秒'; + final direction = value > 0 ? 'Delay' : 'Advance'; + return '$direction ${value.abs().toStringAsFixed(1)} s'; } @override @@ -75,21 +75,21 @@ class _DanmakuSettingsSheetState extends State { child: SettingsList( sections: [ SettingsSection( - title: Text('弹幕屏蔽', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku blocking', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { showDanmakuShieldSheet(); }, - title: Text('关键词屏蔽', style: TextStyle(fontFamily: fontFamily)), + title: Text('Keyword blocking', style: TextStyle(fontFamily: fontFamily)), ), ], ), SettingsSection( - title: Text('弹幕样式', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku style', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( - title: Text('字体大小', style: TextStyle(fontFamily: fontFamily)), + title: Text('Font size', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.fontSize, min: 10, @@ -108,7 +108,7 @@ class _DanmakuSettingsSheetState extends State { ), ), SettingsTile( - title: Text('弹幕不透明度', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku opacity', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.opacity, min: 0.1, @@ -129,10 +129,10 @@ class _DanmakuSettingsSheetState extends State { ], ), SettingsSection( - title: Text('弹幕显示', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku display', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( - title: Text('时间轴偏移', style: TextStyle(fontFamily: fontFamily)), + title: Text('Timeline offset', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: _danmakuTimeOffset, min: -60, @@ -150,7 +150,7 @@ class _DanmakuSettingsSheetState extends State { ), ), SettingsTile( - title: Text('弹幕区域', style: TextStyle(fontFamily: fontFamily)), + title: Text('Danmaku area', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.area, min: 0, @@ -169,7 +169,7 @@ class _DanmakuSettingsSheetState extends State { ), ), SettingsTile( - title: Text('持续时间', style: TextStyle(fontFamily: fontFamily)), + title: Text('Duration', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.duration.toDouble(), min: 2, @@ -188,7 +188,7 @@ class _DanmakuSettingsSheetState extends State { ), ), SettingsTile( - title: Text('行高', style: TextStyle(fontFamily: fontFamily)), + title: Text('Line height', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: widget.danmakuController.option.lineHeight, min: 0, @@ -217,7 +217,7 @@ class _DanmakuSettingsSheetState extends State { )); setting.put(SettingBoxKey.danmakuTop, show); }, - title: Text('顶部弹幕', style: TextStyle(fontFamily: fontFamily)), + title: Text('Top danmaku', style: TextStyle(fontFamily: fontFamily)), initialValue: !widget.danmakuController.option.hideTop, ), SettingsTile.switchTile( @@ -231,7 +231,7 @@ class _DanmakuSettingsSheetState extends State { )); setting.put(SettingBoxKey.danmakuBottom, show); }, - title: Text('底部弹幕', style: TextStyle(fontFamily: fontFamily)), + title: Text('Bottom danmaku', style: TextStyle(fontFamily: fontFamily)), initialValue: !widget.danmakuController.option.hideBottom, ), SettingsTile.switchTile( @@ -245,7 +245,7 @@ class _DanmakuSettingsSheetState extends State { )); setting.put(SettingBoxKey.danmakuScroll, show); }, - title: Text('滚动弹幕', style: TextStyle(fontFamily: fontFamily)), + title: Text('Scrolling danmaku', style: TextStyle(fontFamily: fontFamily)), initialValue: !widget.danmakuController.option.hideScroll, ), SettingsTile.switchTile( @@ -257,8 +257,8 @@ class _DanmakuSettingsSheetState extends State { widget.onUpdateDanmakuSpeed?.call(); setState(() {}); }, - title: Text('跟随视频倍速', style: TextStyle(fontFamily: fontFamily)), - description: Text('弹幕速度随视频倍速变化', + title: Text('Follow playback speed', style: TextStyle(fontFamily: fontFamily)), + description: Text('Danmaku speed changes with playback speed', style: TextStyle(fontFamily: fontFamily)), initialValue: setting.get(SettingBoxKey.danmakuFollowSpeed, defaultValue: true), diff --git a/lib/pages/settings/danmaku/danmaku_shield_settings.dart b/lib/pages/settings/danmaku/danmaku_shield_settings.dart index 4bf3acbbf..0904dc926 100644 --- a/lib/pages/settings/danmaku/danmaku_shield_settings.dart +++ b/lib/pages/settings/danmaku/danmaku_shield_settings.dart @@ -25,7 +25,7 @@ class _DanmakuShieldSettingsState extends State { Widget build(BuildContext context) { return Scaffold( appBar: SysAppBar( - title: const Text("弹幕屏蔽"), + title: const Text("Danmaku blocking"), ), body: ListView( padding: EdgeInsets.all(12), @@ -34,7 +34,7 @@ class _DanmakuShieldSettingsState extends State { controller: textEditingController, decoration: InputDecoration( border: const OutlineInputBorder(), - hintText: "输入关键词或正则表达式", + hintText: "Enter a keyword or regular expression", suffixIcon: TextButton.icon( onPressed: () { myController.addShieldList( @@ -42,7 +42,7 @@ class _DanmakuShieldSettingsState extends State { ); }, icon: const Icon(Icons.add), - label: const Text("添加"), + label: const Text("Add"), ), ), onSubmitted: (_) { @@ -53,11 +53,11 @@ class _DanmakuShieldSettingsState extends State { ), SizedBox(height: 12), Text( - '以"/"开头和结尾将视作正则表达式, 如"/\\d+/"表示屏蔽所有数字', + 'Wrapping with "/" treats it as a regular expression, e.g. "/\\d+/" blocks all digits', ), Observer(builder: (context) { return Text( - "已添加${myController.shieldList.length}个关键词", + "Added ${myController.shieldList.length} keywords", ); }), SizedBox(height: 12), diff --git a/lib/pages/settings/danmaku/danmaku_shield_settings_sheet.dart b/lib/pages/settings/danmaku/danmaku_shield_settings_sheet.dart index 1a4d525b1..a3ad01ca1 100644 --- a/lib/pages/settings/danmaku/danmaku_shield_settings_sheet.dart +++ b/lib/pages/settings/danmaku/danmaku_shield_settings_sheet.dart @@ -26,7 +26,7 @@ class _DanmakuShieldSettingsSheetState Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("弹幕屏蔽"), + title: const Text("Danmaku blocking"), ), body: ListView( padding: EdgeInsets.all(12), @@ -35,7 +35,7 @@ class _DanmakuShieldSettingsSheetState controller: textEditingController, decoration: InputDecoration( border: const OutlineInputBorder(), - hintText: "输入关键词或正则表达式", + hintText: "Enter a keyword or regular expression", suffixIcon: TextButton.icon( onPressed: () { myController.addShieldList( @@ -43,7 +43,7 @@ class _DanmakuShieldSettingsSheetState ); }, icon: const Icon(Icons.add), - label: const Text("添加"), + label: const Text("Add"), ), ), onSubmitted: (_) { @@ -54,11 +54,11 @@ class _DanmakuShieldSettingsSheetState ), SizedBox(height: 12), Text( - '以"/"开头和结尾将视作正则表达式, 如"/\\d+/"表示屏蔽所有数字', + 'Wrapping with "/" treats it as a regular expression, e.g. "/\\d+/" blocks all digits', ), Observer(builder: (context) { return Text( - "已添加${myController.shieldList.length}个关键词", + "Added ${myController.shieldList.length} keywords", ); }), SizedBox(height: 12), diff --git a/lib/pages/settings/decoder_settings.dart b/lib/pages/settings/decoder_settings.dart index eb0cd64b9..4d480e2ac 100644 --- a/lib/pages/settings/decoder_settings.dart +++ b/lib/pages/settings/decoder_settings.dart @@ -29,13 +29,13 @@ class _DecoderSettingsState extends State { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar( - title: Text('硬件解码器'), + title: Text('Hardware decoder'), ), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('选择不受支持的解码器将回退到软件解码', + title: Text('Selecting an unsupported decoder falls back to software decoding', style: TextStyle(fontFamily: fontFamily)), tiles: hardwareDecodersList.entries .map((e) => SettingsTile.radioTile( diff --git a/lib/pages/settings/displaymode_settings.dart b/lib/pages/settings/displaymode_settings.dart index e80460713..5cca518c6 100644 --- a/lib/pages/settings/displaymode_settings.dart +++ b/lib/pages/settings/displaymode_settings.dart @@ -71,14 +71,14 @@ class _SetDisplayModeState extends State { Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( - appBar: AppBar(title: const Text('屏幕帧率设置')), + appBar: AppBar(title: const Text('Screen refresh rate settings')), body: (modes.isEmpty) ? const CircularProgressIndicator() : SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('没有生效? 重启app试试', + title: Text('Not working? Try restarting the app', style: TextStyle(fontFamily: fontFamily)), tiles: modes .map((e) => SettingsTile.radioTile( @@ -93,9 +93,9 @@ class _SetDisplayModeState extends State { await fetchAll(); }, title: e == DisplayMode.auto - ? Text('自动', + ? Text('Auto', style: TextStyle(fontFamily: fontFamily)) - : Text('$e${e == active ? " [系统]" : ""}', + : Text('$e${e == active ? " [System]" : ""}', style: TextStyle(fontFamily: fontFamily)), )) .toList(), diff --git a/lib/pages/settings/download_settings.dart b/lib/pages/settings/download_settings.dart index 560d7770e..ee25a76f8 100644 --- a/lib/pages/settings/download_settings.dart +++ b/lib/pages/settings/download_settings.dart @@ -38,20 +38,20 @@ class _DownloadSettingsPageState extends State { Widget build(BuildContext context) { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( - appBar: const SysAppBar(title: Text('下载设置')), + appBar: const SysAppBar(title: Text('Download settings')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('并发设置', style: TextStyle(fontFamily: fontFamily)), + title: Text('Concurrency settings', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( - title: Text('同时下载集数', style: TextStyle(fontFamily: fontFamily)), + title: Text('Simultaneous episode downloads', style: TextStyle(fontFamily: fontFamily)), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '同时下载 $parallelEpisodes 集', + 'Download $parallelEpisodes episodes at once', style: TextStyle(fontFamily: fontFamily), ), Slider( @@ -72,12 +72,12 @@ class _DownloadSettingsPageState extends State { ), ), SettingsTile( - title: Text('分片并发数', style: TextStyle(fontFamily: fontFamily)), + title: Text('Segment concurrency', style: TextStyle(fontFamily: fontFamily)), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '每集同时下载 $parallelSegments 个分片', + 'Download $parallelSegments segments per episode at once', style: TextStyle(fontFamily: fontFamily), ), Slider( @@ -100,16 +100,16 @@ class _DownloadSettingsPageState extends State { ], ), SettingsSection( - title: Text('缓存设置', style: TextStyle(fontFamily: fontFamily)), + title: Text('Cache settings', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) { setState(() => downloadDanmaku = value ?? !downloadDanmaku); setting.put(SettingBoxKey.downloadDanmaku, downloadDanmaku); }, - title: Text('缓存弹幕', style: TextStyle(fontFamily: fontFamily)), + title: Text('Cache danmaku', style: TextStyle(fontFamily: fontFamily)), description: Text( - '下载视频时同时缓存弹幕数据', + 'Cache danmaku data when downloading videos', style: TextStyle(fontFamily: fontFamily), ), initialValue: downloadDanmaku, @@ -117,15 +117,15 @@ class _DownloadSettingsPageState extends State { ], ), SettingsSection( - title: Text('说明', style: TextStyle(fontFamily: fontFamily)), + title: Text('Notes', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile( - title: Text('关于并发设置', style: TextStyle(fontFamily: fontFamily)), + title: Text('About concurrency settings', style: TextStyle(fontFamily: fontFamily)), description: Text( - '• 集数并发:同时下载多少集视频\n' - '• 分片并发:每集内同时下载多少个视频片段\n' - '• 较高的并发可提升速度,但可能被服务器限制\n' - '• 修改后对新开始的下载生效', + '• Episode concurrency: how many episodes to download at once\n' + '• Segment concurrency: how many video segments to download per episode at once\n' + '• Higher concurrency can improve speed but may be throttled by the server\n' + '• Changes apply to newly started downloads', style: TextStyle(fontFamily: fontFamily), ), ), diff --git a/lib/pages/settings/interface_settings.dart b/lib/pages/settings/interface_settings.dart index 69d14e03c..d1ae05756 100644 --- a/lib/pages/settings/interface_settings.dart +++ b/lib/pages/settings/interface_settings.dart @@ -20,10 +20,10 @@ class _InterfaceSettingsPageState extends State { final MenuController defaultPageMenuController = MenuController(); static const Map defaultPageMap = { - '/tab/popular/': '推荐', - '/tab/timeline/': '时间表', - '/tab/collect/': '追番', - '/tab/my/': '我的', + '/tab/popular/': 'Recommended', + '/tab/timeline/': 'Schedule', + '/tab/collect/': 'Tracking', + '/tab/my/': 'Me', }; @override @@ -47,7 +47,7 @@ class _InterfaceSettingsPageState extends State { return Scaffold( appBar: SysAppBar( - title: Text('界面设置'), + title: Text('Interface settings'), ), body: SettingsList( sections: [ @@ -60,15 +60,15 @@ class _InterfaceSettingsPageState extends State { defaultPageMenuController.open(); } }, - title: Text('启动界面设置', style: TextStyle(fontFamily: fontFamily)), - description: Text('设置应用开启时的默认页面', + title: Text('Startup screen settings', style: TextStyle(fontFamily: fontFamily)), + description: Text('Set the default page when the app opens', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: defaultPageMenuController, builder: (_, __, ___) { return Text( - defaultPageMap[defaultPage] ?? '推荐', + defaultPageMap[defaultPage] ?? 'Recommended', style: TextStyle(fontFamily: fontFamily), ); }, @@ -105,8 +105,8 @@ class _InterfaceSettingsPageState extends State { await setting.put(SettingBoxKey.showRating, showRating); setState(() {}); }, - title: Text('显示评分', style: TextStyle(fontFamily: fontFamily)), - description: Text('关闭后将在概览中隐藏评分信息', + title: Text('Show ratings', style: TextStyle(fontFamily: fontFamily)), + description: Text('When off, rating info is hidden in the overview', style: TextStyle(fontFamily: fontFamily)), initialValue: showRating, ), diff --git a/lib/pages/settings/keyboard_settings.dart b/lib/pages/settings/keyboard_settings.dart index 19a112864..cab03786f 100644 --- a/lib/pages/settings/keyboard_settings.dart +++ b/lib/pages/settings/keyboard_settings.dart @@ -64,7 +64,7 @@ class _KeyboardSettingsPageState extends State { if (otherFunc == func && i == index) continue; if (otherKeys[i] == rawKey) { final name = shortcutsChineseName[otherFunc] ?? otherFunc; - KazumiDialog.showToast(message: "按键已被【$name】占用,请重新输入"); + KazumiDialog.showToast(message: "This key is already used by [$name], please enter another"); return true; } } @@ -96,11 +96,11 @@ class _KeyboardSettingsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: SysAppBar( - title: Text('快捷键'), + title: Text('Shortcuts'), actions: [ IconButton( icon: const Icon(Icons.refresh), - tooltip: '恢复默认', + tooltip: 'Restore defaults', onPressed: () { setState(() { for (final func in shortcuts.keys) { diff --git a/lib/pages/settings/player_settings.dart b/lib/pages/settings/player_settings.dart index 5089543df..bcc5554ef 100644 --- a/lib/pages/settings/player_settings.dart +++ b/lib/pages/settings/player_settings.dart @@ -119,8 +119,8 @@ class _PlayerSettingsPageState extends State { Future updateButtonSkipTime() async { final int? newButtonSkipTime = await _showSkipTimeChangeDialog( - title: '顶部按钮快进时长', initialValue: playerButtonSkipTime.toString()); - print('新设置的顶部按钮快进时长: $newButtonSkipTime'); + title: 'Top button skip duration', initialValue: playerButtonSkipTime.toString()); + print('New top button skip duration: $newButtonSkipTime'); if (newButtonSkipTime != null && newButtonSkipTime != playerButtonSkipTime) { @@ -157,7 +157,7 @@ class _PlayerSettingsPageState extends State { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -166,18 +166,18 @@ class _PlayerSettingsPageState extends State { final int? newValue = int.tryParse(input); if (newValue == null) { - KazumiDialog.showToast(message: '请输入数字'); + KazumiDialog.showToast(message: 'Please enter a number'); return; } if (newValue <= 0) { - KazumiDialog.showToast(message: '请输入大于0的数字'); + KazumiDialog.showToast(message: 'Please enter a number greater than 0'); return; } // 以新设置的值弹出 KazumiDialog.dismiss(popWith: newValue); }, - child: const Text('确定'), + child: const Text('OK'), ), ], ); @@ -193,7 +193,7 @@ class _PlayerSettingsPageState extends State { onBackPressed(context); }, child: Scaffold( - appBar: const SysAppBar(title: Text('播放设置')), + appBar: const SysAppBar(title: Text('Playback settings')), body: SettingsList( maxWidth: 1000, sections: [ @@ -205,7 +205,7 @@ class _PlayerSettingsPageState extends State { await setting.put(SettingBoxKey.hAenable, hAenable); setState(() {}); }, - title: Text('硬件解码', style: TextStyle(fontFamily: fontFamily)), + title: Text('Hardware decoding', style: TextStyle(fontFamily: fontFamily)), initialValue: hAenable, ), SettingsTile.navigation( @@ -213,8 +213,8 @@ class _PlayerSettingsPageState extends State { await Modular.to.pushNamed('/settings/player/decoder'); }, title: - Text('硬件解码器', style: TextStyle(fontFamily: fontFamily)), - description: Text('仅在硬件解码启用时生效', + Text('Hardware decoder', style: TextStyle(fontFamily: fontFamily)), + description: Text('Only effective when hardware decoding is enabled', style: TextStyle(fontFamily: fontFamily)), ), if (Platform.isAndroid) ...[ @@ -223,8 +223,8 @@ class _PlayerSettingsPageState extends State { await Modular.to.pushNamed('/settings/player/renderer'); }, title: - Text('视频渲染器', style: TextStyle(fontFamily: fontFamily)), - description: Text('选择视频输出方式', + Text('Video renderer', style: TextStyle(fontFamily: fontFamily)), + description: Text('Choose the video output method', style: TextStyle(fontFamily: fontFamily)), ), ], @@ -236,8 +236,8 @@ class _PlayerSettingsPageState extends State { setState(() {}); }, title: - Text('低内存模式', style: TextStyle(fontFamily: fontFamily)), - description: Text('禁用高级缓存以减少内存占用', + Text('Low memory mode', style: TextStyle(fontFamily: fontFamily)), + description: Text('Disable advanced caching to reduce memory usage', style: TextStyle(fontFamily: fontFamily)), initialValue: lowMemoryMode, ), @@ -250,8 +250,8 @@ class _PlayerSettingsPageState extends State { setState(() {}); }, title: - Text('低延迟音频', style: TextStyle(fontFamily: fontFamily)), - description: Text('启用OpenSLES音频输出以降低延时', + Text('Low latency audio', style: TextStyle(fontFamily: fontFamily)), + description: Text('Enable OpenSLES audio output to reduce latency', style: TextStyle(fontFamily: fontFamily)), initialValue: androidEnableOpenSLES, ), @@ -260,7 +260,7 @@ class _PlayerSettingsPageState extends State { onPressed: (_) async { Modular.to.pushNamed('/settings/player/super'); }, - title: Text('超分辨率', style: TextStyle(fontFamily: fontFamily)), + title: Text('Super resolution', style: TextStyle(fontFamily: fontFamily)), ), ], ), @@ -273,8 +273,8 @@ class _PlayerSettingsPageState extends State { SettingBoxKey.backgroundPlayback, backgroundPlayback); setState(() {}); }, - title: Text('后台播放', style: TextStyle(fontFamily: fontFamily)), - description: Text('应用退到后台或熄屏时继续播放音频', + title: Text('Background playback', style: TextStyle(fontFamily: fontFamily)), + description: Text('Keep playing audio when the app is in the background or the screen is off', style: TextStyle(fontFamily: fontFamily)), initialValue: backgroundPlayback, ), @@ -284,8 +284,8 @@ class _PlayerSettingsPageState extends State { await setting.put(SettingBoxKey.playResume, playResume); setState(() {}); }, - title: Text('自动跳转', style: TextStyle(fontFamily: fontFamily)), - description: Text('跳转到上次播放位置', + title: Text('Auto seek', style: TextStyle(fontFamily: fontFamily)), + description: Text('Seek to the last playback position', style: TextStyle(fontFamily: fontFamily)), initialValue: playResume, ), @@ -295,8 +295,8 @@ class _PlayerSettingsPageState extends State { await setting.put(SettingBoxKey.autoPlayNext, autoPlayNext); setState(() {}); }, - title: Text('自动连播', style: TextStyle(fontFamily: fontFamily)), - description: Text('当前视频播放完毕后自动播放下一集', + title: Text('Auto play next', style: TextStyle(fontFamily: fontFamily)), + description: Text('Automatically play the next episode when the current video finishes', style: TextStyle(fontFamily: fontFamily)), initialValue: autoPlayNext, ), @@ -310,9 +310,9 @@ class _PlayerSettingsPageState extends State { androidAutoEnterPIP); setState(() {}); }, - title: Text('自动进入画中画', + title: Text('Auto enter picture-in-picture', style: TextStyle(fontFamily: fontFamily)), - description: Text('切到后台时,自动进入画中画', + description: Text('Automatically enter picture-in-picture when going to the background', style: TextStyle(fontFamily: fontFamily)), initialValue: androidAutoEnterPIP, ), @@ -323,8 +323,8 @@ class _PlayerSettingsPageState extends State { SettingBoxKey.forceAdBlocker, forceAdBlocker); setState(() {}); }, - title: Text('广告过滤', style: TextStyle(fontFamily: fontFamily)), - description: Text('强制启用HLS广告过滤,忽略规则设置', + title: Text('Ad filtering', style: TextStyle(fontFamily: fontFamily)), + description: Text('Force-enable HLS ad filtering, ignoring rule settings', style: TextStyle(fontFamily: fontFamily)), initialValue: forceAdBlocker, ), @@ -335,8 +335,8 @@ class _PlayerSettingsPageState extends State { playerDisableAnimations); setState(() {}); }, - title: Text('禁用动画', style: TextStyle(fontFamily: fontFamily)), - description: Text('禁用播放器内的过渡动画', + title: Text('Disable animations', style: TextStyle(fontFamily: fontFamily)), + description: Text('Disable transition animations in the player', style: TextStyle(fontFamily: fontFamily)), initialValue: playerDisableAnimations, ), @@ -350,8 +350,8 @@ class _PlayerSettingsPageState extends State { setState(() {}); }, title: - Text('滑动手势', style: TextStyle(fontFamily: fontFamily)), - description: Text('竖向滑动调节音量和亮度', + Text('Swipe gestures', style: TextStyle(fontFamily: fontFamily)), + description: Text('Swipe vertically to adjust volume and brightness', style: TextStyle(fontFamily: fontFamily)), initialValue: brightnessVolumeGesture, ), @@ -361,9 +361,9 @@ class _PlayerSettingsPageState extends State { await setting.put(SettingBoxKey.privateMode, privateMode); setState(() {}); }, - title: Text('隐身模式', style: TextStyle(fontFamily: fontFamily)), + title: Text('Incognito mode', style: TextStyle(fontFamily: fontFamily)), description: - Text('不保留观看记录', style: TextStyle(fontFamily: fontFamily)), + Text('Do not keep watch history', style: TextStyle(fontFamily: fontFamily)), initialValue: privateMode, ), ], @@ -377,8 +377,8 @@ class _PlayerSettingsPageState extends State { SettingBoxKey.showPlayerError, showPlayerError); setState(() {}); }, - title: Text('错误提示', style: TextStyle(fontFamily: fontFamily)), - description: Text('显示播放器内部错误提示', + title: Text('Error notices', style: TextStyle(fontFamily: fontFamily)), + description: Text('Show player internal error notices', style: TextStyle(fontFamily: fontFamily)), initialValue: showPlayerError, ), @@ -389,8 +389,8 @@ class _PlayerSettingsPageState extends State { SettingBoxKey.playerDebugMode, playerDebugMode); setState(() {}); }, - title: Text('调试模式', style: TextStyle(fontFamily: fontFamily)), - description: Text('记录播放器内部日志', + title: Text('Debug mode', style: TextStyle(fontFamily: fontFamily)), + description: Text('Record player internal logs', style: TextStyle(fontFamily: fontFamily)), initialValue: playerDebugMode, ), @@ -402,8 +402,8 @@ class _PlayerSettingsPageState extends State { playerLogLevelMenuController.open(); } }, - title: Text('日志等级', style: TextStyle(fontFamily: fontFamily)), - description: Text('播放器内部日志等级', + title: Text('Log level', style: TextStyle(fontFamily: fontFamily)), + description: Text('Player internal log level', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, @@ -442,7 +442,7 @@ class _PlayerSettingsPageState extends State { SettingsSection( tiles: [ SettingsTile( - title: Text('默认倍速', style: TextStyle(fontFamily: fontFamily)), + title: Text('Default speed', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultPlaySpeed, min: 0.25, @@ -457,7 +457,7 @@ class _PlayerSettingsPageState extends State { ), SettingsTile( title: - Text('默认方向键倍速', style: TextStyle(fontFamily: fontFamily)), + Text('Default arrow key speed', style: TextStyle(fontFamily: fontFamily)), description: Slider( value: defaultShortcutForwardPlaySpeed, min: 1.25, @@ -476,10 +476,10 @@ class _PlayerSettingsPageState extends State { min: 0, max: 15, divisions: 15, - label: '$playerArrowKeySkipTime秒', + label: '$playerArrowKeySkipTime s', onChanged: (value) { final newArrowKeySkipTime = value.toInt(); - print('新设置的方向键快进/快退时长: $newArrowKeySkipTime'); + print('New arrow key skip duration: $newArrowKeySkipTime'); if (value != playerArrowKeySkipTime) { setting.put(SettingBoxKey.arrowKeySkipTime, @@ -490,17 +490,17 @@ class _PlayerSettingsPageState extends State { } }, ), - title: Text('左右方向键的快进/快退秒数', + title: Text('Skip seconds for the left and right arrow keys', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( onPressed: (_) async { await updateButtonSkipTime(); }, - title: Text('跳过时长', style: TextStyle(fontFamily: fontFamily)), - description: Text('顶栏跳过按钮的秒数', + title: Text('Skip duration', style: TextStyle(fontFamily: fontFamily)), + description: Text('Seconds for the top bar skip button', style: TextStyle(fontFamily: fontFamily)), - value: Text('$playerButtonSkipTime 秒', + value: Text('$playerButtonSkipTime s', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.navigation( @@ -512,13 +512,13 @@ class _PlayerSettingsPageState extends State { } }, title: - Text('默认视频比例', style: TextStyle(fontFamily: fontFamily)), + Text('Default aspect ratio', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: playerAspectRatioMenuController, builder: (_, __, ___) { return Text( - aspectRatioTypeMap[defaultAspectRatioType] ?? '自动', + aspectRatioTypeMap[defaultAspectRatioType] ?? 'Auto', style: TextStyle(fontFamily: fontFamily), ); }, diff --git a/lib/pages/settings/proxy/proxy_editor_page.dart b/lib/pages/settings/proxy/proxy_editor_page.dart index c19b5539f..2f1171664 100644 --- a/lib/pages/settings/proxy/proxy_editor_page.dart +++ b/lib/pages/settings/proxy/proxy_editor_page.dart @@ -43,7 +43,7 @@ class _ProxyEditorPageState extends State { final url = urlController.text.trim(); if (url.isEmpty) { - KazumiDialog.showToast(message: '请输入代理地址'); + KazumiDialog.showToast(message: 'Please enter the proxy address'); return; } @@ -82,18 +82,18 @@ class _ProxyEditorPageState extends State { ) .timeout(const Duration(seconds: 15)); await setting.put(SettingBoxKey.proxyConfigured, true); - KazumiDialog.showToast(message: '测试成功'); + KazumiDialog.showToast(message: 'Test succeeded'); } catch (e) { await setting.put(SettingBoxKey.proxyEnable, false); ProxyManager.clearProxy(); - KazumiDialog.showToast(message: '代理连接失败'); + KazumiDialog.showToast(message: 'Proxy connection failed'); } } @override Widget build(BuildContext context) { return Scaffold( - appBar: const SysAppBar(title: Text('代理配置')), + appBar: const SysAppBar(title: Text('Proxy configuration')), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Center( @@ -106,16 +106,16 @@ class _ProxyEditorPageState extends State { TextFormField( controller: urlController, decoration: const InputDecoration( - labelText: '代理地址', + labelText: 'Proxy address', hintText: 'http://127.0.0.1:7890', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { - return '请输入代理地址'; + return 'Please enter the proxy address'; } if (!ProxyUtils.isValidProxyUrl(value)) { - return '格式错误,请使用 http://host:port 格式'; + return 'Invalid format, please use http://host:port'; } return null; }, @@ -124,7 +124,7 @@ class _ProxyEditorPageState extends State { TextFormField( controller: testUrlController, decoration: const InputDecoration( - labelText: '测试地址', + labelText: 'Test address', hintText: 'https://www.google.com', border: OutlineInputBorder(), ), @@ -138,7 +138,7 @@ class _ProxyEditorPageState extends State { floatingActionButton: FloatingActionButton.extended( onPressed: saveAndTest, icon: const Icon(Icons.save), - label: const Text('保存并测试'), + label: const Text('Save and test'), ), ); } diff --git a/lib/pages/settings/proxy/proxy_settings_page.dart b/lib/pages/settings/proxy/proxy_settings_page.dart index 0d2aec159..cc8e35aff 100644 --- a/lib/pages/settings/proxy/proxy_settings_page.dart +++ b/lib/pages/settings/proxy/proxy_settings_page.dart @@ -36,7 +36,7 @@ class _ProxySettingsPageState extends State { final proxyConfigured = setting.get(SettingBoxKey.proxyConfigured, defaultValue: false); if (!proxyConfigured) { - KazumiDialog.showToast(message: '请先在代理配置中完成测试'); + KazumiDialog.showToast(message: 'Please complete the test in proxy configuration first'); return; } await setting.put(SettingBoxKey.proxyEnable, true); @@ -59,19 +59,19 @@ class _ProxySettingsPageState extends State { onBackPressed(context); }, child: Scaffold( - appBar: const SysAppBar(title: Text('代理设置')), + appBar: const SysAppBar(title: Text('Proxy settings')), body: SettingsList( maxWidth: 800, sections: [ SettingsSection( - title: Text('代理', style: TextStyle(fontFamily: fontFamily)), + title: Text('Proxy', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( onToggle: (value) async { await updateProxyEnable(value ?? !proxyEnable); }, - title: Text('启用代理', style: TextStyle(fontFamily: fontFamily)), - description: Text('启用后网络请求将通过代理服务器', + title: Text('Enable proxy', style: TextStyle(fontFamily: fontFamily)), + description: Text('When enabled, network requests go through the proxy server', style: TextStyle(fontFamily: fontFamily)), initialValue: proxyEnable, ), @@ -83,8 +83,8 @@ class _ProxySettingsPageState extends State { defaultValue: false); }); }, - title: Text('代理配置', style: TextStyle(fontFamily: fontFamily)), - description: Text('配置代理服务器地址和认证信息', + title: Text('Proxy configuration', style: TextStyle(fontFamily: fontFamily)), + description: Text('Configure the proxy server address and credentials', style: TextStyle(fontFamily: fontFamily)), ), ], diff --git a/lib/pages/settings/renderer_settings.dart b/lib/pages/settings/renderer_settings.dart index 838617aad..d44880f31 100644 --- a/lib/pages/settings/renderer_settings.dart +++ b/lib/pages/settings/renderer_settings.dart @@ -29,13 +29,13 @@ class _RendererSettingsState extends State { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar( - title: Text('视频渲染器'), + title: Text('Video renderer'), ), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('选择合适的渲染器以获得最佳播放体验', + title: Text('Choose a suitable renderer for the best playback experience', style: TextStyle(fontFamily: fontFamily)), tiles: androidVideoRenderersList.entries .map((e) => SettingsTile.radioTile( diff --git a/lib/pages/settings/super_resolution_settings.dart b/lib/pages/settings/super_resolution_settings.dart index 3c9d8c56f..efb9d8769 100644 --- a/lib/pages/settings/super_resolution_settings.dart +++ b/lib/pages/settings/super_resolution_settings.dart @@ -39,18 +39,18 @@ class _SuperResolutionSettingsState extends State { final fontFamily = Theme.of(context).textTheme.bodyMedium?.fontFamily; return Scaffold( appBar: const SysAppBar( - title: Text('超分辨率'), + title: Text('Super resolution'), ), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('超分辨率需要启用硬件解码, 若启用硬件解码后仍然不生效, 尝试切换视频渲染器为 gpu', + title: Text('Super resolution requires hardware decoding. If it still does not work after enabling hardware decoding, try switching the video renderer to gpu', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.radioTile( title: Text("OFF", style: TextStyle(fontFamily: fontFamily)), - description: Text("默认禁用超分辨率", + description: Text("Super resolution disabled by default", style: TextStyle(fontFamily: fontFamily)), radioValue: "1", groupValue: superResolutionType.value, @@ -67,7 +67,7 @@ class _SuperResolutionSettingsState extends State { SettingsTile.radioTile( title: Text("Efficiency", style: TextStyle(fontFamily: fontFamily)), - description: Text("默认启用基于Anime4K的超分辨率 (效率优先)", + description: Text("Anime4K-based super resolution enabled by default (performance first)", style: TextStyle(fontFamily: fontFamily)), radioValue: "2", groupValue: superResolutionType.value, @@ -84,7 +84,7 @@ class _SuperResolutionSettingsState extends State { SettingsTile.radioTile( title: Text("Quality", style: TextStyle(fontFamily: fontFamily)), - description: Text("默认启用基于Anime4K的超分辨率 (质量优先)", + description: Text("Anime4K-based super resolution enabled by default (quality first)", style: TextStyle(fontFamily: fontFamily)), radioValue: "3", groupValue: superResolutionType.value, @@ -100,11 +100,11 @@ class _SuperResolutionSettingsState extends State { ) ]), SettingsSection( - title: Text('默认行为', style: TextStyle(fontFamily: fontFamily)), + title: Text('Default behavior', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.switchTile( - title: Text('关闭提示', style: TextStyle(fontFamily: fontFamily)), - description: Text('关闭每次启用超分辨率时的提示', + title: Text('Disable prompt', style: TextStyle(fontFamily: fontFamily)), + description: Text('Disable the prompt shown each time super resolution is enabled', style: TextStyle(fontFamily: fontFamily)), initialValue: promptOnEnable, onToggle: (value) async { diff --git a/lib/pages/settings/theme_settings_page.dart b/lib/pages/settings/theme_settings_page.dart index fd27bd220..d355296ee 100644 --- a/lib/pages/settings/theme_settings_page.dart +++ b/lib/pages/settings/theme_settings_page.dart @@ -152,12 +152,12 @@ class _ThemeSettingsPageState extends State { onBackPressed(context); }, child: Scaffold( - appBar: const SysAppBar(title: Text('外观设置')), + appBar: const SysAppBar(title: Text('Appearance settings')), body: SettingsList( maxWidth: 1000, sections: [ SettingsSection( - title: Text('外观', style: TextStyle(fontFamily: fontFamily)), + title: Text('Appearance', style: TextStyle(fontFamily: fontFamily)), tiles: [ SettingsTile.navigation( onPressed: (_) { @@ -167,15 +167,15 @@ class _ThemeSettingsPageState extends State { menuController.open(); } }, - title: Text('深色模式', style: TextStyle(fontFamily: fontFamily)), + title: Text('Dark mode', style: TextStyle(fontFamily: fontFamily)), value: MenuAnchor( consumeOutsideTap: true, controller: menuController, builder: (_, __, ___) { return Text( defaultThemeMode == 'light' - ? '浅色' - : (defaultThemeMode == 'dark' ? '深色' : '跟随系统'), + ? 'Light' + : (defaultThemeMode == 'dark' ? 'Dark' : 'Follow system'), style: TextStyle(fontFamily: fontFamily), ); }, @@ -198,7 +198,7 @@ class _ThemeSettingsPageState extends State { ), SizedBox(width: 8), Text( - '跟随系统', + 'Follow system', style: TextStyle( color: defaultThemeMode == 'system' ? Theme.of(context).colorScheme.primary @@ -229,7 +229,7 @@ class _ThemeSettingsPageState extends State { ), SizedBox(width: 8), Text( - '浅色', + 'Light', style: TextStyle( color: defaultThemeMode == 'light' ? Theme.of(context) @@ -261,7 +261,7 @@ class _ThemeSettingsPageState extends State { ), SizedBox(width: 8), Text( - '深色', + 'Dark', style: TextStyle( color: defaultThemeMode == 'dark' ? Theme.of(context).colorScheme.primary @@ -282,7 +282,7 @@ class _ThemeSettingsPageState extends State { onPressed: (_) async { KazumiDialog.show(builder: (context) { return AlertDialog( - title: Text('配色方案', + title: Text('Color scheme', style: TextStyle(fontFamily: fontFamily)), content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -326,7 +326,7 @@ class _ThemeSettingsPageState extends State { ); }); }, - title: Text('配色方案', style: TextStyle(fontFamily: fontFamily)), + title: Text('Color scheme', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile.switchTile( enabled: !Platform.isIOS, @@ -337,7 +337,7 @@ class _ThemeSettingsPageState extends State { themeProvider.setDynamic(useDynamicColor); setState(() {}); }, - title: Text('动态配色', style: TextStyle(fontFamily: fontFamily)), + title: Text('Dynamic color', style: TextStyle(fontFamily: fontFamily)), initialValue: useDynamicColor, ), SettingsTile.switchTile( @@ -356,13 +356,13 @@ class _ThemeSettingsPageState extends State { setState(() {}); }, title: - Text('使用系统字体', style: TextStyle(fontFamily: fontFamily)), - description: Text('关闭后使用 MI Sans 字体', + Text('Use system font', style: TextStyle(fontFamily: fontFamily)), + description: Text('When off, use the MI Sans font', style: TextStyle(fontFamily: fontFamily)), initialValue: useSystemFont, ), ], - bottomInfo: Text('动态配色仅支持安卓12及以上和桌面平台', + bottomInfo: Text('Dynamic color is only supported on Android 12 and above and on desktop platforms', style: TextStyle(fontFamily: fontFamily)), ), SettingsSection( @@ -375,8 +375,8 @@ class _ThemeSettingsPageState extends State { setState(() {}); }, title: - Text('OLED优化', style: TextStyle(fontFamily: fontFamily)), - description: Text('深色模式下使用纯黑背景', + Text('OLED optimization', style: TextStyle(fontFamily: fontFamily)), + description: Text('Use a pure black background in dark mode', style: TextStyle(fontFamily: fontFamily)), initialValue: oledEnhance, ), @@ -392,9 +392,9 @@ class _ThemeSettingsPageState extends State { SettingBoxKey.showWindowButton, showWindowButton); setState(() {}); }, - title: Text('使用系统标题栏', + title: Text('Use system title bar', style: TextStyle(fontFamily: fontFamily)), - description: Text('重启应用生效', + description: Text('Takes effect after restarting the app', style: TextStyle(fontFamily: fontFamily)), initialValue: showWindowButton, ), @@ -408,7 +408,7 @@ class _ThemeSettingsPageState extends State { Modular.to.pushNamed('/settings/theme/display'); }, title: - Text('屏幕帧率', style: TextStyle(fontFamily: fontFamily)), + Text('Screen refresh rate', style: TextStyle(fontFamily: fontFamily)), ), ], ), diff --git a/lib/pages/timeline/timeline_controller.dart b/lib/pages/timeline/timeline_controller.dart index c67668357..a824bea66 100644 --- a/lib/pages/timeline/timeline_controller.dart +++ b/lib/pages/timeline/timeline_controller.dart @@ -113,7 +113,7 @@ abstract class _TimelineController with Store { void tryEnterSeason(DateTime date) { selectedDate = date; - seasonString = "加载中 ٩(◦`꒳´◦)۶"; + seasonString = "Loading ٩(◦`꒳´◦)۶"; } /// 排序方式 diff --git a/lib/pages/timeline/timeline_page.dart b/lib/pages/timeline/timeline_page.dart index f897b1d6c..c9a95cf28 100644 --- a/lib/pages/timeline/timeline_page.dart +++ b/lib/pages/timeline/timeline_page.dart @@ -62,13 +62,13 @@ class _TimelinePageState extends State DateTime generateDateTime(int year, String season) { switch (season) { - case '冬': + case 'Winter': return DateTime(year, 1, 1); - case '春': + case 'Spring': return DateTime(year, 4, 1); - case '夏': + case 'Summer': return DateTime(year, 7, 1); - case '秋': + case 'Autumn': return DateTime(year, 10, 1); default: return DateTime.now(); @@ -76,16 +76,16 @@ class _TimelinePageState extends State } final List tabs = const [ - Tab(text: '一'), - Tab(text: '二'), - Tab(text: '三'), - Tab(text: '四'), - Tab(text: '五'), - Tab(text: '六'), - Tab(text: '日'), + Tab(text: 'Mon'), + Tab(text: 'Tue'), + Tab(text: 'Wed'), + Tab(text: 'Thu'), + Tab(text: 'Fri'), + Tab(text: 'Sat'), + Tab(text: 'Sun'), ]; - final seasons = ['秋', '夏', '春', '冬']; + final seasons = ['Autumn', 'Summer', 'Spring', 'Winter']; String getStringByDateTime(DateTime d) { return d.year.toString() + getSeasonStringByMonth(d.month); @@ -171,7 +171,7 @@ class _TimelinePageState extends State const SizedBox(width: 12), IconButton.filledTonal( onPressed: KazumiDialog.dismiss, - tooltip: '关闭', + tooltip: 'Close', icon: const Icon(Icons.close), ), ], @@ -293,8 +293,8 @@ class _TimelinePageState extends State return buildTimelineBottomSheetHeaderCard( context, - title: '时间机器', - description: '按季度回到任意放送季,时间线会立即切换。', + title: 'Time machine', + description: 'Jump back to any broadcast season by quarter, the timeline switches instantly.', footer: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( @@ -302,7 +302,7 @@ class _TimelinePageState extends State borderRadius: BorderRadius.circular(16), ), child: Text( - '当前查看 ${getStringByDateTime(timelineController.selectedDate)}', + 'Currently viewing ${getStringByDateTime(timelineController.selectedDate)}', style: textTheme.labelLarge?.copyWith( color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, @@ -345,7 +345,7 @@ class _TimelinePageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '$year年', + '$year', style: textTheme.titleMedium?.copyWith( color: colorScheme.onSurface, fontWeight: FontWeight.w700, @@ -354,7 +354,7 @@ class _TimelinePageState extends State if (!hasSelectedSeason) ...[ const SizedBox(height: 4), Text( - '共 ${availableSeasons.length} 个季度可选', + '${availableSeasons.length} seasons available', style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -433,13 +433,13 @@ class _TimelinePageState extends State String getSortTypeLabel(int sortType) { switch (sortType) { case 1: - return '时间优先'; + return 'Time first'; case 2: - return '评分优先'; + return 'Rating first'; case 3: - return '热度优先'; + return 'Popularity first'; default: - return '热度优先'; + return 'Popularity first'; } } @@ -510,8 +510,8 @@ class _TimelinePageState extends State Widget buildTimelineOptionsSheetHeader(BuildContext context) { return buildTimelineBottomSheetHeaderCard( context, - title: '时间线选项', - description: '调整排序和过滤条件,结果会立即应用到当前时间线。', + title: 'Timeline options', + description: 'Adjust sorting and filters, changes apply to the current timeline instantly.', footer: Observer( builder: (context) { final enabledFilterCount = getEnabledTimelineFilterCount(); @@ -521,14 +521,14 @@ class _TimelinePageState extends State children: [ buildTimelineOptionSummaryChip( context, - label: '当前排序 ${getSortTypeLabel(timelineController.sortType)}', + label: 'Current sort ${getSortTypeLabel(timelineController.sortType)}', highlighted: true, ), buildTimelineOptionSummaryChip( context, label: enabledFilterCount == 0 - ? '未启用过滤条件' - : '已启用 $enabledFilterCount 个过滤条件', + ? 'No filters enabled' + : '$enabledFilterCount filters enabled', onTap: scrollToFilterSection, ), ], @@ -712,15 +712,15 @@ class _TimelinePageState extends State return buildTimelineOptionSection( context, sectionKey: filterSectionKey, - title: '过滤器', - description: '按收藏状态收起不需要显示的条目,支持连续调整。', + title: 'Filters', + description: 'Collapse entries you do not want to see by collection status, supports continuous adjustment.', child: Column( children: [ Observer( builder: (context) => buildFilterOptionTile( context, - title: '不显示已抛弃的番剧', - description: '隐藏已经标记为抛弃的条目。', + title: 'Hide dropped anime', + description: 'Hide entries marked as dropped.', value: timelineController.notShowAbandonedBangumis, onChanged: (value) { timelineController.setNotShowAbandonedBangumis(value); @@ -732,8 +732,8 @@ class _TimelinePageState extends State Observer( builder: (context) => buildFilterOptionTile( context, - title: '不显示已看过的番剧', - description: '把已经看完的条目从时间线中移除。', + title: 'Hide watched anime', + description: 'Remove finished entries from the timeline.', value: timelineController.notShowWatchedBangumis, onChanged: (value) { timelineController.setNotShowWatchedBangumis(value); @@ -745,8 +745,8 @@ class _TimelinePageState extends State Observer( builder: (context) => buildFilterOptionTile( context, - title: '只显示在看的番剧', - description: '聚焦当前正在追更的条目。', + title: 'Show only currently watching anime', + description: 'Focus on entries you are currently following.', value: timelineController.onlyShowWatchingBangumis, onChanged: (value) { timelineController.setOnlyShowWatchingBangumis(value); @@ -762,31 +762,31 @@ class _TimelinePageState extends State Widget showSortSwitcher() { return buildTimelineOptionSection( context, - title: '排序方式', - description: '选择每一天内番剧卡片的排列方式。', + title: 'Sort by', + description: 'Choose how anime cards are arranged within each day.', child: Column( children: [ buildSortOptionTile( context, sortType: 3, - title: '按热度排序', - description: '优先展示讨论度和关注度更高的条目。', + title: 'Sort by popularity', + description: 'Show entries with higher discussion and attention first.', icon: Icons.local_fire_department_rounded, ), const SizedBox(height: 12), buildSortOptionTile( context, sortType: 2, - title: '按评分排序', - description: '优先展示评分更高的条目。', + title: 'Sort by rating', + description: 'Show higher-rated entries first.', icon: Icons.star_rounded, ), const SizedBox(height: 12), buildSortOptionTile( context, sortType: 1, - title: '按时间排序', - description: '恢复默认时间顺序,方便按播出节奏查看。', + title: 'Sort by time', + description: 'Restore the default time order to follow the broadcast schedule.', icon: Icons.schedule_rounded, ), ], diff --git a/lib/pages/video/video_controller.dart b/lib/pages/video/video_controller.dart index 881c74cd7..8c93efc61 100644 --- a/lib/pages/video/video_controller.dart +++ b/lib/pages/video/video_controller.dart @@ -212,11 +212,11 @@ abstract class _VideoPageController with Store { roadList.clear(); episodes.sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber)); roadList.add(Road( - name: '播放列表1', + name: 'Playlist1', data: episodes.map((e) => e.episodeNumber.toString()).toList(), identifier: episodes .map((e) => - e.episodeName.isNotEmpty ? e.episodeName : '第${e.episodeNumber}集') + e.episodeName.isNotEmpty ? e.episodeName : 'Ep ${e.episodeNumber}') .toList(), )); } @@ -329,7 +329,7 @@ abstract class _VideoPageController with Store { if (actualEpisodeNumber == null) { KazumiLogger().e( 'VideoPageController: failed to parse episode number from roadList data: ${roadList[selection.road].data[selection.episode - 1]}'); - KazumiDialog.showToast(message: '集数解析失败'); + KazumiDialog.showToast(message: 'Failed to parse episode'); return; } @@ -339,7 +339,7 @@ abstract class _VideoPageController with Store { actualEpisodeNumber, ); if (localPath == null) { - KazumiDialog.showToast(message: '该集数未下载'); + KazumiDialog.showToast(message: 'This episode is not downloaded'); return; } if (session.isStale) { @@ -417,14 +417,14 @@ abstract class _VideoPageController with Store { } else { playerController.danmaku.applyUnavailableDanmakuLoad(result); if (result.isFailed) { - KazumiDialog.showToast(message: '弹幕加载失败,可手动检索'); + KazumiDialog.showToast(message: 'Failed to load danmaku, you can search manually'); } } } } catch (e) { if (session.isActive && danmakuSession.isActive) { playerController.danmaku.finishDanmakuLoad(disableDanmaku: true); - KazumiDialog.showToast(message: '弹幕加载失败,可手动检索'); + KazumiDialog.showToast(message: 'Failed to load danmaku, you can search manually'); } KazumiLogger().w('VideoPageController: failed to load danmaku', error: e); } @@ -511,7 +511,7 @@ abstract class _VideoPageController with Store { return; } loading = false; - errorMessage = '视频解析超时,请重试'; + errorMessage = 'Video parsing timed out, please retry'; } on VideoSourceCancelledException { KazumiLogger().i('VideoPageController: video URL resolution cancelled'); } catch (e) { @@ -519,7 +519,7 @@ abstract class _VideoPageController with Store { return; } loading = false; - errorMessage = '视频解析失败:${e.toString()}'; + errorMessage = 'Video parsing failed: ${e.toString()}'; } } diff --git a/lib/pages/video/video_page.dart b/lib/pages/video/video_page.dart index 9f844e9b1..112593252 100644 --- a/lib/pages/video/video_page.dart +++ b/lib/pages/video/video_page.dart @@ -117,7 +117,7 @@ class _VideoPageState extends State ); if (mounted) { videoPageController.loading = false; - videoPageController.errorMessage = '播放器初始化失败'; + videoPageController.errorMessage = 'Player initialization failed'; } return; } @@ -455,15 +455,15 @@ class _VideoPageState extends State keyboardFocus.requestFocus(); if (playerController.danmaku.danDanmakus.isEmpty) { KazumiDialog.showToast( - message: '当前剧集不支持弹幕发送的说', + message: 'This episode does not support sending danmaku', ); return; } if (msg.isEmpty) { - KazumiDialog.showToast(message: '弹幕内容为空'); + KazumiDialog.showToast(message: 'Danmaku content is empty'); return; } else if (msg.length > 100) { - KazumiDialog.showToast(message: '弹幕内容过长'); + KazumiDialog.showToast(message: 'Danmaku content is too long'); return; } @@ -471,12 +471,12 @@ class _VideoPageState extends State if (destination == DanmakuDestination.chatRoom) { if (playerController.syncplay.syncplayRoom.isEmpty) { - KazumiDialog.showToast(message: '你还没有加入一起看,无法发送聊天室弹幕'); + KazumiDialog.showToast(message: 'You have not joined Watch Together, cannot send chat room danmaku'); return; } final sender = - playerController.syncplay.syncplayController?.username ?? '我'; + playerController.syncplay.syncplayController?.username ?? 'Me'; final String displayText = '$sender:$msg'; playerController.danmaku.canvasController.addDanmaku( @@ -526,7 +526,7 @@ class _VideoPageState extends State decoration: const InputDecoration( filled: true, floatingLabelBehavior: FloatingLabelBehavior.never, - hintText: '发个友善的弹幕见证当下', + hintText: 'Send a friendly danmaku to mark the moment', hintStyle: TextStyle(fontSize: 14), alignLabelWithHint: true, contentPadding: @@ -568,7 +568,7 @@ class _VideoPageState extends State return; } if (msg.trim().isEmpty) { - KazumiDialog.showToast(message: '弹幕内容为空'); + KazumiDialog.showToast(message: 'Danmaku content is empty'); return; } @@ -584,12 +584,12 @@ class _VideoPageState extends State mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: const Text('发送到聊天室'), + title: const Text('Send to chat room'), onTap: () => Navigator.of(context).pop(DanmakuDestination.chatRoom), ), ListTile( - title: const Text('发送到远程弹幕库'), + title: const Text('Send to remote danmaku server'), onTap: () => Navigator.of(context).pop(DanmakuDestination.remoteDanmaku), ), @@ -781,8 +781,8 @@ class _VideoPageState extends State const SizedBox(height: 10), Text( videoPageController.loading - ? '视频资源解析中' - : '视频资源解析成功, 播放器加载中', + ? 'Parsing video resource' + : 'Video resource parsed, loading player', style: const TextStyle(color: Colors.white), ), ], @@ -903,7 +903,7 @@ class _VideoPageState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text(' 合集 '), + const Text(' Collection '), Expanded( child: Text( videoPageController.title, @@ -932,7 +932,7 @@ class _VideoPageState extends State } }, child: Text( - '播放列表${visibleRoad + 1} ', + 'Playlist${visibleRoad + 1} ', style: const TextStyle(fontSize: 13), ), ), @@ -952,7 +952,7 @@ class _VideoPageState extends State child: Align( alignment: Alignment.centerLeft, child: Text( - '播放列表${i + 1}', + 'Playlist${i + 1}', style: TextStyle( color: i == visibleRoad ? Theme.of(context).colorScheme.primary @@ -1030,7 +1030,7 @@ class _VideoPageState extends State builder: (context) { var cardList = []; for (var road in videoPageController.roadList) { - if (road.name == '播放列表${visibleRoad + 1}') { + if (road.name == 'Playlist${visibleRoad + 1}') { int count = 1; for (var urlItem in road.data) { int count0 = count; @@ -1157,8 +1157,8 @@ class _VideoPageState extends State } }, tabs: const [ - Tab(text: '选集'), - Tab(text: '评论'), + Tab(text: 'Episodes'), + Tab(text: 'Reviews'), ], ), if (MediaQuery.sizeOf(context).width <= @@ -1180,15 +1180,15 @@ class _VideoPageState extends State if (danmakuOn && !videoPageController.loading) { showMobileDanmakuInput(); } else if (videoPageController.loading) { - KazumiDialog.showToast(message: '请等待视频加载完成'); + KazumiDialog.showToast(message: 'Please wait for the video to finish loading'); } else { - KazumiDialog.showToast(message: '请先打开弹幕'); + KazumiDialog.showToast(message: 'Please turn on danmaku first'); } }, child: Row( children: [ Text( - danmakuOn ? ' 点我发弹幕 ' : ' 已关闭弹幕 ', + danmakuOn ? ' Tap to send danmaku ' : ' Danmaku is off ', softWrap: false, overflow: TextOverflow.clip, style: TextStyle( diff --git a/lib/pages/webdav_editor/webdav_editor_page.dart b/lib/pages/webdav_editor/webdav_editor_page.dart index d3bf14d95..8740e57f9 100644 --- a/lib/pages/webdav_editor/webdav_editor_page.dart +++ b/lib/pages/webdav_editor/webdav_editor_page.dart @@ -46,7 +46,7 @@ class _WebDavEditorPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: const SysAppBar( - title: Text('WEBDAV编辑'), + title: Text('WebDAV editing'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), @@ -87,7 +87,7 @@ class _WebDavEditorPageState extends State { ), // const SizedBox(height: 20), // ExpansionTile( - // title: const Text('高级选项'), + // title: const Text('Advanced options'), // children: [], // ), ], @@ -107,16 +107,16 @@ class _WebDavEditorPageState extends State { try { await webDav.init(); } catch (e) { - KazumiDialog.showToast(message: '配置失败 ${e.toString()}'); + KazumiDialog.showToast(message: 'Configuration failed ${e.toString()}'); await setting.put(SettingBoxKey.webDavEnable, false); return; } - KazumiDialog.showToast(message: '配置成功, 开始测试'); + KazumiDialog.showToast(message: 'Configuration succeeded, starting test'); try { await webDav.ping(); - KazumiDialog.showToast(message: '测试成功'); + KazumiDialog.showToast(message: 'Test succeeded'); } catch (e) { - KazumiDialog.showToast(message: '测试失败 ${e.toString()}'); + KazumiDialog.showToast(message: 'Test failed ${e.toString()}'); await setting.put(SettingBoxKey.webDavEnable, false); } }, diff --git a/lib/pages/webdav_editor/webdav_setting.dart b/lib/pages/webdav_editor/webdav_setting.dart index de462c3f1..9caea6ce1 100644 --- a/lib/pages/webdav_editor/webdav_setting.dart +++ b/lib/pages/webdav_editor/webdav_setting.dart @@ -53,24 +53,24 @@ class _PlayerSettingsPageState extends State { await setting.get(SettingBoxKey.webDavEnable, defaultValue: false); if (webDavEnable) { KazumiLogger().i('WebDav: manual history sync started'); - KazumiDialog.showToast(message: '正在同步观看记录'); + KazumiDialog.showToast(message: 'Syncing watch history'); var webDav = WebDav(); try { await webDav.ping(); try { await webDav.syncHistory(); KazumiLogger().i('WebDav: manual history sync completed'); - KazumiDialog.showToast(message: '观看记录同步完成'); + KazumiDialog.showToast(message: 'Watch history sync complete'); } catch (e) { KazumiLogger().w('WebDav: manual history sync failed', error: e); - KazumiDialog.showToast(message: '观看记录同步失败 ${e.toString()}'); + KazumiDialog.showToast(message: 'Watch history sync failed ${e.toString()}'); } } catch (e) { KazumiLogger().w('WebDav: manual history sync ping failed', error: e); - KazumiDialog.showToast(message: 'WebDav连接失败'); + KazumiDialog.showToast(message: 'WebDav connection failed'); } } else { - KazumiDialog.showToast(message: '未开启WebDav同步或配置无效'); + KazumiDialog.showToast(message: 'WebDav sync is not enabled or the configuration is invalid'); } } @@ -83,7 +83,7 @@ class _PlayerSettingsPageState extends State { onBackPressed(context); }, child: Scaffold( - appBar: const SysAppBar(title: Text('同步设置')), + appBar: const SysAppBar(title: Text('Sync settings')), body: SettingsList( maxWidth: 1000, sections: [ @@ -97,9 +97,9 @@ class _PlayerSettingsPageState extends State { SettingBoxKey.enableGitProxy, enableGitProxy); setState(() {}); }, - title: Text('Github镜像', + title: Text('Github mirror', style: TextStyle(fontFamily: fontFamily)), - description: Text('使用镜像访问规则托管仓库', + description: Text('Use a mirror to access the rule hosting repository', style: TextStyle(fontFamily: fontFamily)), initialValue: enableGitProxy, ), @@ -117,9 +117,9 @@ class _PlayerSettingsPageState extends State { setState(() {}); } }, - title: Text('Bangumi 镜像', + title: Text('Bangumi mirror', style: TextStyle(fontFamily: fontFamily)), - description: Text('使用本地 Bangumi 缓存后端加载热门与分类榜单', + description: Text('Use the local Bangumi cache backend to load trending and category rankings', style: TextStyle(fontFamily: fontFamily)), initialValue: enableBangumiProxy, ), @@ -135,7 +135,7 @@ class _PlayerSettingsPageState extends State { .trim(); if (token.isEmpty) { KazumiDialog.showToast( - message: '请先配置 Bangumi 的 Access Token'); + message: 'Please configure your Bangumi Access Token first'); return; } else { if (!bangumi.initialized) { @@ -143,7 +143,7 @@ class _PlayerSettingsPageState extends State { await bangumi.init(); } catch (e) { KazumiDialog.showToast( - message: "Bangumi 初始化失败,请稍后再试"); + message: "Bangumi initialization failed, please try again later"); return; } } @@ -157,9 +157,9 @@ class _PlayerSettingsPageState extends State { } setState(() {}); }, - title: Text('Bangumi 同步', + title: Text('Bangumi sync', style: TextStyle(fontFamily: fontFamily)), - description: Text('允许与Bangumi自动同步收藏/追番状态', + description: Text('Allow automatic syncing of collection and tracking status with Bangumi', style: TextStyle(fontFamily: fontFamily)), initialValue: bangumiSyncEnable, ), @@ -171,7 +171,7 @@ class _PlayerSettingsPageState extends State { defaultValue: false); setState(() {}); }, - title: Text('Bangumi 配置', + title: Text('Bangumi configuration', style: TextStyle(fontFamily: fontFamily)), ), ], @@ -187,7 +187,7 @@ class _PlayerSettingsPageState extends State { await WebDav().init(); } catch (e) { webDavEnable = false; - KazumiDialog.showToast(message: 'WEBDAV初始化失败 $e'); + KazumiDialog.showToast(message: 'WebDAV initialization failed $e'); } } if (!webDavEnable) { @@ -203,14 +203,14 @@ class _PlayerSettingsPageState extends State { setState(() {}); } }, - title: Text('WEBDAV同步', + title: Text('WebDAV sync', style: TextStyle(fontFamily: fontFamily)), initialValue: webDavEnable, ), SettingsTile.switchTile( onToggle: (value) async { if (!webDavEnable) { - KazumiDialog.showToast(message: '请先开启WEBDAV同步'); + KazumiDialog.showToast(message: 'Please enable WebDAV sync first'); return; } webDavEnableHistory = value ?? !webDavEnableHistory; @@ -219,15 +219,15 @@ class _PlayerSettingsPageState extends State { setState(() {}); }, title: - Text('观看记录同步', style: TextStyle(fontFamily: fontFamily)), - description: Text('允许自动同步观看记录', + Text('Watch history sync', style: TextStyle(fontFamily: fontFamily)), + description: Text('Allow automatic syncing of watch history', style: TextStyle(fontFamily: fontFamily)), initialValue: webDavEnableHistory, ), SettingsTile.switchTile( onToggle: (value) async { if (!webDavEnable) { - KazumiDialog.showToast(message: '请先开启WEBDAV同步'); + KazumiDialog.showToast(message: 'Please enable WebDAV sync first'); return; } webDavEnableCollect = value ?? !webDavEnableCollect; @@ -235,8 +235,8 @@ class _PlayerSettingsPageState extends State { SettingBoxKey.webDavEnableCollect, webDavEnableCollect); setState(() {}); }, - title: Text('收藏同步', style: TextStyle(fontFamily: fontFamily)), - description: Text('允许 WebDAV 参与追番状态同步', + title: Text('Collection sync', style: TextStyle(fontFamily: fontFamily)), + description: Text('Allow WebDAV to participate in tracking status sync', style: TextStyle(fontFamily: fontFamily)), initialValue: webDavEnableCollect, ), @@ -244,7 +244,7 @@ class _PlayerSettingsPageState extends State { onPressed: (_) async { Modular.to.pushNamed('/settings/webdav/editor'); }, - title: Text('WEBDAV配置', + title: Text('WebDAV configuration', style: TextStyle(fontFamily: fontFamily)), ), SettingsTile( @@ -252,9 +252,9 @@ class _PlayerSettingsPageState extends State { onPressed: (_) { syncHistoryWithWebDav(); }, - title: Text('立即同步观看记录', + title: Text('Sync watch history now', style: TextStyle(fontFamily: fontFamily)), - description: Text('与WEBDAV双向合并观看记录', + description: Text('Two-way merge watch history with WebDAV', style: TextStyle(fontFamily: fontFamily)), ), ], diff --git a/lib/plugins/plugins.dart b/lib/plugins/plugins.dart index 72734e64b..7594dc66a 100644 --- a/lib/plugins/plugins.dart +++ b/lib/plugins/plugins.dart @@ -279,7 +279,7 @@ class Plugin { }); if (chapterUrlList.isNotEmpty && chapterNameList.isNotEmpty) { Road road = Road( - name: '播放列表$count', + name: 'Playlist$count', data: chapterUrlList, identifier: chapterNameList); roadList.add(road); diff --git a/lib/request/apis/bangumi_api.dart b/lib/request/apis/bangumi_api.dart index 6f8661c61..69f1ce984 100644 --- a/lib/request/apis/bangumi_api.dart +++ b/lib/request/apis/bangumi_api.dart @@ -441,7 +441,7 @@ class BangumiApi { } on NetworkException catch (e) { if (e.statusCode == 401) { KazumiLogger().e('Bangumi token unauthorized, please check your token'); - throw StateError('Bangumi token 未授权,请检查您的 token'); + throw StateError('Bangumi token unauthorized, please check your token'); } rethrow; } catch (e) { @@ -518,7 +518,7 @@ class BangumiApi { bangumiCollection.add(BangumiCollection.fromJson(jsonItem)); progressCurrent++; onProgress?.call( - '正在拉取${collectionType.label}收藏', + 'Fetching ${collectionType.label} collection', progressCurrent, progressTotal, ); @@ -568,13 +568,13 @@ class BangumiApi { String str; switch (e.statusCode) { case 400: - str = 'Validation Error 验证错误'; + str = 'Validation Error'; break; case 401: - str = 'Unauthorized 未经授权'; + str = 'Unauthorized'; break; case 404: - str = 'User not found 用户不存在'; + str = 'User not found'; break; default: str = 'Error $e'; diff --git a/lib/request/core/network_config.dart b/lib/request/core/network_config.dart index 298b5ae86..9175276f0 100644 --- a/lib/request/core/network_config.dart +++ b/lib/request/core/network_config.dart @@ -83,7 +83,7 @@ class NetworkConfig { final proxyUrl = setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final parsed = ProxyUtils.parseProxyUrl(proxyUrl); if (parsed == null) { - KazumiLogger().w('Proxy: 代理地址格式错误或为空'); + KazumiLogger().w('Proxy: proxy address is malformed or empty'); return NetworkConfig( connectTimeout: connectTimeout, receiveTimeout: receiveTimeout, diff --git a/lib/request/core/network_error_mapper.dart b/lib/request/core/network_error_mapper.dart index 2e859e6cc..c2498c134 100644 --- a/lib/request/core/network_error_mapper.dart +++ b/lib/request/core/network_error_mapper.dart @@ -10,14 +10,14 @@ class NetworkErrorMapper { case DioExceptionType.badCertificate: return NetworkException( type: NetworkExceptionType.badCertificate, - message: '证书有误!', + message: 'Certificate error!', rawError: error, stackTrace: error.stackTrace, ); case DioExceptionType.badResponse: return NetworkException( type: NetworkExceptionType.badResponse, - message: '服务器异常,请稍后重试!', + message: 'Server error, please try again later!', statusCode: error.response?.statusCode, rawError: error, stackTrace: error.stackTrace, @@ -25,35 +25,35 @@ class NetworkErrorMapper { case DioExceptionType.cancel: return NetworkException( type: NetworkExceptionType.cancel, - message: '请求已被取消,请重新请求', + message: 'Request canceled, please try again', rawError: error, stackTrace: error.stackTrace, ); case DioExceptionType.connectionError: return NetworkException( type: NetworkExceptionType.connectionError, - message: '连接错误,请检查网络设置', + message: 'Connection error, please check your network settings', rawError: error, stackTrace: error.stackTrace, ); case DioExceptionType.connectionTimeout: return NetworkException( type: NetworkExceptionType.connectionTimeout, - message: '网络连接超时,请检查网络设置', + message: 'Network connection timed out, please check your network settings', rawError: error, stackTrace: error.stackTrace, ); case DioExceptionType.receiveTimeout: return NetworkException( type: NetworkExceptionType.receiveTimeout, - message: '响应超时,请稍后重试!', + message: 'Response timed out, please try again later!', rawError: error, stackTrace: error.stackTrace, ); case DioExceptionType.sendTimeout: return NetworkException( type: NetworkExceptionType.sendTimeout, - message: '发送请求超时,请检查网络设置', + message: 'Request send timed out, please check your network settings', rawError: error, stackTrace: error.stackTrace, ); @@ -61,7 +61,7 @@ class NetworkErrorMapper { final connection = await _connectionLabel(); return NetworkException( type: NetworkExceptionType.unknown, - message: '$connection 网络异常'.trimLeft(), + message: '$connection network error'.trimLeft(), rawError: error, stackTrace: error.stackTrace, ); @@ -71,7 +71,7 @@ class NetworkErrorMapper { static NetworkException parse(Object error, StackTrace stackTrace) { return NetworkException( type: NetworkExceptionType.parseError, - message: '响应解析失败', + message: 'Failed to parse response', rawError: error, stackTrace: stackTrace, ); @@ -80,22 +80,22 @@ class NetworkErrorMapper { static Future _connectionLabel() async { final connectivityResult = await Connectivity().checkConnectivity(); if (connectivityResult.contains(ConnectivityResult.mobile)) { - return '正在使用移动流量'; + return 'Using mobile data'; } if (connectivityResult.contains(ConnectivityResult.wifi)) { - return '正在使用wifi'; + return 'Using Wi-Fi'; } if (connectivityResult.contains(ConnectivityResult.ethernet)) { - return '正在使用局域网'; + return 'Using LAN'; } if (connectivityResult.contains(ConnectivityResult.vpn)) { - return '正在使用代理网络'; + return 'Using proxy network'; } if (connectivityResult.contains(ConnectivityResult.other)) { - return '正在使用其他网络'; + return 'Using another network'; } if (connectivityResult.contains(ConnectivityResult.none)) { - return '未连接到任何网络'; + return 'Not connected to any network'; } return ''; } diff --git a/lib/services/download/background_download_service.dart b/lib/services/download/background_download_service.dart index feb949bf5..c0259e381 100644 --- a/lib/services/download/background_download_service.dart +++ b/lib/services/download/background_download_service.dart @@ -34,8 +34,8 @@ class BackgroundDownloadService { FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'kazumi_download_channel', - channelName: '下载服务', - channelDescription: '视频下载后台服务', + channelName: 'Download service', + channelDescription: 'Video download background service', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, onlyAlertOnce: true, @@ -105,10 +105,10 @@ class BackgroundDownloadService { try { final result = await FlutterForegroundTask.startService( - notificationTitle: '正在下载', - notificationText: '准备中...', + notificationTitle: 'Downloading', + notificationText: 'Preparing...', notificationButtons: [ - const NotificationButton(id: 'pause_all', text: '暂停全部'), + const NotificationButton(id: 'pause_all', text: 'Pause all'), ], callback: _backgroundCallback, ); @@ -170,11 +170,11 @@ class BackgroundDownloadService { String text; if (activeCount == 0) { - title = '下载已暂停'; - text = '共 $totalCount 个任务'; + title = 'Downloads paused'; + text = '$totalCount tasks total'; } else { final percent = (overallProgress * 100).toInt(); - title = '正在下载 ($activeCount/$totalCount)'; + title = 'Downloading ($activeCount/$totalCount)'; text = '$percent% · $speedText'; } diff --git a/lib/services/download/download_manager.dart b/lib/services/download/download_manager.dart index 596ca9e5c..e2f4cae26 100644 --- a/lib/services/download/download_manager.dart +++ b/lib/services/download/download_manager.dart @@ -24,7 +24,7 @@ class _InsufficientStorageException implements Exception { final int requiredBytes; _InsufficientStorageException(this.availableBytes, this.requiredBytes); @override - String toString() => '存储空间不足'; + String toString() => 'Insufficient storage space'; } class DownloadTask { @@ -167,17 +167,17 @@ class DownloadManager implements IDownloadManager { String _getStorageErrorMessage(FileSystemException e) { // POSIX error code 28 = ENOSPC (No space left on device) if (e.osError?.errorCode == 28) { - return '存储空间不足,请清理后重试'; + return 'Insufficient storage space, please free up space and retry'; } // POSIX error code 13 = EACCES (Permission denied) if (e.osError?.errorCode == 13) { - return '存储权限被拒绝'; + return 'Storage permission denied'; } // POSIX error code 30 = EROFS (Read-only file system) if (e.osError?.errorCode == 30) { - return '存储为只读,无法写入'; + return 'Storage is read-only, cannot write'; } - return '存储错误: ${e.message}'; + return 'Storage error: ${e.message}'; } @override @@ -399,14 +399,14 @@ class DownloadManager implements IDownloadManager { if (!resolvedPlaylist.isVod) { episode.status = DownloadStatus.failed; - episode.errorMessage = '不支持下载直播流 (无有效分片)'; + episode.errorMessage = 'Live streams cannot be downloaded (no valid segments)'; _notifyProgress(task.recordKey, task.episodeNumber, episode); return; } if (resolvedPlaylist.segments.isEmpty) { episode.status = DownloadStatus.failed; - episode.errorMessage = 'M3U8 中未找到可下载的分片'; + episode.errorMessage = 'No downloadable segments found in the M3U8'; _notifyProgress(task.recordKey, task.episodeNumber, episode); return; } @@ -532,7 +532,7 @@ class DownloadManager implements IDownloadManager { if (failedCount > 0) { episode.status = DownloadStatus.failed; - episode.errorMessage = '$failedCount 个分片下载失败'; + episode.errorMessage = '$failedCount segments failed to download'; _notifyProgress(task.recordKey, task.episodeNumber, episode); return; } @@ -561,7 +561,7 @@ class DownloadManager implements IDownloadManager { } on _InsufficientStorageException catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = - '存储空间不足 (可用: ${fmt.formatBytes(e.availableBytes)})'; + 'Insufficient storage space (available: ${fmt.formatBytes(e.availableBytes)})'; _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().w('DownloadManager: insufficient storage space', error: e); } on FileSystemException catch (e) { @@ -710,7 +710,7 @@ class DownloadManager implements IDownloadManager { } on _InsufficientStorageException catch (e) { episode.status = DownloadStatus.failed; episode.errorMessage = - '存储空间不足 (可用: ${fmt.formatBytes(e.availableBytes)})'; + 'Insufficient storage space (available: ${fmt.formatBytes(e.availableBytes)})'; _notifyProgress(task.recordKey, task.episodeNumber, episode); KazumiLogger().w('DownloadManager: insufficient storage space', error: e); } on FileSystemException catch (e) { @@ -758,7 +758,7 @@ class DownloadManager implements IDownloadManager { if (cancelToken.isCancelled) { throw NetworkException( type: NetworkExceptionType.cancel, - message: '请求已被取消,请重新请求', + message: 'Request canceled, please try again', ); } @@ -781,14 +781,14 @@ class DownloadManager implements IDownloadManager { final trimmed = content.trimLeft(); if (!trimmed.startsWith('#EXTM3U')) { - throw _NotM3u8Exception('URL 不是 M3U8 播放列表'); + throw _NotM3u8Exception('URL is not an M3U8 playlist'); } return content; } on NetworkException catch (e) { if (cancelToken.isCancelled) rethrow; if (e.type == NetworkExceptionType.cancel) { - throw _NotM3u8Exception('响应过大,非 M3U8 播放列表'); + throw _NotM3u8Exception('Response too large, not an M3U8 playlist'); } rethrow; } diff --git a/lib/services/network/proxy_aware_image_cache_manager.dart b/lib/services/network/proxy_aware_image_cache_manager.dart index d976a530d..8c1b7bb55 100644 --- a/lib/services/network/proxy_aware_image_cache_manager.dart +++ b/lib/services/network/proxy_aware_image_cache_manager.dart @@ -83,7 +83,7 @@ class ProxyAwareImageFileService extends FileService { setting.get(SettingBoxKey.proxyUrl, defaultValue: ''); final parsed = ProxyUtils.parseProxyUrl(proxyUrl); if (parsed == null) { - KazumiLogger().w('Proxy: 图片缓存代理地址格式错误或为空'); + KazumiLogger().w('Proxy: image cache proxy address is malformed or empty'); } return parsed; } diff --git a/lib/services/network/proxy_manager.dart b/lib/services/network/proxy_manager.dart index 932424afe..e5b4a791b 100644 --- a/lib/services/network/proxy_manager.dart +++ b/lib/services/network/proxy_manager.dart @@ -13,14 +13,14 @@ class ProxyManager { static void applyProxy() { DioFactory.reset(); _applyImageCacheManager(); - KazumiLogger().i('Proxy: 网络客户端配置已刷新'); + KazumiLogger().i('Proxy: network client configuration refreshed'); } /// 清除代理设置 static void clearProxy() { DioFactory.reset(); _applyImageCacheManager(); - KazumiLogger().i('Proxy: 网络客户端代理已清除'); + KazumiLogger().i('Proxy: network client proxy cleared'); } static void _applyImageCacheManager() { diff --git a/lib/services/player/external_playback_launcher.dart b/lib/services/player/external_playback_launcher.dart index 2d3a8be4b..ce14c8b4f 100644 --- a/lib/services/player/external_playback_launcher.dart +++ b/lib/services/player/external_playback_launcher.dart @@ -21,11 +21,11 @@ class ExternalPlaybackLauncher { currentVideoUrl, 'video/mp4')) { KazumiDialog.dismiss(); KazumiDialog.showToast( - message: '尝试唤起外部播放器', + message: 'Trying to launch external player', ); } else { KazumiDialog.showToast( - message: '唤起外部播放器失败', + message: 'Failed to launch external player', ); } } else if (Platform.isMacOS || Platform.isIOS) { @@ -33,11 +33,11 @@ class ExternalPlaybackLauncher { currentVideoUrl, currentReferer)) { KazumiDialog.dismiss(); KazumiDialog.showToast( - message: '尝试唤起外部播放器', + message: 'Trying to launch external player', ); } else { KazumiDialog.showToast( - message: '唤起外部播放器失败', + message: 'Failed to launch external player', ); } } else if (Platform.isLinux && currentReferer.isEmpty) { @@ -45,21 +45,21 @@ class ExternalPlaybackLauncher { if (await canLaunchUrlString(currentVideoUrl)) { launchUrlString(currentVideoUrl); KazumiDialog.showToast( - message: '尝试唤起外部播放器', + message: 'Trying to launch external player', ); } else { KazumiDialog.showToast( - message: '无法使用外部播放器', + message: 'Cannot use external player', ); } } else { if (currentReferer.isEmpty) { KazumiDialog.showToast( - message: '暂不支持该设备', + message: 'This device is not supported yet', ); } else { KazumiDialog.showToast( - message: '暂不支持该规则', + message: 'This rule is not supported yet', ); } } diff --git a/lib/services/player/remote.dart b/lib/services/player/remote.dart index 1a8686e46..3e159890a 100644 --- a/lib/services/player/remote.dart +++ b/lib/services/player/remote.dart @@ -13,7 +13,7 @@ class RemotePlay { await KazumiDialog.show(builder: (BuildContext context) { return StatefulBuilder(builder: (context, setState) { return AlertDialog( - title: const Text('远程投屏'), + title: const Text('Remote casting'), content: SingleChildScrollView( child: Column( children: dlnaDevice, @@ -26,7 +26,7 @@ class RemotePlay { KazumiDialog.dismiss(); }, child: Text( - '退出', + 'Exit', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -34,7 +34,7 @@ class RemotePlay { onPressed: () { setState(() {}); KazumiDialog.showToast( - message: '开始搜索', + message: 'Start search', ); dlna.devices.stream.listen((deviceList) { dlnaDevice = []; @@ -51,7 +51,7 @@ class RemotePlay { onTap: () { try { KazumiDialog.showToast( - message: '尝试投屏至 ${value.info.friendlyName}', + message: 'Trying to cast to ${value.info.friendlyName}', ); DLNADevice(value.info).setUrl(video); DLNADevice(value.info).play(); @@ -60,7 +60,7 @@ class RemotePlay { 'RemotePlay: failed to cast to device', error: e); KazumiDialog.showToast( - message: 'DLNA 异常: $e \n尝试重新进入 DLNA 投屏或切换设备', + message: 'DLNA error: $e \nTry re-entering DLNA casting or switching devices', ); } })); @@ -74,7 +74,7 @@ class RemotePlay { // }); }, child: Text( - '搜索', + 'Search', style: TextStyle(color: Theme.of(context).colorScheme.outline), )), diff --git a/lib/services/player/timed_shutdown_service.dart b/lib/services/player/timed_shutdown_service.dart index d3f2d5c98..54ec20f60 100644 --- a/lib/services/player/timed_shutdown_service.dart +++ b/lib/services/player/timed_shutdown_service.dart @@ -117,17 +117,17 @@ class TimedShutdownService { }, builder: (context) { return AlertDialog( - title: const Text('定时关闭'), - content: const Text('定时时间已到,视频已暂停'), + title: const Text('Sleep timer'), + content: const Text('The sleep timer has ended, the video has been paused'), actions: [ TextButton( onPressed: () { _isDialogShowing = false; KazumiDialog.dismiss(); repeat(); - KazumiDialog.showToast(message: '已重新开始 $_lastSetMinutes 分钟定时'); + KazumiDialog.showToast(message: 'Restarted the $_lastSetMinutes minute timer'); }, - child: const Text('重复'), + child: const Text('Repeat'), ), TextButton( onPressed: () { @@ -135,7 +135,7 @@ class TimedShutdownService { KazumiDialog.dismiss(); }, child: Text( - '关闭', + 'Close', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -159,11 +159,11 @@ class TimedShutdownService { final hours = totalMinutes ~/ 60; final minutes = totalMinutes % 60; if (hours > 0 && minutes > 0) { - return '$hours 小时 $minutes 分钟'; + return '$hours h $minutes min'; } else if (hours > 0) { - return '$hours 小时'; + return '$hours h'; } else { - return '$minutes 分钟'; + return '$minutes min'; } } @@ -171,7 +171,7 @@ class TimedShutdownService { /// Uses KazumiDialog to avoid context-related resource leaks /// [onExpired] callback is invoked when timer expires (before showing dialog) static void showCustomTimerDialog({ - String title = '自定义定时', + String title = 'Custom timer', bool autoStart = true, VoidCallback? onExpired, void Function(int)? onResult, @@ -228,7 +228,7 @@ class _CustomTimerDialogState extends State<_CustomTimerDialog> { void _confirm() { final totalMinutes = _selectedHours * 60 + _selectedMinutes; if (totalMinutes <= 0) { - KazumiDialog.showToast(message: '请选择有效的时间'); + KazumiDialog.showToast(message: 'Please select a valid time'); return; } KazumiDialog.dismiss(); @@ -236,7 +236,7 @@ class _CustomTimerDialogState extends State<_CustomTimerDialog> { TimedShutdownService().start(totalMinutes, onExpired: widget.onExpired); KazumiDialog.showToast( message: - '已设置 ${TimedShutdownService().formatMinutesToDisplay(totalMinutes)} 后定时关闭', + 'Sleep timer set for ${TimedShutdownService().formatMinutesToDisplay(totalMinutes)}', ); } widget.onResult?.call(totalMinutes); @@ -253,7 +253,7 @@ class _CustomTimerDialogState extends State<_CustomTimerDialog> { Expanded( child: Column( children: [ - const Text('时', style: TextStyle(fontSize: 14)), + const Text('h', style: TextStyle(fontSize: 14)), const SizedBox(height: 8), Expanded( child: CupertinoPicker( @@ -283,7 +283,7 @@ class _CustomTimerDialogState extends State<_CustomTimerDialog> { Expanded( child: Column( children: [ - const Text('分', style: TextStyle(fontSize: 14)), + const Text('min', style: TextStyle(fontSize: 14)), const SizedBox(height: 8), Expanded( child: CupertinoPicker( @@ -313,13 +313,13 @@ class _CustomTimerDialogState extends State<_CustomTimerDialog> { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '取消', + 'Cancel', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: _confirm, - child: const Text('确定'), + child: const Text('OK'), ), ], ); diff --git a/lib/services/sync/bangumi_sync_service.dart b/lib/services/sync/bangumi_sync_service.dart index bf75d5735..ccb1151df 100644 --- a/lib/services/sync/bangumi_sync_service.dart +++ b/lib/services/sync/bangumi_sync_service.dart @@ -48,7 +48,7 @@ class BangumiSyncService { initialized = false; username = ''; if (_configuredToken.isEmpty) { - throw Exception('请先填写Bangumi Access Token'); + throw Exception('Please fill in the Bangumi Access Token first'); } try { await ping(); @@ -61,13 +61,13 @@ class BangumiSyncService { Future ping() async { if (isUsing) { - throw Exception('Bangumi: 当前有操作正在进行,请稍后再试'); + throw Exception('Bangumi: an operation is in progress, please try again later'); } await _runExclusive(() async { try { final name = await BangumiApi.getUsername(); if (name == null) { - throw Exception('Bangumi: 获取用户名失败'); + throw Exception('Bangumi: failed to get username'); } else { username = name; } @@ -135,17 +135,17 @@ class BangumiSyncService { final syncEnable = setting.get(SettingBoxKey.bangumiSyncEnable, defaultValue: false); if (!syncEnable) { - KazumiDialog.showToast(message: '同步已关闭'); + KazumiDialog.showToast(message: 'Sync is disabled'); KazumiLogger().i('Bangumi: sync disabled'); return false; } if (isUsing) { KazumiLogger().w('Bangumi is currently syncing'); - throw Exception('Bangumi 正在同步'); + throw Exception('Bangumi is syncing'); } return _runExclusive(() async { try { - onProgress?.call('开始同步 Bangumi 状态', 0, 0); + onProgress?.call('Starting Bangumi status sync', 0, 0); final priority = BangumiSyncPriority.fromValue( setting.get(SettingBoxKey.bangumiSyncPriority, defaultValue: 0), @@ -167,31 +167,31 @@ class BangumiSyncService { final totalOperations = mergePlan.totalOperations; if (totalOperations == 0) { - onProgress?.call('未发现状态差异,无需同步', 1, 1); + onProgress?.call('No status differences found, no sync needed', 1, 1); return false; } int syncedCount = 0; // 3. 仅本地有:直接上传到 Bangumi if (mergePlan.localOnlyUploads.isNotEmpty) { - onProgress?.call('正在上传本地新增状态', syncedCount, totalOperations); + onProgress?.call('Uploading newly added local status', syncedCount, totalOperations); for (final upload in mergePlan.localOnlyUploads) { final updated = await BangumiApi.updateBangumiByType( upload.bangumiId, upload.type, ); if (!updated) { - onProgress?.call('上传本地新增状态失败', syncedCount, totalOperations); - throw Exception('同步失败:条目 ${upload.bangumiId} 上传到 Bangumi 失败'); + onProgress?.call('Failed to upload newly added local status', syncedCount, totalOperations); + throw Exception('Sync failed: entry ${upload.bangumiId} failed to upload to Bangumi'); } syncedCount++; - onProgress?.call('正在上传本地新增状态', syncedCount, totalOperations); + onProgress?.call('Uploading newly added local status', syncedCount, totalOperations); } } // 4. 仅远程有:直接补到本地 if (mergePlan.remoteOnlyPuts.isNotEmpty) { - onProgress?.call('正在补全本地缺失状态', syncedCount, totalOperations); + onProgress?.call('Filling in missing local status', syncedCount, totalOperations); for (final mutation in mergePlan.remoteOnlyPuts) { await GStorage.putCollectible(mutation.collectible); await _recordCollectibleChange( @@ -200,26 +200,26 @@ class BangumiSyncService { mutation.collectible.type, ); syncedCount++; - onProgress?.call('正在补全本地缺失状态', syncedCount, totalOperations); + onProgress?.call('Filling in missing local status', syncedCount, totalOperations); } } // 5. 双方都有但不一致:按优先级处理 if (priority == BangumiSyncPriority.localFirst) { - onProgress?.call('本地优先:正在处理冲突状态', syncedCount, totalOperations); + onProgress?.call('Local first: resolving conflicting status', syncedCount, totalOperations); for (final upload in mergePlan.conflictUploads) { final updated = await BangumiApi.updateBangumiByType( upload.bangumiId, upload.type, ); if (updated != true) { - throw Exception('同步失败:条目 ${upload.bangumiId} 上传到 Bangumi 失败'); + throw Exception('Sync failed: entry ${upload.bangumiId} failed to upload to Bangumi'); } syncedCount++; - onProgress?.call('本地优先:正在处理冲突状态', syncedCount, totalOperations); + onProgress?.call('Local first: resolving conflicting status', syncedCount, totalOperations); } } else { - onProgress?.call('Bangumi优先:正在处理冲突状态', syncedCount, totalOperations); + onProgress?.call('Bangumi first: resolving conflicting status', syncedCount, totalOperations); for (final mutation in mergePlan.conflictLocalUpdates) { await GStorage.putCollectible(mutation.collectible); await _recordCollectibleChange( @@ -229,10 +229,10 @@ class BangumiSyncService { ); syncedCount++; onProgress?.call( - 'Bangumi优先:正在处理冲突状态', syncedCount, totalOperations); + 'Bangumi first: resolving conflicting status', syncedCount, totalOperations); } } - onProgress?.call('Bangumi 状态同步完成', 1, 1); + onProgress?.call('Bangumi status sync complete', 1, 1); return true; } catch (e) { KazumiLogger().e('Bangumi sync failed', error: e); diff --git a/lib/services/sync/webdav.dart b/lib/services/sync/webdav.dart index d4630b548..b411c99fc 100644 --- a/lib/services/sync/webdav.dart +++ b/lib/services/sync/webdav.dart @@ -43,7 +43,7 @@ class WebDav { setting.get(SettingBoxKey.webDavPassword, defaultValue: ''); if (webDavURL.isEmpty) { //KazumiLogger().log(Level.warning, 'WebDAV URL is not set'); - throw Exception('请先填写WebDAV URL'); + throw Exception('Please fill in the WebDAV URL first'); } client = webdav.newClient( webDavURL, diff --git a/lib/services/update/auto_updater.dart b/lib/services/update/auto_updater.dart index bdec5a70a..98e98af20 100644 --- a/lib/services/update/auto_updater.dart +++ b/lib/services/update/auto_updater.dart @@ -111,7 +111,7 @@ class AutoUpdater { final data = await _githubClient.latestRelease(); if (!data.containsKey('tag_name')) { - throw Exception('无效的响应数据'); + throw Exception('Invalid response data'); } final remoteVersion = data['tag_name'] as String; @@ -122,7 +122,7 @@ class AutoUpdater { return UpdateInfo( version: remoteVersion, - description: data['body'] ?? '发现新版本', + description: data['body'] ?? 'New version available', downloadUrl: '', // 将在用户选择安装类型后填充 releaseNotes: data['html_url'] ?? '', @@ -165,10 +165,10 @@ class AutoUpdater { if (updateInfo != null) { _showUpdateDialog(updateInfo, isAutoCheck: false); } else { - KazumiDialog.showToast(message: '当前已经是最新版本!'); + KazumiDialog.showToast(message: 'You are already on the latest version!'); } } catch (e) { - KazumiDialog.showToast(message: '检查更新失败'); + KazumiDialog.showToast(message: 'Failed to check for updates'); } } @@ -177,7 +177,7 @@ class AutoUpdater { KazumiDialog.show( builder: (context) { return AlertDialog( - title: Text('发现新版本 ${updateInfo.version}'), + title: Text('New version ${updateInfo.version} available'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -187,7 +187,7 @@ class AutoUpdater { if (updateInfo.publishedAt.isNotEmpty) ...[ const SizedBox(height: 8), Text( - '发布时间: ${formatDate(updateInfo.publishedAt)}', + 'Released: ${formatDate(updateInfo.publishedAt)}', style: Theme.of(context).textTheme.bodySmall, ), ], @@ -204,7 +204,7 @@ class AutoUpdater { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '选择安装类型:', + 'Select installation type:', style: Theme.of(context).textTheme.labelSmall, ), const SizedBox(height: 8), @@ -276,10 +276,10 @@ class AutoUpdater { onPressed: () { setting.put(SettingBoxKey.autoUpdate, false); KazumiDialog.dismiss(); - KazumiDialog.showToast(message: '已关闭自动更新'); + KazumiDialog.showToast(message: 'Auto update disabled'); }, child: Text( - '关闭自动更新', + 'Disable auto update', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), @@ -287,7 +287,7 @@ class AutoUpdater { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '稍后提醒', + 'Remind me later', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -297,7 +297,7 @@ class AutoUpdater { launchUrl(Uri.parse(updateInfo.releaseNotes), mode: LaunchMode.externalApplication); }, - child: const Text('查看详情'), + child: const Text('View details'), ), TextButton( onPressed: () { @@ -308,7 +308,7 @@ class AutoUpdater { updateInfo, updateInfo.availableInstallationTypes.first); } }, - child: const Text('立即更新'), + child: const Text('Update now'), ), ], ); @@ -320,21 +320,21 @@ class AutoUpdater { String _getInstallationTypeDescription(InstallationType type) { switch (type) { case InstallationType.windowsMsix: - return 'Windows MSIX 包'; + return 'Windows MSIX package'; case InstallationType.windowsPortable: - return 'Windows 便携版 (ZIP)'; + return 'Windows portable (ZIP)'; case InstallationType.linuxDeb: - return 'Linux DEB 包'; + return 'Linux DEB package'; case InstallationType.linuxTar: - return 'Linux TAR 包'; + return 'Linux TAR package'; case InstallationType.macosDmg: - return 'macOS DMG 镜像'; + return 'macOS DMG image'; case InstallationType.androidApk: return 'Android APK'; case InstallationType.ios: return 'iOS ipa'; case InstallationType.unknown: - return '未知安装类型'; + return 'Unknown installation type'; } } @@ -359,7 +359,7 @@ class AutoUpdater { if (downloadUrl.isEmpty) { KazumiDialog.showToast( message: - '没有找到 ${_getInstallationTypeDescription(selectedType)} 的下载链接'); + 'No download link found for ${_getInstallationTypeDescription(selectedType)}'); return; } @@ -381,7 +381,7 @@ class AutoUpdater { _downloadUpdate(downloadInfo, expectedHash); } catch (e) { - KazumiDialog.showToast(message: '下载失败: ${e.toString()}'); + KazumiDialog.showToast(message: 'Download failed: ${e.toString()}'); KazumiLogger().e('Update: download update failed', error: e); } } @@ -390,7 +390,7 @@ class AutoUpdater { Future _downloadUpdate( UpdateInfo updateInfo, String expectedHash) async { if (updateInfo.downloadUrl.isEmpty) { - KazumiDialog.showToast(message: '没有找到合适的下载链接'); + KazumiDialog.showToast(message: 'No suitable download link found'); return; } @@ -399,7 +399,7 @@ class AutoUpdater { clickMaskDismiss: false, builder: (context) { return AlertDialog( - title: const Text('正在下载更新'), + title: const Text('Downloading update'), content: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -423,7 +423,7 @@ class AutoUpdater { _cancelDownload(); KazumiDialog.dismiss(); }, - child: const Text('取消'), + child: const Text('Cancel'), ), ], ); @@ -440,22 +440,22 @@ class AutoUpdater { KazumiDialog.dismiss(); // 显示详细的错误信息 - String errorMessage = '下载失败'; + String errorMessage = 'Download failed'; if (e.toString().contains('Permission denied') || e.toString().contains('Operation not permitted')) { - errorMessage = '权限不足,文件已保存到应用临时目录'; + errorMessage = 'Insufficient permissions, the file has been saved to the app temp directory'; } else if (e.toString().contains('No space left')) { - errorMessage = '磁盘空间不足'; + errorMessage = 'Insufficient disk space'; } else if (e.toString().contains('Network')) { - errorMessage = '网络连接错误'; - } else if (e.toString().contains('文件完整性验证失败')) { - errorMessage = '文件完整性验证失败,可能是网络传输错误'; + errorMessage = 'Network connection error'; + } else if (e.toString().contains('File integrity verification failed')) { + errorMessage = 'File integrity verification failed, possibly due to a network transmission error'; } KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('下载失败'), + title: const Text('Download failed'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -463,7 +463,7 @@ class AutoUpdater { Text(errorMessage), const SizedBox(height: 8), Text( - '错误详情: ${e.toString()}', + 'Error details: ${e.toString()}', style: Theme.of(context).textTheme.bodySmall, ), ], @@ -471,7 +471,7 @@ class AutoUpdater { actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(), - child: const Text('确定'), + child: const Text('OK'), ), TextButton( onPressed: () { @@ -479,7 +479,7 @@ class AutoUpdater { // 重新尝试下载 _downloadUpdate(updateInfo, expectedHash); }, - child: const Text('重试'), + child: const Text('Retry'), ), ], ); @@ -505,7 +505,7 @@ class AutoUpdater { KazumiDialog.show( builder: (context) { return AlertDialog( - title: const Text('下载完成'), + title: const Text('Download complete'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -519,13 +519,13 @@ class AutoUpdater { ), const SizedBox(width: 8), Expanded( - child: Text('新版本 ${updateInfo.version} 已下载完成'), + child: Text('New version ${updateInfo.version} has been downloaded'), ), ], ), const SizedBox(height: 12), Text( - '安装过程中应用将会退出', + 'The app will exit during installation', style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 12, @@ -542,7 +542,7 @@ class AutoUpdater { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '文件位置:', + 'File location:', style: Theme.of(context).textTheme.labelSmall, ), const SizedBox(height: 4), @@ -561,7 +561,7 @@ class AutoUpdater { TextButton( onPressed: () => KazumiDialog.dismiss(), child: Text( - '稍后安装', + 'Install later', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), @@ -571,7 +571,7 @@ class AutoUpdater { // 在文件管理器中显示文件 _revealInFileManager(filePath); }, - child: const Text('打开文件夹'), + child: const Text('Open folder'), ), TextButton( onPressed: () { @@ -579,7 +579,7 @@ class AutoUpdater { _installUpdate( filePath, updateInfo.recommendedInstallationType); }, - child: const Text('立即安装'), + child: const Text('Install now'), ), ], ); @@ -643,7 +643,7 @@ class AutoUpdater { if (downloadedHash != expectedHash) { // 哈希不匹配,删除文件并抛出异常 await file.delete(); - throw Exception('文件完整性验证失败: 期望 $expectedHash,实际 $downloadedHash'); + throw Exception('File integrity verification failed: expected $expectedHash, got $downloadedHash'); } KazumiLogger().i('Update: file downloaded and hash verified: $filePath'); @@ -655,7 +655,7 @@ class AutoUpdater { String filePath, InstallationType installationType) async { try { // 显示准备退出的提示 - KazumiDialog.showToast(message: '准备安装更新,应用即将退出...'); + KazumiDialog.showToast(message: 'Preparing to install the update, the app will exit shortly...'); await Future.delayed(const Duration(seconds: 2)); @@ -680,12 +680,12 @@ class AutoUpdater { } else if (Platform.isAndroid) { final result = await OpenFilex.open(filePath); if (result.type != ResultType.done) { - KazumiDialog.showToast(message: '无法打开安装文件: ${result.message}'); + KazumiDialog.showToast(message: 'Cannot open installation file: ${result.message}'); return; } } } catch (e) { - KazumiDialog.showToast(message: '启动安装程序失败: ${e.toString()}'); + KazumiDialog.showToast(message: 'Failed to launch the installer: ${e.toString()}'); KazumiLogger().e('Update: launch installer failed', error: e); } } @@ -699,7 +699,7 @@ class AutoUpdater { // 如果传入的本来就是目录则打开这个目录 // 如果是文件则打开包含它的目录 if (type == FileSystemEntityType.notFound) { - KazumiDialog.showToast(message: '文件或目录不存在'); + KazumiDialog.showToast(message: 'File or directory does not exist'); return; } else if (type == FileSystemEntityType.directory) { targetDirOrFile = filePath; @@ -726,10 +726,10 @@ class AutoUpdater { // 尝试打开包含文件的文件夹 await Process.start('xdg-open', [targetDirOrFile]); } else { - KazumiDialog.showToast(message: '此平台不支持通过此方法打开文件管理器'); + KazumiDialog.showToast(message: 'This platform does not support opening the file manager this way'); } } catch (e) { - KazumiDialog.showToast(message: '无法打开文件管理器'); + KazumiDialog.showToast(message: 'Cannot open the file manager'); KazumiLogger().w('Update: reveal in file manager failed', error: e); } finally { try { diff --git a/lib/utils/anime_season.dart b/lib/utils/anime_season.dart index 8ee07653d..fd86adddd 100644 --- a/lib/utils/anime_season.dart +++ b/lib/utils/anime_season.dart @@ -1,7 +1,7 @@ /// This class asks for DateTime to get a string to indicate seasonal anime class AnimeSeason { late DateTime _date; - final _seasons = ['冬季', '春季', '夏季', '秋季']; + final _seasons = ['Winter', 'Spring', 'Summer', 'Autumn']; AnimeSeason(DateTime date) { _date = date; @@ -48,15 +48,15 @@ class AnimeSeason { String toString() { var yas = _getYearAndSeason(_date); - return '${yas[0]}年${_seasons[yas[1]]}新番'; + return '${yas[0]} ${_seasons[yas[1]]} anime'; } } String getSeasonStringByMonth(int month) { - if (month <= 3) return '冬'; - if (month <= 6) return '春'; - if (month <= 9) return '夏'; - return '秋'; + if (month <= 3) return 'Winter'; + if (month <= 6) return 'Spring'; + if (month <= 9) return 'Summer'; + return 'Autumn'; } bool isSameSeason(DateTime d1, DateTime d2) { diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 46d692dd0..7457f9c64 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -97,39 +97,39 @@ Map bangumiHTTPHeader = { /// 可选硬件解码器 const Map hardwareDecodersList = { - 'auto': '启用任意可用解码器', - 'auto-safe': '启用最佳解码器', - 'auto-copy': '启用带拷贝功能的最佳解码器', - 'd3d11va': 'DirectX11 (windows8 及以上)', - 'd3d11va-copy': 'DirectX11 (windows8 及以上) (非直通)', + 'auto': 'Enable any available decoder', + 'auto-safe': 'Enable the best decoder', + 'auto-copy': 'Enable the best decoder with copy support', + 'd3d11va': 'DirectX11 (Windows 8 and above)', + 'd3d11va-copy': 'DirectX11 (Windows 8 and above) (non-passthrough)', 'videotoolbox': 'VideoToolbox (macOS / iOS)', - 'videotoolbox-copy': 'VideoToolbox (macOS / iOS) (非直通)', + 'videotoolbox-copy': 'VideoToolbox (macOS / iOS) (non-passthrough)', 'vaapi': 'VAAPI (Linux)', - 'vaapi-copy': 'VAAPI (Linux) (非直通)', - 'nvdec': 'NVDEC (NVIDIA独占)', - 'nvdec-copy': 'NVDEC (NVIDIA独占) (非直通)', + 'vaapi-copy': 'VAAPI (Linux) (non-passthrough)', + 'nvdec': 'NVDEC (NVIDIA only)', + 'nvdec-copy': 'NVDEC (NVIDIA only) (non-passthrough)', 'drm': 'DRM (Linux)', - 'drm-copy': 'DRM (Linux) (非直通)', - 'vulkan': 'Vulkan (全平台) (实验性)', - 'vulkan-copy': 'Vulkan (全平台) (实验性) (非直通)', - 'dxva2': 'DXVA2 (Windows7 及以上)', - 'dxva2-copy': 'DXVA2 (Windows7 及以上) (非直通)', + 'drm-copy': 'DRM (Linux) (non-passthrough)', + 'vulkan': 'Vulkan (all platforms) (experimental)', + 'vulkan-copy': 'Vulkan (all platforms) (experimental) (non-passthrough)', + 'dxva2': 'DXVA2 (Windows 7 and above)', + 'dxva2-copy': 'DXVA2 (Windows 7 and above) (non-passthrough)', 'vdpau': 'VDPAU (Linux)', - 'vdpau-copy': 'VDPAU (Linux) (非直通)', + 'vdpau-copy': 'VDPAU (Linux) (non-passthrough)', 'mediacodec': 'MediaCodec (Android)', - 'mediacodec-copy': 'MediaCodec (Android) (非直通)', - 'cuda': 'CUDA (NVIDIA独占) (过时)', - 'cuda-copy': 'CUDA (NVIDIA独占) (过时) (非直通)', - 'crystalhd': 'CrystalHD (全平台) (过时)', - 'rkmpp': 'Rockchip MPP (仅部分Rockchip芯片)', + 'mediacodec-copy': 'MediaCodec (Android) (non-passthrough)', + 'cuda': 'CUDA (NVIDIA only) (deprecated)', + 'cuda-copy': 'CUDA (NVIDIA only) (deprecated) (non-passthrough)', + 'crystalhd': 'CrystalHD (all platforms) (deprecated)', + 'rkmpp': 'Rockchip MPP (only some Rockchip chips)', }; /// Android 可选视频渲染器 const Map androidVideoRenderersList = { - 'auto': '自动选择', - 'gpu': '基于 OpenGL, 通用和稳健的选项', - 'gpu-next': '基于 Vulkan, 在新设备上表现最好', - 'mediacodec_embed': '功耗最低,不支持超分辨率', + 'auto': 'Auto select', + 'gpu': 'OpenGL-based, a general and robust option', + 'gpu-next': 'Vulkan-based, performs best on newer devices', + 'mediacodec_embed': 'Lowest power usage, does not support super resolution', }; /// 超分辨率滤镜 @@ -178,39 +178,39 @@ const String danmakuOnSvg = ''' /// 可选默认视频比例 const Map aspectRatioTypeMap = { - 1: "自动", - 2: "裁切填充", - 3: "拉伸填充", + 1: "Auto", + 2: "Crop to fill", + 3: "Stretch to fill", }; /// 可选播放器日志等级 /// LogLevel 0: 错误 1: 警告 2: 简略 3: 详细 4: 调试(隐藏) 5: 全部(隐藏) const Map playerLogLevelMap = { - 0: "错误", - 1: "警告", - 2: "简略", - 3: "详细", + 0: "Error", + 1: "Warning", + 2: "Brief", + 3: "Verbose", // 以下两个级别被MPV官方支持,但是输出内容过于冗长,暂时隐藏 // 4: "调试", // 5: "全部", }; final List defaultAnimeTags = const [ - '日常', - '原创', - '校园', - '搞笑', - '奇幻', - '百合', - '恋爱', - '悬疑', - '热血', - '后宫', - '机战', - '轻改', - '偶像', - '治愈', - '异世界', + 'Slice of life', + 'Original', + 'School', + 'Comedy', + 'Fantasy', + 'Yuri', + 'Romance', + 'Mystery', + 'Hot-blooded', + 'Harem', + 'Mecha', + 'Light novel adaptation', + 'Idol', + 'Healing', + 'Isekai', ]; // 播放器默认快捷键 @@ -237,35 +237,35 @@ final Map> defaultShortcuts = const { // 键位别名 final Map keyAliases = { - ' ': '空格', + ' ': 'Space', 'Arrow Up': '↑', 'Arrow Down': '↓', 'Arrow Left': '←', 'Arrow Right': '→', - 'Enter': '回车', + 'Enter': 'Enter', 'Tab': 'Tab', 'Escape': 'Esc', - 'Backspace': '退格', + 'Backspace': 'Backspace', }; //功能中文名对应 final Map shortcutsChineseName = { - 'playorpause': '播放 / 暂停', - 'forward': '快进 / 长按倍速', - 'rewind': '快退', - 'next': '下一集', - 'prev': '上一集', - 'volumeup': '音量加', - 'volumedown': '音量减', - 'togglemute': '静音', - 'fullscreen': '全屏', - 'exitfullscreen': '退出全屏', - 'toggledanmaku': '弹幕开关', - 'screenshot': '截图', - 'skip': '跳过', - 'speed1': '倍速:1x', - 'speed2': '倍速:2x', - 'speed3': '倍速:3x', - 'speedup': '倍速加', - 'speeddown': '倍速减', + 'playorpause': 'Play / Pause', + 'forward': 'Fast forward / long press for speed', + 'rewind': 'Rewind', + 'next': 'Next episode', + 'prev': 'Previous episode', + 'volumeup': 'Volume up', + 'volumedown': 'Volume down', + 'togglemute': 'Mute', + 'fullscreen': 'Fullscreen', + 'exitfullscreen': 'Exit fullscreen', + 'toggledanmaku': 'Toggle danmaku', + 'screenshot': 'Screenshot', + 'skip': 'Skip', + 'speed1': 'Speed: 1x', + 'speed2': 'Speed: 2x', + 'speed3': 'Speed: 3x', + 'speedup': 'Speed up', + 'speeddown': 'Speed down', }; diff --git a/lib/utils/date_time.dart b/lib/utils/date_time.dart index 30494947d..eb485f83d 100644 --- a/lib/utils/date_time.dart +++ b/lib/utils/date_time.dart @@ -3,24 +3,24 @@ String formatTimestampToRelativeTime(int timeStamp) { .difference(DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000)); if (difference.inDays > 365) { - return '${difference.inDays ~/ 365}年前'; + return '${difference.inDays ~/ 365}y ago'; } else if (difference.inDays > 30) { - return '${difference.inDays ~/ 30}个月前'; + return '${difference.inDays ~/ 30}mo ago'; } else if (difference.inDays > 0) { - return '${difference.inDays}天前'; + return '${difference.inDays}d ago'; } else if (difference.inHours > 0) { - return '${difference.inHours}小时前'; + return '${difference.inHours}h ago'; } else if (difference.inMinutes > 0) { - return '${difference.inMinutes}分钟前'; + return '${difference.inMinutes}m ago'; } - return '刚刚'; + return 'Just now'; } String dateFormat(int timeStamp, {String formatType = 'list'}) { final time = (DateTime.now().millisecondsSinceEpoch / 1000).round(); final distance = time - timeStamp; - var currentYearStr = 'MM月DD日 hh:mm'; - var lastYearStr = 'YY年MM月DD日 hh:mm'; + var currentYearStr = 'MM-DD hh:mm'; + var lastYearStr = 'YY-MM-DD hh:mm'; if (formatType == 'detail') { currentYearStr = 'MM-DD hh:mm'; lastYearStr = 'YY-MM-DD hh:mm'; @@ -32,11 +32,11 @@ String dateFormat(int timeStamp, {String formatType = 'list'}) { ); } if (distance <= 60) { - return '刚刚'; + return 'Just now'; } else if (distance <= 3600) { - return '${(distance / 60).floor()}分钟前'; + return '${(distance / 60).floor()}m ago'; } else if (distance <= 43200) { - return '${(distance / 60 / 60).floor()}小时前'; + return '${(distance / 60 / 60).floor()}h ago'; } else if (DateTime.fromMillisecondsSinceEpoch(time * 1000).year == DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000).year) { return _customTimestampString( @@ -94,7 +94,7 @@ String _customTimestampString({ if (int.parse(yy) == DateTime.now().year && int.parse(mm) == DateTime.now().month && int.parse(dd) == DateTime.now().day) { - return '今天'; + return 'Today'; } return formatted; } diff --git a/lib/webview/captcha/impl/captcha_webview_linux_impl.dart b/lib/webview/captcha/impl/captcha_webview_linux_impl.dart index d28e7e3d2..c380b96dd 100644 --- a/lib/webview/captcha/impl/captcha_webview_linux_impl.dart +++ b/lib/webview/captcha/impl/captcha_webview_linux_impl.dart @@ -39,7 +39,7 @@ class CaptchaWebviewLinuxImpl extends CaptchaWebviewController { if (parsed == null) return null; final (host, port) = parsed; - KazumiLogger().i('[Captcha WebView] 代理设置成功 $host:$port'); + KazumiLogger().i('[Captcha WebView] proxy set successfully $host:$port'); return ProxyConfiguration(host: host, port: port); } diff --git a/lib/webview/captcha/impl/captcha_webview_windows_impl.dart b/lib/webview/captcha/impl/captcha_webview_windows_impl.dart index aa27e5d10..e85860c38 100644 --- a/lib/webview/captcha/impl/captcha_webview_windows_impl.dart +++ b/lib/webview/captcha/impl/captcha_webview_windows_impl.dart @@ -488,9 +488,9 @@ $script await WebviewController.initializeEnvironment( additionalArguments: '--proxy-server=$formattedProxy', ); - KazumiLogger().i('[Captcha WebView] 代理设置成功 $formattedProxy'); + KazumiLogger().i('[Captcha WebView] proxy set successfully $formattedProxy'); } catch (e) { - KazumiLogger().e('[Captcha WebView] 设置代理失败 $e'); + KazumiLogger().e('[Captcha WebView] failed to set proxy $e'); } } } diff --git a/lib/webview/video/impl/video_webview_android_impl.dart b/lib/webview/video/impl/video_webview_android_impl.dart index 5a2730b83..fdbb5284d 100644 --- a/lib/webview/video/impl/video_webview_android_impl.dart +++ b/lib/webview/video/impl/video_webview_android_impl.dart @@ -308,7 +308,7 @@ class VideoWebviewAndroidImpl await android_webview.AndroidWebViewFeature.instance() .isFeatureSupported(WebViewFeature.PROXY_OVERRIDE); if (!proxyAvailable) { - KazumiLogger().w('WebView: 当前 Android 版本不支持代理'); + KazumiLogger().w('WebView: the current Android version does not support proxies'); return; } @@ -321,9 +321,9 @@ class VideoWebviewAndroidImpl ], ), ); - KazumiLogger().i('WebView: 代理设置成功 $formattedProxy'); + KazumiLogger().i('WebView: proxy set successfully $formattedProxy'); } catch (e) { - KazumiLogger().e('WebView: 设置代理失败 $e'); + KazumiLogger().e('WebView: failed to set proxy $e'); } } } diff --git a/lib/webview/video/impl/video_webview_impl.dart b/lib/webview/video/impl/video_webview_impl.dart index 285f90a03..2b8c4401c 100644 --- a/lib/webview/video/impl/video_webview_impl.dart +++ b/lib/webview/video/impl/video_webview_impl.dart @@ -494,7 +494,7 @@ class VideoWebviewImpl await android_webview.AndroidWebViewFeature.instance() .isFeatureSupported(WebViewFeature.PROXY_OVERRIDE); if (!proxyAvailable) { - KazumiLogger().w('WebView: 当前 Android 版本不支持代理'); + KazumiLogger().w('WebView: the current Android version does not support proxies'); return; } @@ -507,9 +507,9 @@ class VideoWebviewImpl ], ), ); - KazumiLogger().i('WebView: 代理设置成功 $formattedProxy'); + KazumiLogger().i('WebView: proxy set successfully $formattedProxy'); } catch (e) { - KazumiLogger().e('WebView: 设置代理失败 $e'); + KazumiLogger().e('WebView: failed to set proxy $e'); } } } diff --git a/lib/webview/video/impl/video_webview_linux_impl.dart b/lib/webview/video/impl/video_webview_linux_impl.dart index 2d1cc2775..bce82cb60 100644 --- a/lib/webview/video/impl/video_webview_linux_impl.dart +++ b/lib/webview/video/impl/video_webview_linux_impl.dart @@ -52,7 +52,7 @@ class VideoWebviewLinuxImpl extends VideoWebviewController { } final (host, port) = parsed; - KazumiLogger().i('WebView: 代理设置成功 $host:$port'); + KazumiLogger().i('WebView: proxy set successfully $host:$port'); return ProxyConfiguration(host: host, port: port); } diff --git a/lib/webview/video/impl/video_webview_windows_impl.dart b/lib/webview/video/impl/video_webview_windows_impl.dart index e024b3aae..d6e9d5dc3 100644 --- a/lib/webview/video/impl/video_webview_windows_impl.dart +++ b/lib/webview/video/impl/video_webview_windows_impl.dart @@ -39,9 +39,9 @@ class VideoWebviewWindowsImpl await WebviewController.initializeEnvironment( additionalArguments: '--proxy-server=$formattedProxy', ); - KazumiLogger().i('WebView: 代理设置成功 $formattedProxy'); + KazumiLogger().i('WebView: proxy set successfully $formattedProxy'); } catch (e) { - KazumiLogger().e('WebView: 设置代理失败 $e'); + KazumiLogger().e('WebView: failed to set proxy $e'); } } From 35f5f4834c6dde9c18a33de9abb75a54a16f8506 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Sun, 31 May 2026 00:30:17 +0000 Subject: [PATCH 2/4] Force dynamic content to English and rename app to Akiora Force English (dynamic content): - Add TranslationService: on-device machine translation (keyless endpoint) with in-memory + Hive-persistent caching, proxy-aware Dio, graceful fallback - Add TranslatedText widget that translates at display time (data untouched) - Translate anime titles in popular/search/collect grids, info page, history, and timeline cards; translate the info-page summary - Add 'Force English' toggle in Interface settings (default on) - Underlying nameCn/name kept intact so plugin search & storage still work Rename app to Akiora (display name only): - Dart: window/app title, app bar, exit dialog, tray tooltip/menu, X11 and shortcut prompts, audio notification channel, plugin version message - Native: Android label, iOS/macOS display name, Windows window title + resource metadata, Linux .desktop Name, web title/manifest - Kept functional identifiers unchanged: Kazumi package name, KazumiLogger/ KazumiDialog classes, update asset prefix (matches upstream releases), Linux tray icon id, bundle/application ids --- android/app/src/main/AndroidManifest.xml | 2 +- .../linux/io.github.Predidit.Kazumi.desktop | 2 +- ios/Runner/Info.plist | 2 +- lib/app_widget.dart | 10 +- lib/bean/card/bangumi_card.dart | 3 +- lib/bean/card/bangumi_history_card.dart | 3 +- lib/bean/card/bangumi_info_card.dart | 3 +- lib/bean/card/bangumi_timeline_card.dart | 5 +- lib/bean/widget/translated_text.dart | 88 ++++++++++++ lib/main.dart | 2 +- lib/pages/about/about_module.dart | 2 +- lib/pages/about/about_page.dart | 2 +- lib/pages/index_module.dart | 2 +- lib/pages/info/info_page.dart | 3 +- lib/pages/info/info_tabview.dart | 26 +++- lib/pages/init_page.dart | 4 +- lib/pages/plugin_editor/plugin_shop_page.dart | 4 +- lib/pages/plugin_editor/plugin_view_page.dart | 2 +- lib/pages/settings/interface_settings.dart | 19 +++ lib/request/config/api_endpoints.dart | 4 + lib/request/core/dio_factory.dart | 13 ++ lib/services/player/audio_controller.dart | 4 +- lib/services/storage/storage.dart | 7 +- .../translation/translation_service.dart | 129 ++++++++++++++++++ macos/Runner/Info.plist | 2 + web/index.html | 4 +- web/manifest.json | 4 +- windows/runner/Runner.rc | 4 +- windows/runner/main.cpp | 4 +- 29 files changed, 322 insertions(+), 37 deletions(-) create mode 100644 lib/bean/widget/translated_text.dart create mode 100644 lib/services/translation/translation_service.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a6d9c79bc..d1723ff2f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Kazumi + Akiora CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/lib/app_widget.dart b/lib/app_widget.dart index 534a5873e..d11761f85 100644 --- a/lib/app_widget.dart +++ b/lib/app_widget.dart @@ -220,7 +220,7 @@ class _AppWidgetState extends State mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text('Do you want to exit Kazumi?'), + const Text('Do you want to exit Akiora?'), const SizedBox(height: 24), StatefulBuilder(builder: (context, setState) { onChanged(value) { @@ -247,7 +247,7 @@ class _AppWidgetState extends State } exit(0); }, - child: const Text('Exit Kazumi')), + child: const Text('Exit Akiora')), TextButton( onPressed: () async { if (saveExitBehavior) { @@ -304,13 +304,13 @@ class _AppWidgetState extends State } if (!Platform.isLinux) { - await trayManager.setToolTip('Kazumi'); + await trayManager.setToolTip('Akiora'); } Menu trayMenu = Menu(items: [ MenuItem(key: 'show_window', label: 'Show window'), MenuItem.separator(), - MenuItem(key: 'exit', label: 'Exit Kazumi') + MenuItem(key: 'exit', label: 'Exit Akiora') ]); await trayManager.setContextMenu(trayMenu); } @@ -344,7 +344,7 @@ class _AppWidgetState extends State : dynamicDarkTheme; return MaterialApp.router( - title: "Kazumi", + title: "Akiora", localizationsDelegates: GlobalMaterialLocalizations.delegates, supportedLocales: const [Locale('en')], locale: const Locale('en'), diff --git a/lib/bean/card/bangumi_card.dart b/lib/bean/card/bangumi_card.dart index 83fde595f..1e83172b4 100644 --- a/lib/bean/card/bangumi_card.dart +++ b/lib/bean/card/bangumi_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:kazumi/bean/widget/translated_text.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; import 'package:kazumi/bean/dialog/dialog_helper.dart'; @@ -90,7 +91,7 @@ class BangumiContent extends StatelessWidget { return Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(5, 3, 5, 1), - child: Text( + child: TranslatedText( bangumiItem.nameCn, textAlign: TextAlign.start, style: const TextStyle( diff --git a/lib/bean/card/bangumi_history_card.dart b/lib/bean/card/bangumi_history_card.dart index 004c77342..7c2c84d97 100644 --- a/lib/bean/card/bangumi_history_card.dart +++ b/lib/bean/card/bangumi_history_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:kazumi/bean/widget/translated_text.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; @@ -141,7 +142,7 @@ class _BangumiHistoryCardVState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + TranslatedText( title, style: theme.textTheme.titleSmall?.copyWith( color: colorScheme.onSurface, diff --git a/lib/bean/card/bangumi_info_card.dart b/lib/bean/card/bangumi_info_card.dart index 139ca06ff..70cd78a43 100644 --- a/lib/bean/card/bangumi_info_card.dart +++ b/lib/bean/card/bangumi_info_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:kazumi/bean/widget/translated_text.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; import 'package:kazumi/utils/constants.dart'; @@ -124,7 +125,7 @@ class _BangumiInfoCardVState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + TranslatedText( widget.bangumiItem.nameCn == '' ? widget.bangumiItem.name : (widget.bangumiItem.nameCn), diff --git a/lib/bean/card/bangumi_timeline_card.dart b/lib/bean/card/bangumi_timeline_card.dart index e66cedee6..23f5d5d7e 100644 --- a/lib/bean/card/bangumi_timeline_card.dart +++ b/lib/bean/card/bangumi_timeline_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:kazumi/bean/widget/translated_text.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/modules/bangumi/bangumi_item.dart'; import 'package:kazumi/bean/card/network_img_layer.dart'; @@ -131,7 +132,7 @@ class BangumiTimelineCard extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + TranslatedText( title, style: nameStyle, maxLines: useWideLayout ? 2 : 1, @@ -143,7 +144,7 @@ class BangumiTimelineCard extends StatelessWidget { child: Padding( padding: EdgeInsets.only(top: supportingText.isNotEmpty ? 6 : 0), child: supportingText.isNotEmpty - ? Text( + ? TranslatedText( supportingText, style: subStyle, maxLines: supportingLines, diff --git a/lib/bean/widget/translated_text.dart b/lib/bean/widget/translated_text.dart new file mode 100644 index 000000000..4a6a88395 --- /dev/null +++ b/lib/bean/widget/translated_text.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:kazumi/services/translation/translation_service.dart'; + +/// A drop-in replacement for [Text] that force-translates dynamic content +/// (e.g. anime titles, summaries from the Bangumi API) to English at display +/// time when the "force English" feature is enabled. +/// +/// The original text is shown immediately; once the (cached) translation is +/// available the widget rebuilds with the English text. The underlying data is +/// never mutated. +class TranslatedText extends StatefulWidget { + const TranslatedText( + this.data, { + super.key, + this.style, + this.textAlign, + this.overflow, + this.maxLines, + this.softWrap, + this.textScaler, + }); + + final String data; + final TextStyle? style; + final TextAlign? textAlign; + final TextOverflow? overflow; + final int? maxLines; + final bool? softWrap; + final TextScaler? textScaler; + + @override + State createState() => _TranslatedTextState(); +} + +class _TranslatedTextState extends State { + late String _display; + + @override + void initState() { + super.initState(); + _resolve(); + } + + @override + void didUpdateWidget(covariant TranslatedText oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.data != widget.data) { + _resolve(); + } + } + + void _resolve() { + final service = TranslationService.instance; + if (!service.enabled || !service.needsTranslation(widget.data)) { + _display = widget.data; + return; + } + + final cached = service.cached(widget.data); + if (cached != null) { + _display = cached; + return; + } + + // Show the original first, then translate asynchronously. + _display = widget.data; + final source = widget.data; + service.translateToEnglish(source).then((translated) { + if (!mounted || translated == null) return; + if (widget.data != source) return; // widget recycled to a new value + if (translated == _display) return; + setState(() => _display = translated); + }); + } + + @override + Widget build(BuildContext context) { + return Text( + _display, + style: widget.style, + textAlign: widget.textAlign, + overflow: widget.overflow, + maxLines: widget.maxLines, + softWrap: widget.softWrap, + textScaler: widget.textScaler, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 067eab5fb..3c74cbe0a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -73,7 +73,7 @@ void main() async { ? TitleBarStyle.hidden : TitleBarStyle.normal, windowButtonVisibility: showWindowButton, - title: 'Kazumi', + title: 'Akiora', ); windowManager.waitUntilReadyToShow(windowOptions, () async { // window_manager controls desktop visibility to avoid startup flicker. diff --git a/lib/pages/about/about_module.dart b/lib/pages/about/about_module.dart index d4de79b07..3c73fbfc8 100644 --- a/lib/pages/about/about_module.dart +++ b/lib/pages/about/about_module.dart @@ -15,7 +15,7 @@ class AboutModule extends Module { r.child( "/license", child: (_) => const LicensePage( - applicationName: 'Kazumi', + applicationName: 'Akiora', applicationVersion: ApiEndpoints.version, applicationLegalese: 'Open source licenses', ), diff --git a/lib/pages/about/about_page.dart b/lib/pages/about/about_page.dart index 8df104316..67ed20dcd 100644 --- a/lib/pages/about/about_page.dart +++ b/lib/pages/about/about_page.dart @@ -22,7 +22,7 @@ class AboutPage extends StatefulWidget { } class _AboutPageState extends State { - final exitBehaviorTitles = ['Exit Kazumi', 'Minimize to tray', 'Always ask']; + final exitBehaviorTitles = ['Exit Akiora', 'Minimize to tray', 'Always ask']; late dynamic defaultDanmakuArea; late dynamic defaultThemeMode; late dynamic defaultThemeColor; diff --git a/lib/pages/index_module.dart b/lib/pages/index_module.dart index 235d67df3..e1a1ecd0e 100644 --- a/lib/pages/index_module.dart +++ b/lib/pages/index_module.dart @@ -60,7 +60,7 @@ class IndexModule extends Module { ChildRoute( "/error", child: (_) => Scaffold( - appBar: AppBar(title: const Text("Kazumi")), + appBar: AppBar(title: const Text("Akiora")), body: const Center(child: Text("Initialization failed")), ), ), diff --git a/lib/pages/info/info_page.dart b/lib/pages/info/info_page.dart index c808bca08..f466ed51b 100644 --- a/lib/pages/info/info_page.dart +++ b/lib/pages/info/info_page.dart @@ -5,6 +5,7 @@ import 'package:kazumi/pages/info/rating_review_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/bean/widget/collect_button.dart'; +import 'package:kazumi/bean/widget/translated_text.dart'; import 'package:kazumi/bean/widget/embedded_native_control_area.dart'; import 'package:kazumi/utils/constants.dart'; import 'package:kazumi/services/storage/storage.dart'; @@ -353,7 +354,7 @@ class _InfoPageState extends State with TickerProviderStateMixin { child: Container( width: double.infinity, alignment: Alignment.centerLeft, - child: Text( + child: TranslatedText( infoController.bangumiItem.nameCn == '' ? infoController.bangumiItem.name : infoController.bangumiItem.nameCn, diff --git a/lib/pages/info/info_tabview.dart b/lib/pages/info/info_tabview.dart index f24d23c6f..0f9c94c07 100644 --- a/lib/pages/info/info_tabview.dart +++ b/lib/pages/info/info_tabview.dart @@ -11,6 +11,7 @@ import 'package:kazumi/modules/comments/comment_item.dart'; import 'package:kazumi/modules/characters/character_item.dart'; import 'package:kazumi/modules/staff/staff_item.dart'; import 'package:kazumi/utils/device.dart'; +import 'package:kazumi/services/translation/translation_service.dart'; class InfoTabView extends StatefulWidget { const InfoTabView({ @@ -61,6 +62,24 @@ class _InfoTabViewState extends State final maxWidth = 950.0; bool fullIntro = false; bool fullTag = false; + final Set _translatingSummaries = {}; + + /// Returns the summary, force-translated to English when enabled. Shows the + /// original first and rebuilds once the (cached) translation is ready. + String _resolvedSummary() { + final raw = widget.bangumiItem.summary; + final service = TranslationService.instance; + if (!service.enabled || !service.needsTranslation(raw)) return raw; + final cached = service.cached(raw); + if (cached != null) return cached; + if (!_translatingSummaries.contains(raw)) { + _translatingSummaries.add(raw); + service.translateToEnglish(raw).then((_) { + if (mounted) setState(() {}); + }); + } + return raw; + } @override void initState() { @@ -99,7 +118,8 @@ class _InfoTabViewState extends State // https://stackoverflow.com/questions/54091055/flutter-how-to-get-the-number-of-text-lines // only show expand button when line > 7 LayoutBuilder(builder: (context, constraints) { - final span = TextSpan(text: widget.bangumiItem.summary); + final summary = _resolvedSummary(); + final span = TextSpan(text: summary); final tp = TextPainter(text: span, textDirection: TextDirection.ltr); tp.layout(maxWidth: constraints.maxWidth); @@ -115,7 +135,7 @@ class _InfoTabViewState extends State ? maxWidth : MediaQuery.sizeOf(context).width - 32, child: SelectableText( - widget.bangumiItem.summary, + summary, textAlign: TextAlign.start, scrollBehavior: const ScrollBehavior().copyWith( scrollbars: false, @@ -136,7 +156,7 @@ class _InfoTabViewState extends State ); } else { return SelectableText( - widget.bangumiItem.summary, + summary, textAlign: TextAlign.start, scrollPhysics: NeverScrollableScrollPhysics(), selectionHeightStyle: ui.BoxHeightStyle.max, diff --git a/lib/pages/init_page.dart b/lib/pages/init_page.dart index 153e95980..f37980f53 100644 --- a/lib/pages/init_page.dart +++ b/lib/pages/init_page.dart @@ -204,7 +204,7 @@ class _InitPageState extends State { child: AlertDialog( title: const Text('X11 environment detected'), content: const Text( - 'You are currently running under X11. Kazumi may have performance issues or display glitches under X11, and switching to Wayland is recommended for a better experience. Do you want to continue using Kazumi under X11?'), + 'You are currently running under X11. Akiora may have performance issues or display glitches under X11, and switching to Wayland is recommended for a better experience. Do you want to continue using Akiora under X11?'), actions: [ TextButton( onPressed: () { @@ -240,7 +240,7 @@ class _InitPageState extends State { clickMaskDismiss: false, builder: (context) => AlertDialog( title: const Text('Create desktop shortcut'), - content: const Text('Create a Kazumi shortcut on the desktop?'), + content: const Text('Create an Akiora shortcut on the desktop?'), actions: [ TextButton( onPressed: () => KazumiDialog.dismiss(popWith: false), diff --git a/lib/pages/plugin_editor/plugin_shop_page.dart b/lib/pages/plugin_editor/plugin_shop_page.dart index 7437c3ef6..fd67c90b7 100644 --- a/lib/pages/plugin_editor/plugin_shop_page.dart +++ b/lib/pages/plugin_editor/plugin_shop_page.dart @@ -171,7 +171,7 @@ class _PluginShopPageState extends State { setState(() {}); } else if (res == 1) { KazumiDialog.showToast( - message: 'Kazumi version is too low, this rule is not compatible with the current version'); + message: 'Akiora version is too low, this rule is not compatible with the current version'); } else if (res == 2) { KazumiDialog.showToast(message: 'Failed to import rule'); } @@ -186,7 +186,7 @@ class _PluginShopPageState extends State { setState(() {}); } else if (res == 1) { KazumiDialog.showToast( - message: 'Kazumi version is too low, this rule is not compatible with the current version'); + message: 'Akiora version is too low, this rule is not compatible with the current version'); } else if (res == 2) { KazumiDialog.showToast(message: 'Failed to update rule'); } diff --git a/lib/pages/plugin_editor/plugin_view_page.dart b/lib/pages/plugin_editor/plugin_view_page.dart index ff986c6bd..10a3b2ddf 100644 --- a/lib/pages/plugin_editor/plugin_view_page.dart +++ b/lib/pages/plugin_editor/plugin_view_page.dart @@ -412,7 +412,7 @@ class _PluginViewPageState extends State { if (res == 0) { KazumiDialog.showToast(message: 'Update succeeded'); } else if (res == 1) { - KazumiDialog.showToast(message: 'Kazumi version is too low, this rule is not compatible with the current version'); + KazumiDialog.showToast(message: 'Akiora version is too low, this rule is not compatible with the current version'); } else if (res == 2) { KazumiDialog.showToast(message: 'Failed to update rule'); } diff --git a/lib/pages/settings/interface_settings.dart b/lib/pages/settings/interface_settings.dart index d1ae05756..0fb1833c7 100644 --- a/lib/pages/settings/interface_settings.dart +++ b/lib/pages/settings/interface_settings.dart @@ -16,6 +16,7 @@ class InterfaceSettingsPage extends StatefulWidget { class _InterfaceSettingsPageState extends State { Box setting = GStorage.setting; late bool showRating; + late bool forceEnglish; late String defaultPage; final MenuController defaultPageMenuController = MenuController(); @@ -30,6 +31,8 @@ class _InterfaceSettingsPageState extends State { void initState() { super.initState(); showRating = setting.get(SettingBoxKey.showRating, defaultValue: true); + forceEnglish = + setting.get(SettingBoxKey.forceEnglishTranslation, defaultValue: true); defaultPage = setting.get(SettingBoxKey.defaultStartupPage, defaultValue: '/tab/popular/'); } @@ -111,6 +114,22 @@ class _InterfaceSettingsPageState extends State { initialValue: showRating, ), ]), + SettingsSection(tiles: [ + SettingsTile.switchTile( + onToggle: (value) async { + forceEnglish = value ?? !forceEnglish; + await setting.put( + SettingBoxKey.forceEnglishTranslation, forceEnglish); + setState(() {}); + }, + title: Text('Force English', + style: TextStyle(fontFamily: fontFamily)), + description: Text( + 'Translate anime titles and summaries to English when the source is not English', + style: TextStyle(fontFamily: fontFamily)), + initialValue: forceEnglish, + ), + ]), ], ), ); diff --git a/lib/request/config/api_endpoints.dart b/lib/request/config/api_endpoints.dart index 5143bc111..433813545 100644 --- a/lib/request/config/api_endpoints.dart +++ b/lib/request/config/api_endpoints.dart @@ -120,6 +120,10 @@ class ApiEndpoints { /// 图片识别番剧 static const String traceApi = 'https://api.trace.moe/search'; + /// 翻译 API (Google 免密钥端点) - 将内容强制翻译为英文 + static const String translateApi = + 'https://translate.googleapis.com/translate_a/single'; + static String formatUrl(String url, List params) { for (int i = 0; i < params.length; i++) { url = url.replaceAll('{$i}', params[i].toString()); diff --git a/lib/request/core/dio_factory.dart b/lib/request/core/dio_factory.dart index 8266112d8..bd8c1924e 100644 --- a/lib/request/core/dio_factory.dart +++ b/lib/request/core/dio_factory.dart @@ -13,6 +13,7 @@ class DioFactory { static Dio? _githubDio; static Dio? _pluginDio; static Dio? _downloadDio; + static Dio? _translateDio; static Dio get apiDio => _apiDio ??= _create( NetworkConfig.fromSettings(), @@ -54,11 +55,23 @@ class DioFactory { return _create(config); } + /// Dedicated client for the translation endpoint (no mirror interceptors). + static Dio get translateDio => _translateDio ??= _create( + NetworkConfig.fromSettings( + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 15), + ), + defaultHeaders: { + 'user-agent': getRandomUA(), + }, + ); + static void reset() { _apiDio = null; _githubDio = null; _pluginDio = null; _downloadDio = null; + _translateDio = null; } static Dio _create( diff --git a/lib/services/player/audio_controller.dart b/lib/services/player/audio_controller.dart index 5ed527b1a..99cca0c42 100644 --- a/lib/services/player/audio_controller.dart +++ b/lib/services/player/audio_controller.dart @@ -37,7 +37,7 @@ class AudioController { if (Platform.isLinux) { AudioServiceMpris.init( dBusName: 'io.github.Predidit.Kazumi.channel.audio', - identity: 'Kazumi Playback', + identity: 'Akiora Playback', canControl: true, canPlay: true, canPause: true, @@ -52,7 +52,7 @@ class AudioController { }, config: const AudioServiceConfig( androidNotificationChannelId: 'io.github.Predidit.Kazumi.channel.audio', - androidNotificationChannelName: 'Kazumi Playback', + androidNotificationChannelName: 'Akiora Playback', androidNotificationOngoing: true, ), ); diff --git a/lib/services/storage/storage.dart b/lib/services/storage/storage.dart index 06e89d5fc..1322f6f4c 100644 --- a/lib/services/storage/storage.dart +++ b/lib/services/storage/storage.dart @@ -23,6 +23,9 @@ class GStorage { static late Box searchHistory; static late Box downloads; + /// Cache of source-text -> English translation (force English feature). + static late Box translationCache; + /// Hive directory path, initialized during init() static String? _hivePath; @@ -162,6 +165,7 @@ class GStorage { shieldList = await _openBoxSafe('shieldList'); searchHistory = await _openBoxSafe('searchHistory'); downloads = await _openBoxSafe('downloads'); + translationCache = await _openBoxSafe('translationCache'); } /// Open a Hive box with automatic recovery on corruption. @@ -422,5 +426,6 @@ class SettingBoxKey { brightnessVolumeGesture = 'brightnessVolumeGesture', historySyncDeviceId = 'historySyncDeviceId', historySyncSequence = 'historySyncSequence', - historySyncSnapshotInitialized = 'historySyncSnapshotInitialized'; + historySyncSnapshotInitialized = 'historySyncSnapshotInitialized', + forceEnglishTranslation = 'forceEnglishTranslation'; } diff --git a/lib/services/translation/translation_service.dart b/lib/services/translation/translation_service.dart new file mode 100644 index 000000000..73b6aa679 --- /dev/null +++ b/lib/services/translation/translation_service.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:kazumi/request/config/api_endpoints.dart'; +import 'package:kazumi/request/core/dio_factory.dart'; +import 'package:kazumi/services/logging/logger.dart'; +import 'package:kazumi/services/storage/storage.dart'; + +/// Forces dynamic content (anime titles, summaries, etc.) coming from the +/// Bangumi API to English by machine-translating it on device. +/// +/// Translations are cached both in memory and on disk (Hive) so each unique +/// string is only translated once. The underlying data model is never mutated, +/// translation happens only at display time via [TranslatedText]. +class TranslationService { + TranslationService._internal(); + static final TranslationService instance = TranslationService._internal(); + factory TranslationService() => instance; + + final Map _memoryCache = {}; + final Map> _inFlight = {}; + + /// Matches CJK ideographs and Japanese kana - i.e. text that is not English. + static final RegExp _nonLatin = RegExp( + r'[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]'); + + /// Whether the force-English feature is enabled (defaults to true). + bool get enabled { + try { + return GStorage.setting + .get(SettingBoxKey.forceEnglishTranslation, defaultValue: true); + } catch (_) { + return true; + } + } + + /// Returns true when [text] contains non-Latin characters worth translating. + bool needsTranslation(String text) { + if (text.isEmpty) return false; + return _nonLatin.hasMatch(text); + } + + /// Synchronously returns a cached translation if available, else null. + String? cached(String text) { + if (_memoryCache.containsKey(text)) return _memoryCache[text]; + try { + final stored = GStorage.translationCache.get(text); + if (stored != null) { + _memoryCache[text] = stored; + return stored; + } + } catch (_) {} + return null; + } + + /// Translates [text] to English. Returns the original text on failure so the + /// UI never ends up empty. Results are cached. + Future translateToEnglish(String text) async { + final trimmed = text.trim(); + if (trimmed.isEmpty || !needsTranslation(trimmed)) return text; + + final hit = cached(trimmed); + if (hit != null) return hit; + + if (_inFlight.containsKey(trimmed)) return _inFlight[trimmed]; + + final future = _request(trimmed); + _inFlight[trimmed] = future; + try { + return await future; + } finally { + _inFlight.remove(trimmed); + } + } + + Future _request(String text) async { + try { + final Dio dio = DioFactory.translateDio; + final response = await dio.post( + ApiEndpoints.translateApi, + queryParameters: const { + 'client': 'gtx', + 'sl': 'auto', + 'tl': 'en', + 'dt': 't', + }, + data: {'q': text}, + options: Options( + contentType: Headers.formUrlEncodedContentType, + responseType: ResponseType.plain, + ), + ); + + final translated = _parse(response.data); + if (translated != null && translated.trim().isNotEmpty) { + _memoryCache[text] = translated; + try { + await GStorage.translationCache.put(text, translated); + } catch (_) {} + return translated; + } + } catch (e) { + KazumiLogger().w('Translation: failed to translate text', error: e); + } + return text; + } + + /// The endpoint returns a nested JSON array: + /// [[["translated","original",...], ...], null, "zh-CN", ...] + String? _parse(dynamic data) { + try { + final decoded = data is String ? jsonDecode(data) : data; + if (decoded is List && decoded.isNotEmpty && decoded[0] is List) { + final buffer = StringBuffer(); + for (final segment in (decoded[0] as List)) { + if (segment is List && segment.isNotEmpty && segment[0] is String) { + buffer.write(segment[0] as String); + } + } + final result = buffer.toString(); + return result.isEmpty ? null : result; + } + } catch (e) { + KazumiLogger().w('Translation: failed to parse response', error: e); + } + return null; + } +} diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 90a8d2900..73607bf31 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -14,6 +14,8 @@ 6.0 CFBundleName $(PRODUCT_NAME) + CFBundleDisplayName + Akiora CFBundlePackageType APPL CFBundleShortVersionString diff --git a/web/index.html b/web/index.html index 3a6441478..074eedeee 100644 --- a/web/index.html +++ b/web/index.html @@ -23,13 +23,13 @@ - + - kazumi + Akiora