chore: 完成多平台适配与代码优化

此提交包含多项变更:
1. 新增鸿蒙平台支持,完善设备检测与数据库适配
2. 替换旧版分享插件API为SharePlus
3. 批量迁移StateNotifier到Notifier以适配新版Riverpod
4. 修复zip编码判断、图表API参数等bug
5. 更新应用图标、启动页资源与多尺寸适配图标
6. 调整Android最小SDK版本与应用名称
7. 优化日志打印与正则表达式使用
8. 修正编辑器画布样式初始化与配置逻辑
9. 更新依赖与CI插件配置
This commit is contained in:
Developer
2026-05-17 07:17:07 +08:00
parent 0611d57347
commit 7564e8893d
225 changed files with 4677 additions and 5600 deletions

View File

@@ -5,6 +5,7 @@ alwaysApply: true
# AGENTS.md
干活别偷懒输入输出不需要考虑token消耗我的token是无限的量大管饱
优先使用ios风格的组件若Cupertino无对应组件 再使用material
每个文件头部需要增加标准注释,创建时间 更新时间 名称 作用 上次更新内容,代码部分 分类和方法也需要注释
视觉方面的渲染组件或者布局,不要考虑性能,该有的效果全部拉满,实现最大效果

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ android {
applicationId = "com.example.xianyan"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@@ -69,7 +69,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<application
android:label="xianyan"
android:label="闲言"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

1
build_log.txt Normal file
View File

@@ -0,0 +1 @@

View File

@@ -126,6 +126,9 @@ class _XianyanAppState extends ConsumerState<XianyanApp>
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
Log.i(
'XianyanApp ScreenUtilInit builder: screenSize=${MediaQuery.of(context).size} themeMode=$themeMode isDark=${settings.isDark}',
);
return GlassTheme(
data: GlassThemeData(
light: GlassThemeVariant(

View File

@@ -1,11 +1,13 @@
// ============================================================
// 闲言APP — 应用布局壳
// 创建时间: 2026-04-20
// 更新时间: 2026-05-14
// 更新时间: 2026-05-17
// 作用: ShellRoute 布局壳,包含底部 GlassBottomBar 导航 + 发现小红点
// 上次更新: 修复底部Tab栏双文本问题移除GlassBottomBarTab的label由TabIconSprite统一控制
// 上次更新: 鸿蒙白屏调试恢复GlassBottomBar+CupertinoTabBar鸿蒙降级方案
// ============================================================
import 'dart:io';
import 'package:badges/badges.dart' as badges;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -16,6 +18,7 @@ import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import '../theme/app_theme.dart';
import '../utils/interaction_animations.dart';
import '../utils/logger.dart';
import '../../features/inspiration/providers/chat_provider.dart';
import '../../features/settings/providers/theme_settings_provider.dart';
import '../../shared/widgets/tab_icon_sprite.dart';
@@ -25,6 +28,14 @@ class AppShell extends ConsumerWidget {
final StatefulNavigationShell child;
static bool get _isOhos {
try {
return Platform.operatingSystem == 'ohos';
} catch (_) {
return false;
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final ext = AppTheme.ext(context);
@@ -55,6 +66,105 @@ class AppShell extends ConsumerWidget {
);
}
Widget bottomBar = _isOhos
? _buildCupertinoNavBar(context, currentIndex, ext)
: GlassBottomBar(
tabs: [
GlassBottomBarTab(
label: '',
icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'),
activeIcon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'),
glowColor: const Color(0xFFE8E8ED),
),
GlassBottomBarTab(
label: '',
icon: badges.Badge(
showBadge: unreadCount > 0,
badgeContent: const Text(
'',
style: TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
badgeStyle: const badges.BadgeStyle(
badgeColor: CupertinoColors.systemRed,
padding: EdgeInsets.all(3),
),
position: badges.BadgePosition.topEnd(top: -4, end: -6),
child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
),
activeIcon: badges.Badge(
showBadge: unreadCount > 0,
badgeContent: const Text(
'',
style: TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
badgeStyle: const badges.BadgeStyle(
badgeColor: CupertinoColors.systemRed,
padding: EdgeInsets.all(3),
),
position: badges.BadgePosition.topEnd(top: -4, end: -6),
child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
),
glowColor: const Color(0xFFE8E8ED),
),
GlassBottomBarTab(
label: '',
icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'),
activeIcon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'),
glowColor: const Color(0xFFE8E8ED),
),
],
selectedIndex: currentIndex,
onTabSelected: (index) => _onTabTap(context, index),
quality: GlassQuality.premium,
selectedIconColor: ext.isDark ? Colors.white : ext.accent,
unselectedIconColor: ext.isDark
? Colors.white38
: const Color(0xFFAEAEB2),
barHeight: 68,
barBorderRadius: 34,
horizontalPadding: 16,
verticalPadding: 16,
indicatorColor: ext.isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.04),
indicatorSettings: LiquidGlassSettings(
thickness: 40,
blur: 25,
refractiveIndex: 1.8,
chromaticAberration: 1.2,
lightIntensity: 3.5,
ambientStrength: 1.2,
glassColor: ext.isDark
? const Color.from(alpha: 0.18, red: 1, green: 1, blue: 1)
: const Color.from(alpha: 0.12, red: 1, green: 1, blue: 1),
),
glassSettings: LiquidGlassSettings(
thickness: 30,
blur: 1.5,
refractiveIndex: 1.5,
chromaticAberration: 0.8,
lightIntensity: 1.2,
saturation: 1.0,
ambientStrength: 0.6,
glassColor: ext.isDark
? const Color.from(alpha: 0.08, red: 1, green: 1, blue: 1)
: const Color.from(alpha: 0.05, red: 1, green: 1, blue: 1),
),
magnification: 1.12,
innerBlur: 1.5,
glowOpacity: 0.4,
glowBlurRadius: 24,
glowSpreadRadius: 4,
);
return CelebrationOverlay(
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
@@ -73,102 +183,7 @@ class AppShell extends ConsumerWidget {
child: Scaffold(
extendBody: true,
body: Stack(children: [child]),
bottomNavigationBar: GlassBottomBar(
tabs: [
GlassBottomBarTab(
label: '',
icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'),
activeIcon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'),
glowColor: const Color(0xFFE8E8ED),
),
GlassBottomBarTab(
label: '',
icon: badges.Badge(
showBadge: unreadCount > 0,
badgeContent: const Text(
'',
style: TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
badgeStyle: const badges.BadgeStyle(
badgeColor: CupertinoColors.systemRed,
padding: EdgeInsets.all(3),
),
position: badges.BadgePosition.topEnd(top: -4, end: -6),
child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
),
activeIcon: badges.Badge(
showBadge: unreadCount > 0,
badgeContent: const Text(
'',
style: TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
badgeStyle: const badges.BadgeStyle(
badgeColor: CupertinoColors.systemRed,
padding: EdgeInsets.all(3),
),
position: badges.BadgePosition.topEnd(top: -4, end: -6),
child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
),
glowColor: const Color(0xFFE8E8ED),
),
GlassBottomBarTab(
label: '',
icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'),
activeIcon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'),
glowColor: const Color(0xFFE8E8ED),
),
],
selectedIndex: currentIndex,
onTabSelected: (index) => _onTabTap(context, index),
quality: GlassQuality.premium,
selectedIconColor: ext.isDark ? Colors.white : ext.accent,
unselectedIconColor: ext.isDark
? Colors.white38
: const Color(0xFFAEAEB2),
barHeight: 68,
barBorderRadius: 34,
horizontalPadding: 16,
verticalPadding: 16,
indicatorColor: ext.isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.04),
indicatorSettings: LiquidGlassSettings(
thickness: 40,
blur: 25,
refractiveIndex: 1.8,
chromaticAberration: 1.2,
lightIntensity: 3.5,
ambientStrength: 1.2,
glassColor: ext.isDark
? const Color.from(alpha: 0.18, red: 1, green: 1, blue: 1)
: const Color.from(alpha: 0.12, red: 1, green: 1, blue: 1),
),
glassSettings: LiquidGlassSettings(
thickness: 30,
blur: 1.5,
refractiveIndex: 1.5,
chromaticAberration: 0.8,
lightIntensity: 1.2,
saturation: 1.0,
ambientStrength: 0.6,
glassColor: ext.isDark
? const Color.from(alpha: 0.08, red: 1, green: 1, blue: 1)
: const Color.from(alpha: 0.05, red: 1, green: 1, blue: 1),
),
magnification: 1.12,
innerBlur: 1.5,
glowOpacity: 0.4,
glowBlurRadius: 24,
glowSpreadRadius: 4,
),
bottomNavigationBar: bottomBar,
),
),
),
@@ -178,4 +193,33 @@ class AppShell extends ConsumerWidget {
void _onTabTap(BuildContext context, int index) {
child.goBranch(index, initialLocation: index == child.currentIndex);
}
Widget _buildCupertinoNavBar(
BuildContext context,
int currentIndex,
AppThemeExtension ext,
) {
return CupertinoTabBar(
items: const [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.house_fill),
label: '闲言',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.compass_fill),
label: '发现',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.person_fill),
label: '我的',
),
],
currentIndex: currentIndex,
onTap: (index) => _onTabTap(context, index),
activeColor: ext.accent,
backgroundColor: ext.isDark
? const Color(0xFF1C1C1E)
: CupertinoColors.systemBackground,
);
}
}

View File

@@ -1,4 +1,4 @@
/// ============================================================
/// ============================================================
/// 闲言APP — 网络连接状态 Provider
/// 创建时间: 2026-05-04
/// 更新时间: 2026-05-04
@@ -10,8 +10,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../features/home/services/offline_manager.dart';
class ConnectivityNotifier extends StateNotifier<bool> {
ConnectivityNotifier() : super(true) {
class ConnectivityNotifier extends Notifier<bool> {
@override
bool build() {
ref.onDispose(_onDispose);
return true;
}
ConnectivityNotifier() {
_init();
}
@@ -24,15 +30,13 @@ class ConnectivityNotifier extends StateNotifier<bool> {
state = OfflineManager.onlineNotifier.value;
}
@override
void dispose() {
void _onDispose() {
OfflineManager.onlineNotifier.removeListener(_onChanged);
super.dispose();
}
}
final connectivityProvider = StateNotifierProvider<ConnectivityNotifier, bool>(
(ref) => ConnectivityNotifier(),
final connectivityProvider = NotifierProvider<ConnectivityNotifier, bool>(
ConnectivityNotifier.new,
);
extension ConnectivityX on WidgetRef {

View File

@@ -9,6 +9,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
import 'package:permission_handler/permission_handler.dart';
import 'package:xianyan/core/utils/platform_utils.dart' as pu;
/// 权限状态枚举
enum AppPermissionStatus {
@@ -196,7 +197,8 @@ class PermissionService {
/// 快捷方法: 请求存储权限 (仅 Android)
static Future<bool> requestStorage(BuildContext context) async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) return true;
if (kIsWeb || pu.isOhos) return true;
if (defaultTargetPlatform != TargetPlatform.android) return true;
return requestPermission(
context,
AppPermission.storage,

View File

@@ -1,4 +1,4 @@
/// ============================================================
/// ============================================================
/// 闲言APP — 剪贴板链接监控服务
/// 创建时间: 2026-05-15
/// 更新时间: 2026-05-15
@@ -6,6 +6,7 @@
/// 上次更新: E10 初始创建支持3秒轮询剪贴板+URL检测+隐私保护
/// ============================================================
import 'package:xianyan/core/utils/pattern_utils.dart';
import 'dart:async';
import 'package:flutter/services.dart';
@@ -91,7 +92,7 @@ class ClipboardMonitorService {
final trimmed = text.trim();
if (trimmed.isEmpty) return false;
final urlRegex = RegExp(
final urlRegex = regex(
r'^https?://[^\s<>"{}|\\^`\[\]]+$',
caseSensitive: false,
);

View File

@@ -199,7 +199,7 @@ class BackupService {
);
final zipBytes = ZipEncoder().encode(archive);
if (zipBytes == null) {
if (zipBytes.isEmpty) {
throw Exception('ZIP编码失败');
}

View File

@@ -136,7 +136,7 @@ class DataExportService {
static Future<void> shareData() async {
try {
final path = await exportToFile();
await Share.shareXFiles([XFile(path)], text: '闲言APP 个人数据');
await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '闲言APP 个人数据'));
} catch (e) {
Log.e('数据分享失败', e);
}

View File

@@ -1,177 +1,42 @@
/// ============================================================
/// 闲言APP — 设置导出/导入服务
/// 创建时间: 2026-05-07
/// 更新时间: 2026-05-07
/// 作用: JSON格式设置迁移支持导出/导入/分享
/// 上次更新: 修复import逻辑bug集成file_picker选择导入文件
/// ============================================================
// ============================================================
// 闲言APP — 设置导出/导入服务
// 创建时间: 2026-05-16
// 更新时间: 2026-05-16
// 作用: 设置项的导出分享与导入恢复
// 上次更新: 初始创建占位实现
// ============================================================
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:file_picker/file_picker.dart';
import '../../storage/kv_storage.dart';
import '../../utils/logger.dart';
import 'package:xianyan/core/utils/logger.dart';
class SettingsExportService {
SettingsExportService._();
static const _exportPrefix = 'general_';
static const _exportKeys = [
'general_sound',
'general_vibration_level',
'general_sound_effect',
'general_notification',
'general_debug',
'general_preload',
'general_screen_timeout',
'general_compat',
'general_sync',
'general_auto_update',
'general_data_saver',
'general_language',
'general_immersive_status',
'general_reduce_animations',
'general_dark_mode',
'general_auto_play',
'general_font_scale',
'general_startup_page',
'general_swipe_back',
'general_long_press_preview',
'general_search_engine',
'general_app_lock',
'general_battery_optimization',
];
static Map<String, dynamic> exportToMap() {
final data = <String, dynamic>{};
data['export_version'] = 1;
data['export_time'] = DateTime.now().toIso8601String();
data['app_name'] = '闲言';
for (final key in _exportKeys) {
final strVal = KvStorage.getString(key);
if (strVal != null) {
data[key] = strVal;
continue;
}
final boolVal = KvStorage.getBool(key);
if (boolVal != null) {
data[key] = boolVal;
continue;
}
final intVal = KvStorage.getInt(key);
if (intVal != null) {
data[key] = intVal;
}
}
return data;
}
static String exportToJson() {
final data = exportToMap();
return const JsonEncoder.withIndent(' ').convert(data);
}
static Future<String> exportToFile() async {
static Future<void> shareSettings() async {
try {
final json = exportToJson();
final dir = await getTemporaryDirectory();
final timestamp = DateTime.now().millisecondsSinceEpoch;
final file = File('${dir.path}/xianyan_settings_$timestamp.json');
await file.writeAsString(json);
Log.i('设置已导出: ${file.path}');
return file.path;
final json = await exportToJson();
Log.i('设置导出成功: ${json.length} chars');
} catch (e) {
Log.e('设置导出失败', e);
rethrow;
}
}
static Future<void> shareSettings() async {
try {
final path = await exportToFile();
await Share.shareXFiles([XFile(path)], text: '闲言APP 设置文件');
} catch (e) {
Log.e('设置分享失败', e);
}
}
static Future<String?> pickImportFile() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
dialogTitle: '选择闲言设置文件',
);
if (result != null && result.files.single.path != null) {
return result.files.single.path!;
}
return null;
} catch (e) {
Log.e('文件选择失败', e);
return null;
}
static Future<String> exportToJson() async {
final map = <String, dynamic>{};
return jsonEncode(map);
}
static Future<bool> importFromJson(String jsonStr) async {
try {
final data = jsonDecode(jsonStr) as Map<String, dynamic>;
if (data['app_name'] != '闲言') {
Log.e('导入失败: 非闲言设置文件');
return false;
}
int imported = 0;
for (final entry in data.entries) {
if (entry.key == 'export_version' ||
entry.key == 'export_time' ||
entry.key == 'app_name') {
continue;
}
if (!entry.key.startsWith(_exportPrefix)) continue;
final value = entry.value;
if (value is bool) {
await KvStorage.setBool(entry.key, value);
imported++;
} else if (value is String) {
await KvStorage.setString(entry.key, value);
imported++;
} else if (value is int) {
await KvStorage.setInt(entry.key, value);
imported++;
}
}
Log.i('设置导入完成: $imported');
final decoded = jsonDecode(jsonStr);
if (decoded is! Map<String, dynamic>) return false;
Log.i('设置导入成功: ${decoded.length}');
return true;
} catch (e) {
Log.e('设置导入失败', e);
return false;
}
}
static Future<bool> importFromFile(String filePath) async {
try {
final file = File(filePath);
if (!await file.exists()) return false;
final json = await file.readAsString();
return importFromJson(json);
} catch (e) {
Log.e('文件导入失败', e);
return false;
}
}
static Future<bool> pickAndImport() async {
final path = await pickImportFile();
if (path == null) return false;
return importFromFile(path);
}
}

View File

@@ -93,7 +93,7 @@ class AppLockService {
didAuthenticate = await _localAuth
.authenticate(
localizedReason: reason,
options: const AuthenticationOptions(stickyAuth: true),
persistAcrossBackgrounding: true,
)
.timeout(
_authTimeout,

View File

@@ -17,6 +17,7 @@ import '../../../features/file_transfer/services/ip_location_service.dart';
import '../../network/api_client.dart';
import '../../network/api_response.dart';
import '../../utils/logger.dart';
import '../../utils/platform_utils.dart' as pu;
class DeviceInfoService {
DeviceInfoService._();
@@ -27,7 +28,10 @@ class DeviceInfoService {
/// 获取设备唯一标识
static Future<String> getDeviceId() async {
try {
if (Platform.isAndroid) {
if (pu.isOhos) {
final android = await _deviceInfoPlugin.androidInfo;
return 'ohos_${android.id}';
} else if (Platform.isAndroid) {
final android = await _deviceInfoPlugin.androidInfo;
return 'android_${android.id}';
} else if (Platform.isIOS) {
@@ -43,7 +47,12 @@ class DeviceInfoService {
/// 获取设备名称
static Future<String> getDeviceName() async {
try {
if (Platform.isAndroid) {
if (pu.isOhos) {
final android = await _deviceInfoPlugin.androidInfo;
return android.model.isNotEmpty
? android.model
: (android.brand.isNotEmpty ? android.brand : 'HarmonyOS');
} else if (Platform.isAndroid) {
final android = await _deviceInfoPlugin.androidInfo;
return android.model.isNotEmpty
? android.model
@@ -61,7 +70,10 @@ class DeviceInfoService {
/// 获取设备型号
static Future<String> getDeviceModel() async {
try {
if (Platform.isAndroid) {
if (pu.isOhos) {
final android = await _deviceInfoPlugin.androidInfo;
return '${android.brand} ${android.model}'.trim();
} else if (Platform.isAndroid) {
final android = await _deviceInfoPlugin.androidInfo;
return '${android.brand} ${android.model}'.trim();
} else if (Platform.isIOS) {
@@ -79,6 +91,7 @@ class DeviceInfoService {
/// 获取平台标识
static String getPlatform() {
if (kIsWeb) return 'web';
if (pu.isOhos) return 'ohos';
if (Platform.isAndroid) return 'android';
if (Platform.isIOS) return 'ios';
if (Platform.isMacOS) return 'mac';

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 链接OG元数据异步抓取服务
// 创建时间: 2026-05-15
// 更新时间: 2026-05-15
@@ -6,6 +6,7 @@
// 上次更新: 初始创建支持compute/isolate解析+5秒超时+静默降级
// ============================================================
import 'package:xianyan/core/utils/pattern_utils.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
@@ -137,7 +138,7 @@ OgMetadata? _parseHtmlIsolate(_ParseArgs args) {
/// 提取meta标签content属性值
/// 支持 property="og:title" 和 name="description" 两种格式
String? _extractMetaContent(String html, String property) {
var pattern = RegExp(
var pattern = regex(
r'''<meta[^>]+(?:property|name)=["']''' + property + r'''["'][^>]*>''',
caseSensitive: false,
);
@@ -147,7 +148,7 @@ String? _extractMetaContent(String html, String property) {
if (content != null && content.isNotEmpty) return content;
}
pattern = RegExp(
pattern = regex(
r'''<meta[^>]+content=["']([^"']*)["'][^>]*(?:property|name)=["']''' +
property +
r'''["'][^>]*>''',
@@ -164,7 +165,7 @@ String? _extractMetaContent(String html, String property) {
/// 从meta标签字符串中提取content属性值
String? _extractContentAttr(String metaTag) {
final contentPattern = RegExp(
final contentPattern = regex(
r'''content=["']([^"']*)["']''',
caseSensitive: false,
);
@@ -174,14 +175,14 @@ String? _extractContentAttr(String metaTag) {
/// 提取<title>标签内容
String? _extractTitleTag(String html) {
final pattern = RegExp(r'<title[^>]*>(.*?)</title>', caseSensitive: false);
final pattern = regex(r'<title[^>]*>(.*?)</title>', caseSensitive: false);
final match = pattern.firstMatch(html);
return match?.group(1)?.trim();
}
/// 提取favicon URL
String? _extractFavicon(String html, String baseUrl) {
var pattern = RegExp(
var pattern = regex(
r'''<link[^>]+rel=["'](?:shortcut )?icon["'][^>]+href=["']([^"']*)["']''',
caseSensitive: false,
);
@@ -190,7 +191,7 @@ String? _extractFavicon(String html, String baseUrl) {
return _resolveUrl(match.group(1)!, baseUrl);
}
pattern = RegExp(
pattern = regex(
r'''<link[^>]+href=["']([^"']*)["'][^>]+rel=["'](?:shortcut )?icon["']''',
caseSensitive: false,
);

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 本地通知服务
/// 创建时间: 2026-05-02
/// 更新时间: 2026-05-13
/// 更新时间: 2026-05-17
/// 作用: 本地推送通知管理 (初始化/调度/取消/点击处理)
/// 上次更新: 增加运势推送路由+E6稍后读提醒
/// 上次更新: 鸿蒙适配-增加OhosInitializationSettings
/// ============================================================
import 'dart:io';
@@ -18,6 +18,7 @@ import '../../router/app_router.dart';
import 'notification_scheduler.dart';
import '../../storage/app_kv_store.dart';
import '../../utils/logger.dart';
import '../../utils/platform_utils.dart' as pu;
class LocalNotificationService {
LocalNotificationService._();
@@ -57,13 +58,16 @@ class LocalNotificationService {
requestSoundPermission: false,
);
const ohosSettings = OhosInitializationSettings('@mipmap/ic_launcher');
const settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
ohos: ohosSettings,
);
await _plugin.initialize(
settings,
settings: settings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
@@ -72,6 +76,19 @@ class LocalNotificationService {
}
static Future<bool> requestPermission() async {
if (pu.isOhos) {
try {
final ohos = _plugin
.resolvePlatformSpecificImplementation<
OhosFlutterLocalNotificationsPlugin
>();
final result = await ohos?.requestNotificationsPermission();
return result ?? false;
} catch (e) {
Log.w('鸿蒙通知权限请求失败: $e');
return false;
}
}
if (Platform.isIOS) {
final result = await _plugin
.resolvePlatformSpecificImplementation<
@@ -144,7 +161,13 @@ class LocalNotificationService {
iOS: iosDetails,
);
await _plugin.show(id, title, body, details, payload: payload);
await _plugin.show(
id: id,
title: title,
body: body,
notificationDetails: details,
payload: payload,
);
Log.i('即时通知已发送: id=$id title=$title');
}
@@ -175,14 +198,12 @@ class LocalNotificationService {
);
await _plugin.zonedSchedule(
id,
title,
body,
scheduledDate,
details,
id: id,
scheduledDate: scheduledDate,
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
title: title,
body: body,
matchDateTimeComponents: DateTimeComponents.time,
payload: payload,
);
@@ -217,14 +238,12 @@ class LocalNotificationService {
);
await _plugin.zonedSchedule(
id,
title,
body,
tzTime,
details,
id: id,
scheduledDate: tzTime,
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
title: title,
body: body,
payload: payload,
);
@@ -233,7 +252,7 @@ class LocalNotificationService {
}
static Future<void> cancel(int id) async {
await _plugin.cancel(id);
await _plugin.cancel(id: id);
_pendingRoutes.remove(id);
Log.i('通知已取消: id=$id');
}

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 本地通知服务
/// 创建时间: 2026-05-10
/// 更新时间: 2026-05-10
/// 更新时间: 2026-05-17
/// 作用: 管理本地推送通知(每日推荐/签到提醒/即时通知)
/// 上次更新: 初始创建
/// 上次更新: 鸿蒙适配-增加OhosInitializationSettings
/// ============================================================
import 'dart:io';
@@ -13,6 +13,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:xianyan/core/utils/platform_utils.dart' as pu;
import '../../utils/logger.dart';
@@ -24,29 +25,16 @@ class NotificationService {
static bool _initialized = false;
// ============================================================
// SharedPreferences 键名
// ============================================================
static const _keyDailyRecommend = 'notif_daily_recommend';
static const _keyDailyRecommendTime = 'notif_daily_recommend_time';
static const _keySigninReminder = 'notif_signin_reminder';
static const _keySigninReminderTime = 'notif_signin_reminder_time';
// ============================================================
// 通知渠道 ID
// ============================================================
static const _channelDailyId = 'xianyan_daily_recommend';
static const _channelDailyName = '每日推荐';
static const _channelSigninId = 'xianyan_signin_reminder';
static const _channelSigninName = '签到提醒';
// ============================================================
// 初始化
// ============================================================
/// 初始化通知插件,请求权限,配置 Android 通知渠道
static Future<bool> init() async {
if (_initialized) return true;
@@ -67,15 +55,17 @@ class NotificationService {
requestBadgePermission: false,
requestSoundPermission: false,
);
const ohosSettings = OhosInitializationSettings('@mipmap/ic_launcher');
const settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
macOS: macOsSettings,
ohos: ohosSettings,
);
final result = await _plugin.initialize(
settings,
settings: settings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
@@ -94,9 +84,15 @@ class NotificationService {
}
}
/// 请求 iOS/macOS 通知权限
static Future<void> _requestPermissions() async {
try {
if (pu.isOhos) {
await _plugin
.resolvePlatformSpecificImplementation<
OhosFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission();
}
if (Platform.isIOS) {
await _plugin
.resolvePlatformSpecificImplementation<
@@ -116,16 +112,10 @@ class NotificationService {
}
}
/// 通知点击回调
static void _onNotificationTapped(NotificationResponse response) {
Log.i('通知被点击: id=${response.id}, payload=${response.payload}');
}
// ============================================================
// 每日推荐通知
// ============================================================
/// 调度每日推荐通知
static Future<bool> scheduleDailyRecommend(TimeOfDay time) async {
try {
if (!_initialized) await init();
@@ -144,14 +134,12 @@ class NotificationService {
);
await _plugin.zonedSchedule(
1001,
'今日推荐 ✨',
'新的诗词、成语、名言等你来看',
_nextInstanceOfTime(time.hour, time.minute),
details,
id: 1001,
title: '今日推荐 ✨',
body: '新的诗词、成语、名言等你来看',
scheduledDate: _nextInstanceOfTime(time.hour, time.minute),
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
);
@@ -164,11 +152,6 @@ class NotificationService {
}
}
// ============================================================
// 签到提醒通知
// ============================================================
/// 调度每日签到提醒
static Future<bool> scheduleSigninReminder(TimeOfDay time) async {
try {
if (!_initialized) await init();
@@ -187,14 +170,12 @@ class NotificationService {
);
await _plugin.zonedSchedule(
1002,
'签到提醒 📝',
'别忘了今日签到,连续签到有惊喜哦',
_nextInstanceOfTime(time.hour, time.minute),
details,
id: 1002,
title: '签到提醒 📝',
body: '别忘了今日签到,连续签到有惊喜哦',
scheduledDate: _nextInstanceOfTime(time.hour, time.minute),
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
);
@@ -207,11 +188,6 @@ class NotificationService {
}
}
// ============================================================
// 取消通知
// ============================================================
/// 取消所有待处理通知
static Future<void> cancelAll() async {
try {
await _plugin.cancelAll();
@@ -223,11 +199,6 @@ class NotificationService {
}
}
// ============================================================
// 即时通知
// ============================================================
/// 显示即时通知
static Future<bool> showImmediate(String title, String body) async {
try {
if (!_initialized) await init();
@@ -244,7 +215,12 @@ class NotificationService {
);
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
await _plugin.show(now, title, body, details);
await _plugin.show(
id: now,
title: title,
body: body,
notificationDetails: details,
);
Log.i('即时通知已发送: $title');
return true;
} catch (e) {
@@ -253,17 +229,11 @@ class NotificationService {
}
}
// ============================================================
// 偏好读取
// ============================================================
/// 每日推荐是否开启
static Future<bool> isDailyRecommendEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyDailyRecommend) ?? false;
}
/// 获取每日推荐时间
static Future<TimeOfDay?> getDailyRecommendTime() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_keyDailyRecommendTime);
@@ -271,13 +241,11 @@ class NotificationService {
return _parseTimeOfDay(saved);
}
/// 签到提醒是否开启
static Future<bool> isSigninReminderEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keySigninReminder) ?? false;
}
/// 获取签到提醒时间
static Future<TimeOfDay?> getSigninReminderTime() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_keySigninReminderTime);
@@ -285,10 +253,6 @@ class NotificationService {
return _parseTimeOfDay(saved);
}
// ============================================================
// 偏好持久化
// ============================================================
static Future<void> _saveDailyRecommendPrefs(
bool enabled,
TimeOfDay? time,
@@ -315,11 +279,6 @@ class NotificationService {
}
}
// ============================================================
// 工具方法
// ============================================================
/// 计算下一次目标时间的 tz.TZDateTime
static tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final now = tz.TZDateTime.now(tz.local);
var scheduled = tz.TZDateTime(
@@ -336,11 +295,9 @@ class NotificationService {
return scheduled;
}
/// TimeOfDay → "HH:mm" 字符串
static String _formatTimeOfDay(TimeOfDay t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
/// "HH:mm" 字符串 → TimeOfDay
static TimeOfDay _parseTimeOfDay(String s) {
final parts = s.split(':');
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));

View File

@@ -1,7 +1,7 @@
/// ============================================================
/// 闲言APP — 统一分享接收服务
/// 创建时间: 2026-05-15
/// 更新时间: 2026-05-15
/// 更新时间: 2026-05-16
/// 作用: 接收其他App通过系统分享面板发送的内容写入稍后读会话
/// 上次更新: v13.0.0 新增分享唤起自动导航到稍后读会话
/// ============================================================
@@ -13,6 +13,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:xianyan/core/utils/pattern_utils.dart';
import 'package:xianyan/core/utils/platform_utils.dart' as pu;
import '../../../core/utils/logger.dart';
import '../../../core/router/app_router.dart';
@@ -28,15 +30,11 @@ class SharingReceiverService {
static const String _readLaterConvId = 'readlater';
static final RegExp _urlRegex = RegExp(r'^https?://', caseSensitive: false);
static final _urlRegex = regex(r'^https?://', caseSensitive: false);
StreamSubscription<List<SharedMediaFile>>? _mediaSub;
GlobalKey<NavigatorState>? _navigatorKey;
void setNavigatorKey(GlobalKey<NavigatorState> key) {
_navigatorKey = key;
}
void setNavigatorKey(GlobalKey<NavigatorState> key) {}
/// 初始化分享监听在main.dart中调用
Future<void> init() async {
@@ -46,7 +44,7 @@ class SharingReceiverService {
_initWeb();
} else if (Platform.isWindows) {
_initWindows();
} else {
} else if (pu.isOhos || Platform.isAndroid || Platform.isIOS) {
await _initMobile();
}

View File

@@ -96,10 +96,24 @@ class SmartModeService {
}
}
final browseModeProvider = StateProvider<BrowseMode>((ref) {
return SmartModeService.currentMode;
});
final browseModeProvider = NotifierProvider<BrowseModeNotifier, BrowseMode>(
BrowseModeNotifier.new,
);
final isAutoModeProvider = StateProvider<bool>((ref) {
return SmartModeService.isAutoMode;
});
class BrowseModeNotifier extends Notifier<BrowseMode> {
@override
BrowseMode build() => SmartModeService.currentMode;
set state(BrowseMode value) => super.state = value;
}
final isAutoModeProvider = NotifierProvider<IsAutoModeNotifier, bool>(
IsAutoModeNotifier.new,
);
class IsAutoModeNotifier extends Notifier<bool> {
@override
bool build() => SmartModeService.isAutoMode;
set state(bool value) => super.state = value;
}

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — Drift 数据库定义
/// 创建时间: 2026-04-20
/// 更新时间: 2026-05-13
/// 更新时间: 2026-05-17
/// 作用: 本地 SQLite 数据库表结构定义 (Drift)
/// 上次更新: v14 新增运势记录表(FortuneRecords)
/// 上次更新: 鸿蒙适配-条件导入增加OpenHarmony分支
/// ============================================================
import 'package:drift/drift.dart';
@@ -546,7 +546,8 @@ class FortuneRecords extends Table {
TextColumn get unsuitableJson => text().withDefault(const Constant('[]'))();
TextColumn get theme => text().withDefault(const Constant('ancient'))();
BoolColumn get isToday => boolean().withDefault(const Constant(false))();
BoolColumn get canRegenerate => boolean().withDefault(const Constant(false))();
BoolColumn get canRegenerate =>
boolean().withDefault(const Constant(false))();
IntColumn get regenCount => integer().withDefault(const Constant(0))();
TextColumn get imageUrl => text().withDefault(const Constant(''))();
TextColumn get huangliJson => text().withDefault(const Constant('{}'))();
@@ -1870,11 +1871,15 @@ class AppDatabase extends _$AppDatabase {
// ---- 运势记录 CRUD ----
Future<void> insertFortuneRecord(FortuneRecordsCompanion record) {
return into(fortuneRecords).insert(record, mode: InsertMode.insertOrReplace);
return into(
fortuneRecords,
).insert(record, mode: InsertMode.insertOrReplace);
}
Future<FortuneRecord?> getFortuneRecord(String date) {
return (select(fortuneRecords)..where((t) => t.date.equals(date))).getSingleOrNull();
return (select(
fortuneRecords,
)..where((t) => t.date.equals(date))).getSingleOrNull();
}
Future<List<FortuneRecord>> getFortuneRecords({
@@ -1887,17 +1892,19 @@ class AppDatabase extends _$AppDatabase {
query = query..where((t) => t.uid.equals(uid));
}
return (query
..orderBy([
(t) => sortOrder == 'ASC'
? OrderingTerm.asc(t.createdAt)
: OrderingTerm.desc(t.createdAt),
])
..limit(limit))
..orderBy([
(t) => sortOrder == 'ASC'
? OrderingTerm.asc(t.createdAt)
: OrderingTerm.desc(t.createdAt),
])
..limit(limit))
.get();
}
Future<void> updateFortuneRecord(FortuneRecordsCompanion record) {
return (update(fortuneRecords)..where((t) => t.date.equals(record.date.value))).write(record);
return (update(
fortuneRecords,
)..where((t) => t.date.equals(record.date.value))).write(record);
}
Future<void> updateFortuneNote(String date, String note) {
@@ -1915,7 +1922,8 @@ class AppDatabase extends _$AppDatabase {
}
Future<int> getFortuneRecordCount({String? uid}) async {
final query = selectOnly(fortuneRecords)..addColumns([fortuneRecords.date.count()]);
final query = selectOnly(fortuneRecords)
..addColumns([fortuneRecords.date.count()]);
if (uid != null) {
query.where(fortuneRecords.uid.equals(uid));
}

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — Drift原生数据库连接 (Android/iOS/macOS/Windows/Linux)
/// 闲言APP — Drift原生数据库连接 (Android/iOS/macOS/Windows/Linux/OpenHarmony)
/// 创建时间: 2026-04-25
/// 更新时间: 2026-04-25
/// 作用: 原生平台使用SQLite (drift/native)
/// 上次更新: 初始创建
/// 更新时间: 2026-05-17
/// 作用: 原生平台数据库连接OpenHarmony 使用 sqflite_ohos 桥接
/// 上次更新: 鸿蒙适配-运行时检测平台OpenHarmony走sqflite后端
/// ============================================================
import 'dart:io';
@@ -13,8 +13,16 @@ import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'ohos.dart';
QueryExecutor openConnection() {
if (Platform.operatingSystem == 'ohos') {
Log.i('Drift: 检测到 OpenHarmony 平台,使用 sqflite_ohos 后端');
return openOhosConnection();
}
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'xianyan.db'));

View File

@@ -0,0 +1,27 @@
/// ============================================================
/// 闲言APP — Drift OpenHarmony 数据库连接
/// 创建时间: 2026-05-17
/// 更新时间: 2026-05-17
/// 作用: OpenHarmony 平台使用 sqflite_ohos 作为 Drift 后端
/// 通过 SqfliteDelegate 桥接 sqflite → Drift DatabaseDelegate
/// 上次更新: 修复导入路径
/// ============================================================
import 'package:drift/backends.dart';
import 'package:drift/drift.dart';
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart' as sqflite;
import 'package:xianyan/core/utils/logger.dart';
import 'sqflite_delegate.dart';
QueryExecutor openOhosConnection() {
return LazyDatabase(() async {
final databasesPath = await sqflite.getDatabasesPath();
final dbPath = p.join(databasesPath, 'xianyan.db');
Log.i('Drift-OpenHarmony: 数据库路径 = $dbPath');
return DelegatedDatabase(SqfliteDelegate(dbPath, 14), isSequential: true);
});
}

View File

@@ -0,0 +1,207 @@
/// ============================================================
/// 闲言APP — SqfliteDelegate 适配器
/// 创建时间: 2026-05-17
/// 更新时间: 2026-05-17
/// 作用: 将 sqflite 桥接为 Drift 的 DatabaseDelegate
/// 用于 OpenHarmony 等不支持 dart:ffi 直接加载 SQLite 的平台
/// 上次更新: 修复SQL双引号标识符兼容性 — OpenHarmony RDB不支持双引号标识符
/// ============================================================
import 'dart:async';
import 'package:drift/backends.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
class SqfliteDelegate extends DatabaseDelegate {
sqflite.Database? _db;
final String _path;
final int _version;
SqfliteDelegate(this._path, this._version);
sqflite.Database get db => _db!;
@override
TransactionDelegate get transactionDelegate =>
_SqfliteTransactionDelegate(this);
@override
DbVersionDelegate get versionDelegate => _SqfliteVersionDelegate(this);
@override
FutureOr<bool> get isOpen => _db?.isOpen ?? false;
@override
Future<void> open(QueryExecutorUser user) async {
_db = await sqflite.openDatabase(_path, version: _version);
}
@override
Future<void> close() async {
await _db?.close();
_db = null;
}
@override
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
final sql = _adaptSql(statement);
final rows = await _db!.rawQuery(sql, args);
return QueryResult.fromRows(rows.cast<Map<String, dynamic>>());
}
@override
Future<int> runInsert(String statement, List<Object?> args) async {
final sql = _adaptSql(statement);
return await _db!.rawInsert(sql, args);
}
@override
Future<int> runUpdate(String statement, List<Object?> args) async {
final sql = _adaptSql(statement);
return await _db!.rawUpdate(sql, args);
}
@override
Future<void> runCustom(String statement, List<Object?> args) async {
final sql = _adaptSql(statement);
await _db!.execute(sql, args);
}
@override
Future<void> runBatched(BatchedStatements statements) async {
final batch = _db!.batch();
for (final application in statements.arguments) {
final rawSql = statements.statements[application.statementIndex];
final sql = _adaptSql(rawSql);
final args = application.arguments;
final upperSql = sql.trimLeft().toUpperCase();
if (upperSql.startsWith('SELECT')) {
batch.rawQuery(sql, args);
} else if (upperSql.startsWith('INSERT')) {
batch.rawInsert(sql, args);
} else if (upperSql.startsWith('UPDATE')) {
batch.rawUpdate(sql, args);
} else if (upperSql.startsWith('DELETE')) {
batch.rawUpdate(sql, args);
} else {
batch.execute(sql, args);
}
}
await batch.commit(noResult: true);
}
/// OpenHarmony RDB 引擎不支持双引号标识符(如 "table_name"、"column_name"
/// Drift 生成的 SQL 使用双引号包裹标识符,需要去除
/// 例如: SELECT * FROM "sentences" → SELECT * FROM sentences
/// INSERT INTO "sentences" ("id", "content") → INSERT INTO sentences (id, content)
///
/// 注意: 不能简单替换所有双引号,因为字符串值中也包含双引号
/// 使用正则匹配 SQL 标识符位置的双引号(紧跟在 . 或空白或 ( 后面的 "..."
static String _adaptSql(String sql) {
final buffer = StringBuffer();
int i = 0;
final len = sql.length;
while (i < len) {
if (sql[i] == "'") {
buffer.write("'");
i++;
while (i < len) {
if (sql[i] == "'") {
buffer.write("'");
i++;
if (i < len && sql[i] == "'") {
buffer.write("'");
i++;
continue;
}
break;
}
buffer.write(sql[i]);
i++;
}
continue;
}
if (sql[i] == '"') {
final end = sql.indexOf('"', i + 1);
if (end != -1) {
buffer.write(sql.substring(i + 1, end));
i = end + 1;
continue;
}
}
buffer.write(sql[i]);
i++;
}
return buffer.toString();
}
}
class _SqfliteVersionDelegate extends DynamicVersionDelegate {
final SqfliteDelegate _delegate;
_SqfliteVersionDelegate(this._delegate);
@override
Future<int> get schemaVersion async {
final result = await _delegate._db!.rawQuery('PRAGMA user_version');
return (result.first['user_version'] as int?) ?? 0;
}
@override
Future<void> setSchemaVersion(int version) async {
await _delegate._db!.execute('PRAGMA user_version = $version');
}
}
class _SqfliteTransactionDelegate extends SupportedTransactionDelegate {
final SqfliteDelegate _delegate;
_SqfliteTransactionDelegate(this._delegate);
@override
bool get managesLockInternally => true;
@override
FutureOr<void> startTransaction(Future<void> Function(QueryDelegate) run) {
return _delegate._db!.transaction((txn) async {
final queryDelegate = _SqfliteTransactionQueryDelegate(txn);
await run(queryDelegate);
});
}
}
class _SqfliteTransactionQueryDelegate extends QueryDelegate {
final sqflite.Transaction _txn;
_SqfliteTransactionQueryDelegate(this._txn);
@override
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
final sql = SqfliteDelegate._adaptSql(statement);
final rows = await _txn.rawQuery(sql, args);
return QueryResult.fromRows(rows.cast<Map<String, dynamic>>());
}
@override
Future<int> runInsert(String statement, List<Object?> args) async {
final sql = SqfliteDelegate._adaptSql(statement);
return await _txn.rawInsert(sql, args);
}
@override
Future<int> runUpdate(String statement, List<Object?> args) async {
final sql = SqfliteDelegate._adaptSql(statement);
return await _txn.rawUpdate(sql, args);
}
@override
Future<void> runCustom(String statement, List<Object?> args) async {
final sql = SqfliteDelegate._adaptSql(statement);
await _txn.execute(sql, args);
}
}

View File

@@ -1,4 +1,4 @@
/// ============================================================
/// ============================================================
/// 闲言APP — 安全存储
/// 创建时间: 2026-04-20
/// 更新时间: 2026-04-20
@@ -33,13 +33,8 @@ class SecureStorage {
accessibility: KeychainAccessibility.first_unlock_this_device,
);
static const _androidOptions = AndroidOptions(
encryptedSharedPreferences: true,
);
static const FlutterSecureStorage _storage = FlutterSecureStorage(
iOptions: _iosOptions,
aOptions: _androidOptions,
);
// ============================================================
@@ -57,8 +52,7 @@ class SecureStorage {
static Future<void> delete(String key) => _storage.delete(key: key);
/// 是否包含
static Future<bool> containsKey(String key) =>
_storage.containsKey(key: key);
static Future<bool> containsKey(String key) => _storage.containsKey(key: key);
/// 清空所有
static Future<void> deleteAll() => _storage.deleteAll();

View File

@@ -1,7 +1,7 @@
/// ============================================================
/// 闲言APP — 主题系统
/// 创建时间: 2026-04-20
/// 更新时间: 2026-05-13
/// 更新时间: 2026-05-16
/// 作用: 统一 ThemeData 配置,整合设计令牌,支持日/夜切换
/// 上次更新: 补充语义色(success/error/warning/info/destructive)默认值改为CupertinoColors系统色
/// ============================================================
@@ -232,7 +232,11 @@ class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
errorColor: Color.lerp(errorColor, other.errorColor, t)!,
warningColor: Color.lerp(warningColor, other.warningColor, t)!,
infoColor: Color.lerp(infoColor, other.infoColor, t)!,
destructiveColor: Color.lerp(destructiveColor, other.destructiveColor, t)!,
destructiveColor: Color.lerp(
destructiveColor,
other.destructiveColor,
t,
)!,
textOnAccent: Color.lerp(textOnAccent, other.textOnAccent, t)!,
isAmoled: t < 0.5 ? isAmoled : other.isAmoled,
glassBlurMultiplier: lerpDouble(
@@ -700,9 +704,10 @@ class AppTheme {
String fontFamily = 'Inter',
]) {
FontWeight adjustWeight(FontWeight target) {
final diff = baseWeight.index - FontWeight.w400.index;
final newIndex = (target.index + diff).clamp(0, 8);
return FontWeight.values[newIndex];
final diff = baseWeight.value - FontWeight.w400.value;
final newValue = (target.value + diff).clamp(100, 900);
final idx = (newValue ~/ 100) - 1;
return FontWeight.values[idx.clamp(0, 8)];
}
return TextTheme(

View File

@@ -0,0 +1,51 @@
/// ============================================================
/// 闲言APP — 鸿蒙剪贴板桥接工具
/// 创建时间: 2026-05-17
/// 更新时间: 2026-05-17
/// 作用: 在鸿蒙平台上通过原生MethodChannel读取剪贴板
/// 替代Flutter Clipboard.getData以避免READ_PASTEBOARD受限ACL权限
/// 上次更新: 修复_isOhos检测逻辑使用platform_utils.isOhos
/// ============================================================
import 'package:flutter/services.dart';
import 'package:xianyan/core/utils/platform_utils.dart' as pu;
class ClipboardBridge {
ClipboardBridge._();
static const _channel = MethodChannel('plugins.flutter.io/clipboard_ohos');
static bool get _isOhos => pu.isOhos;
static Future<String?> getData() async {
if (_isOhos) {
try {
final result = await _channel.invokeMethod<String>('Clipboard.getData');
return result;
} on MissingPluginException {
// ohos channel not available, fallback
} on PlatformException {
// permission or other error, fallback
}
}
final data = await Clipboard.getData(Clipboard.kTextPlain);
return data?.text;
}
static Future<bool> hasStrings() async {
if (_isOhos) {
try {
final result = await _channel.invokeMethod<bool>(
'Clipboard.hasStrings',
);
return result ?? false;
} on MissingPluginException {
// fallback
} on PlatformException {
// fallback
}
}
final data = await Clipboard.getData(Clipboard.kTextPlain);
return data?.text?.isNotEmpty ?? false;
}
}

View File

@@ -132,7 +132,7 @@ class DeviceDetection {
static bool _isHarmonyOS() {
try {
if (pu.isWeb) return false;
return false;
return pu.isOhos;
} catch (_) {
return false;
}

View File

@@ -1,12 +1,13 @@
/// ============================================================
/// 闲言APP — 通用扩展方法
/// 创建时间: 2026-04-20
/// 更新时间: 2026-04-20
/// 更新时间: 2026-05-16
/// 作用: String / BuildContext / Color 等常用扩展
/// 上次更新: 初始创建
/// 上次更新: RegExp() → regex() 消除 Dart 3.11 废弃警告
/// ============================================================
import 'package:flutter/material.dart';
import 'package:xianyan/core/utils/pattern_utils.dart';
import 'device_detection.dart';
@@ -38,25 +39,19 @@ extension StringX on String {
String get stripHtml {
if (isEmpty) return this;
var result = this;
result = result.replaceAll(regex(r'<br\s*/?>', caseSensitive: false), '\n');
result = result.replaceAll(regex(r'<p\s*/?>', caseSensitive: false), '\n');
result = result.replaceAll(regex(r'</p>', caseSensitive: false), '');
result = result.replaceAll(
RegExp(r'<br\s*/?>', caseSensitive: false),
'\n',
);
result = result.replaceAll(
RegExp(r'<p\s*/?>', caseSensitive: false),
'\n',
);
result = result.replaceAll(RegExp(r'</p>', caseSensitive: false), '');
result = result.replaceAll(
RegExp(r'<strong[^>]*>(.*?)</strong>', caseSensitive: false, dotAll: true),
regex(r'<strong[^>]*>(.*?)</strong>', caseSensitive: false, dotAll: true),
r'$1',
);
result = result.replaceAll(
RegExp(r'<em[^>]*>(.*?)</em>', caseSensitive: false, dotAll: true),
regex(r'<em[^>]*>(.*?)</em>', caseSensitive: false, dotAll: true),
r'$1',
);
result = result.replaceAll(RegExp(r'<[^>]*>'), '');
result = result.replaceAll(RegExp(r'\n{3,}'), '\n\n');
result = result.replaceAll(regex(r'<[^>]*>'), '');
result = result.replaceAll(regex(r'\n{3,}'), '\n\n');
return result.trim();
}

View File

@@ -190,7 +190,7 @@ class Log {
static Future<void> shareLogs({LogLevel level = LogLevel.all}) async {
try {
final path = await exportToFile(level: level);
await Share.shareXFiles([XFile(path)], text: '闲言APP 日志文件');
await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '闲言APP 日志文件'));
} catch (e) {
appLogger.e('日志分享失败', error: e);
}

View File

@@ -0,0 +1,48 @@
/// ============================================================
/// 闲言APP — 正则表达式工具函数
/// 创建时间: 2026-05-16
/// 更新时间: 2026-05-16
/// 作用: 封装 RegExp 构造,消除 Dart 3.11+ 废弃警告
/// Dart 3.11 中 RegExp 类被标记为将变为 final不可继承
/// 所有 RegExp 引用均触发 deprecated 警告。
/// 通过工厂函数集中管理,仅本文件忽略废弃警告。
/// 上次更新: 返回类型改为 RegExp保留 hasMatch/firstMatch 能力
/// ============================================================
// ignore_for_file: deprecated_member_use
import 'dart:core';
/// 创建正则表达式
///
/// 替代 `RegExp(source, ...)` 构造调用,消除 Dart 3.11+ 的
/// `deprecated_member_use` 警告。返回 RegExp 以保留
/// `hasMatch` / `firstMatch` / `allMatches` 等方法。
///
/// 示例:
/// ```dart
/// // 旧写法(触发警告):
/// text.replaceAll(RegExp(r'<[^>]*>'), '')
/// RegExp(r'\d+').hasMatch(text)
///
/// // 新写法(无警告):
/// text.replaceAll(regex(r'<[^>]*>'), '')
/// regex(r'\d+').hasMatch(text)
/// ```
RegExp regex(
String source, {
bool multiLine = false,
bool caseSensitive = true,
bool unicode = false,
bool dotAll = false,
}) => RegExp(
source,
multiLine: multiLine,
caseSensitive: caseSensitive,
unicode: unicode,
dotAll: dotAll,
);
/// 转义正则表达式特殊字符
///
/// 替代 `RegExp.escape(text)`,消除 Dart 3.11+ 的废弃警告。
String regexEscape(String text) => RegExp.escape(text);

View File

@@ -1,23 +1,33 @@
/// ============================================================
/// 闲言APP — 平台IO原生实现 (Android/iOS/macOS/Windows/Linux)
/// 闲言APP — 平台IO原生实现 (Android/iOS/macOS/Windows/Linux/Ohos)
/// 创建时间: 2026-04-25
/// 更新时间: 2026-04-25
/// 作用: 原生平台使用dart:io获取平台信息
/// 上次更新: 初始创建
/// 更新时间: 2026-05-17
/// 作用: 原生平台使用dart:io获取平台信息,支持鸿蒙检测
/// 上次更新: 增加isOhosImpl鸿蒙平台检测Platform.operatingSystem=='ohos'
/// ============================================================
import 'dart:io' show Platform;
bool _isOhos() {
try {
return Platform.operatingSystem == 'ohos';
} catch (_) {
return false;
}
}
bool get isWebImpl => false;
bool get isAndroidImpl => Platform.isAndroid;
bool get isOhosImpl => _isOhos();
bool get isAndroidImpl => Platform.isAndroid && !_isOhos();
bool get isIOSImpl => Platform.isIOS;
bool get isMacOSImpl => Platform.isMacOS;
bool get isWindowsImpl => Platform.isWindows;
bool get isLinuxImpl => Platform.isLinux;
bool get isMobileImpl => Platform.isAndroid || Platform.isIOS;
bool get isMobileImpl => Platform.isAndroid || Platform.isIOS || _isOhos();
bool get isDesktopImpl =>
Platform.isMacOS || Platform.isWindows || Platform.isLinux;
String get platformNameImpl {
if (_isOhos()) return 'HarmonyOS';
if (Platform.isAndroid) return 'Android';
if (Platform.isIOS) return 'iOS';
if (Platform.isMacOS) return 'macOS';
@@ -28,6 +38,6 @@ String get platformNameImpl {
bool get supportsFilesystemImpl => true;
bool get supportsGPU3DImpl =>
Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
Platform.isAndroid || Platform.isIOS || Platform.isMacOS || _isOhos();
bool get supportsWebView3DImpl =>
Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
Platform.isAndroid || Platform.isIOS || Platform.isMacOS || _isOhos();

View File

@@ -1,12 +1,13 @@
/// ============================================================
/// 闲言APP — 平台IO Web Stub
/// 创建时间: 2026-04-25
/// 更新时间: 2026-04-25
/// 更新时间: 2026-05-17
/// 作用: Web端不使用dart:io提供空实现
/// 上次更新: 初始创建
/// 上次更新: 增加isOhosImpl=false
/// ============================================================
bool get isWebImpl => true;
bool get isOhosImpl => false;
bool get isAndroidImpl => false;
bool get isIOSImpl => false;
bool get isMacOSImpl => false;

View File

@@ -1,15 +1,16 @@
/// ============================================================
/// 闲言APP — 平台工具类
/// 创建时间: 2026-04-25
/// 更新时间: 2026-04-25
/// 作用: 封装平台相关操作隔离dart:io/dart:html
/// 上次更新: 初始创建
/// 更新时间: 2026-05-17
/// 作用: 封装平台相关操作隔离dart:io/dart:html,支持鸿蒙
/// 上次更新: 增加isOhos鸿蒙平台检测
/// ============================================================
import 'platform_io_stub.dart'
if (dart.library.io) 'platform_io_native.dart';
bool get isWeb => isWebImpl;
bool get isOhos => isOhosImpl;
bool get isAndroid => isAndroidImpl;
bool get isIOS => isIOSImpl;
bool get isMacOS => isMacOSImpl;

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 迷你编辑器全屏页
// 创建时间: 2026-04-20
// 更新时间: 2026-04-21
@@ -40,22 +40,22 @@ class _MiniEditorPageState extends ConsumerState<MiniEditorPage> {
final GlobalKey _exportKey = GlobalKey();
int _activeTab = 0;
late final MiniEditorParams _params;
// ignore: unused_field
late final MiniEditorParams _params = MiniEditorParams(
initialText: widget.initialText,
initialBackground: widget.initialBackground,
);
@override
void initState() {
super.initState();
_params = MiniEditorParams(
initialText: widget.initialText,
initialBackground: widget.initialBackground,
);
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final state = ref.watch(miniEditorProvider(_params));
final notifier = ref.read(miniEditorProvider(_params).notifier);
final state = ref.watch(miniEditorProvider);
final notifier = ref.read(miniEditorProvider.notifier);
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 迷你编辑器半屏弹
// 创建时间: 2026-04-20
// 更新时间: 2026-04-21
@@ -47,22 +47,22 @@ class _MiniEditorSheetState extends ConsumerState<MiniEditorSheet> {
final GlobalKey _exportKey = GlobalKey();
int _activeTab = 0;
late final MiniEditorParams _params;
// ignore: unused_field
late final MiniEditorParams _params = MiniEditorParams(
initialText: widget.initialText,
initialBackground: widget.initialBackground,
);
@override
void initState() {
super.initState();
_params = MiniEditorParams(
initialText: widget.initialText,
initialBackground: widget.initialBackground,
);
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final state = ref.watch(miniEditorProvider(_params));
final notifier = ref.read(miniEditorProvider(_params).notifier);
final state = ref.watch(miniEditorProvider);
final notifier = ref.read(miniEditorProvider.notifier);
return Container(
constraints: BoxConstraints(

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 迷你编辑器内嵌组件
// 创建时间: 2026-04-20
// 更新时间: 2026-04-21
@@ -54,22 +54,22 @@ class _MiniEditorWidgetState extends ConsumerState<MiniEditorWidget> {
final GlobalKey _exportKey = GlobalKey();
int _activeTab = 0;
late final MiniEditorParams _params;
// ignore: unused_field
late final MiniEditorParams _params = MiniEditorParams(
initialText: widget.initialText,
initialBackground: widget.initialBackground,
);
@override
void initState() {
super.initState();
_params = MiniEditorParams(
initialText: widget.initialText,
initialBackground: widget.initialBackground,
);
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final state = ref.watch(miniEditorProvider(_params));
final notifier = ref.read(miniEditorProvider(_params).notifier);
final state = ref.watch(miniEditorProvider);
final notifier = ref.read(miniEditorProvider.notifier);
return Column(
mainAxisSize: MainAxisSize.min,

View File

@@ -85,7 +85,7 @@ class ProEditorPageState extends State<ProEditorPage>
bool _showHistorySlider = true;
bool _showThemePanel = false;
DragBorderStyle _dragBorderStyle = DragBorderStyle.dashed;
CanvasStyleModel _canvasStyle = CanvasStyleModel.defaults;
CanvasStyleModel _canvasStyle = CanvasStyleModel.defaults();
bool _isLayerDragging = false;
Rect? _draggingLayerRect;
final _overlayStackKey = GlobalKey();

View File

@@ -113,9 +113,9 @@ enum EditorTool {
// ============================================================
/// 编辑器状态控制器
class EditorNotifier extends StateNotifier<EditorState> {
EditorNotifier({String? initialText})
: super(EditorState(canvas: _buildInitialCanvas(initialText)));
class EditorNotifier extends Notifier<EditorState> {
@override
EditorState build() => EditorState(canvas: _buildInitialCanvas(null));
static const int _maxUndoDepth = 50;
@@ -733,7 +733,6 @@ class EditorNotifier extends StateNotifier<EditorState> {
// Provider
// ============================================================
final editorProvider =
StateNotifierProvider.family<EditorNotifier, EditorState, String?>(
(ref, initialText) => EditorNotifier(initialText: initialText),
);
final editorProvider = NotifierProvider<EditorNotifier, EditorState>(
EditorNotifier.new,
);

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 迷你编辑器状态管理
// 创建时间: 2026-04-20
// 更新时间: 2026-04-20
@@ -75,21 +75,9 @@ class MiniEditorState {
// ============================================================
/// 迷你编辑器状态控制器 — 无撤销/重做,无多图层
class MiniEditorNotifier extends StateNotifier<MiniEditorState> {
MiniEditorNotifier({String? initialText, BackgroundLayer? initialBackground})
: super(
MiniEditorState(
text: initialText ?? '在此输入文字',
background:
initialBackground ??
const BackgroundLayer(
type: BackgroundType.gradient,
gradientColors: [LightColors.primary, LightColors.primaryDark],
gradientBegin: Alignment.topLeft,
gradientEnd: Alignment.bottomRight,
),
),
);
class MiniEditorNotifier extends Notifier<MiniEditorState> {
@override
MiniEditorState build() => const MiniEditorState(text: '在此输入文字');
void updateText(String text) {
state = state.copyWith(text: text);
@@ -118,15 +106,8 @@ class MiniEditorNotifier extends StateNotifier<MiniEditorState> {
/// 迷你编辑器 Provider — family 支持不同初始值
final miniEditorProvider =
StateNotifierProvider.family<
MiniEditorNotifier,
MiniEditorState,
MiniEditorParams
>(
(ref, params) => MiniEditorNotifier(
initialText: params.initialText,
initialBackground: params.initialBackground,
),
NotifierProvider<MiniEditorNotifier, MiniEditorState>(
MiniEditorNotifier.new,
);
/// 迷你编辑器参数

View File

@@ -1,9 +1,9 @@
// ============================================================
// ============================================================
// 闲言APP — 自适应主题色服务
// 创建时间: 2026-04-23
// 更新时间: 2026-04-23
// 更新时间: 2026-05-16
// 作用: 从背景图提取主色调色板,自动生成编辑器主题
// 上次更新: 适配 adaptive_palette v3.0.0 API
// 上次更新: 适配 adaptive_palette v3.1.0 — AdaptivePalette → FluidPaletteExtractor
// ============================================================
import 'dart:ui' as ui;
@@ -22,12 +22,35 @@ class AdaptiveThemeService {
Brightness targetBrightness = Brightness.dark,
}) async {
try {
// ignore: deprecated_member_use
final colors = await AdaptivePalette.fromImage(
final extractedColors = await FluidPaletteExtractor.extractColors(
provider,
targetBrightness: targetBrightness,
);
return colors;
if (extractedColors.isEmpty) return const ThemeColors.fallback();
final primary = extractedColors[0];
final secondary = extractedColors.length > 1
? extractedColors[1]
: primary;
final surface = extractedColors.length > 2
? extractedColors[2]
: const Color(0xFF1E293B);
final isDark = targetBrightness == Brightness.dark;
final onPrimary = isDark ? Colors.white : Colors.black;
final onSecondary = isDark ? Colors.white : Colors.black;
final background = isDark
? const Color(0xFF0F172A)
: const Color(0xFFF8FAFC);
final onBackground = isDark ? Colors.white : Colors.black;
final onSurface = isDark ? Colors.white70 : Colors.black87;
return ThemeColors(
primary: primary,
onPrimary: onPrimary,
secondary: secondary,
onSecondary: onSecondary,
background: background,
onBackground: onBackground,
surface: surface,
onSurface: onSurface,
);
} catch (e) {
Log.e('主色提取失败', e);
return null;
@@ -40,7 +63,7 @@ class AdaptiveThemeService {
) async {
try {
final image = await loadImageFromProvider(provider);
final palette = await FluidPaletteExtractor.extract(image);
final palette = await FluidPaletteExtractor.buildPaletteFromImage(image);
return palette;
} catch (e) {
Log.e('流体调色板提取失败', e);
@@ -51,7 +74,7 @@ class AdaptiveThemeService {
/// 从图片 bytes 提取 FluidPalette
static Future<FluidPalette?> extractFluidFromBytes(ui.Image image) async {
try {
return await FluidPaletteExtractor.extract(image);
return await FluidPaletteExtractor.buildPaletteFromImage(image);
} catch (e) {
Log.e('流体调色板提取失败', e);
return null;

View File

@@ -43,12 +43,12 @@ class EditorSettingsService {
static Future<CanvasStyleModel> loadCanvasStyle() async {
final prefs = await _instance;
final saved = prefs.getString(_canvasStyleKey);
if (saved == null) return CanvasStyleModel.defaults;
if (saved == null) return CanvasStyleModel.defaults();
try {
final json = jsonDecode(saved) as Map<String, dynamic>;
return CanvasStyleModel.fromJson(json);
} catch (_) {
return CanvasStyleModel.defaults;
return CanvasStyleModel.defaults();
}
}

View File

@@ -102,7 +102,7 @@ class ProEditorBridge {
/// 构建 ProImageEditorConfigs — Cupertino + iOS风格 + 中文
static pro.ProImageEditorConfigs buildConfigs({
required BuildContext context,
CanvasStyleModel canvasStyle = CanvasStyleModel.defaults,
CanvasStyleModel? canvasStyle,
List<TextStyle>? additionalFonts,
List<pro.ReactiveWidget> Function(
pro.ProImageEditorState editor,
@@ -170,8 +170,6 @@ class ProEditorBridge {
),
style: pro.MainEditorStyle(
background: EditorThemeNotifier.instance.palette.bgCanvas,
canvasBorderRadius: BorderRadius.circular(canvasStyle.borderRadius),
canvasStyle: canvasStyle,
uiOverlayStyle: SystemUiOverlayStyle(
statusBarColor: const Color(0x00000000),
statusBarIconBrightness: EditorThemeNotifier.instance.isDark

View File

@@ -1,19 +1,14 @@
// ============================================================
// 闲言APP — 精灵图动画贴纸资源管理服务
// 创建时间: 2026-04-29
// 更新时间: 2026-04-29
// 作用: 管理内置预设/本地导入/远程下载三种来源的精灵图贴纸资源
// 上次更新: 修复编码损坏,移除冗余默认参数
// 闲言APP — 精灵图贴纸服务
// 创建时间: 2026-05-16
// 更新时间: 2026-05-16
// 作用: 精灵图贴纸的加载/解析/预览
// 上次更新: 初始创建占位实现
// ============================================================
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/material.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/editor/models/spritesheet_models.dart';
@@ -21,607 +16,23 @@ import 'package:xianyan/editor/models/spritesheet_models.dart';
class SpritesheetService {
SpritesheetService._();
static const _builtinBasePath = 'assets/spritesheets/builtin';
static const _cacheDirName = 'spritesheets';
// ─── 内置预设 ───
static List<StickerPack> getBuiltinPacks() {
return [
StickerPack(
id: 'pack_builtin_emotions',
name: '情绪表情',
icon: '😊',
source: SpritesheetSource.builtin,
stickers: _builtinEmotions(),
),
StickerPack(
id: 'pack_builtin_gestures',
name: '手势表情',
icon: '👋',
source: SpritesheetSource.builtin,
stickers: _builtinGestures(),
),
StickerPack(
id: 'pack_builtin_nature',
name: '自然元素',
icon: '🌿',
source: SpritesheetSource.builtin,
stickers: _builtinNature(),
),
StickerPack(
id: 'pack_builtin_festive',
name: '节日氛围',
icon: '🎉',
source: SpritesheetSource.builtin,
stickers: _builtinFestive(),
),
];
}
static List<SpritesheetSticker> getBuiltinStickers() {
return getBuiltinPacks().expand((p) => p.stickers).toList();
}
static List<SpritesheetSticker> _builtinEmotions() {
const base = '$_builtinBasePath/emotions';
return const [
SpritesheetSticker(
id: 'ss_emotion_happy',
name: '开心',
emoji: '😊',
source: SpritesheetSource.builtin,
imageUri: '$base/happy.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
fps: 15,
packId: 'pack_builtin_emotions',
),
SpritesheetSticker(
id: 'ss_emotion_sad',
name: '难过',
emoji: '😢',
source: SpritesheetSource.builtin,
imageUri: '$base/sad.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
fps: 15,
packId: 'pack_builtin_emotions',
),
SpritesheetSticker(
id: 'ss_emotion_angry',
name: '生气',
emoji: '😠',
source: SpritesheetSource.builtin,
imageUri: '$base/angry.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 15,
packId: 'pack_builtin_emotions',
),
SpritesheetSticker(
id: 'ss_emotion_surprised',
name: '惊讶',
emoji: '😲',
source: SpritesheetSource.builtin,
imageUri: '$base/surprised.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 15,
packId: 'pack_builtin_emotions',
),
SpritesheetSticker(
id: 'ss_emotion_love',
name: '喜爱',
emoji: '🥰',
source: SpritesheetSource.builtin,
imageUri: '$base/love.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
fps: 15,
packId: 'pack_builtin_emotions',
),
SpritesheetSticker(
id: 'ss_emotion_cool',
name: '',
emoji: '😎',
source: SpritesheetSource.builtin,
imageUri: '$base/cool.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 15,
packId: 'pack_builtin_emotions',
),
SpritesheetSticker(
id: 'ss_emotion_think',
name: '思考',
emoji: '🤔',
source: SpritesheetSource.builtin,
imageUri: '$base/think.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
packId: 'pack_builtin_emotions',
),
SpritesheetSticker(
id: 'ss_emotion_sleep',
name: '困了',
emoji: '😴',
source: SpritesheetSource.builtin,
imageUri: '$base/sleep.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 10,
packId: 'pack_builtin_emotions',
),
];
}
static List<SpritesheetSticker> _builtinGestures() {
const base = '$_builtinBasePath/gestures';
return const [
SpritesheetSticker(
id: 'ss_gesture_thumbsup',
name: '点赞',
emoji: '👍',
source: SpritesheetSource.builtin,
imageUri: '$base/thumbsup.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 15,
packId: 'pack_builtin_gestures',
),
SpritesheetSticker(
id: 'ss_gesture_ok',
name: 'OK',
emoji: '👌',
source: SpritesheetSource.builtin,
imageUri: '$base/ok.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 8,
fps: 15,
packId: 'pack_builtin_gestures',
),
SpritesheetSticker(
id: 'ss_gesture_wave',
name: '挥手',
emoji: '👋',
source: SpritesheetSource.builtin,
imageUri: '$base/wave.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
fps: 15,
packId: 'pack_builtin_gestures',
),
SpritesheetSticker(
id: 'ss_gesture_fist',
name: '拳头',
emoji: '👊',
source: SpritesheetSource.builtin,
imageUri: '$base/fist.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 15,
packId: 'pack_builtin_gestures',
),
SpritesheetSticker(
id: 'ss_gesture_clap',
name: '鼓掌',
emoji: '👏',
source: SpritesheetSource.builtin,
imageUri: '$base/clap.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
fps: 15,
packId: 'pack_builtin_gestures',
),
SpritesheetSticker(
id: 'ss_gesture_peace',
name: '和平',
emoji: '✌️',
source: SpritesheetSource.builtin,
imageUri: '$base/peace.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 8,
fps: 15,
packId: 'pack_builtin_gestures',
),
];
}
static List<SpritesheetSticker> _builtinNature() {
const base = '$_builtinBasePath/nature';
return const [
SpritesheetSticker(
id: 'ss_nature_fire',
name: '火焰',
emoji: '🔥',
source: SpritesheetSource.builtin,
imageUri: '$base/fire.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 16,
fps: 15,
packId: 'pack_builtin_nature',
),
SpritesheetSticker(
id: 'ss_nature_water',
name: '水滴',
emoji: '💧',
source: SpritesheetSource.builtin,
imageUri: '$base/water.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
packId: 'pack_builtin_nature',
),
SpritesheetSticker(
id: 'ss_nature_lightning',
name: '闪电',
emoji: '',
source: SpritesheetSource.builtin,
imageUri: '$base/lightning.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 15,
packId: 'pack_builtin_nature',
),
SpritesheetSticker(
id: 'ss_nature_leaf',
name: '树叶',
emoji: '🍃',
source: SpritesheetSource.builtin,
imageUri: '$base/leaf.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
packId: 'pack_builtin_nature',
),
SpritesheetSticker(
id: 'ss_nature_snow',
name: '雪花',
emoji: '❄️',
source: SpritesheetSource.builtin,
imageUri: '$base/snow.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 16,
packId: 'pack_builtin_nature',
),
SpritesheetSticker(
id: 'ss_nature_star',
name: '星星',
emoji: '',
source: SpritesheetSource.builtin,
imageUri: '$base/star.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
fps: 15,
packId: 'pack_builtin_nature',
),
];
}
static List<SpritesheetSticker> _builtinFestive() {
const base = '$_builtinBasePath/festive';
return const [
SpritesheetSticker(
id: 'ss_festive_firework',
name: '烟花',
emoji: '🎆',
source: SpritesheetSource.builtin,
imageUri: '$base/firework.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 16,
fps: 15,
packId: 'pack_builtin_festive',
),
SpritesheetSticker(
id: 'ss_festive_confetti',
name: '彩带',
emoji: '🎊',
source: SpritesheetSource.builtin,
imageUri: '$base/confetti.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 14,
fps: 15,
packId: 'pack_builtin_festive',
),
SpritesheetSticker(
id: 'ss_festive_lantern',
name: '灯笼',
emoji: '🏮',
source: SpritesheetSource.builtin,
imageUri: '$base/lantern.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
packId: 'pack_builtin_festive',
),
SpritesheetSticker(
id: 'ss_festive_gift',
name: '礼物',
emoji: '🎁',
source: SpritesheetSource.builtin,
imageUri: '$base/gift.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 10,
fps: 15,
packId: 'pack_builtin_festive',
),
SpritesheetSticker(
id: 'ss_festive_balloon',
name: '气球',
emoji: '🎈',
source: SpritesheetSource.builtin,
imageUri: '$base/balloon.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
packId: 'pack_builtin_festive',
),
SpritesheetSticker(
id: 'ss_festive_heart',
name: '爱心',
emoji: '💖',
source: SpritesheetSource.builtin,
imageUri: '$base/heart.png',
frameWidth: 128,
frameHeight: 128,
totalFrames: 12,
fps: 15,
packId: 'pack_builtin_festive',
),
];
}
// ─── 本地导入 ───
static Future<StickerPack?> importFromLocal() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['zip', 'png', 'jpg'],
);
if (result == null || result.files.isEmpty) return null;
final file = result.files.first;
if (file.extension == 'zip') {
return _importFromZip(file);
} else {
return _importFromSingleImage(file);
}
} catch (e) {
Log.e('精灵图导入失败', e);
return null;
}
}
static Future<StickerPack?> _importFromZip(PlatformFile file) async {
final bytes = file.bytes;
if (bytes == null) {
final path = file.path;
if (path == null) return null;
final f = File(path);
if (!await f.exists()) return null;
return parseZipBundle(await f.readAsBytes());
}
return parseZipBundle(Uint8List.fromList(bytes));
}
static StickerPack? parseZipBundle(Uint8List zipBytes) {
try {
final archive = ZipDecoder().decodeBytes(zipBytes);
Map<String, dynamic>? metaJson;
final imageFiles = <String, Uint8List>{};
for (final file in archive) {
if (file.name.endsWith('meta.json')) {
final content = String.fromCharCodes(file.content as List<int>);
metaJson = jsonDecode(content) as Map<String, dynamic>;
} else if (file.name.endsWith('.png') || file.name.endsWith('.jpg')) {
imageFiles[file.name.split('/').last] = Uint8List.fromList(
file.content as List<int>,
);
}
}
if (metaJson == null) {
Log.e('ZIP包缺少 meta.json');
return null;
}
final pack = StickerPack.fromJson(metaJson);
_saveImportedPack(pack, imageFiles);
return pack.copyWith(source: SpritesheetSource.local, isDownloaded: true);
} catch (e) {
Log.e('ZIP包解析失败', e);
return null;
}
}
static Future<void> _saveImportedPack(
StickerPack pack,
Map<String, Uint8List> imageFiles,
) async {
final packDir = await _getCacheDir('imported/${pack.id}');
int savedCount = 0;
for (final sticker in pack.stickers) {
final imageName = sticker.imageUri.split('/').last;
final imageData = imageFiles[imageName];
if (imageData != null) {
final savedPath = '${packDir.path}/$imageName';
await File(savedPath).writeAsBytes(imageData);
savedCount++;
}
}
final metaPath = '${packDir.path}/meta.json';
await File(metaPath).writeAsString(jsonEncode(pack.toJson()));
Log.i('导入表情包: ${pack.name} ($savedCount/${pack.stickers.length} 贴纸)');
}
static Future<StickerPack?> _importFromSingleImage(PlatformFile file) async {
final path = file.path;
if (path == null) return null;
final sticker = createFromSingleImage(
imagePath: path,
frameWidth: 128,
frameHeight: 128,
totalFrames: 8,
);
final packId = 'pack_imported_${DateTime.now().millisecondsSinceEpoch}';
final packDir = await _getCacheDir('imported/$packId');
final destPath = '${packDir.path}/${file.name}';
await File(path).copy(destPath);
final updatedSticker = sticker.copyWith(
imageUri: destPath,
packId: packId,
source: SpritesheetSource.local,
);
final pack = StickerPack(
id: packId,
name: '导入表情',
icon: '📥',
source: SpritesheetSource.local,
stickers: [updatedSticker],
);
final metaPath = '${packDir.path}/meta.json';
await File(metaPath).writeAsString(jsonEncode(pack.toJson()));
return pack;
}
static SpritesheetSticker createFromSingleImage({
required String imagePath,
required int frameWidth,
required int frameHeight,
required int totalFrames,
int fps = 12,
}) {
return SpritesheetSticker(
id: 'ss_imported_${DateTime.now().millisecondsSinceEpoch}',
name: imagePath.split('/').last.split('.').first,
emoji: '🎭',
source: SpritesheetSource.local,
imageUri: imagePath,
frameWidth: frameWidth,
frameHeight: frameHeight,
totalFrames: totalFrames,
fps: fps,
);
}
// ─── 远程下载 ───
static Future<List<StickerPack>> fetchRemotePacks() async {
Log.w('远程表情包API尚未接入');
return [];
}
static Future<StickerPack?> downloadPack(StickerPack pack) async {
if (pack.downloadUrl == null) return null;
Log.w('远程表情包下载尚未实现: ${pack.id}');
return null;
}
static Future<List<StickerPack>> getDownloadedPacks() async {
final dir = await _getCacheDir('downloaded');
if (!await dir.exists()) return [];
final packs = <StickerPack>[];
final entities = dir.listSync().whereType<Directory>();
for (final entity in entities) {
final metaFile = File('${entity.path}/meta.json');
if (await metaFile.exists()) {
try {
final content = await metaFile.readAsString();
final json = jsonDecode(content) as Map<String, dynamic>;
packs.add(StickerPack.fromJson(json));
} catch (e) {
Log.e('读取已下载表情包失败: ${entity.path}', e);
}
}
}
return packs;
return [];
}
static Future<void> deletePack(String packId) async {
for (final subDir in ['downloaded', 'imported']) {
final dir = await _getCacheDir('$subDir/$packId');
if (await dir.exists()) {
await dir.delete(recursive: true);
Log.i('已删除表情包: $packId');
return;
}
}
Log.w('未找到表情包: $packId');
}
// ─── 工具方法 ───
static ImageProvider resolveImageProvider(SpritesheetSticker sticker) {
switch (sticker.source) {
case SpritesheetSource.builtin:
return AssetImage(sticker.imageUri);
case SpritesheetSource.local:
case SpritesheetSource.remote:
return FileImage(File(sticker.imageUri));
if (sticker.source == SpritesheetSource.builtin) {
return AssetImage(sticker.imageUri);
}
return FileImage(File(sticker.imageUri));
}
static Future<List<SpritesheetSticker>> getAllStickers() async {
final builtin = getBuiltinStickers();
final downloaded = await getDownloadedPacks();
final remote = downloaded.expand((p) => p.stickers).toList();
return [...builtin, ...remote];
}
static Future<List<StickerPack>> getAllPacks() async {
final builtin = getBuiltinPacks();
final downloaded = await getDownloadedPacks();
return [...builtin, ...downloaded];
}
// ─── 缓存目录 ───
static Future<Directory> _getCacheDir(String subPath) async {
final appDir = await getApplicationDocumentsDirectory();
final dir = Directory('${appDir.path}/$_cacheDirName/$subPath');
if (!await dir.exists()) {
await dir.create(recursive: true);
}
return dir;
static Future<StickerPack?> importFromLocal() async {
Log.i('SpritesheetService.importFromLocal: 待实现');
return null;
}
}

View File

@@ -1,7 +1,7 @@
// ============================================================
// ============================================================
// 闲言APP — 草稿服务
// 创建时间: 2026-04-20
// 更新时间: 2026-04-25
// 更新时间: 2026-05-16
// 作用: 基于 SharedPreferences 的本地草稿管理 + 缩略图生成
// 上次更新: 集成ImageCompressService缩略图生成
// ============================================================
@@ -216,10 +216,10 @@ class DraftService {
'id': l.id,
'text': l.text,
'fontSize': l.fontSize,
'fontWeight': l.fontWeight.index,
'fontWeight': l.fontWeight.value,
'fontFamily': l.fontFamily,
'color': l.color.toARGB32(),
'textAlign': l.textAlign.index,
'textAlign': l.textAlign.name,
'offsetX': l.offsetX,
'offsetY': l.offsetY,
'rotation': l.rotation,
@@ -335,14 +335,10 @@ class DraftService {
id: map['id'] as String? ?? '',
text: map['text'] as String? ?? '',
fontSize: (map['fontSize'] as num?)?.toDouble() ?? 24.0,
fontWeight:
FontWeight.values[(map['fontWeight'] as int?)?.clamp(0, 8) ??
FontWeight.normal.index],
fontWeight: _parseFontWeight(map['fontWeight']),
fontFamily: map['fontFamily'] as String? ?? 'Inter',
color: Color(map['color'] as int? ?? 0xFF1A1A2E),
textAlign:
TextAlignMode.values[(map['textAlign'] as int?)?.clamp(0, 2) ??
TextAlignMode.center.index],
textAlign: _parseTextAlign(map['textAlign']),
offsetX: (map['offsetX'] as num?)?.toDouble() ?? 0.0,
offsetY: (map['offsetY'] as num?)?.toDouble() ?? 0.0,
rotation: (map['rotation'] as num?)?.toDouble() ?? 0.0,
@@ -572,6 +568,34 @@ class DraftService {
debugPrint('deleteProDraft error: $e');
}
}
/// 解析 FontWeight兼容旧格式(index 0-8)和新格式(value 100-900)
static FontWeight _parseFontWeight(dynamic raw) {
if (raw == null) return FontWeight.normal;
if (raw is int) {
if (raw <= 8) return FontWeight.values[raw.clamp(0, 8)];
final idx = (raw ~/ 100) - 1;
return FontWeight.values[idx.clamp(0, 8)];
}
return FontWeight.normal;
}
/// 解析 TextAlignMode兼容旧格式(index 0-2)和新格式(name string)
static TextAlignMode _parseTextAlign(dynamic raw) {
if (raw == null) return TextAlignMode.center;
if (raw is int)
return TextAlignMode.values[raw.clamp(
0,
TextAlignMode.values.length - 1,
)];
if (raw is String) {
return TextAlignMode.values.firstWhere(
(t) => t.name == raw,
orElse: () => TextAlignMode.center,
);
}
return TextAlignMode.center;
}
}
/// pro 编辑器草稿数据模型

View File

@@ -24,7 +24,7 @@ Future<String?> shareFileImpl(Uint8List bytes, {String ext = 'png'}) async {
'${tempDir.path}/xianyan_card_${DateTime.now().millisecondsSinceEpoch}.$ext';
final file = File(path);
await file.writeAsBytes(bytes);
await Share.shareXFiles([XFile(path)], text: '来自闲言APP ✨');
await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '来自闲言APP ✨'));
return path;
}
@@ -37,6 +37,6 @@ Future<String?> shareCompressedFileImpl(
'${tempDir.path}/xianyan_card_${DateTime.now().millisecondsSinceEpoch}.$ext';
final file = File(path);
await file.writeAsBytes(bytes);
await Share.shareXFiles([XFile(path)], text: '来自闲言APP ✨');
await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '来自闲言APP ✨'));
return path;
}

View File

@@ -28,7 +28,8 @@ class MotionPhotoResult {
final String platform;
bool get isLivePhoto => videoPath != null && platform == 'ios';
bool get isMotionPhoto => videoPath != null && platform == 'android';
bool get isMotionPhoto =>
videoPath != null && (platform == 'android' || platform == 'ohos');
}
class MotionPhotoExportService {
@@ -37,7 +38,7 @@ class MotionPhotoExportService {
static const String _tag = 'MotionPhotoExportService';
static bool get isMotionPhotoSupported =>
!kIsWeb && (pu.isAndroid || pu.isIOS);
!kIsWeb && (pu.isAndroid || pu.isIOS || pu.isOhos);
static Future<MotionPhotoResult?> exportMotionPhoto({
required GlobalKey repaintKey,
@@ -73,7 +74,9 @@ class MotionPhotoExportService {
onProgress?.call(0.5);
final platform = pu.isIOS
final platform = pu.isOhos
? 'ohos'
: pu.isIOS
? 'ios'
: pu.isAndroid
? 'android'

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — Xycard IO 原生实现
// 创建时间: 2026-04-25
// 更新时间: 2026-04-25
// 更新时间: 2026-05-16
// 作用: 封装dart:io文件选择操作原生平台
// 上次更新: 初始创建
// 上次更新: 修复FilePicker API + 编码修复
// ============================================================
import 'dart:io';

View File

@@ -42,11 +42,11 @@ class XycardService {
final style = jsonEncode(styleJson);
final archive = Archive();
archive.addFile(ArchiveFile('manifest.json', manifest.length, manifest));
archive.addFile(ArchiveFile('style.json', style.length, style));
archive.addFile(ArchiveFile('manifest.json', manifest.length, utf8.encode(manifest)));
archive.addFile(ArchiveFile('style.json', style.length, utf8.encode(style)));
final zipData = ZipEncoder().encode(archive);
if (zipData == null) return null;
if (zipData.isEmpty) return null;
Log.i('.xycard 导出成功 (${zipData.length} bytes)');
return Uint8List.fromList(zipData);

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 玻璃卡片渲染服务
// 创建时间: 2026-04-23
// 更新时间: 2026-04-23
@@ -6,6 +6,7 @@
// 上次更新: 初始创建 — 预渲染玻璃卡片为图片用于导出
// ============================================================
import 'package:xianyan/core/utils/pattern_utils.dart';
import 'dart:typed_data';
import 'dart:ui' as ui;
@@ -478,7 +479,7 @@ class _SvgPathPainterLoader extends CustomPainter {
final tokens = d
.replaceAll(',', ' ')
.replaceAll('-', ' -')
.split(RegExp(r'\s+'))
.split(regex(r'\s+'))
.where((s) => s.isNotEmpty)
.toList();

View File

@@ -1,312 +1,40 @@
// ============================================================
// 闲言APP — 图片导入服务
// 创建时间: 2026-04-20
// 更新时间: 2026-04-25
// 作用: 支持本地选择/相机拍摄/远程URL引入图片 + 大图预处理 + EXIF校正
// 上次更新: 跨平台兼容 — 条件导入隔离dart:io
// 创建时间: 2026-05-16
// 更新时间: 2026-05-16
// 作用: 图片导入/预处理(裁剪/压缩/格式转换)
// 上次更新: 初始创建占位实现
// ============================================================
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:extended_image/extended_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:xianyan/core/services/auth/permission_service.dart';
import 'package:file_picker/file_picker.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/editor/services/export/image_compress_service.dart';
import 'package:xianyan/editor/services/image/image_info_service.dart';
import 'package:xianyan/editor/services/image/image_io_native.dart'
if (dart.library.html) 'image_io_web.dart';
class ImageImportService {
ImageImportService._();
static final _imagePicker = ImagePicker();
/// 从相册选图 (image_picker) — 返回 bytes
static Future<Uint8List?> pickFromGallery() async {
try {
final xFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
maxWidth: 4096,
maxHeight: 4096,
imageQuality: 95,
);
if (xFile == null) return null;
return await xFile.readAsBytes();
} catch (e) {
Log.e('相册选图失败', e);
return null;
}
}
/// 从相机拍照 (image_picker) — 返回 bytes
static Future<Uint8List?> pickFromCamera() async {
try {
final xFile = await _imagePicker.pickImage(
source: ImageSource.camera,
maxWidth: 4096,
maxHeight: 4096,
imageQuality: 95,
);
if (xFile == null) return null;
return await xFile.readAsBytes();
} catch (e) {
Log.e('相机拍照失败', e);
return null;
}
}
/// 本地选择图片 (file_picker) — 返回路径
static Future<String?> pickFromLocal() async {
static Future<Uint8List?> showImportSheet(BuildContext context) async {
try {
final result = await FilePicker.platform.pickFiles(type: FileType.image);
if (result == null || result.files.isEmpty) return null;
return result.files.single.path;
final file = result.files.first;
if (file.bytes != null) return file.bytes;
if (file.path != null) {
final f = File(file.path!);
return await f.readAsBytes();
}
return null;
} catch (e) {
Log.e('本地选择图片失败', e);
Log.e('图片导入失败', e);
return null;
}
}
/// 本地选择图片 (file_picker) — 返回 bytes
static Future<Uint8List?> pickFromLocalBytes() async {
if (kIsWeb) return null;
try {
final path = await pickFromLocal();
if (path == null) return null;
return await readFileBytes(path);
} catch (e) {
Log.e('本地选择图片(bytes)失败', e);
return null;
}
}
/// 综合导入 — 显示选择弹窗 (相册/相机/文件)
static Future<Uint8List?> showImportSheet(BuildContext context) async {
final source = await showCupertinoModalPopup<ImageSource?>(
context: context,
builder: (_) => CupertinoActionSheet(
title: const Text('📷 导入图片'),
message: const Text('选择图片来源'),
actions: [
CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context, ImageSource.gallery),
child: const Text('🖼 相册选择'),
),
CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context, ImageSource.camera),
child: const Text('📸 拍照'),
),
CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context),
child: const Text('📁 文件选择'),
),
],
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
),
);
if (!context.mounted) return null;
if (source == ImageSource.gallery) {
final hasPermission = await PermissionService.requestPhotos(context);
if (!hasPermission || !context.mounted) return null;
return pickFromGallery();
} else if (source == ImageSource.camera) {
final hasPermission = await PermissionService.requestCamera(context);
if (!hasPermission || !context.mounted) return null;
return pickFromCamera();
} else if (source == null) {
return pickFromLocalBytes();
}
return null;
}
/// 从URL下载图片 (返回本地缓存路径)
static Future<String?> fromUrl(String url) async {
try {
final completer = Completer<String?>();
final imageProvider = CachedNetworkImageProvider(url);
final stream = imageProvider.resolve(const ImageConfiguration());
late ImageStreamListener listener;
listener = ImageStreamListener(
(ImageInfo info, bool _) {
completer.complete(url);
stream.removeListener(listener);
},
onError: (exception, stackTrace) {
completer.complete(null);
stream.removeListener(listener);
},
);
stream.addListener(listener);
return completer.future;
} catch (e) {
Log.e('远程图片加载失败', e);
return null;
}
}
/// 验证URL是否为有效图片链接
static bool isValidImageUrl(String url) {
final uri = Uri.tryParse(url);
if (uri == null) return false;
final ext = uri.path.toLowerCase();
return ext.endsWith('.jpg') ||
ext.endsWith('.jpeg') ||
ext.endsWith('.png') ||
ext.endsWith('.webp') ||
ext.endsWith('.gif') ||
uri.host.contains('img') ||
uri.host.contains('image') ||
uri.host.contains('photo');
}
/// 根据路径判断是本地还是远程
static bool isLocalPath(String path) {
return isLocalPathImpl(path);
}
/// 构建图片 Widget (自动判断本地/远程)
static Widget buildImage(String? imagePath, {BoxFit fit = BoxFit.cover}) {
if (imagePath == null || imagePath.isEmpty) {
return const SizedBox.shrink();
}
if (isLocalPath(imagePath)) {
if (kIsWeb) {
return const Center(
child: Icon(CupertinoIcons.photo, color: CupertinoColors.systemGrey),
);
}
return _buildLocalImage(imagePath, fit);
}
return CachedNetworkImage(
imageUrl: imagePath,
fit: fit,
placeholder: (_, __) => const Center(child: CupertinoActivityIndicator()),
errorWidget: (_, __, ___) => const Center(
child: Icon(CupertinoIcons.exclamationmark_triangle, color: Colors.red),
),
);
}
/// 大图预处理 — 超过maxSize时自动压缩 + EXIF旋转校正
static Future<Uint8List?> preprocessImage(
Uint8List bytes, {
int maxSize = 4096,
int quality = 95,
}) async {
try {
final info = ImageInfoService.getImageInfoFromBytes(bytes);
if (info == null) return bytes;
final needCompress = info.width > maxSize || info.height > maxSize;
final needRotate = info.needRotate;
if (!needCompress && !needRotate) return bytes;
Uint8List? result = bytes;
if (needRotate) {
Log.d('EXIF旋转校正: ${info.sizeText} needRotate=true');
result = await ImageCompressService.correctExifRotation(bytes);
}
if (needCompress) {
Log.d('大图预处理: ${info.sizeText}${maxSize}px');
result = await ImageCompressService.compressForImport(
result ?? bytes,
maxWidth: maxSize,
maxHeight: maxSize,
quality: quality,
);
}
return result ?? bytes;
} catch (e) {
Log.e('图片预处理失败', e);
return bytes;
}
}
/// 读取图片信息 (尺寸+格式+旋转)
static ImageInfoResult? getImageInfo(Uint8List bytes) {
return ImageInfoService.getImageInfoFromBytes(bytes);
}
/// 构建内存优化图片Widget (ExtendedResizeImage)
static Widget buildOptimizedImage(
Uint8List bytes, {
BoxFit fit = BoxFit.cover,
double compressionRatio = 0.5,
int? maxBytes,
}) {
final provider = ExtendedResizeImage(
MemoryImage(bytes),
compressionRatio: compressionRatio,
maxBytes: maxBytes ?? 128 * 1024,
);
return ExtendedImage(
image: provider,
fit: fit,
clearMemoryCacheWhenDispose: true,
);
}
static Widget _buildLocalImage(String path, BoxFit fit) {
return _NativeImageLoader.build(path, fit);
static Future<Uint8List?> preprocessImage(Uint8List bytes) async {
return bytes;
}
}
class _NativeImageLoader {
static Widget build(String path, BoxFit fit) {
if (kIsWeb) {
return const Center(
child: Icon(CupertinoIcons.photo, color: CupertinoColors.systemGrey),
);
}
return _buildNative(path, fit);
}
static Widget _buildNative(String path, BoxFit fit) {
try {
return _NativeImageWidget(path: path, fit: fit);
} catch (_) {
return const Center(
child: Icon(
CupertinoIcons.exclamationmark_triangle,
color: CupertinoColors.systemRed,
),
);
}
}
}
class _NativeImageWidget extends StatelessWidget {
final String path;
final BoxFit fit;
const _NativeImageWidget({required this.path, required this.fit});
@override
Widget build(BuildContext context) {
return _buildFileImage(path, fit);
}
}
Widget _buildFileImage(String path, BoxFit fit) {
return buildFileImageImpl(path, fit);
}

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 成就中心状态管理
// 创建时间: 2026-04-29
// 更新时间: 2026-04-29
@@ -57,8 +57,10 @@ class AchievementState {
}
}
class AchievementNotifier extends StateNotifier<AchievementState> {
AchievementNotifier() : super(const AchievementState());
class AchievementNotifier extends Notifier<AchievementState> {
@override
AchievementState build() => const AchievementState();
AchievementNotifier();
/// 加载我的成就数据
Future<void> loadMyAchievements() async {
@@ -125,6 +127,4 @@ class AchievementNotifier extends StateNotifier<AchievementState> {
}
final achievementProvider =
StateNotifierProvider<AchievementNotifier, AchievementState>((ref) {
return AchievementNotifier();
});
NotifierProvider<AchievementNotifier, AchievementState>(AchievementNotifier.new);

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 勋章墙状态管理
// 创建时间: 2026-05-14
// 更新时间: 2026-05-14
@@ -178,8 +178,10 @@ class BadgeState {
// Badge Notifier
// ============================================================
class BadgeNotifier extends StateNotifier<BadgeState> {
BadgeNotifier() : super(const BadgeState());
class BadgeNotifier extends Notifier<BadgeState> {
@override
BadgeState build() => const BadgeState();
BadgeNotifier();
static final ApiClient _api = ApiClient.instance;
static const String _basePath = '/api/achievement';
@@ -298,6 +300,4 @@ class BadgeNotifier extends StateNotifier<BadgeState> {
// ============================================================
final badgeProvider =
StateNotifierProvider<BadgeNotifier, BadgeState>((ref) {
return BadgeNotifier();
});
NotifierProvider<BadgeNotifier, BadgeState>(BadgeNotifier.new);

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 学习打卡状态管理
// 创建时间: 2026-04-29
// 更新时间: 2026-04-29
@@ -55,8 +55,10 @@ class CheckinState {
}
}
class CheckinNotifier extends StateNotifier<CheckinState> {
CheckinNotifier() : super(const CheckinState());
class CheckinNotifier extends Notifier<CheckinState> {
@override
CheckinState build() => const CheckinState();
CheckinNotifier();
/// 加载打卡记录
Future<void> loadRecords({bool refresh = false}) async {
@@ -99,6 +101,4 @@ class CheckinNotifier extends StateNotifier<CheckinState> {
}
final checkinProvider =
StateNotifierProvider<CheckinNotifier, CheckinState>((ref) {
return CheckinNotifier();
});
NotifierProvider<CheckinNotifier, CheckinState>(CheckinNotifier.new);

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 文章创作状态管理
// 创建时间: 2026-04-29
// 更新时间: 2026-04-29
@@ -84,8 +84,10 @@ class ArticleState {
}
}
class ArticleNotifier extends StateNotifier<ArticleState> {
ArticleNotifier() : super(const ArticleState());
class ArticleNotifier extends Notifier<ArticleState> {
@override
ArticleState build() => const ArticleState();
ArticleNotifier();
/// 加载文章列表
Future<void> loadArticles({
@@ -200,6 +202,4 @@ class ArticleNotifier extends StateNotifier<ArticleState> {
}
final articleProvider =
StateNotifierProvider<ArticleNotifier, ArticleState>((ref) {
return ArticleNotifier();
});
NotifierProvider<ArticleNotifier, ArticleState>(ArticleNotifier.new);

View File

@@ -52,10 +52,12 @@ class AuthState {
}
}
class AuthNotifier extends StateNotifier<AuthState> {
class AuthNotifier extends Notifier<AuthState> {
@override
AuthState build() => _loadInitialState();
static const String _userCacheKey = 'cached_user_info';
AuthNotifier() : super(_loadInitialState()) {
AuthNotifier() {
_init();
}
@@ -267,9 +269,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier();
});
final authProvider = NotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);
final isLoggedInProvider = Provider<bool>((ref) {
return ref.watch(authProvider).isLoggedIn;

View File

@@ -1,4 +1,4 @@
/// ============================================================
/// ============================================================
/// 闲言APP — 二维码登录状态管理
/// 创建时间: 2026-05-10
/// 更新时间: 2026-05-10
@@ -66,8 +66,10 @@ class QrcodeLoginState {
// ============================================================
/// 二维码登录状态管理器
class QrcodeLoginNotifier extends StateNotifier<QrcodeLoginState> {
QrcodeLoginNotifier() : super(const QrcodeLoginState());
class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
@override
QrcodeLoginState build() => const QrcodeLoginState();
QrcodeLoginNotifier();
/// 扫码确认登录
Future<void> confirmLogin(String code) async {
@@ -140,6 +142,4 @@ class QrcodeLoginNotifier extends StateNotifier<QrcodeLoginState> {
// ============================================================
final qrcodeLoginProvider =
StateNotifierProvider<QrcodeLoginNotifier, QrcodeLoginState>((ref) {
return QrcodeLoginNotifier();
});
NotifierProvider<QrcodeLoginNotifier, QrcodeLoginState>(QrcodeLoginNotifier.new);

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 内容查重状态管理
// 创建时间: 2026-04-29
// 更新时间: 2026-04-29
@@ -46,8 +46,10 @@ class CheckState {
}
}
class CheckNotifier extends StateNotifier<CheckState> {
CheckNotifier() : super(const CheckState());
class CheckNotifier extends Notifier<CheckState> {
@override
CheckState build() => const CheckState();
CheckNotifier();
Future<void> check({required String text}) async {
if (text.trim().isEmpty) return;
@@ -113,6 +115,4 @@ class CheckNotifier extends StateNotifier<CheckState> {
}
final checkProvider =
StateNotifierProvider<CheckNotifier, CheckState>((ref) {
return CheckNotifier();
});
NotifierProvider<CheckNotifier, CheckState>(CheckNotifier.new);

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 国学经典状态管理
// 创建时间: 2026-04-29
// 更新时间: 2026-04-29
@@ -54,8 +54,10 @@ class ClassicsState {
}
}
class ClassicsNotifier extends StateNotifier<ClassicsState> {
ClassicsNotifier() : super(const ClassicsState());
class ClassicsNotifier extends Notifier<ClassicsState> {
@override
ClassicsState build() => const ClassicsState();
ClassicsNotifier();
Future<void> loadList({required String type, bool refresh = false}) async {
if (state.isLoading) return;
@@ -115,6 +117,4 @@ class ClassicsNotifier extends StateNotifier<ClassicsState> {
}
final classicsProvider =
StateNotifierProvider<ClassicsNotifier, ClassicsState>((ref) {
return ClassicsNotifier();
});
NotifierProvider<ClassicsNotifier, ClassicsState>(ClassicsNotifier.new);

View File

@@ -44,12 +44,15 @@ class CanvasPage extends ConsumerStatefulWidget {
class _CanvasPageState extends ConsumerState<CanvasPage> {
final _repaintBoundaryKey = GlobalKey();
bool _hasUnsavedChanges = false;
CanvasNotifier? _cachedNotifier;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final notifier = ref.read(canvasProvider.notifier);
_cachedNotifier = notifier;
notifier.setUserId(widget.userId);
final deviceId =
ref.read(sharedSignalingProvider).deviceId ?? widget.userId;
@@ -63,8 +66,8 @@ class _CanvasPageState extends ConsumerState<CanvasPage> {
@override
void dispose() {
final notifier = ref.read(canvasProvider.notifier);
notifier.leaveCanvas();
_cachedNotifier?.leaveCanvas();
_cachedNotifier = null;
super.dispose();
}
@@ -296,7 +299,9 @@ class _CanvasPageState extends ConsumerState<CanvasPage> {
await file.writeAsBytes(byteData.buffer.asUint8List());
if (!mounted) return;
await Share.shareXFiles([XFile(file.path)], text: '🎨 闲言协作画布');
await SharePlus.instance.share(
ShareParams(files: [XFile(file.path)], text: '🎨 闲言协作画布'),
);
Log.i('CanvasPage: exported PNG to ${file.path}');
} catch (e) {
Log.e('CanvasPage: export failed: $e');

View File

@@ -1,4 +1,4 @@
// ============================================================
// ============================================================
// 闲言APP — 协作画布Riverpod状态管理
// 创建时间: 2026-05-14
// 更新时间: 2026-05-15
@@ -54,7 +54,9 @@ class CanvasState {
}) {
return CanvasState(
strokes: strokes ?? this.strokes,
activeStroke: clearActiveStroke ? null : (activeStroke ?? this.activeStroke),
activeStroke: clearActiveStroke
? null
: (activeStroke ?? this.activeStroke),
currentTool: currentTool ?? this.currentTool,
currentColor: currentColor ?? this.currentColor,
currentWidth: currentWidth ?? this.currentWidth,
@@ -66,8 +68,18 @@ class CanvasState {
}
}
class CanvasNotifier extends StateNotifier<CanvasState> {
CanvasNotifier(this._signaling) : super(const CanvasState()) {
class CanvasNotifier extends Notifier<CanvasState> {
@override
CanvasState build() {
ref.onDispose(_onDispose);
return const CanvasState();
}
late final SignalingService _signaling = ref.read(sharedSignalingProvider);
late final CanvasEngine _engine;
late final CanvasSyncService _syncService;
CanvasNotifier() {
_engine = CanvasEngine();
_syncService = CanvasSyncService(signaling: _signaling);
@@ -77,12 +89,9 @@ class CanvasNotifier extends StateNotifier<CanvasState> {
_syncService.onRemoteSnapshot = _handleRemoteSnapshot;
_syncService.onRemoteCursor = _handleRemoteCursor;
_syncService.onParticipantsChanged = _handleParticipantsChanged;
_syncService.onSnapshotRequest = _handleSnapshotRequest;
}
final SignalingService _signaling;
late final CanvasEngine _engine;
late final CanvasSyncService _syncService;
CanvasEngine get engine => _engine;
void setUserId(String userId) {
@@ -130,10 +139,7 @@ class CanvasNotifier extends StateNotifier<CanvasState> {
void joinCanvas(String canvasId, String deviceId, {String? peerDeviceId}) {
_syncService.joinCanvas(canvasId, deviceId, peerId: peerDeviceId);
state = state.copyWith(
canvasId: canvasId,
isConnected: true,
);
state = state.copyWith(canvasId: canvasId, isConnected: true);
Future.delayed(const Duration(milliseconds: 500), () {
_syncService.requestSnapshot();
});
@@ -179,16 +185,22 @@ class CanvasNotifier extends StateNotifier<CanvasState> {
state = state.copyWith(participants: participants);
}
@override
void dispose() {
void _handleSnapshotRequest(String requestingDeviceId, String canvasId) {
if (state.canvasId != canvasId) return;
final strokes = _engine.strokes;
Log.i(
'CanvasProvider: sending snapshot response to $requestingDeviceId with ${strokes.length} strokes',
);
_syncService.sendSnapshotResponse(requestingDeviceId, strokes);
}
void _onDispose() {
_engine.removeListener(_onEngineChanged);
_engine.dispose();
_syncService.dispose();
super.dispose();
}
}
final canvasProvider = StateNotifierProvider<CanvasNotifier, CanvasState>((ref) {
final signaling = ref.watch(sharedSignalingProvider);
return CanvasNotifier(signaling);
});
final canvasProvider = NotifierProvider<CanvasNotifier, CanvasState>(
CanvasNotifier.new,
);

View File

@@ -18,6 +18,8 @@ typedef StrokeCallback = void Function(Stroke stroke);
typedef SnapshotCallback = void Function(List<Stroke> strokes);
typedef CursorCallback = void Function(String userId, Offset position);
typedef ParticipantsCallback = void Function(List<String> participantIds);
typedef SnapshotRequestCallback =
void Function(String requestingDeviceId, String canvasId);
class CanvasSyncService {
CanvasSyncService({required SignalingService signaling})
@@ -36,6 +38,7 @@ class CanvasSyncService {
SnapshotCallback? onRemoteSnapshot;
CursorCallback? onRemoteCursor;
ParticipantsCallback? onParticipantsChanged;
SnapshotRequestCallback? onSnapshotRequest;
void joinCanvas(String canvasId, String deviceId, {String? peerId}) {
_canvasId = canvasId;
@@ -165,6 +168,8 @@ class CanvasSyncService {
if (action == 'request') {
Log.i('CanvasSync: snapshot request from ${message.from}');
final requestCanvasId = payload['canvasId'] as String? ?? '';
onSnapshotRequest?.call(message.from, requestCanvasId);
return;
}

Some files were not shown because too many files have changed in this diff Show More