同步
70
CHANGELOG.md
@@ -4,6 +4,76 @@
|
||||
|
||||
***
|
||||
|
||||
## [v6.10.0] - 2026-06-02
|
||||
|
||||
### 🏗️ 架构优化 + 灵动岛增强 + Lint规则 + 应用图标
|
||||
|
||||
**1. MacosPlatformService 统一 🏗️:**
|
||||
- 🔄 分散在多文件的 MethodChannel 统一为 `MacosPlatformService`
|
||||
- 🔄 通道名 `com.xianyan.theme` → `com.xianyan.macos`
|
||||
- 🔄 `MacosTitleBarService` 标记 @Deprecated,桥接到新服务
|
||||
- ✅ Swift 端扩展为 10 个方法处理器(主题/窗口/标题栏/全屏/触觉等)
|
||||
|
||||
**2. 二维码 WebSocket 长连接推送 🌐:**
|
||||
- ✅ 新增 `QrcodeWsService` — WebSocket 推送服务(心跳25秒/指数退避重连)
|
||||
- ✅ `QrcodeLoginProvider` 重构为双通道架构(WS优先 + HTTP轮询降级)
|
||||
- ✅ 新增 `qrcode_ws_relay.dart` — shelf_web_socket 中继服务器
|
||||
- ✅ PHP 后端 `_notifyWsRelay()` — cURL 通知 WS 中继
|
||||
|
||||
**3. RSS 全文阅读增强 📖:**
|
||||
- ✅ `RssService.fetchFullText()` — 简易 Readability 算法
|
||||
- ✅ `RssReaderPage` 阅读模式 — 全文内容 + 图片画廊
|
||||
- ✅ `RssFullTextResult` 数据类
|
||||
|
||||
**4. 灵动岛增强 — 倒计时聚焦模式 ⏰:**
|
||||
- ✅ `LiveActivityService` — 新增 `startCountdownActivity/updateCountdownActivity/endCountdownActivity`
|
||||
- ✅ `CountdownNotifier` — 集成灵动岛聚焦模式(focusEvent/unfocusEvent)
|
||||
- ✅ `LiveActivityProvider` — 补充 `updateCountdownActivity/endCountdownActivity`
|
||||
- ✅ `CountdownPage` UI — 灵动岛聚焦横幅 + 卡片铃铛按钮 + 操作菜单
|
||||
- ✅ `_activeType` 跟踪 — 类型切换时自动结束旧活动
|
||||
|
||||
**5. 自定义 Lint 规则 📏:**
|
||||
- ✅ 新增 `tools/xianyan_lint/` 自定义 lint 包
|
||||
- ✅ `double_angle_brackets` — 检测双书名号《《
|
||||
- ✅ `hardcoded_color` — 检测非主题系统硬编码颜色
|
||||
- ✅ `hardcoded_chinese` — 检测UI层硬编码中文(默认关闭)
|
||||
- ✅ `analysis_options.yaml` — 新增 custom_lint 规则配置
|
||||
|
||||
**6. 应用图标和名称 🎨:**
|
||||
- ✅ iOS/macOS 应用名称改为中文「闲言」
|
||||
- ✅ iOS `CFBundleDisplayName` → 闲言
|
||||
- ✅ macOS 新增 `CFBundleDisplayName` → 闲言
|
||||
- ✅ 全平台图标更新(从 `assets/templates/resized/` 复制)
|
||||
- ✅ iOS 小组件图标补全
|
||||
- ✅ Web 图标更新
|
||||
|
||||
**修改文件**
|
||||
- `lib/core/services/device/macos_platform_service.dart` — 新建统一服务
|
||||
- `lib/core/services/ui/macos_title_bar_service.dart` — @Deprecated桥接
|
||||
- `macos/Runner/MainFlutterWindow.swift` — 统一通道+10方法
|
||||
- `lib/app/app.dart` — 引用新服务
|
||||
- `lib/features/auth/services/qrcode_ws_service.dart` — 新建WS服务
|
||||
- `lib/features/auth/providers/qrcode_login_provider.dart` — 双通道重构
|
||||
- `docs/toolsapi/scripts/qrcode_ws_relay.dart` — 新建WS中继
|
||||
- `docs/toolsapi/application/api/controller/UserSecurity.php` — WS通知
|
||||
- `lib/features/discover/services/rss_service.dart` — 全文提取
|
||||
- `lib/features/discover/presentation/pages/tool/rss_reader_page.dart` — 阅读模式
|
||||
- `lib/core/services/device/live_activity_service.dart` — 倒计时活动
|
||||
- `lib/core/services/device/live_activity_provider.dart` — 补充方法
|
||||
- `lib/features/countdown/providers/countdown_provider.dart` — 灵动岛集成
|
||||
- `lib/features/countdown/presentation/countdown_page.dart` — 灵动岛UI
|
||||
- `tools/xianyan_lint/` — 新建自定义lint包
|
||||
- `analysis_options.yaml` — lint规则配置
|
||||
- `pubspec.yaml` — 新增xianyan_lint依赖
|
||||
- `ios/Runner/Info.plist` — 中文名称
|
||||
- `macos/Runner/Info.plist` — 中文名称+CFBundleDisplayName
|
||||
- `ios/Runner/Assets.xcassets/AppIcon.appiconset/` — 图标更新
|
||||
- `macos/Runner/Assets.xcassets/AppIcon.appiconset/` — 图标更新
|
||||
- `ios/XianyanWidget/Assets.xcassets/AppIcon.appiconset/` — 小组件图标
|
||||
- `web/icons/` — Web图标更新
|
||||
|
||||
***
|
||||
|
||||
## [v6.9.51] - 2026-06-01
|
||||
|
||||
### 🔧 修复 MacBook Pro 端 iOS/macOS 构建报错
|
||||
|
||||
@@ -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:
|
||||
# 自由化代码的警告降级
|
||||
|
||||
@@ -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) {
|
||||
// 静默失败,不影响主流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
170
docs/toolsapi/scripts/qrcode_ws_relay.dart
Normal 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();
|
||||
}
|
||||
255
docs/toolsapi/scripts/test_qrcode_login.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-App-1024x1024.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
||||
|
After Width: | Height: | Size: 4.0 MiB |
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
120
lib/core/services/device/macos_platform_service.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
12
lib/core/services/ui/macos_title_bar_service.dart
Normal 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';
|
||||
187
lib/features/auth/services/qrcode_ws_service.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -561,4 +561,175 @@ class RssService {
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll('&', '&');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 全文提取(阅读模式)
|
||||
// ============================================================
|
||||
|
||||
/// 从文章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' '), ' ');
|
||||
text = text.replaceAll(RegExp(r'&'), '&');
|
||||
text = text.replaceAll(RegExp(r'<'), '<');
|
||||
text = text.replaceAll(RegExp(r'>'), '>');
|
||||
text = text.replaceAll(RegExp(r'"'), '"');
|
||||
text = text.replaceAll(RegExp(r'&#\d+;'), '');
|
||||
text = text.replaceAll(RegExp(r'\n{3,}'), '\n\n');
|
||||
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
/// 提取正文中的图片URL
|
||||
static List<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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll(' ', ' ');
|
||||
}
|
||||
}
|
||||
|
||||
/// 全文提取结果
|
||||
class RssFullTextResult {
|
||||
const RssFullTextResult({
|
||||
this.success = false,
|
||||
this.title,
|
||||
this.content,
|
||||
this.images = const [],
|
||||
this.sourceUrl,
|
||||
this.error,
|
||||
});
|
||||
|
||||
final bool success;
|
||||
final String? title;
|
||||
final String? content;
|
||||
final List<String> images;
|
||||
final String? sourceUrl;
|
||||
final String? error;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -304,7 +304,6 @@ class FontDownloadService {
|
||||
final result = await FilePicker.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['ttf', 'otf'],
|
||||
allowMultiple: true,
|
||||
withData: true,
|
||||
);
|
||||
|
||||
|
||||
39
lib/shared/widgets/containers/deferred_builder.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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/
|
||||
46
tools/xianyan_lint/lib/src/rules/double_angle_brackets.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
tools/xianyan_lint/lib/src/rules/hardcoded_color.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
22
tools/xianyan_lint/lib/xianyan_lint.dart
Normal 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(),
|
||||
];
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 1.0 MiB |
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||