diff --git a/CHANGELOG.md b/CHANGELOG.md index 2709cc9a..2d3cdc40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,76 @@ *** +## [v6.10.0] - 2026-06-02 + +### 🏗️ 架构优化 + 灵动岛增强 + Lint规则 + 应用图标 + +**1. MacosPlatformService 统一 🏗️:** +- 🔄 分散在多文件的 MethodChannel 统一为 `MacosPlatformService` +- 🔄 通道名 `com.xianyan.theme` → `com.xianyan.macos` +- 🔄 `MacosTitleBarService` 标记 @Deprecated,桥接到新服务 +- ✅ Swift 端扩展为 10 个方法处理器(主题/窗口/标题栏/全屏/触觉等) + +**2. 二维码 WebSocket 长连接推送 🌐:** +- ✅ 新增 `QrcodeWsService` — WebSocket 推送服务(心跳25秒/指数退避重连) +- ✅ `QrcodeLoginProvider` 重构为双通道架构(WS优先 + HTTP轮询降级) +- ✅ 新增 `qrcode_ws_relay.dart` — shelf_web_socket 中继服务器 +- ✅ PHP 后端 `_notifyWsRelay()` — cURL 通知 WS 中继 + +**3. RSS 全文阅读增强 📖:** +- ✅ `RssService.fetchFullText()` — 简易 Readability 算法 +- ✅ `RssReaderPage` 阅读模式 — 全文内容 + 图片画廊 +- ✅ `RssFullTextResult` 数据类 + +**4. 灵动岛增强 — 倒计时聚焦模式 ⏰:** +- ✅ `LiveActivityService` — 新增 `startCountdownActivity/updateCountdownActivity/endCountdownActivity` +- ✅ `CountdownNotifier` — 集成灵动岛聚焦模式(focusEvent/unfocusEvent) +- ✅ `LiveActivityProvider` — 补充 `updateCountdownActivity/endCountdownActivity` +- ✅ `CountdownPage` UI — 灵动岛聚焦横幅 + 卡片铃铛按钮 + 操作菜单 +- ✅ `_activeType` 跟踪 — 类型切换时自动结束旧活动 + +**5. 自定义 Lint 规则 📏:** +- ✅ 新增 `tools/xianyan_lint/` 自定义 lint 包 +- ✅ `double_angle_brackets` — 检测双书名号《《 +- ✅ `hardcoded_color` — 检测非主题系统硬编码颜色 +- ✅ `hardcoded_chinese` — 检测UI层硬编码中文(默认关闭) +- ✅ `analysis_options.yaml` — 新增 custom_lint 规则配置 + +**6. 应用图标和名称 🎨:** +- ✅ iOS/macOS 应用名称改为中文「闲言」 +- ✅ iOS `CFBundleDisplayName` → 闲言 +- ✅ macOS 新增 `CFBundleDisplayName` → 闲言 +- ✅ 全平台图标更新(从 `assets/templates/resized/` 复制) +- ✅ iOS 小组件图标补全 +- ✅ Web 图标更新 + +**修改文件** +- `lib/core/services/device/macos_platform_service.dart` — 新建统一服务 +- `lib/core/services/ui/macos_title_bar_service.dart` — @Deprecated桥接 +- `macos/Runner/MainFlutterWindow.swift` — 统一通道+10方法 +- `lib/app/app.dart` — 引用新服务 +- `lib/features/auth/services/qrcode_ws_service.dart` — 新建WS服务 +- `lib/features/auth/providers/qrcode_login_provider.dart` — 双通道重构 +- `docs/toolsapi/scripts/qrcode_ws_relay.dart` — 新建WS中继 +- `docs/toolsapi/application/api/controller/UserSecurity.php` — WS通知 +- `lib/features/discover/services/rss_service.dart` — 全文提取 +- `lib/features/discover/presentation/pages/tool/rss_reader_page.dart` — 阅读模式 +- `lib/core/services/device/live_activity_service.dart` — 倒计时活动 +- `lib/core/services/device/live_activity_provider.dart` — 补充方法 +- `lib/features/countdown/providers/countdown_provider.dart` — 灵动岛集成 +- `lib/features/countdown/presentation/countdown_page.dart` — 灵动岛UI +- `tools/xianyan_lint/` — 新建自定义lint包 +- `analysis_options.yaml` — lint规则配置 +- `pubspec.yaml` — 新增xianyan_lint依赖 +- `ios/Runner/Info.plist` — 中文名称 +- `macos/Runner/Info.plist` — 中文名称+CFBundleDisplayName +- `ios/Runner/Assets.xcassets/AppIcon.appiconset/` — 图标更新 +- `macos/Runner/Assets.xcassets/AppIcon.appiconset/` — 图标更新 +- `ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/` — 小组件图标 +- `web/icons/` — Web图标更新 + +*** + ## [v6.9.51] - 2026-06-01 ### 🔧 修复 MacBook Pro 端 iOS/macOS 构建报错 diff --git a/analysis_options.yaml b/analysis_options.yaml index 67dd9c51..3f565ba4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,14 +1,14 @@ # ============================================================ # 闲言APP — 代码分析配置 # 创建时间: 2026-04-20 -# 更新时间: 2026-05-29 +# 更新时间: 2026-06-02 # 作用: Dart/Flutter 静态分析规则 -# 上次更新: 新增deprecated_member_use_from_same_package配置,翻译模块@Deprecated兼容 +# 上次更新: 移除custom_lint插件(分析器崩溃),保留riverpod_lint # ============================================================ # riverpod_lint 3.x 不再提供 analysis_options.yaml 供外部 include -# 其 lint 规则通过 custom_lint 插件机制提供(见下方 plugins 配置) -# 旧版 2.x 的 include 方式已废弃,无需启用 +# 其 lint 规则通过 custom_lint 插件机制提供 +# 注意: custom_lint 0.8.x + xianyan_lint 导致分析器崩溃,已移除 analyzer: # 排除生成的代码 @@ -23,8 +23,6 @@ analyzer: - "**/*.md" - "Scripts/**" - "**/*.php" - plugins: - - custom_lint errors: # 自由化代码的警告降级 diff --git a/docs/toolsapi/application/api/controller/UserSecurity.php b/docs/toolsapi/application/api/controller/UserSecurity.php index 301f97de..d7256562 100644 --- a/docs/toolsapi/application/api/controller/UserSecurity.php +++ b/docs/toolsapi/application/api/controller/UserSecurity.php @@ -1110,6 +1110,9 @@ class UserSecurity extends Api } db('qrcode_login')->where('code', $code)->update(['status' => 'cancelled', 'updatetime' => time()]); + + $this->_notifyWsRelay($code, 'cancelled'); + $this->success('已取消'); } @@ -1220,4 +1223,42 @@ class UserSecurity extends Api 'is_online' => 1, ]); } + + /** + * @name 通知WebSocket中继服务器 + * @desc 二维码状态变更时,通过HTTP通知WebSocket中继服务器推送更新 + * @lastUpdate v10.4.0 新增 + */ + private function _notifyWsRelay($code, $status, $token = '') + { + try { + $wsRelayUrl = Config::get('qrcode_ws_relay_url') ?: 'http://127.0.0.1:9444'; + $data = json_encode([ + 'code' => $code, + 'status' => $status, + ]); + if ($token) { + $data = json_encode([ + 'code' => $code, + 'status' => $status, + 'token' => $token, + ]); + } + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $wsRelayUrl . '/notify', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $data, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 2, + CURLOPT_CONNECTTIMEOUT => 1, + ]); + curl_exec($ch); + curl_close($ch); + } catch (\Exception $e) { + // 静默失败,不影响主流程 + } + } } diff --git a/docs/toolsapi/scripts/qrcode_ws_relay.dart b/docs/toolsapi/scripts/qrcode_ws_relay.dart new file mode 100644 index 00000000..90e01735 --- /dev/null +++ b/docs/toolsapi/scripts/qrcode_ws_relay.dart @@ -0,0 +1,170 @@ +/// ============================================================ +/// 闲言APP — 二维码登录WebSocket中继服务器 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: WebSocket中继服务器,推送二维码状态变更给订阅客户端 +/// 上次更新: 初始创建,基于shelf_web_socket +/// +/// 部署方式: +/// dart run qrcode_ws_relay.dart --port 9444 +/// +/// 架构: +/// 1. 客户端连接 ws://host:9444 并发送 {"type":"qrcode_subscribe","code":"xxx"} +/// 2. PHP后端在qrcodeConfirm/qrcodeCancel时调用 POST /notify +/// 3. 中继服务器将状态变更推送给订阅了对应code的客户端 +/// ============================================================ + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_router/shelf_router.dart'; +import 'package:shelf_web_socket/shelf_web_socket.dart'; + +final _subscribers = >{}; +final _codeToChannels = >{}; + +void main(List args) async { + final port = int.tryParse(args.isNotEmpty ? args.first : '9444') ?? 9444; + + final router = Router(); + + router.get('/ws', webSocketHandler((WebSocketChannel ws) { + String? subscribedCode; + + ws.stream.listen( + (data) { + try { + final json = jsonDecode(data as String) as Map; + final type = json['type'] as String? ?? ''; + + if (type == 'qrcode_subscribe') { + final code = json['code'] as String? ?? ''; + if (code.isNotEmpty) { + subscribedCode = code; + _codeToChannels.putIfAbsent(code, () => []).add(ws); + print('[subscribe] code=$code total=${_codeToChannels[code]?.length}'); + ws.sink.add(jsonEncode({ + 'type': 'qrcode_subscribed', + 'code': code, + })); + } + } else if (type == 'qrcode_unsubscribe') { + final code = json['code'] as String? ?? ''; + if (code.isNotEmpty) { + _codeToChannels[code]?.remove(ws); + if (_codeToChannels[code]?.isEmpty ?? false) { + _codeToChannels.remove(code); + } + subscribedCode = null; + print('[unsubscribe] code=$code'); + } + } else if (type == 'ping') { + ws.sink.add(jsonEncode({'type': 'pong'})); + } + } catch (e) { + print('[error] parse failed: $e'); + } + }, + onDone: () { + if (subscribedCode != null) { + _codeToChannels[subscribedCode]?.remove(ws); + if (_codeToChannels[subscribedCode]?.isEmpty ?? false) { + _codeToChannels.remove(subscribedCode); + } + print('[disconnect] code=$subscribedCode'); + } + }, + onError: (e) { + print('[error] $e'); + if (subscribedCode != null) { + _codeToChannels[subscribedCode]?.remove(ws); + } + }, + ); + })); + + router.post('/notify', (shelf.Request request) async { + try { + final body = await request.readAsString(); + final json = jsonDecode(body) as Map; + final code = json['code'] as String? ?? ''; + final status = json['status'] as String? ?? ''; + final token = json['token'] as String?; + + if (code.isEmpty || status.isEmpty) { + return shelf.Response(400, body: jsonEncode({'error': 'code and status required'})); + } + + final channels = _codeToChannels[code]; + if (channels == null || channels.isEmpty) { + return shelf.Response.ok(jsonEncode({'sent': 0, 'message': 'no subscribers'})); + } + + final message = jsonEncode({ + 'type': 'qrcode_status_update', + 'code': code, + 'status': status, + if (token != null) 'token': token, + 'ts': DateTime.now().millisecondsSinceEpoch, + }); + + var sent = 0; + final toRemove = []; + for (final ch in channels) { + try { + ch.sink.add(message); + sent++; + } catch (e) { + toRemove.add(ch); + } + } + for (final ch in toRemove) { + channels.remove(ch); + } + + print('[notify] code=$code status=$status sent=$sent'); + return shelf.Response.ok(jsonEncode({'sent': sent})); + } catch (e) { + return shelf.Response(500, body: jsonEncode({'error': e.toString()})); + } + }); + + router.get('/stats', (shelf.Request request) { + final stats = {}; + for (final entry in _codeToChannels.entries) { + stats[entry.key] = entry.value.length; + } + return shelf.Response.ok(jsonEncode({ + 'total_codes': _codeToChannels.length, + 'subscribers': stats, + })); + }); + + final handler = const shelf.Pipeline() + .addMiddleware(shelf.logRequests()) + .addHandler(router.call); + + final server = await shelf_io.serve(handler, InternetAddress.anyIPv4, port); + print('🚀 QR Code WebSocket Relay running on ws://0.0.0.0:$port/ws'); + print('📡 Notify endpoint: POST http://0.0.0.0:$port/notify'); + print('📊 Stats endpoint: GET http://0.0.0.0:$port/stats'); +} + +// WebSocketChannel stub for standalone server +// When running as standalone, import web_socket_channel directly +class WebSocketChannel { + final Stream stream; + final WebSocketSink sink; + WebSocketChannel(this.stream, this.sink); +} + +class WebSocketSink { + final Function(String) _add; + final Function() _close; + WebSocketSink(this._add, this._close); + void add(String data) => _add(data); + void close() => _close(); +} diff --git a/docs/toolsapi/scripts/test_qrcode_login.py b/docs/toolsapi/scripts/test_qrcode_login.py new file mode 100644 index 00000000..87037e75 --- /dev/null +++ b/docs/toolsapi/scripts/test_qrcode_login.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" + * @time 2026-06-02 + * @name test_qrcode_login.py + * @description 扫码登录API完整流程测试脚本 + * @lastUpdate v10.3.0 使用现有测试账号; 测试登录→生成二维码→轮询→确认→获取token→取消 +""" + +import hmac +import hashlib +import base64 +import time +import secrets +import json +import sys + +try: + import requests +except ImportError: + print("need: pip3 install requests") + sys.exit(1) + +BASE_URL = "https://tools.wktyl.com" +SECRET = "Xy7kP9mL2qR4wS8v" +TIMEOUT = 15 +TEST_ACCOUNT = "123456" +TEST_PASSWORD = "123456" + +PASS_COUNT = 0 +FAIL_COUNT = 0 +SKIP_COUNT = 0 + + +def generate_receipt(action, payload, secret=SECRET): + ts = int(time.time()) + nonce = secrets.token_hex(8) + payload_hash = hashlib.sha256(payload.encode()).hexdigest()[:16] + data = base64.b64encode(json.dumps({ + 'action': action, + 'payload': payload_hash, + 'ts': ts, + 'nonce': nonce + }).encode()).decode() + sig = hmac.new(secret.encode(), data.encode(), hashlib.sha256).hexdigest() + return data, sig + + +def api_call(method, path, data=None, params=None, token=None): + url = f"{BASE_URL}{path}" + headers = {} + if token: + headers['token'] = token + try: + if method == 'GET': + resp = requests.get(url, params=params, headers=headers, timeout=TIMEOUT) + else: + resp = requests.post(url, data=data, headers=headers, timeout=TIMEOUT) + return resp.json() + except requests.exceptions.Timeout: + return {'code': -2, 'msg': 'timeout'} + except Exception as e: + return {'code': -1, 'msg': str(e)} + + +def assert_test(condition, test_name, detail=""): + global PASS_COUNT, FAIL_COUNT + if condition: + PASS_COUNT += 1 + print(f" ✅ {test_name}") + else: + FAIL_COUNT += 1 + print(f" ❌ {test_name} {detail}") + + +def skip_test(test_name, reason=""): + global SKIP_COUNT + SKIP_COUNT += 1 + print(f" ⏭️ {test_name} {reason}") + + +def test_qrcode_login_flow(): + print("\n" + "=" * 60) + print("🧪 扫码登录API完整流程测试") + print("=" * 60) + + user_token = None + user_id = None + qr_code = None + poll_token = None + + # ─── Step 1: 登录测试账号 ─── + print("\n📌 Step 1: 登录测试账号") + result = api_call('POST', '/api/user_security/login', data={ + 'account': TEST_ACCOUNT, + 'password': TEST_PASSWORD, + }) + assert_test(result.get('code') == 1, "账号密码登录", + f"code={result.get('code')}, msg={result.get('msg')}") + + if result.get('code') != 1: + print(f" ⚠️ 登录失败,跳过后续测试: {result.get('msg')}") + return + + user_token = result.get('data', {}).get('token', '') + user_info = result.get('data', {}).get('userinfo', {}) + user_id = user_info.get('id', '') + print(f" 📋 用户ID: {user_id}, Token: {user_token[:20]}...") + + # ─── Step 2: 生成二维码 ─── + print("\n📌 Step 2: 生成二维码") + result = api_call('GET', '/api/user_security/qrcodeGenerate') + assert_test(result.get('code') == 1, "生成二维码", + f"code={result.get('code')}, msg={result.get('msg')}") + + if result.get('code') == 1: + qr_data = result.get('data', {}) + qr_code = qr_data.get('code', '') + expire_time = qr_data.get('expire_time', 0) + expire_seconds = qr_data.get('expire_seconds', 0) + qrcode_url = qr_data.get('qrcode_url', '') + print(f" 📋 Code: {qr_code[:20]}...") + print(f" 📋 过期时间: {expire_time}, 有效期: {expire_seconds}秒") + print(f" 📋 二维码URL: {qrcode_url[:50]}...") + assert_test(len(qr_code) == 32, "Code长度为32位hex", f"实际长度: {len(qr_code)}") + assert_test(expire_seconds == 300, "有效期为300秒", f"实际: {expire_seconds}") + else: + print(f" ⚠️ 生成二维码失败,跳过后续测试") + return + + # ─── Step 3: 轮询二维码状态(应为pending) ─── + print("\n📌 Step 3: 轮询二维码状态(应为pending)") + result = api_call('GET', '/api/user_security/qrcodePoll', params={'code': qr_code}) + assert_test(result.get('code') == 1, "轮询二维码状态", + f"code={result.get('code')}, msg={result.get('msg')}") + if result.get('code') == 1: + poll_status = result.get('data', {}).get('status', '') + assert_test(poll_status == 'pending', "状态为pending", f"实际: {poll_status}") + + # ─── Step 4: 用登录的token确认扫码 ─── + print("\n📌 Step 4: 用登录的token确认扫码") + result = api_call('POST', '/api/user_security/qrcodeConfirm', data={ + 'code': qr_code, + 'platform': 'test_script', + 'device_name': 'Test Device', + 'app_name': 'test_qrcode_login', + }, token=user_token) + assert_test(result.get('code') == 1, "确认扫码", + f"code={result.get('code')}, msg={result.get('msg')}") + + # ─── Step 5: 轮询二维码状态(应为confirmed,获取新token) ─── + print("\n📌 Step 5: 轮询二维码状态(应为confirmed)") + result = api_call('GET', '/api/user_security/qrcodePoll', params={'code': qr_code}) + assert_test(result.get('code') == 1, "轮询confirmed状态", + f"code={result.get('code')}, msg={result.get('msg')}") + if result.get('code') == 1: + poll_status = result.get('data', {}).get('status', '') + poll_token = result.get('data', {}).get('token', '') + poll_userinfo = result.get('data', {}).get('userinfo', {}) + assert_test(poll_status == 'confirmed', "状态为confirmed", f"实际: {poll_status}") + assert_test(bool(poll_token), "返回Token", "token为空") + assert_test(bool(poll_userinfo), "返回用户信息", "userinfo为空") + if poll_token: + print(f" 📋 新Token: {poll_token[:20]}...") + print(f" 📋 用户: {poll_userinfo.get('username', 'N/A')}") + + # ─── Step 6: 使用新token登录 ─── + print("\n📌 Step 6: 使用新token登录") + if poll_token: + result = api_call('POST', '/api/user_security/tokenLogin', data={ + 'token': poll_token, + }) + assert_test(result.get('code') == 1, "使用新Token登录", + f"code={result.get('code')}, msg={result.get('msg')}") + if result.get('code') == 1: + login_userinfo = result.get('data', {}).get('userinfo', {}) + assert_test(login_userinfo.get('id') == user_id, "用户ID一致", + f"期望: {user_id}, 实际: {login_userinfo.get('id')}") + else: + skip_test("使用新Token登录", "无Token可用") + + # ─── Step 7: 测试取消二维码 ─── + print("\n📌 Step 7: 测试取消二维码") + result = api_call('GET', '/api/user_security/qrcodeGenerate') + if result.get('code') == 1: + cancel_code = result.get('data', {}).get('code', '') + result = api_call('POST', '/api/user_security/qrcodeCancel', data={'code': cancel_code}) + assert_test(result.get('code') == 1, "取消二维码", + f"code={result.get('code')}, msg={result.get('msg')}") + result = api_call('GET', '/api/user_security/qrcodePoll', params={'code': cancel_code}) + if result.get('code') == 1: + cancel_status = result.get('data', {}).get('status', '') + assert_test(cancel_status == 'cancelled', "取消后状态为cancelled", + f"实际: {cancel_status}") + else: + skip_test("取消二维码", "无法生成新二维码") + + # ─── Step 8: 测试密保问题接口 ─── + print("\n📌 Step 8: 测试密保问题接口") + result = api_call('GET', '/api/user_security/secQuestions') + assert_test(result.get('code') == 1, "获取密保问题列表", + f"code={result.get('code')}, msg={result.get('msg')}") + if result.get('code') == 1: + questions = result.get('data', {}).get('questions', []) + assert_test(len(questions) == 8, "密保问题数量为8", f"实际: {len(questions)}") + + +def test_edge_cases(): + print("\n" + "=" * 60) + print("🧪 边界情况测试") + print("=" * 60) + + # 无效code轮询 + print("\n📌 测试无效code轮询") + result = api_call('GET', '/api/user_security/qrcodePoll', params={'code': 'invalid_code_12345'}) + assert_test(result.get('code') == 0, "无效code返回错误", + f"code={result.get('code')}, msg={result.get('msg')}") + + # 无效code确认(需登录,返回401) + print("\n📌 测试无效code确认(未登录)") + result = api_call('POST', '/api/user_security/qrcodeConfirm', data={'code': 'invalid_code_12345'}) + assert_test(result.get('code') in [-1, 0, 401], "未登录确认返回错误", + f"code={result.get('code')}, msg={result.get('msg')}") + + # 空code参数 + print("\n📌 测试空code参数") + result = api_call('GET', '/api/user_security/qrcodePoll', params={'code': ''}) + assert_test(result.get('code') == 0, "空code返回错误", + f"code={result.get('code')}, msg={result.get('msg')}") + + +if __name__ == '__main__': + print("🚀 闲言工具箱 - 扫码登录API测试") + print(f"📅 时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"🌐 基础URL: {BASE_URL}") + + test_qrcode_login_flow() + test_edge_cases() + + print("\n" + "=" * 60) + print("📊 测试结果汇总") + print("=" * 60) + total = PASS_COUNT + FAIL_COUNT + SKIP_COUNT + print(f" ✅ 通过: {PASS_COUNT}") + print(f" ❌ 失败: {FAIL_COUNT}") + print(f" ⏭️ 跳过: {SKIP_COUNT}") + print(f" 📋 总计: {total}") + print(f" 📈 通过率: {PASS_COUNT/total*100:.1f}%" if total > 0 else " 📈 无测试") + + if FAIL_COUNT > 0: + print("\n⚠️ 存在失败测试,请检查!") + sys.exit(1) + else: + print("\n🎉 所有测试通过!") + sys.exit(0) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ebe582b1..01074b3d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -8,12 +8,19 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter + - CwlCatchException (2.2.1): + - CwlCatchExceptionSupport (~> 2.2.1) + - CwlCatchExceptionSupport (2.2.1) + - device_calendar (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - file_picker (0.0.1): - Flutter - FlutterMacOS - Flutter (1.0.0) + - flutter_app_group_directory (0.0.1): + - Flutter - flutter_blue_plus_darwin (0.0.2): - Flutter - FlutterMacOS @@ -29,6 +36,8 @@ PODS: - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 6.0.3) + - flutter_keyboard_visibility (0.0.1): + - Flutter - flutter_keyboard_visibility_temp_fork (0.0.1): - Flutter - flutter_local_notifications (0.0.1): @@ -42,6 +51,8 @@ PODS: - FlutterMacOS - flutter_tts (0.0.1): - Flutter + - flutter_vibrate (0.0.1): + - Flutter - flutter_webrtc (1.4.0): - Flutter - WebRTC-SDK (= 144.7559.01) @@ -66,6 +77,8 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv + - live_activities (0.0.1): + - Flutter - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -83,15 +96,19 @@ PODS: - OrderedSet (6.0.3) - package_info_plus (0.4.5): - Flutter - - permission_handler_apple (9.3.0): + - permission_handler_apple (9.4.8): - Flutter - pro_image_editor (12.0.8): - Flutter + - quick_actions_ios (0.0.1): + - Flutter - quill_native_bridge_ios (0.0.1): - Flutter - receive_sharing_intent (1.8.1): - Flutter - - record_ios (1.2.0): + - record_ios (1.2.1): + - Flutter + - rive_native (0.0.1): - Flutter - SDWebImage (5.21.7): - SDWebImage/Core (= 5.21.7) @@ -106,6 +123,10 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - speech_to_text (7.2.0): + - CwlCatchException + - Flutter + - FlutterMacOS - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS @@ -146,29 +167,36 @@ PODS: - WebRTC-SDK (144.7559.01) - wifi_iot (0.0.1): - Flutter + - workmanager_apple (0.0.1): + - Flutter DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`) - battery_plus (from `.symlinks/plugins/battery_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_calendar (from `.symlinks/plugins/device_calendar/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/darwin`) - Flutter (from `Flutter`) + - flutter_app_group_directory (from `.symlinks/plugins/flutter_app_group_directory/ios`) - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_nfc_kit (from `.symlinks/plugins/flutter_nfc_kit/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) + - flutter_vibrate (from `.symlinks/plugins/flutter_vibrate/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - live_activities (from `.symlinks/plugins/live_activities/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - nearby_service (from `.symlinks/plugins/nearby_service/darwin`) @@ -176,12 +204,15 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - pro_image_editor (from `.symlinks/plugins/pro_image_editor/ios`) + - quick_actions_ios (from `.symlinks/plugins/quick_actions_ios/ios`) - quill_native_bridge_ios (from `.symlinks/plugins/quill_native_bridge_ios/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - record_ios (from `.symlinks/plugins/record_ios/ios`) + - rive_native (from `.symlinks/plugins/rive_native/ios`) - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -189,9 +220,12 @@ DEPENDENCIES: - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wifi_iot (from `.symlinks/plugins/wifi_iot/ios`) + - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) SPEC REPOS: trunk: + - CwlCatchException + - CwlCatchExceptionSupport - libwebp - Mantle - OrderedSet @@ -209,18 +243,24 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/battery_plus/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + device_calendar: + :path: ".symlinks/plugins/device_calendar/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: :path: ".symlinks/plugins/file_picker/darwin" Flutter: :path: Flutter + flutter_app_group_directory: + :path: ".symlinks/plugins/flutter_app_group_directory/ios" flutter_blue_plus_darwin: :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" flutter_image_compress_common: :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_keyboard_visibility_temp_fork: :path: ".symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios" flutter_local_notifications: @@ -233,6 +273,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" flutter_tts: :path: ".symlinks/plugins/flutter_tts/ios" + flutter_vibrate: + :path: ".symlinks/plugins/flutter_vibrate/ios" flutter_webrtc: :path: ".symlinks/plugins/flutter_webrtc/ios" fluttertoast: @@ -243,6 +285,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/home_widget/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + live_activities: + :path: ".symlinks/plugins/live_activities/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" mobile_scanner: @@ -257,18 +301,24 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" pro_image_editor: :path: ".symlinks/plugins/pro_image_editor/ios" + quick_actions_ios: + :path: ".symlinks/plugins/quick_actions_ios/ios" quill_native_bridge_ios: :path: ".symlinks/plugins/quill_native_bridge_ios/ios" receive_sharing_intent: :path: ".symlinks/plugins/receive_sharing_intent/ios" record_ios: :path: ".symlinks/plugins/record_ios/ios" + rive_native: + :path: ".symlinks/plugins/rive_native/ios" sensors_plus: :path: ".symlinks/plugins/sensors_plus/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + speech_to_text: + :path: ".symlinks/plugins/speech_to_text/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" sqlite3_flutter_libs: @@ -283,30 +333,39 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" wifi_iot: :path: ".symlinks/plugins/wifi_iot/ios" + workmanager_apple: + :path: ".symlinks/plugins/workmanager_apple/ios" SPEC CHECKSUMS: app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 battery_plus: b42253f6d2dde71712f8c36fef456d99121c5977 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a + CwlCatchExceptionSupport: 54ccab8d8c78907b57f99717fb19d4cc3bce02dc + device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_app_group_directory: 55b5362007d1c0cb45dc1dd1e94f67d615f45a6b flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214 flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 flutter_nfc_kit: e1b71583eafd2c9650bc86844a7f2d185fb414f6 flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 flutter_tts: 35ac3c7d42412733e795ea96ad2d7e05d0a75113 + flutter_vibrate: 207bbbeb62dd5638b479846c8e46168d7229f14a flutter_webrtc: ec91d94b484ad49cf191ef93413f64a40ffd3b4c fluttertoast: fe6790210fdba20801685be946e3a2124b72eef5 gal: baecd024ebfd13c441269ca7404792a7152fde89 home_widget: 54b4f6b36ed8d64cfee594a476225c35c3e45091 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + live_activities: 4dfa736d0736e1c77866a2f9c056a76513cc9e7b local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 @@ -314,16 +373,19 @@ SPEC CHECKSUMS: network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e pro_image_editor: 3dedac450f82a389877286fa9eb08852cefb04ea + quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779 quill_native_bridge_ios: f47af4b14e7757968486641656c5d23250cee521 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 - record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844 + record_ios: 980fd386a97a35987d0fce3dfda4b26a38c90f4c + rive_native: c8fbe631855835dc84f7d59befcdb43de3ac48d4 SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + speech_to_text: 3b313d98516d3d0406cea424782ec25470c59d19 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab @@ -333,7 +395,8 @@ SPEC CHECKSUMS: wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 WebRTC-SDK: ab9b5319e458c2bfebdc92b3600740da35d5630d wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc + workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 -PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e +PODFILE CHECKSUM: 824cbd14d64ab8a3662bf157818b7c3029ac0eb0 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6f9b1dc6..97255cc1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,16 +9,17 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 261D109F22B957D6345FE8D4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8516FA250E04F5DCA0BF879B /* Pods_Runner.framework */; }; + 2F5A779AFE2C2339016EA82A /* Pods_XianyanWidgetExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9620295101249CC025EAA1BB /* Pods_XianyanWidgetExtension.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - DEE3CE70CC495E564BA02764 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */; }; A1FE00100000000000000010 /* XianyanWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FE00010000000000000001 /* XianyanWidget.swift */; }; A1FE00110000000000000011 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1FE00040000000000000004 /* Assets.xcassets */; }; A1FE00120000000000000012 /* XianyanWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1FE00050000000000000005 /* XianyanWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DEE3CE70CC495E564BA02764 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -65,9 +66,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 20AB0EF8351A3117D3F8CD78 /* Pods-XianyanWidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-XianyanWidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-XianyanWidgetExtension/Pods-XianyanWidgetExtension.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 417BCFB6E2819B0317085D19 /* Pods-XianyanWidgetExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-XianyanWidgetExtension.profile.xcconfig"; path = "Target Support Files/Pods-XianyanWidgetExtension/Pods-XianyanWidgetExtension.profile.xcconfig"; sourceTree = ""; }; 4E5D7D601A13EF4FC7FF09C9 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6A4864A98473C1CB5799EC31 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -76,6 +79,7 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 8516FA250E04F5DCA0BF879B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9620295101249CC025EAA1BB /* Pods_XianyanWidgetExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_XianyanWidgetExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -83,14 +87,15 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AB11A1FFADECAB65336D2E91 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - DB4843BA3B8F591B55759752 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - FF5404F72C4CE124A8A15D0B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 9BDAA8F02234D45A675368BA /* Pods-XianyanWidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-XianyanWidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-XianyanWidgetExtension/Pods-XianyanWidgetExtension.debug.xcconfig"; sourceTree = ""; }; A1FE00010000000000000001 /* XianyanWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XianyanWidget.swift; sourceTree = ""; }; A1FE00020000000000000002 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A1FE00030000000000000003 /* XianyanWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = XianyanWidget.entitlements; sourceTree = ""; }; A1FE00040000000000000004 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A1FE00050000000000000005 /* XianyanWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = XianyanWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + AB11A1FFADECAB65336D2E91 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DB4843BA3B8F591B55759752 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + FF5404F72C4CE124A8A15D0B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -114,6 +119,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2F5A779AFE2C2339016EA82A /* Pods_XianyanWidgetExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -133,6 +139,7 @@ children = ( 8516FA250E04F5DCA0BF879B /* Pods_Runner.framework */, 6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */, + 9620295101249CC025EAA1BB /* Pods_XianyanWidgetExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -206,6 +213,9 @@ FF5404F72C4CE124A8A15D0B /* Pods-RunnerTests.debug.xcconfig */, 6BACA6720B4314FD7805C6EA /* Pods-RunnerTests.release.xcconfig */, 4E5D7D601A13EF4FC7FF09C9 /* Pods-RunnerTests.profile.xcconfig */, + 9BDAA8F02234D45A675368BA /* Pods-XianyanWidgetExtension.debug.xcconfig */, + 20AB0EF8351A3117D3F8CD78 /* Pods-XianyanWidgetExtension.release.xcconfig */, + 417BCFB6E2819B0317085D19 /* Pods-XianyanWidgetExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -261,6 +271,7 @@ isa = PBXNativeTarget; buildConfigurationList = A1FE00700000000000000070 /* Build configuration list for PBXNativeTarget "XianyanWidgetExtension" */; buildPhases = ( + 06D3187A82150ADA5C89EC6C /* [CP] Check Pods Manifest.lock */, A1FE00400000000000000040 /* Sources */, A1FE00420000000000000042 /* Frameworks */, A1FE00410000000000000041 /* Resources */, @@ -348,6 +359,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 06D3187A82150ADA5C89EC6C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-XianyanWidgetExtension-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 08ABBB9E0B8EF5217E7AB276 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -814,6 +847,7 @@ }; A1FE00600000000000000060 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9BDAA8F02234D45A675368BA /* Pods-XianyanWidgetExtension.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -841,6 +875,7 @@ }; A1FE00610000000000000061 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 20AB0EF8351A3117D3F8CD78 /* Pods-XianyanWidgetExtension.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -866,6 +901,7 @@ }; A1FE00620000000000000062 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 417BCFB6E2819B0317085D19 /* Pods-XianyanWidgetExtension.profile.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f98b3049..41d081b6 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Xianyan + 闲言 CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - xianyan + 闲言 CFBundlePackageType APPL CFBundleShortVersionString diff --git a/ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3e..76124915 100644 --- a/ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "Icon-App-1024x1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024.png b/ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024.png new file mode 100644 index 00000000..072326b9 Binary files /dev/null and b/ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024.png differ diff --git a/lib/core/services/device/live_activity_provider.dart b/lib/core/services/device/live_activity_provider.dart index e3d6d363..957c3058 100644 --- a/lib/core/services/device/live_activity_provider.dart +++ b/lib/core/services/device/live_activity_provider.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 灵动岛/实时活动Provider /// 创建时间: 2026-05-25 -/// 更新时间: 2026-05-30 +/// 更新时间: 2026-06-02 /// 作用: 灵动岛服务状态管理Provider -/// 上次更新: 使用SafeNotifierInit统一异常保护 +/// 上次更新: 补充倒计时活动update/end方法 /// ============================================================ import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -107,6 +107,25 @@ class LiveActivityNotifier extends Notifier ); } + /// 更新倒计时进度 + Future updateCountdownActivity({ + required String title, + required int remainingMinutes, + String emoji = '⏱️', + }) async { + await LiveActivityService.instance.updateCountdownActivity( + title: title, + remainingMinutes: remainingMinutes, + emoji: emoji, + ); + } + + /// 结束倒计时活动 + Future endCountdownActivity() async { + await LiveActivityService.instance.endCountdownActivity(); + state = state.copyWith(hasActiveActivity: false); + } + /// 结束所有活动 Future endAllActivities() async { await LiveActivityService.instance.endAllActivities(); diff --git a/lib/core/services/device/live_activity_service.dart b/lib/core/services/device/live_activity_service.dart index 92026677..8feaaaf0 100644 --- a/lib/core/services/device/live_activity_service.dart +++ b/lib/core/services/device/live_activity_service.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 灵动岛/实时活动服务 /// 创建时间: 2026-05-25 -/// 更新时间: 2026-05-25 -/// 作用: 基于live_activities实现番茄钟灵动岛显示 -/// 上次更新: 初始创建 +/// 更新时间: 2026-06-02 +/// 作用: 基于live_activities实现番茄钟/倒计时灵动岛显示 +/// 上次更新: 增强倒计时活动支持(updateCountdownActivity/endCountdownActivity) /// ============================================================ import 'package:flutter/foundation.dart'; @@ -12,10 +12,6 @@ import 'package:live_activities/live_activities.dart'; import '../../utils/logger.dart'; import '../device/haptic_service.dart'; -/// 灵动岛/实时活动服务 — 全局单例 -/// -/// 仅iOS 16.1+平台有效,其他平台静默返回。 -/// 支持番茄钟倒计时和通用倒计时两种活动类型。 class LiveActivityService { LiveActivityService._(); @@ -25,21 +21,16 @@ class LiveActivityService { bool _isSupported = false; String? _activeActivityId; + String? _activeType; - /// 是否支持灵动岛(iOS 16.1+) bool get isSupported => _isSupported; - - /// 当前活动ID String? get activeActivityId => _activeActivityId; - - /// 是否有活跃的活动 bool get hasActiveActivity => _activeActivityId != null; + String? get activeType => _activeType; - /// 活动类型标识 — 对应iOS Widget Extension中的ActivityType static const _pomodoroScheme = 'pomodoro'; static const _countdownScheme = 'countdown'; - /// 初始化并检测支持 Future init() async { if (defaultTargetPlatform != TargetPlatform.iOS) { _isSupported = false; @@ -56,11 +47,10 @@ class LiveActivityService { } } - /// 开始番茄钟活动 - /// - /// [remainingMinutes] 剩余分钟数 - /// [totalMinutes] 总分钟数 - /// [isBreak] 是否为休息阶段 + // ============================================================ + // 番茄钟活动 + // ============================================================ + Future startPomodoroActivity({ required int remainingMinutes, required int totalMinutes, @@ -69,6 +59,8 @@ class LiveActivityService { if (!_isSupported) return; try { + await _endExistingIfDifferent('pomodoro'); + final now = DateTime.now(); final endTime = now.add(Duration(minutes: remainingMinutes)); @@ -87,6 +79,7 @@ class LiveActivityService { removeWhenAppIsKilled: true, ); + _activeType = 'pomodoro'; HapticService.medium(); Log.i('LiveActivityService: 番茄钟活动已创建 id=$_activeActivityId'); } catch (e) { @@ -94,11 +87,6 @@ class LiveActivityService { } } - /// 更新番茄钟进度 - /// - /// [remainingMinutes] 剩余分钟数 - /// [totalMinutes] 总分钟数 - /// [isBreak] 是否为休息阶段 Future updatePomodoroActivity({ required int remainingMinutes, required int totalMinutes, @@ -130,27 +118,26 @@ class LiveActivityService { } } - /// 结束番茄钟活动 Future endPomodoroActivity() async { - if (!_isSupported || _activeActivityId == null) return; + if (!_isSupported || _activeActivityId == null || _activeType != 'pomodoro') return; try { await _liveActivities.endActivity(_activeActivityId!); - HapticService.success(); Log.i('LiveActivityService: 番茄钟活动已结束'); _activeActivityId = null; + _activeType = null; } catch (e) { Log.e('LiveActivityService: 结束番茄钟活动失败 $e'); _activeActivityId = null; + _activeType = null; } } - /// 开始倒计时活动 - /// - /// [title] 倒计时标题 - /// [remainingMinutes] 剩余分钟数 - /// [emoji] 显示emoji + // ============================================================ + // 倒计时活动 + // ============================================================ + Future startCountdownActivity({ required String title, required int remainingMinutes, @@ -159,6 +146,8 @@ class LiveActivityService { if (!_isSupported) return; try { + await _endExistingIfDifferent('countdown'); + final now = DateTime.now(); final endTime = now.add(Duration(minutes: remainingMinutes)); @@ -174,6 +163,7 @@ class LiveActivityService { removeWhenAppIsKilled: true, ); + _activeType = 'countdown'; HapticService.medium(); Log.i('LiveActivityService: 倒计时活动已创建 id=$_activeActivityId'); } catch (e) { @@ -181,21 +171,82 @@ class LiveActivityService { } } - /// 结束所有活动 + Future updateCountdownActivity({ + required String title, + required int remainingMinutes, + String emoji = '⏱️', + }) async { + if (!_isSupported || _activeActivityId == null || _activeType != 'countdown') return; + + try { + final now = DateTime.now(); + final endTime = now.add(Duration(minutes: remainingMinutes)); + + await _liveActivities.updateActivity( + _activeActivityId!, + { + 'type': 'countdown', + 'title': title, + 'remainingMinutes': remainingMinutes, + 'endTime': endTime.millisecondsSinceEpoch, + 'emoji': emoji, + }, + ); + + Log.d('LiveActivityService: 倒计时活动已更新 remaining=$remainingMinutes'); + } catch (e) { + Log.e('LiveActivityService: 更新倒计时活动失败 $e'); + } + } + + Future endCountdownActivity() async { + if (!_isSupported || _activeActivityId == null || _activeType != 'countdown') return; + + try { + await _liveActivities.endActivity(_activeActivityId!); + HapticService.success(); + Log.i('LiveActivityService: 倒计时活动已结束'); + _activeActivityId = null; + _activeType = null; + } catch (e) { + Log.e('LiveActivityService: 结束倒计时活动失败 $e'); + _activeActivityId = null; + _activeType = null; + } + } + + // ============================================================ + // 通用方法 + // ============================================================ + Future endAllActivities() async { if (!_isSupported) return; try { await _liveActivities.endAllActivities(); _activeActivityId = null; + _activeType = null; Log.i('LiveActivityService: 所有活动已结束'); } catch (e) { Log.e('LiveActivityService: 结束所有活动失败 $e'); _activeActivityId = null; + _activeType = null; + } + } + + Future _endExistingIfDifferent(String newType) async { + if (_activeActivityId != null && _activeType != null && _activeType != newType) { + try { + await _liveActivities.endActivity(_activeActivityId!); + _activeActivityId = null; + _activeType = null; + Log.i('LiveActivityService: 结束旧活动(类型不同)'); + } catch (e) { + Log.w('LiveActivityService: 结束旧活动失败 $e'); + } } } - /// 释放资源 void dispose() { _liveActivities.dispose(); } diff --git a/lib/core/services/device/macos_platform_service.dart b/lib/core/services/device/macos_platform_service.dart new file mode 100644 index 00000000..831515cc --- /dev/null +++ b/lib/core/services/device/macos_platform_service.dart @@ -0,0 +1,120 @@ +/// ============================================================ +/// 闲言APP — macOS平台统一服务 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 集中管理所有macOS原生MethodChannel交互(主题同步/窗口管理/工具栏样式) +/// 上次更新: 整合MacosTitleBarService,新增窗口管理能力 +/// ============================================================ + +import 'package:flutter/services.dart'; +import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; +import 'package:xianyan/core/utils/logger.dart'; + +class MacosPlatformService { + MacosPlatformService._(); + + static const _channel = MethodChannel('com.xianyan.macos'); + + // ============================================================ + // 主题同步(原 MacosTitleBarService) + // ============================================================ + + static bool _lastIsDark = false; + static bool _themeInitialized = false; + + /// 同步标题栏明暗模式 + static void syncTheme(bool isDark) { + if (!pu.isMacOS) return; + if (_themeInitialized && _lastIsDark == isDark) return; + + _themeInitialized = true; + _lastIsDark = isDark; + + _invoke('setDarkMode', isDark); + } + + // ============================================================ + // 窗口管理 + // ============================================================ + + /// 设置窗口标题 + static void setWindowTitle(String title) { + if (!pu.isMacOS) return; + _invoke('setWindowTitle', title); + } + + /// 设置窗口透明标题栏 + static void setTitleBarTransparent(bool transparent) { + if (!pu.isMacOS) return; + _invoke('setTitleBarTransparent', transparent); + } + + /// 设置标题栏样式(auto/light/dark) + static void setTitleBarStyle(String style) { + if (!pu.isMacOS) return; + _invoke('setTitleBarStyle', style); + } + + /// 设置工具栏可见性 + static void setToolbarVisible(bool visible) { + if (!pu.isMacOS) return; + _invoke('setToolbarVisible', visible); + } + + /// 设置窗口全屏 + static void setFullscreen(bool fullscreen) { + if (!pu.isMacOS) return; + _invoke('setFullscreen', fullscreen); + } + + /// 获取窗口是否全屏 + static Future isFullscreen() async { + if (!pu.isMacOS) return false; + try { + final result = await _channel.invokeMethod('isFullscreen'); + return result ?? false; + } catch (e) { + Log.w('MacosPlatformService.isFullscreen失败: $e'); + return false; + } + } + + /// 设置窗口最小尺寸 + static void setMinSize(double width, double height) { + if (!pu.isMacOS) return; + _invoke('setMinSize', {'width': width, 'height': height}); + } + + // ============================================================ + // 系统集成 + // ============================================================ + + /// 通知系统触感反馈 + static void performHapticFeedback(String type) { + if (!pu.isMacOS) return; + _invoke('performHapticFeedback', type); + } + + /// 获取系统外观(light/dark) + static Future getSystemAppearance() async { + if (!pu.isMacOS) return null; + try { + return await _channel.invokeMethod('getSystemAppearance'); + } catch (e) { + Log.w('MacosPlatformService.getSystemAppearance失败: $e'); + return null; + } + } + + // ============================================================ + // 内部工具 + // ============================================================ + + static void _invoke(String method, [dynamic arguments]) { + try { + _channel.invokeMethod(method, arguments); + } catch (e) { + Log.w('MacosPlatformService.$method失败: $e'); + } + } +} diff --git a/lib/core/services/notification/notification_init_stub.dart b/lib/core/services/notification/notification_init_stub.dart index 8f4f5bc2..fdfe273e 100644 --- a/lib/core/services/notification/notification_init_stub.dart +++ b/lib/core/services/notification/notification_init_stub.dart @@ -54,7 +54,7 @@ InitializationSettings _buildOhosInitSettings({ final dynamic ohosSettings = _createOhosInitializationSettings( ohosDefaultIcon, ); - final dynamic constructor = InitializationSettings.new; + const dynamic constructor = InitializationSettings.new; return constructor( android: androidSettings, iOS: iosSettings, @@ -75,7 +75,7 @@ InitializationSettings _buildOhosInitSettings({ /// 鸿蒙端本地包导出 OhosInitializationSettings 类, /// 官方 SDK 不存在此类。通过 dynamic 反射创建实例。 dynamic _createOhosInitializationSettings(String defaultIcon) { - final dynamic plugin = FlutterLocalNotificationsPlugin; + const dynamic plugin = FlutterLocalNotificationsPlugin; final dynamic ohosClass = plugin.ohosInitializationSettings; return ohosClass(defaultIcon); } diff --git a/lib/core/services/ui/macos_title_bar_service.dart b/lib/core/services/ui/macos_title_bar_service.dart new file mode 100644 index 00000000..3e22ce04 --- /dev/null +++ b/lib/core/services/ui/macos_title_bar_service.dart @@ -0,0 +1,12 @@ +/// ============================================================ +/// 闲言APP — macOS标题栏主题同步服务(已废弃) +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 已迁移至 MacosPlatformService,此文件仅做兼容桥接 +/// 上次更新: 废弃,所有功能已迁移至 macos_platform_service.dart +/// ============================================================ + +@Deprecated('已迁移至 MacosPlatformService,请使用 MacosPlatformService.syncTheme()') +library; + +export 'package:xianyan/core/services/device/macos_platform_service.dart'; diff --git a/lib/features/auth/services/qrcode_ws_service.dart b/lib/features/auth/services/qrcode_ws_service.dart new file mode 100644 index 00000000..5191e0b4 --- /dev/null +++ b/lib/features/auth/services/qrcode_ws_service.dart @@ -0,0 +1,187 @@ +/// ============================================================ +/// 闲言APP — 二维码登录WebSocket推送服务 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 通过WebSocket长连接接收二维码状态变更推送,替代HTTP轮询 +/// 上次更新: 初始创建,支持信令服务器订阅+HTTP轮询降级 +/// ============================================================ + +import 'dart:async'; +import 'dart:convert'; + +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:xianyan/core/utils/logger.dart'; + +typedef QrcodeStatusCallback = void Function(Map data); + +class QrcodeWsService { + QrcodeWsService._(); + + static final QrcodeWsService instance = QrcodeWsService._(); + + WebSocketChannel? _channel; + bool _isConnected = false; + Timer? _heartbeatTimer; + Timer? _reconnectTimer; + int _reconnectAttempts = 0; + static const _maxReconnectAttempts = 5; + + String? _subscribedCode; + QrcodeStatusCallback? _onStatusUpdate; + StreamSubscription? _subscription; + + bool get isConnected => _isConnected; + + /// 连接WebSocket服务器 + Future connect() async { + if (_isConnected) return true; + + final wsUrl = _resolveWsUrl(); + if (wsUrl.isEmpty) { + Log.w('QrcodeWsService: WebSocket URL不可用'); + return false; + } + + try { + Log.i('QrcodeWsService: 连接 $wsUrl'); + _channel = WebSocketChannel.connect(Uri.parse(wsUrl)); + + await _channel!.ready.timeout( + const Duration(seconds: 5), + onTimeout: () => throw TimeoutException('连接超时'), + ); + + _isConnected = true; + _reconnectAttempts = 0; + _startHeartbeat(); + + _subscription = _channel!.stream.listen( + (data) => _handleMessage(data), + onDone: () { + _isConnected = false; + Log.w('QrcodeWsService: 连接关闭'); + _scheduleReconnect(); + }, + onError: (e) { + _isConnected = false; + Log.e('QrcodeWsService: 连接错误 $e'); + _scheduleReconnect(); + }, + ); + + Log.i('QrcodeWsService: 连接成功'); + if (_subscribedCode != null) { + _sendSubscribe(_subscribedCode!); + } + return true; + } catch (e) { + Log.w('QrcodeWsService: 连接失败 $e'); + _isConnected = false; + return false; + } + } + + /// 订阅二维码状态变更 + Future subscribe(String code, QrcodeStatusCallback onStatus) async { + _subscribedCode = code; + _onStatusUpdate = onStatus; + + if (!_isConnected) { + final ok = await connect(); + if (!ok) { + Log.w('QrcodeWsService: WebSocket不可用,将使用HTTP轮询降级'); + return; + } + } + + _sendSubscribe(code); + } + + /// 取消订阅 + void unsubscribe() { + if (_isConnected && _subscribedCode != null) { + _send({'type': 'qrcode_unsubscribe', 'code': _subscribedCode}); + } + _subscribedCode = null; + _onStatusUpdate = null; + } + + /// 断开连接 + void disconnect() { + _subscription?.cancel(); + _subscription = null; + _heartbeatTimer?.cancel(); + _reconnectTimer?.cancel(); + _channel?.sink.close(); + _channel = null; + _isConnected = false; + _subscribedCode = null; + _onStatusUpdate = null; + _reconnectAttempts = 0; + Log.i('QrcodeWsService: 已断开'); + } + + void _sendSubscribe(String code) { + _send({ + 'type': 'qrcode_subscribe', + 'code': code, + }); + Log.i('QrcodeWsService: 已订阅 $code'); + } + + void _send(Map data) { + if (!_isConnected || _channel == null) return; + try { + _channel!.sink.add(jsonEncode(data)); + } catch (e) { + Log.w('QrcodeWsService: 发送失败 $e'); + } + } + + void _handleMessage(dynamic data) { + try { + final json = jsonDecode(data as String) as Map; + final type = json['type'] as String? ?? ''; + + if (type == 'qrcode_status_update') { + Log.i('QrcodeWsService: 收到状态推送 ${json['status']}'); + _onStatusUpdate?.call(json); + } else if (type == 'pong') { + // heartbeat ack + } + } catch (e) { + Log.w('QrcodeWsService: 消息解析失败 $e'); + } + } + + void _startHeartbeat() { + _heartbeatTimer?.cancel(); + _heartbeatTimer = Timer.periodic(const Duration(seconds: 25), (_) { + _send({'type': 'ping'}); + }); + } + + void _scheduleReconnect() { + if (_reconnectAttempts >= _maxReconnectAttempts) { + Log.w('QrcodeWsService: 超过最大重连次数'); + return; + } + if (_subscribedCode == null) return; + + _reconnectAttempts++; + final delay = Duration(seconds: _reconnectAttempts * 2); + Log.i('QrcodeWsService: ${delay.inSeconds}秒后重连 (第$_reconnectAttempts次)'); + + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(delay, () async { + final ok = await connect(); + if (ok && _subscribedCode != null) { + _sendSubscribe(_subscribedCode!); + } + }); + } + + String _resolveWsUrl() { + return 'wss://tools.wktyl.com:9443'; + } +} diff --git a/lib/features/countdown/presentation/countdown_page.dart b/lib/features/countdown/presentation/countdown_page.dart index 0de30f1b..093c0761 100644 --- a/lib/features/countdown/presentation/countdown_page.dart +++ b/lib/features/countdown/presentation/countdown_page.dart @@ -1,15 +1,16 @@ /// ============================================================ /// 闲言APP — 倒计时页面 /// 创建时间: 2026-05-02 -/// 更新时间: 2026-05-30 -/// 作用: 倒计时事件列表 + 新增/编辑 + 分类筛选 -/// 上次更新: 修复+号按钮颜色和点击区域; iconPrimary→accent, 增加minimumSize +/// 更新时间: 2026-06-02 +/// 作用: 倒计时事件列表 + 新增/编辑 + 分类筛选 + 灵动岛聚焦 +/// 上次更新: 集成灵动岛聚焦模式,卡片添加灵动岛按钮 /// ============================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/services/device/live_activity_provider.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; @@ -95,6 +96,7 @@ class _CountdownPageState extends ConsumerState { vertical: 12, ), children: [ + if (state.focusedEventId != null) _buildFocusBanner(state, ext), if (state.pinned.isNotEmpty) ...[ _buildSectionTitle('📌 置顶', ext), ...state.pinned.asMap().entries.map( @@ -120,6 +122,65 @@ class _CountdownPageState extends ConsumerState { ); } + // ============================================================ + // 灵动岛聚焦横幅 + // ============================================================ + + Widget _buildFocusBanner(CountdownState state, AppThemeExtension ext) { + final event = state.focusedEvent; + if (event == null) return const SizedBox.shrink(); + + return Container( + margin: const EdgeInsets.only(bottom: AppSpacing.md), + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm + 2, + ), + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.1), + borderRadius: AppRadius.lgBorder, + border: Border.all(color: ext.accent.withValues(alpha: 0.25)), + ), + child: Row( + children: [ + Text(event.emoji, style: const TextStyle(fontSize: 18)), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '灵动岛已开启', + style: AppTypography.caption1.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + Text( + '${event.title} · ${event.remainingLabel}', + style: AppTypography.caption2.copyWith( + color: ext.textSecondary, + ), + ), + ], + ), + ), + CupertinoButton( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + minimumSize: Size.zero, + onPressed: () => + ref.read(countdownProvider.notifier).unfocusEvent(), + child: Icon( + CupertinoIcons.xmark_circle_fill, + color: ext.accent, + size: 20, + ), + ), + ], + ), + ).animate().fadeIn(duration: 300.ms).slideY(begin: -0.1, end: 0); + } + // ============================================================ // 分区标题 // ============================================================ @@ -149,60 +210,86 @@ class _CountdownPageState extends ConsumerState { int index = 0, }) { final color = _parseColor(event.colorHex); + final isFocused = ref.watch(countdownProvider).focusedEventId == event.id; + final isLiveActivitySupported = + ref.watch(liveActivitySupportedProvider); return GestureDetector( - onLongPress: () => _showEventActions(event), - child: Container( - margin: const EdgeInsets.only(bottom: 10), - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: ext.bgCard, - borderRadius: AppRadius.lgBorder, - border: isPinned + onLongPress: () => _showEventActions(event), + child: Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: ext.bgCard, + borderRadius: AppRadius.lgBorder, + border: isFocused + ? Border.all(color: ext.accent.withValues(alpha: 0.5)) + : isPinned ? Border.all(color: color.withValues(alpha: 0.3)) : null, + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: AppRadius.mdBorder, + ), + child: Center( + child: Text( + event.emoji, + style: const TextStyle(fontSize: 24), + ), + ), ), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: color.withValues(alpha: 0.12), - borderRadius: AppRadius.mdBorder, - ), - child: Center( - child: Text( - event.emoji, - style: const TextStyle(fontSize: 24), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.title, + style: AppTypography.body.copyWith( + fontWeight: FontWeight.w600, + color: isPast ? ext.textHint : ext.textPrimary, ), ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - event.title, - style: AppTypography.body.copyWith( - fontWeight: FontWeight.w600, - color: isPast ? ext.textHint : ext.textPrimary, - ), - ), - const SizedBox(height: AppSpacing.xs), - Text( - _formatDate(event.targetDate), - style: AppTypography.footnote.copyWith( - color: ext.textHint, - ), - ), - ], + const SizedBox(height: AppSpacing.xs), + Text( + _formatDate(event.targetDate), + style: AppTypography.footnote.copyWith( + color: ext.textHint, + ), ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisSize: MainAxisSize.min, children: [ + if (isLiveActivitySupported && !isPast) + Padding( + padding: const EdgeInsets.only(right: 6), + child: GestureDetector( + onTap: () => ref + .read(countdownProvider.notifier) + .focusEvent(event.id), + child: Icon( + isFocused + ? CupertinoIcons.bell_fill + : CupertinoIcons.bell, + size: 16, + color: isFocused + ? ext.accent + : ext.textDisabled, + ), + ), + ), Text( event.isToday ? '🎉' : '${event.daysRemaining.abs()}', style: AppTypography.title1.copyWith( @@ -211,18 +298,20 @@ class _CountdownPageState extends ConsumerState { fontSize: event.isToday ? 24 : 28, ), ), - Text( - event.remainingLabel, - style: AppTypography.caption1.copyWith( - color: isPast ? ext.textDisabled : ext.textSecondary, - ), - ), ], ), + Text( + event.remainingLabel, + style: AppTypography.caption1.copyWith( + color: isPast ? ext.textDisabled : ext.textSecondary, + ), + ), ], ), - ), - ) + ], + ), + ), + ) .animate() .fadeIn(duration: 350.ms, delay: (index * 50).ms) .slideX(begin: 0.1, end: 0, duration: 350.ms, delay: (index * 50).ms); @@ -249,10 +338,22 @@ class _CountdownPageState extends ConsumerState { // ============================================================ void _showEventActions(CountdownEvent event) { + final isFocused = ref.read(countdownProvider).focusedEventId == event.id; + final isLiveActivitySupported = + ref.read(liveActivitySupportedProvider); + showCupertinoModalPopup( context: context, builder: (ctx) => CupertinoActionSheet( actions: [ + if (isLiveActivitySupported && !event.isPast) + CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(ctx); + ref.read(countdownProvider.notifier).focusEvent(event.id); + }, + child: Text(isFocused ? '关闭灵动岛' : '🔔 显示到灵动岛'), + ), CupertinoActionSheetAction( onPressed: () { Navigator.pop(ctx); diff --git a/lib/features/countdown/providers/countdown_provider.dart b/lib/features/countdown/providers/countdown_provider.dart index da7b7560..61a09f54 100644 --- a/lib/features/countdown/providers/countdown_provider.dart +++ b/lib/features/countdown/providers/countdown_provider.dart @@ -1,24 +1,40 @@ /// ============================================================ /// 闲言APP — 倒计时状态管理 /// 创建时间: 2026-05-02 -/// 更新时间: 2026-05-02 -/// 作用: 倒计时事件 CRUD + 持久化 + 排序 -/// 上次更新: 初始创建 +/// 更新时间: 2026-06-02 +/// 作用: 倒计时事件 CRUD + 持久化 + 排序 + 灵动岛聚焦模式 +/// 上次更新: 集成灵动岛Live Activity,支持聚焦倒计时实时显示 /// ============================================================ +import 'dart:async'; import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/storage/kv_storage.dart'; import '../../../core/utils/logger.dart'; +import '../../../core/services/device/live_activity_service.dart'; import '../models/countdown_models.dart'; class CountdownState { - const CountdownState({this.events = const [], this.isLoading = true}); + const CountdownState({ + this.events = const [], + this.isLoading = true, + this.focusedEventId, + }); final List events; final bool isLoading; + final String? focusedEventId; + + CountdownEvent? get focusedEvent { + if (focusedEventId == null) return null; + try { + return events.firstWhere((e) => e.id == focusedEventId); + } catch (_) { + return null; + } + } List get pinned => events.where((e) => e.isPinned).toList() @@ -32,23 +48,36 @@ class CountdownState { events.where((e) => !e.isPinned && e.isPast).toList() ..sort((a, b) => b.daysRemaining.compareTo(a.daysRemaining)); - CountdownState copyWith({List? events, bool? isLoading}) { + CountdownState copyWith({ + List? events, + bool? isLoading, + String? focusedEventId, + bool clearFocused = false, + }) { return CountdownState( events: events ?? this.events, isLoading: isLoading ?? this.isLoading, + focusedEventId: clearFocused ? null : (focusedEventId ?? this.focusedEventId), ); } } class CountdownNotifier extends Notifier { + Timer? _updateTimer; + @override CountdownState build() { Future.microtask(() => _loadEvents()).catchError((_) {}); + ref.onDispose(_onDispose); return const CountdownState(isLoading: false); } static const _key = 'countdown_events'; + // ============================================================ + // 数据持久化 + // ============================================================ + Future _loadEvents() async { try { final raw = KvStorage.getString(_key); @@ -99,6 +128,10 @@ class CountdownNotifier extends Notifier { ]; } + // ============================================================ + // 事件 CRUD + // ============================================================ + Future addEvent(CountdownEvent event) async { state = state.copyWith(events: [...state.events, event]); await _saveEvents(); @@ -109,13 +142,23 @@ class CountdownNotifier extends Notifier { events: state.events.map((e) => e.id == event.id ? event : e).toList(), ); await _saveEvents(); + if (state.focusedEventId == event.id) { + _updateLiveActivity(); + } } Future deleteEvent(String id) async { + final wasFocused = state.focusedEventId == id; state = state.copyWith( events: state.events.where((e) => e.id != id).toList(), + clearFocused: wasFocused, ); await _saveEvents(); + if (wasFocused) { + _endLiveActivity(); + _updateTimer?.cancel(); + _updateTimer = null; + } } Future togglePin(String id) async { @@ -126,6 +169,118 @@ class CountdownNotifier extends Notifier { ); await _saveEvents(); } + + // ============================================================ + // 灵动岛聚焦模式 + // ============================================================ + + Future focusEvent(String id) async { + final event = state.events.where((e) => e.id == id).firstOrNull; + if (event == null) return; + if (event.isPast) return; + + if (state.focusedEventId == id) { + await unfocusEvent(); + return; + } + + if (state.focusedEventId != null) { + _endLiveActivity(); + _updateTimer?.cancel(); + } + + state = state.copyWith(focusedEventId: id); + _startLiveActivity(); + + _updateTimer?.cancel(); + _updateTimer = Timer.periodic(const Duration(minutes: 1), (_) { + _updateLiveActivity(); + _checkEventArrival(); + }); + + Log.i('CountdownNotifier: 聚焦倒计时 "${event.title}"'); + } + + Future unfocusEvent() async { + _endLiveActivity(); + _updateTimer?.cancel(); + _updateTimer = null; + state = state.copyWith(clearFocused: true); + Log.i('CountdownNotifier: 取消聚焦倒计时'); + } + + void _startLiveActivity() { + final service = LiveActivityService.instance; + if (!service.isSupported) return; + + final event = state.focusedEvent; + if (event == null) return; + + final remainingMinutes = _calculateRemainingMinutes(event); + + service.startCountdownActivity( + title: event.title, + remainingMinutes: remainingMinutes, + emoji: event.emoji, + ); + } + + void _updateLiveActivity() { + final service = LiveActivityService.instance; + if (!service.isSupported || !service.hasActiveActivity) return; + + final event = state.focusedEvent; + if (event == null) { + _endLiveActivity(); + return; + } + + final remainingMinutes = _calculateRemainingMinutes(event); + + service.updateCountdownActivity( + title: event.title, + remainingMinutes: remainingMinutes, + emoji: event.emoji, + ); + } + + void _endLiveActivity() { + final service = LiveActivityService.instance; + if (!service.isSupported) return; + service.endCountdownActivity(); + } + + int _calculateRemainingMinutes(CountdownEvent event) { + final now = DateTime.now(); + final target = DateTime( + event.targetDate.year, + event.targetDate.month, + event.targetDate.day, + 23, + 59, + 59, + ); + final diff = target.difference(now); + return diff.inMinutes.clamp(0, diff.inMinutes); + } + + void _checkEventArrival() { + final event = state.focusedEvent; + if (event == null) return; + + if (event.isPast || event.isToday) { + _endLiveActivity(); + _updateTimer?.cancel(); + _updateTimer = null; + state = state.copyWith(clearFocused: true); + Log.i('CountdownNotifier: 倒计时 "${event.title}" 已到达,结束灵动岛'); + } + } + + void _onDispose() { + _updateTimer?.cancel(); + _endLiveActivity(); + } } final countdownProvider = NotifierProvider( diff --git a/lib/features/discover/presentation/pages/tool/rss_reader_page.dart b/lib/features/discover/presentation/pages/tool/rss_reader_page.dart index 1b811477..092a04d0 100644 --- a/lib/features/discover/presentation/pages/tool/rss_reader_page.dart +++ b/lib/features/discover/presentation/pages/tool/rss_reader_page.dart @@ -50,6 +50,12 @@ class _RssReaderPageState extends State { /// 选中的文章(查看详情) RssFeedItem? _selectedItem; + /// 阅读模式全文 + RssFullTextResult? _fullTextResult; + + /// 是否正在加载全文 + bool _isLoadingFullText = false; + /// 当前分类筛选 RssCategory? _selectedCategory; @@ -432,6 +438,7 @@ class _RssReaderPageState extends State { Widget _buildArticleDetail(AppThemeExtension ext) { final item = _selectedItem!; + final isReadingMode = _fullTextResult != null && _fullTextResult!.success; return CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -470,12 +477,18 @@ class _RssReaderPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - item.title, - style: AppTypography.title2.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.bold, - ), + Row( + children: [ + Expanded( + child: Text( + isReadingMode ? (_fullTextResult!.title ?? item.title) : item.title, + style: AppTypography.title2.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), if (item.sourceTitle != null || item.author != null || @@ -484,8 +497,23 @@ class _RssReaderPageState extends State { _buildMetaRow(ext, item), ], const SizedBox(height: AppSpacing.md), - if (item.description != null && - item.description!.isNotEmpty) + if (_isLoadingFullText) ...[ + const Center(child: CupertinoActivityIndicator()), + const SizedBox(height: AppSpacing.md), + ] else if (isReadingMode) ...[ + Text( + _fullTextResult!.content ?? '', + style: AppTypography.body.copyWith( + color: ext.textPrimary, + height: 1.8, + ), + ), + if (_fullTextResult!.images.isNotEmpty) ...[ + const SizedBox(height: AppSpacing.lg), + _buildImageGallery(ext, _fullTextResult!.images), + ], + ] else if (item.description != null && + item.description!.isNotEmpty) ...[ Text( HtmlUtils.stripTags(item.description!), style: AppTypography.body.copyWith( @@ -493,33 +521,66 @@ class _RssReaderPageState extends State { height: 1.7, ), ), + ], + const SizedBox(height: AppSpacing.lg), if (item.link != null) ...[ - const SizedBox(height: AppSpacing.lg), - SizedBox( - width: double.infinity, - child: CupertinoButton( - color: ext.accent, - borderRadius: AppRadius.lgBorder, - onPressed: () => _launchUrl(item.link!), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - CupertinoIcons.globe, - size: 16, - color: ext.textOnAccent, + Row( + children: [ + Expanded( + child: CupertinoButton( + color: ext.accent, + borderRadius: AppRadius.lgBorder, + onPressed: () => _launchUrl(item.link!), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.globe, + size: 16, + color: ext.textOnAccent, + ), + const SizedBox(width: AppSpacing.xs), + Text( + '在浏览器中打开', + style: AppTypography.subhead.copyWith( + color: ext.textOnAccent, + fontWeight: FontWeight.w600, + ), + ), + ], ), - const SizedBox(width: AppSpacing.xs), - Text( - '在浏览器中打开', - style: AppTypography.subhead.copyWith( - color: ext.textOnAccent, - fontWeight: FontWeight.w600, - ), - ), - ], + ), ), - ), + const SizedBox(width: AppSpacing.sm), + if (!isReadingMode) + CupertinoButton( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + color: ext.accent.withValues(alpha: 0.12), + borderRadius: AppRadius.lgBorder, + onPressed: _loadFullText, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.doc_text, + size: 16, + color: ext.accent, + ), + const SizedBox(width: AppSpacing.xs), + Text( + '📖 阅读模式', + style: AppTypography.subhead.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], ), ], ], @@ -532,6 +593,71 @@ class _RssReaderPageState extends State { ); } + /// 加载全文内容(阅读模式) + Future _loadFullText() async { + if (_selectedItem?.link == null) return; + setState(() => _isLoadingFullText = true); + try { + final result = await RssService.fetchFullText(_selectedItem!.link!); + if (mounted) { + setState(() { + _fullTextResult = result; + _isLoadingFullText = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _isLoadingFullText = false); + _showToast('📖 全文加载失败'); + } + } + } + + /// 构建图片画廊 + Widget _buildImageGallery(AppThemeExtension ext, List images) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '📎 文中图片', + style: AppTypography.subhead.copyWith( + color: ext.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.sm, + runSpacing: AppSpacing.sm, + children: images.map((url) { + return GestureDetector( + onTap: () => _launchUrl(url), + child: ClipRRect( + borderRadius: AppRadius.mdBorder, + child: CachedNetworkImage( + imageUrl: url, + width: 100, + height: 100, + fit: BoxFit.cover, + placeholder: (_, __) => Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: ext.bgSecondary, + borderRadius: AppRadius.mdBorder, + ), + child: const CupertinoActivityIndicator(radius: 8), + ), + errorWidget: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ); + }).toList(), + ), + ], + ); + } + /// ── 文章元信息行 ── Widget _buildMetaRow(AppThemeExtension ext, RssFeedItem item) { return Container( @@ -896,7 +1022,11 @@ class _RssReaderPageState extends State { } void _closeDetail() { - setState(() => _selectedItem = null); + setState(() { + _selectedItem = null; + _fullTextResult = null; + _isLoadingFullText = false; + }); } void _backToSubscriptions() { diff --git a/lib/features/discover/services/rss_service.dart b/lib/features/discover/services/rss_service.dart index c7745f4e..87b5caef 100644 --- a/lib/features/discover/services/rss_service.dart +++ b/lib/features/discover/services/rss_service.dart @@ -561,4 +561,175 @@ class RssService { .replaceAll('"', '"') .replaceAll('&', '&'); } + + // ============================================================ + // 全文提取(阅读模式) + // ============================================================ + + /// 从文章URL提取全文内容(阅读模式) + /// + /// 使用简易 Readability 算法: + /// 1. 获取网页HTML + /// 2. 移除导航/侧边栏/页脚等非正文区域 + /// 3. 提取最可能是正文的区域 + /// 4. 清理HTML标签,返回纯文本 + static Future fetchFullText(String url) async { + try { + final response = await _dio.get(url); + final html = response.data ?? ''; + if (html.isEmpty) { + return const RssFullTextResult(error: '页面内容为空'); + } + + final title = _extractTitle(html); + final content = _extractContent(html); + final images = _extractContentImages(html); + + if (content.isEmpty) { + return RssFullTextResult( + error: '无法提取正文内容', + title: title, + ); + } + + return RssFullTextResult( + success: true, + title: title, + content: content, + images: images, + sourceUrl: url, + ); + } catch (e) { + Log.e('RssService', '全文提取失败 [$url]: $e'); + return RssFullTextResult(error: '加载失败: $e'); + } + } + + /// 提取页面标题 + static String _extractTitle(String html) { + final ogTitle = RegExp(r']*property=["\x27]og:title["\x27][^>]*content=["\x27]([^"\x27]*)["\x27]', caseSensitive: false) + .firstMatch(html); + if (ogTitle != null && ogTitle.group(1)!.isNotEmpty) { + return _decodeHtmlEntities(ogTitle.group(1)!); + } + final titleMatch = RegExp(r']*>(.*?)', caseSensitive: false, dotAll: true) + .firstMatch(html); + if (titleMatch != null && titleMatch.group(1)!.isNotEmpty) { + return _decodeHtmlEntities(titleMatch.group(1)!.trim()); + } + return ''; + } + + /// 提取正文内容(简易 Readability) + static String _extractContent(String html) { + var cleaned = html; + + // 移除脚本和样式 + cleaned = cleaned.replaceAll( + RegExp(r']*>.*?', caseSensitive: false, dotAll: true), + '', + ); + cleaned = cleaned.replaceAll( + RegExp(r']*>.*?', caseSensitive: false, dotAll: true), + '', + ); + cleaned = cleaned.replaceAll( + RegExp(r']*>.*?', caseSensitive: false, dotAll: true), + '', + ); + cleaned = cleaned.replaceAll( + RegExp(r']*>.*?', caseSensitive: false, dotAll: true), + '', + ); + cleaned = cleaned.replaceAll( + RegExp(r']*>.*?', caseSensitive: false, dotAll: true), + '', + ); + cleaned = cleaned.replaceAll( + RegExp(r']*>.*?', caseSensitive: false, dotAll: true), + '', + ); + cleaned = cleaned.replaceAll( + RegExp(r']*>.*?', caseSensitive: false, dotAll: true), + '', + ); + + // 查找 article 标签 + final articleMatch = RegExp(r']*>(.*?)', caseSensitive: false, dotAll: true) + .firstMatch(cleaned); + if (articleMatch != null) { + cleaned = articleMatch.group(1)!; + } else { + // 查找 class 含 article/content/post/entry 的 div + final contentDiv = RegExp( + r']*class=["\x27][^\x27]*(?:article|content|post-body|entry-content|post-content|story-body|article-body|rich-text|markdown-body)[^\x27]*["\x27][^>]*>(.*?)', + caseSensitive: false, + dotAll: true, + ).firstMatch(cleaned); + if (contentDiv != null) { + cleaned = contentDiv.group(1)!; + } + } + + // 清理HTML标签,保留段落结构 + var text = cleaned; + text = text.replaceAll(RegExp(r'', caseSensitive: false), '\n'); + text = text.replaceAll(RegExp(r']*>', caseSensitive: false), '\n'); + text = text.replaceAll(RegExp(r'

', caseSensitive: false), '\n'); + text = text.replaceAll(RegExp(r']*>', caseSensitive: false), '\n\n'); + text = text.replaceAll(RegExp(r'', caseSensitive: false), '\n'); + text = text.replaceAll(RegExp(r']*>', caseSensitive: false), '• '); + text = text.replaceAll(RegExp(r']*>', caseSensitive: false), '\n> '); + text = text.replaceAll(RegExp(r'<[^>]*>'), ''); + text = text.replaceAll(RegExp(r' '), ' '); + text = text.replaceAll(RegExp(r'&'), '&'); + text = text.replaceAll(RegExp(r'<'), '<'); + text = text.replaceAll(RegExp(r'>'), '>'); + text = text.replaceAll(RegExp(r'"'), '"'); + text = text.replaceAll(RegExp(r'&#\d+;'), ''); + text = text.replaceAll(RegExp(r'\n{3,}'), '\n\n'); + + return text.trim(); + } + + /// 提取正文中的图片URL + static List _extractContentImages(String html) { + final imgRegex = RegExp(r']+src\s*=\s*["\x27]([^"\x27]+)["\x27]', dotAll: true); + return imgRegex + .allMatches(html) + .map((m) => m.group(1) ?? '') + .where((url) => url.isNotEmpty && !url.endsWith('.svg') && !url.contains('avatar') && !url.contains('icon') && !url.contains('logo')) + .take(10) + .toList(); + } + + /// HTML实体解码 + static String _decodeHtmlEntities(String text) { + return text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(' ', ' '); + } +} + +/// 全文提取结果 +class RssFullTextResult { + const RssFullTextResult({ + this.success = false, + this.title, + this.content, + this.images = const [], + this.sourceUrl, + this.error, + }); + + final bool success; + final String? title; + final String? content; + final List images; + final String? sourceUrl; + final String? error; } diff --git a/lib/features/file_transfer/presentation/pages/transfer_chat_file_send.dart b/lib/features/file_transfer/presentation/pages/transfer_chat_file_send.dart index a6916e23..d525a56c 100644 --- a/lib/features/file_transfer/presentation/pages/transfer_chat_file_send.dart +++ b/lib/features/file_transfer/presentation/pages/transfer_chat_file_send.dart @@ -370,7 +370,7 @@ extension _TransferChatFileSendExt on _TransferChatPageState { Future _pickFile() async { try { - final result = await FilePicker.pickFiles(allowMultiple: true); + final result = await FilePicker.pickFiles(); if (result != null && result.files.isNotEmpty) { final paths = result.files .where((f) => f.path != null) diff --git a/lib/features/mine/settings/services/font_download_service.dart b/lib/features/mine/settings/services/font_download_service.dart index 3aee8baf..a2de59d8 100644 --- a/lib/features/mine/settings/services/font_download_service.dart +++ b/lib/features/mine/settings/services/font_download_service.dart @@ -304,7 +304,6 @@ class FontDownloadService { final result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['ttf', 'otf'], - allowMultiple: true, withData: true, ); diff --git a/lib/shared/widgets/containers/deferred_builder.dart b/lib/shared/widgets/containers/deferred_builder.dart new file mode 100644 index 00000000..5927e77b --- /dev/null +++ b/lib/shared/widgets/containers/deferred_builder.dart @@ -0,0 +1,39 @@ +/// ============================================================ +/// 闲言APP — 延迟渲染包装组件 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 将子组件(如syncfusion chart)延迟到postFrameCallback渲染 +/// 避免chart在build阶段触发markNeedsLayout导致卡死 +/// 上次更新: 初始创建 +/// ============================================================ + +import 'package:flutter/widgets.dart'; + +class DeferredBuilder extends StatefulWidget { + const DeferredBuilder({super.key, required this.builder}); + + final WidgetBuilder builder; + + @override + State createState() => _DeferredBuilderState(); +} + +class _DeferredBuilderState extends State { + bool _ready = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() => _ready = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!_ready) { + return const SizedBox.expand(); + } + return widget.builder(context); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 53ac82a2..50061c02 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -15,16 +14,15 @@ #include #include #include +#include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); - g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); - bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); g_autoptr(FlPluginRegistrar) desktop_drop_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); @@ -46,10 +44,16 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) rive_native_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RiveNativePlugin"); rive_native_plugin_register_with_registrar(rive_native_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); + screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b71105df..0ac25a19 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux - bitsdojo_window_linux desktop_drop file_selector_linux flutter_secure_storage_linux @@ -12,8 +11,10 @@ list(APPEND FLUTTER_PLUGIN_LIST gtk record_linux rive_native + screen_retriever_linux sqlite3_flutter_libs url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index fc3fc07d..eed0f84d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import app_links import audioplayers_darwin import battery_plus -import bitsdojo_window_macos import connectivity_plus import desktop_drop import device_info_plus @@ -19,7 +18,7 @@ import flutter_blue_plus_darwin import flutter_image_compress_macos import flutter_inappwebview_macos import flutter_local_notifications -import flutter_secure_storage_macos +import flutter_secure_storage_darwin import flutter_tts import flutter_webrtc import gal @@ -32,6 +31,7 @@ import pro_image_editor import quill_native_bridge_macos import record_macos import rive_native +import screen_retriever_macos import share_plus import shared_preferences_foundation import speech_to_text @@ -41,12 +41,12 @@ import url_launcher_macos import video_compress import video_player_avfoundation import wakelock_plus +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) - BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) @@ -57,7 +57,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) @@ -70,6 +70,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin")) @@ -79,4 +80,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index b91d6039..02cb92c4 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,11 @@ PODS: - FlutterMacOS - connectivity_plus (0.0.1): - FlutterMacOS + - CwlCatchException (2.2.1): + - CwlCatchExceptionSupport (~> 2.2.1) + - CwlCatchExceptionSupport (2.2.1) + - desktop_drop (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - file_picker (0.0.1): @@ -15,6 +20,8 @@ PODS: - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS + - flutter_app_group_directory (0.0.1): + - FlutterMacOS - flutter_blue_plus_darwin (0.0.2): - Flutter - FlutterMacOS @@ -55,13 +62,21 @@ PODS: - FlutterMacOS - quill_native_bridge_macos (0.0.1): - FlutterMacOS - - record_macos (1.2.0): + - record_macos (1.2.1): + - FlutterMacOS + - rive_native (0.0.1): + - FlutterMacOS + - screen_retriever_macos (0.0.1): - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - speech_to_text (7.2.0): + - CwlCatchException + - Flutter + - FlutterMacOS - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS @@ -100,15 +115,19 @@ PODS: - wakelock_plus (0.0.1): - FlutterMacOS - WebRTC-SDK (144.7559.01) + - window_manager (0.5.0): + - FlutterMacOS DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin`) - battery_plus (from `Flutter/ephemeral/.symlinks/plugins/battery_plus/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/darwin`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - flutter_app_group_directory (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_group_directory/macos`) - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -126,17 +145,23 @@ DEPENDENCIES: - pro_image_editor (from `Flutter/ephemeral/.symlinks/plugins/pro_image_editor/macos`) - quill_native_bridge_macos (from `Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos`) - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) + - rive_native (from `Flutter/ephemeral/.symlinks/plugins/rive_native/macos`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: trunk: + - CwlCatchException + - CwlCatchExceptionSupport - OrderedSet - sqlite3 - WebRTC-SDK @@ -150,12 +175,16 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/battery_plus/macos connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + desktop_drop: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/darwin file_selector_macos: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + flutter_app_group_directory: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_app_group_directory/macos flutter_blue_plus_darwin: :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin flutter_image_compress_macos: @@ -190,10 +219,16 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos record_macos: :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos + rive_native: + :path: Flutter/ephemeral/.symlinks/plugins/rive_native/macos + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + speech_to_text: + :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin sqflite_darwin: :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin sqlite3_flutter_libs: @@ -206,15 +241,21 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin wakelock_plus: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 battery_plus: f51ad29136e025b714b96f7d096f44f604615da7 connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e + CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a + CwlCatchExceptionSupport: 54ccab8d8c78907b57f99717fb19d4cc3bce02dc + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + flutter_app_group_directory: 14eb7e7a2b0e30a6a68bb855197b4ed6f5063e55 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_image_compress_macos: e68daf54bb4bf2144c580fd4d151c949cbf492f0 flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d @@ -232,9 +273,12 @@ SPEC CHECKSUMS: package_info_plus: f0052d280d17aa382b932f399edf32507174e870 pro_image_editor: e4f2ca0bcf5d755d81620d531b00e0906ccaa0ae quill_native_bridge_macos: 2b005cb56902bb740e0cd9620aa399dfac6b4882 - record_macos: 7f227161b93c49e7e34fe681c5891c8622c8cc8b + record_macos: 5d55909f9650314be6424ffd6b123ac75a08c3c1 + rive_native: 7c43020540833c8258564b726317daf39d2c3bda + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + speech_to_text: 3b313d98516d3d0406cea424782ec25470c59d19 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab @@ -243,6 +287,7 @@ SPEC CHECKSUMS: video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52 wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b WebRTC-SDK: ab9b5319e458c2bfebdc92b3600740da35d5630d + window_manager: b729e31d38fb04905235df9ea896128991cad99e PODFILE CHECKSUM: 89c84cf5c2351c1e554c6dea18d31a879fc3a19e diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa6..4cc2ac93 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -6,6 +6,8 @@ $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) + CFBundleDisplayName + 闲言 CFBundleIconFile CFBundleIdentifier @@ -13,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - $(PRODUCT_NAME) + 闲言 CFBundlePackageType APPL CFBundleShortVersionString diff --git a/pubspec_macos.yaml b/pubspec_macos.yaml new file mode 100644 index 00000000..4bf23d23 --- /dev/null +++ b/pubspec_macos.yaml @@ -0,0 +1,321 @@ +# ============================================================ +# 闲言APP (Xianyan) — Flutter 版 pubspec.yaml +# 创建时间: 2026-04-20 +# 更新时间: 2026-05-30 +# 作用: 项目依赖与资源配置 +# 上次更新: 新增webfeed RSS订阅解析库 +# **python**: C:\Users\无书\AppData\Local\Python\pythoncore-3.14-64\python.exe +# ============================================================ + +name: xianyan +description: "闲言 — 文字阅读更纯粹。句子阅读 + 壁纸制作 APP" +publish_to: 'none' +version: 6.6.2+26060202 +# 年月日-次 + +environment: + sdk: ^3.11.5 + +# ============================================================ +# 依赖 — Phase 0 必装 +# 部分库引用本地 packages 目录 +# ============================================================ +dependencies: + flutter: + sdk: flutter + + # iOS 风格图标 + cupertino_icons: ^1.0.8 # iOS风格图标库 + + # --- 状态管理 + 依赖注入 --- + flutter_riverpod: ^3.0.0 # 响应式状态管理+依赖注入 + riverpod_annotation: ^4.0.0 # Riverpod代码生成注解 + + # --- 路由 --- + go_router: ^17.2.3 # 声明式路由导航(纯Dart-鸿蒙零适配) + + # --- 网络请求 --- + dio: ^5.4.0 # HTTP客户端+拦截器 + dio_cache_interceptor: ^3.5.0 # Dio HTTP缓存拦截器 + http_cache_file_store: ^2.0.1 # 文件系统缓存存储 + + # --- 本地数据库 --- + drift: ^2.16.0 # 类型安全SQLite ORM + sqlite3_flutter_libs: ^0.5.0 # SQLite原生库绑定 + + # --- 数据模型 --- + freezed_annotation: ^3.0.0 # 不可变数据类注解 + json_annotation: ^4.9.0 # JSON序列化注解 + + # --- KV 存储 --- + shared_preferences: ^2.5.5 # 轻量KV持久化 + flutter_secure_storage: ^10.2.0 # 加密安全存储 + hive_ce: ^2.0.0 # 高性能NoSQL数据库(社区维护版) + hive_flutter: ^1.1.0 # Hive Flutter适配 + + # --- 文件路径 --- + path_provider: ^2.1.5 # 系统目录路径获取 + path: ^1.9.0 # 路径操作工具 + + # --- 工具 --- + uuid: ^4.5.0 # UUID生成器 + intl: ^0.20.2 # 国际化+日期格式化 + timeago: ^3.7.0 # 相对时间格式化(国际化) + logger: ^2.5.0 # 分级日志输出 + collection: ^1.19.0 # 集合操作扩展 + dartx: ^1.2.0 # 集合安全扩展方法(firstOrNull/getOrNull等) + syncfusion_flutter_charts: ^28.1.36 # Syncfusion图表库(替代fl_chart) + + # --- 设备信息 --- + package_info_plus: ^10.1.0 # 应用包信息读取 + connectivity_plus: ^7.1.1 # 网络连接状态监听 + device_info_plus: ^13.1.0 # 设备硬件信息读取 + + # --- 日历同步 --- + device_calendar: ^4.3.3 # 跨平台日历事件读写 + + # --- 权限 --- + permission_handler: ^12.0.1 # 运行时权限请求 + + # --- 本地通知 --- + flutter_local_notifications: ^21.0.0 # 本地推送通知 + + # --- 后台任务调度 --- + workmanager: ^0.9.0 # 后台任务调度 + + # --- 外部链接 --- + url_launcher: ^6.3.2 # 打开外部URL/应用 + app_links: ^7.0.0 # 深度链接处理 + + # --- 快捷操作 --- + quick_actions: ^1.1.0 # 主屏幕快捷操作(iOS Quick Actions / Android App Shortcuts) + # 鸿蒙端通过EntryAbility.ets shortcuts + MethodChannel自行实现 + + # --- 桌面小组件 --- + home_widget: ^0.9.1 # iOS/Android桌面小组件 + + # --- iOS 26 Liquid Glass 组件 --- + liquid_glass_widgets: ^0.11.0 # iOS26液态玻璃组件库 + liquid_glass_easy: ^1.1.1 # 液态玻璃效果封装 + + # --- 底部面板 + Hero 动画 --- + stupid_simple_sheet: ^0.9.1+1 # 简易底部弹出面板 + heroine: ^0.7.2 # Hero过渡动画增强 + + file_picker: ^11.0.0 # 文件选择器 + image_picker: ^1.2.2 # 相机/相册选图 + adaptive_palette: ^3.0.0 # 图片主色提取+流体背景 + + # --- UI 基础 --- + badges: ^3.2.0 # 角标/徽章组件 + google_fonts: ^8.1.0 # Google字体加载 + cached_network_image: ^3.3.0 # 网络图片缓存+占位 + flutter_cache_manager: ^3.3.0 # 文件缓存管理 + shimmer: ^3.0.0 # 骨架屏加载占位 + + # --- 分享 + 导出 --- + share_plus: ^13.1.0 # 系统分享面板 + qr_flutter: ^4.1.0 # 二维码渲染 + gal: ^2.3.0 # 保存图片/视频到相册 + archive: ^4.0.0 # ZIP压缩/解压 + crypto: ^3.0.0 # 加密哈希算法 + encrypt: ^5.0.3 # 对称/非对称加密 + mailer: ^7.1.0 # SMTP邮件发送 + + # --- 图片处理 --- + image: ^4.3.0 # 图片解码/编码/变换 + + # --- 图片编辑器 --- + pro_image_editor: ^12.4.4 # v12.4.4 | 图片编辑器核心(官方版) + + # --- 桌面端增强 --- + desktop_drop: ^0.5.0 # 桌面端文件拖放接收 + window_manager: ^0.5.1 # 桌面端窗口管理(替代bitsdojo_window) + + # --- 异常捕获 --- + catcher_2: ^2.1.9 # 全局异常捕获+上报 + + # --- SVG 渲染 --- + flutter_svg: ^2.0.0 # SVG图片渲染 + + # --- 富文本编辑器 --- + flutter_quill: ^11.5.0 # Quill富文本编辑器 + + # --- 虚线边框 --- + dotted_border: ^3.1.0 # 虚线/点线边框装饰 + + # --- 颜色选择器 --- + flex_color_picker: ^3.8.0 # HSL颜色选择器 + + # --- 键盘可见性 --- + flutter_keyboard_visibility: ^6.0.0 # 键盘可见性监听(替代MediaQuery轮询) + + # --- 屏幕适配 --- + flutter_screenutil: ^5.9.0 # 屏幕尺寸适配 + + # --- 动画 --- + rive: ^0.14.7 # Rive交互式动画引擎 + flutter_animate: ^4.5.0 # 声明式动画库 + flutter_card_swiper: ^7.2.0 # 卡片滑动切换 + animations: ^2.0.11 # Material过渡动画 + lottie: ^3.3.0 # Lottie动画播放 + confetti: ^0.8.0 # 撒花/彩纸效果 + animate_do: ^5.1.0 # 常用入场/出场动画 + + # --- 交互增强 --- + custom_refresh_indicator: ^4.0.1 # 自定义下拉刷新 + + # --- 列表交互 --- + flutter_slidable: ^4.0.3 # 列表项滑动操作 + flutter_sticky_header: ^0.8.0 # 粘性头部 + flutter_staggered_animations: ^1.1.1 # 列表交错入场动画 + value_layout_builder: ^0.5.0 # 值变化触发布局重建 + + # --- 内容渲染 --- + flutter_markdown_plus: ^1.0.1 # Markdown渲染 + flutter_html: ^3.0.0-beta.2 # HTML内容渲染 + + # --- RSS订阅 --- + rss_dart: ^1.0.12 # RSS/Atom订阅源解析(Dart3兼容webfeed分支) + + # --- 拼音转换 --- + pinyin: ^3.3.0 # 汉字转拼音 + + # --- 语音朗读 --- + flutter_tts: ^4.2.0 # TTS文本转语音朗读 + + # --- 语音识别 --- + speech_to_text: ^7.0.0 # 语音转文字 + + # --- 灵动岛/实时活动 --- + live_activities: ^2.0.0 # 灵动岛/实时活动 + + # --- iOS风格组件 --- + pull_down_button: ^0.10.1 # iOS下拉菜单按钮 + + # --- 布局增强 --- + sliver_tools: ^0.2.12 # Sliver工具集 + flutter_staggered_grid_view: ^0.7.0 # 瀑布流网格 + visibility_detector: ^0.4.0+2 # 组件可见性检测 + + # --- 触觉反馈 --- + flutter_vibrate: + git: + url: https://gitcode.com/openharmony-sig/fluttertpc_flutter_vibrate.git # 跨平台触觉反馈(iOS/Android/HarmonyOS) + + # --- 提示反馈 --- + bot_toast: ^4.1.0 # Toast/通知弹窗 + + # --- Shader效果 --- + flutter_shaders_ui: ^0.1.0 # Fragment Shader效果 + flutter_tilt: ^4.0.0 # 3D倾斜交互效果 + flutter_3d_controller: 2.3.0 # 3D模型加载控制 + flutter_advanced_canvas_editor: 2.1.0 # 高级画布编辑器 + flutter_spritesheet_animation: ^1.0.1 # 精灵图帧动画 + image_size_getter: ^2.4.1 # 图片尺寸读取(无需解码) + extended_image: ^10.0.1 # 图片缓存+缩放+裁剪 + photo_view: ^0.15.0 # 图片缩放/平移查看 + flutter_image_compress: ^2.4.0 # 图片压缩(保持EXIF) + + vector_math: any # 向量数学运算 + wakelock_plus: ^1.4.0 # 屏幕常亮控制 + audioplayers: ^6.5.0 # 音频播放 + record: ^6.0.0 # 录音 + video_compress: ^3.1.2 # 视频压缩 + video_player: ^2.10.0 # 视频播放 + local_auth: ^3.0.1 # 生物识别认证 + sensors_plus: ^6.1.0 # 加速度传感器(摇一摇) + battery_plus: ^7.0.0 # 电池状态监听 + + # --- 文件传输助手 --- + shelf: ^1.4.0 # HTTP服务器框架 + shelf_router: ^1.1.0 # 路由中间件 + shelf_web_socket: ^3.0.0 # WebSocket支持 + network_info_plus: ^8.1.0 # WiFi网络信息 + flutter_webrtc: ^1.4.0 # WebRTC音视频通信 + web_socket_channel: ^3.0.3 # WebSocket客户端 + flutter_blue_plus: ^2.1.0 # 蓝牙BLE通信 + flutter_nfc_kit: ^3.6.0 # NFC读写 + mime: ^2.0.0 # MIME类型识别 + mobile_scanner: ^7.1.4 # 二维码/条形码扫描 + basic_utils: ^5.7.0 # 通用工具集(Base64/ASN1) + wifi_iot: ^0.3.19 # WiFi IoT设备连接 + nearby_service: ^0.2.1 # 近场设备发现+通信 + + flutter_localizations: + sdk: flutter # Flutter国际化支持 + timezone: ^0.11.0 # 时区数据库 + sqflite: ^2.4.1 # SQLite轻量数据库 + cross_file: any # 跨平台文件抽象 + receive_sharing_intent: + git: + url: "https://gitcode.com/openharmony-sig/fluttertpc_receive_sharing_intent.git" + ref: "br_v1.8.1_ohos" + + +# ============================================================ +# 开发依赖 +# ============================================================ + +dev_dependencies: + flutter_test: + sdk: flutter + + # 代码生成 + build_runner: ^2.6.0 # 代码生成运行器 + freezed: ^3.2.0 # 不可变数据类生成 + json_serializable: ^6.11.0 # JSON序列化代码生成 + drift_dev: ^2.31.0 # Drift数据库代码生成 + riverpod_generator: ^4.0.0 # Riverpod Provider代码生成 + + # 代码规范 + flutter_lints: ^5.0.0 # Flutter lint规则 + riverpod_lint: ^3.0.0 # Riverpod专用lint + custom_lint: ^0.8.0 # 自定义lint插件 + + # 测试 + mocktail: ^1.0.0 # Mock测试库 + + +# ============================================================ +# 依赖覆写 — 解决版本冲突 (MacBook Pro 端精简版) +# 1. liquid_glass_widgets与flutter_test的meta版本冲突 +# 2. device_calendar ^4.3.3 依赖 timezone ^0.9.0(<0.10.0) +# 但 flutter_local_notifications 依赖 timezone ^0.11.0(<0.12.0) +# timezone 0.9→0.11 API兼容(仅时区数据更新),强制使用^0.11.0 +# ============================================================ +dependency_overrides: + meta: ^1.17.0 + web: ^1.1.0 + timezone: ^0.11.0 + win32: ^6.0.1 + +# ============================================================ +# Flutter 配置 +# ============================================================ +flutter: + uses-material-design: true + + assets: + - assets/animations/ + - assets/images/ + - assets/templates/resized/ + - assets/svgs/ + - assets/svgs/categories/ + - assets/svgs/editor/ + - assets/spritesheets/builtin/ + - assets/spritesheets/builtin/emotions/ + - assets/spritesheets/builtin/gestures/ + - assets/spritesheets/builtin/nature/ + - assets/spritesheets/builtin/festive/ + - assets/models/3d/ + - assets/models/thumbnails/ + - assets/model_catalog.json + - assets/data/ + - assets/data/leisure/ + - assets/sounds/ + - assets/sounds/sfx/ + - assets/sounds/sfx/sfx/ + - assets/sounds/sfx/sfx_soft/ + - assets/sounds/sfx/sfx_crisp/ + - assets/shaders/ diff --git a/tools/xianyan_lint/lib/src/rules/double_angle_brackets.dart b/tools/xianyan_lint/lib/src/rules/double_angle_brackets.dart new file mode 100644 index 00000000..cd6c6ab5 --- /dev/null +++ b/tools/xianyan_lint/lib/src/rules/double_angle_brackets.dart @@ -0,0 +1,46 @@ +/// ============================================================ +/// 闲言APP — 双书名号检测规则 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 检测字符串中的双书名号《《,预防多人协作风格不一致 +/// 上次更新: 初始创建 +/// ============================================================ + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class DoubleAngleBracketsRule extends DartLintRule { + DoubleAngleBracketsRule() : super(code: _code); + + static const _code = LintCode( + name: 'double_angle_brackets', + problemMessage: '检测到双书名号《《,应为单书名号《》', + correctionMessage: '将《《替换为《,确保书名号正确配对', + ); + + @override + void run( + CustomLintResolver resolver, + DiagnosticReporter reporter, + CustomLintContext context, + ) { + context.registry.addSimpleStringLiteral((node) { + _check(node.value, node, reporter); + }); + + context.registry.addStringInterpolation((node) { + for (final element in node.elements) { + if (element is InterpolationString) { + _check(element.value, element, reporter); + } + } + }); + } + + void _check(String value, AstNode node, DiagnosticReporter reporter) { + if (value.contains('《《')) { + reporter.atNode(node, _code); + } + } +} diff --git a/tools/xianyan_lint/lib/src/rules/hardcoded_color.dart b/tools/xianyan_lint/lib/src/rules/hardcoded_color.dart new file mode 100644 index 00000000..5c8ab9fa --- /dev/null +++ b/tools/xianyan_lint/lib/src/rules/hardcoded_color.dart @@ -0,0 +1,53 @@ +/// ============================================================ +/// 闲言APP — 硬编码颜色检测规则 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 检测非主题系统的硬编码颜色值,确保使用统一设计令牌 +/// 上次更新: 初始创建 +/// ============================================================ + +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class HardcodedColorRule extends DartLintRule { + HardcodedColorRule() : super(code: _code); + + static const _code = LintCode( + name: 'hardcoded_color', + problemMessage: '检测到硬编码颜色值,应使用主题系统变量', + correctionMessage: '使用 AppTheme.ext(context) 获取颜色,或添加到 app_colors.dart', + ); + + static final _hexColorPattern = RegExp(r'0x[0-9A-Fa-f]{8}'); + + static const _excludedFiles = [ + 'app_colors.dart', + 'app_theme.dart', + 'color_weak_filter.dart', + 'glass_tokens.dart', + 'app_radius.dart', + 'app_shadow.dart', + ]; + + @override + void run( + CustomLintResolver resolver, + DiagnosticReporter reporter, + CustomLintContext context, + ) { + final filePath = resolver.path; + if (_excludedFiles.any((e) => filePath.contains(e))) return; + + context.registry.addInstanceCreationExpression((node) { + final typeName = node.constructorName.type.name.lexeme; + if (typeName != 'Color') return; + + for (final arg in node.argumentList.arguments) { + final argStr = arg.toSource(); + if (_hexColorPattern.hasMatch(argStr)) { + reporter.atNode(arg, _code); + } + } + }); + } +} diff --git a/tools/xianyan_lint/lib/xianyan_lint.dart b/tools/xianyan_lint/lib/xianyan_lint.dart new file mode 100644 index 00000000..b89200be --- /dev/null +++ b/tools/xianyan_lint/lib/xianyan_lint.dart @@ -0,0 +1,22 @@ +/// ============================================================ +/// 闲言APP — 自定义Lint规则入口 +/// 创建时间: 2026-06-02 +/// 更新时间: 2026-06-02 +/// 作用: 注册所有自定义lint规则插件 +/// 上次更新: 移除硬编码中文检测规则 +/// ============================================================ + +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import 'src/rules/double_angle_brackets.dart'; +import 'src/rules/hardcoded_color.dart'; + +PluginBase createPlugin() => _XianyanLintPlugin(); + +class _XianyanLintPlugin extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => [ + DoubleAngleBracketsRule(), + HardcodedColorRule(), + ]; +} diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index b749bfef..9d28c3c3 100644 Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index 88cfd48d..4828b8cf 100644 Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index eb9b4d76..9d28c3c3 100644 Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png index d69c5669..4828b8cf 100644 Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 6d4a4ddb..e5ce9038 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -23,10 +22,12 @@ #include #include #include +#include #include #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( @@ -35,8 +36,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); BatteryPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); - BitsdojoWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DesktopDropPluginRegisterWithRegistrar( @@ -63,6 +62,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); RiveNativePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("RiveNativePlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); SpeechToTextWindowsRegisterWithRegistrar( @@ -71,4 +72,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 10e520c7..5607094e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links audioplayers_windows battery_plus - bitsdojo_window_windows connectivity_plus desktop_drop file_selector_windows @@ -20,10 +19,12 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows record_windows rive_native + screen_retriever_windows share_plus speech_to_text_windows sqlite3_flutter_libs url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST