This commit is contained in:
Developer
2026-06-02 03:52:54 +08:00
parent 1cb9bc8649
commit 10df6b705c
38 changed files with 2285 additions and 167 deletions

View File

@@ -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 构建报错

View File

@@ -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:
# 自由化代码的警告降级

View File

@@ -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) {
// 静默失败,不影响主流程
}
}
}

View File

@@ -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 = <String, List<WebSocketChannel>>{};
final _codeToChannels = <String, List<WebSocketChannel>>{};
void main(List<String> 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<String, dynamic>;
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<String, dynamic>;
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 = <WebSocketChannel>[];
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 = <String, int>{};
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<dynamic> 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();
}

View File

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

View File

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

View File

@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@@ -76,6 +79,7 @@
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
A1FE00010000000000000001 /* XianyanWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XianyanWidget.swift; sourceTree = "<group>"; };
A1FE00020000000000000002 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A1FE00030000000000000003 /* XianyanWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = XianyanWidget.entitlements; sourceTree = "<group>"; };
A1FE00040000000000000004 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
@@ -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 = "<group>";
@@ -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;

View File

@@ -7,7 +7,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Xianyan</string>
<string>闲言</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -15,7 +15,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>xianyan</string>
<string>闲言</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>

View File

@@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "Icon-App-1024x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

View File

@@ -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<LiveActivityState>
);
}
/// 更新倒计时进度
Future<void> updateCountdownActivity({
required String title,
required int remainingMinutes,
String emoji = '⏱️',
}) async {
await LiveActivityService.instance.updateCountdownActivity(
title: title,
remainingMinutes: remainingMinutes,
emoji: emoji,
);
}
/// 结束倒计时活动
Future<void> endCountdownActivity() async {
await LiveActivityService.instance.endCountdownActivity();
state = state.copyWith(hasActiveActivity: false);
}
/// 结束所有活动
Future<void> endAllActivities() async {
await LiveActivityService.instance.endAllActivities();

View File

@@ -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<void> init() async {
if (defaultTargetPlatform != TargetPlatform.iOS) {
_isSupported = false;
@@ -56,11 +47,10 @@ class LiveActivityService {
}
}
/// 开始番茄钟活动
///
/// [remainingMinutes] 剩余分钟数
/// [totalMinutes] 总分钟数
/// [isBreak] 是否为休息阶段
// ============================================================
// 番茄钟活动
// ============================================================
Future<void> 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<void> updatePomodoroActivity({
required int remainingMinutes,
required int totalMinutes,
@@ -130,27 +118,26 @@ class LiveActivityService {
}
}
/// 结束番茄钟活动
Future<void> 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<void> 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<void> 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!,
<String, dynamic>{
'type': 'countdown',
'title': title,
'remainingMinutes': remainingMinutes,
'endTime': endTime.millisecondsSinceEpoch,
'emoji': emoji,
},
);
Log.d('LiveActivityService: 倒计时活动已更新 remaining=$remainingMinutes');
} catch (e) {
Log.e('LiveActivityService: 更新倒计时活动失败 $e');
}
}
Future<void> 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<void> 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<void> _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();
}

View File

@@ -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<bool> isFullscreen() async {
if (!pu.isMacOS) return false;
try {
final result = await _channel.invokeMethod<bool>('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<String?> getSystemAppearance() async {
if (!pu.isMacOS) return null;
try {
return await _channel.invokeMethod<String>('getSystemAppearance');
} catch (e) {
Log.w('MacosPlatformService.getSystemAppearance失败: $e');
return null;
}
}
// ============================================================
// 内部工具
// ============================================================
static void _invoke(String method, [dynamic arguments]) {
try {
_channel.invokeMethod<void>(method, arguments);
} catch (e) {
Log.w('MacosPlatformService.$method失败: $e');
}
}
}

View File

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

View File

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

View File

@@ -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<String, dynamic> 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<bool> 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<void> 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<String, dynamic> 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<String, dynamic>;
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';
}
}

View File

@@ -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<CountdownPage> {
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<CountdownPage> {
);
}
// ============================================================
// 灵动岛聚焦横幅
// ============================================================
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<CountdownPage> {
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<CountdownPage> {
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<CountdownPage> {
// ============================================================
void _showEventActions(CountdownEvent event) {
final isFocused = ref.read(countdownProvider).focusedEventId == event.id;
final isLiveActivitySupported =
ref.read(liveActivitySupportedProvider);
showCupertinoModalPopup<void>(
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);

View File

@@ -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<CountdownEvent> 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<CountdownEvent> 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<CountdownEvent>? events, bool? isLoading}) {
CountdownState copyWith({
List<CountdownEvent>? 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<CountdownState> {
Timer? _updateTimer;
@override
CountdownState build() {
Future.microtask(() => _loadEvents()).catchError((_) {});
ref.onDispose(_onDispose);
return const CountdownState(isLoading: false);
}
static const _key = 'countdown_events';
// ============================================================
// 数据持久化
// ============================================================
Future<void> _loadEvents() async {
try {
final raw = KvStorage.getString(_key);
@@ -99,6 +128,10 @@ class CountdownNotifier extends Notifier<CountdownState> {
];
}
// ============================================================
// 事件 CRUD
// ============================================================
Future<void> addEvent(CountdownEvent event) async {
state = state.copyWith(events: [...state.events, event]);
await _saveEvents();
@@ -109,13 +142,23 @@ class CountdownNotifier extends Notifier<CountdownState> {
events: state.events.map((e) => e.id == event.id ? event : e).toList(),
);
await _saveEvents();
if (state.focusedEventId == event.id) {
_updateLiveActivity();
}
}
Future<void> 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<void> togglePin(String id) async {
@@ -126,6 +169,118 @@ class CountdownNotifier extends Notifier<CountdownState> {
);
await _saveEvents();
}
// ============================================================
// 灵动岛聚焦模式
// ============================================================
Future<void> 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<void> 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<CountdownNotifier, CountdownState>(

View File

@@ -50,6 +50,12 @@ class _RssReaderPageState extends State<RssReaderPage> {
/// 选中的文章(查看详情)
RssFeedItem? _selectedItem;
/// 阅读模式全文
RssFullTextResult? _fullTextResult;
/// 是否正在加载全文
bool _isLoadingFullText = false;
/// 当前分类筛选
RssCategory? _selectedCategory;
@@ -432,6 +438,7 @@ class _RssReaderPageState extends State<RssReaderPage> {
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<RssReaderPage> {
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<RssReaderPage> {
_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<RssReaderPage> {
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<RssReaderPage> {
);
}
/// 加载全文内容(阅读模式)
Future<void> _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<String> 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<RssReaderPage> {
}
void _closeDetail() {
setState(() => _selectedItem = null);
setState(() {
_selectedItem = null;
_fullTextResult = null;
_isLoadingFullText = false;
});
}
void _backToSubscriptions() {

View File

@@ -561,4 +561,175 @@ class RssService {
.replaceAll('&quot;', '"')
.replaceAll('&amp;', '&');
}
// ============================================================
// 全文提取(阅读模式)
// ============================================================
/// 从文章URL提取全文内容阅读模式
///
/// 使用简易 Readability 算法:
/// 1. 获取网页HTML
/// 2. 移除导航/侧边栏/页脚等非正文区域
/// 3. 提取最可能是正文的区域
/// 4. 清理HTML标签返回纯文本
static Future<RssFullTextResult> fetchFullText(String url) async {
try {
final response = await _dio.get<String>(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'<meta[^>]*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'<title[^>]*>(.*?)</title>', 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'<script[^>]*>.*?</script>', caseSensitive: false, dotAll: true),
'',
);
cleaned = cleaned.replaceAll(
RegExp(r'<style[^>]*>.*?</style>', caseSensitive: false, dotAll: true),
'',
);
cleaned = cleaned.replaceAll(
RegExp(r'<nav[^>]*>.*?</nav>', caseSensitive: false, dotAll: true),
'',
);
cleaned = cleaned.replaceAll(
RegExp(r'<footer[^>]*>.*?</footer>', caseSensitive: false, dotAll: true),
'',
);
cleaned = cleaned.replaceAll(
RegExp(r'<header[^>]*>.*?</header>', caseSensitive: false, dotAll: true),
'',
);
cleaned = cleaned.replaceAll(
RegExp(r'<aside[^>]*>.*?</aside>', caseSensitive: false, dotAll: true),
'',
);
cleaned = cleaned.replaceAll(
RegExp(r'<noscript[^>]*>.*?</noscript>', caseSensitive: false, dotAll: true),
'',
);
// 查找 article 标签
final articleMatch = RegExp(r'<article[^>]*>(.*?)</article>', caseSensitive: false, dotAll: true)
.firstMatch(cleaned);
if (articleMatch != null) {
cleaned = articleMatch.group(1)!;
} else {
// 查找 class 含 article/content/post/entry 的 div
final contentDiv = RegExp(
r'<div[^>]*class=["\x27][^\x27]*(?:article|content|post-body|entry-content|post-content|story-body|article-body|rich-text|markdown-body)[^\x27]*["\x27][^>]*>(.*?)</div>',
caseSensitive: false,
dotAll: true,
).firstMatch(cleaned);
if (contentDiv != null) {
cleaned = contentDiv.group(1)!;
}
}
// 清理HTML标签保留段落结构
var text = cleaned;
text = text.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n');
text = text.replaceAll(RegExp(r'<p[^>]*>', caseSensitive: false), '\n');
text = text.replaceAll(RegExp(r'</p>', caseSensitive: false), '\n');
text = text.replaceAll(RegExp(r'<h[1-6][^>]*>', caseSensitive: false), '\n\n');
text = text.replaceAll(RegExp(r'</h[1-6]>', caseSensitive: false), '\n');
text = text.replaceAll(RegExp(r'<li[^>]*>', caseSensitive: false), '');
text = text.replaceAll(RegExp(r'<blockquote[^>]*>', caseSensitive: false), '\n> ');
text = text.replaceAll(RegExp(r'<[^>]*>'), '');
text = text.replaceAll(RegExp(r'&nbsp;'), ' ');
text = text.replaceAll(RegExp(r'&amp;'), '&');
text = text.replaceAll(RegExp(r'&lt;'), '<');
text = text.replaceAll(RegExp(r'&gt;'), '>');
text = text.replaceAll(RegExp(r'&quot;'), '"');
text = text.replaceAll(RegExp(r'&#\d+;'), '');
text = text.replaceAll(RegExp(r'\n{3,}'), '\n\n');
return text.trim();
}
/// 提取正文中的图片URL
static List<String> _extractContentImages(String html) {
final imgRegex = RegExp(r'<img[^>]+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('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&nbsp;', ' ');
}
}
/// 全文提取结果
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<String> images;
final String? sourceUrl;
final String? error;
}

View File

@@ -370,7 +370,7 @@ extension _TransferChatFileSendExt on _TransferChatPageState {
Future<void> _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)

View File

@@ -304,7 +304,6 @@ class FontDownloadService {
final result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: ['ttf', 'otf'],
allowMultiple: true,
withData: true,
);

View File

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

View File

@@ -7,7 +7,6 @@
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
@@ -15,16 +14,15 @@
#include <gtk/gtk_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <rive_native/rive_native_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
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);
}

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,8 @@
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleDisplayName</key>
<string>闲言</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
@@ -13,7 +15,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>闲言</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>

321
pubspec_macos.yaml Normal file
View File

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

View File

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

View File

@@ -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 = <String>[
'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);
}
}
});
}
}

View File

@@ -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<LintRule> getLintRules(CustomLintConfigs configs) => [
DoubleAngleBracketsRule(),
HardcodedColorRule(),
];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -9,7 +9,6 @@
#include <app_links/app_links_plugin_c_api.h>
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <battery_plus/battery_plus_windows_plugin.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
@@ -23,10 +22,12 @@
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <rive_native/rive_native_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <speech_to_text_windows/speech_to_text_windows.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
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"));
}

View File

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