diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3cdc40..8bb4ea21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,104 @@ *** +## [v6.10.3] - 2026-06-02 + +### 🌐 多语言翻译字段补全 + +**问题:** 新增 `roleNative`(原生栈)和7个分发渠道翻译字段后,10个非中英语言文件缺少这些字段导致编译 Error。 + +**修复方案:** +- ✅ 10个语言文件(es, fr, it, pt, ru, ar, hi, bn, ko, ja, zh_tw, de)补全8个 TAbout 字段 +- ✅ `roleNative` — 各语言原生栈翻译 +- ✅ `distributionChannel` — 分发渠道标题 +- ✅ `distAndroid/distIOS/distMacOS/distHarmony/distWeb/distWindows` — 各平台分发说明 +- ✅ fr.dart 双引号 lint 修复 + +**涉及文件:** +- `lib/l10n/languages/es.dart` — 西班牙语 +- `lib/l10n/languages/fr.dart` — 法语 +- `lib/l10n/languages/it.dart` — 意大利语 +- `lib/l10n/languages/pt.dart` — 葡萄牙语 +- `lib/l10n/languages/ru.dart` — 俄语 +- `lib/l10n/languages/ar.dart` — 阿拉伯语 +- `lib/l10n/languages/hi.dart` — 印地语 +- `lib/l10n/languages/bn.dart` — 孟加拉语 +- `lib/l10n/languages/ko.dart` — 韩语 +- `lib/l10n/languages/ja.dart` — 日语 +- `lib/l10n/languages/zh_tw.dart` — 繁体中文 +- `lib/l10n/languages/de.dart` — 德语 + +*** + +## [v6.10.2] - 2026-06-02 + +### 🔐 二维码扫码登录自动跳转 + +**问题:** Device A 扫描 Device B(登录页)的二维码后,Device A 显示"扫码成功"但 Device B 无响应,不会自动登录。 + +**根因:** 生成二维码后缺少轮询/WebSocket 监听机制,无法感知扫码确认状态变化。 + +**修复方案:** +- ✅ `qrcode_login_provider.dart` — 新增 `waiting`/`scanned`/`logging` 步骤,生成二维码后自动启动 HTTP 轮询(3秒间隔) + WebSocket 双通道监听 +- ✅ 收到 `confirmed` + `token` 后自动调用 `AuthService.tokenLogin(token)` 完成登录 +- ✅ `qrcode_ws_service.dart` — 处理 confirmed 状态携带 token 的推送,终态自动取消订阅 +- ✅ `qrcode_login_page.dart` — 新增状态指示器(等待扫码/已扫码/正在登录),登录成功后自动跳转首页 +- ✅ 切换 Tab / 退出页面时自动停止轮询,防止资源泄漏 + +**涉及文件:** +- `features/auth/providers/qrcode_login_provider.dart` — 核心轮询+自动登录逻辑 +- `features/auth/services/qrcode_ws_service.dart` — WS终态自动取消订阅 +- `features/auth/presentation/qrcode_login_page.dart` — UI状态指示+自动跳转 + +*** + +## [v6.10.1] - 2026-06-02 + +### 🐛 修复 iOS 离线模式 Syncfusion Chart 崩溃 + +**问题:** 点击"离线模式"或导航到含图表页面时,Syncfusion Chart 在 build/layout 阶段调用 `markNeedsLayout`,导致 "build during layout" 错误,App 卡死/闪退。 + +**根因:** Syncfusion Flutter Charts 已知 Bug — `ChartSeriesRenderer.markNeedsLayout` 在 widget 树构建期间被调用,违反 Flutter 渲染协议。 + +**修复方案:** +- ✅ 增强 `DeferredBuilder` — 添加 `RepaintBoundary` 隔离重绘 + 可选 `placeholder` 参数 +- ✅ 全项目 **21个文件、约35处** Syncfusion 图表全部用 `DeferredBuilder` 包裹,延迟到 `postFrameCallback` 渲染 +- ✅ 所有图表系列补齐 `animationDuration: 0`,防止动画触发 `markNeedsLayout` + +**涉及文件:** +- `shared/widgets/containers/deferred_builder.dart` — 增加 RepaintBoundary + placeholder +- `features/mine/user_center/` — learning_charts, learning_center_page, coin_log_page, learning_progress_page +- `features/tool_center/statistics/` — learning_stats_tab, coin_stats_tab, favorite_stats_tab, statistics_page +- `features/reading_report/` — trend_chart +- `features/file_transfer/` — transfer_speed_chart, transfer_stats_page +- `features/mine/achievement/` — achievement_page +- `features/check/` — check_page +- `features/discover/` — readlater_stats_page +- `features/home/` — history_page, favorite_page +- `features/tool_center/leisure/` — leisure_settings_sections +- `features/mine/settings/` — data_management_page, translate_plugin_page, permission_management_page + +*** + ## [v6.10.0] - 2026-06-02 -### 🏗️ 架构优化 + 灵动岛增强 + Lint规则 + 应用图标 +### 🏗️ 架构优化 + 灵动岛增强 + Lint规则 + 应用图标 + 引导页协议多语言 + +**0. 协议页软件著作权证书图片 📜:** +- 🔄 协议内容"2.1 平台版权"→"2.1 软件著作权"(中文+英文同步修改) +- ✅ 新增 `WatermarkedCopyrightImage` 组件 — 展示软件著作权证书图片 +- ✅ 对角线"闲言"水印覆盖(CustomPaint + 旋转文字) +- ✅ 点击图片弹出全屏查看(InteractiveViewer 支持缩放) +- ✅ 通用协议页 + 引导页协议页 均支持图片展示 +- ✅ 图片右上角全屏提示标签 + +**1. 引导页协议多语言修复 🌐:** +- 🐛 修复引导页协议内容始终显示中文的问题 +- ✅ `AgreementData.getContent/getUpdateDate` 传入当前语言ID +- ✅ `agreementType.title` → `agreementType.titleFor(languageId)` 显示本地化标题 +- ✅ `_parseChapters` 章节解析兼容英文罗马数字编号(I. II. III. / Zero.) +- ✅ 新增 `_localeToLanguageId` 方法将 Flutter Locale 映射为协议语言ID +- ✅ 支持14种语言的协议内容、标题、更新日期自动切换 **1. MacosPlatformService 统一 🏗️:** - 🔄 分散在多文件的 MethodChannel 统一为 `MacosPlatformService` diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 41d081b6..ce5be335 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -104,24 +104,5 @@ group.apps.xy.xianyan.share - UIApplicationShortcutItems - - - UIApplicationShortcutItemType - action_theme - UIApplicationShortcutItemTitle - 主题个性化 - UIApplicationShortcutItemIconName - palette - - - UIApplicationShortcutItemType - action_general_settings - UIApplicationShortcutItemTitle - 通用设置 - UIApplicationShortcutItemIconName - settings - - diff --git a/lib/app/app.dart b/lib/app/app.dart index 152eb68d..6e9cb334 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -24,6 +24,7 @@ import 'package:flutter_quill/flutter_quill.dart' import 'package:flutter/services.dart'; import '../core/services/device/quick_actions_service.dart'; +import '../core/services/device/macos_platform_service.dart'; import '../core/services/data/home_widget_service.dart'; import '../core/services/ui/status_bar_service.dart'; import '../core/router/app_router.dart' show appRouter, rootNavigatorKey; @@ -222,6 +223,18 @@ class _XianyanAppState extends ConsumerState } } + @override + void didChangePlatformBrightness() { + super.didChangePlatformBrightness(); + final settings = ref.read(themeSettingsProvider); + if (settings.themeMode == AppThemeMode.system) { + final isDark = + WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark; + MacosPlatformService.syncTheme(isDark); + } + } + @override void didChangeLocales(List? locales) { super.didChangeLocales(locales); @@ -316,6 +329,15 @@ class _XianyanAppState extends ConsumerState final themeMode = _resolveThemeMode(settings.themeMode); + final effectiveIsDark = switch (themeMode) { + ThemeMode.dark => true, + ThemeMode.light => false, + ThemeMode.system => + WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark, + }; + MacosPlatformService.syncTheme(effectiveIsDark); + return Directionality( textDirection: textDirection, child: _LocaleTransitionWrapper( diff --git a/lib/features/agreements/data/agreement_data.dart b/lib/features/agreements/data/agreement_data.dart index ae1554d4..5cdf1369 100644 --- a/lib/features/agreements/data/agreement_data.dart +++ b/lib/features/agreements/data/agreement_data.dart @@ -872,7 +872,7 @@ class AgreementData { 二、内容版权归属 -2.1 平台版权 +2.1 软件著作权 • **闲言APP**的软件著作权归**弥勒市朋普镇微风暴网络科技工作室**所有 • 软件著作权登记号:【2020SR0421982】 • 应用的界面设计、图标、代码等受【著作权法】保护 @@ -1996,7 +1996,7 @@ I. Disclaimers II. Content Copyright -2.1 Platform Copyright +2.1 Software Copyright • The software copyright of **Xianyan APP** belongs to **Mile City Pengpu Town Weifengbao Network Technology Studio** • Software Copyright Registration Number: 2020SR0421982 • The interface design, icons, code, etc. of the app are protected by copyright law diff --git a/lib/features/agreements/presentation/agreement_page.dart b/lib/features/agreements/presentation/agreement_page.dart index d1348f60..99487939 100644 --- a/lib/features/agreements/presentation/agreement_page.dart +++ b/lib/features/agreements/presentation/agreement_page.dart @@ -16,6 +16,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_radius.dart'; import '../../../l10n/app_locale.dart'; import '../../../shared/widgets/containers/glass_container.dart'; +import '../../../shared/widgets/media/watermarked_copyright_image.dart'; import '../data/agreement_types.dart'; import '../data/agreement_data.dart'; import '../../../shared/widgets/adaptive/adaptive_back_button.dart'; @@ -199,6 +200,10 @@ class _AgreementSection extends StatelessWidget { final AppThemeExtension ext; final _Section section; + bool get _isCopyrightSection => + section.title.contains('软件著作权') || + section.title.contains('Software Copyright'); + @override Widget build(BuildContext context) { return Padding( @@ -217,6 +222,10 @@ class _AgreementSection extends StatelessWidget { const SizedBox(height: AppSpacing.sm), ], if (section.body.isNotEmpty) _RichBody(ext: ext, text: section.body), + if (_isCopyrightSection) ...[ + const SizedBox(height: AppSpacing.sm), + const WatermarkedCopyrightImage(), + ], ], ), ); diff --git a/lib/features/auth/presentation/login_page.dart b/lib/features/auth/presentation/login_page.dart index 2b5b17d0..ad7af208 100644 --- a/lib/features/auth/presentation/login_page.dart +++ b/lib/features/auth/presentation/login_page.dart @@ -489,7 +489,7 @@ class _LoginPageState extends ConsumerState ext, icon: CupertinoIcons.person_2, label: auth.legacyUser, - accentColor: Theme.of(context).colorScheme.secondary, + accentColor: ext.accentLight, onTap: () => _switchLoginMode(_LoginMode.legacy), ), const SizedBox(width: AppSpacing.lg), @@ -678,7 +678,9 @@ class _LoginPageState extends ConsumerState child: GestureDetector( onTap: () => _showAgreement(true), child: Text( - '《${auth.userAgreement}》', + auth.userAgreement.startsWith('《') + ? auth.userAgreement + : '《${auth.userAgreement}》', style: AppTypography.footnote.copyWith( color: ext.accent, fontWeight: FontWeight.w600, @@ -696,7 +698,9 @@ class _LoginPageState extends ConsumerState child: GestureDetector( onTap: () => _showAgreement(false), child: Text( - '《${auth.privacyPolicy}》', + auth.privacyPolicy.startsWith('《') + ? auth.privacyPolicy + : '《${auth.privacyPolicy}》', style: AppTypography.footnote.copyWith( color: ext.accent, fontWeight: FontWeight.w600, diff --git a/lib/features/auth/presentation/qrcode_login_page.dart b/lib/features/auth/presentation/qrcode_login_page.dart index 033b0bba..ded5c05e 100644 --- a/lib/features/auth/presentation/qrcode_login_page.dart +++ b/lib/features/auth/presentation/qrcode_login_page.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 二维码登录页面 /// 创建时间: 2026-05-10 -/// 更新时间: 2026-05-10 +/// 更新时间: 2026-06-02 /// 作用: 扫码登录(扫描Web端二维码确认登录)+ 生成二维码(Web端扫码登录) -/// 上次更新: 初始创建 +/// 上次更新: 生成二维码后自动轮询监听,confirmed后自动tokenLogin跳转首页 /// ============================================================ import 'dart:async'; @@ -15,11 +15,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import '../../../core/router/app_routes.dart'; +import '../../../core/router/app_nav_extension.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/utils/logger.dart'; +import '../providers/auth_provider.dart'; import '../providers/qrcode_login_provider.dart'; import '../../../shared/widgets/adaptive/adaptive_back_button.dart'; import '../../../shared/widgets/feedback/app_toast.dart'; @@ -45,6 +48,7 @@ class _QrcodeLoginPageState extends ConsumerState { bool _hasScanned = false; StreamSubscription? _subscription; Timer? _expireTimer; + bool _hasNavigated = false; @override void initState() { @@ -59,6 +63,7 @@ class _QrcodeLoginPageState extends ConsumerState { _subscription?.cancel(); _expireTimer?.cancel(); _scannerController.dispose(); + ref.read(qrcodeLoginProvider.notifier).stopPolling(); super.dispose(); } @@ -148,10 +153,11 @@ class _QrcodeLoginPageState extends ConsumerState { final qrcodeState = ref.watch(qrcodeLoginProvider); ref.listen(qrcodeLoginProvider, (prev, next) { - if (next.isSuccess && prev?.isSuccess != true) { - _showSuccessDialog(ext); + if (next.isSuccess && prev?.isSuccess != true && !_hasNavigated) { + _onLoginSuccess(ext, next); } - if (next.isError && next.errorMessage != null) { + if (next.isError && next.errorMessage != null && + prev?.errorMessage != next.errorMessage) { AppToast.showError(next.errorMessage!); } }); @@ -188,6 +194,91 @@ class _QrcodeLoginPageState extends ConsumerState { ); } + // ============================================================ + // 登录成功处理 + // ============================================================ + + void _onLoginSuccess(AppThemeExtension ext, QrcodeLoginState state) { + _hasNavigated = true; + ref.invalidate(authProvider); + + if (_currentTab == 1) { + _showQrcodeLoginSuccessDialog(ext); + } else { + _showScanSuccessDialog(ext); + } + } + + /// 生成二维码Tab: 扫码自动登录成功 → 跳转首页 + void _showQrcodeLoginSuccessDialog(AppThemeExtension ext) { + showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('✅ '), + Text( + '登录成功', + style: AppTypography.headline.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ), + content: Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: Text( + '扫码登录成功,即将进入首页', + style: AppTypography.subhead.copyWith(color: ext.textSecondary), + ), + ), + ), + ); + + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + Navigator.of(context).pop(); + context.appGo(AppRoutes.home); + } + }).catchError((_) {}); + } + + /// 扫码Tab: 确认成功 → 返回上一页 + void _showScanSuccessDialog(AppThemeExtension ext) { + showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('✅ '), + Text( + '登录成功', + style: AppTypography.headline.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ), + content: Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: Text( + '已成功在Web端登录,即将返回', + style: AppTypography.subhead.copyWith(color: ext.textSecondary), + ), + ), + ), + ); + + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + } + }).catchError((_) {}); + } + // ============================================================ // Tab栏 // ============================================================ @@ -222,6 +313,7 @@ class _QrcodeLoginPageState extends ConsumerState { _generateQrcode(); } else { _hasScanned = false; + ref.read(qrcodeLoginProvider.notifier).stopPolling(); _startScanner(); } }, @@ -631,7 +723,7 @@ class _QrcodeLoginPageState extends ConsumerState { children: [ const SizedBox(height: AppSpacing.md), Text( - 'Web端扫码登录', + '扫码登录', style: AppTypography.headline.copyWith( color: ext.textPrimary, fontWeight: FontWeight.w700, @@ -639,13 +731,15 @@ class _QrcodeLoginPageState extends ConsumerState { ).animate().fadeIn(duration: 400.ms), const SizedBox(height: AppSpacing.xs), Text( - '在Web端扫描下方二维码即可登录', + '使用已登录的设备扫描下方二维码即可登录', style: AppTypography.subhead.copyWith(color: ext.textHint), textAlign: TextAlign.center, ).animate().fadeIn(duration: 400.ms, delay: 100.ms), const SizedBox(height: AppSpacing.lg), _buildQrcodeCard(ext, state), const SizedBox(height: AppSpacing.lg), + _buildQrcodeStatusIndicator(ext, state), + const SizedBox(height: AppSpacing.md), _buildQrcodeActions(ext, state), ], ), @@ -710,7 +804,7 @@ class _QrcodeLoginPageState extends ConsumerState { ), ], ), - ] else if (isExpired) ...[ + ] else if (isExpired || state.isError) ...[ _buildExpiredPlaceholder(ext), ] else ...[ _buildLoadingPlaceholder(ext), @@ -723,6 +817,77 @@ class _QrcodeLoginPageState extends ConsumerState { .slideY(begin: 0.05, end: 0, duration: 500.ms, delay: 200.ms); } + /// 二维码状态指示器 + Widget _buildQrcodeStatusIndicator( + AppThemeExtension ext, + QrcodeLoginState state, + ) { + if (state.isLogging) { + return _buildStatusPill( + ext, + icon: CupertinoActivityIndicator(radius: 10, color: ext.accent), + text: '正在登录…', + bgColor: ext.accent.withValues(alpha: 0.1), + textColor: ext.accent, + ); + } + + if (state.isScanned) { + return _buildStatusPill( + ext, + icon: const Icon(CupertinoIcons.checkmark_circle_fill, size: 20, color: CupertinoColors.systemGreen), + text: '已扫码,等待确认…', + bgColor: CupertinoColors.systemGreen.withValues(alpha: 0.1), + textColor: CupertinoColors.systemGreen, + ); + } + + if (state.isWaiting) { + return _buildStatusPill( + ext, + icon: CupertinoActivityIndicator(radius: 10, color: ext.accent), + text: '等待扫码…', + bgColor: ext.accent.withValues(alpha: 0.08), + textColor: ext.textSecondary, + ); + } + + return const SizedBox.shrink(); + } + + Widget _buildStatusPill( + AppThemeExtension ext, { + required Widget icon, + required String text, + required Color bgColor, + required Color textColor, + }) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: bgColor, + borderRadius: AppRadius.pillBorder, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + icon, + const SizedBox(width: AppSpacing.sm), + Text( + text, + style: AppTypography.footnote.copyWith( + color: textColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ).animate().fadeIn(duration: 300.ms); + } + Widget _buildExpiredPlaceholder(AppThemeExtension ext) { return Column( children: [ @@ -735,6 +900,7 @@ class _QrcodeLoginPageState extends ConsumerState { ), child: Center( child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( CupertinoIcons.exclamationmark_triangle, @@ -807,6 +973,7 @@ class _QrcodeLoginPageState extends ConsumerState { } Widget _buildQrcodeActions(AppThemeExtension ext, QrcodeLoginState state) { + final isLogging = state.isLogging; return GlassContainer( padding: const EdgeInsets.all(AppSpacing.md), child: Column( @@ -817,7 +984,7 @@ class _QrcodeLoginPageState extends ConsumerState { child: CupertinoButton( color: ext.accent, borderRadius: AppRadius.mdBorder, - onPressed: _generateQrcode, + onPressed: isLogging ? null : _generateQrcode, child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, @@ -858,44 +1025,6 @@ class _QrcodeLoginPageState extends ConsumerState { ), ).animate().fadeIn(duration: 400.ms, delay: 400.ms); } - - // ============================================================ - // 成功弹窗 - // ============================================================ - - void _showSuccessDialog(AppThemeExtension ext) { - showCupertinoDialog( - context: context, - builder: (ctx) => CupertinoAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('✅ '), - Text( - '登录成功', - style: AppTypography.headline.copyWith( - fontWeight: FontWeight.w700, - ), - ), - ], - ), - content: Padding( - padding: const EdgeInsets.only(top: AppSpacing.sm), - child: Text( - '已成功在Web端登录,即将返回', - style: AppTypography.subhead.copyWith(color: ext.textSecondary), - ), - ), - ), - ); - - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - } - }).catchError((_) {}); - } } // ============================================================ diff --git a/lib/features/auth/presentation/register_section.dart b/lib/features/auth/presentation/register_section.dart index 26ccc538..5c572ef6 100644 --- a/lib/features/auth/presentation/register_section.dart +++ b/lib/features/auth/presentation/register_section.dart @@ -7,8 +7,10 @@ /// ============================================================ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/router/app_nav_extension.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_typography.dart'; @@ -43,6 +45,7 @@ class _RegisterSectionState extends ConsumerState { int _regStep = 1; bool _obscureRegPassword = true; bool _subscribeEmail = false; + bool _agreedToTerms = false; bool _emsSending = false; int _emsCountdown = 0; bool _showSecQuestion = false; @@ -623,6 +626,8 @@ class _RegisterSectionState extends ConsumerState { ), ), const SizedBox(height: AppSpacing.lg), + _buildAgreementRow(ext, auth), + const SizedBox(height: AppSpacing.md), Row( children: [ Expanded( @@ -666,7 +671,8 @@ class _RegisterSectionState extends ConsumerState { color: ext.accent, borderRadius: AppRadius.mdBorder, onPressed: - (_regPasswordController.text.isNotEmpty && + (_agreedToTerms && + _regPasswordController.text.isNotEmpty && _regConfirmPasswordController.text.isNotEmpty && !authState.isLoading) ? _handleRegister @@ -707,6 +713,98 @@ class _RegisterSectionState extends ConsumerState { } } + Widget _buildAgreementRow(AppThemeExtension ext, TAuth auth) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => _agreedToTerms = !_agreedToTerms), + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 44, minHeight: 44), + child: Center( + child: Container( + width: 20, + height: 20, + margin: const EdgeInsets.only(top: 1), + decoration: BoxDecoration( + color: _agreedToTerms ? ext.accent : Colors.transparent, + borderRadius: BorderRadius.circular(5), + border: Border.all( + color: _agreedToTerms ? ext.accent : ext.textHint, + width: 1.5, + ), + ), + child: _agreedToTerms + ? Icon( + CupertinoIcons.checkmark, + size: 13, + color: ext.textOnAccent, + ) + : null, + ), + ), + ), + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text.rich( + TextSpan( + text: auth.registerAgreePrefix, + style: AppTypography.footnote.copyWith( + color: ext.textSecondary, + ), + children: [ + WidgetSpan( + child: GestureDetector( + onTap: () => _showAgreement(true), + child: Text( + auth.userAgreement.startsWith('《') + ? auth.userAgreement + : '《${auth.userAgreement}》', + style: AppTypography.footnote.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + TextSpan( + text: auth.and, + style: AppTypography.footnote.copyWith( + color: ext.textSecondary, + ), + ), + WidgetSpan( + child: GestureDetector( + onTap: () => _showAgreement(false), + child: Text( + auth.privacyPolicy.startsWith('《') + ? auth.privacyPolicy + : '《${auth.privacyPolicy}》', + style: AppTypography.footnote.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + void _showAgreement(bool isUserAgreement) { + if (isUserAgreement) { + context.appPush('/agreement/user-service-agreement'); + } else { + context.appPush('/agreement/privacy-policy'); + } + } + Future _handleRegister() async { final t = ref.read(translationsProvider); final username = _usernameController.text.trim(); diff --git a/lib/features/auth/providers/qrcode_login_provider.dart b/lib/features/auth/providers/qrcode_login_provider.dart index 2549fdec..1b86e884 100644 --- a/lib/features/auth/providers/qrcode_login_provider.dart +++ b/lib/features/auth/providers/qrcode_login_provider.dart @@ -1,14 +1,18 @@ /// ============================================================ /// 闲言APP — 二维码登录状态管理 /// 创建时间: 2026-05-10 -/// 更新时间: 2026-05-10 -/// 作用: 管理扫码登录流程状态(扫描/确认/成功/错误) -/// 上次更新: 初始创建 +/// 更新时间: 2026-06-02 +/// 作用: 管理扫码登录流程状态(扫描/确认/成功/错误)+ 轮询/WebSocket监听自动登录 +/// 上次更新: 新增轮询+WS双通道监听,confirmed时自动tokenLogin完成登录 /// ============================================================ +import 'dart:async'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/utils/logger.dart'; +import '../services/auth_service.dart'; +import '../services/qrcode_ws_service.dart'; import '../services/user_security_service.dart'; // ============================================================ @@ -20,7 +24,10 @@ enum QrcodeLoginStep { idle, scanning, confirming, + waiting, + scanned, success, + logging, error, } @@ -31,12 +38,14 @@ class QrcodeLoginState { this.qrCode = '', this.qrcodeResult, this.errorMessage, + this.loginToken, }); final QrcodeLoginStep step; final String qrCode; final QrcodeGenerateResult? qrcodeResult; final String? errorMessage; + final String? loginToken; QrcodeLoginState copyWith({ QrcodeLoginStep? step, @@ -45,19 +54,27 @@ class QrcodeLoginState { bool clearQrcodeResult = false, String? errorMessage, bool clearError = false, + String? loginToken, + bool clearLoginToken = false, }) { return QrcodeLoginState( step: step ?? this.step, qrCode: qrCode ?? this.qrCode, - qrcodeResult: clearQrcodeResult ? null : (qrcodeResult ?? this.qrcodeResult), + qrcodeResult: clearQrcodeResult + ? null + : (qrcodeResult ?? this.qrcodeResult), errorMessage: clearError ? null : (errorMessage ?? this.errorMessage), + loginToken: clearLoginToken ? null : (loginToken ?? this.loginToken), ); } bool get isIdle => step == QrcodeLoginStep.idle; bool get isScanning => step == QrcodeLoginStep.scanning; bool get isConfirming => step == QrcodeLoginStep.confirming; + bool get isWaiting => step == QrcodeLoginStep.waiting; + bool get isScanned => step == QrcodeLoginStep.scanned; bool get isSuccess => step == QrcodeLoginStep.success; + bool get isLogging => step == QrcodeLoginStep.logging; bool get isError => step == QrcodeLoginStep.error; } @@ -68,8 +85,17 @@ class QrcodeLoginState { /// 二维码登录状态管理器 class QrcodeLoginNotifier extends Notifier { @override - QrcodeLoginState build() => const QrcodeLoginState(); - QrcodeLoginNotifier(); + QrcodeLoginState build() { + ref.onDispose(_cleanup); + return const QrcodeLoginState(); + } + + Timer? _pollTimer; + bool _polling = false; + + // ============================================================ + // 扫码确认登录 (Device A 扫码后调用) + // ============================================================ /// 扫码确认登录 Future confirmLogin(String code) async { @@ -99,16 +125,23 @@ class QrcodeLoginNotifier extends Notifier { } } - /// 生成二维码(Web端登录用) + // ============================================================ + // 生成二维码 + 启动轮询 (Device B 生成二维码) + // ============================================================ + + /// 生成二维码并启动轮询监听 Future generateQrcode() async { - state = state.copyWith(clearError: true); + stopPolling(); + state = state.copyWith(clearError: true, clearQrcodeResult: true); try { final result = await UserSecurityService.qrcodeGenerate(); state = state.copyWith( + step: QrcodeLoginStep.waiting, qrCode: result.code, qrcodeResult: result, ); Log.i('二维码生成成功: ${result.code}'); + _startPolling(result.code); } catch (e) { state = state.copyWith( step: QrcodeLoginStep.error, @@ -118,13 +151,142 @@ class QrcodeLoginNotifier extends Notifier { } } + // ============================================================ + // 轮询 + WebSocket 双通道监听 + // ============================================================ + + /// 启动轮询 + WebSocket 订阅 + void _startPolling(String code) { + _subscribeWs(code); + _pollTimer?.cancel(); + _pollTimer = Timer.periodic( + const Duration(seconds: 3), + (_) => _pollOnce(code), + ); + _pollOnce(code); + Log.i('二维码轮询已启动: $code'); + } + + /// 停止轮询 + 取消WS订阅 + void stopPolling() { + _pollTimer?.cancel(); + _pollTimer = null; + _polling = false; + QrcodeWsService.instance.unsubscribe(); + Log.i('二维码轮询已停止'); + } + + /// 订阅WebSocket推送 + void _subscribeWs(String code) { + QrcodeWsService.instance.subscribe(code, (data) { + final status = data['status'] as String? ?? ''; + Log.i('WS推送: status=$status'); + _handleStatus(status, token: data['token'] as String?); + }); + } + + /// 单次轮询 + Future _pollOnce(String code) async { + if (_polling) return; + if (state.step == QrcodeLoginStep.success || + state.step == QrcodeLoginStep.logging || + state.step == QrcodeLoginStep.error) { + stopPolling(); + return; + } + _polling = true; + try { + final result = await UserSecurityService.qrcodePoll(code: code); + _handleStatus(result.status, token: result.token); + } catch (e) { + Log.w('轮询异常: $e'); + } finally { + _polling = false; + } + } + + /// 统一处理状态变更 + void _handleStatus(String status, {String? token}) { + switch (status) { + case 'pending': + if (state.step != QrcodeLoginStep.waiting) { + state = state.copyWith(step: QrcodeLoginStep.waiting); + } + break; + case 'scanned': + if (state.step != QrcodeLoginStep.scanned) { + state = state.copyWith(step: QrcodeLoginStep.scanned); + Log.i('二维码已被扫码,等待确认'); + } + break; + case 'confirmed': + stopPolling(); + _onConfirmed(token); + break; + case 'expired': + stopPolling(); + state = state.copyWith( + step: QrcodeLoginStep.error, + errorMessage: '二维码已过期,请重新生成', + ); + break; + case 'cancelled': + stopPolling(); + state = state.copyWith( + step: QrcodeLoginStep.error, + errorMessage: '二维码已取消', + ); + break; + } + } + + /// confirmed 后自动登录 + Future _onConfirmed(String? token) async { + if (token == null || token.isEmpty) { + state = state.copyWith( + step: QrcodeLoginStep.error, + errorMessage: '扫码登录Token为空', + ); + Log.e('扫码登录Token为空'); + return; + } + + state = state.copyWith( + step: QrcodeLoginStep.logging, + loginToken: token, + ); + Log.i('收到扫码登录Token,开始自动登录'); + + try { + final user = await AuthService.tokenLogin( + token, + platform: 'ios', + appName: 'xianyan', + ); + state = state.copyWith(step: QrcodeLoginStep.success); + Log.i('扫码自动登录成功: ${user.displayName}'); + } catch (e) { + state = state.copyWith( + step: QrcodeLoginStep.error, + errorMessage: '扫码登录失败: $e', + ); + Log.e('扫码自动登录失败', e); + } + } + + // ============================================================ + // 通用操作 + // ============================================================ + /// 取消/重置 void cancel() { + stopPolling(); state = const QrcodeLoginState(); } /// 重置到扫描状态 void resetToScan() { + stopPolling(); state = const QrcodeLoginState(step: QrcodeLoginStep.scanning); } @@ -135,6 +297,11 @@ class QrcodeLoginNotifier extends Notifier { clearError: true, ); } + + /// 资源清理 + void _cleanup() { + stopPolling(); + } } // ============================================================ @@ -142,4 +309,6 @@ class QrcodeLoginNotifier extends Notifier { // ============================================================ final qrcodeLoginProvider = - NotifierProvider(QrcodeLoginNotifier.new); + NotifierProvider( + QrcodeLoginNotifier.new, +); diff --git a/lib/features/auth/services/qrcode_ws_service.dart b/lib/features/auth/services/qrcode_ws_service.dart index 5191e0b4..bfa387b7 100644 --- a/lib/features/auth/services/qrcode_ws_service.dart +++ b/lib/features/auth/services/qrcode_ws_service.dart @@ -3,7 +3,7 @@ /// 创建时间: 2026-06-02 /// 更新时间: 2026-06-02 /// 作用: 通过WebSocket长连接接收二维码状态变更推送,替代HTTP轮询 -/// 上次更新: 初始创建,支持信令服务器订阅+HTTP轮询降级 +/// 上次更新: 处理confirmed状态携带token的推送,收到终态后自动取消订阅 /// ============================================================ import 'dart:async'; @@ -144,8 +144,14 @@ class QrcodeWsService { final type = json['type'] as String? ?? ''; if (type == 'qrcode_status_update') { - Log.i('QrcodeWsService: 收到状态推送 ${json['status']}'); + final status = json['status'] as String? ?? ''; + Log.i('QrcodeWsService: 收到状态推送 $status'); _onStatusUpdate?.call(json); + if (status == 'confirmed' || status == 'expired' || + status == 'cancelled') { + Log.i('QrcodeWsService: 终态 $status,自动取消订阅'); + unsubscribe(); + } } else if (type == 'pong') { // heartbeat ack } diff --git a/lib/features/check/presentation/check_page.dart b/lib/features/check/presentation/check_page.dart index c432767b..62670aef 100644 --- a/lib/features/check/presentation/check_page.dart +++ b/lib/features/check/presentation/check_page.dart @@ -16,6 +16,7 @@ import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_theme.dart'; +import '../../../shared/widgets/containers/deferred_builder.dart'; import '../../../shared/widgets/containers/glass_container.dart'; import '../models/check_models.dart'; import '../providers/check_provider.dart'; @@ -734,23 +735,25 @@ class _SimilarityGauge extends StatelessWidget { SizedBox( width: 160, height: 160, - child: SfCircularChart( - margin: EdgeInsets.zero, - series: [ - DoughnutSeries<_SimPie, String>( - animationDuration: 0, - dataSource: [ - _SimPie('相似', simPercent, riskColor), - _SimPie('差异', 100 - simPercent, ext.bgSecondary), - ], - xValueMapper: (d, _) => d.label, - yValueMapper: (d, _) => d.value, - pointColorMapper: (d, _) => d.color, - innerRadius: '68%', - radius: '82%', - strokeWidth: 0, - ), - ], + child: DeferredBuilder( + builder: (context) => SfCircularChart( + margin: EdgeInsets.zero, + series: [ + DoughnutSeries<_SimPie, String>( + animationDuration: 0, + dataSource: [ + _SimPie('相似', simPercent, riskColor), + _SimPie('差异', 100 - simPercent, ext.bgSecondary), + ], + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, + pointColorMapper: (d, _) => d.color, + innerRadius: '68%', + radius: '82%', + strokeWidth: 0, + ), + ], + ), ), ), Column( diff --git a/lib/features/discover/presentation/pages/readlater_stats_page.dart b/lib/features/discover/presentation/pages/readlater_stats_page.dart index 9b92cd33..70adaabe 100644 --- a/lib/features/discover/presentation/pages/readlater_stats_page.dart +++ b/lib/features/discover/presentation/pages/readlater_stats_page.dart @@ -12,6 +12,7 @@ import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; +import 'package:xianyan/shared/widgets/containers/deferred_builder.dart'; import 'package:xianyan/shared/widgets/containers/glass_container.dart'; import 'package:xianyan/features/discover/models/chat_message.dart'; import 'package:xianyan/shared/widgets/adaptive/adaptive_back_button.dart'; @@ -180,7 +181,7 @@ class ReadlaterStatsPage extends StatelessWidget { SizedBox( width: 160, height: 160, - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( series: >[ DoughnutSeries<_TypeData, String>( dataSource: chartData, @@ -205,7 +206,7 @@ class ReadlaterStatsPage extends StatelessWidget { ), ], ), - ), + )), const SizedBox(width: AppSpacing.md), Expanded( child: Column( @@ -303,7 +304,7 @@ class ReadlaterStatsPage extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( + child: DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), @@ -354,7 +355,7 @@ class ReadlaterStatsPage extends StatelessWidget { ), ], ), - ), + )), ], ), ); @@ -407,7 +408,7 @@ class ReadlaterStatsPage extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 200, - child: SfCartesianChart( + child: DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), @@ -451,7 +452,7 @@ class ReadlaterStatsPage extends StatelessWidget { ), ], ), - ), + )), ], ), ); @@ -524,7 +525,7 @@ class ReadlaterStatsPage extends StatelessWidget { SizedBox( width: 160, height: 160, - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( annotations: [ CircularChartAnnotation( widget: Text( @@ -562,7 +563,7 @@ class ReadlaterStatsPage extends StatelessWidget { ), ], ), - ), + )), const SizedBox(width: AppSpacing.md), Expanded( child: Column( @@ -691,7 +692,7 @@ class ReadlaterStatsPage extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 200, - child: SfCartesianChart( + child: DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), @@ -730,7 +731,7 @@ class ReadlaterStatsPage extends StatelessWidget { ), ], ), - ), + )), ], ), ); @@ -902,7 +903,7 @@ class ReadlaterStatsPage extends StatelessWidget { Expanded( child: SizedBox( height: 160, - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( annotations: [ CircularChartAnnotation( widget: Text( @@ -937,7 +938,7 @@ class ReadlaterStatsPage extends StatelessWidget { ), ], ), - ), + )), ), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart b/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart index d582ff49..d5740d01 100644 --- a/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart +++ b/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart @@ -15,6 +15,7 @@ import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; +import 'package:xianyan/shared/widgets/containers/deferred_builder.dart'; import 'package:xianyan/features/file_transfer/providers/transfer_stats_provider.dart'; import 'package:xianyan/features/file_transfer/services/transfer_stats_service.dart'; import '../../../../shared/widgets/adaptive/adaptive_back_button.dart'; @@ -232,7 +233,7 @@ class _TransferStatsPageState extends ConsumerState { ); }).toList(); - return SfCartesianChart( + return DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, margin: EdgeInsets.zero, primaryXAxis: CategoryAxis( @@ -268,6 +269,7 @@ class _TransferStatsPageState extends ConsumerState { name: '接收', ), ], + ), ); } @@ -399,7 +401,7 @@ class _TransferStatsPageState extends ConsumerState { child: Row( children: [ Expanded( - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( margin: EdgeInsets.zero, series: [ DoughnutSeries<_FileTypePie, String>( @@ -439,6 +441,7 @@ class _TransferStatsPageState extends ConsumerState { ], ), ), + ), const SizedBox(width: AppSpacing.md), Expanded( child: Column( diff --git a/lib/features/file_transfer/presentation/widgets/transfer_speed_chart.dart b/lib/features/file_transfer/presentation/widgets/transfer_speed_chart.dart index 5a58c3f4..8919e8bb 100644 --- a/lib/features/file_transfer/presentation/widgets/transfer_speed_chart.dart +++ b/lib/features/file_transfer/presentation/widgets/transfer_speed_chart.dart @@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart'; import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; +import 'package:xianyan/shared/widgets/containers/deferred_builder.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; class TransferSpeedChart extends StatelessWidget { @@ -62,7 +63,8 @@ class TransferSpeedChart extends StatelessWidget { final data = speedHistory.isEmpty ? [0.0] : speedHistory; final chartData = data.asMap().entries.map((e) => _SpeedPt(e.key, e.value)).toList(); - return SfCartesianChart( + return DeferredBuilder( + builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, margin: EdgeInsets.zero, primaryXAxis: const NumericAxis( @@ -99,6 +101,7 @@ class TransferSpeedChart extends StatelessWidget { ), ), ], + ), ); } diff --git a/lib/features/home/presentation/favorite_page.dart b/lib/features/home/presentation/favorite_page.dart index 5797aa25..3ebe3f57 100644 --- a/lib/features/home/presentation/favorite_page.dart +++ b/lib/features/home/presentation/favorite_page.dart @@ -26,6 +26,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/router/app_routes.dart'; import '../../../shared/widgets/adaptive/adaptive_back_button.dart'; +import '../../../shared/widgets/containers/deferred_builder.dart'; import '../../../shared/widgets/containers/glass_container.dart'; import '../../../shared/widgets/input/app_slidable.dart'; import '../../../shared/widgets/input/app_popup_menu.dart'; @@ -441,7 +442,8 @@ class _FavoritePageState extends ConsumerState { ); } - return SfCircularChart( + return DeferredBuilder( + builder: (context) => SfCircularChart( series: >[ DoughnutSeries<_PieData, String>( animationDuration: 0, @@ -452,6 +454,7 @@ class _FavoritePageState extends ConsumerState { innerRadius: '35%', ), ], + ), ); } diff --git a/lib/features/home/presentation/history_page.dart b/lib/features/home/presentation/history_page.dart index a435b0ac..fd667108 100644 --- a/lib/features/home/presentation/history_page.dart +++ b/lib/features/home/presentation/history_page.dart @@ -22,6 +22,7 @@ import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_typography.dart'; import '../../../core/network/connectivity_provider.dart'; import '../../../shared/widgets/adaptive/adaptive_back_button.dart'; +import '../../../shared/widgets/containers/deferred_builder.dart'; import '../../../shared/widgets/containers/glass_container.dart'; import '../../../shared/widgets/input/app_slidable.dart'; import '../../../shared/widgets/feedback/app_toast.dart'; @@ -545,7 +546,8 @@ class _HistoryPageState extends ConsumerState { .map((e) => _TrendPt(e.key, e.value.value.toDouble())) .toList(); - return SfCartesianChart( + return DeferredBuilder( + builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, margin: EdgeInsets.zero, primaryXAxis: const NumericAxis( @@ -581,6 +583,7 @@ class _HistoryPageState extends ConsumerState { ), ), ], + ), ); } diff --git a/lib/features/mine/achievement/presentation/achievement_page.dart b/lib/features/mine/achievement/presentation/achievement_page.dart index 9f6fdba7..1dbcce4c 100644 --- a/lib/features/mine/achievement/presentation/achievement_page.dart +++ b/lib/features/mine/achievement/presentation/achievement_page.dart @@ -21,6 +21,7 @@ import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/utils/data/level_utils.dart'; +import '../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../shared/widgets/display/app_icon.dart'; import '../../../../shared/widgets/containers/glass_container.dart'; import '../../../../shared/widgets/feedback/offline_banner.dart'; @@ -598,22 +599,24 @@ class _ProfileSummary extends StatelessWidget { SizedBox( width: 80, height: 80, - child: SfCircularChart( - margin: EdgeInsets.zero, - series: [ - DoughnutSeries<_AchPie, String>( - animationDuration: 0, - dataSource: [ - _AchPie('已达成', achieved.toDouble(), ext.accent), - _AchPie('未达成', (total - achieved).toDouble(), ext.bgSecondary), - ], - xValueMapper: (d, _) => d.label, - yValueMapper: (d, _) => d.value, - pointColorMapper: (d, _) => d.color, - innerRadius: '70%', - radius: '90%', - ), - ], + child: DeferredBuilder( + builder: (context) => SfCircularChart( + margin: EdgeInsets.zero, + series: [ + DoughnutSeries<_AchPie, String>( + animationDuration: 0, + dataSource: [ + _AchPie('已达成', achieved.toDouble(), ext.accent), + _AchPie('未达成', (total - achieved).toDouble(), ext.bgSecondary), + ], + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, + pointColorMapper: (d, _) => d.color, + innerRadius: '70%', + radius: '90%', + ), + ], + ), ), ), Column( diff --git a/lib/features/mine/profile/presentation/app_info_sections.dart b/lib/features/mine/profile/presentation/app_info_sections.dart index a0ce2967..a26d094a 100644 --- a/lib/features/mine/profile/presentation/app_info_sections.dart +++ b/lib/features/mine/profile/presentation/app_info_sections.dart @@ -473,51 +473,54 @@ class PlatformSection extends StatelessWidget { runSpacing: AppSpacing.sm, children: platforms.map((p) { final isCurrent = p.isCurrent; - return Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.sm, - vertical: AppSpacing.xs, - ), - decoration: BoxDecoration( - color: isCurrent - ? ext.accent.withValues(alpha: 0.15) - : p.supported - ? ext.successColor.withValues(alpha: 0.08) - : ext.textHint.withValues(alpha: 0.06), - borderRadius: AppRadius.pillBorder, - border: Border.all( - color: isCurrent - ? ext.accent.withValues(alpha: 0.5) - : p.supported - ? ext.successColor.withValues(alpha: 0.3) - : ext.textHint.withValues(alpha: 0.15), + return GestureDetector( + onTap: () => _showDistributionDialog(context, p.name), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isCurrent) ...[ - Icon( - CupertinoIcons.checkmark_circle_fill, - size: 12, - color: ext.accent, - ), - const SizedBox(width: AppSpacing.xs), - ], - Text( - p.name, - style: AppTypography.caption1.copyWith( - fontWeight: isCurrent - ? FontWeight.w700 - : FontWeight.w600, - color: isCurrent - ? ext.accent - : p.supported - ? ext.successColor - : ext.textHint, - ), + decoration: BoxDecoration( + color: isCurrent + ? ext.accent.withValues(alpha: 0.15) + : p.supported + ? ext.successColor.withValues(alpha: 0.08) + : ext.textHint.withValues(alpha: 0.06), + borderRadius: AppRadius.pillBorder, + border: Border.all( + color: isCurrent + ? ext.accent.withValues(alpha: 0.5) + : p.supported + ? ext.successColor.withValues(alpha: 0.3) + : ext.textHint.withValues(alpha: 0.15), ), - ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isCurrent) ...[ + Icon( + CupertinoIcons.checkmark_circle_fill, + size: 12, + color: ext.accent, + ), + const SizedBox(width: AppSpacing.xs), + ], + Text( + p.name, + style: AppTypography.caption1.copyWith( + fontWeight: isCurrent + ? FontWeight.w700 + : FontWeight.w600, + color: isCurrent + ? ext.accent + : p.supported + ? ext.successColor + : ext.textHint, + ), + ), + ], + ), ), ); }).toList(), @@ -527,6 +530,55 @@ class PlatformSection extends StatelessWidget { ), ); } + + void _showDistributionDialog(BuildContext context, String platformName) { + final channel = _getDistributionChannel(platformName); + showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: Text(platformName), + content: Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + t.about.distributionChannel, + style: AppTypography.caption1.copyWith( + color: ext.textSecondary, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + channel, + style: AppTypography.subhead.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + actions: [ + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () => Navigator.of(ctx).pop(), + child: Text(t.about.okButton), + ), + ], + ), + ); + } + + String _getDistributionChannel(String platformName) { + if (platformName.contains('Android')) return t.about.distAndroid; + if (platformName.contains('iOS')) return t.about.distIOS; + if (platformName.contains('macOS')) return t.about.distMacOS; + if (platformName.contains('HarmonyOS')) return t.about.distHarmony; + if (platformName.contains('Web')) return t.about.distWeb; + if (platformName.contains('Windows')) return t.about.distWindows; + return ''; + } } // ============================================================ diff --git a/lib/features/mine/profile/presentation/learn_us_sections.dart b/lib/features/mine/profile/presentation/learn_us_sections.dart index 4cafa7b0..2f691792 100644 --- a/lib/features/mine/profile/presentation/learn_us_sections.dart +++ b/lib/features/mine/profile/presentation/learn_us_sections.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 了解我们页面(分区组件) /// 创建时间: 2026-05-29 -/// 更新时间: 2026-06-01 +/// 更新时间: 2026-06-02 /// 作用: 官方网站、开发者、团队、QQ群、贡献者与特别鸣谢等分区 -/// 上次更新: 贡献者头像改为首字占位,移除emoji字段;功能分级标准 +/// 上次更新: 团队新增原生栈成员,简化贡献者和特别鸣谢列表 /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -364,6 +364,13 @@ class TeamSection extends StatelessWidget { t.about.member4Sig, '', ), + TeamMemberData( + '📱', + t.about.roleNative, + '秋叶qy', + '春风若有怜花意,可否许我再少年', + '', + ), ]; return GlassContainer( @@ -742,11 +749,7 @@ class ContributorsSection extends StatelessWidget { void _showContributorsSheet(BuildContext context) { final contributors = [ - ContributorData('AI Coder', t.about.contributorRole1), - ContributorData('AI Designer', t.about.contributorRole2), - ContributorData('AI Tester', t.about.contributorRole3), - ContributorData('AI Doc Writer', t.about.contributorRole4), - ContributorData(t.about.contributorRole5Name, t.about.contributorRole5), + ContributorData('佳佳', t.about.contributorRole5), ]; showCupertinoModalPopup( @@ -831,10 +834,6 @@ class ContributorsSection extends StatelessWidget { void _showSpecialThanksSheet(BuildContext context) { final thanks = [ - ContributorData('Flutter Team', t.about.thanksFlutter), - ContributorData('Open Source Community', t.about.thanksOpenSource), - ContributorData('QQ Group Members', t.about.thanksQQGroup), - ContributorData('All Users', t.about.thanksUsers), ContributorData('Tools/Plugins', t.about.specialThanksTools), ]; diff --git a/lib/features/mine/profile/presentation/learn_us_widgets.dart b/lib/features/mine/profile/presentation/learn_us_widgets.dart index 134613b9..aa1cb83c 100644 --- a/lib/features/mine/profile/presentation/learn_us_widgets.dart +++ b/lib/features/mine/profile/presentation/learn_us_widgets.dart @@ -508,7 +508,7 @@ class WeChatTile extends StatelessWidget { ), const SizedBox(width: AppSpacing.xs), const Text( - '微风暴', + '微风暴(微信搜索)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, diff --git a/lib/features/mine/settings/presentation/data_management_page.dart b/lib/features/mine/settings/presentation/data_management_page.dart index e83429ea..81016897 100644 --- a/lib/features/mine/settings/presentation/data_management_page.dart +++ b/lib/features/mine/settings/presentation/data_management_page.dart @@ -22,6 +22,7 @@ import '../../../../core/router/app_nav_extension.dart'; import '../../../../core/router/app_routes.dart'; import '../../../../l10n/translations.dart'; import '../../../../shared/widgets/feedback/app_toast.dart'; +import '../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../shared/widgets/containers/glass_container.dart'; import '../../../../shared/widgets/adaptive/responsive_layout.dart'; import '../providers/general_settings_provider.dart'; @@ -236,7 +237,7 @@ class _DataManagementPageState extends ConsumerState const SizedBox(height: AppSpacing.md), SizedBox( height: 160, - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( margin: EdgeInsets.zero, series: [ DoughnutSeries<_StoragePie, String>( @@ -258,7 +259,7 @@ class _DataManagementPageState extends ConsumerState ), ], ), - ), + )), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.md, diff --git a/lib/features/mine/settings/presentation/plugin/translate_plugin_page.dart b/lib/features/mine/settings/presentation/plugin/translate_plugin_page.dart index d0bbc93e..2dcacea7 100644 --- a/lib/features/mine/settings/presentation/plugin/translate_plugin_page.dart +++ b/lib/features/mine/settings/presentation/plugin/translate_plugin_page.dart @@ -20,6 +20,7 @@ import 'package:xianyan/core/storage/database/app_database.dart'; import 'package:xianyan/features/mine/settings/providers/plugin_provider.dart'; import 'package:xianyan/features/mine/settings/providers/translate_record_provider.dart'; import 'package:xianyan/features/discover/models/translate_language.dart'; +import 'package:xianyan/shared/widgets/containers/deferred_builder.dart'; import 'package:xianyan/shared/widgets/containers/glass_container.dart'; import 'package:xianyan/shared/widgets/feedback/app_toast.dart'; import 'package:xianyan/shared/widgets/adaptive/adaptive_back_button.dart'; @@ -819,7 +820,7 @@ class _TranslatePluginPageState extends ConsumerState { ), SizedBox( height: 120, - child: SfCartesianChart( + child: DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), @@ -848,7 +849,7 @@ class _TranslatePluginPageState extends ConsumerState { ), ], ), - ), + )), ], ), ); diff --git a/lib/features/mine/settings/presentation/privacy/permission_management_page.dart b/lib/features/mine/settings/presentation/privacy/permission_management_page.dart index 4ca4ca5d..72022594 100644 --- a/lib/features/mine/settings/presentation/privacy/permission_management_page.dart +++ b/lib/features/mine/settings/presentation/privacy/permission_management_page.dart @@ -16,6 +16,7 @@ import '../../../../../../core/theme/app_theme.dart'; import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; import '../../../../../../core/theme/app_radius.dart'; +import '../../../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../../../shared/widgets/containers/glass_container.dart'; import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart'; import '../../../../../l10n/translation_resolver.dart'; @@ -353,7 +354,7 @@ class _PermissionManagementPageState SizedBox( width: 160, height: 160, - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( series: >[ DoughnutSeries<_PermissionChartEntry, String>( animationDuration: 0, @@ -381,7 +382,7 @@ class _PermissionManagementPageState ), ], ), - ), + )), const SizedBox(width: AppSpacing.md), Expanded( child: Column( diff --git a/lib/features/mine/user_center/presentation/coin_log_page.dart b/lib/features/mine/user_center/presentation/coin_log_page.dart index 86cc0da2..ec54acc9 100644 --- a/lib/features/mine/user_center/presentation/coin_log_page.dart +++ b/lib/features/mine/user_center/presentation/coin_log_page.dart @@ -16,6 +16,7 @@ import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../core/theme/app_radius.dart'; +import '../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../shared/widgets/containers/glass_container.dart'; import '../providers/coin_provider.dart'; import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart'; @@ -500,7 +501,8 @@ class _CoinTrendChart extends StatelessWidget { ); final adjustedMax = maxVal == 0 ? 10.0 : maxVal * 1.2; - return SfCartesianChart( + return DeferredBuilder( + builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), @@ -543,6 +545,7 @@ class _CoinTrendChart extends StatelessWidget { name: '支出', ), ], + ), ); } } diff --git a/lib/features/mine/user_center/presentation/learning_center_page.dart b/lib/features/mine/user_center/presentation/learning_center_page.dart index 9f72c7da..e3a2ea46 100644 --- a/lib/features/mine/user_center/presentation/learning_center_page.dart +++ b/lib/features/mine/user_center/presentation/learning_center_page.dart @@ -23,6 +23,7 @@ import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../core/router/app_routes.dart'; import '../../../../core/utils/ui/interaction_animations.dart'; +import '../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../shared/widgets/containers/glass_container.dart'; import '../../../../shared/widgets/feedback/offline_banner.dart'; import '../../../../shared/widgets/adaptive/responsive_layout.dart'; @@ -407,25 +408,28 @@ class _LearningCenterPageState extends ConsumerState { SizedBox( width: 180, height: 180, - child: SfCircularChart( - margin: EdgeInsets.zero, - series: [ - DoughnutSeries<_RingData, String>( - dataSource: [ - _RingData('已完成', rate, ext.accent), - _RingData( - '剩余', - 1.0 - rate, - ext.accent.withValues(alpha: 0.12), - ), - ], - xValueMapper: (d, _) => d.label, - yValueMapper: (d, _) => d.value, - pointColorMapper: (d, _) => d.color, - innerRadius: '66%', - strokeWidth: 0, - ), - ], + child: DeferredBuilder( + builder: (context) => SfCircularChart( + margin: EdgeInsets.zero, + series: [ + DoughnutSeries<_RingData, String>( + dataSource: [ + _RingData('已完成', rate, ext.accent), + _RingData( + '剩余', + 1.0 - rate, + ext.accent.withValues(alpha: 0.12), + ), + ], + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, + pointColorMapper: (d, _) => d.color, + innerRadius: '66%', + strokeWidth: 0, + animationDuration: 0, + ), + ], + ), ), ), Column( @@ -590,60 +594,63 @@ class _LearningCenterPageState extends ConsumerState { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - margin: EdgeInsets.zero, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - ), - ), - primaryYAxis: NumericAxis( - majorGridLines: MajorGridLines( - width: 0.5, - color: ext.textHint.withValues(alpha: 0.1), - ), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 0, - ), - minimum: 0, - ), - tooltipBehavior: TooltipBehavior( - enable: true, - format: 'point.y', - ), - series: [ - AreaSeries<_TrendPoint, String>( - dataSource: points, - xValueMapper: (d, _) => d.label, - yValueMapper: (d, _) => d.value, - color: ext.accent, - borderColor: ext.accent, - borderWidth: 2.5, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - ext.accent.withValues(alpha: 0.25), - ext.accent.withValues(alpha: 0.02), - ], + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + margin: EdgeInsets.zero, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, ), - markerSettings: MarkerSettings( - isVisible: true, - height: 6, - width: 6, + ), + primaryYAxis: NumericAxis( + majorGridLines: MajorGridLines( + width: 0.5, + color: ext.textHint.withValues(alpha: 0.1), + ), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 0, + ), + minimum: 0, + ), + tooltipBehavior: TooltipBehavior( + enable: true, + format: 'point.y', + ), + series: [ + AreaSeries<_TrendPoint, String>( + dataSource: points, + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, color: ext.accent, - borderColor: ext.bgPrimary, - borderWidth: 1.5, + borderColor: ext.accent, + borderWidth: 2.5, + animationDuration: 0, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + ext.accent.withValues(alpha: 0.25), + ext.accent.withValues(alpha: 0.02), + ], + ), + markerSettings: MarkerSettings( + isVisible: true, + height: 6, + width: 6, + color: ext.accent, + borderColor: ext.bgPrimary, + borderWidth: 1.5, + ), ), - ), - ], + ], + ), ), ), ], @@ -724,37 +731,40 @@ class _LearningCenterPageState extends ConsumerState { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - margin: EdgeInsets.zero, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - ), - ), - primaryYAxis: const NumericAxis( - majorGridLines: MajorGridLines(width: 0), - majorTickLines: MajorTickLines(size: 0), - axisLine: AxisLine(width: 0), - labelStyle: TextStyle(fontSize: 0), - minimum: 0, - ), - tooltipBehavior: TooltipBehavior(enable: true), - series: [ - ColumnSeries<_CategoryItem, String>( - dataSource: categories, - xValueMapper: (d, _) => d.name, - yValueMapper: (d, _) => d.value, - pointColorMapper: (d, _) => d.color, - width: 0.5, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppRadius.md), + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + margin: EdgeInsets.zero, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, ), ), - ], + primaryYAxis: const NumericAxis( + majorGridLines: MajorGridLines(width: 0), + majorTickLines: MajorTickLines(size: 0), + axisLine: AxisLine(width: 0), + labelStyle: TextStyle(fontSize: 0), + minimum: 0, + ), + tooltipBehavior: TooltipBehavior(enable: true), + series: [ + ColumnSeries<_CategoryItem, String>( + dataSource: categories, + xValueMapper: (d, _) => d.name, + yValueMapper: (d, _) => d.value, + pointColorMapper: (d, _) => d.color, + width: 0.5, + animationDuration: 0, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppRadius.md), + ), + ), + ], + ), ), ), ], diff --git a/lib/features/mine/user_center/presentation/learning_progress_page.dart b/lib/features/mine/user_center/presentation/learning_progress_page.dart index 61a1e662..0267fda3 100644 --- a/lib/features/mine/user_center/presentation/learning_progress_page.dart +++ b/lib/features/mine/user_center/presentation/learning_progress_page.dart @@ -17,6 +17,7 @@ import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart'; +import '../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../shared/widgets/containers/glass_container.dart'; import '../providers/learning_progress_provider.dart'; @@ -209,21 +210,24 @@ class _LearningProgressPageState extends ConsumerState { SizedBox( width: 180, height: 180, - child: SfCircularChart( - margin: EdgeInsets.zero, - series: [ - DoughnutSeries<_RingData, String>( - dataSource: [ - _RingData('已完成', rate, ext.accent), - _RingData('剩余', 1.0 - rate, ext.accent.withValues(alpha: 0.12)), - ], - xValueMapper: (d, _) => d.label, - yValueMapper: (d, _) => d.value, - pointColorMapper: (d, _) => d.color, - innerRadius: '66%', - strokeWidth: 0, - ), - ], + child: DeferredBuilder( + builder: (context) => SfCircularChart( + margin: EdgeInsets.zero, + series: [ + DoughnutSeries<_RingData, String>( + dataSource: [ + _RingData('已完成', rate, ext.accent), + _RingData('剩余', 1.0 - rate, ext.accent.withValues(alpha: 0.12)), + ], + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, + pointColorMapper: (d, _) => d.color, + innerRadius: '66%', + strokeWidth: 0, + animationDuration: 0, + ), + ], + ), ), ), Column( @@ -362,61 +366,63 @@ class _LearningProgressPageState extends ConsumerState { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - margin: EdgeInsets.zero, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - ), - ), - primaryYAxis: NumericAxis( - majorGridLines: MajorGridLines( - width: 0.5, - color: ext.textHint.withValues(alpha: 0.1), - ), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 0, - ), - minimum: 0, - ), - tooltipBehavior: TooltipBehavior( - enable: true, - format: 'point.y', - ), - series: [ - AreaSeries<_TrendPoint, String>( - dataSource: points, - xValueMapper: (d, _) => d.label, - yValueMapper: (d, _) => d.value, - color: ext.accent, - borderColor: ext.accent, - borderWidth: 2.5, - animationDuration: 0, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - ext.accent.withValues(alpha: 0.25), - ext.accent.withValues(alpha: 0.02), - ], + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + margin: EdgeInsets.zero, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, ), - markerSettings: MarkerSettings( - isVisible: true, - height: 6, - width: 6, + ), + primaryYAxis: NumericAxis( + majorGridLines: MajorGridLines( + width: 0.5, + color: ext.textHint.withValues(alpha: 0.1), + ), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 0, + ), + minimum: 0, + ), + tooltipBehavior: TooltipBehavior( + enable: true, + format: 'point.y', + ), + series: [ + AreaSeries<_TrendPoint, String>( + dataSource: points, + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, color: ext.accent, - borderColor: ext.bgPrimary, - borderWidth: 1.5, + borderColor: ext.accent, + borderWidth: 2.5, + animationDuration: 0, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + ext.accent.withValues(alpha: 0.25), + ext.accent.withValues(alpha: 0.02), + ], + ), + markerSettings: MarkerSettings( + isVisible: true, + height: 6, + width: 6, + color: ext.accent, + borderColor: ext.bgPrimary, + borderWidth: 1.5, + ), ), - ), - ], + ], + ), ), ), ], @@ -493,40 +499,42 @@ class _LearningProgressPageState extends ConsumerState { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - margin: EdgeInsets.zero, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - ), - ), - primaryYAxis: const NumericAxis( - majorGridLines: MajorGridLines(width: 0), - majorTickLines: MajorTickLines(size: 0), - axisLine: AxisLine(width: 0), - labelStyle: TextStyle(fontSize: 0), - minimum: 0, - ), - tooltipBehavior: TooltipBehavior( - enable: true, - ), - series: [ - ColumnSeries<_CategoryItem, String>( - dataSource: categories, - xValueMapper: (d, _) => d.name, - yValueMapper: (d, _) => d.value, - pointColorMapper: (d, _) => d.color, - width: 0.5, - animationDuration: 0, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppRadius.md), + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + margin: EdgeInsets.zero, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, ), ), - ], + primaryYAxis: const NumericAxis( + majorGridLines: MajorGridLines(width: 0), + majorTickLines: MajorTickLines(size: 0), + axisLine: AxisLine(width: 0), + labelStyle: TextStyle(fontSize: 0), + minimum: 0, + ), + tooltipBehavior: TooltipBehavior( + enable: true, + ), + series: [ + ColumnSeries<_CategoryItem, String>( + dataSource: categories, + xValueMapper: (d, _) => d.name, + yValueMapper: (d, _) => d.value, + pointColorMapper: (d, _) => d.color, + width: 0.5, + animationDuration: 0, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppRadius.md), + ), + ), + ], + ), ), ), ], diff --git a/lib/features/mine/user_center/presentation/user_center_page.dart b/lib/features/mine/user_center/presentation/user_center_page.dart index 4cd69591..5d6a2886 100644 --- a/lib/features/mine/user_center/presentation/user_center_page.dart +++ b/lib/features/mine/user_center/presentation/user_center_page.dart @@ -22,6 +22,7 @@ import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart'; import '../../../../shared/widgets/feedback/offline_banner.dart'; import '../../../../shared/widgets/feedback/app_toast.dart'; import '../../../../shared/widgets/adaptive/responsive_layout.dart'; +import '../../../../l10n/translation_resolver.dart'; import '../../../auth/providers/auth_provider.dart'; import '../services/user_center_service.dart'; import '../providers/account_insights_provider.dart'; @@ -126,11 +127,12 @@ class _UserCenterPageState extends ConsumerState { void _showAvatarPicker() { final ext = AppTheme.ext(context); + final t = ref.read(translationsProvider); showCupertinoModalPopup( context: context, builder: (ctx) => CupertinoActionSheet( title: Text( - '修改头像', + t.profile.changeAvatar, style: AppTypography.subhead.copyWith(color: ext.textSecondary), ), actions: [ @@ -144,7 +146,7 @@ class _UserCenterPageState extends ConsumerState { children: [ Icon(CupertinoIcons.link, size: 18, color: ext.accent), const SizedBox(width: 8), - const Text('输入头像URL'), + Text(t.profile.inputAvatarUrl), ], ), ), @@ -156,7 +158,7 @@ class _UserCenterPageState extends ConsumerState { children: [ Icon(CupertinoIcons.lock, size: 18, color: ext.textHint), const SizedBox(width: 8), - Text('从相册选择(暂未开放)', style: TextStyle(color: ext.textHint)), + Text(t.profile.selectFromAlbum, style: TextStyle(color: ext.textHint)), ], ), ), @@ -164,7 +166,7 @@ class _UserCenterPageState extends ConsumerState { cancelButton: CupertinoActionSheetAction( isDestructiveAction: true, onPressed: () => Navigator.pop(ctx), - child: const Text('取消'), + child: Text(t.common.cancel), ), ), ); @@ -173,10 +175,11 @@ class _UserCenterPageState extends ConsumerState { void _showAvatarUrlInput() { final controller = TextEditingController(); final dialogExt = AppTheme.ext(context); + final t = ref.read(translationsProvider); showCupertinoDialog( context: context, builder: (ctx) => CupertinoAlertDialog( - title: const Text('🔗 输入头像URL'), + title: Text(t.profile.avatarUnderReview.replaceAll('🔍 ', '🔗 ')), content: Padding( padding: const EdgeInsets.only(top: 8), child: Column( @@ -196,7 +199,7 @@ class _UserCenterPageState extends ConsumerState { ), const SizedBox(height: 6), Text( - 'URL长度不能超过2048个字符,仅支持 http/https 开头的图片链接', + t.profile.avatarUrlHint, style: AppTypography.caption2.copyWith( color: dialogExt.textHint, ), @@ -206,7 +209,7 @@ class _UserCenterPageState extends ConsumerState { ), actions: [ CupertinoDialogAction( - child: const Text('取消'), + child: Text(t.common.cancel), onPressed: () { controller.dispose(); ctx.dismissDialog(); @@ -214,27 +217,27 @@ class _UserCenterPageState extends ConsumerState { ), CupertinoDialogAction( isDefaultAction: true, - child: const Text('确认'), + child: Text(t.common.confirm), onPressed: () async { final url = controller.text.trim(); ctx.dismissDialog(); controller.dispose(); if (url.isEmpty) { - AppToast.showWarning('请输入URL地址'); + AppToast.showWarning(t.profile.pleaseInputUrl); return; } if (!url.startsWith('http://') && !url.startsWith('https://')) { - AppToast.showWarning('请输入以 http:// 或 https:// 开头的URL'); + AppToast.showWarning(t.profile.urlMustStartWithHttp); return; } if (url.length > 2048) { - AppToast.showWarning('URL长度超过2048字符限制,请使用更短的链接'); + AppToast.showWarning(t.profile.urlTooLong); return; } try { Uri.parse(url); } catch (_) { - AppToast.showWarning('URL格式不正确,请检查后重试'); + AppToast.showWarning(t.profile.invalidUrlFormat); return; } final ext = AppTheme.ext(context); @@ -246,13 +249,13 @@ class _UserCenterPageState extends ConsumerState { children: [ CupertinoActivityIndicator(color: ext.accent, radius: 12), const SizedBox(width: 10), - const Text('🔍 图像审核中'), + Text(t.profile.avatarUnderReview), ], ), content: Padding( padding: const EdgeInsets.only(top: 8), child: Text( - '正在审核头像图片,请稍候...', + t.profile.avatarReviewing, style: AppTypography.subhead.copyWith( color: ext.textSecondary, ), @@ -266,12 +269,12 @@ class _UserCenterPageState extends ConsumerState { await ref.read(authProvider.notifier).refreshUser(); if (mounted) { Navigator.of(context, rootNavigator: true).pop(); - _showAvatarResult(true, '头像修改成功'); + _showAvatarResult(true, t.profile.avatarChangeSuccess); } } catch (e) { if (mounted) { Navigator.of(context, rootNavigator: true).pop(); - _showAvatarResult(false, '头像修改失败: $e'); + _showAvatarResult(false, '${t.profile.avatarChangeFailed}: $e'); } } }, @@ -282,6 +285,7 @@ class _UserCenterPageState extends ConsumerState { } void _showAvatarResult(bool success, String msg) { + final t = ref.read(translationsProvider); showCupertinoDialog( context: context, builder: (ctx) => CupertinoAlertDialog( @@ -298,13 +302,13 @@ class _UserCenterPageState extends ConsumerState { : CupertinoColors.systemRed, ), const SizedBox(width: 8), - Text(success ? '成功' : '失败'), + Text(success ? t.profile.success : t.profile.failed), ], ), content: Text(msg), actions: [ CupertinoDialogAction( - child: const Text('好的'), + child: Text(t.profile.ok), onPressed: () => ctx.dismissDialog(), ), ], @@ -317,12 +321,13 @@ class _UserCenterPageState extends ConsumerState { final ext = AppTheme.ext(context); final authState = ref.watch(authProvider); final user = authState.user; + final t = ref.watch(translationsProvider); return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( middle: Text( - '个人中心', + t.profile.title, style: AppTypography.title3.copyWith(color: ext.textPrimary), ), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), @@ -354,7 +359,7 @@ class _UserCenterPageState extends ConsumerState { CupertinoActivityIndicator(color: ext.accent), const SizedBox(height: AppSpacing.md), Text( - '加载中...', + t.profile.loading, style: AppTypography.subhead.copyWith( color: ext.textSecondary, ), @@ -443,6 +448,7 @@ class _UserCenterPageState extends ConsumerState { } Widget _buildLoggedOutContent(BuildContext context, AppThemeExtension ext) { + final t = ref.read(translationsProvider); return Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, @@ -458,12 +464,12 @@ class _UserCenterPageState extends ConsumerState { ), const SizedBox(height: AppSpacing.md), Text( - '请先登录', + t.profile.tapToLogin, style: AppTypography.title3.copyWith(color: ext.textPrimary), ), const SizedBox(height: AppSpacing.sm), Text( - '登录后查看个人中心', + t.profile.loginToViewProfile, style: AppTypography.subhead.copyWith(color: ext.textSecondary), ), const SizedBox(height: AppSpacing.md), @@ -472,7 +478,7 @@ class _UserCenterPageState extends ConsumerState { borderRadius: AppRadius.pillBorder, onPressed: () => context.appPush(AppRoutes.login), child: Text( - '去登录', + t.profile.goLogin, style: AppTypography.body.copyWith( color: CupertinoColors.white, fontWeight: FontWeight.w600, diff --git a/lib/features/mine/user_center/presentation/widgets/account_insights_sheet.dart b/lib/features/mine/user_center/presentation/widgets/account_insights_sheet.dart index c9f674c9..72bd6c7a 100644 --- a/lib/features/mine/user_center/presentation/widgets/account_insights_sheet.dart +++ b/lib/features/mine/user_center/presentation/widgets/account_insights_sheet.dart @@ -15,6 +15,7 @@ import '../../../../../core/theme/app_theme.dart'; import '../../../../../core/theme/app_typography.dart'; import '../../../../../shared/widgets/feedback/app_toast.dart'; import '../../../../../shared/widgets/containers/bottom_sheet.dart'; +import '../../../../auth/providers/auth_provider.dart'; import '../../models/account_insight_model.dart'; import '../../providers/account_insights_provider.dart'; @@ -53,6 +54,7 @@ class _AccountInsightsSheetContentState children: [ _buildHandle(), _buildHeader(ext, insightsState), + _buildTestAccountWarning(ext), Expanded( child: insightsState.isLoading ? Center(child: CupertinoActivityIndicator(color: ext.accent)) @@ -125,6 +127,45 @@ class _AccountInsightsSheetContentState ); } + Widget _buildTestAccountWarning(AppThemeExtension ext) { + final username = ref.watch(authProvider).user?.username; + if (username != '123456') return const SizedBox.shrink(); + return Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: CupertinoColors.systemOrange.withValues(alpha: 0.12), + borderRadius: AppRadius.lgBorder, + border: Border.all( + color: CupertinoColors.systemOrange.withValues(alpha: 0.25), + width: 0.5, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + CupertinoIcons.exclamationmark_triangle_fill, + size: 18, + color: CupertinoColors.systemOrange.darkColor, + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + '⚠️ 当前账号可能是闲言官方提供的测试账号,多人在使用,请勿修改密码,请勿使用当前账号创建笔记,如有需要请自行注册账户', + style: AppTypography.caption1.copyWith( + color: CupertinoColors.systemOrange.darkColor, + fontWeight: FontWeight.w500, + height: 1.4, + ), + ), + ), + ], + ), + ); + } + Widget _buildInsightList( AppThemeExtension ext, List insights, diff --git a/lib/features/mine/user_center/presentation/widgets/learning_charts.dart b/lib/features/mine/user_center/presentation/widgets/learning_charts.dart index 319840d0..76c15188 100644 --- a/lib/features/mine/user_center/presentation/widgets/learning_charts.dart +++ b/lib/features/mine/user_center/presentation/widgets/learning_charts.dart @@ -14,6 +14,7 @@ import '../../../../../../core/theme/app_radius.dart'; import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_theme.dart'; import '../../../../../../core/theme/app_typography.dart'; +import '../../../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../../../shared/widgets/containers/glass_container.dart'; // ============================================================ @@ -75,61 +76,63 @@ class WeeklyTrendChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 160, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - margin: EdgeInsets.zero, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 10, - ), - ), - primaryYAxis: NumericAxis( - majorGridLines: MajorGridLines( - width: 1, - color: ext.textHint.withValues(alpha: 0.08), - ), - majorTickLines: const MajorTickLines(size: 0), - axisLine: const AxisLine(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textHint, - fontSize: 10, - ), - minimum: 0, - ), - tooltipBehavior: TooltipBehavior( - enable: true, - format: 'point.y 次', - ), - series: [ - AreaSeries<_TrendPoint, String>( - dataSource: trendData, - xValueMapper: (d, _) => d.label, - yValueMapper: (d, _) => d.value, - color: ext.accent, - borderColor: ext.accent, - borderWidth: 2.5, - animationDuration: 0, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - ext.accent.withValues(alpha: 0.2), - ext.accent.withValues(alpha: 0.02), - ], + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + margin: EdgeInsets.zero, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 10, ), - markerSettings: MarkerSettings( - isVisible: true, - height: 7, - width: 7, + ), + primaryYAxis: NumericAxis( + majorGridLines: MajorGridLines( + width: 1, + color: ext.textHint.withValues(alpha: 0.08), + ), + majorTickLines: const MajorTickLines(size: 0), + axisLine: const AxisLine(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textHint, + fontSize: 10, + ), + minimum: 0, + ), + tooltipBehavior: TooltipBehavior( + enable: true, + format: 'point.y 次', + ), + series: [ + AreaSeries<_TrendPoint, String>( + dataSource: trendData, + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, color: ext.accent, - borderColor: ext.bgCard, + borderColor: ext.accent, + borderWidth: 2.5, + animationDuration: 0, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + ext.accent.withValues(alpha: 0.2), + ext.accent.withValues(alpha: 0.02), + ], + ), + markerSettings: MarkerSettings( + isVisible: true, + height: 7, + width: 7, + color: ext.accent, + borderColor: ext.bgCard, + ), ), - ), - ], + ], + ), ), ), ], diff --git a/lib/features/onboarding/presentation/pages/agreement_page.dart b/lib/features/onboarding/presentation/pages/agreement_page.dart index 3d0839a2..235c2048 100644 --- a/lib/features/onboarding/presentation/pages/agreement_page.dart +++ b/lib/features/onboarding/presentation/pages/agreement_page.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — 软件协议页(引导页第2页) // 创建时间: 2026-05-21 -// 更新时间: 2026-05-30 +// 更新时间: 2026-06-02 // 作用: 隐私政策/用户协议/权限说明,勾选同意后继续 -// 上次更新: 协议勾选文字《》内容使用强调色+可点击跳转对应Tab +// 上次更新: 协议内容/标题/更新日期支持多语言,章节解析兼容英文罗马数字编号 // ============================================================ import 'package:flutter/cupertino.dart'; @@ -15,6 +15,7 @@ import '../../../../core/constants/character_expression.dart'; import '../../../../l10n/translations.dart'; import '../../../../core/router/app_nav_extension.dart'; import '../../../../core/router/app_routes.dart'; +import '../../../../l10n/app_locale.dart'; import '../../../../core/services/auth/permission_service.dart'; import '../../../../core/services/device/haptic_service.dart'; import '../../../../core/theme/app_radius.dart'; @@ -25,6 +26,7 @@ import '../../../../shared/widgets/animation/appbar_character_sprite.dart'; import '../../../../shared/widgets/containers/glass_container.dart'; import '../../../agreements/data/agreement_data.dart'; import '../../../agreements/data/agreement_types.dart'; +import '../../../../shared/widgets/media/watermarked_copyright_image.dart'; import '../../providers/onboarding_provider.dart'; import '../onboarding_page.dart'; import '../widgets/page_nav_header.dart'; @@ -91,20 +93,28 @@ class _AgreementPageState extends ConsumerState { ); } - static List<_ChapterInfo> _parseChapters(String content) { - final regex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+'); + static List<_ChapterInfo> _parseChapters(String content, String languageId) { + final zhRegex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+'); + final enRegex = RegExp(r'^(Zero|[IVXLCDM]+)\.\s+.+'); final lines = content.split('\n'); final chapters = <_ChapterInfo>[]; for (final line in lines) { final trimmed = line.trim(); if (trimmed.isEmpty) continue; - if (regex.hasMatch(trimmed)) { + if (zhRegex.hasMatch(trimmed)) { final title = trimmed .replaceAll(RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、'), '') .trim(); if (title.isNotEmpty) { chapters.add(_ChapterInfo(fullTitle: trimmed, shortTitle: title)); } + } else if (enRegex.hasMatch(trimmed)) { + final title = trimmed + .replaceFirst(RegExp(r'^(Zero|[IVXLCDM]+)\.\s+'), '') + .trim(); + if (title.isNotEmpty) { + chapters.add(_ChapterInfo(fullTitle: trimmed, shortTitle: title)); + } } } return chapters; @@ -116,6 +126,7 @@ class _AgreementPageState extends ConsumerState { final state = ref.watch(onboardingProvider); final notifier = ref.read(onboardingProvider.notifier); final ob = ref.watch(translationsProvider).onboarding; + final languageId = _localeToLanguageId(ref.watch(appLocaleProvider)); return LayoutBuilder( builder: (context, constraints) { @@ -149,7 +160,7 @@ class _AgreementPageState extends ConsumerState { const SizedBox(height: AppSpacing.xs), _buildTabBar(ext, state, notifier, ob), const SizedBox(height: AppSpacing.xs), - Expanded(child: _buildContent(ext, state, ob)), + Expanded(child: _buildContent(ext, state, ob, languageId)), const SizedBox(height: AppSpacing.xs), _buildCheckboxes(ext, state, notifier, ob), const SizedBox(height: AppSpacing.xs), @@ -268,15 +279,16 @@ class _AgreementPageState extends ConsumerState { AppThemeExtension ext, OnboardingState state, TOnboarding ob, + String languageId, ) { if (state.agreementTabIndex == 2) { return _buildPermissionList(ext, ob); } final agreementType = _agreementTypeForIndex(state.agreementTabIndex); - final content = AgreementData.getContent(agreementType); - final updateDate = AgreementData.getUpdateDate(agreementType); - final chapters = _parseChapters(content); + final content = AgreementData.getContent(agreementType, languageId: languageId); + final updateDate = AgreementData.getUpdateDate(agreementType, languageId: languageId); + final chapters = _parseChapters(content, languageId); if (_chapterKeys.isEmpty && chapters.isNotEmpty) { for (int i = 0; i < chapters.length; i++) { @@ -302,6 +314,7 @@ class _AgreementPageState extends ConsumerState { updateDate, chapters, ob, + languageId, ), ), ), @@ -359,6 +372,7 @@ class _AgreementPageState extends ConsumerState { String updateDate, List<_ChapterInfo> chapters, TOnboarding ob, + String languageId, ) { final lines = content.split('\n'); final chapterIndexMap = {}; @@ -366,13 +380,16 @@ class _AgreementPageState extends ConsumerState { chapterIndexMap[chapters[i].fullTitle] = i; } + final zhChapterRegex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+'); + final enChapterRegex = RegExp(r'^(Zero|[IVXLCDM]+)\.\s+.+'); + final widgets = [ Row( children: [ Icon(agreementType.icon, size: 18, color: ext.accent), const SizedBox(width: AppSpacing.sm), Text( - agreementType.title, + agreementType.titleFor(languageId), style: AppTypography.headline.copyWith(color: ext.textPrimary), ), ], @@ -387,13 +404,14 @@ class _AgreementPageState extends ConsumerState { const SizedBox(height: AppSpacing.sm), ]; - final regex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+'); - for (final line in lines) { final trimmed = line.trim(); if (trimmed.isEmpty) continue; - if (regex.hasMatch(trimmed)) { + final isZhChapter = zhChapterRegex.hasMatch(trimmed); + final isEnChapter = enChapterRegex.hasMatch(trimmed); + + if (isZhChapter || isEnChapter) { final chIdx = chapterIndexMap[trimmed]; final key = chIdx != null && _chapterKeys.containsKey('ch_$chIdx') ? _chapterKeys['ch_$chIdx'] @@ -430,6 +448,16 @@ class _AgreementPageState extends ConsumerState { ), ), ); + + if (trimmed.contains('软件著作权') || + trimmed.contains('Software Copyright')) { + widgets.add( + const Padding( + padding: EdgeInsets.only(top: AppSpacing.sm), + child: WatermarkedCopyrightImage(), + ), + ); + } } else if (trimmed.startsWith('•') || trimmed.startsWith('-')) { widgets.add( Padding( @@ -948,6 +976,16 @@ class _AgreementPageState extends ConsumerState { }; } + String _localeToLanguageId(Locale locale) { + if (locale.languageCode == 'zh') { + if (locale.countryCode == 'TW' || locale.scriptCode == 'Hant') { + return 'zh_tw'; + } + return 'zh'; + } + return locale.languageCode; + } + Widget _buildSkipButton( AppThemeExtension ext, OnboardingState state, diff --git a/lib/features/reading_report/presentation/widgets/trend_chart.dart b/lib/features/reading_report/presentation/widgets/trend_chart.dart index 941cf93d..5b2ec2b7 100644 --- a/lib/features/reading_report/presentation/widgets/trend_chart.dart +++ b/lib/features/reading_report/presentation/widgets/trend_chart.dart @@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/widgets/containers/deferred_builder.dart'; import '../../models/reading_report_models.dart'; class _TrendData { @@ -87,7 +88,8 @@ class TrendChart extends StatelessWidget { return SizedBox( height: 220, - child: SfCartesianChart( + child: DeferredBuilder( + builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: NumericAxis( majorGridLines: const MajorGridLines(width: 0), @@ -123,6 +125,7 @@ class TrendChart extends StatelessWidget { ), series: series, ), + ), ); } diff --git a/lib/features/tool_center/leisure/presentation/pages/leisure_settings_sections.dart b/lib/features/tool_center/leisure/presentation/pages/leisure_settings_sections.dart index 3a2d4b49..6c8192f2 100644 --- a/lib/features/tool_center/leisure/presentation/pages/leisure_settings_sections.dart +++ b/lib/features/tool_center/leisure/presentation/pages/leisure_settings_sections.dart @@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:xianyan/core/theme/app_theme.dart'; +import 'package:xianyan/shared/widgets/containers/deferred_builder.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; @@ -93,7 +94,7 @@ Widget buildBookmarkTrend( borderRadius: AppRadius.lgBorder, ), height: 160, - child: SfCartesianChart( + child: DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: const NumericAxis(isVisible: false), primaryYAxis: const NumericAxis(isVisible: false), @@ -117,7 +118,7 @@ Widget buildBookmarkTrend( ), ], ), - ); + )); } Widget buildMonthDistribution( @@ -151,7 +152,7 @@ Widget buildMonthDistribution( child: Row( children: [ Expanded( - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( series: >[ DoughnutSeries<_SeasonData, String>( dataSource: [ @@ -185,7 +186,7 @@ Widget buildMonthDistribution( ), ], ), - ), + )), const SizedBox(width: AppSpacing.sm), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/tool_center/statistics/presentation/statistics_page.dart b/lib/features/tool_center/statistics/presentation/statistics_page.dart index 95d7c66f..39f02d09 100644 --- a/lib/features/tool_center/statistics/presentation/statistics_page.dart +++ b/lib/features/tool_center/statistics/presentation/statistics_page.dart @@ -21,6 +21,7 @@ import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../shared/widgets/animation/animated_widgets.dart'; import '../../../../shared/widgets/display/app_icon.dart'; +import '../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../shared/widgets/containers/glass_container.dart'; import '../../../../shared/widgets/feedback/offline_banner.dart'; import '../../../../shared/widgets/adaptive/responsive_layout.dart'; @@ -1086,7 +1087,7 @@ class _SigninTrendChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 160, - child: SfCartesianChart( + child: DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), @@ -1124,7 +1125,7 @@ class _SigninTrendChart extends StatelessWidget { ), ], ), - ), + )), ], ), ), @@ -1276,7 +1277,7 @@ class _CoinBarChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 160, - child: SfCartesianChart( + child: DeferredBuilder(builder: (context) => SfCartesianChart( plotAreaBorderWidth: 0, primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), @@ -1309,7 +1310,7 @@ class _CoinBarChart extends StatelessWidget { ), ], ), - ), + )), ], ), ), @@ -1388,7 +1389,7 @@ class _ContentPieChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 200, - child: SfCircularChart( + child: DeferredBuilder(builder: (context) => SfCircularChart( series: >[ DoughnutSeries<_NotePie, String>( dataSource: pieData, @@ -1408,7 +1409,7 @@ class _ContentPieChart extends StatelessWidget { ), ], ), - ), + )), ], ), ), diff --git a/lib/features/tool_center/statistics/presentation/widgets/coin_stats_tab.dart b/lib/features/tool_center/statistics/presentation/widgets/coin_stats_tab.dart index 019a4413..bd929b9c 100644 --- a/lib/features/tool_center/statistics/presentation/widgets/coin_stats_tab.dart +++ b/lib/features/tool_center/statistics/presentation/widgets/coin_stats_tab.dart @@ -16,6 +16,7 @@ import '../../../../../core/theme/app_radius.dart'; import '../../../../../core/theme/app_spacing.dart'; import '../../../../../core/theme/app_theme.dart'; import '../../../../../core/theme/app_typography.dart'; +import '../../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../../shared/widgets/containers/glass_container.dart'; import '../../providers/user_stats_provider.dart'; import 'stats_shared.dart'; @@ -151,37 +152,40 @@ class CoinTrendChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - interval: 5, - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 10, + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + interval: 5, + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 10, + ), ), + primaryYAxis: NumericAxis( + majorGridLines: MajorGridLines( + width: 1, + color: ext.textHint.withValues(alpha: 0.08), + ), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textHint, + fontSize: 10, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + LineSeries<_CoinTrend, String>( + dataSource: trendData, + xValueMapper: (_CoinTrend d, _) => d.label, + yValueMapper: (_CoinTrend d, _) => d.value, + color: const Color(0xFFF39C12), + width: 2.5, + animationDuration: 0, + name: '积分', + ), + ], ), - primaryYAxis: NumericAxis( - majorGridLines: MajorGridLines( - width: 1, - color: ext.textHint.withValues(alpha: 0.08), - ), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textHint, - fontSize: 10, - ), - ), - tooltipBehavior: TooltipBehavior(enable: true), - series: >[ - LineSeries<_CoinTrend, String>( - dataSource: trendData, - xValueMapper: (_CoinTrend d, _) => d.label, - yValueMapper: (_CoinTrend d, _) => d.value, - color: const Color(0xFFF39C12), - width: 2.5, - name: '积分', - ), - ], ), ), ], @@ -257,39 +261,41 @@ class CoinSourceBarChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 10, - ), - ), - primaryYAxis: NumericAxis( - maximum: maxVal, - majorGridLines: const MajorGridLines(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textHint, - fontSize: 10, - ), - ), - tooltipBehavior: TooltipBehavior(enable: true), - series: >[ - ColumnSeries<_SourceBar, String>( - dataSource: barData, - xValueMapper: (_SourceBar d, _) => d.label, - yValueMapper: (_SourceBar d, _) => d.value, - pointColorMapper: (_SourceBar d, _) => d.color, - width: 0.5, - animationDuration: 0, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(6), - topRight: Radius.circular(6), + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 10, ), - name: '数量', ), - ], + primaryYAxis: NumericAxis( + maximum: maxVal, + majorGridLines: const MajorGridLines(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textHint, + fontSize: 10, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + ColumnSeries<_SourceBar, String>( + dataSource: barData, + xValueMapper: (_SourceBar d, _) => d.label, + yValueMapper: (_SourceBar d, _) => d.value, + pointColorMapper: (_SourceBar d, _) => d.color, + width: 0.5, + animationDuration: 0, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + name: '数量', + ), + ], + ), ), ), ], @@ -353,30 +359,32 @@ class CoinRingChart extends StatelessWidget { Expanded( child: SizedBox( height: 180, - child: SfCircularChart( - series: >[ - DoughnutSeries<_RingData, String>( - dataSource: ringData, - xValueMapper: (_RingData d, _) => d.label, - yValueMapper: (_RingData d, _) => d.value, - pointColorMapper: (_RingData d, _) => d.color, - innerRadius: '45%', - animationDuration: 0, - dataLabelSettings: DataLabelSettings( - isVisible: true, - textStyle: AppTypography.caption2.copyWith( - color: hasData - ? CupertinoColors.white - : ext.textHint, - fontWeight: FontWeight.w600, + child: DeferredBuilder( + builder: (context) => SfCircularChart( + series: >[ + DoughnutSeries<_RingData, String>( + dataSource: ringData, + xValueMapper: (_RingData d, _) => d.label, + yValueMapper: (_RingData d, _) => d.value, + pointColorMapper: (_RingData d, _) => d.color, + innerRadius: '45%', + animationDuration: 0, + dataLabelSettings: DataLabelSettings( + isVisible: true, + textStyle: AppTypography.caption2.copyWith( + color: hasData + ? CupertinoColors.white + : ext.textHint, + fontWeight: FontWeight.w600, + ), ), + dataLabelMapper: (_RingData d, _) { + if (!hasData) return d.label; + return d.label; + }, ), - dataLabelMapper: (_RingData d, _) { - if (!hasData) return d.label; - return d.label; - }, - ), - ], + ], + ), ), ), ), diff --git a/lib/features/tool_center/statistics/presentation/widgets/favorite_stats_tab.dart b/lib/features/tool_center/statistics/presentation/widgets/favorite_stats_tab.dart index b612e1df..397ec8bc 100644 --- a/lib/features/tool_center/statistics/presentation/widgets/favorite_stats_tab.dart +++ b/lib/features/tool_center/statistics/presentation/widgets/favorite_stats_tab.dart @@ -16,6 +16,7 @@ import '../../../../../core/theme/app_radius.dart'; import '../../../../../core/theme/app_spacing.dart'; import '../../../../../core/theme/app_theme.dart'; import '../../../../../core/theme/app_typography.dart'; +import '../../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../../shared/widgets/containers/glass_container.dart'; import '../../providers/user_stats_provider.dart'; import 'stats_shared.dart'; @@ -144,27 +145,29 @@ class FavoriteCategoryPie extends StatelessWidget { child: Row( children: [ Expanded( - child: SfCircularChart( - series: >[ - DoughnutSeries<_PieItem, String>( - dataSource: pieData, - xValueMapper: (_PieItem d, _) => d.label, - yValueMapper: (_PieItem d, _) => d.value, - pointColorMapper: (_PieItem d, _) => d.color, - innerRadius: '38%', - animationDuration: 0, - dataLabelSettings: DataLabelSettings( - isVisible: true, - textStyle: AppTypography.caption2.copyWith( - color: CupertinoColors.white, - fontWeight: FontWeight.w600, + child: DeferredBuilder( + builder: (context) => SfCircularChart( + series: >[ + DoughnutSeries<_PieItem, String>( + dataSource: pieData, + xValueMapper: (_PieItem d, _) => d.label, + yValueMapper: (_PieItem d, _) => d.value, + pointColorMapper: (_PieItem d, _) => d.color, + innerRadius: '38%', + animationDuration: 0, + dataLabelSettings: DataLabelSettings( + isVisible: true, + textStyle: AppTypography.caption2.copyWith( + color: CupertinoColors.white, + fontWeight: FontWeight.w600, + ), ), + dataLabelMapper: (_PieItem d, _) => total > 0 + ? '${(d.value / total * 100).toStringAsFixed(0)}%' + : '', ), - dataLabelMapper: (_PieItem d, _) => total > 0 - ? '${(d.value / total * 100).toStringAsFixed(0)}%' - : '', - ), - ], + ], + ), ), ), const SizedBox(width: AppSpacing.md), @@ -277,44 +280,46 @@ class FavoriteTrendChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 10, - ), - ), - primaryYAxis: NumericAxis( - majorGridLines: MajorGridLines( - width: 1, - color: ext.textHint.withValues(alpha: 0.08), - ), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textHint, - fontSize: 10, - ), - ), - tooltipBehavior: TooltipBehavior(enable: true), - series: >[ - LineSeries<_LinePoint, String>( - dataSource: trendData, - xValueMapper: (_LinePoint d, _) => d.label, - yValueMapper: (_LinePoint d, _) => d.value, - color: const Color(0xFFFF6B6B), - width: 2.5, - animationDuration: 0, - markerSettings: const MarkerSettings( - isVisible: true, - height: 7, - width: 7, - color: Color(0xFFFF6B6B), - borderColor: CupertinoColors.white, + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 10, ), - name: '收藏', ), - ], + primaryYAxis: NumericAxis( + majorGridLines: MajorGridLines( + width: 1, + color: ext.textHint.withValues(alpha: 0.08), + ), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textHint, + fontSize: 10, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + LineSeries<_LinePoint, String>( + dataSource: trendData, + xValueMapper: (_LinePoint d, _) => d.label, + yValueMapper: (_LinePoint d, _) => d.value, + color: const Color(0xFFFF6B6B), + width: 2.5, + animationDuration: 0, + markerSettings: const MarkerSettings( + isVisible: true, + height: 7, + width: 7, + color: Color(0xFFFF6B6B), + borderColor: CupertinoColors.white, + ), + name: '收藏', + ), + ], + ), ), ), ], @@ -370,44 +375,46 @@ class FavoriteGroupBarChart extends StatelessWidget { const SizedBox(height: AppSpacing.md), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 10, - ), - ), - primaryYAxis: NumericAxis( - maximum: maxVal, - majorGridLines: const MajorGridLines(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textHint, - fontSize: 10, - ), - ), - tooltipBehavior: TooltipBehavior(enable: true), - series: >[ - ColumnSeries<_BarItem, String>( - dataSource: items - .map( - (e) => - _BarItem(e.group, e.count.toDouble(), ext.accent), - ) - .toList(), - xValueMapper: (_BarItem d, _) => d.label, - yValueMapper: (_BarItem d, _) => d.value, - color: ext.accent, - width: 0.5, - animationDuration: 0, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(6), - topRight: Radius.circular(6), + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 10, ), - name: '数量', ), - ], + primaryYAxis: NumericAxis( + maximum: maxVal, + majorGridLines: const MajorGridLines(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textHint, + fontSize: 10, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + ColumnSeries<_BarItem, String>( + dataSource: items + .map( + (e) => + _BarItem(e.group, e.count.toDouble(), ext.accent), + ) + .toList(), + xValueMapper: (_BarItem d, _) => d.label, + yValueMapper: (_BarItem d, _) => d.value, + color: ext.accent, + width: 0.5, + animationDuration: 0, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + name: '数量', + ), + ], + ), ), ), ], diff --git a/lib/features/tool_center/statistics/presentation/widgets/learning_stats_tab.dart b/lib/features/tool_center/statistics/presentation/widgets/learning_stats_tab.dart index 897fb87f..4363e549 100644 --- a/lib/features/tool_center/statistics/presentation/widgets/learning_stats_tab.dart +++ b/lib/features/tool_center/statistics/presentation/widgets/learning_stats_tab.dart @@ -17,6 +17,7 @@ import '../../../../../core/theme/app_radius.dart'; import '../../../../../core/theme/app_spacing.dart'; import '../../../../../core/theme/app_theme.dart'; import '../../../../../core/theme/app_typography.dart'; +import '../../../../../shared/widgets/containers/deferred_builder.dart'; import '../../../../../shared/widgets/containers/glass_container.dart'; import '../../providers/user_stats_provider.dart'; import 'stats_shared.dart'; @@ -190,74 +191,78 @@ class LearningTrendChart extends StatelessWidget { const SizedBox(height: AppSpacing.sm), SizedBox( height: 180, - child: SfCartesianChart( - plotAreaBorderWidth: 0, - primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textSecondary, - fontSize: 10, + child: DeferredBuilder( + builder: (context) => SfCartesianChart( + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 10, + ), ), + primaryYAxis: NumericAxis( + majorGridLines: MajorGridLines( + width: 1, + color: ext.textHint.withValues(alpha: 0.08), + ), + labelStyle: AppTypography.caption2.copyWith( + color: ext.textHint, + fontSize: 10, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + LineSeries<_TrendPoint, String>( + dataSource: viewData, + xValueMapper: (_TrendPoint d, _) => d.label, + yValueMapper: (_TrendPoint d, _) => d.value, + color: const Color(0xFF6C63FF), + width: 2.5, + animationDuration: 0, + markerSettings: const MarkerSettings( + isVisible: true, + height: 6, + width: 6, + color: Color(0xFF6C63FF), + borderColor: CupertinoColors.white, + ), + name: '浏览', + ), + LineSeries<_TrendPoint, String>( + dataSource: interactionData, + xValueMapper: (_TrendPoint d, _) => d.label, + yValueMapper: (_TrendPoint d, _) => d.value, + color: const Color(0xFF4ECDC4), + width: 2.5, + animationDuration: 0, + markerSettings: const MarkerSettings( + isVisible: true, + height: 6, + width: 6, + color: Color(0xFF4ECDC4), + borderColor: CupertinoColors.white, + ), + name: '互动', + ), + LineSeries<_TrendPoint, String>( + dataSource: favData, + xValueMapper: (_TrendPoint d, _) => d.label, + yValueMapper: (_TrendPoint d, _) => d.value, + color: const Color(0xFFFF6B6B), + width: 2.5, + animationDuration: 0, + markerSettings: const MarkerSettings( + isVisible: true, + height: 6, + width: 6, + color: Color(0xFFFF6B6B), + borderColor: CupertinoColors.white, + ), + name: '收藏', + ), + ], ), - primaryYAxis: NumericAxis( - majorGridLines: MajorGridLines( - width: 1, - color: ext.textHint.withValues(alpha: 0.08), - ), - labelStyle: AppTypography.caption2.copyWith( - color: ext.textHint, - fontSize: 10, - ), - ), - tooltipBehavior: TooltipBehavior(enable: true), - series: >[ - LineSeries<_TrendPoint, String>( - dataSource: viewData, - xValueMapper: (_TrendPoint d, _) => d.label, - yValueMapper: (_TrendPoint d, _) => d.value, - color: const Color(0xFF6C63FF), - width: 2.5, - animationDuration: 0, - markerSettings: const MarkerSettings( - isVisible: true, - height: 6, - width: 6, - color: Color(0xFF6C63FF), - borderColor: CupertinoColors.white, - ), - name: '浏览', - ), - LineSeries<_TrendPoint, String>( - dataSource: interactionData, - xValueMapper: (_TrendPoint d, _) => d.label, - yValueMapper: (_TrendPoint d, _) => d.value, - color: const Color(0xFF4ECDC4), - width: 2.5, - markerSettings: const MarkerSettings( - isVisible: true, - height: 6, - width: 6, - color: Color(0xFF4ECDC4), - borderColor: CupertinoColors.white, - ), - name: '互动', - ), - LineSeries<_TrendPoint, String>( - dataSource: favData, - xValueMapper: (_TrendPoint d, _) => d.label, - yValueMapper: (_TrendPoint d, _) => d.value, - color: const Color(0xFFFF6B6B), - width: 2.5, - markerSettings: const MarkerSettings( - isVisible: true, - height: 6, - width: 6, - color: Color(0xFFFF6B6B), - borderColor: CupertinoColors.white, - ), - name: '收藏', - ), - ], ), ), ], @@ -335,27 +340,29 @@ class CategoryPieChart extends StatelessWidget { Expanded( child: SizedBox( height: 180, - child: SfCircularChart( - series: >[ - DoughnutSeries<_CatData, String>( - dataSource: pieData, - xValueMapper: (_CatData d, _) => d.label, - yValueMapper: (_CatData d, _) => d.value, - pointColorMapper: (_CatData d, _) => d.color, - innerRadius: '38%', - animationDuration: 0, - dataLabelSettings: DataLabelSettings( - isVisible: true, - textStyle: AppTypography.caption2.copyWith( - color: CupertinoColors.white, - fontWeight: FontWeight.w600, + child: DeferredBuilder( + builder: (context) => SfCircularChart( + series: >[ + DoughnutSeries<_CatData, String>( + dataSource: pieData, + xValueMapper: (_CatData d, _) => d.label, + yValueMapper: (_CatData d, _) => d.value, + pointColorMapper: (_CatData d, _) => d.color, + innerRadius: '38%', + animationDuration: 0, + dataLabelSettings: DataLabelSettings( + isVisible: true, + textStyle: AppTypography.caption2.copyWith( + color: CupertinoColors.white, + fontWeight: FontWeight.w600, + ), ), + dataLabelMapper: (_CatData d, _) => total > 0 + ? '${(d.value / total * 100).toStringAsFixed(0)}%' + : '', ), - dataLabelMapper: (_CatData d, _) => total > 0 - ? '${(d.value / total * 100).toStringAsFixed(0)}%' - : '', - ), - ], + ], + ), ), ), ), diff --git a/lib/features/widget/presentation/widget_management_page.dart b/lib/features/widget/presentation/widget_management_page.dart index b22e2f78..778b9aee 100644 --- a/lib/features/widget/presentation/widget_management_page.dart +++ b/lib/features/widget/presentation/widget_management_page.dart @@ -483,81 +483,122 @@ class _PriorityDot extends StatelessWidget { } } -class _PlatformCompatibilityCard extends StatelessWidget { +class _PlatformCompatibilityCard extends StatefulWidget { const _PlatformCompatibilityCard({required this.ext}); final AppThemeExtension ext; + @override + State<_PlatformCompatibilityCard> createState() => + _PlatformCompatibilityCardState(); +} + +class _PlatformCompatibilityCardState + extends State<_PlatformCompatibilityCard> { + bool _isExpanded = false; + @override Widget build(BuildContext context) { + final ext = widget.ext; return GlassContainer( depth: GlassDepth.elevated, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Icon( - CupertinoIcons.info_circle_fill, - size: 18, - color: ext.accent, - ), - const SizedBox(width: AppSpacing.sm), - Text( - '平台兼容说明', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.sm), - _PlatformRow( - ext: ext, - icon: '🤖', - name: 'Android', - desc: '功能完整,支持交互式按钮和快捷固定', - ), - const SizedBox(height: 4), - _PlatformRow( - ext: ext, - icon: '🍎', - name: 'iOS', - desc: 'WidgetKit + SwiftUI,交互需 iOS 17+', - ), - const SizedBox(height: 4), - _PlatformRow( - ext: ext, - icon: '🔴', - name: '鸿蒙', - desc: 'FormExtension + ArkUI,无快捷固定,无实时刷新', - ), - const SizedBox(height: AppSpacing.sm), - Container( - padding: const EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: ext.accent.withValues(alpha: 0.06), - borderRadius: AppRadius.smBorder, - ), + GestureDetector( + onTap: () => setState(() => _isExpanded = !_isExpanded), + behavior: HitTestBehavior.opaque, child: Row( children: [ Icon( - CupertinoIcons.lightbulb_fill, - size: 14, + CupertinoIcons.info_circle_fill, + size: 18, color: ext.accent, ), - const SizedBox(width: AppSpacing.xs), + const SizedBox(width: AppSpacing.sm), Expanded( child: Text( - '点击「同步主题」可将当前深色/浅色模式推送到所有已安装小部件', - style: AppTypography.footnote.copyWith( - color: ext.textSecondary, + '平台兼容说明', + style: AppTypography.subhead.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, ), ), ), + AnimatedRotation( + turns: _isExpanded ? 0.0 : -0.25, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: Icon( + CupertinoIcons.chevron_down, + size: 16, + color: ext.textHint, + ), + ), ], ), ), + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _PlatformRow( + ext: ext, + icon: '🤖', + name: 'Android', + desc: '功能不完整,原生侧存在通信问题', + ), + const SizedBox(height: 4), + _PlatformRow( + ext: ext, + icon: '🍎', + name: 'iOS', + desc: 'WidgetKit + SwiftUI,交互需 iOS 17+', + ), + const SizedBox(height: 4), + _PlatformRow( + ext: ext, + icon: '🔴', + name: '鸿蒙', + desc: 'FormExtension + ArkUI,能力受限,系统限制刷新频率', + ), + const SizedBox(height: AppSpacing.sm), + Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.06), + borderRadius: AppRadius.smBorder, + ), + child: Row( + children: [ + Icon( + CupertinoIcons.lightbulb_fill, + size: 14, + color: ext.accent, + ), + const SizedBox(width: AppSpacing.xs), + Expanded( + child: Text( + '点击「同步主题」可将当前深色/浅色模式推送到所有已安装小部件', + style: AppTypography.footnote.copyWith( + color: ext.textSecondary, + ), + ), + ), + ], + ), + ), + ], + ), + ), + crossFadeState: _isExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 250), + sizeCurve: Curves.easeInOut, + ), ], ), ); diff --git a/lib/l10n/languages/ar.dart b/lib/l10n/languages/ar.dart index ebf19717..73770269 100644 --- a/lib/l10n/languages/ar.dart +++ b/lib/l10n/languages/ar.dart @@ -530,6 +530,24 @@ const ar = T( appStoreNotFound: 'لم يتم العثور على متجر التطبيقات', experimentalFeature: 'ميزات تجريبية', underReview: 'قيد المراجعة', + changeAvatar: 'تغيير الصورة الرمزية', + inputAvatarUrl: 'أدخل رابط الصورة الرمزية', + selectFromAlbum: 'اختر من الألبوم (قريباً)', + avatarUrlHint: 'يجب ألا يتجاوز الرابط 2048 حرفاً، روابط http/https فقط', + pleaseInputUrl: 'الرجاء إدخال رابط', + urlMustStartWithHttp: 'يجب أن يبدأ الرابط بـ http:// أو https://', + urlTooLong: 'الرابط يتجاوز حد 2048 حرفاً', + invalidUrlFormat: 'صيغة الرابط غير صحيحة', + avatarUnderReview: '🔍 الصورة قيد المراجعة', + avatarReviewing: 'جاري مراجعة الصورة الرمزية، يرجى الانتظار...', + avatarChangeSuccess: 'تم تغيير الصورة الرمزية بنجاح', + avatarChangeFailed: 'فشل تغيير الصورة الرمزية', + success: 'نجاح', + failed: 'فشل', + ok: 'حسناً', + loading: 'جاري التحميل...', + loginToViewProfile: 'سجل الدخول لعرض ملفك الشخصي', + goLogin: 'تسجيل الدخول', ), settings: TSettings( language: 'اللغة', @@ -945,6 +963,7 @@ const ar = T( roleDesign: 'تطوير وتصميم', roleUIUX: 'UI/UX', roleBackend: 'الخلفية', + roleNative: 'المكدس الأصلي', roleSupport: 'دعم i18n', member1: '无书的书', member1Sig: 'دائماً تقريباً', @@ -1024,6 +1043,13 @@ const ar = T( alreadyLatestDesc: 'لديك بالفعل أحدث إصدار', okButton: 'حسناً', comingSoon: 'قريباً', + distributionChannel: 'قناة التوزيع', + distAndroid: 'تم التنزيل من الموقع الرسمي لـ Xianyan', + distIOS: 'التوزيع عبر App Store', + distMacOS: 'التوزيع عبر App Store', + distHarmony: 'التوزيع عبر AppGallery', + distWeb: 'يتطلب تقديم طلب', + distWindows: 'تم التنزيل من الموقع الرسمي لـ Xianyan', ), auth: TAuth( welcomeBack: 'مرحباً بعودتك', diff --git a/lib/l10n/languages/bn.dart b/lib/l10n/languages/bn.dart index d3095a19..0840c581 100644 --- a/lib/l10n/languages/bn.dart +++ b/lib/l10n/languages/bn.dart @@ -528,6 +528,24 @@ const bn = T( appStoreNotFound: 'অ্যাপ স্টোর পাওয়া যায়নি', experimentalFeature: 'পরীক্ষামূলক বৈশিষ্ট্য', underReview: 'পর্যালোচনাধীন', + changeAvatar: 'অবতার পরিবর্তন করুন', + inputAvatarUrl: 'অবতার URL লিখুন', + selectFromAlbum: 'অ্যালবাম থেকে নির্বাচন (শীঘ্রই)', + avatarUrlHint: 'URL ২০৪৮ অক্ষরের বেশি হতে পারবে না, শুধুমাত্র http/https লিংক', + pleaseInputUrl: 'অনুগ্রহ করে URL লিখুন', + urlMustStartWithHttp: 'URL http:// বা https:// দিয়ে শুরু হতে হবে', + urlTooLong: 'URL ২০৪৮ অক্ষরের সীমা ছাড়িয়ে গেছে', + invalidUrlFormat: 'অবৈধ URL ফরম্যাট', + avatarUnderReview: '🔍 ছবি পর্যালোচনাধীন', + avatarReviewing: 'অবতার ছবি পর্যালোচনা হচ্ছে...', + avatarChangeSuccess: 'অবতার সফলভাবে পরিবর্তিত হয়েছে', + avatarChangeFailed: 'অবতার পরিবর্তন ব্যর্থ হয়েছে', + success: 'সফল', + failed: 'ব্যর্থ', + ok: 'ঠিক আছে', + loading: 'লোড হচ্ছে...', + loginToViewProfile: 'প্রোফাইল দেখতে লগইন করুন', + goLogin: 'লগইন', ), settings: TSettings( language: 'ভাষা', @@ -949,6 +967,7 @@ const bn = T( roleDesign: 'ডেভ ও ডিজাইন', roleUIUX: 'UI/UX', roleBackend: 'ব্যাকএন্ড', + roleNative: 'নেটিভ স্ট্যাক', roleSupport: 'i18n সাপোর্ট', member1: '无书的书', member1Sig: 'সবসময় প্রায়', @@ -1029,6 +1048,13 @@ const bn = T( alreadyLatestDesc: 'আপনার কাছে ইতিমধ্যে সর্বশেষ সংস্করণ আছে', okButton: 'ঠিক আছে', comingSoon: 'শীঘ্রই আসছে', + distributionChannel: 'বিতরণ চ্যানেল', + distAndroid: 'Xianyan অফিসিয়াল ওয়েবসাইট থেকে ডাউনলোড', + distIOS: 'App Store এর মাধ্যমে বিতরণ', + distMacOS: 'App Store এর মাধ্যমে বিতরণ', + distHarmony: 'AppGallery এর মাধ্যমে বিতরণ', + distWeb: 'আবেদন প্রয়োজন', + distWindows: 'Xianyan অফিসিয়াল ওয়েবসাইট থেকে ডাউনলোড', ), auth: TAuth( welcomeBack: 'ফিরে আসার জন্য স্বাগতম', diff --git a/lib/l10n/languages/de.dart b/lib/l10n/languages/de.dart index 79b9181e..d7778b5c 100644 --- a/lib/l10n/languages/de.dart +++ b/lib/l10n/languages/de.dart @@ -528,6 +528,24 @@ const de = T( appStoreNotFound: 'App-Store nicht gefunden', experimentalFeature: 'Experimentelle Funktionen', underReview: 'Überprüfung läuft', + changeAvatar: 'Avatar ändern', + inputAvatarUrl: 'Avatar-URL eingeben', + selectFromAlbum: 'Aus Album wählen (Demnächst)', + avatarUrlHint: 'URL darf 2048 Zeichen nicht überschreiten, nur http/https-Bildlinks', + pleaseInputUrl: 'Bitte URL eingeben', + urlMustStartWithHttp: 'URL muss mit http:// oder https:// beginnen', + urlTooLong: 'URL überschreitet 2048-Zeichen-Limit', + invalidUrlFormat: 'Ungültiges URL-Format', + avatarUnderReview: '🔍 Bild wird überprüft', + avatarReviewing: 'Avatar-Bild wird überprüft, bitte warten...', + avatarChangeSuccess: 'Avatar erfolgreich geändert', + avatarChangeFailed: 'Avatar-Änderung fehlgeschlagen', + success: 'Erfolg', + failed: 'Fehlgeschlagen', + ok: 'OK', + loading: 'Laden...', + loginToViewProfile: 'Anmelden um Profil zu sehen', + goLogin: 'Anmelden', ), settings: TSettings( language: 'Sprache', @@ -960,6 +978,7 @@ const de = T( roleDesign: 'Entwickler', roleUIUX: 'UI/UX', roleBackend: 'Backend', + roleNative: 'Nativer Stack', roleSupport: 'i18n-Support', member1: '无书的书', member1Sig: 'Immer fast da', @@ -1044,6 +1063,13 @@ const de = T( alreadyLatestDesc: 'Sie verwenden bereits die neueste Version', okButton: 'OK', comingSoon: 'Demnächst', + distributionChannel: 'Vertriebskanal', + distAndroid: 'Von der Xianyan-Website heruntergeladen', + distIOS: 'Über den App Store vertrieben', + distMacOS: 'Über den App Store vertrieben', + distHarmony: 'Über AppGallery vertrieben', + distWeb: 'Antrag erforderlich', + distWindows: 'Von der Xianyan-Website heruntergeladen', ), auth: TAuth( welcomeBack: 'Willkommen zurück', diff --git a/lib/l10n/languages/en.dart b/lib/l10n/languages/en.dart index 400d6971..bfb828fd 100644 --- a/lib/l10n/languages/en.dart +++ b/lib/l10n/languages/en.dart @@ -537,6 +537,24 @@ const en = T( appStoreNotFound: 'App store not found', experimentalFeature: 'Experimental Features', underReview: 'Under Review', + changeAvatar: 'Change Avatar', + inputAvatarUrl: 'Enter Avatar URL', + selectFromAlbum: 'Choose from Album (Coming Soon)', + avatarUrlHint: 'URL must not exceed 2048 characters, only http/https image links are supported', + pleaseInputUrl: 'Please enter a URL', + urlMustStartWithHttp: 'URL must start with http:// or https://', + urlTooLong: 'URL exceeds 2048 character limit, please use a shorter link', + invalidUrlFormat: 'Invalid URL format, please check and try again', + avatarUnderReview: '🔍 Image Under Review', + avatarReviewing: 'Reviewing avatar image, please wait...', + avatarChangeSuccess: 'Avatar changed successfully', + avatarChangeFailed: 'Avatar change failed', + success: 'Success', + failed: 'Failed', + ok: 'OK', + loading: 'Loading...', + loginToViewProfile: 'Login to view your profile', + goLogin: 'Login', ), settings: TSettings( language: 'Language', @@ -957,6 +975,7 @@ const en = T( roleDesign: 'Developer', roleUIUX: 'UI/UX', roleBackend: 'Backend', + roleNative: 'Native Stack', roleSupport: 'i18n Support', member1: '无书的书', member1Sig: 'Always almost there', @@ -1038,6 +1057,13 @@ const en = T( alreadyLatestDesc: 'You are already on the latest version', okButton: 'OK', comingSoon: 'Coming Soon', + distributionChannel: 'Distribution Channel', + distAndroid: 'Downloaded from Xianyan Official Website', + distIOS: 'Distributed via App Store', + distMacOS: 'Distributed via App Store', + distHarmony: 'Distributed via AppGallery', + distWeb: 'Application Required', + distWindows: 'Downloaded from Xianyan Official Website', ), auth: TAuth( welcomeBack: 'Welcome Back', diff --git a/lib/l10n/languages/es.dart b/lib/l10n/languages/es.dart index 90215259..a6174b4e 100644 --- a/lib/l10n/languages/es.dart +++ b/lib/l10n/languages/es.dart @@ -538,6 +538,24 @@ const es = T( appStoreNotFound: 'Tienda de apps no encontrada', experimentalFeature: 'Funciones experimentales', underReview: 'En revisión', + changeAvatar: 'Cambiar avatar', + inputAvatarUrl: 'Ingresar URL del avatar', + selectFromAlbum: 'Elegir del álbum (Próximamente)', + avatarUrlHint: 'La URL no debe superar 2048 caracteres, solo enlaces http/https', + pleaseInputUrl: 'Por favor ingrese una URL', + urlMustStartWithHttp: 'La URL debe comenzar con http:// o https://', + urlTooLong: 'La URL supera el límite de 2048 caracteres', + invalidUrlFormat: 'Formato de URL inválido', + avatarUnderReview: '🔍 Imagen en revisión', + avatarReviewing: 'Revisando imagen del avatar, espere...', + avatarChangeSuccess: 'Avatar cambiado exitosamente', + avatarChangeFailed: 'Error al cambiar el avatar', + success: 'Éxito', + failed: 'Fallido', + ok: 'OK', + loading: 'Cargando...', + loginToViewProfile: 'Inicia sesión para ver tu perfil', + goLogin: 'Iniciar sesión', ), settings: TSettings( language: 'Idioma', @@ -969,6 +987,7 @@ const es = T( roleDesign: 'Dev y Diseño', roleUIUX: 'UI/UX', roleBackend: 'Backend', + roleNative: 'Stack Nativo', roleSupport: 'Soporte i18n', member1: '无书的书', member1Sig: 'Siempre a punto', @@ -1053,6 +1072,13 @@ const es = T( alreadyLatestDesc: 'Ya tienes la última versión', okButton: 'OK', comingSoon: 'Próximamente', + distributionChannel: 'Canal de distribución', + distAndroid: 'Descargado desde el sitio web oficial de Xianyan', + distIOS: 'Distribuido a través de App Store', + distMacOS: 'Distribuido a través de App Store', + distHarmony: 'Distribuido a través de AppGallery', + distWeb: 'Solicitud requerida', + distWindows: 'Descargado desde el sitio web oficial de Xianyan', ), auth: TAuth( welcomeBack: 'Bienvenido de nuevo', diff --git a/lib/l10n/languages/fr.dart b/lib/l10n/languages/fr.dart index 0a66dff5..228bedab 100644 --- a/lib/l10n/languages/fr.dart +++ b/lib/l10n/languages/fr.dart @@ -530,6 +530,24 @@ const fr = T( appStoreNotFound: 'Magasin d\'apps introuvable', experimentalFeature: 'Fonctionnalités expérimentales', underReview: 'En cours de révision', + changeAvatar: 'Changer l\'avatar', + inputAvatarUrl: 'Entrer l\'URL de l\'avatar', + selectFromAlbum: 'Choisir dans l\'album (Bientôt)', + avatarUrlHint: 'L\'URL ne doit pas dépasser 2048 caractères, seuls les liens http/https sont supportés', + pleaseInputUrl: 'Veuillez entrer une URL', + urlMustStartWithHttp: 'L\'URL doit commencer par http:// ou https://', + urlTooLong: 'L\'URL dépasse la limite de 2048 caractères', + invalidUrlFormat: 'Format d\'URL invalide', + avatarUnderReview: '🔍 Image en cours de vérification', + avatarReviewing: 'Vérification de l\'avatar en cours...', + avatarChangeSuccess: 'Avatar modifié avec succès', + avatarChangeFailed: 'Échec du changement d\'avatar', + success: 'Succès', + failed: 'Échoué', + ok: 'OK', + loading: 'Chargement...', + loginToViewProfile: 'Connectez-vous pour voir votre profil', + goLogin: 'Connexion', ), settings: TSettings( language: 'Langue', @@ -975,6 +993,7 @@ const fr = T( roleDesign: 'Dev et Design', roleUIUX: 'UI/UX', roleBackend: 'Backend', + roleNative: 'Stack Natif', roleSupport: 'Support i18n', member1: '无书的书', member1Sig: 'Toujours presque', @@ -1059,6 +1078,13 @@ const fr = T( alreadyLatestDesc: 'Vous avez déjà la dernière version', okButton: 'OK', comingSoon: 'Bientôt', + distributionChannel: 'Canal de distribution', + distAndroid: 'Téléchargé depuis le site officiel de Xianyan', + distIOS: "Distribué via l'App Store", + distMacOS: "Distribué via l'App Store", + distHarmony: 'Distribué via AppGallery', + distWeb: 'Demande requise', + distWindows: 'Téléchargé depuis le site officiel de Xianyan', ), auth: TAuth( welcomeBack: 'Bon retour', diff --git a/lib/l10n/languages/hi.dart b/lib/l10n/languages/hi.dart index e838a0e2..7b7c8c2d 100644 --- a/lib/l10n/languages/hi.dart +++ b/lib/l10n/languages/hi.dart @@ -526,6 +526,24 @@ const hi = T( appStoreNotFound: 'ऐप स्टोर नहीं मिला', experimentalFeature: 'प्रायोगिक सुविधाएँ', underReview: 'समीक्षा में', + changeAvatar: 'अवतार बदलें', + inputAvatarUrl: 'अवतार URL दर्ज करें', + selectFromAlbum: 'एल्बम से चुनें (जल्दी आ रहा है)', + avatarUrlHint: 'URL 2048 अक्षरों से अधिक नहीं होना चाहिए, केवल http/https लिंक', + pleaseInputUrl: 'कृपया URL दर्ज करें', + urlMustStartWithHttp: 'URL http:// या https:// से शुरू होना चाहिए', + urlTooLong: 'URL 2048 अक्षर की सीमा से अधिक है', + invalidUrlFormat: 'अमान्य URL प्रारूप', + avatarUnderReview: '🔍 छवि समीक्षा में', + avatarReviewing: 'अवतार छवि की समीक्षा हो रही है...', + avatarChangeSuccess: 'अवतार सफलतापूर्वक बदला गया', + avatarChangeFailed: 'अवतार बदलने में विफल', + success: 'सफल', + failed: 'विफल', + ok: 'ठीक है', + loading: 'लोड हो रहा है...', + loginToViewProfile: 'प्रोफ़ाइल देखने के लिए लॉग इन करें', + goLogin: 'लॉग इन', ), settings: TSettings( language: 'भाषा', @@ -944,6 +962,7 @@ const hi = T( roleDesign: 'डेव और डिज़ाइन', roleUIUX: 'UI/UX', roleBackend: 'बैकएंड', + roleNative: 'नेटिव स्टैक', roleSupport: 'i18n सहायता', member1: '无书的书', member1Sig: 'हमेशा लगभग', @@ -1025,6 +1044,13 @@ const hi = T( alreadyLatestDesc: 'आपके पास पहले से नवीनतम संस्करण है', okButton: 'ठीक है', comingSoon: 'जल्द आ रहा है', + distributionChannel: 'वितरण चैनल', + distAndroid: 'Xianyan आधिकारिक वेबसाइट से डाउनलोड', + distIOS: 'App Store के माध्यम से वितरित', + distMacOS: 'App Store के माध्यम से वितरित', + distHarmony: 'AppGallery के माध्यम से वितरित', + distWeb: 'आवेदन आवश्यक', + distWindows: 'Xianyan आधिकारिक वेबसाइट से डाउनलोड', ), auth: TAuth( welcomeBack: 'वापसी पर स्वागत', diff --git a/lib/l10n/languages/it.dart b/lib/l10n/languages/it.dart index f524c02d..c83a31f5 100644 --- a/lib/l10n/languages/it.dart +++ b/lib/l10n/languages/it.dart @@ -536,6 +536,24 @@ const it = T( appStoreNotFound: 'App store non trovato', experimentalFeature: 'Funzionalità sperimentali', underReview: 'In revisione', + changeAvatar: 'Cambia avatar', + inputAvatarUrl: 'Inserisci URL avatar', + selectFromAlbum: 'Scegli dall\'album (Prossimamente)', + avatarUrlHint: 'L\'URL non deve superare 2048 caratteri, solo link http/https', + pleaseInputUrl: 'Inserisci un URL', + urlMustStartWithHttp: 'L\'URL deve iniziare con http:// o https://', + urlTooLong: 'L\'URL supera il limite di 2048 caratteri', + invalidUrlFormat: 'Formato URL non valido', + avatarUnderReview: '🔍 Immagine in revisione', + avatarReviewing: 'Revisione avatar in corso...', + avatarChangeSuccess: 'Avatar cambiato con successo', + avatarChangeFailed: 'Cambio avatar fallito', + success: 'Successo', + failed: 'Fallito', + ok: 'OK', + loading: 'Caricamento...', + loginToViewProfile: 'Accedi per visualizzare il profilo', + goLogin: 'Accedi', ), settings: TSettings( language: 'Lingua', @@ -970,6 +988,7 @@ const it = T( roleDesign: 'Sviluppatore', roleUIUX: 'UI/UX', roleBackend: 'Backend', + roleNative: 'Stack Nativo', roleSupport: 'Supporto i18n', member1: '无书的书', member1Sig: 'Quasi sempre', @@ -1052,6 +1071,13 @@ const it = T( alreadyLatestDesc: 'Hai già la versione più recente', okButton: 'OK', comingSoon: 'Prossimamente', + distributionChannel: 'Canale di distribuzione', + distAndroid: 'Scaricato dal sito ufficiale di Xianyan', + distIOS: 'Distribuito tramite App Store', + distMacOS: 'Distribuito tramite App Store', + distHarmony: 'Distribuito tramite AppGallery', + distWeb: 'Richiesta necessaria', + distWindows: 'Scaricato dal sito ufficiale di Xianyan', ), auth: TAuth( welcomeBack: 'Bentornato', diff --git a/lib/l10n/languages/ja.dart b/lib/l10n/languages/ja.dart index b4a816c4..6fcbf602 100644 --- a/lib/l10n/languages/ja.dart +++ b/lib/l10n/languages/ja.dart @@ -525,6 +525,24 @@ const ja = T( appStoreNotFound: 'アプリストアが見つかりません', experimentalFeature: '実験的機能', underReview: '審査中', + changeAvatar: 'アバターを変更', + inputAvatarUrl: 'アバターURLを入力', + selectFromAlbum: 'アルバムから選択(未対応)', + avatarUrlHint: 'URLは2048文字以内、http/httpsで始まる画像リンクのみ対応', + pleaseInputUrl: 'URLを入力してください', + urlMustStartWithHttp: 'http:// または https:// で始まるURLを入力してください', + urlTooLong: 'URLが2048文字の制限を超えています', + invalidUrlFormat: 'URLの形式が正しくありません', + avatarUnderReview: '🔍 画像審査中', + avatarReviewing: 'アバター画像を審査中です...', + avatarChangeSuccess: 'アバターの変更に成功しました', + avatarChangeFailed: 'アバターの変更に失敗しました', + success: '成功', + failed: '失敗', + ok: 'OK', + loading: '読み込み中...', + loginToViewProfile: 'ログインしてプロフィールを表示', + goLogin: 'ログイン', ), settings: TSettings( language: '言語', @@ -915,6 +933,7 @@ const ja = T( roleDesign: '開発・デザイン', roleUIUX: 'UI/UX', roleBackend: 'バックエンド', + roleNative: 'ネイティブスタック', roleSupport: '多言語対応', member1: '无书的书', member1Sig: 'いつもあと少し', @@ -994,6 +1013,13 @@ const ja = T( alreadyLatestDesc: '最新バージョンのため、更新は不要です', okButton: 'OK', comingSoon: '準備中', + distributionChannel: '配布チャンネル', + distAndroid: '閒言公式サイトからダウンロード', + distIOS: 'App Store経由で配布', + distMacOS: 'App Store経由で配布', + distHarmony: 'AppGallery経由で配布', + distWeb: '申請が必要', + distWindows: '閒言公式サイトからダウンロード', ), auth: TAuth( welcomeBack: 'おかえりなさい', diff --git a/lib/l10n/languages/ko.dart b/lib/l10n/languages/ko.dart index be9b6b8d..c019cae7 100644 --- a/lib/l10n/languages/ko.dart +++ b/lib/l10n/languages/ko.dart @@ -525,6 +525,24 @@ const ko = T( appStoreNotFound: '앱 스토어를 찾을 수 없습니다', experimentalFeature: '실험적 기능', underReview: '검토 중', + changeAvatar: '아바타 변경', + inputAvatarUrl: '아바타 URL 입력', + selectFromAlbum: '앨범에서 선택 (준비 중)', + avatarUrlHint: 'URL은 2048자를 초과할 수 없으며, http/https로 시작하는 이미지 링크만 지원', + pleaseInputUrl: 'URL을 입력하세요', + urlMustStartWithHttp: 'http:// 또는 https://로 시작하는 URL을 입력하세요', + urlTooLong: 'URL이 2048자 제한을 초과합니다', + invalidUrlFormat: 'URL 형식이 올바르지 않습니다', + avatarUnderReview: '🔍 이미지 검토 중', + avatarReviewing: '아바타 이미지를 검토 중입니다...', + avatarChangeSuccess: '아바타가 변경되었습니다', + avatarChangeFailed: '아바타 변경에 실패했습니다', + success: '성공', + failed: '실패', + ok: '확인', + loading: '로딩 중...', + loginToViewProfile: '로그인하여 프로필 보기', + goLogin: '로그인', ), settings: TSettings( language: '언어', @@ -915,6 +933,7 @@ const ko = T( roleDesign: '개발자', roleUIUX: 'UI/UX', roleBackend: '백엔드', + roleNative: '네이티브 스택', roleSupport: '다국어 지원', member1: '无书的书', member1Sig: '항상 아쉬움이 남아', @@ -994,6 +1013,13 @@ const ko = T( alreadyLatestDesc: '이미 최신 버전입니다', okButton: '확인', comingSoon: '준비 중', + distributionChannel: '배포 채널', + distAndroid: '閒言 공식 웹사이트에서 다운로드', + distIOS: 'App Store를 통해 배포', + distMacOS: 'App Store를 통해 배포', + distHarmony: 'AppGallery를 통해 배포', + distWeb: '신청 필요', + distWindows: '閒言 공식 웹사이트에서 다운로드', ), auth: TAuth( welcomeBack: '돌아오신 것을 환영합니다', diff --git a/lib/l10n/languages/pt.dart b/lib/l10n/languages/pt.dart index 1ea301f3..a71dc001 100644 --- a/lib/l10n/languages/pt.dart +++ b/lib/l10n/languages/pt.dart @@ -536,6 +536,24 @@ const pt = T( appStoreNotFound: 'Loja de apps não encontrada', experimentalFeature: 'Funcionalidades experimentais', underReview: 'Em revisão', + changeAvatar: 'Alterar avatar', + inputAvatarUrl: 'Inserir URL do avatar', + selectFromAlbum: 'Escolher do álbum (Em breve)', + avatarUrlHint: 'A URL não deve exceder 2048 caracteres, apenas links http/https', + pleaseInputUrl: 'Por favor, insira uma URL', + urlMustStartWithHttp: 'A URL deve começar com http:// ou https://', + urlTooLong: 'A URL excede o limite de 2048 caracteres', + invalidUrlFormat: 'Formato de URL inválido', + avatarUnderReview: '🔍 Imagem em revisão', + avatarReviewing: 'Revisando imagem do avatar, aguarde...', + avatarChangeSuccess: 'Avatar alterado com sucesso', + avatarChangeFailed: 'Falha ao alterar o avatar', + success: 'Sucesso', + failed: 'Falhou', + ok: 'OK', + loading: 'Carregando...', + loginToViewProfile: 'Entre para ver seu perfil', + goLogin: 'Entrar', ), settings: TSettings( language: 'Idioma', @@ -964,6 +982,7 @@ const pt = T( roleDesign: 'Dev e Design', roleUIUX: 'UI/UX', roleBackend: 'Backend', + roleNative: 'Stack Nativo', roleSupport: 'Suporte i18n', member1: '无书的书', member1Sig: 'Sempre quase lá', @@ -1047,6 +1066,13 @@ const pt = T( alreadyLatestDesc: 'Você já tem a versão mais recente', okButton: 'OK', comingSoon: 'Em breve', + distributionChannel: 'Canal de distribuição', + distAndroid: 'Baixado do site oficial do Xianyan', + distIOS: 'Distribuído via App Store', + distMacOS: 'Distribuído via App Store', + distHarmony: 'Distribuído via AppGallery', + distWeb: 'Solicitação necessária', + distWindows: 'Baixado do site oficial do Xianyan', ), auth: TAuth( welcomeBack: 'Bem-vindo de volta', diff --git a/lib/l10n/languages/ru.dart b/lib/l10n/languages/ru.dart index bd6226d7..b5a9254c 100644 --- a/lib/l10n/languages/ru.dart +++ b/lib/l10n/languages/ru.dart @@ -533,6 +533,24 @@ const ru = T( appStoreNotFound: 'Магазин приложений не найден', experimentalFeature: 'Экспериментальные функции', underReview: 'На рассмотрении', + changeAvatar: 'Изменить аватар', + inputAvatarUrl: 'Введите URL аватара', + selectFromAlbum: 'Выбрать из альбома (Скоро)', + avatarUrlHint: 'URL не должен превышать 2048 символов, поддерживаются только ссылки http/https', + pleaseInputUrl: 'Пожалуйста, введите URL', + urlMustStartWithHttp: 'URL должен начинаться с http:// или https://', + urlTooLong: 'URL превышает лимит в 2048 символов', + invalidUrlFormat: 'Неверный формат URL', + avatarUnderReview: '🔍 Изображение на проверке', + avatarReviewing: 'Проверка изображения аватара, подождите...', + avatarChangeSuccess: 'Аватар успешно изменён', + avatarChangeFailed: 'Не удалось изменить аватар', + success: 'Успешно', + failed: 'Ошибка', + ok: 'OK', + loading: 'Загрузка...', + loginToViewProfile: 'Войдите, чтобы увидеть профиль', + goLogin: 'Войти', ), settings: TSettings( language: 'Язык', @@ -963,6 +981,7 @@ const ru = T( roleDesign: 'Разработка и дизайн', roleUIUX: 'UI/UX', roleBackend: 'Бэкенд', + roleNative: 'Нативный стек', roleSupport: 'Поддержка i18n', member1: '无书的书', member1Sig: 'Всегда почти', @@ -1046,6 +1065,13 @@ const ru = T( alreadyLatestDesc: 'У вас уже последняя версия', okButton: 'OK', comingSoon: 'Скоро', + distributionChannel: 'Канал распространения', + distAndroid: 'Загрузка с официального сайта Xianyan', + distIOS: 'Распространение через App Store', + distMacOS: 'Распространение через App Store', + distHarmony: 'Распространение через AppGallery', + distWeb: 'Требуется заявка', + distWindows: 'Загрузка с официального сайта Xianyan', ), auth: TAuth( welcomeBack: 'С возвращением', diff --git a/lib/l10n/languages/zh_cn.dart b/lib/l10n/languages/zh_cn.dart index cbbcacdc..e0f344a5 100644 --- a/lib/l10n/languages/zh_cn.dart +++ b/lib/l10n/languages/zh_cn.dart @@ -524,6 +524,24 @@ const zhCN = T( appStoreNotFound: '未找到应用商店', experimentalFeature: 'Beta', underReview: '审核中', + changeAvatar: '修改头像', + inputAvatarUrl: '输入头像URL', + selectFromAlbum: '从相册选择(暂未开放)', + avatarUrlHint: 'URL长度不能超过2048个字符,仅支持 http/https 开头的图片链接', + pleaseInputUrl: '请输入URL地址', + urlMustStartWithHttp: '请输入以 http:// 或 https:// 开头的URL', + urlTooLong: 'URL长度超过2048字符限制,请使用更短的链接', + invalidUrlFormat: 'URL格式不正确,请检查后重试', + avatarUnderReview: '🔍 图像审核中', + avatarReviewing: '正在审核头像图片,请稍候...', + avatarChangeSuccess: '头像修改成功', + avatarChangeFailed: '头像修改失败', + success: '成功', + failed: '失败', + ok: '好的', + loading: '加载中...', + loginToViewProfile: '登录后查看个人中心', + goLogin: '去登录', ), settings: TSettings( language: '语言', @@ -902,13 +920,14 @@ const zhCN = T( roleDesign: '程序设计', roleUIUX: 'UI/UX', roleBackend: '后端开发', + roleNative: '原生栈', roleSupport: '多语言支持', member1: '无书的书', member1Sig: '总是差点意思', member2: 'ayk', member2Sig: '小作坊小料就是猛', member3: '伯乐不相马', - member3Sig: '谁家紫啧这么哇塞~', + member3Sig: '小作坊下料就是猛', member4: '泼茶香', member4Sig: '一起养猫 一起看海', member1Social: '酷安@无书的书', @@ -981,6 +1000,13 @@ const zhCN = T( alreadyLatestDesc: '当前已是最新版本,无需更新', okButton: '好的', comingSoon: '待开发', + distributionChannel: '分发渠道', + distAndroid: '由闲言官网提供下载', + distIOS: '由App Store提供分发', + distMacOS: '由App Store提供分发', + distHarmony: '由AppGallery提供分发', + distWeb: '需申请', + distWindows: '由闲言官网提供下载', ), auth: TAuth( welcomeBack: '欢迎回来', diff --git a/lib/l10n/languages/zh_tw.dart b/lib/l10n/languages/zh_tw.dart index ca859650..de7f4136 100644 --- a/lib/l10n/languages/zh_tw.dart +++ b/lib/l10n/languages/zh_tw.dart @@ -524,6 +524,24 @@ const zhTW = T( appStoreNotFound: '未找到應用商店', experimentalFeature: '實驗中的功能', underReview: '審核中', + changeAvatar: '修改頭像', + inputAvatarUrl: '輸入頭像URL', + selectFromAlbum: '從相簿選擇(暫未開放)', + avatarUrlHint: 'URL長度不能超過2048個字元,僅支援 http/https 開頭的圖片連結', + pleaseInputUrl: '請輸入URL地址', + urlMustStartWithHttp: '請輸入以 http:// 或 https:// 開頭的URL', + urlTooLong: 'URL長度超過2048字元限制,請使用更短的連結', + invalidUrlFormat: 'URL格式不正確,請檢查後重試', + avatarUnderReview: '🔍 圖像審核中', + avatarReviewing: '正在審核頭像圖片,請稍候...', + avatarChangeSuccess: '頭像修改成功', + avatarChangeFailed: '頭像修改失敗', + success: '成功', + failed: '失敗', + ok: '好的', + loading: '載入中...', + loginToViewProfile: '登入後查看個人中心', + goLogin: '去登入', ), settings: TSettings( language: '語言', @@ -901,6 +919,7 @@ const zhTW = T( roleDesign: '開發設計', roleUIUX: 'UI/UX', roleBackend: '後端開發', + roleNative: '原生棧', roleSupport: '多語言支援', member1: '無書的書', member1Sig: '總是差點意思', @@ -980,6 +999,13 @@ const zhTW = T( alreadyLatestDesc: '當前已是最新版本,無需更新', okButton: '好的', comingSoon: '待開發', + distributionChannel: '分發渠道', + distAndroid: '由閒言官網提供下載', + distIOS: '由App Store提供分發', + distMacOS: '由App Store提供分發', + distHarmony: '由AppGallery提供分發', + distWeb: '需申請', + distWindows: '由閒言官網提供下載', ), auth: TAuth( welcomeBack: '歡迎回來', diff --git a/lib/l10n/translation_io_service.dart b/lib/l10n/translation_io_service.dart index 4c8ab1e1..f47e3bb5 100644 --- a/lib/l10n/translation_io_service.dart +++ b/lib/l10n/translation_io_service.dart @@ -345,6 +345,37 @@ class TranslationIOService { experimentalFeature: map['experimentalFeature'] as String? ?? fallback.experimentalFeature, underReview: map['underReview'] as String? ?? fallback.underReview, + changeAvatar: map['changeAvatar'] as String? ?? fallback.changeAvatar, + inputAvatarUrl: + map['inputAvatarUrl'] as String? ?? fallback.inputAvatarUrl, + selectFromAlbum: + map['selectFromAlbum'] as String? ?? fallback.selectFromAlbum, + avatarUrlHint: + map['avatarUrlHint'] as String? ?? fallback.avatarUrlHint, + pleaseInputUrl: + map['pleaseInputUrl'] as String? ?? fallback.pleaseInputUrl, + urlMustStartWithHttp: + map['urlMustStartWithHttp'] as String? ?? + fallback.urlMustStartWithHttp, + urlTooLong: map['urlTooLong'] as String? ?? fallback.urlTooLong, + invalidUrlFormat: + map['invalidUrlFormat'] as String? ?? fallback.invalidUrlFormat, + avatarUnderReview: + map['avatarUnderReview'] as String? ?? fallback.avatarUnderReview, + avatarReviewing: + map['avatarReviewing'] as String? ?? fallback.avatarReviewing, + avatarChangeSuccess: + map['avatarChangeSuccess'] as String? ?? + fallback.avatarChangeSuccess, + avatarChangeFailed: + map['avatarChangeFailed'] as String? ?? fallback.avatarChangeFailed, + success: map['success'] as String? ?? fallback.success, + failed: map['failed'] as String? ?? fallback.failed, + ok: map['ok'] as String? ?? fallback.ok, + loading: map['loading'] as String? ?? fallback.loading, + loginToViewProfile: + map['loginToViewProfile'] as String? ?? fallback.loginToViewProfile, + goLogin: map['goLogin'] as String? ?? fallback.goLogin, ); } diff --git a/lib/l10n/types/t_about.dart b/lib/l10n/types/t_about.dart index 7b61336b..7719d269 100644 --- a/lib/l10n/types/t_about.dart +++ b/lib/l10n/types/t_about.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 关于页翻译类型 /// 创建时间: 2026-05-29 -/// 更新时间: 2026-06-01 +/// 更新时间: 2026-06-02 /// 作用: 关于页面翻译键定义(软件信息、了解我们、技术栈、构建信息、设备信息、官网、开发者、团队、QQ群、备案、贡献者等) -/// 上次更新: 新增roleUIUX/member1Social~3Social/qqGroupTelegram/qqGroupTelegramDesc/contributorRole5/contributorRole5Name/specialThanksTools字段 +/// 上次更新: 新增roleNative字段 /// ============================================================ class TAbout { @@ -59,6 +59,7 @@ class TAbout { required this.roleDesign, required this.roleUIUX, required this.roleBackend, + required this.roleNative, required this.roleSupport, required this.member1, required this.member1Sig, @@ -137,6 +138,13 @@ class TAbout { required this.alreadyLatestDesc, required this.okButton, required this.comingSoon, + required this.distributionChannel, + required this.distAndroid, + required this.distIOS, + required this.distMacOS, + required this.distHarmony, + required this.distWeb, + required this.distWindows, }); // ===== 应用信息 ===== @@ -318,6 +326,9 @@ class TAbout { /// 后端角色 final String roleBackend; + /// 原生栈角色 + final String roleNative; + /// 支持角色 final String roleSupport; @@ -554,6 +565,29 @@ class TAbout { /// 待开发 final String comingSoon; + // ===== 分发渠道 ===== + + /// 分发渠道 + final String distributionChannel; + + /// Android分发渠道 + final String distAndroid; + + /// iOS分发渠道 + final String distIOS; + + /// macOS分发渠道 + final String distMacOS; + + /// 鸿蒙分发渠道 + final String distHarmony; + + /// Web分发渠道 + final String distWeb; + + /// Windows分发渠道 + final String distWindows; + Map toMap() => { 'appInfo': appInfo, 'learnUs': learnUs, @@ -606,6 +640,7 @@ class TAbout { 'roleDesign': roleDesign, 'roleUIUX': roleUIUX, 'roleBackend': roleBackend, + 'roleNative': roleNative, 'roleSupport': roleSupport, 'member1': member1, 'member1Sig': member1Sig, @@ -684,6 +719,13 @@ class TAbout { 'alreadyLatestDesc': alreadyLatestDesc, 'okButton': okButton, 'comingSoon': comingSoon, + 'distributionChannel': distributionChannel, + 'distAndroid': distAndroid, + 'distIOS': distIOS, + 'distMacOS': distMacOS, + 'distHarmony': distHarmony, + 'distWeb': distWeb, + 'distWindows': distWindows, }; static TAbout fromMap(Map map, {TAbout? fallback}) => TAbout( @@ -838,6 +880,9 @@ class TAbout { roleBackend: map['roleBackend']?.isNotEmpty == true ? map['roleBackend']! : (fallback?.roleBackend ?? ''), + roleNative: map['roleNative']?.isNotEmpty == true + ? map['roleNative']! + : (fallback?.roleNative ?? ''), roleSupport: map['roleSupport']?.isNotEmpty == true ? map['roleSupport']! : (fallback?.roleSupport ?? ''), @@ -1072,5 +1117,26 @@ class TAbout { comingSoon: map['comingSoon']?.isNotEmpty == true ? map['comingSoon']! : (fallback?.comingSoon ?? ''), + distributionChannel: map['distributionChannel']?.isNotEmpty == true + ? map['distributionChannel']! + : (fallback?.distributionChannel ?? ''), + distAndroid: map['distAndroid']?.isNotEmpty == true + ? map['distAndroid']! + : (fallback?.distAndroid ?? ''), + distIOS: map['distIOS']?.isNotEmpty == true + ? map['distIOS']! + : (fallback?.distIOS ?? ''), + distMacOS: map['distMacOS']?.isNotEmpty == true + ? map['distMacOS']! + : (fallback?.distMacOS ?? ''), + distHarmony: map['distHarmony']?.isNotEmpty == true + ? map['distHarmony']! + : (fallback?.distHarmony ?? ''), + distWeb: map['distWeb']?.isNotEmpty == true + ? map['distWeb']! + : (fallback?.distWeb ?? ''), + distWindows: map['distWindows']?.isNotEmpty == true + ? map['distWindows']! + : (fallback?.distWindows ?? ''), ); } diff --git a/lib/l10n/types/t_profile.dart b/lib/l10n/types/t_profile.dart index 6c2a2c5e..74736c95 100644 --- a/lib/l10n/types/t_profile.dart +++ b/lib/l10n/types/t_profile.dart @@ -39,6 +39,24 @@ class TProfile { required this.appStoreNotFound, required this.experimentalFeature, required this.underReview, + required this.changeAvatar, + required this.inputAvatarUrl, + required this.selectFromAlbum, + required this.avatarUrlHint, + required this.pleaseInputUrl, + required this.urlMustStartWithHttp, + required this.urlTooLong, + required this.invalidUrlFormat, + required this.avatarUnderReview, + required this.avatarReviewing, + required this.avatarChangeSuccess, + required this.avatarChangeFailed, + required this.success, + required this.failed, + required this.ok, + required this.loading, + required this.loginToViewProfile, + required this.goLogin, }); /// 页面标题 @@ -110,6 +128,43 @@ class TProfile { /// 审核中 final String underReview; + /// 修改头像 + final String changeAvatar; + /// 输入头像URL + final String inputAvatarUrl; + /// 从相册选择 + final String selectFromAlbum; + /// URL长度提示 + final String avatarUrlHint; + /// 请输入URL地址 + final String pleaseInputUrl; + /// URL必须以http开头 + final String urlMustStartWithHttp; + /// URL过长 + final String urlTooLong; + /// URL格式不正确 + final String invalidUrlFormat; + /// 图像审核中 + final String avatarUnderReview; + /// 正在审核头像图片 + final String avatarReviewing; + /// 头像修改成功 + final String avatarChangeSuccess; + /// 头像修改失败 + final String avatarChangeFailed; + /// 成功 + final String success; + /// 失败 + final String failed; + /// 好的 + final String ok; + /// 加载中 + final String loading; + /// 登录后查看个人中心 + final String loginToViewProfile; + /// 去登录 + final String goLogin; + Map toMap() => { 'title': title, 'myFavorites': myFavorites, @@ -142,6 +197,24 @@ class TProfile { 'appStoreNotFound': appStoreNotFound, 'experimentalFeature': experimentalFeature, 'underReview': underReview, + 'changeAvatar': changeAvatar, + 'inputAvatarUrl': inputAvatarUrl, + 'selectFromAlbum': selectFromAlbum, + 'avatarUrlHint': avatarUrlHint, + 'pleaseInputUrl': pleaseInputUrl, + 'urlMustStartWithHttp': urlMustStartWithHttp, + 'urlTooLong': urlTooLong, + 'invalidUrlFormat': invalidUrlFormat, + 'avatarUnderReview': avatarUnderReview, + 'avatarReviewing': avatarReviewing, + 'avatarChangeSuccess': avatarChangeSuccess, + 'avatarChangeFailed': avatarChangeFailed, + 'success': success, + 'failed': failed, + 'ok': ok, + 'loading': loading, + 'loginToViewProfile': loginToViewProfile, + 'goLogin': goLogin, }; static TProfile fromMap(Map map, {TProfile? fallback}) => @@ -239,5 +312,59 @@ class TProfile { underReview: map['underReview']?.isNotEmpty == true ? map['underReview']! : (fallback?.underReview ?? ''), + changeAvatar: map['changeAvatar']?.isNotEmpty == true + ? map['changeAvatar']! + : (fallback?.changeAvatar ?? ''), + inputAvatarUrl: map['inputAvatarUrl']?.isNotEmpty == true + ? map['inputAvatarUrl']! + : (fallback?.inputAvatarUrl ?? ''), + selectFromAlbum: map['selectFromAlbum']?.isNotEmpty == true + ? map['selectFromAlbum']! + : (fallback?.selectFromAlbum ?? ''), + avatarUrlHint: map['avatarUrlHint']?.isNotEmpty == true + ? map['avatarUrlHint']! + : (fallback?.avatarUrlHint ?? ''), + pleaseInputUrl: map['pleaseInputUrl']?.isNotEmpty == true + ? map['pleaseInputUrl']! + : (fallback?.pleaseInputUrl ?? ''), + urlMustStartWithHttp: map['urlMustStartWithHttp']?.isNotEmpty == true + ? map['urlMustStartWithHttp']! + : (fallback?.urlMustStartWithHttp ?? ''), + urlTooLong: map['urlTooLong']?.isNotEmpty == true + ? map['urlTooLong']! + : (fallback?.urlTooLong ?? ''), + invalidUrlFormat: map['invalidUrlFormat']?.isNotEmpty == true + ? map['invalidUrlFormat']! + : (fallback?.invalidUrlFormat ?? ''), + avatarUnderReview: map['avatarUnderReview']?.isNotEmpty == true + ? map['avatarUnderReview']! + : (fallback?.avatarUnderReview ?? ''), + avatarReviewing: map['avatarReviewing']?.isNotEmpty == true + ? map['avatarReviewing']! + : (fallback?.avatarReviewing ?? ''), + avatarChangeSuccess: map['avatarChangeSuccess']?.isNotEmpty == true + ? map['avatarChangeSuccess']! + : (fallback?.avatarChangeSuccess ?? ''), + avatarChangeFailed: map['avatarChangeFailed']?.isNotEmpty == true + ? map['avatarChangeFailed']! + : (fallback?.avatarChangeFailed ?? ''), + success: map['success']?.isNotEmpty == true + ? map['success']! + : (fallback?.success ?? ''), + failed: map['failed']?.isNotEmpty == true + ? map['failed']! + : (fallback?.failed ?? ''), + ok: map['ok']?.isNotEmpty == true + ? map['ok']! + : (fallback?.ok ?? ''), + loading: map['loading']?.isNotEmpty == true + ? map['loading']! + : (fallback?.loading ?? ''), + loginToViewProfile: map['loginToViewProfile']?.isNotEmpty == true + ? map['loginToViewProfile']! + : (fallback?.loginToViewProfile ?? ''), + goLogin: map['goLogin']?.isNotEmpty == true + ? map['goLogin']! + : (fallback?.goLogin ?? ''), ); } diff --git a/lib/shared/widgets/containers/deferred_builder.dart b/lib/shared/widgets/containers/deferred_builder.dart index 5927e77b..02b58d25 100644 --- a/lib/shared/widgets/containers/deferred_builder.dart +++ b/lib/shared/widgets/containers/deferred_builder.dart @@ -3,17 +3,26 @@ /// 创建时间: 2026-06-02 /// 更新时间: 2026-06-02 /// 作用: 将子组件(如syncfusion chart)延迟到postFrameCallback渲染 -/// 避免chart在build阶段触发markNeedsLayout导致卡死 -/// 上次更新: 初始创建 +/// 避免chart在build阶段触发markNeedsLayout导致卡死/闪退 +/// 内置RepaintBoundary隔离重绘,避免图表重绘波及父级 +/// 上次更新: 增加RepaintBoundary隔离 + 可选placeholder + mounted安全检查 /// ============================================================ import 'package:flutter/widgets.dart'; class DeferredBuilder extends StatefulWidget { - const DeferredBuilder({super.key, required this.builder}); + const DeferredBuilder({ + super.key, + required this.builder, + this.placeholder, + }); final WidgetBuilder builder; + /// 首帧占位组件,默认 SizedBox.expand() 填充父级约束 + /// 对于有固定高度的图表区域,SizedBox.expand 会自动适配父级高度 + final Widget? placeholder; + @override State createState() => _DeferredBuilderState(); } @@ -32,8 +41,8 @@ class _DeferredBuilderState extends State { @override Widget build(BuildContext context) { if (!_ready) { - return const SizedBox.expand(); + return widget.placeholder ?? const SizedBox.expand(); } - return widget.builder(context); + return RepaintBoundary(child: widget.builder(context)); } } diff --git a/lib/shared/widgets/feedback/offline_banner.dart b/lib/shared/widgets/feedback/offline_banner.dart index 269f960f..b2e1a38c 100644 --- a/lib/shared/widgets/feedback/offline_banner.dart +++ b/lib/shared/widgets/feedback/offline_banner.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 网络状态横幅组件 /// 创建时间: 2026-05-04 -/// 更新时间: 2026-05-31 +/// 更新时间: 2026-06-02 /// 作用: 在页面顶部显示网络状态提示(离线/网络异常),支持关闭和网络恢复自动隐藏 -/// 上次更新: 增强为NetworkStatusBanner,支持离线+DNS异常双状态检测、可关闭、自动隐藏 +/// 上次更新: OfflineBanner增加刷新/关闭按钮,改为ConsumerStatefulWidget /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -15,6 +15,7 @@ import '../../../core/services/network/connectivity_provider.dart' show connectivityCheckProvider; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_typography.dart'; +import 'app_toast.dart'; /// 网络状态横幅 — 显示在页面顶部 /// @@ -113,14 +114,28 @@ class _NetworkStatusBannerState extends ConsumerState { } } -/// 简洁离线横幅 — 仅显示离线状态,不可关闭 -class OfflineBanner extends ConsumerWidget { +/// 简洁离线横幅 — 显示离线状态,支持刷新和关闭 +class OfflineBanner extends ConsumerStatefulWidget { const OfflineBanner({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _OfflineBannerState(); +} + +class _OfflineBannerState extends ConsumerState { + bool _dismissed = false; + bool _wasOffline = false; + + @override + Widget build(BuildContext context) { final isOffline = ref.isOffline; - if (!isOffline) return const SizedBox.shrink(); + + if (_wasOffline && !isOffline) { + _dismissed = false; + } + _wasOffline = isOffline; + + if (!isOffline || _dismissed) return const SizedBox.shrink(); return Container( width: double.infinity, @@ -138,7 +153,6 @@ class OfflineBanner extends ConsumerWidget { ), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( CupertinoIcons.wifi_slash, @@ -146,11 +160,48 @@ class OfflineBanner extends ConsumerWidget { color: CupertinoColors.systemOrange.darkColor, ), const SizedBox(width: AppSpacing.sm), - Text( - '离线模式 · 使用缓存数据', - style: AppTypography.caption1.copyWith( - color: CupertinoColors.systemOrange.darkColor, - fontWeight: FontWeight.w600, + Expanded( + child: Text( + '离线模式 · 使用缓存数据', + style: AppTypography.caption1.copyWith( + color: CupertinoColors.systemOrange.darkColor, + fontWeight: FontWeight.w600, + ), + ), + ), + GestureDetector( + onTap: () { + final stillOffline = ref.read(connectivityProvider) == false; + if (stillOffline) { + AppToast.showWarning('网络仍未连接'); + } + }, + child: Padding( + padding: const EdgeInsets.only(left: AppSpacing.sm), + child: Icon( + CupertinoIcons.refresh, + size: 16, + color: CupertinoColors.systemOrange.darkColor + .withValues(alpha: 0.7), + ), + ), + ), + GestureDetector( + onTap: () { + if (mounted) { + setState(() { + _dismissed = true; + }); + } + }, + child: Padding( + padding: const EdgeInsets.only(left: AppSpacing.sm), + child: Icon( + CupertinoIcons.xmark_circle_fill, + size: 16, + color: CupertinoColors.systemOrange.darkColor + .withValues(alpha: 0.6), + ), ), ), ], diff --git a/lib/shared/widgets/media/watermarked_copyright_image.dart b/lib/shared/widgets/media/watermarked_copyright_image.dart new file mode 100644 index 00000000..5a797c57 --- /dev/null +++ b/lib/shared/widgets/media/watermarked_copyright_image.dart @@ -0,0 +1,271 @@ +/// ============================================================ +/// 闲言APP — 带水印的版权证书图片组件 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 展示软件著作权证书图片,带对角线"闲言"水印,点击全屏查看 +/// 上次更新: 初始创建 +/// ============================================================ + +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; + +import '../../../core/theme/app_radius.dart'; +import '../../../core/theme/app_spacing.dart'; +import '../../../core/theme/app_theme.dart'; +import '../../../core/theme/app_typography.dart'; + +class WatermarkedCopyrightImage extends StatelessWidget { + const WatermarkedCopyrightImage({super.key}); + + static const _assetPath = 'assets/images/empty/rz.png'; + static const _watermarkText = '闲言'; + + @override + Widget build(BuildContext context) { + final ext = AppTheme.ext(context); + + return GestureDetector( + onTap: () => _showFullScreen(context), + child: Container( + margin: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + decoration: BoxDecoration( + borderRadius: AppRadius.lgBorder, + border: Border.all( + color: ext.textHint.withValues(alpha: 0.15), + width: 0.5, + ), + boxShadow: [ + BoxShadow( + color: ext.textPrimary.withValues(alpha: 0.06), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + ClipRRect( + borderRadius: AppRadius.lgBorder, + child: Image.asset( + _assetPath, + fit: BoxFit.fitWidth, + width: double.infinity, + errorBuilder: (_, __, ___) => _buildErrorPlaceholder(ext), + ), + ), + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: _DiagonalWatermarkPainter( + text: _watermarkText, + color: ext.textPrimary.withValues(alpha: 0.08), + ), + ), + ), + ), + Positioned( + right: AppSpacing.sm, + top: AppSpacing.sm, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xs, + vertical: 2, + ), + decoration: BoxDecoration( + color: ext.bgPrimary.withValues(alpha: 0.7), + borderRadius: AppRadius.smBorder, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.fullscreen, + size: 12, + color: ext.textSecondary, + ), + const SizedBox(width: 3), + Text( + '🔍', + style: AppTypography.caption2.copyWith( + color: ext.textSecondary, + fontSize: 10, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildErrorPlaceholder(AppThemeExtension ext) { + return Container( + height: 200, + decoration: BoxDecoration( + color: ext.bgSecondary, + borderRadius: AppRadius.lgBorder, + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(CupertinoIcons.doc_text_search, size: 36, color: ext.textHint), + const SizedBox(height: AppSpacing.xs), + Text( + '证书图片加载失败', + style: AppTypography.caption1.copyWith(color: ext.textHint), + ), + ], + ), + ), + ); + } + + void _showFullScreen(BuildContext context) { + final ext = AppTheme.ext(context); + showCupertinoDialog( + context: context, + barrierDismissible: true, + builder: (dialogContext) => CupertinoPageScaffold( + backgroundColor: ext.bgPrimary.withValues(alpha: 0.95), + child: SafeArea( + child: Column( + children: [ + CupertinoNavigationBar( + backgroundColor: ext.bgPrimary.withValues(alpha: 0.8), + border: null, + leading: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: () => Navigator.of(dialogContext).pop(), + child: Icon( + CupertinoIcons.xmark_circle_fill, + color: ext.textHint, + size: 28, + ), + ), + middle: Text( + '📜 软件著作权证书', + style: AppTypography.subhead.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + Expanded( + child: InteractiveViewer( + minScale: 0.5, + maxScale: 4.0, + child: Center( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(AppSpacing.md), + child: Image.asset( + _assetPath, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.doc_text_search, + size: 48, + color: ext.textHint, + ), + const SizedBox(height: AppSpacing.sm), + Text( + '图片加载失败', + style: AppTypography.subhead.copyWith( + color: ext.textHint, + ), + ), + ], + ), + ), + ), + ), + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: _DiagonalWatermarkPainter( + text: _watermarkText, + color: ext.textPrimary.withValues(alpha: 0.06), + fontSize: 20, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _DiagonalWatermarkPainter extends CustomPainter { + _DiagonalWatermarkPainter({ + required this.text, + required this.color, + this.fontSize = 14, + }); + + final String text; + final Color color; + final double fontSize; + + @override + void paint(Canvas canvas, Size size) { + final textStyle = TextStyle( + color: color, + fontSize: fontSize, + fontWeight: FontWeight.w600, + letterSpacing: 4, + ); + final textSpan = TextSpan(text: text, style: textStyle); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + + final stepX = textPainter.width + 60; + final stepY = textPainter.height + 80; + const angle = -pi / 6; + + canvas.save(); + canvas.translate(size.width / 2, size.height / 2); + canvas.rotate(angle); + canvas.translate(-size.width, -size.height); + + final cols = (size.width * 3 / stepX).ceil() + 1; + final rows = (size.height * 3 / stepY).ceil() + 1; + + for (var row = 0; row < rows; row++) { + for (var col = 0; col < cols; col++) { + final x = col * stepX.toDouble(); + final y = row * stepY.toDouble(); + textPainter.paint(canvas, Offset(x, y)); + } + } + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _DiagonalWatermarkPainter oldDelegate) { + return oldDelegate.text != text || + oldDelegate.color != color || + oldDelegate.fontSize != fontSize; + } +} diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 9f9c4854..def40d9f 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -44,6 +44,7 @@ export 'input/infinite_paged_list.dart'; export 'media/safe_cached_image.dart'; export 'media/thumbnail_image.dart'; export 'media/tts_player_bar.dart'; +export 'media/watermarked_copyright_image.dart'; // cards export 'cards/rank_item_card.dart'; diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d9..072326b9 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eba..0597eace 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa4..75fc06b7 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb57226..4c1c3a87 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index f083318e..fa885a3c 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e72..4828b8cf 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632cf..50ab1dbe 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ