chore: v6.6.6 版本迭代更新

主要变更:
1. 重构"国学"相关模块为"经典名句",统一命名规范
2. 重命名"阅读报告"为"使用报告",调整相关文案与配置
3. 修复iOS模拟器图片缓存兼容问题,优化图表渲染逻辑
4. 新增设备活跃状态前端兜底判断,修复在线计数异常
5. 完善登录/注册流程,新增忘记密码路由与账户编辑提示
6. 优化文件传输与字体导入逻辑,废弃过时的bytes属性使用
7. 添加Spotlight全局快捷键支持,更新隐私权限与通知配置
8. 补充数据库迁移脚本与部署文档,修复后端接口兼容问题
9. 调整部分UI交互细节,优化内存占用与应用稳定性
This commit is contained in:
Developer
2026-06-07 06:56:52 +08:00
parent e119c84868
commit f281e465bb
159 changed files with 10502 additions and 2120 deletions

View File

@@ -173,7 +173,7 @@ class OverviewDashboard extends ConsumerWidget {
(emoji: '📖', label: '稍后读', route: AppRoutes.readLater),
(emoji: '🕐', label: '历史', route: AppRoutes.history),
(emoji: '', label: '签到', route: AppRoutes.signin),
(emoji: '📊', label: '阅读报告', route: AppRoutes.readingReport),
(emoji: '📊', label: '使用报告', route: AppRoutes.readingReport),
(emoji: '🌤️', label: '每日推荐', route: AppRoutes.dailyCard),
(emoji: '⚙️', label: '设置', route: AppRoutes.generalSettings),
];

View File

@@ -130,15 +130,19 @@ String? _handleRedirect(BuildContext context, GoRouterState state) {
}
// 引导页已完成但当前在引导页 → 跳转首页
// 例外:如果带有 review=true 参数,说明是用户主动查看,不拦截
if (onboardingDone && !shouldShow && currentPath == AppRoutes.onboarding) {
final savedPage = KvStorage.getString('general_startup_page');
final location = switch (savedPage) {
'inspiration' => AppRoutes.inspiration,
'discover' => AppRoutes.discover,
'profile' => AppRoutes.profile,
_ => AppRoutes.home,
};
return location;
final isReview = state.uri.queryParameters['review'] == 'true';
if (!isReview) {
final savedPage = KvStorage.getString('general_startup_page');
final location = switch (savedPage) {
'inspiration' => AppRoutes.inspiration,
'discover' => AppRoutes.discover,
'profile' => AppRoutes.profile,
_ => AppRoutes.home,
};
return location;
}
}
}

View File

@@ -35,6 +35,7 @@ class AppRoutes {
static const String likes = '/likes';
static const String quickCard = '/quick-card';
static const String login = '/login';
static const String forgotPassword = '/forgot-password';
static const String signin = '/signin';
static const String myDevices = '/my-devices';
static const String qrcodeLogin = '/qrcode-login';
@@ -127,6 +128,8 @@ class AppRoutes {
static const String toolCenterSettings = '/tool-center/settings';
static const String rssReader = '/tool/rss_reader';
static const String onboarding = '/onboarding';
/// 主动查看引导页从关于页等入口带此参数跳过redirect拦截
static const String onboardingReview = '/onboarding?review=true';
static const String experimentalFeatures = '/settings/experimental-features';
static const String exchangeRate = '/tool/exchange_rate';
static const String nickTool = '/tool/nick';

View File

@@ -10,6 +10,7 @@ import 'package:xianyan/core/router/ohos_route_types.dart';
import 'package:xianyan/core/router/app_routes.dart';
import 'package:xianyan/features/auth/presentation/login_page.dart';
import 'package:xianyan/features/auth/presentation/forgot_password_page.dart';
import 'package:xianyan/features/auth/presentation/qrcode_login_page.dart';
import 'package:xianyan/features/mine/signin/presentation/signin_page.dart';
import 'package:xianyan/features/mine/user_center/presentation/my_devices_page.dart';
@@ -150,6 +151,12 @@ final List<RouteDef> routeRegistry = [
module: RouteModule.user,
page: () => const LoginPage(),
),
RouteDef(
path: AppRoutes.forgotPassword,
name: 'forgot-password',
module: RouteModule.user,
page: () => const ForgotPasswordPage(),
),
RouteDef(
path: AppRoutes.signin,
name: 'signin',

View File

@@ -143,6 +143,12 @@ enum AppPermission {
CupertinoIcons.arrow_counterclockwise,
Color(0xFF5856D6),
isVirtual: true,
),
tracking(
Permission.notification,
CupertinoIcons.eye_fill,
Color(0xFF5AC8FA),
group: PermissionGroup.optional,
);
const AppPermission(
@@ -176,6 +182,7 @@ enum AppPermission {
AppPermission.clipboard => t.permClipboardLabel,
AppPermission.share => t.permShareLabel,
AppPermission.shake => t.permShakeLabel,
AppPermission.tracking => t.permTrackingLabel,
};
}
@@ -194,6 +201,7 @@ enum AppPermission {
AppPermission.clipboard => t.permClipboardDesc,
AppPermission.share => t.permShareDesc,
AppPermission.shake => t.permShakeDesc,
AppPermission.tracking => t.permTrackingDesc,
};
}
@@ -212,6 +220,7 @@ enum AppPermission {
AppPermission.clipboard => t.permClipboardUsage,
AppPermission.share => t.permShareUsage,
AppPermission.shake => t.permShakeUsage,
AppPermission.tracking => t.permTrackingUsage,
};
if (raw.isEmpty) return const [];
return raw.split('|');
@@ -232,16 +241,21 @@ enum AppPermission {
AppPermission.clipboard => t.permClipboardDenial,
AppPermission.share => t.permShareDenial,
AppPermission.shake => t.permShakeDenial,
AppPermission.tracking => t.permTrackingDenial,
};
}
/// Android 13+ 不需要 storage 权限(由 photos 替代)
/// tracking 权限仅 iOS 展示
bool get isPlatformRelevant {
if (this == AppPermission.storage) {
if (!pu.isAndroid) return false;
final sdkInt = _androidSdkInt;
return sdkInt != null && sdkInt <= 32;
}
if (this == AppPermission.tracking) {
return Platform.isIOS;
}
return true;
}
@@ -397,6 +411,10 @@ class PermissionService {
if (perm.isVirtual) {
return _checkVirtualStatus(perm);
}
// ATT 追踪权限使用专用 API 查询
if (perm == AppPermission.tracking) {
return _checkTrackingStatus();
}
try {
final status = await perm.permission.status;
return AppPermissionStatus.fromPermissionStatus(status);
@@ -406,6 +424,24 @@ class PermissionService {
}
}
/// 检查 ATT 追踪权限状态
static Future<AppPermissionStatus> _checkTrackingStatus() async {
if (!Platform.isIOS) return AppPermissionStatus.granted;
try {
final status = await AppTrackingTransparency.trackingAuthorizationStatus;
return switch (status) {
TrackingStatus.authorized => AppPermissionStatus.granted,
TrackingStatus.denied => AppPermissionStatus.permanentlyDenied,
TrackingStatus.notDetermined => AppPermissionStatus.notDetermined,
TrackingStatus.restricted => AppPermissionStatus.restricted,
_ => AppPermissionStatus.notDetermined,
};
} catch (e) {
_log.e('ATT状态查询异常', e);
return AppPermissionStatus.notDetermined;
}
}
/// 虚拟权限状态检查
static Future<AppPermissionStatus> _checkVirtualStatus(
AppPermission perm,
@@ -444,6 +480,10 @@ class PermissionService {
String? rationale,
}) async {
if (perm.isVirtual) return true;
// ATT 追踪权限使用专用 API 请求
if (perm == AppPermission.tracking) {
return _requestTrackingPermission(context);
}
try {
final status = await perm.permission.status;
@@ -573,6 +613,33 @@ class PermissionService {
}
}
/// 从权限管理页面请求 ATT 追踪权限(带状态检查和对话框提示)
static Future<bool> _requestTrackingPermission(BuildContext context) async {
if (!Platform.isIOS) return true;
try {
// 先检查当前状态
final currentStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
if (currentStatus == TrackingStatus.authorized) {
_log.i('ATT已授权无需重复请求');
return true;
}
if (currentStatus == TrackingStatus.denied ||
currentStatus == TrackingStatus.restricted) {
// 已被拒绝或受限,引导去系统设置
if (context.mounted) {
_showSettingsDialog(context, AppPermission.tracking);
}
return false;
}
// notDetermined 状态,发起请求
final authorized = await requestTrackingPermission();
return authorized;
} catch (e) {
_log.e('ATT权限请求异常', e);
return false;
}
}
/// 检查当前ATT授权状态仅iOS
static Future<TrackingStatus> getTrackingStatus() async {
if (!Platform.isIOS) return TrackingStatus.authorized;

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 图片缓存管理器
/// 创建时间: 2026-05-25
/// 更新时间: 2026-05-25
/// 更新时间: 2026-06-06
/// 作用: 统一管理图片内存/磁盘缓存策略
/// 上次更新: 初始创建
/// 上次更新: 修复iOS模拟器objective_c FFI不兼容导致的崩溃
/// ============================================================
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
@@ -21,17 +21,28 @@ class CustomCacheManager {
static CacheManager? _instance;
/// 获取CacheManager单例
/// 在iOS模拟器上path_provider_foundation的FFI绑定可能不兼容
/// 此时回退到DefaultCacheManager
static CacheManager get instance {
_instance ??= CacheManager(
Config(
key,
stalePeriod: maxStale,
maxNrOfCacheObjects: maxNrOfCacheObjects,
repo: JsonCacheInfoRepository(databaseName: key),
fileSystem: IOFileSystem(key),
fileService: HttpFileService(),
),
);
if (_instance != null) return _instance!;
try {
_instance = CacheManager(
Config(
key,
stalePeriod: maxStale,
maxNrOfCacheObjects: maxNrOfCacheObjects,
repo: JsonCacheInfoRepository(databaseName: key),
fileSystem: IOFileSystem(key),
fileService: HttpFileService(),
),
);
} catch (e) {
// iOS模拟器上objective_c FFI可能不兼容回退到DefaultCacheManager
Log.w('ImageCacheManager: IOFileSystem初始化失败回退到DefaultCacheManager', e);
return DefaultCacheManager();
}
return _instance!;
}

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — 最近路由服务(单一数据源)
// 创建时间: 2026-05-24
// 更新时间: 2026-06-01
// 更新时间: 2026-06-07
// 作用: 统一管理最近访问路由和自定义路由消除KvStorage双写问题
// 上次更新: 添加动态路由归一化,/chat-flow/123 → /chat-flow避免重复记录
// 上次更新: 添加 removeRecentRoute 方法支持长按删除
// ============================================================
import 'dart:convert';
@@ -98,6 +98,18 @@ class RecentRouteService {
}
}
/// 删除指定路由(归一化后匹配)
static Future<void> removeRecentRoute(String route) async {
final normalized = _normalizeRoute(route);
try {
final updated = List<String>.from(getRecentRoutes())..remove(normalized);
await KvStorage.setString(_kRecentRoutes, jsonEncode(updated));
Log.i('最近路由: 删除 → $normalized');
} catch (e) {
Log.w('最近路由: 删除失败 $e');
}
}
static Map<String, int> getRouteCounts() {
try {
final raw = KvStorage.getString(_kRouteCounts);

View File

@@ -1,9 +1,10 @@
// ignore_for_file: avoid_dynamic_calls
/// ============================================================
/// 闲言APP — 通知初始化桥接
/// 创建时间: 2026-05-22
/// 更新时间: 2026-06-05
/// 更新时间: 2026-06-06
/// 作用: 构建含OHOS参数的InitializationSettings
/// 上次更新: 新增PlatformCapabilities能力查询补充localNotification判断
/// 上次更新: 添加avoid_dynamic_calls忽略鸿蒙端需dynamic反射
/// ============================================================
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@@ -95,8 +96,7 @@ Future<bool> requestOhosNotificationPermission(
final dynamic ohosPlugin =
plugin.resolvePlatformSpecificImplementation();
if (ohosPlugin == null) return false;
final dynamic result =
await ohosPlugin.requestNotificationsPermission();
final dynamic result = await ohosPlugin.requestNotificationsPermission();
return result == true;
} catch (e) {
return false;

View File

@@ -8,6 +8,8 @@
import 'dart:io';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import '../storage/kv_storage.dart';
import '../utils/logger.dart';
import '../utils/platform/platform_utils.dart' as pu;
@@ -38,10 +40,16 @@ class PostAgreementInitializer {
Log.i('PostAgreementInitializer: 开始初始化权限敏感服务...');
// iOS ATT授权请求必须在协议同意后、其他服务初始化前请求
// 先检查当前授权状态仅在未决定时请求避免Info.plist缺少描述时原生崩溃
if (Platform.isIOS) {
try {
final authorized = await PermissionService.requestTrackingPermission();
Log.i('iOS ATT授权结果: ${authorized ? "已授权" : "未授权"}');
final status = await AppTrackingTransparency.trackingAuthorizationStatus;
if (status == TrackingStatus.notDetermined) {
final authorized = await PermissionService.requestTrackingPermission();
Log.i('iOS ATT授权结果: ${authorized ? "已授权" : "未授权"}');
} else {
Log.i('iOS ATT已授权状态: $status,跳过请求');
}
} catch (e, st) {
Log.e('iOS ATT授权请求失败', e, st);
}

View File

@@ -1203,14 +1203,10 @@ class AppDatabase extends _$AppDatabase {
Future<void> toggleFavorite(String id) async {
await _safeDbVoid(() async {
final exists = await (select(
sentences,
)..where((t) => t.id.equals(id))).getSingleOrNull();
if (exists == null) return;
await (update(sentences)..where((t) => t.id.equals(id))).write(
SentencesCompanion(
isFavorite: Value(!exists.isFavorite),
updatedAt: Value(DateTime.now()),
SentencesCompanion.custom(
isFavorite: sentences.isFavorite.not(),
updatedAt: Constant(DateTime.now()),
),
);
}, 'toggleFavorite');
@@ -1218,14 +1214,10 @@ class AppDatabase extends _$AppDatabase {
Future<void> toggleLike(String id) async {
await _safeDbVoid(() async {
final exists = await (select(
sentences,
)..where((t) => t.id.equals(id))).getSingleOrNull();
if (exists == null) return;
await (update(sentences)..where((t) => t.id.equals(id))).write(
SentencesCompanion(
isLiked: Value(!exists.isLiked),
updatedAt: Value(DateTime.now()),
SentencesCompanion.custom(
isLiked: sentences.isLiked.not(),
updatedAt: Constant(DateTime.now()),
),
);
}, 'toggleLike');
@@ -1233,10 +1225,6 @@ class AppDatabase extends _$AppDatabase {
Future<void> markAsRead(String id) async {
await _safeDbVoid(() async {
final exists = await (select(
sentences,
)..where((t) => t.id.equals(id))).getSingleOrNull();
if (exists == null) return;
await (update(sentences)..where((t) => t.id.equals(id))).write(
SentencesCompanion.custom(
isRead: const Constant(true),
@@ -1626,28 +1614,22 @@ class AppDatabase extends _$AppDatabase {
Future<void> recordToolUsage(String toolId, String toolName) {
return _safeDbVoid(() async {
final existing = await (select(
toolUsageStats,
)..where((t) => t.toolId.equals(toolId))).getSingleOrNull();
if (existing != null) {
await (update(
toolUsageStats,
)..where((t) => t.toolId.equals(toolId))).write(
ToolUsageStatsCompanion(
useCount: Value(existing.useCount + 1),
lastUsedAt: Value(DateTime.now()),
),
);
} else {
await into(toolUsageStats).insert(
ToolUsageStatsCompanion.insert(
toolId: toolId,
toolName: Value(toolName),
lastUsedAt: DateTime.now(),
createdAt: DateTime.now(),
),
);
}
await into(toolUsageStats).insert(
ToolUsageStatsCompanion.insert(
toolId: toolId,
toolName: Value(toolName),
lastUsedAt: DateTime.now(),
createdAt: DateTime.now(),
),
mode: InsertMode.insertOrIgnore,
);
await (update(toolUsageStats)..where((t) => t.toolId.equals(toolId)))
.write(
ToolUsageStatsCompanion.custom(
useCount: toolUsageStats.useCount + const Variable<int>(1),
lastUsedAt: Constant(DateTime.now()),
),
);
}, 'recordToolUsage');
}
@@ -1802,14 +1784,10 @@ class AppDatabase extends _$AppDatabase {
Future<void> incrementRetryCount(int id) {
return _safeDbVoid(() async {
final existing = await (select(
offlineActionQueue,
)..where((t) => t.id.equals(id))).getSingleOrNull();
if (existing == null) return;
await (update(offlineActionQueue)..where((t) => t.id.equals(id))).write(
OfflineActionQueueCompanion(
retryCount: Value(existing.retryCount + 1),
lastAttemptAt: Value(DateTime.now()),
OfflineActionQueueCompanion.custom(
retryCount: offlineActionQueue.retryCount + const Variable<int>(1),
lastAttemptAt: Constant(DateTime.now()),
),
);
}, 'incrementRetryCount');

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 颜色令牌
/// 创建时间: 2026-04-20
/// 更新时间: 2026-04-20
/// 作用: 统一颜色定义,支持日间/夜间模式
/// 上次更新: 初始创建
/// 更新时间: 2026-06-07
/// 作用: 统一颜色定义,支持日间/夜间/AMOLED模式
/// 上次更新: 修复深色模式对比度不足问题提亮textSecondary/textHint/iconDisabled/overlaySubtle
/// ============================================================
import 'package:flutter/cupertino.dart';
@@ -52,6 +52,9 @@ class LightColors {
}
/// 颜色令牌 — AMOLED纯黑模式
///
/// 纯黑背景下文字对比度天然较高,但仍需确保 textHint 等弱色可读,
/// overlaySubtle 使用白色半透明避免边框消失。
class AmoledColors {
AmoledColors._();
@@ -74,18 +77,18 @@ class AmoledColors {
// ---- 文字色 ----
static const Color textPrimary = Color(0xFFF5F5F5);
static const Color textSecondary = Color(0xFF9CA3AF);
static const Color textHint = Color(0xFF6B7280);
static const Color textDisabled = Color(0xFF4B5563);
static const Color textSecondary = Color(0xFFAEAEB2); // iOS systemGray2
static const Color textHint = Color(0xFF8E8E93); // iOS systemGray对比度≈8.5:1
static const Color textDisabled = Color(0xFF636366); // 提亮确保最低可读
static const Color textInverse = Color(0xFF000000);
// ---- 图标色 ----
static const Color iconPrimary = Color(0xFFFFFFFF);
static const Color iconSecondary = Color(0xFF9CA3AF);
static const Color iconDisabled = Color(0xFF4B5563);
static const Color iconSecondary = Color(0xFFAEAEB2); // 与 textSecondary 一致
static const Color iconDisabled = Color(0xFF636366); // 与 textDisabled 一致
// ---- 遮罩色 ----
static const Color overlaySubtle = Color(0x01000000);
static const Color overlaySubtle = Color(0x55FFFFFF); // 白色33%透明度,确保边框可见
static const Color overlayMedium = Color(0xCB000000);
static const Color overlayLight = Color(0xB3000000);
static const Color overlayStrong = Color(0x80000000);
@@ -95,6 +98,10 @@ class AmoledColors {
}
/// 颜色令牌 — 夜间模式
///
/// 深色模式颜色遵循 WCAG AA 对比度标准文字≥4.5:1
/// textHint/iconDisabled 适当提亮确保可读性,
/// overlaySubtle 使用白色半透明确保边框/分割线可见。
class DarkColors {
DarkColors._();
@@ -117,18 +124,18 @@ class DarkColors {
// ---- 文字色 ----
static const Color textPrimary = Color(0xFFE5E5E5);
static const Color textSecondary = Color(0xFF9CA3AF);
static const Color textHint = Color(0xFF6B7280);
static const Color textDisabled = Color(0xFF4B5563);
static const Color textSecondary = Color(0xFFAEAEB2); // iOS systemGray2对比度≈6:1
static const Color textHint = Color(0xFF8E8E93); // iOS systemGray对比度≈4.5:1
static const Color textDisabled = Color(0xFF636366); // 提亮确保最低可读
static const Color textInverse = Color(0xFF1A1A2E);
// ---- 图标色 ----
static const Color iconPrimary = Color(0xFFFFFFFF);
static const Color iconSecondary = Color(0xFF9CA3AF);
static const Color iconDisabled = Color(0xFF4B5563);
static const Color iconSecondary = Color(0xFFAEAEB2); // 与 textSecondary 一致
static const Color iconDisabled = Color(0xFF636366); // 与 textDisabled 一致
// ---- 遮罩色 ----
static const Color overlaySubtle = Color(0x01333333);
static const Color overlaySubtle = Color(0x55FFFFFF); // 白色33%透明度,确保边框可见
static const Color overlayMedium = Color(0xCB333333);
static const Color overlayLight = Color(0xB3333333);
static const Color overlayStrong = Color(0x80000000);

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 主题系统
/// 创建时间: 2026-04-20
/// 更新时间: 2026-05-26
/// 更新时间: 2026-06-07
/// 作用: 统一 ThemeData 配置,整合设计令牌,支持日/夜切换
/// 上次更新: 集成高对比度覆盖和色弱过滤buildFromSettings新增highContrast/colorWeakTypeId参数
/// 上次更新: 修复深色模式对比度不足DarkColors/AmoledColors提亮textSecondary/textHint/overlaySubtle
/// ============================================================
import 'package:flutter/cupertino.dart';