chore: 完成v2.4.7版本迭代更新

本次更新包含多项功能优化与兼容性修复:
1. iOS/鸿蒙端添加加密出口合规配置,跳过App Store审核问卷
2. 新增学习计划设置页路由与国际化支持
3. 修复鸿蒙端剪贴板粘贴不工作问题,安装标准剪贴板拦截器
4. 优化收藏功能:兼容复合ID、添加状态同步与触觉反馈
5. 修复鸿蒙端相册保存兼容性,统一使用系统分享降级方案
6. 优化搜索快捷方式跳转逻辑,避免白屏问题
7. 更新本地化资源,新增闲情逸致、学习计划等模块翻译
8. 修复节气日期表排序与跨年边界问题
9. 优化设备信息页面显示,新增系统版本号展示
10. 重构文件传输二维码逻辑,使用纯URL提升兼容性
11. 优化设置项布局,避免文本溢出问题
12. 修复登录页记住账户功能,新增隐私协议守卫
13. 更新macOS依赖库,替换flutter_secure_storage为darwin版本
This commit is contained in:
Developer
2026-06-17 08:45:34 +08:00
parent 49b6323772
commit 544f77c0ce
82 changed files with 9779 additions and 5301 deletions

View File

@@ -0,0 +1,178 @@
/// ============================================================
/// 闲言APP — 应用商店跳转服务
/// 创建时间: 2026-06-17
/// 更新时间: 2026-06-17
/// 作用: 根据平台和语言地区生成正确的应用商店URL
/// 上次更新: 初始版本支持iOS/Android/鸿蒙/Windows/macOS多平台多地区
/// ============================================================
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../l10n/app_locale.dart';
import '../../l10n/types/t_root.dart';
import '../utils/logger.dart';
import '../utils/platform/platform_utils.dart' as pu;
/// 应用商店服务
///
/// 根据当前平台和语言地区生成对应的应用商店URL。
/// 不同地区的App Store可能使用不同的应用ID
/// 此服务统一管理这些ID和URL。
class AppStoreService {
AppStoreService._();
// ============================================================
// 应用ID配置 — 按地区区分
// ============================================================
/// iOS App Store 应用ID按地区
///
/// 不同地区的App Store可能有不同的应用ID
/// 如果应用在所有地区使用同一ID则都填同一个。
/// 当前配置:
/// - 中国大陆 (cn): 6771828376
/// - 其他地区: 6771828376
static const Map<String, String> _iosAppIdsByRegion = {
'CN': '6771828376',
'TW': '6771828376',
'HK': '6771828376',
'MO': '6771828376',
'US': '6771828376',
'JP': '6771828376',
'KR': '6771828376',
'GB': '6771828376',
'FR': '6771828376',
'DE': '6771828376',
'ES': '6771828376',
'IT': '6771828376',
'PT': '6771828376',
'RU': '6771828376',
'IN': '6771828376',
'BR': '6771828376',
'AR': '6771828376', // 阿拉伯语地区
'BN': '6771828376', // 孟加拉语地区
'HI': '6771828376', // 印地语地区
};
/// 默认iOS应用ID当地区未在映射中时使用
static const String _iosDefaultAppId = '6771828376';
/// Android 包名
static const String _androidPackageName = 'apps.xy.xianyan';
/// 鸿蒙应用包名
static const String _ohosPackageName = 'apps.xy.xianyan';
/// Windows 应用商店 Product ID
/// 注意用户提供两个URL第二个看起来是错误的少了个5
/// 正确的 Product ID: 9nqcv5gz10wnb
static const String _windowsProductId = '9nqcv5gz10wnb';
// ============================================================
// URL生成方法
// ============================================================
/// 根据当前语言地区获取iOS App Store URL
///
/// [locale] 当前语言Locale用于判断地区
static Uri getIOSAppStoreUrl(Locale locale) {
final countryCode = locale.countryCode ?? '';
final appId = _iosAppIdsByRegion[countryCode.toUpperCase()] ??
_iosDefaultAppId;
// App Store URL格式: https://apps.apple.com/{region}/app/id{appId}
// 如果有地区代码,加入地区路径以正确跳转
final region = countryCode.isNotEmpty ? '$countryCode/' : '';
final url = 'https://apps.apple.com/${region}app/id$appId';
Log.d('AppStoreService: iOS URL = $url (region: $countryCode, appId: $appId)');
return Uri.parse(url);
}
/// 获取Android Google Play URL
static Uri getAndroidPlayStoreUrl({bool webFallback = false}) {
if (webFallback) {
return Uri.parse(
'https://play.google.com/store/apps/details?id=$_androidPackageName',
);
}
return Uri.parse('market://details?id=$_androidPackageName');
}
/// 获取鸿蒙应用市场URL
static Uri getOhosAppGalleryUrl() {
return Uri.parse(
'https://appgallery.huawei.com/app/detail?id=$_ohosPackageName',
);
}
/// 获取Windows应用商店URL
///
/// [locale] 当前语言Locale用于设置hl参数
static Uri getWindowsStoreUrl(Locale locale) {
final langCode = locale.languageCode;
final countryCode = locale.countryCode ?? '';
// 根据语言构造hl参数如 zh-CN, en-GB
String hl;
if (countryCode.isNotEmpty) {
hl = '${langCode.toLowerCase()}-${countryCode.toUpperCase()}';
} else {
// 默认中文
hl = langCode == 'zh' ? 'zh-CN' : 'en-US';
}
return Uri.parse(
'https://apps.microsoft.com/detail/$_windowsProductId?hl=$hl',
);
}
/// 获取macOS App Store URL
///
/// macOS使用与iOS相同的应用ID如果应用同时支持macOS
static Uri getMacOSAppStoreUrl(Locale locale) {
// macOS版可能使用不同的应用ID这里暂时使用与iOS相同的逻辑
return getIOSAppStoreUrl(locale);
}
// ============================================================
// 统一入口方法
// ============================================================
/// 获取当前平台的应用商店URL
///
/// 自动根据平台和语言地区返回正确的URL。
/// 返回null表示当前平台不支持跳转应用商店。
static Uri? getStoreUrl(BuildContext context, WidgetRef ref) {
final locale = ref.read(appLocaleProvider);
return getStoreUrlByLocale(locale);
}
/// 根据Locale获取当前平台的应用商店URL
static Uri? getStoreUrlByLocale(Locale locale) {
if (pu.isIOS) {
return getIOSAppStoreUrl(locale);
}
if (pu.isAndroid) {
return getAndroidPlayStoreUrl();
}
if (pu.isOhos) {
return getOhosAppGalleryUrl();
}
if (pu.isWindows) {
return getWindowsStoreUrl(locale);
}
if (pu.isMacOS) {
return getMacOSAppStoreUrl(locale);
}
return null;
}
/// 获取应用商店显示名称
static String getStoreName(T t) {
if (pu.isIOS) return 'App Store';
if (pu.isAndroid) return 'Google Play';
if (pu.isOhos) return t.about.huaweiStore;
if (pu.isWindows) return 'Microsoft Store';
if (pu.isMacOS) return 'App Store';
return t.about.huaweiStore;
}
}

View File

@@ -0,0 +1,168 @@
/// ============================================================
/// 闲言APP — Token过期智能续期监听器
/// 创建时间: 2026-06-17
/// 更新时间: 2026-06-17
/// 作用: 监听网络恢复事件自动尝试Token续期失败则弹窗引导重新登录
/// 上次更新: 初始创建
/// ============================================================
import 'dart:async';
import 'package:flutter/cupertino.dart';
import '../network/connectivity_service.dart';
import 'token_service.dart';
import '../../storage/kv_storage.dart';
import '../../utils/logger.dart';
import '../../router/app_router.dart' show rootNavigatorKey;
import '../../router/app_routes.dart';
class TokenRefreshWatcher {
TokenRefreshWatcher._();
// ============================================================
// 私有状态
// ============================================================
static StreamSubscription<NetworkType>? _networkSub;
static bool _isWatching = false;
static bool _wasOffline = false;
static bool _dialogShowing = false;
static const String _dialogFlagKey = '_relogin_dialog_showing';
// ============================================================
// 公开接口
// ============================================================
/// 启动监听(在 ConnectivityService 初始化之后调用)
static void startWatching() {
if (_isWatching) return;
_isWatching = true;
// 记录初始网络状态
_wasOffline = ConnectivityService.isOffline;
_networkSub = ConnectivityService.onTypeChange.listen(_onNetworkChanged);
Log.i('TokenRefreshWatcher: 开始监听网络变化');
}
/// 停止监听
static void stopWatching() {
_networkSub?.cancel();
_networkSub = null;
_isWatching = false;
Log.i('TokenRefreshWatcher: 停止监听');
}
/// 当前是否正在监听
static bool get isWatching => _isWatching;
// ============================================================
// 网络变化回调
// ============================================================
/// 网络类型变化时触发
static void _onNetworkChanged(NetworkType type) {
final isOffline = type == NetworkType.none;
// 从离线恢复到在线时,尝试续期
if (_wasOffline && !isOffline) {
Log.i('TokenRefreshWatcher: 检测到网络恢复尝试Token续期');
_tryRefreshToken();
}
_wasOffline = isOffline;
}
// ============================================================
// Token续期逻辑
// ============================================================
/// 尝试续期Token
static Future<void> _tryRefreshToken() async {
try {
// 先检查Token是否有效
final result = await TokenService.checkToken();
if (result.valid) {
Log.i('TokenRefreshWatcher: Token有效无需续期');
return;
}
// Token无效尝试续期
Log.i('TokenRefreshWatcher: Token无效(${result.reason}),尝试续期...');
final refreshed = await TokenService.refreshToken();
if (refreshed) {
Log.i('TokenRefreshWatcher: Token续期成功 ✓');
} else {
Log.w('TokenRefreshWatcher: Token续期失败引导重新登录');
_showReLoginDialog();
}
} catch (e) {
Log.e('TokenRefreshWatcher: 续期异常', e);
}
}
// ============================================================
// 重新登录引导弹窗
// ============================================================
/// 弹出重新登录引导弹窗Cupertino风格
static void _showReLoginDialog() {
final context = rootNavigatorKey.currentContext;
if (context == null) {
Log.w('TokenRefreshWatcher: context不可用无法弹窗');
return;
}
// 避免重复弹窗
if (_dialogShowing) return;
if (KvStorage.getBool(_dialogFlagKey) == true) return;
_dialogShowing = true;
KvStorage.setBool(_dialogFlagKey, true);
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('🔑 登录已过期'),
content: const Text('您的登录凭证已失效,请重新登录以继续使用完整功能。'),
actions: [
CupertinoDialogAction(
onPressed: () {
_dismissDialog(ctx);
},
child: const Text('稍后'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
_dismissDialog(ctx);
_navigateToLogin();
},
child: const Text('重新登录'),
),
],
),
);
}
/// 关闭弹窗并重置标记
static void _dismissDialog(BuildContext ctx) {
_dialogShowing = false;
KvStorage.setBool(_dialogFlagKey, false);
Navigator.pop(ctx);
}
/// 导航到登录页
static void _navigateToLogin() {
try {
final nav = rootNavigatorKey.currentState;
if (nav != null) {
nav.pushNamed(AppRoutes.login);
Log.i('TokenRefreshWatcher: 已导航到登录页');
}
} catch (e) {
Log.e('TokenRefreshWatcher: 导航到登录页失败', e);
}
}
}

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 桌面小组件数据管理服务
/// 创建时间: 2026-05-15
/// 更新时间: 2026-06-16
/// 更新时间: 2026-06-17
/// 作用: 基于home_widget库管理桌面小组件数据推送与交互
/// 上次更新: 添加平台检测Windows/Linux桌面端跳过HomeWidget调用防止MissingPluginException
/// 上次更新: 统一使用PlatformCapabilities.supports(CapabilityKey.homeWidget)判断平台支持移除_isPlatformSupported双轨逻辑
/// ============================================================
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -69,8 +69,13 @@ class HomeWidgetService {
bool get isInitialized => _initialized;
/// 当前平台是否支持桌面小组件(仅 iOS/Android/macOS 支持Windows/Linux 跳过)
bool get _isPlatformSupported => !pu.isDesktop || pu.isMacOS;
/// 当前平台是否支持桌面小组件
///
/// 统一通过 [PlatformCapabilities] 查询 [CapabilityKey.homeWidget] 能力,
/// 避免与平台判断逻辑pu.isDesktop 等)产生双轨不一致问题。
/// 支持平台: iOS / Android / 鸿蒙macOS/Windows/Linux/Web 不支持。
bool get _isPlatformSupported =>
PlatformCapabilities.supports(CapabilityKey.homeWidget);
// ============================================================
// 初始化
@@ -542,12 +547,6 @@ class HomeWidgetService {
try {
await _ensureInit();
// [PlatformCapabilities] 统一能力查询: homeWidget
if (!PlatformCapabilities.supports(CapabilityKey.homeWidget)) {
Log.w('HomeWidgetService: 当前平台不支持桌面小组件,跳过刷新');
return;
}
if (type == WidgetType.dailyWithCharacter) {
if (!pu.isOhos) {
Log.i('HomeWidgetService: ${type.title} 原生端尚未实现,跳过刷新');

View File

@@ -1,11 +1,12 @@
// ============================================================
// 闲言APP — 设备信息服务
// 创建时间: 2026-05-10
// 更新时间: 2026-06-05
// 更新时间: 2026-06-17
// 作用: 采集设备信息并自动注册到服务端
// 上次更新: 鸿蒙端增加设备信息回退逻辑Web端精确识别浏览器标识
// 上次更新: 新增 getSystemVersion() 方法获取系统版本号
// ============================================================
import 'dart:io' show Platform;
import 'dart:math';
import 'package:device_info_plus/device_info_plus.dart';
@@ -638,6 +639,73 @@ class DeviceInfoService {
return browserCn;
}
// ============================================================
// 获取系统版本号
// ============================================================
static Future<String> getSystemVersion() async {
if (kIsWeb) return '';
try {
if (pu.isOhos) {
// 鸿蒙:从 Platform.operatingSystemVersion 获取
try {
final ver = Platform.operatingSystemVersion;
if (ver.isNotEmpty) return _extractOhosVersion(ver);
} catch (_) {}
// 回退:使用 Android 信息
final android = await _deviceInfoPlugin.androidInfo;
return 'Android ${android.version.release}';
} else if (pu.isAndroid) {
final android = await _deviceInfoPlugin.androidInfo;
return 'Android ${android.version.release}';
} else if (pu.isIOS) {
final ios = await _deviceInfoPlugin.iosInfo;
return 'iOS ${ios.systemVersion}';
} else if (pu.isMacOS) {
final mac = await _deviceInfoPlugin.macOsInfo;
return 'macOS ${mac.majorVersion}.${mac.minorVersion}.${mac.patchVersion}';
} else if (pu.isWindows) {
final windows = await _deviceInfoPlugin.windowsInfo;
final major = windows.majorVersion;
final display = windows.displayVersion;
if (display.isNotEmpty) return 'Windows $major ($display)';
return 'Windows $major';
} else if (pu.isLinux) {
try {
final linux = await _deviceInfoPlugin.linuxInfo;
if (linux.version != null && linux.version!.isNotEmpty) {
return 'Linux ${linux.version}';
}
} catch (_) {}
// 回退:从 Platform.operatingSystemVersion 提取
try {
final ver = Platform.operatingSystemVersion;
if (ver.isNotEmpty) return 'Linux ${_extractKernelVersion(ver)}';
} catch (_) {}
return 'Linux';
}
} catch (e) {
Log.w('获取系统版本失败: $e', null, null, LogCategory.device);
}
return '';
}
/// 从鸿蒙 operatingSystemVersion 提取版本号
/// 例如 "HarmonyOS 5.0.0" → "鸿蒙 5.0"
static String _extractOhosVersion(String ver) {
final match = RegExp(r'(\d+\.\d+)').firstMatch(ver);
if (match != null) return '鸿蒙 ${match.group(1)}';
return '鸿蒙';
}
/// 从 Linux operatingSystemVersion 提取内核版本
/// 例如 "6.1.0-generic" → "6.1"
static String _extractKernelVersion(String ver) {
final match = RegExp(r'(\d+\.\d+)').firstMatch(ver);
if (match != null) return match.group(1)!;
return ver;
}
// ============================================================
// 获取平台标识
// ============================================================

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — 快捷操作服务
// 创建时间: 2026-05-31
/// 更新时间: 2026-06-16
/// 更新时间: 2026-06-17
/// 作用: 管理主屏幕快捷操作(Quick Actions / App Shortcuts)
/// 上次更新: 使用ShortcutManager API创建快捷方式修复图标和跳转问题
/// 上次更新: 修复搜索快捷方式白屏问题,改用 action:search 标记由回调方直接弹搜索浮层
// 跨平台: iOS(UIApplicationShortcutItems) + Android(ShortcutManager)
// + 鸿蒙(module.json5 shortcuts + MethodChannel)
// ============================================================
@@ -176,13 +176,22 @@ class QuickActionsService {
});
}
/// 解析快捷方式为路由字符串
///
/// 搜索快捷方式使用特殊标记 `action:search`由回调方app.dart负责
/// 1. 切换到 profile TabgoBranch而非 push 新页面
/// 2. 直接在当前 context 弹出 SpotlightSearchOverlay
///
/// 这样可避免以下问题:
/// - push `/profile` 会在 shell 之外创建新页面,导致白屏只显示底栏
/// - ProfilePage 已构建过时 initState 不会再触发pendingSearch 永不消费
static String? _resolveRoute(String shortcutType) {
switch (shortcutType) {
case 'action_theme':
return AppRoutes.themeSettings;
case 'action_search':
// 带参数路由通知ProfilePage自动弹出搜索
return '${AppRoutes.profile}?action=search';
// 特殊标记:搜索动作,由回调方直接弹出搜索浮层
return 'action:search';
case 'action_general_settings':
return AppRoutes.generalSettings;
default:

View File

@@ -15,6 +15,7 @@ import '../storage/kv_storage.dart';
import '../utils/logger.dart';
import '../utils/platform/platform_utils.dart' as pu;
import '../router/app_router.dart' show rootNavigatorKey;
import 'auth/token_refresh_watcher.dart';
import 'auth/permission_service.dart';
import 'device/haptic_service.dart';
import 'network/connectivity_service.dart';
@@ -140,6 +141,14 @@ class PostAgreementInitializer {
}
}
// Token续期监听器依赖ConnectivityService必须在其之后启动
try {
TokenRefreshWatcher.startWatching();
Log.i('Token续期监听器启动完成');
} catch (e, st) {
Log.e('Token续期监听器启动失败', e, st);
}
if (!pu.isWeb) {
try {
await BackgroundTaskService.instance.init();