feat: v6.10.3 多语言翻译补全 + 17项功能修复

- 引导页协议多语言支持(languageId传递)
- 登录页双书名号修复 + 注册页协议勾选
- 个人中心页面多语言(18个翻译键)
- 网络断开提示增加关闭/刷新按钮
- 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索
- iOS快捷按钮重复修复(删除Info.plist静态定义)
- 测试账号123456警告提示
- 扫码登录自动跳转(HTTP轮询+WebSocket双通道)
- 登录页老用户按钮改次要色
- Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0)
- macOS标题栏跟随软件夜间模式
- 平台兼容分发渠道弹窗
- 软件著作权图片+交叉水印
- 桌面小部件平台兼容说明默认收起
- iOS/macOS图标更新+名称确认为闲言
- 12个语言文件补全roleNative+7个分发渠道翻译字段
This commit is contained in:
Developer
2026-06-02 04:50:32 +08:00
parent 10df6b705c
commit ae1df22732
65 changed files with 2506 additions and 823 deletions

View File

@@ -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`

View File

@@ -104,24 +104,5 @@
<array>
<string>group.apps.xy.xianyan.share</string>
</array>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>action_theme</string>
<key>UIApplicationShortcutItemTitle</key>
<string>主题个性化</string>
<key>UIApplicationShortcutItemIconName</key>
<string>palette</string>
</dict>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>action_general_settings</string>
<key>UIApplicationShortcutItemTitle</key>
<string>通用设置</string>
<key>UIApplicationShortcutItemIconName</key>
<string>settings</string>
</dict>
</array>
</dict>
</plist>

View File

@@ -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<XianyanApp>
}
}
@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<Locale>? locales) {
super.didChangeLocales(locales);
@@ -316,6 +329,15 @@ class _XianyanAppState extends ConsumerState<XianyanApp>
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(

View File

@@ -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

View File

@@ -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(),
],
],
),
);

View File

@@ -489,7 +489,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
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<LoginPage>
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<LoginPage>
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,

View File

@@ -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<QrcodeLoginPage> {
bool _hasScanned = false;
StreamSubscription<Object>? _subscription;
Timer? _expireTimer;
bool _hasNavigated = false;
@override
void initState() {
@@ -59,6 +63,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
_subscription?.cancel();
_expireTimer?.cancel();
_scannerController.dispose();
ref.read(qrcodeLoginProvider.notifier).stopPolling();
super.dispose();
}
@@ -148,10 +153,11 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
final qrcodeState = ref.watch(qrcodeLoginProvider);
ref.listen<QrcodeLoginState>(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<QrcodeLoginPage> {
);
}
// ============================================================
// 登录成功处理
// ============================================================
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<void>(
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<void>(
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<QrcodeLoginPage> {
_generateQrcode();
} else {
_hasScanned = false;
ref.read(qrcodeLoginProvider.notifier).stopPolling();
_startScanner();
}
},
@@ -631,7 +723,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
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<QrcodeLoginPage> {
).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<QrcodeLoginPage> {
),
],
),
] else if (isExpired) ...[
] else if (isExpired || state.isError) ...[
_buildExpiredPlaceholder(ext),
] else ...[
_buildLoadingPlaceholder(ext),
@@ -723,6 +817,77 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
.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<QrcodeLoginPage> {
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.exclamationmark_triangle,
@@ -807,6 +973,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
}
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<QrcodeLoginPage> {
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<QrcodeLoginPage> {
),
).animate().fadeIn(duration: 400.ms, delay: 400.ms);
}
// ============================================================
// 成功弹窗
// ============================================================
void _showSuccessDialog(AppThemeExtension ext) {
showCupertinoDialog<void>(
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((_) {});
}
}
// ============================================================

View File

@@ -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<RegisterSection> {
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<RegisterSection> {
),
),
const SizedBox(height: AppSpacing.lg),
_buildAgreementRow(ext, auth),
const SizedBox(height: AppSpacing.md),
Row(
children: [
Expanded(
@@ -666,7 +671,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
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<RegisterSection> {
}
}
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<void> _handleRegister() async {
final t = ref.read(translationsProvider);
final username = _usernameController.text.trim();

View File

@@ -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<QrcodeLoginState> {
@override
QrcodeLoginState build() => const QrcodeLoginState();
QrcodeLoginNotifier();
QrcodeLoginState build() {
ref.onDispose(_cleanup);
return const QrcodeLoginState();
}
Timer? _pollTimer;
bool _polling = false;
// ============================================================
// 扫码确认登录 (Device A 扫码后调用)
// ============================================================
/// 扫码确认登录
Future<void> confirmLogin(String code) async {
@@ -99,16 +125,23 @@ class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
}
}
/// 生成二维码Web端登录用
// ============================================================
// 生成二维码 + 启动轮询 (Device B 生成二维码)
// ============================================================
/// 生成二维码并启动轮询监听
Future<void> 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<QrcodeLoginState> {
}
}
// ============================================================
// 轮询 + 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<void> _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<void> _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<QrcodeLoginState> {
clearError: true,
);
}
/// 资源清理
void _cleanup() {
stopPolling();
}
}
// ============================================================
@@ -142,4 +309,6 @@ class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
// ============================================================
final qrcodeLoginProvider =
NotifierProvider<QrcodeLoginNotifier, QrcodeLoginState>(QrcodeLoginNotifier.new);
NotifierProvider<QrcodeLoginNotifier, QrcodeLoginState>(
QrcodeLoginNotifier.new,
);

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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: <CircularSeries<_TypeData, String>>[
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>[
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>[
CircularChartAnnotation(
widget: Text(
@@ -937,7 +938,7 @@ class ReadlaterStatsPage extends StatelessWidget {
),
],
),
),
)),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -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<TransferStatsPage> {
);
}).toList();
return SfCartesianChart(
return DeferredBuilder(builder: (context) => SfCartesianChart(
plotAreaBorderWidth: 0,
margin: EdgeInsets.zero,
primaryXAxis: CategoryAxis(
@@ -268,6 +269,7 @@ class _TransferStatsPageState extends ConsumerState<TransferStatsPage> {
name: '接收',
),
],
),
);
}
@@ -399,7 +401,7 @@ class _TransferStatsPageState extends ConsumerState<TransferStatsPage> {
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<TransferStatsPage> {
],
),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(

View File

@@ -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 {
),
),
],
),
);
}

View File

@@ -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<FavoritePage> {
);
}
return SfCircularChart(
return DeferredBuilder(
builder: (context) => SfCircularChart(
series: <CircularSeries<_PieData, String>>[
DoughnutSeries<_PieData, String>(
animationDuration: 0,
@@ -452,6 +454,7 @@ class _FavoritePageState extends ConsumerState<FavoritePage> {
innerRadius: '35%',
),
],
),
);
}

View File

@@ -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<HistoryPage> {
.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<HistoryPage> {
),
),
],
),
);
}

View File

@@ -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(

View File

@@ -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<void>(
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 '';
}
}
// ============================================================

View File

@@ -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<void>(
@@ -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),
];

View File

@@ -508,7 +508,7 @@ class WeChatTile extends StatelessWidget {
),
const SizedBox(width: AppSpacing.xs),
const Text(
'微风暴',
'微风暴(微信搜索)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,

View File

@@ -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<DataManagementPage>
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<DataManagementPage>
),
],
),
),
)),
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.md,

View File

@@ -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<TranslatePluginPage> {
),
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<TranslatePluginPage> {
),
],
),
),
)),
],
),
);

View File

@@ -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: <CircularSeries<_PermissionChartEntry, String>>[
DoughnutSeries<_PermissionChartEntry, String>(
animationDuration: 0,
@@ -381,7 +382,7 @@ class _PermissionManagementPageState
),
],
),
),
)),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(

View File

@@ -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: '支出',
),
],
),
);
}
}

View File

@@ -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<LearningCenterPage> {
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<LearningCenterPage> {
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<LearningCenterPage> {
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),
),
),
],
),
),
),
],

View File

@@ -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<LearningProgressPage> {
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<LearningProgressPage> {
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<LearningProgressPage> {
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),
),
),
],
),
),
),
],

View File

@@ -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<UserCenterPage> {
void _showAvatarPicker() {
final ext = AppTheme.ext(context);
final t = ref.read(translationsProvider);
showCupertinoModalPopup<void>(
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<UserCenterPage> {
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<UserCenterPage> {
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<UserCenterPage> {
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
child: Text(t.common.cancel),
),
),
);
@@ -173,10 +175,11 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
void _showAvatarUrlInput() {
final controller = TextEditingController();
final dialogExt = AppTheme.ext(context);
final t = ref.read(translationsProvider);
showCupertinoDialog<void>(
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<UserCenterPage> {
),
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<UserCenterPage> {
),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
child: Text(t.common.cancel),
onPressed: () {
controller.dispose();
ctx.dismissDialog();
@@ -214,27 +217,27 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
),
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<UserCenterPage> {
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<UserCenterPage> {
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<UserCenterPage> {
}
void _showAvatarResult(bool success, String msg) {
final t = ref.read(translationsProvider);
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
@@ -298,13 +302,13 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
: 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<UserCenterPage> {
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<UserCenterPage> {
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<UserCenterPage> {
}
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<UserCenterPage> {
),
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<UserCenterPage> {
borderRadius: AppRadius.pillBorder,
onPressed: () => context.appPush(AppRoutes.login),
child: Text(
'去登录',
t.profile.goLogin,
style: AppTypography.body.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,

View File

@@ -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<AccountInsight> insights,

View File

@@ -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,
),
),
),
],
],
),
),
),
],

View File

@@ -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<AgreementPage> {
);
}
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<AgreementPage> {
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<AgreementPage> {
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<AgreementPage> {
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<AgreementPage> {
updateDate,
chapters,
ob,
languageId,
),
),
),
@@ -359,6 +372,7 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
String updateDate,
List<_ChapterInfo> chapters,
TOnboarding ob,
String languageId,
) {
final lines = content.split('\n');
final chapterIndexMap = <String, int>{};
@@ -366,13 +380,16 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
chapterIndexMap[chapters[i].fullTitle] = i;
}
final zhChapterRegex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+');
final enChapterRegex = RegExp(r'^(Zero|[IVXLCDM]+)\.\s+.+');
final widgets = <Widget>[
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<AgreementPage> {
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<AgreementPage> {
),
),
);
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<AgreementPage> {
};
}
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,

View File

@@ -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,
),
),
);
}

View File

@@ -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: <CircularSeries<_SeasonData, String>>[
DoughnutSeries<_SeasonData, String>(
dataSource: [
@@ -185,7 +186,7 @@ Widget buildMonthDistribution(
),
],
),
),
)),
const SizedBox(width: AppSpacing.sm),
Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -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: <CircularSeries<_NotePie, String>>[
DoughnutSeries<_NotePie, String>(
dataSource: pieData,
@@ -1408,7 +1409,7 @@ class _ContentPieChart extends StatelessWidget {
),
],
),
),
)),
],
),
),

View File

@@ -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: <CartesianSeries<_CoinTrend, String>>[
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: <CartesianSeries<_CoinTrend, String>>[
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: <CartesianSeries<_SourceBar, String>>[
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: <CartesianSeries<_SourceBar, String>>[
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: <CircularSeries<_RingData, String>>[
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: <CircularSeries<_RingData, String>>[
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;
},
),
],
],
),
),
),
),

View File

@@ -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: <CircularSeries<_PieItem, String>>[
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: <CircularSeries<_PieItem, String>>[
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: <CartesianSeries<_LinePoint, String>>[
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: <CartesianSeries<_LinePoint, String>>[
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: <CartesianSeries<_BarItem, String>>[
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: <CartesianSeries<_BarItem, String>>[
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: '数量',
),
],
),
),
),
],

View File

@@ -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: <CartesianSeries<_TrendPoint, String>>[
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: <CartesianSeries<_TrendPoint, String>>[
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: <CircularSeries<_CatData, String>>[
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: <CircularSeries<_CatData, String>>[
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)}%'
: '',
),
],
],
),
),
),
),

View File

@@ -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,
),
],
),
);

View File

@@ -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: 'مرحباً بعودتك',

View File

@@ -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: 'ফিরে আসার জন্য স্বাগতম',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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: 'वापसी पर स्वागत',

View File

@@ -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',

View File

@@ -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: 'おかえりなさい',

View File

@@ -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: '돌아오신 것을 환영합니다',

View File

@@ -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',

View File

@@ -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: 'С возвращением',

View File

@@ -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: '欢迎回来',

View File

@@ -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: '歡迎回來',

View File

@@ -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,
);
}

View File

@@ -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<String, String> 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<String, String> 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 ?? ''),
);
}

View File

@@ -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<String, String> 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<String, String> 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 ?? ''),
);
}

View File

@@ -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<DeferredBuilder> createState() => _DeferredBuilderState();
}
@@ -32,8 +41,8 @@ class _DeferredBuilderState extends State<DeferredBuilder> {
@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));
}
}

View File

@@ -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<NetworkStatusBanner> {
}
}
/// 简洁离线横幅 — 显示离线状态,不可关闭
class OfflineBanner extends ConsumerWidget {
/// 简洁离线横幅 — 显示离线状态,支持刷新和关闭
class OfflineBanner extends ConsumerStatefulWidget {
const OfflineBanner({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<OfflineBanner> createState() => _OfflineBannerState();
}
class _OfflineBannerState extends ConsumerState<OfflineBanner> {
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),
),
),
),
],

View File

@@ -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<void>(
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;
}
}

View File

@@ -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';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 16 KiB