From 49b632377211e907c0f5618f657a64a5661555f5 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 17 Jun 2026 04:47:06 +0800 Subject: [PATCH] =?UTF-8?q?release:=20=E5=8F=91=E5=B8=836.6.18=E7=89=88?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C=E5=AE=8C=E6=88=90=E5=A4=9A=E9=A1=B9=E5=90=88?= =?UTF-8?q?=E8=A7=84=E4=B8=8E=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此版本包含以下核心更新: 1. 版本号升级至6.6.18,更新全平台配置文件 2. 实现隐私合规改造: - 新增剪贴板隐私守卫,未同意协议前禁止读取剪贴板 - 所有桌面小部件继承隐私感知基类,未同意协议时显示占位提示 - 移除自动剪贴板监控,改为用户主动触发 3. 新增Windows平台深色主题同步功能 4. 补全多语言默认句子翻译 5. 优化安卓快捷方式配置与小部件合规性 6. 修复macOS插件注册问题 7. 新增Windows安装脚本 8. 优化触觉反馈服务初始化时机 --- CHANGELOG.md | 194 +++++++ Scripts/package_msix.ps1 | 118 ++++ android/app/src/main/AndroidManifest.xml | 9 + .../kotlin/apps/xy/xianyan/MainActivity.kt | 160 +++++- .../kotlin/apps/xy/xianyan/SplashActivity.kt | 71 ++- .../apps/xy/xianyan/widget/CheckinProvider.kt | 9 +- .../xy/xianyan/widget/CountdownProvider.kt | 9 +- .../xianyan/widget/CtcLatestNoteProvider.kt | 9 +- .../xy/xianyan/widget/DailyCardProvider.kt | 9 +- .../xianyan/widget/DailySentenceProvider.kt | 9 +- .../apps/xy/xianyan/widget/FortuneProvider.kt | 9 +- .../xy/xianyan/widget/PomodoroProvider.kt | 9 +- .../widget/PrivacyAwareHomeWidgetProvider.kt | 111 ++++ .../xy/xianyan/widget/ReadlaterProvider.kt | 9 +- .../xy/xianyan/widget/SolarTermProvider.kt | 9 +- .../res/layout/widget_privacy_placeholder.xml | 40 ++ .../res/mipmap-hdpi/ic_launcher_shortcut.png | Bin 0 -> 5537 bytes .../res/mipmap-mdpi/ic_launcher_shortcut.png | Bin 0 -> 3160 bytes .../res/mipmap-xhdpi/ic_launcher_shortcut.png | Bin 0 -> 8497 bytes .../mipmap-xxhdpi/ic_launcher_shortcut.png | Bin 0 -> 15867 bytes .../mipmap-xxxhdpi/ic_launcher_shortcut.png | Bin 0 -> 25904 bytes android/app/src/main/res/values/strings.xml | 1 + android/app/src/main/res/xml/shortcuts.xml | 20 +- installer.iss | 77 +++ lib/app/app.dart | 128 +++-- .../services/clipboard_monitor_service.dart | 11 +- .../services/data/home_widget_service.dart | 60 ++- lib/core/services/device/haptic_service.dart | 30 +- .../device/quick_actions_service.dart | 34 +- .../device/windows_platform_service.dart | 47 ++ .../services/post_agreement_initializer.dart | 14 +- lib/core/utils/platform/clipboard_bridge.dart | 34 +- .../utils/platform/platform_capability.dart | 52 +- .../pages/ctc_note_edit_page.dart | 11 +- .../presentation/pages/ctc_settings_page.dart | 510 ++++++++++++++---- .../chat/chat_flow_readlater_sync_helper.dart | 13 +- .../widgets/chat_input/link_input_sheet.dart | 46 +- .../clipboard/clipboard_flow_page.dart | 7 +- .../clipboard/clipboard_manager_service.dart | 11 +- .../services/clipboard_sync_service.dart | 11 +- .../providers/readlater_page.dart | 8 +- .../profile/presentation/about_page.dart | 43 +- .../profile/presentation/profile_page.dart | 44 +- .../pages/leisure_import_dialog.dart | 7 +- lib/l10n/languages/fr.dart | 7 +- lib/l10n/languages/hi.dart | 7 +- lib/l10n/languages/it.dart | 7 +- lib/l10n/languages/pt.dart | 7 +- lib/l10n/languages/ru.dart | 7 +- lib/l10n/translation_resolver.dart | 58 +- lib/main.dart | 20 +- lib/shared/widgets/feedback/app_toast.dart | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- ohos/AppScope/app.json5 | 6 +- .../resources/base/media/layered_image.json | 4 +- ohos/entry/src/main/module.json5 | 2 +- .../resources/base/media/layered_image.json | 4 +- pubspec.macos.yaml | 2 +- pubspec.ohos.yaml | 2 +- windows/CMakeLists.txt | 5 + windows/runner/flutter_window.cpp | 28 + windows/runner/flutter_window.h | 4 + windows/runner/resources/app_icon.ico | Bin 33772 -> 47957 bytes windows/runner/win32_window.cpp | 6 + windows/runner/win32_window.h | 3 + 65 files changed, 1745 insertions(+), 442 deletions(-) create mode 100644 Scripts/package_msix.ps1 create mode 100644 android/app/src/main/kotlin/apps/xy/xianyan/widget/PrivacyAwareHomeWidgetProvider.kt create mode 100644 android/app/src/main/res/layout/widget_privacy_placeholder.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_shortcut.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_shortcut.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_shortcut.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_shortcut.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_shortcut.png create mode 100644 installer.iss create mode 100644 lib/core/services/device/windows_platform_service.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be3e9b0..68d92834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,200 @@ *** +## [v6.79.0] - 2026-06-17 + +### 🔒 隐私合规 — 修复安卓端自启动问题(androidx.glance.appwidget) + +#### 问题描述 +1. 应用商店审核发现:`androidx.glance.appwidget` SDK 在应用退出后触发自启动(1次/秒),无隐私文本覆盖 +2. 审核依据:《个人信息保护法》要求 APP 未向用户明示且未经用户同意,不得存在频繁自启动行为 +3. 根因:`home_widget` 包引入了 `androidx.glance:glance-appwidget:1.1.1` 依赖,项目未使用 Glance Widget 但该库被打包进 APK,其内部 `GlanceAppWidgetReceiver`/`GlanceAppWidgetService` 被系统广播触发导致自启动 +4. 次要问题:8个桌面小部件 Provider 在系统定时 `APPWIDGET_UPDATE` 广播触发时,未检查隐私协议状态即执行数据读取 + +#### 修复内容 + +**1. 彻底移除 androidx.glance.appwidget 依赖** +| 文件 | 变更 | +|---|---| +| `packages/home_widget/android/build.gradle` | 移除 `implementation "androidx.glance:glance-appwidget:1.1.1"` 依赖 | +| `HomeWidgetGlanceWidgetReceiver.kt` | 删除未使用的 Glance Receiver 源文件 | +| `HomeWidgetGlanceState.kt` | 删除未使用的 Glance State 源文件 | + +**2. AndroidManifest 排除 Glance 组件合并** +| 文件 | 变更 | +|---|---| +| `AndroidManifest.xml` | 新增 `tools:node="remove"` 移除 `GlanceAppWidgetReceiver` 和 `GlanceAppWidgetService`,防止残留库通过 manifest merge 注入 | + +**3. 所有 Widget Provider 增加隐私协议守门** +| 文件 | 变更 | +|---|---| +| `PrivacyAwareHomeWidgetProvider.kt` | 新建基类,在 `onUpdate` 中检查 `agreement_accepted` 标志,未同意时显示占位视图不读取任何业务数据 | +| `widget_privacy_placeholder.xml` | 新建隐私占位布局,提示"请先同意隐私政策" | +| `DailySentenceProvider.kt` | 改为继承 `PrivacyAwareHomeWidgetProvider`,`onUpdate` → `onUpdateWithAgreement` | +| `ReadlaterProvider.kt` | 同上 | +| `DailyCardProvider.kt` | 同上 | +| `FortuneProvider.kt` | 同上 | +| `CountdownProvider.kt` | 同上 | +| `PomodoroProvider.kt` | 同上 | +| `SolarTermProvider.kt` | 同上 | +| `CheckinProvider.kt` | 同上 | +| `CtcLatestNoteProvider.kt` | 同上 | + +#### 合规影响 +- `androidx.glance.appwidget` 自启动行为彻底消除(依赖移除 + Manifest 排除 + 源文件删除三重防护) +- Widget Provider 在用户未同意隐私政策前不执行任何数据操作,仅显示占位提示 +- 与现有 `SplashActivity` 协议守门机制形成完整闭环 + +*** + +## [v6.78.1] - 2026-06-16 + +### 🐛 Bug 修复 + +**鸿蒙端应用商店跳转URL修正 + 外部跳转确认弹窗** + +| 文件 | 变更 | +|---|---| +| `profile_page.dart` | 鸿蒙应用市场URL从 `C108129465` 修正为 `detail?id=apps.xy.xianyan`;评分跳转前增加 `ExternalLinkDialog` 确认弹窗 | +| `about_page.dart` | 同上,鸿蒙URL修正 + 外部跳转确认弹窗 | + +- 修复鸿蒙端"给个好评"/"评价应用"跳转到错误的应用商店页面 +- 所有平台评分跳转前统一使用 `ExternalLinkDialog` 弹窗提示用户即将离开应用 + +*** + +## [v6.78.0] - 2026-06-16 + +### 🖥️ Windows 桌面端跨平台适配 + +#### 问题描述 +1. Windows 运行时 `sqlite3.dll` 未找到,数据库无法初始化 +2. `home_widget` 插件无 Windows 实现,抛出 MissingPluginException +3. `LiquidGlassLayer` 需要 Impeller 渲染引擎,Windows 不支持导致大量警告 +4. BotToast `markInitialized()` 在每次 widget rebuild 时重复打印日志 +5. Windows 标题栏不跟随应用深色主题 +6. Windows 应用使用默认 Flutter 图标 + +#### 修复内容 + +**1. sqlite3.dll 加载修复** +| 文件 | 变更 | +|---|---| +| `windows/CMakeLists.txt` | 新增 install 规则,将预编译 sqlite3.dll 复制到构建输出目录 | + +**2. home_widget 跨平台兼容** +| 文件 | 变更 | +|---|---| +| `home_widget_service.dart` | 新增 `_isPlatformSupported` 检查,Windows/Linux 桌面端跳过所有 HomeWidget 调用 | + +**3. LiquidGlass 平台能力检测** +| 文件 | 变更 | +|---|---| +| `platform_capability.dart` | `liquidGlass` 能力注册改为仅 iOS/Android/macOS | +| `app.dart` | 标准端路径根据 `PlatformCapabilities.supports(CapabilityKey.liquidGlass)` 决定是否使用 GlassTheme | + +**4. BotToast 重复初始化修复** +| 文件 | 变更 | +|---|---| +| `app_toast.dart` | `markInitialized()` 添加去重检查,避免重复打印日志 | + +**5. Windows 标题栏深色主题** +| 文件 | 变更 | +|---|---| +| `win32_window.h/cpp` | 新增 `SetDarkMode(HWND, bool)` 方法 | +| `flutter_window.cpp` | 注册 `MethodChannel("com.xianyan.windows")` 接收 Flutter 端主题切换 | +| `windows_platform_service.dart` | 新增 Windows 平台服务,`syncTheme(bool)` 通知原生层 | +| `app.dart` | 调用 `WindowsPlatformService.syncTheme()` 同步标题栏主题 | + +**6. Windows 应用图标替换** +| 文件 | 变更 | +|---|---| +| `windows/runner/resources/app_icon.ico` | 使用项目图标替换默认 Flutter 图标 | + +*** + +## [v6.77.0] - 2026-06-16 + +### 🔒 隐私合规 — 移除自动剪贴板监听 + 协议前权限使用增强 + +#### 问题描述 +1. 安卓端未同意隐私政策前,`ClipboardSyncService` 以2秒定时轮询读取剪贴板,违反合规要求 +2. 多处 UI 代码直接调用 `Clipboard.getData`,绕过隐私协议检查 +3. `HapticService.init()` 在协议前调用 `Vibrate.canVibrate`,触发原生插件 +4. `ClipboardMonitorService` 在用户进入稍后读页面时自动检查剪贴板,用户无感知 + +#### 修复内容 + +**1. ClipboardBridge 统一剪贴板入口(核心修复)** +| 变更 | 说明 | +|---|---| +| `clipboard_bridge.dart` | 新增隐私协议守卫,未同意协议时 `getData()` 返回 null,`hasStrings()` 返回 false | +| 所有 `Clipboard.getData` 调用点 | 统一替换为 `ClipboardBridge.getData()`,共7个文件 | + +**2. 移除自动剪贴板监听,改为用户点击触发** +| 变更 | 说明 | +|---|---| +| `readlater_page.dart` | 移除 `initState` 中的 `checkClipboardOnce()` 调用 | +| `chat_flow_readlater_sync_helper.dart` | 启用监控按钮点击后立即检查一次剪贴板 | + +**3. 剪贴板服务统一入口** +| 文件 | 变更 | +|---|---| +| `clipboard_monitor_service.dart` | 使用 `ClipboardBridge.getData()` 替代 `Clipboard.getData` | +| `clipboard_sync_service.dart` | 使用 `ClipboardBridge.getData()` 替代 `Clipboard.getData` | +| `clipboard_manager_service.dart` | `syncNow()` 使用 `ClipboardBridge.getData()` 替代 `Clipboard.getData` | + +**4. UI层剪贴板调用统一** +| 文件 | 变更 | +|---|---| +| `ctc_note_edit_page.dart` | 粘贴功能使用 `ClipboardBridge.getData()` | +| `ctc_settings_page.dart` | 从剪贴板导入使用 `ClipboardBridge.getData()` | +| `link_input_sheet.dart` | 从剪贴板粘贴链接使用 `ClipboardBridge.getData()` | +| `chat_flow_readlater_sync_helper.dart` | 查看剪贴板使用 `ClipboardBridge.getData()` | +| `leisure_import_dialog.dart` | 读取剪贴板使用 `ClipboardBridge.getData()` | +| `clipboard_flow_page.dart` | 粘贴同步使用 `ClipboardBridge.getData()` | + +**5. 触觉反馈服务延迟初始化** +| 变更 | 说明 | +|---|---| +| `main.dart` | 移除 `HapticService.init()` 调用 | +| `post_agreement_initializer.dart` | 新增 `HapticService.init()` 调用,协议同意后才初始化 | +| `haptic_service.dart` | 所有公开方法增加 `_agreementAccepted` 守卫,未同意协议时不执行震动 | + +**6. 原生端敏感插件列表扩展** +| 变更 | 说明 | +|---|---| +| `MainActivity.kt` | `SENSITIVE_PLUGIN_CLASSES` 新增3个插件:`NetworkInfoPlugin`(网络信息)、`AudioplayersPlugin`(音频播放)、`FlutterVibratePlugin`(震动) | + +**7. 快捷方式修复** +| 变更 | 说明 | +|---|---| +| `MainActivity.kt` | 修复 `shortcutManager.shortcuts` 编译错误,改用 `setDynamicShortcuts()` | +| `MainActivity.kt` | `handleShortcutIntent` 增加对 `android:data` URI 解析,支持 `xianyan://shortcut/action_xxx` 协议 | +| `MainActivity.kt` | 快捷方式图标改用 `R.drawable.ic_shortcut_theme` 和 `R.drawable.ic_shortcut_search` 矢量图标 | +| `shortcuts.xml` | 添加静态快捷方式定义,包含图标和 intent 配置 | + +#### 修改文件 +- `lib/core/utils/platform/clipboard_bridge.dart` +- `lib/core/services/clipboard_monitor_service.dart` +- `lib/features/file_transfer/services/clipboard_sync_service.dart` +- `lib/features/file_transfer/collaboration/clipboard/clipboard_manager_service.dart` +- `lib/features/file_transfer/collaboration/clipboard/clipboard_flow_page.dart` +- `lib/features/ctc/presentation/pages/ctc_note_edit_page.dart` +- `lib/features/ctc/presentation/pages/ctc_settings_page.dart` +- `lib/features/discover/presentation/widgets/chat_input/link_input_sheet.dart` +- `lib/features/discover/presentation/widgets/chat/chat_flow_readlater_sync_helper.dart` +- `lib/features/tool_center/leisure/presentation/pages/leisure_import_dialog.dart` +- `lib/core/services/device/haptic_service.dart` +- `lib/core/services/post_agreement_initializer.dart` +- `lib/main.dart` +- `lib/features/home/presentation/providers/readlater_page.dart` +- `android/app/src/main/kotlin/apps/xy/xianyan/MainActivity.kt` +- `android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt` +- `android/app/src/main/res/xml/shortcuts.xml` + +*** + ## [v6.76.0] - 2026-06-15 ### 🔧 多项UI修复与功能增强 diff --git a/Scripts/package_msix.ps1 b/Scripts/package_msix.ps1 new file mode 100644 index 00000000..dece00b5 --- /dev/null +++ b/Scripts/package_msix.ps1 @@ -0,0 +1,118 @@ +# Xianyan MSIX Packaging Script +# Auto-syncs version from pubspec.yaml to msix_version +# +# Usage: +# .\scripts\package_msix.ps1 # Store submission +# .\scripts\package_msix.ps1 -LocalTest # Local test (sign + install) + +param( + [switch]$LocalTest = $false +) + +$ErrorActionPreference = "Stop" +$ProjectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$PubspecPath = Join-Path $ProjectRoot "pubspec.yaml" + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Xianyan MSIX Packaging Tool" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan + +# 1. Read version from pubspec.yaml +$pubspecContent = Get-Content $PubspecPath -Raw -Encoding UTF8 +if ($pubspecContent -match 'version:\s*(\d+)\.(\d+)\.(\d+)\+(\d+)') { + $major = $Matches[1] + $minor = $Matches[2] + $patch = $Matches[3] + $build = $Matches[4] + $flutterVersion = "${major}.${minor}.${patch}+${build}" + $msixVersion = "${major}.${minor}.${patch}.0" + Write-Host "[1/5] Version: $flutterVersion -> MSIX: $msixVersion" -ForegroundColor Green +} else { + Write-Host "[1/5] ERROR: Cannot read version from pubspec.yaml" -ForegroundColor Red + exit 1 +} + +# 2. Sync msix_version in pubspec.yaml +$updatedContent = $pubspecContent -replace 'msix_version:\s*\d+\.\d+\.\d+\.\d+', "msix_version: $msixVersion" +[System.IO.File]::WriteAllText($PubspecPath, $updatedContent, [System.Text.UTF8Encoding]::new($false)) +Write-Host "[2/5] msix_version synced: $msixVersion" -ForegroundColor Green + +# 3. Set store mode +if ($LocalTest) { + $updatedContent = $updatedContent -replace 'store:\s*true', 'store: false' + [System.IO.File]::WriteAllText($PubspecPath, $updatedContent, [System.Text.UTF8Encoding]::new($false)) + Write-Host "[3/5] Mode: Local Test (store: false)" -ForegroundColor Yellow +} else { + $updatedContent = $updatedContent -replace 'store:\s*false', 'store: true' + [System.IO.File]::WriteAllText($PubspecPath, $updatedContent, [System.Text.UTF8Encoding]::new($false)) + Write-Host "[3/5] Mode: Store Submit (store: true)" -ForegroundColor Green +} + +# 4. Build MSIX +Set-Location $ProjectRoot +Write-Host "[4/5] Building MSIX..." -ForegroundColor Cyan +dart run msix:create 2>&1 | ForEach-Object { Write-Host $_ } + +$msixPath = Join-Path $ProjectRoot "build\windows\x64\runner\Release\xianyan.msix" +if (-not (Test-Path $msixPath)) { + Write-Host "[4/5] ERROR: MSIX file not generated" -ForegroundColor Red + exit 1 +} +$msixSize = [math]::Round((Get-Item $msixPath).Length / 1MB, 2) +Write-Host "[4/5] MSIX generated: $msixPath ($msixSize MB)" -ForegroundColor Green + +# 5. Local test: sign + install +if ($LocalTest) { + $certPath = Join-Path $ProjectRoot "build\cert\test_certificate.pfx" + $cerPath = Join-Path $ProjectRoot "build\cert\test.cer" + $signtoolPath = "d:\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" + + if (-not (Test-Path $certPath)) { + Write-Host "[5/5] Generating test certificate..." -ForegroundColor Cyan + New-Item -ItemType Directory -Force -Path (Split-Path $certPath) | Out-Null + $cert = New-SelfSignedCertificate -Type Custom ` + -Subject "CN=0334AD95-A5D7-4597-B71F-AA0696B7E9F7" ` + -KeyUsage DigitalSignature ` + -FriendlyName "Xianyan Test Cert" ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") + Export-PfxCertificate -Cert $cert -FilePath $certPath ` + -Password (ConvertTo-SecureString -String "123456" -Force -AsPlainText) | Out-Null + Export-Certificate -Cert $cert -FilePath $cerPath | Out-Null + Write-Host " Certificate generated" -ForegroundColor Green + } + + if (Test-Path $signtoolPath) { + Write-Host "[5/5] Signing MSIX..." -ForegroundColor Cyan + & $signtoolPath sign /fd SHA256 /f $certPath /p "123456" $msixPath 2>&1 | ForEach-Object { Write-Host $_ } + Write-Host " Signed successfully" -ForegroundColor Green + } else { + Write-Host "[5/5] WARNING: signtool.exe not found, skip signing" -ForegroundColor Yellow + } + + if (Test-Path $cerPath) { + Write-Host "[5/5] Installing certificate to Trusted Root (requires admin)..." -ForegroundColor Cyan + Start-Process powershell -Verb RunAs -Wait -ArgumentList @( + '-Command', + "Import-Certificate -FilePath '$cerPath' -CertStoreLocation 'Cert:\LocalMachine\Root'; Start-Sleep 2" + ) + } + + Write-Host "[5/5] Installing MSIX..." -ForegroundColor Cyan + Add-AppxPackage -Path $msixPath -ForceUpdateFromAnyVersion 2>&1 | ForEach-Object { Write-Host $_ } + Write-Host " Installed! Search 'xianyan' in Start Menu" -ForegroundColor Green + + # Restore store: true + $finalContent = [System.IO.File]::ReadAllText($PubspecPath, [System.Text.UTF8Encoding]::new($false)) + $finalContent = $finalContent -replace 'store:\s*false', 'store: true' + [System.IO.File]::WriteAllText($PubspecPath, $finalContent, [System.Text.UTF8Encoding]::new($false)) + Write-Host " Restored store: true" -ForegroundColor DarkGray +} else { + Write-Host "[5/5] Store package ready. Upload to Partner Center:" -ForegroundColor Green + Write-Host " $msixPath" -ForegroundColor White +} + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Done!" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 105f30af..6c4fba67 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -255,6 +255,15 @@ android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver" tools:node="remove" /> + + + + + pendingManageSpaceAction = null window.decorView.post { invokeFlutterMethod(action) } } pendingShortcutAction?.let { action -> pendingShortcutAction = null + Log.i(TAG, "MainActivity.onResume: 处理快捷方式 $action") window.decorView.post { invokeShortcutAction(action) } } } @@ -246,6 +281,84 @@ class MainActivity : FlutterActivity() { } } } + + // ---- ShortcutManager MethodChannel(快捷方式创建和图标设置)---- + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, + SHORTCUT_MANAGER_CHANNEL + ).setMethodCallHandler { call, result -> + when (call.method) { + "createShortcuts" -> { + // 从Flutter端调用,创建桌面快捷方式 + try { + createShortcuts() + result.success(true) + Log.i(TAG, "ShortcutManager: 快捷方式创建成功") + } catch (e: Exception) { + result.error("SHORTCUT_ERROR", "创建快捷方式失败: ${e.message}", null) + Log.e(TAG, "ShortcutManager: 创建快捷方式失败", e) + } + } + else -> result.notImplemented() + } + } + Log.i(TAG, "ShortcutManager: MethodChannel已注册") + } else { + Log.i(TAG, "ShortcutManager: API版本不支持(${Build.VERSION.SDK_INT}),跳过") + } + } + + /** + * 使用ShortcutManager API创建桌面快捷方式 + * API 25+ 支持,使用应用图标作为快捷方式图标 + */ + private fun createShortcuts() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + Log.w(TAG, "ShortcutManager: API < 25,不支持动态快捷方式") + return + } + + val shortcutManager = getSystemService(ShortcutManager::class.java) + if (!shortcutManager.isRequestPinShortcutSupported) { + Log.w(TAG, "ShortcutManager: 设备不支持快捷方式固定") + return + } + + val themeIntent = Intent(this, MainActivity::class.java).apply { + action = Intent.ACTION_RUN + putExtra(EXTRA_SHORTCUT_ACTION, "action_theme") + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + + val searchIntent = Intent(this, MainActivity::class.java).apply { + action = Intent.ACTION_RUN + putExtra(EXTRA_SHORTCUT_ACTION, "action_search") + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + + // 使用drawable资源作为快捷方式图标(矢量图标) + val themeIcon = Icon.createWithResource(this, R.drawable.ic_shortcut_theme) + val searchIcon = Icon.createWithResource(this, R.drawable.ic_shortcut_search) + + val shortcuts = listOf( + ShortcutInfo.Builder(this, "action_theme") + .setShortLabel("主题个性化") + .setLongLabel("打开主题个性化设置") + .setIcon(themeIcon) + .setIntent(themeIntent) + .build(), + ShortcutInfo.Builder(this, "action_search") + .setShortLabel("搜索功能") + .setLongLabel("搜索APP内功能") + .setIcon(searchIcon) + .setIntent(searchIntent) + .build() + ) + + // 使用setDynamicShortcuts设置动态快捷方式 + shortcutManager.setDynamicShortcuts(shortcuts) + Log.i(TAG, "ShortcutManager: 已设置 ${shortcuts.size} 个动态快捷方式") } override fun onDestroy() { @@ -290,13 +403,26 @@ class MainActivity : FlutterActivity() { /** * 处理桌面快捷方式Intent - * 检测intent中是否包含shortcut_action extra,如果有则通过MethodChannel通知Flutter端 + * 检测intent中是否包含shortcut_action extra或xianyan://协议, + * 如果有则通过MethodChannel通知Flutter端 */ private fun handleShortcutIntent(intent: Intent?) { if (intent == null) return - // 从intent extras中读取shortcut_action - val shortcutAction = intent.getStringExtra(EXTRA_SHORTCUT_ACTION) + // 优先从intent extras中读取shortcut_action(动态快捷方式) + var shortcutAction = intent.getStringExtra(EXTRA_SHORTCUT_ACTION) + + // 如果没有extra,检查intent.data(静态快捷方式使用xianyan://协议) + if (shortcutAction == null && intent.data != null) { + val uri = intent.data.toString() + Log.i(TAG, "handleShortcutIntent: 检查intent.data: $uri") + // xianyan://shortcut/action_theme -> 提取 action_theme + if (uri.startsWith("xianyan://shortcut/")) { + shortcutAction = uri.removePrefix("xianyan://shortcut/") + Log.i(TAG, "handleShortcutIntent: 从data URI提取shortcutAction: $shortcutAction") + } + } + if (shortcutAction != null) { Log.i(TAG, "handleShortcutIntent: 收到快捷方式action: $shortcutAction") pendingShortcutAction = shortcutAction diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt b/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt index 678dac69..edaaeda6 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt @@ -1,10 +1,11 @@ // ============================================================ // 闲言APP — 启动页Activity(隐私协议守门人) // 创建时间: 2026-06-10 -// 更新时间: 2026-06-13 +// 更新时间: 2026-06-16 // 作用: 应用启动入口,在用户同意隐私政策前阻止Flutter引擎启动 // 从根本上防止SensorsPlugin等敏感插件在协议前读取传感器 -// 上次更新: 修复overridePendingTransition弃用警告,兼容Android 14+新API +// 上次更新: 支持快捷方式action转发,确保快捷方式通过SplashActivity +// 不绕过协议守门,同时正确传递shortcut_action到MainActivity // ============================================================ // 设计说明: // 此Activity是应用的LAUNCHER入口,在AndroidManifest中声明。 @@ -16,6 +17,11 @@ // 5. 用户同意 → 持久化状态,启动MainActivity // 6. 用户拒绝 → 退出应用 // +// 快捷方式流程: +// shortcuts.xml中targetClass指向SplashActivity +// SplashActivity读取shortcut_action extra并转发给MainActivity +// 确保协议守门不被绕过 +// // 关键:MainActivity(Flutter引擎)只有在用户同意后才被启动, // 因此GeneratedPluginRegistrant.registerWith()中的SensorsPlugin // 等敏感插件不会在协议前被注册和触发onAttachedToActivity。 @@ -24,21 +30,26 @@ package apps.xy.xianyan import android.content.Intent -import android.net.Uri +import android.content.res.Configuration import android.os.Bundle import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import androidx.core.graphics.toColorInt +import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder import java.io.File +@Suppress( + "unused", // TAG 常量保留供日志使用 + "DEPRECATION" // overridePendingTransition 已有兼容处理 +) class SplashActivity : AppCompatActivity() { companion object { - private const val TAG = "SplashActivity" - // 原生SharedPreferences — 与MainActivity共用 private const val PREFS_NAME = "xianyan_prefs" private const val KEY_AGREEMENT_ACCEPTED = "agreement_accepted" @@ -54,16 +65,14 @@ class SplashActivity : AppCompatActivity() { super.onCreate(savedInstanceState) val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) - val hasAgreed = prefs.getBoolean(KEY_AGREEMENT_ACCEPTED, false) - when { // 已同意协议 → 直接进入Flutter - hasAgreed -> { + prefs.getBoolean(KEY_AGREEMENT_ACCEPTED, false) -> { startMainActivity() } // 老用户升级(有Flutter数据但未设置原生协议标志)→ 自动迁移 isExistingUser() -> { - prefs.edit().putBoolean(KEY_AGREEMENT_ACCEPTED, true).apply() + prefs.edit { putBoolean(KEY_AGREEMENT_ACCEPTED, true) } startMainActivity() } // 新用户 → 显示隐私协议对话框 @@ -73,6 +82,16 @@ class SplashActivity : AppCompatActivity() { } } + // ---- 快捷方式action读取 ---- + + /** + * 从Intent中读取快捷方式action + * shortcuts.xml中的shortcut通过extra传递action + */ + private fun getShortcutAction(): String? { + return intent?.getStringExtra("shortcut_action") + } + // ---- 老用户检测 ---- /** @@ -162,7 +181,9 @@ class SplashActivity : AppCompatActivity() { "• 🎤 麦克风 — 用于语音输入和录音\n" + "• 👆 生物识别 — 用于应用锁和隐私保护\n" + "• 📡 WiFi — 用于局域网文件传输\n" + - "• 🎵 音频/视频 — 用于媒体播放和编辑" + "• 🎵 音频/视频 — 用于媒体播放和编辑\n" + + "• 📋 剪贴板 — 用于用户主动粘贴/分享内容(仅用户操作时读取)\n" + + "• 📐 传感器 — 用于摇一摇等交互功能(仅用户触发时使用)" } // ---- 协议同意/拒绝处理 ---- @@ -173,9 +194,7 @@ class SplashActivity : AppCompatActivity() { */ private fun onAgreementAccepted() { getSharedPreferences(PREFS_NAME, MODE_PRIVATE) - .edit() - .putBoolean(KEY_AGREEMENT_ACCEPTED, true) - .apply() + .edit { putBoolean(KEY_AGREEMENT_ACCEPTED, true) } startMainActivity() } @@ -192,9 +211,17 @@ class SplashActivity : AppCompatActivity() { /** * 启动Flutter主Activity * 使用无动画过渡,保持启动页视觉连续性 + * 同时转发快捷方式action(如果有) + * 如果MainActivity已在运行(热启动),复用现有实例并传递action */ private fun startMainActivity() { val intent = Intent(this, MainActivity::class.java) + // 转发快捷方式action到MainActivity + getShortcutAction()?.let { action -> + intent.putExtra("shortcut_action", action) + } + // 如果MainActivity已在任务栈中,复用实例而非创建新实例 + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) overrideTransitionCompat(0, 0) finish() @@ -218,12 +245,13 @@ class SplashActivity : AppCompatActivity() { /** * 在浏览器中打开URL + * 使用 createChooser 确保走外部浏览器,避免匹配应用自身的 Deep Link */ private fun openUrl(url: String) { try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } catch (e: Exception) { + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(Intent.createChooser(intent, null)) + } catch (_: Exception) { // 无浏览器可用,忽略 } } @@ -231,16 +259,17 @@ class SplashActivity : AppCompatActivity() { /** * 暗色模式适配:调整自定义布局中的文字颜色 */ + @Suppress("NewApi") // minSdk=28,实际不会触发 private fun adaptDarkMode(root: View) { val isDark = resources.configuration.uiMode and - android.content.res.Configuration.UI_MODE_NIGHT_MASK == - android.content.res.Configuration.UI_MODE_NIGHT_YES + Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES if (!isDark) return - val titleColor = android.graphics.Color.parseColor("#E0E0E0") - val bodyColor = android.graphics.Color.parseColor("#B0B0B0") - val linkColor = android.graphics.Color.parseColor("#90CAF9") + val titleColor = "#E0E0E0".toColorInt() + val bodyColor = "#B0B0B0".toColorInt() + val linkColor = "#90CAF9".toColorInt() adjustColors(root, titleColor, bodyColor, linkColor) } diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt index 5599f1be..48da8854 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt @@ -1,10 +1,10 @@ /** * CheckinProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 每日签到桌面小部件Provider * 作用: 在Android桌面展示连续签到天数和快捷签到入口,支持深色主题 - * 上次更新内容: 初始创建,支持light/dark双主题布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class CheckinProvider : HomeWidgetProvider() { - override fun onUpdate( +class CheckinProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt index 9a330101..e6101a45 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt @@ -1,10 +1,10 @@ /** * CountdownProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 倒计时桌面小部件Provider * 作用: 在Android桌面展示自定义倒计时事件天数,支持深色主题 - * 上次更新内容: 初始创建,支持light/dark双主题布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,12 +15,11 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider import java.time.LocalDate import java.time.temporal.ChronoUnit -class CountdownProvider : HomeWidgetProvider() { - override fun onUpdate( +class CountdownProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt index f5cba66c..5691466e 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt @@ -1,10 +1,10 @@ /** * CtcLatestNoteProvider.kt * 创建时间: 2026-06-15 - * 更新时间: 2026-06-15 + * 更新时间: 2026-06-17 * 名称: CTC最新笔记桌面小部件Provider * 作用: 在Android桌面展示CTC最新笔记的钥匙名和内容预览,支持深色主题 - * 上次更新内容: 初始创建,支持light/dark双主题布局,点击跳转/ctc路由 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class CtcLatestNoteProvider : HomeWidgetProvider() { - override fun onUpdate( +class CtcLatestNoteProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt index 85d339e1..8ef6898a 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt @@ -1,10 +1,10 @@ /** * DailyCardProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 日签卡片桌面小部件Provider * 作用: 在Android桌面展示精美日签卡片,含日期、句子和作者,支持深色主题 - * 上次更新内容: 新增dark主题支持,根据widget_theme_mode切换布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class DailyCardProvider : HomeWidgetProvider() { - override fun onUpdate( +class DailyCardProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt index c0f5cc51..5e9bd29b 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt @@ -1,10 +1,10 @@ /** * DailySentenceProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 每日一句桌面小部件Provider * 作用: 在Android桌面展示每日精选句子和作者,支持深色主题 - * 上次更新内容: 新增dark主题支持,根据widget_theme_mode切换布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class DailySentenceProvider : HomeWidgetProvider() { - override fun onUpdate( +class DailySentenceProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt index 5c330fae..dfcb0d81 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt @@ -1,10 +1,10 @@ /** * FortuneProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 每日运势桌面小部件Provider * 作用: 在Android桌面展示每日运势和幸运关键词,支持深色主题 - * 上次更新内容: 初始创建,支持light/dark双主题布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class FortuneProvider : HomeWidgetProvider() { - override fun onUpdate( +class FortuneProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt index d1ab153d..a1ea8d98 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt @@ -1,10 +1,10 @@ /** * PomodoroProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 番茄钟桌面小部件Provider * 作用: 在Android桌面展示番茄钟倒计时和状态,支持深色主题 - * 上次更新内容: 初始创建,支持light/dark双主题布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class PomodoroProvider : HomeWidgetProvider() { - override fun onUpdate( +class PomodoroProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/PrivacyAwareHomeWidgetProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PrivacyAwareHomeWidgetProvider.kt new file mode 100644 index 00000000..1e06ba78 --- /dev/null +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PrivacyAwareHomeWidgetProvider.kt @@ -0,0 +1,111 @@ +/** + * PrivacyAwareHomeWidgetProvider.kt + * 创建时间: 2026-06-17 + * 更新时间: 2026-06-17 + * 名称: 隐私协议感知的桌面小部件基类 + * 作用: 所有Widget Provider的基类,在用户未同意隐私政策前阻止Widget更新, + * 防止应用退出后被系统广播触发自启动导致隐私合规问题 + * 上次更新内容: 初始创建,基于HomeWidgetProvider增加隐私协议守门逻辑 + */ +package apps.xy.xianyan.widget + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import android.view.View +import android.widget.RemoteViews +import apps.xy.xianyan.R +import es.antonborri.home_widget.HomeWidgetProvider + +/** + * 隐私协议感知的桌面小部件基类 + * + * 核心设计: + * - 在 onUpdate 中检查原生 SharedPreferences 的 agreement_accepted 标志 + * - 未同意隐私政策时,显示"请先同意隐私政策"占位视图,不读取任何业务数据 + * - 已同意隐私政策时,调用子类实现的 [onUpdateWithAgreement] 执行正常更新 + * + * 合规依据: + * - 《个人信息保护法》要求:处理个人信息前需取得个人同意 + * - 应用商店审核要求:未同意隐私政策前不得自启动或读取用户数据 + * - androidx.glance.appwidget 自启动问题修复:即使系统触发 APPWIDGET_UPDATE, + * 未同意协议也不执行任何数据操作 + */ +abstract class PrivacyAwareHomeWidgetProvider : HomeWidgetProvider() { + + companion object { + private const val TAG = "PrivacyWidget" + // 与 SplashActivity / MainActivity 共用的原生 SharedPreferences + private const val PREFS_NAME = "xianyan_prefs" + private const val KEY_AGREEMENT_ACCEPTED = "agreement_accepted" + } + + /** + * 检查用户是否已同意隐私政策 + * 读取原生 SharedPreferences,与 SplashActivity 使用同一存储 + */ + private fun isAgreementAccepted(context: Context): Boolean { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + return prefs.getBoolean(KEY_AGREEMENT_ACCEPTED, false) + } + + /** + * HomeWidgetProvider 的 onUpdate 入口 + * 在此进行隐私协议检查,决定是否执行业务更新 + */ + final override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) { + if (!isAgreementAccepted(context)) { + // 未同意隐私政策 — 显示占位视图,不读取任何业务数据 + Log.w(TAG, "${this::class.simpleName}: 隐私政策未同意,显示占位视图") + showPrivacyPlaceholder(context, appWidgetManager, appWidgetIds) + return + } + + // 已同意隐私政策 — 执行子类的正常更新逻辑 + onUpdateWithAgreement(context, appWidgetManager, appWidgetIds, widgetData) + } + + /** + * 显示隐私政策未同意时的占位视图 + * 提示用户需先打开应用同意隐私政策,点击可跳转到 SplashActivity + */ + private fun showPrivacyPlaceholder( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + appWidgetIds.forEach { widgetId -> + val views = RemoteViews(context.packageName, R.layout.widget_privacy_placeholder).apply { + // 点击跳转到应用(通过 SplashActivity 的 LAUNCHER intent) + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + if (launchIntent != null) { + val pendingIntent = android.app.PendingIntent.getActivity( + context, + widgetId, + launchIntent, + android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE + ) + setOnClickPendingIntent(R.id.widget_privacy_container, pendingIntent) + } + } + appWidgetManager.updateAppWidget(widgetId, views) + } + } + + /** + * 子类实现的更新逻辑,仅在用户已同意隐私政策后调用 + * 与 HomeWidgetProvider.onUpdate 签名一致 + */ + abstract fun onUpdateWithAgreement( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) +} diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt index b679fdf7..c2accbe1 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt @@ -1,10 +1,10 @@ /** * ReadlaterProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 稍后读桌面小部件Provider * 作用: 在Android桌面展示稍后读未读数量和预览,支持深色主题 - * 上次更新内容: 新增dark主题支持,根据widget_theme_mode切换布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class ReadlaterProvider : HomeWidgetProvider() { - override fun onUpdate( +class ReadlaterProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt index 7ac6b44d..87e74756 100644 --- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt @@ -1,10 +1,10 @@ /** * SolarTermProvider.kt * 创建时间: 2026-05-19 - * 更新时间: 2026-05-19 + * 更新时间: 2026-06-17 * 名称: 节气诗词桌面小部件Provider * 作用: 在Android桌面展示当前节气与对应诗词,支持深色主题 - * 上次更新内容: 初始创建,支持light/dark双主题布局 + * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新 */ package apps.xy.xianyan.widget @@ -15,10 +15,9 @@ import android.widget.RemoteViews import apps.xy.xianyan.R import apps.xy.xianyan.MainActivity import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider -class SolarTermProvider : HomeWidgetProvider() { - override fun onUpdate( +class SolarTermProvider : PrivacyAwareHomeWidgetProvider() { + override fun onUpdateWithAgreement( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, diff --git a/android/app/src/main/res/layout/widget_privacy_placeholder.xml b/android/app/src/main/res/layout/widget_privacy_placeholder.xml new file mode 100644 index 00000000..31008480 --- /dev/null +++ b/android/app/src/main/res/layout/widget_privacy_placeholder.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_shortcut.png new file mode 100644 index 0000000000000000000000000000000000000000..2d89b837823ba0986b8ccf9d5a92745094e7119f GIT binary patch literal 5537 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_shortcut.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cf72ef2fdf17bd6a5dd4e9e4b6932228828e88 GIT binary patch literal 3160 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_shortcut.png new file mode 100644 index 0000000000000000000000000000000000000000..e8bf90f39fbfb58144a5ccfc834dac57432f894e GIT binary patch literal 15867 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≷AG%A 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桌面快捷专注计时 当前节气与对应诗词 连续签到天数和快捷签到 + 最新笔记内容预览 闲言数据管理 主题个性化 打开主题个性化设置 diff --git a/android/app/src/main/res/xml/shortcuts.xml b/android/app/src/main/res/xml/shortcuts.xml index ac269f21..70ee60da 100644 --- a/android/app/src/main/res/xml/shortcuts.xml +++ b/android/app/src/main/res/xml/shortcuts.xml @@ -2,11 +2,13 @@ + - - + android:targetClass="apps.xy.xianyan.MainActivity" + android:data="xianyan://shortcut/action_theme" /> + + - - + android:targetClass="apps.xy.xianyan.MainActivity" + android:data="xianyan://shortcut/action_search" /> diff --git a/installer.iss b/installer.iss new file mode 100644 index 00000000..259b35a0 --- /dev/null +++ b/installer.iss @@ -0,0 +1,77 @@ +; ============================================================================ +; 闲言APP - Inno Setup 安装脚本 +; ============================================================================ +; 使用方法: +; 方式一(推荐,自动读取版本号): +; .\scripts\package_windows.ps1 +; 方式二(手动指定版本号): +; & "d:\Program Files (x86)\Inno Setup 6\ISCC.exe" /DAppVer=6.78.0 installer.iss +; 方式三(flutter_distributor): +; $env:PATH = "E:\cache\pub\bin;" + $env:PATH +; flutter_distributor package --platform windows --target inno +; +; 前置条件: +; 1. 已安装 Inno Setup 6: https://jrsoftware.org/isdl.php +; 2. 已下载中文语言包到 Inno Setup Languages 目录 +; 3. 已构建 Flutter 应用: flutter build windows +; ============================================================================ + +; ---- 动态版本号(通过 ISCC 命令行 /DAppVer=x.x.x 传入) ---- +; 由 package_windows.ps1 自动传递,无需手动设置 +#ifndef AppVer + #define AppVer "0.0.0" +#endif + +[Setup] +; ---- 应用信息 ---- +AppName=闲言 +AppVersion={#AppVer} +AppPublisher=微风暴工作室 +AppPublisherURL=https://www.wktyl.com +AppSupportURL=https://www.wktyl.com + +; ---- 安装路径 ---- +DefaultDirName={autopf}\xianyan +DefaultGroupName=闲言 +UninstallDisplayName=闲言 + +; ---- 输出配置 ---- +OutputDir=dist +OutputBaseFilename=闲言_Setup_{#AppVer} + +; ---- 压缩配置 ---- +Compression=lzma2/max +SolidCompression=yes + +; ---- 图标和权限 ---- +SetupIconFile=windows\runner\resources\app_icon.ico +PrivilegesRequired=lowest +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible + +[Languages] +Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" + +[Tasks] +Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加图标:"; Flags: unchecked + +[Files] +Source: "build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\闲言"; Filename: "{app}\xianyan.exe" +Name: "{autodesktop}\闲言"; Filename: "{app}\xianyan.exe"; Tasks: desktopicon + +[Run] +Filename: "{app}\xianyan.exe"; Description: "启动闲言"; Flags: nowait postinstall skipifsilent + +[Code] +procedure CurStepChanged(CurStep: TSetupStep); +var + ResultCode: Integer; +begin + if CurStep = ssPostInstall then + begin + Exec('ie4uinit.exe', '-show', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + end; +end; diff --git a/lib/app/app.dart b/lib/app/app.dart index dbe56e94..0366079c 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 应用根组件 /// 创建时间: 2026-04-20 -/// 更新时间: 2026-06-06 +/// 更新时间: 2026-06-16 /// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate + AppLockOverlay -/// 上次更新: 修复安卓端冷启动快捷方式闪退,添加addPostFrameCallback和异常保护 +/// 上次更新: 标准端路径根据liquidGlass平台能力决定是否使用GlassTheme包裹,桌面端跳过避免Impeller警告 /// ============================================================ import 'dart:async'; @@ -25,6 +25,7 @@ import 'package:flutter/services.dart'; import '../core/services/device/quick_actions_service.dart'; import '../core/services/device/macos_platform_service.dart'; +import '../core/services/device/windows_platform_service.dart'; import '../core/services/data/home_widget_service.dart'; import '../core/storage/database/app_database.dart'; import '../core/services/ui/status_bar_service.dart'; @@ -37,6 +38,7 @@ import '../core/services/device/app_lock_service.dart'; import '../core/services/performance/app_lifecycle_gate.dart'; import '../core/theme/app_theme.dart'; import '../core/utils/logger.dart'; +import '../core/utils/platform/platform_capability.dart'; import '../core/utils/platform/platform_utils.dart' as pu; import '../core/network/api_client.dart'; import '../core/providers/connectivity_provider.dart'; @@ -330,6 +332,7 @@ class _XianyanAppState extends ConsumerState WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; MacosPlatformService.syncTheme(isDark); + WindowsPlatformService.syncTheme(isDark); } } @@ -438,6 +441,7 @@ class _XianyanAppState extends ConsumerState Brightness.dark, }; MacosPlatformService.syncTheme(effectiveIsDark); + WindowsPlatformService.syncTheme(effectiveIsDark); return Directionality( textDirection: textDirection, @@ -542,30 +546,35 @@ class _XianyanAppState extends ConsumerState }, ); - return GlassTheme( - data: GlassThemeData( - light: GlassThemeVariant( - settings: GlassThemeSettings( - thickness: 20.0, - blur: settings.glassEnabled ? 2.0 : 0.0, - refractiveIndex: 1.4, - lightIntensity: 0.8, - ambientStrength: 0.4, - saturation: 1.0, - ), - ), - dark: GlassThemeVariant( - settings: GlassThemeSettings( - thickness: 28.0, - blur: settings.glassEnabled ? 3.0 : 0.0, - lightIntensity: 1.0, - refractiveIndex: 1.2, - saturation: 1.0, - ), - ), - ), - child: materialApp, + final useGlass = PlatformCapabilities.supports( + CapabilityKey.liquidGlass, ); + return useGlass + ? GlassTheme( + data: GlassThemeData( + light: GlassThemeVariant( + settings: GlassThemeSettings( + thickness: 20.0, + blur: settings.glassEnabled ? 2.0 : 0.0, + refractiveIndex: 1.4, + lightIntensity: 0.8, + ambientStrength: 0.4, + saturation: 1.0, + ), + ), + dark: GlassThemeVariant( + settings: GlassThemeSettings( + thickness: 28.0, + blur: settings.glassEnabled ? 3.0 : 0.0, + lightIntensity: 1.0, + refractiveIndex: 1.2, + saturation: 1.0, + ), + ), + ), + child: materialApp, + ) + : materialApp; } final iconMode = generalSettings.iconMode; @@ -575,46 +584,47 @@ class _XianyanAppState extends ConsumerState return AppIconModeScope( mode: iconMode, child: KeyboardBackHandler( - child: Stack( - children: [ - buildApp(), - if (isLocked) const Positioned.fill(child: AppLockOverlay()), - if (!connectivity.isConnected) - Positioned( - top: 0, - left: 0, - right: 0, - child: SafeArea( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - color: CupertinoColors.systemGrey6.withValues( - alpha: 0.95, - ), - child: Row( - children: [ - const Icon( - CupertinoIcons.wifi_exclamationmark, - size: 16, - color: CupertinoColors.systemGrey, - ), - const SizedBox(width: 8), - Text( - '网络已断开,部分功能可能不可用', - style: AppTypography.footnote.copyWith( + child: Stack( + children: [ + buildApp(), + if (isLocked) + const Positioned.fill(child: AppLockOverlay()), + if (!connectivity.isConnected) + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + color: CupertinoColors.systemGrey6.withValues( + alpha: 0.95, + ), + child: Row( + children: [ + const Icon( + CupertinoIcons.wifi_exclamationmark, + size: 16, color: CupertinoColors.systemGrey, ), - ), - ], + const SizedBox(width: 8), + Text( + '网络已断开,部分功能可能不可用', + style: AppTypography.footnote.copyWith( + color: CupertinoColors.systemGrey, + ), + ), + ], + ), ), ), ), - ), - ], + ], + ), ), - ), ); }, ), diff --git a/lib/core/services/clipboard_monitor_service.dart b/lib/core/services/clipboard_monitor_service.dart index 5f360eaf..e9c94a6b 100644 --- a/lib/core/services/clipboard_monitor_service.dart +++ b/lib/core/services/clipboard_monitor_service.dart @@ -1,16 +1,16 @@ /// ============================================================ /// 闲言APP — 剪贴板链接监控服务 /// 创建时间: 2026-05-15 -/// 更新时间: 2026-06-10 +/// 更新时间: 2026-06-16 /// 作用: 被动检测剪贴板中的URL链接并提示保存到稍后读 -/// 上次更新: 移除定时轮询,改为被动触发(隐私合规),仅用户主动操作时检查 +/// 上次更新: 使用ClipboardBridge统一入口,含隐私协议守卫, +/// 未同意协议时剪贴板读取返回null,不再触发保存 /// ============================================================ import 'package:xianyan/core/utils/data/pattern_utils.dart'; import 'dart:async'; -import 'package:flutter/services.dart'; - +import '../../core/utils/platform/clipboard_bridge.dart'; import '../../core/storage/kv_storage.dart'; import '../../core/utils/logger.dart'; import '../../shared/widgets/feedback/app_toast.dart'; @@ -62,8 +62,7 @@ class ClipboardMonitorService { _lastCheckTime = now; try { - final clipboardData = await Clipboard.getData(Clipboard.kTextPlain); - final text = clipboardData?.text; + final text = await ClipboardBridge.getData(); if (text == null || text.isEmpty || text == _lastClipboardText) return; diff --git a/lib/core/services/data/home_widget_service.dart b/lib/core/services/data/home_widget_service.dart index 673101a8..9048e897 100644 --- a/lib/core/services/data/home_widget_service.dart +++ b/lib/core/services/data/home_widget_service.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 桌面小组件数据管理服务 /// 创建时间: 2026-05-15 -/// 更新时间: 2026-06-05 +/// 更新时间: 2026-06-16 /// 作用: 基于home_widget库管理桌面小组件数据推送与交互 -/// 上次更新: 新增PlatformCapabilities能力查询补充homeWidget判断 +/// 上次更新: 添加平台检测,Windows/Linux桌面端跳过HomeWidget调用防止MissingPluginException /// ============================================================ import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -69,12 +69,20 @@ class HomeWidgetService { bool get isInitialized => _initialized; + /// 当前平台是否支持桌面小组件(仅 iOS/Android/macOS 支持,Windows/Linux 跳过) + bool get _isPlatformSupported => !pu.isDesktop || pu.isMacOS; + // ============================================================ // 初始化 // ============================================================ Future init() async { if (_initialized) return; + if (!_isPlatformSupported) { + _initialized = true; + Log.i('HomeWidgetService: 当前平台不支持桌面小组件,跳过初始化'); + return; + } try { await HomeWidget.setAppGroupId(_appGroupId); @@ -92,6 +100,7 @@ class HomeWidgetService { // ============================================================ Future pushInitialData() async { + if (!_isPlatformSupported) return; try { await _ensureInit(); @@ -163,6 +172,7 @@ class HomeWidgetService { // ============================================================ Future updateReadlaterCount(int count) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); await HomeWidget.saveWidgetData(_keyReadlaterCount, count); @@ -178,6 +188,7 @@ class HomeWidgetService { // ============================================================ Future updateReadlaterPreview(String text, String? author) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); final safeText = text.isEmpty @@ -203,6 +214,7 @@ class HomeWidgetService { // ============================================================ Future updateDailySentence(String text, String author) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); final safeText = text.isEmpty ? '生活不止眼前的苟且' : text; @@ -224,6 +236,7 @@ class HomeWidgetService { // ============================================================ Future handleWidgetClick() async { + if (!_isPlatformSupported) return; try { await _ensureInit(); final data = await HomeWidget.getWidgetData('clicked_data'); @@ -255,7 +268,10 @@ class HomeWidgetService { final uri = Uri.tryParse(data); final widgetType = uri?.queryParameters['widget']; Log.i('HomeWidgetService: Widget刷新请求 — $widgetType'); - KvStorage.setString(_keyPendingNavRoute, '/home?widget_refresh=$widgetType'); + KvStorage.setString( + _keyPendingNavRoute, + '/home?widget_refresh=$widgetType', + ); return; } // 处理 Widget 交互操作(iOS 17+ AppIntent) @@ -263,7 +279,9 @@ class HomeWidgetService { final uri = Uri.tryParse(data); final actionType = uri?.queryParameters['type']; final contentType = uri?.queryParameters['contentType']; - Log.i('HomeWidgetService: Widget交互操作 — type=$actionType, contentType=$contentType'); + Log.i( + 'HomeWidgetService: Widget交互操作 — type=$actionType, contentType=$contentType', + ); _handleInteractiveAction(actionType ?? '', contentType); return; } @@ -299,16 +317,27 @@ class HomeWidgetService { break; case 'next': // 切换下一条内容 - 触发刷新并切换 - final widgetType = Uri.tryParse(KvStorage.getString('clicked_data') ?? '')?.queryParameters['widget']; - KvStorage.setString(_keyPendingNavRoute, '/home?widget_action=next&widget=$widgetType'); + final widgetType = Uri.tryParse( + KvStorage.getString('clicked_data') ?? '', + )?.queryParameters['widget']; + KvStorage.setString( + _keyPendingNavRoute, + '/home?widget_action=next&widget=$widgetType', + ); break; case 'checkin': // 执行签到 - KvStorage.setString(_keyPendingNavRoute, '/signin?widget_action=checkin'); + KvStorage.setString( + _keyPendingNavRoute, + '/signin?widget_action=checkin', + ); break; case 'save_card': // 保存日签卡片 - KvStorage.setString(_keyPendingNavRoute, '/home?widget_action=save_card'); + KvStorage.setString( + _keyPendingNavRoute, + '/home?widget_action=save_card', + ); break; default: Log.w('HomeWidgetService: 未知的交互操作类型 — $actionType'); @@ -330,6 +359,7 @@ class HomeWidgetService { // ============================================================ void registerInteractivityCallback() { + if (!_isPlatformSupported) return; try { HomeWidget.registerInteractivityCallback(_backgroundCallback); handleWidgetClick(); @@ -352,7 +382,10 @@ class HomeWidgetService { if (route == '_widget_refresh_action') { final widgetType = uri.queryParameters['widget']; Log.i('HomeWidgetService: 后台Widget刷新请求 — $widgetType'); - KvStorage.setString(_keyPendingNavRoute, '/home?widget_refresh=$widgetType'); + KvStorage.setString( + _keyPendingNavRoute, + '/home?widget_refresh=$widgetType', + ); } else if (route == '_widget_interactive_action') { // 处理 iOS 17+ AppIntent 交互操作 final actionType = uri.queryParameters['type']; @@ -374,6 +407,7 @@ class HomeWidgetService { } Future pushThemeMode(bool isDark) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); await HomeWidget.saveWidgetData( @@ -387,6 +421,7 @@ class HomeWidgetService { } Future updateFortune(String text, String keyword) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); final safeText = text.isEmpty ? '今日运势不错' : text; @@ -401,6 +436,7 @@ class HomeWidgetService { } Future updateCountdown(String title, DateTime target) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); await HomeWidget.saveWidgetData(_keyCountdownTitle, title); @@ -416,6 +452,7 @@ class HomeWidgetService { } Future updatePomodoro(int remainingSeconds, String state) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); await HomeWidget.saveWidgetData( @@ -431,6 +468,7 @@ class HomeWidgetService { } Future updateSolarTerm(String name, String poem) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); final safeName = name.isEmpty ? '立春' : name; @@ -445,6 +483,7 @@ class HomeWidgetService { } Future updateCheckin(int days, bool todayDone) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); await HomeWidget.saveWidgetData(_keyCheckinDays, days); @@ -462,6 +501,7 @@ class HomeWidgetService { String? sentenceId, String mood = 'happy', }) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); final safeContent = content.isEmpty @@ -498,6 +538,7 @@ class HomeWidgetService { // ============================================================ Future updateWidget(WidgetType type) async { + if (!_isPlatformSupported) return; try { await _ensureInit(); @@ -560,6 +601,7 @@ class HomeWidgetService { // ============================================================ Future> debugGetAllData() async { + if (!_isPlatformSupported) return {}; try { await _ensureInit(); return { diff --git a/lib/core/services/device/haptic_service.dart b/lib/core/services/device/haptic_service.dart index 7e14ff1f..36fb0fa8 100644 --- a/lib/core/services/device/haptic_service.dart +++ b/lib/core/services/device/haptic_service.dart @@ -1,14 +1,15 @@ /// ============================================================ /// 闲言APP — 触觉反馈服务 /// 创建时间: 2026-05-07 -/// 更新时间: 2026-06-05 +/// 更新时间: 2026-06-16 /// 作用: 统一管理触觉反馈,4档位控制(关闭/轻柔/标准/强烈) /// 优先使用flutter_vibrate,不可用时降级HapticFeedback -/// 上次更新: 新增PlatformCapabilities能力查询补充flutterVibrate判断 +/// 上次更新: 增加隐私协议守卫,未同意协议时不执行震动反馈 /// ============================================================ import 'package:flutter/services.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:xianyan/core/storage/kv_storage.dart'; import 'package:xianyan/core/utils/logger.dart' show Log, LogCategory; import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; import 'package:xianyan/core/utils/platform/platform_capability.dart'; @@ -47,6 +48,9 @@ class HapticService { static VibrationLevel get level => _level; + /// 隐私协议守卫:未同意协议时不执行震动 + static bool get _agreementAccepted => KvStorage.isOnboardingCompleted; + static void setLevel(VibrationLevel level) { _level = level; } @@ -59,15 +63,26 @@ class HapticService { // 鸿蒙端直接使用 HapticFeedback 降级方案 // [PlatformCapabilities] 统一能力查询: flutterVibrate - if (pu.isOhos || !PlatformCapabilities.supports(CapabilityKey.flutterVibrate)) { + if (pu.isOhos || + !PlatformCapabilities.supports(CapabilityKey.flutterVibrate)) { _canVibrate = false; - Log.d('HapticService: 使用HapticFeedback降级方案 (PlatformCapabilities.flutterVibrate=${PlatformCapabilities.supports(CapabilityKey.flutterVibrate)})', null, null, LogCategory.haptic); + Log.d( + 'HapticService: 使用HapticFeedback降级方案 (PlatformCapabilities.flutterVibrate=${PlatformCapabilities.supports(CapabilityKey.flutterVibrate)})', + null, + null, + LogCategory.haptic, + ); return; } try { _canVibrate = await Vibrate.canVibrate; - Log.d('HapticService: 初始化完成 (canVibrate=$_canVibrate, level=${_level.label})', null, null, LogCategory.haptic); + Log.d( + 'HapticService: 初始化完成 (canVibrate=$_canVibrate, level=${_level.label})', + null, + null, + LogCategory.haptic, + ); } catch (e) { Log.d('HapticService: flutter_vibrate不可用,使用HapticFeedback降级'); _canVibrate = false; @@ -76,6 +91,7 @@ class HapticService { /// 通用冲击反馈 — 根据档位自动选择强度 static void impact() { + if (!_agreementAccepted) return; switch (_level) { case VibrationLevel.off: return; @@ -90,24 +106,28 @@ class HapticService { /// 选择反馈 — 切换Tab/滑动选择等 static void selection() { + if (!_agreementAccepted) return; if (_level == VibrationLevel.off) return; _doSelection(); } /// 轻柔反馈 — 仅轻柔/标准/强烈时触发 static void light() { + if (!_agreementAccepted) return; if (_level == VibrationLevel.off) return; _doLight(); } /// 中等反馈 — 仅标准/强烈时触发 static void medium() { + if (!_agreementAccepted) return; if (_level.index < VibrationLevel.medium.index) return; _doMedium(); } /// 强烈反馈 — 仅强烈时触发 static void heavy() { + if (!_agreementAccepted) return; if (_level.index < VibrationLevel.heavy.index) return; _doHeavy(); } diff --git a/lib/core/services/device/quick_actions_service.dart b/lib/core/services/device/quick_actions_service.dart index b2abb0da..a042318e 100644 --- a/lib/core/services/device/quick_actions_service.dart +++ b/lib/core/services/device/quick_actions_service.dart @@ -1,10 +1,10 @@ // ============================================================ // 闲言APP — 快捷操作服务 // 创建时间: 2026-05-31 -/// 更新时间: 2026-06-13 +/// 更新时间: 2026-06-16 /// 作用: 管理主屏幕快捷操作(Quick Actions / App Shortcuts) -/// 上次更新: 添加安卓原生shortcut MethodChannel监听,修复XML快捷方式跳转 -// 跨平台: iOS(UIApplicationShortcutItems) + Android(App Shortcuts) +/// 上次更新: 使用ShortcutManager API创建快捷方式,修复图标和跳转问题 +// 跨平台: iOS(UIApplicationShortcutItems) + Android(ShortcutManager) // + 鸿蒙(module.json5 shortcuts + MethodChannel) // ============================================================ @@ -32,6 +32,11 @@ class QuickActionsService { 'apps.xy.xianyan/shortcut', ); + /// 安卓ShortcutManager MethodChannel — 创建和管理快捷方式 + static const MethodChannel _androidShortcutManagerChannel = MethodChannel( + 'apps.xy.xianyan/shortcut_manager', + ); + static bool _initialized = false; static String? _pendingAction; @@ -133,6 +138,29 @@ class QuickActionsService { } catch (e) { Log.e('🚀 [QuickActions] 设置快捷操作失败', e); } + + // 安卓端:使用ShortcutManager API创建快捷方式 + // 这会覆盖quick_actions插件创建的快捷方式,确保使用应用图标 + if (pu.isAndroid) { + await _createAndroidShortcuts(); + } + } + + /// 使用原生ShortcutManager API创建安卓桌面快捷方式 + /// 确保使用应用图标,而非系统默认图标 + static Future _createAndroidShortcuts() async { + try { + final result = await _androidShortcutManagerChannel.invokeMethod( + 'createShortcuts', + ); + if (result == true) { + Log.i('🚀 [QuickActions] ShortcutManager快捷方式创建成功'); + } + } on PlatformException catch (e) { + Log.w('🚀 [QuickActions] ShortcutManager快捷方式创建失败: ${e.message}'); + } catch (e) { + Log.e('🚀 [QuickActions] ShortcutManager快捷方式创建异常', e); + } } static void _initOhos() { diff --git a/lib/core/services/device/windows_platform_service.dart b/lib/core/services/device/windows_platform_service.dart new file mode 100644 index 00000000..84906aaf --- /dev/null +++ b/lib/core/services/device/windows_platform_service.dart @@ -0,0 +1,47 @@ +/// ============================================================ +/// 闲言APP — Windows平台统一服务 +/// 创建时间: 2026-06-16 +/// 更新时间: 2026-06-16 +/// 作用: 集中管理所有Windows原生MethodChannel交互(标题栏主题同步) +/// 上次更新: 初始创建,支持标题栏深色模式切换 +/// ============================================================ + +import 'package:flutter/services.dart'; +import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; +import 'package:xianyan/core/utils/logger.dart'; + +class WindowsPlatformService { + WindowsPlatformService._(); + + static const _channel = MethodChannel('com.xianyan.windows'); + + // ============================================================ + // 主题同步 + // ============================================================ + + static bool _lastIsDark = false; + static bool _themeInitialized = false; + + /// 同步标题栏明暗模式 + static void syncTheme(bool isDark) { + if (!pu.isWindows) return; + if (_themeInitialized && _lastIsDark == isDark) return; + + _themeInitialized = true; + _lastIsDark = isDark; + + _invoke('setDarkMode', {'isDark': isDark}); + } + + // ============================================================ + // 内部工具 + // ============================================================ + + static void _invoke(String method, [dynamic arguments]) { + try { + _channel.invokeMethod(method, arguments); + } catch (e) { + Log.w('WindowsPlatformService.$method失败: $e'); + } + } +} diff --git a/lib/core/services/post_agreement_initializer.dart b/lib/core/services/post_agreement_initializer.dart index 45ccee58..8686af83 100644 --- a/lib/core/services/post_agreement_initializer.dart +++ b/lib/core/services/post_agreement_initializer.dart @@ -1,9 +1,10 @@ // ============================================================ // 闲言APP — 协议同意后初始化器 // 创建时间: 2026-05-30 -// 更新时间: 2026-06-10 +// 更新时间: 2026-06-16 // 作用: 将权限敏感的服务初始化延迟到用户同意协议后执行 -// 上次更新: 移除ClipboardMonitorService(改为被动触发,不在此初始化) +// 上次更新: 新增HapticService初始化(Vibrate.canVibrate触发原生插件), +// 确保未同意协议前不触发震动/传感器等原生插件 // ============================================================ import 'dart:io'; @@ -15,6 +16,7 @@ import '../utils/logger.dart'; import '../utils/platform/platform_utils.dart' as pu; import '../router/app_router.dart' show rootNavigatorKey; import 'auth/permission_service.dart'; +import 'device/haptic_service.dart'; import 'network/connectivity_service.dart'; import 'background/background_task_service.dart'; import 'notification/local_notification_service.dart'; @@ -38,6 +40,14 @@ class PostAgreementInitializer { Log.i('PostAgreementInitializer: 开始初始化权限敏感服务...'); + // 触觉反馈服务初始化(Vibrate.canVibrate会触发flutter_vibrate原生插件) + try { + await HapticService.init(); + Log.i('触觉反馈服务初始化完成'); + } catch (e, st) { + Log.e('触觉反馈服务初始化失败', e, st); + } + // iOS ATT授权请求(必须在协议同意后、其他服务初始化前请求) // 先检查当前授权状态,仅在未决定时请求,避免Info.plist缺少描述时原生崩溃 if (Platform.isIOS) { diff --git a/lib/core/utils/platform/clipboard_bridge.dart b/lib/core/utils/platform/clipboard_bridge.dart index d6272f43..7d4bdf62 100644 --- a/lib/core/utils/platform/clipboard_bridge.dart +++ b/lib/core/utils/platform/clipboard_bridge.dart @@ -1,13 +1,16 @@ /// ============================================================ -/// 闲言APP — 鸿蒙剪贴板桥接工具 +/// 闲言APP — 剪贴板桥接工具(含隐私协议守卫) /// 创建时间: 2026-05-17 -/// 更新时间: 2026-05-17 -/// 作用: 在鸿蒙平台上通过原生MethodChannel读取剪贴板, -/// 替代Flutter Clipboard.getData以避免READ_PASTEBOARD受限ACL权限 -/// 上次更新: 修复_isOhos检测逻辑,使用platform_utils.isOhos +/// 更新时间: 2026-06-16 +/// 作用: 统一剪贴板读取入口,鸿蒙平台通过原生MethodChannel读取, +/// 其他平台使用Flutter Clipboard;未同意隐私协议时禁止读取 +/// 上次更新: 增加隐私协议守卫,未同意协议时所有读取返回null/空, +/// 防止安卓端协议前读取剪贴板违反合规要求 /// ============================================================ import 'package:flutter/services.dart'; +import 'package:xianyan/core/storage/kv_storage.dart'; +import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; class ClipboardBridge { @@ -17,7 +20,21 @@ class ClipboardBridge { static bool get _isOhos => pu.isOhos; + // ============================================================ + // 隐私协议守卫:未同意协议时禁止读取剪贴板 + // ============================================================ + + /// 用户是否已同意隐私协议(引导完成即视为同意) + static bool get _agreementAccepted => KvStorage.isOnboardingCompleted; + + /// 读取剪贴板文本内容 + /// 未同意隐私协议时返回 null,并打印警告日志 static Future getData() async { + if (!_agreementAccepted) { + Log.w('ClipboardBridge: 隐私协议未同意,禁止读取剪贴板'); + return null; + } + if (_isOhos) { try { final result = await _channel.invokeMethod('Clipboard.getData'); @@ -32,7 +49,14 @@ class ClipboardBridge { return data?.text; } + /// 检查剪贴板是否有文本内容 + /// 未同意隐私协议时返回 false static Future hasStrings() async { + if (!_agreementAccepted) { + Log.w('ClipboardBridge: 隐私协议未同意,禁止检查剪贴板'); + return false; + } + if (_isOhos) { try { final result = await _channel.invokeMethod( diff --git a/lib/core/utils/platform/platform_capability.dart b/lib/core/utils/platform/platform_capability.dart index bfcf7a0a..e285eb30 100644 --- a/lib/core/utils/platform/platform_capability.dart +++ b/lib/core/utils/platform/platform_capability.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 平台能力注册表 /// 创建时间: 2026-06-05 -/// 更新时间: 2026-06-05 +/// 更新时间: 2026-06-16 /// 作用: 将分散的 if(pu.isOhos) 分支抽象为能力查询,便于维护和扩展 -/// 上次更新: 初始创建,定义19项平台能力键和注册表 +/// 上次更新: liquidGlass能力仅注册到支持Impeller的平台(iOS/Android/macOS),桌面端不注册避免警告 /// ============================================================ import 'platform_utils.dart' as pu; @@ -38,8 +38,10 @@ class CapabilityKey { // ---- 视觉效果 ---- /// 毛玻璃效果 (BackdropFilter) static const String backdropFilter = 'backdrop_filter'; + /// 液态玻璃 (LiquidGlass) static const String liquidGlass = 'liquid_glass'; + /// 重度动画 (闪烁/旋转/缩放组合) static const String heavyAnimation = 'heavy_animation'; @@ -58,6 +60,7 @@ class CapabilityKey { // ---- 分享/导出 ---- /// 分享Sheet static const String shareSheet = 'share_sheet'; + /// 导出文件 static const String fileExport = 'file_export'; @@ -68,6 +71,7 @@ class CapabilityKey { // ---- 媒体 ---- /// 画廊保存 static const String gallerySave = 'gallery_save'; + /// WebView 3D static const String webView3d = 'web_view_3d'; @@ -144,12 +148,25 @@ class PlatformCapabilities { static void _initOhos(PlatformCapabilities cap) { cap._register(CapabilityKey.localAuth, false, '鸿蒙端暂不支持生物识别,使用确认对话框替代'); - cap._register(CapabilityKey.flutterVibrate, false, '鸿蒙端使用HapticFeedback降级方案'); + cap._register( + CapabilityKey.flutterVibrate, + false, + '鸿蒙端使用HapticFeedback降级方案', + ); cap._register(CapabilityKey.quickActions, true); cap._register(CapabilityKey.homeWidget, true); - cap._register(CapabilityKey.backdropFilter, pu.OhosDeviceCapabilities.supportsBackdropFilter); - cap._register(CapabilityKey.liquidGlass, pu.OhosDeviceCapabilities.supportsLiquidGlass); - cap._register(CapabilityKey.heavyAnimation, pu.OhosDeviceCapabilities.supportsHeavyAnimation); + cap._register( + CapabilityKey.backdropFilter, + pu.OhosDeviceCapabilities.supportsBackdropFilter, + ); + cap._register( + CapabilityKey.liquidGlass, + pu.OhosDeviceCapabilities.supportsLiquidGlass, + ); + cap._register( + CapabilityKey.heavyAnimation, + pu.OhosDeviceCapabilities.supportsHeavyAnimation, + ); cap._register(CapabilityKey.pushNotification, false, '鸿蒙端推送通知暂未接入'); cap._register(CapabilityKey.filesystem, true); cap._register(CapabilityKey.usbTransfer, true); @@ -191,12 +208,18 @@ class PlatformCapabilities { // ============================================================ static void _initNative(PlatformCapabilities cap) { - cap._register(CapabilityKey.localAuth, pu.isIOS || pu.isAndroid || pu.isMacOS); + cap._register( + CapabilityKey.localAuth, + pu.isIOS || pu.isAndroid || pu.isMacOS, + ); cap._register(CapabilityKey.flutterVibrate, true); cap._register(CapabilityKey.quickActions, pu.isIOS || pu.isAndroid); cap._register(CapabilityKey.homeWidget, pu.isIOS || pu.isAndroid); cap._register(CapabilityKey.backdropFilter, true); - cap._register(CapabilityKey.liquidGlass, true); + cap._register( + CapabilityKey.liquidGlass, + pu.isIOS || pu.isAndroid || pu.isMacOS, + ); cap._register(CapabilityKey.heavyAnimation, true); cap._register(CapabilityKey.pushNotification, true); cap._register(CapabilityKey.filesystem, true); @@ -204,10 +227,16 @@ class PlatformCapabilities { cap._register(CapabilityKey.shareSheet, true); cap._register(CapabilityKey.fileExport, true); cap._register(CapabilityKey.gpu3d, !pu.isLinux); - cap._register(CapabilityKey.gallerySave, pu.isIOS || pu.isAndroid || pu.isMacOS); + cap._register( + CapabilityKey.gallerySave, + pu.isIOS || pu.isAndroid || pu.isMacOS, + ); cap._register(CapabilityKey.webView3d, true); cap._register(CapabilityKey.localNotification, true); - cap._register(CapabilityKey.calendar, pu.isIOS || pu.isAndroid || pu.isMacOS); + cap._register( + CapabilityKey.calendar, + pu.isIOS || pu.isAndroid || pu.isMacOS, + ); } // ============================================================ @@ -248,7 +277,8 @@ class PlatformCapabilities { static Map get unsupported { final result = {}; for (final entry in instance._capabilities.entries) { - if (!entry.value && instance._fallbackDescriptions.containsKey(entry.key)) { + if (!entry.value && + instance._fallbackDescriptions.containsKey(entry.key)) { result[entry.key] = instance._fallbackDescriptions[entry.key]!; } } diff --git a/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart b/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart index 594e57d9..8254214a 100644 --- a/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart +++ b/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:xianyan/core/theme/app_theme.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'package:xianyan/features/ctc/ctc.dart'; import 'package:xianyan/features/ctc/services/ctc_undo_stack.dart'; import 'package:xianyan/features/ctc/presentation/widgets/ctc_qr_sheet.dart'; @@ -570,13 +571,13 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB /// 粘贴 void _paste() async { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data?.text == null || data!.text!.isEmpty) return; + final text = await ClipboardBridge.getData(); + if (text == null || text.isEmpty) return; final sel = _contentController.selection; - final text = _contentController.text; - final newText = text.substring(0, sel.start) + data.text! + text.substring(sel.end); + final currentText = _contentController.text; + final newText = currentText.substring(0, sel.start) + text + currentText.substring(sel.end); _contentController.text = newText; - _contentController.selection = TextSelection.collapsed(offset: sel.start + data.text!.length); + _contentController.selection = TextSelection.collapsed(offset: sel.start + text.length); _onContentChanged(); } diff --git a/lib/features/ctc/presentation/pages/ctc_settings_page.dart b/lib/features/ctc/presentation/pages/ctc_settings_page.dart index 3e307bfd..f9c3f0c4 100644 --- a/lib/features/ctc/presentation/pages/ctc_settings_page.dart +++ b/lib/features/ctc/presentation/pages/ctc_settings_page.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/theme/app_theme.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'package:xianyan/features/ctc/ctc.dart'; import 'package:xianyan/features/ctc/presentation/pages/ctc_history_page.dart'; @@ -38,10 +39,24 @@ class CtcSettingsPage extends ConsumerWidget { // 同步设置 _sectionTitle(ext, '同步设置'), _settingsGroup(ext, [ - _toggleRow(ext, CupertinoIcons.brightness_solid, '屏幕常亮', '编辑笔记时保持屏幕不熄灭', - ext.iconTintYellow, settings.screenAlwaysOn, (v) => notifier.setScreenAlwaysOn(v)), - _toggleRow(ext, CupertinoIcons.arrow_2_circlepath, '自动同步', '每天限制 5000 次(含上传/下载/同步)', - ext.successColor, settings.autoSync, (v) => notifier.setAutoSync(v)), + _toggleRow( + ext, + CupertinoIcons.brightness_solid, + '屏幕常亮', + '编辑笔记时保持屏幕不熄灭', + ext.iconTintYellow, + settings.screenAlwaysOn, + (v) => notifier.setScreenAlwaysOn(v), + ), + _toggleRow( + ext, + CupertinoIcons.arrow_2_circlepath, + '自动同步', + '每天限制 5000 次(含上传/下载/同步)', + ext.successColor, + settings.autoSync, + (v) => notifier.setAutoSync(v), + ), ]), // 配额条 _buildQuotaBar(ext, settings), @@ -51,45 +66,117 @@ class CtcSettingsPage extends ConsumerWidget { // 同步选项 _sectionTitle(ext, '同步选项'), _settingsGroup(ext, [ - _toggleRow(ext, CupertinoIcons.arrow_down, '拉取同步', null, - ext.iconTintCyan, settings.pullEnabled, (v) => notifier.setPullEnabled(v)), - _toggleRow(ext, CupertinoIcons.arrow_up, '推送同步', null, - ext.iconTintYellow, settings.pushEnabled, (v) => notifier.setPushEnabled(v)), - _toggleRow(ext, CupertinoIcons.shuffle, '合并策略', '冲突时自动合并,超过1万字符截断', - ext.iconTintPurple, settings.mergeEnabled, (v) => notifier.setMergeEnabled(v)), - _segmentRow(ext, CupertinoIcons.timer, '同步频率', + _toggleRow( + ext, + CupertinoIcons.arrow_down, + '拉取同步', + null, + ext.iconTintCyan, + settings.pullEnabled, + (v) => notifier.setPullEnabled(v), + ), + _toggleRow( + ext, + CupertinoIcons.arrow_up, + '推送同步', + null, + ext.iconTintYellow, + settings.pushEnabled, + (v) => notifier.setPushEnabled(v), + ), + _toggleRow( + ext, + CupertinoIcons.shuffle, + '合并策略', + '冲突时自动合并,超过1万字符截断', + ext.iconTintPurple, + settings.mergeEnabled, + (v) => notifier.setMergeEnabled(v), + ), + _segmentRow( + ext, + CupertinoIcons.timer, + '同步频率', [const Text('5秒'), const Text('10秒')], - settings.syncFrequency.index, (i) => notifier.setSyncFrequency(CtcSyncFrequency.values[i])), + settings.syncFrequency.index, + (i) => notifier.setSyncFrequency(CtcSyncFrequency.values[i]), + ), ]), // 历史记录 _sectionTitle(ext, '历史记录'), _settingsGroup(ext, [ - _toggleRow(ext, CupertinoIcons.clock, '开启历史记录', null, - ext.successColor, settings.historyEnabled, (v) => notifier.setHistoryEnabled(v)), - _navRow(ext, CupertinoIcons.clock, '显示历史记录', '3 条', - ext.iconTintGrey, () => _showHistory(context, ref)), + _toggleRow( + ext, + CupertinoIcons.clock, + '开启历史记录', + null, + ext.successColor, + settings.historyEnabled, + (v) => notifier.setHistoryEnabled(v), + ), + _navRow( + ext, + CupertinoIcons.clock, + '显示历史记录', + '3 条', + ext.iconTintGrey, + () => _showHistory(context, ref), + ), ]), // 排版样式 _sectionTitle(ext, '排版样式'), _settingsGroup(ext, [ - _segmentRow(ext, CupertinoIcons.square_grid_2x2, '列表样式', + _segmentRow( + ext, + CupertinoIcons.square_grid_2x2, + '列表样式', [const Text('网格'), const Text('列表'), const Text('时间线')], - settings.layoutMode.index, (i) => notifier.setLayoutMode(CtcLayoutMode.values[i])), - _toggleRow(ext, CupertinoIcons.star, '设为主要', '主要笔记长存在顶部', - ext.iconTintYellow, true, null), + settings.layoutMode.index, + (i) => notifier.setLayoutMode(CtcLayoutMode.values[i]), + ), + _toggleRow( + ext, + CupertinoIcons.star, + '设为主要', + '主要笔记长存在顶部', + ext.iconTintYellow, + true, + null, + ), ]), // 元数据 _sectionTitle(ext, '元数据'), _settingsGroup(ext, [ - _toggleRow(ext, CupertinoIcons.clock, '添加时间', null, - ext.iconTintYellow, settings.addTimeMeta, (v) => notifier.setAddTimeMeta(v)), - _toggleRow(ext, CupertinoIcons.location, '添加地点', null, - ext.successColor, settings.addLocationMeta, (v) => notifier.setAddLocationMeta(v)), - _toggleRow(ext, CupertinoIcons.device_phone_portrait, '添加机型', null, - ext.iconTintBlue, settings.addDeviceMeta, (v) => notifier.setAddDeviceMeta(v)), + _toggleRow( + ext, + CupertinoIcons.clock, + '添加时间', + null, + ext.iconTintYellow, + settings.addTimeMeta, + (v) => notifier.setAddTimeMeta(v), + ), + _toggleRow( + ext, + CupertinoIcons.location, + '添加地点', + null, + ext.successColor, + settings.addLocationMeta, + (v) => notifier.setAddLocationMeta(v), + ), + _toggleRow( + ext, + CupertinoIcons.device_phone_portrait, + '添加机型', + null, + ext.iconTintBlue, + settings.addDeviceMeta, + (v) => notifier.setAddDeviceMeta(v), + ), ]), // 安全 @@ -101,20 +188,46 @@ class CtcSettingsPage extends ConsumerWidget { // 显示 _sectionTitle(ext, '显示'), _settingsGroup(ext, [ - _toggleRow(ext, CupertinoIcons.number, '显示行列', null, - ext.iconTintCyan, settings.showLineColumn, (v) => notifier.setShowLineColumn(v)), - _toggleRow(ext, CupertinoIcons.qrcode, '显示二维码', null, - ext.iconTintPurple, settings.showQrCode, (v) => notifier.setShowQrCode(v)), + _toggleRow( + ext, + CupertinoIcons.number, + '显示行列', + null, + ext.iconTintCyan, + settings.showLineColumn, + (v) => notifier.setShowLineColumn(v), + ), + _toggleRow( + ext, + CupertinoIcons.qrcode, + '显示二维码', + null, + ext.iconTintPurple, + settings.showQrCode, + (v) => notifier.setShowQrCode(v), + ), _disabledRow(ext, CupertinoIcons.search, '搜索仓库内容', '所在用户组无权限'), ]), // 数据管理 _sectionTitle(ext, '数据管理'), _settingsGroup(ext, [ - _navRow(ext, CupertinoIcons.arrow_down_doc, '导入笔记', '从JSON导入', - ext.successColor, () => _importNotes(context, ref)), - _navRow(ext, CupertinoIcons.arrow_up_doc, '导出全部笔记', 'JSON格式', - ext.iconTintBlue, () => _exportAllNotes(context, ref)), + _navRow( + ext, + CupertinoIcons.arrow_down_doc, + '导入笔记', + '从JSON导入', + ext.successColor, + () => _importNotes(context, ref), + ), + _navRow( + ext, + CupertinoIcons.arrow_up_doc, + '导出全部笔记', + 'JSON格式', + ext.iconTintBlue, + () => _exportAllNotes(context, ref), + ), ]), // 使用须知 @@ -134,7 +247,14 @@ class CtcSettingsPage extends ConsumerWidget { Widget _sectionTitle(AppThemeExtension ext, String title) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), - child: Text(title.toUpperCase(), style: TextStyle(fontSize: 13, color: ext.textSecondary, letterSpacing: 0.5)), + child: Text( + title.toUpperCase(), + style: TextStyle( + fontSize: 13, + color: ext.textSecondary, + letterSpacing: 0.5, + ), + ), ); } @@ -144,24 +264,43 @@ class CtcSettingsPage extends ConsumerWidget { decoration: BoxDecoration( color: ext.bgCard, borderRadius: BorderRadius.circular(12), - boxShadow: [BoxShadow(color: ext.iconPrimary.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 1))], + boxShadow: [ + BoxShadow( + color: ext.iconPrimary.withValues(alpha: 0.05), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], ), child: Column(children: children), ); } - Widget _toggleRow(AppThemeExtension ext, IconData icon, String title, String? subtitle, - Color iconColor, bool value, ValueChanged? onChanged) { + Widget _toggleRow( + AppThemeExtension ext, + IconData icon, + String title, + String? subtitle, + Color iconColor, + bool value, + ValueChanged? onChanged, + ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: ext.dividerOnCard, width: 0.5)), + border: Border( + bottom: BorderSide(color: ext.dividerOnCard, width: 0.5), + ), ), child: Row( children: [ Container( - width: 30, height: 30, - decoration: BoxDecoration(color: iconColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8)), + width: 30, + height: 30, + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + ), child: Center(child: Icon(icon, size: 16, color: iconColor)), ), const SizedBox(width: 12), @@ -169,9 +308,15 @@ class CtcSettingsPage extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: TextStyle(fontSize: 15, color: ext.textPrimary)), + Text( + title, + style: TextStyle(fontSize: 15, color: ext.textPrimary), + ), if (subtitle != null) - Text(subtitle, style: TextStyle(fontSize: 11, color: ext.textHint)), + Text( + subtitle, + style: TextStyle(fontSize: 11, color: ext.textHint), + ), ], ), ), @@ -187,29 +332,50 @@ class CtcSettingsPage extends ConsumerWidget { ); } - Widget _segmentRow(AppThemeExtension ext, IconData icon, String title, - List segments, int selectedIndex, ValueChanged onChanged) { + Widget _segmentRow( + AppThemeExtension ext, + IconData icon, + String title, + List segments, + int selectedIndex, + ValueChanged onChanged, + ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: ext.dividerOnCard, width: 0.5)), + border: Border( + bottom: BorderSide(color: ext.dividerOnCard, width: 0.5), + ), ), child: Row( children: [ Container( - width: 30, height: 30, - decoration: BoxDecoration(color: ext.iconTintBlue.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8)), + width: 30, + height: 30, + decoration: BoxDecoration( + color: ext.iconTintBlue.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + ), child: Center(child: Icon(icon, size: 16, color: ext.iconTintBlue)), ), const SizedBox(width: 12), - Expanded(child: Text(title, style: TextStyle(fontSize: 15, color: ext.textPrimary))), + Expanded( + child: Text( + title, + style: TextStyle(fontSize: 15, color: ext.textPrimary), + ), + ), SizedBox( width: segments.length * 60.0, child: CupertinoSlidingSegmentedControl( groupValue: selectedIndex, thumbColor: ext.bgCard, - children: {for (var i = 0; i < segments.length; i++) i: segments[i]}, - onValueChanged: (v) { if (v != null) onChanged(v); }, + children: { + for (var i = 0; i < segments.length; i++) i: segments[i], + }, + onValueChanged: (v) { + if (v != null) onChanged(v); + }, ), ), ], @@ -217,8 +383,14 @@ class CtcSettingsPage extends ConsumerWidget { ); } - Widget _navRow(AppThemeExtension ext, IconData icon, String title, String value, - Color iconColor, VoidCallback onTap) { + Widget _navRow( + AppThemeExtension ext, + IconData icon, + String title, + String value, + Color iconColor, + VoidCallback onTap, + ) { return GestureDetector( onTap: onTap, child: Container( @@ -226,32 +398,56 @@ class CtcSettingsPage extends ConsumerWidget { child: Row( children: [ Container( - width: 30, height: 30, - decoration: BoxDecoration(color: iconColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8)), + width: 30, + height: 30, + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + ), child: Center(child: Icon(icon, size: 16, color: iconColor)), ), const SizedBox(width: 12), - Expanded(child: Text(title, style: TextStyle(fontSize: 15, color: ext.textPrimary))), + Expanded( + child: Text( + title, + style: TextStyle(fontSize: 15, color: ext.textPrimary), + ), + ), Text(value, style: TextStyle(fontSize: 13, color: ext.textHint)), const SizedBox(width: 4), - Icon(CupertinoIcons.chevron_right, size: 14, color: ext.iconDisabled), + Icon( + CupertinoIcons.chevron_right, + size: 14, + color: ext.iconDisabled, + ), ], ), ), ); } - Widget _disabledRow(AppThemeExtension ext, IconData icon, String title, String badge) { + Widget _disabledRow( + AppThemeExtension ext, + IconData icon, + String title, + String badge, + ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: ext.dividerOnCard, width: 0.5)), + border: Border( + bottom: BorderSide(color: ext.dividerOnCard, width: 0.5), + ), ), child: Row( children: [ Container( - width: 30, height: 30, - decoration: BoxDecoration(color: ext.iconTintGrey.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8)), + width: 30, + height: 30, + decoration: BoxDecoration( + color: ext.iconTintGrey.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + ), child: Center(child: Icon(icon, size: 16, color: ext.iconTintGrey)), ), const SizedBox(width: 12), @@ -259,15 +455,30 @@ class CtcSettingsPage extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Opacity(opacity: 0.5, child: Text(title, style: TextStyle(fontSize: 15, color: ext.textPrimary))), - Text(badge, style: TextStyle(fontSize: 11, color: ext.textHint)), + Opacity( + opacity: 0.5, + child: Text( + title, + style: TextStyle(fontSize: 15, color: ext.textPrimary), + ), + ), + Text( + badge, + style: TextStyle(fontSize: 11, color: ext.textHint), + ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration(color: ext.bgElevated, borderRadius: BorderRadius.circular(10)), - child: Text('即将推出', style: TextStyle(fontSize: 10, color: ext.textHint)), + decoration: BoxDecoration( + color: ext.bgElevated, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '即将推出', + style: TextStyle(fontSize: 10, color: ext.textHint), + ), ), ], ), @@ -277,15 +488,27 @@ class CtcSettingsPage extends ConsumerWidget { /// 配额条 Widget _buildQuotaBar(AppThemeExtension ext, CtcSettingsState settings) { final ratio = settings.dailyUsageCount / settings.dailyLimit; - final color = ratio > 0.9 ? ext.errorColor : ratio > 0.7 ? ext.warningColor : ext.successColor; + final color = ratio > 0.9 + ? ext.errorColor + : ratio > 0.7 + ? ext.warningColor + : ext.successColor; return Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), height: 6, - decoration: BoxDecoration(color: ext.bgElevated, borderRadius: BorderRadius.circular(3)), + decoration: BoxDecoration( + color: ext.bgElevated, + borderRadius: BorderRadius.circular(3), + ), child: FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: ratio.clamp(0.0, 1.0), - child: Container(decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(3))), + child: Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(3), + ), + ), ), ); } @@ -329,22 +552,47 @@ class CtcSettingsPage extends ConsumerWidget { children: [ Row( children: [ - Icon(CupertinoIcons.exclamationmark_triangle, size: 16, color: ext.iconTintBlue), + Icon( + CupertinoIcons.exclamationmark_triangle, + size: 16, + color: ext.iconTintBlue, + ), const SizedBox(width: 6), - Text('使用须知', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: ext.iconTintBlue)), + Text( + '使用须知', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: ext.iconTintBlue, + ), + ), ], ), const SizedBox(height: 8), - ...notices.map((n) => Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('• ', style: TextStyle(color: ext.iconTintBlue, fontSize: 13)), - Expanded(child: Text(n, style: TextStyle(fontSize: 13, color: ext.textSecondary, height: 1.5))), - ], + ...notices.map( + (n) => Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '• ', + style: TextStyle(color: ext.iconTintBlue, fontSize: 13), + ), + Expanded( + child: Text( + n, + style: TextStyle( + fontSize: 13, + color: ext.textSecondary, + height: 1.5, + ), + ), + ), + ], + ), ), - )), + ), ], ), ); @@ -368,11 +616,23 @@ class CtcSettingsPage extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('设备限制', - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: ext.warningColor)), + Text( + '设备限制', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: ext.warningColor, + ), + ), const SizedBox(height: 4), - Text('每个设备最多支持 10 个笔记。超出后需删除旧笔记才能创建新笔记。', - style: TextStyle(fontSize: 13, color: ext.textSecondary, height: 1.5)), + Text( + '每个设备最多支持 10 个笔记。超出后需删除旧笔记才能创建新笔记。', + style: TextStyle( + fontSize: 13, + color: ext.textSecondary, + height: 1.5, + ), + ), ], ), ), @@ -390,7 +650,10 @@ class CtcSettingsPage extends ConsumerWidget { title: const Text('暂无笔记'), content: const Text('请先添加笔记后再查看历史记录。'), actions: [ - CupertinoDialogAction(child: const Text('确定'), onPressed: () => Navigator.pop(ctx)), + CupertinoDialogAction( + child: const Text('确定'), + onPressed: () => Navigator.pop(ctx), + ), ], ), ); @@ -399,7 +662,9 @@ class CtcSettingsPage extends ConsumerWidget { // 如果只有一个笔记,直接打开 if (notes.length == 1) { Navigator.of(context).push( - CupertinoPageRoute(builder: (_) => CtcHistoryPage(noteKey: notes.first.key)), + CupertinoPageRoute( + builder: (_) => CtcHistoryPage(noteKey: notes.first.key), + ), ); return; } @@ -408,15 +673,21 @@ class CtcSettingsPage extends ConsumerWidget { context: context, builder: (ctx) => CupertinoActionSheet( title: const Text('选择笔记'), - actions: notes.map((CtcNoteModel n) => CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(ctx); - Navigator.of(context).push( - CupertinoPageRoute(builder: (_) => CtcHistoryPage(noteKey: n.key)), - ); - }, - child: Text(n.key), - )).toList(), + actions: notes + .map( + (CtcNoteModel n) => CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(ctx); + Navigator.of(context).push( + CupertinoPageRoute( + builder: (_) => CtcHistoryPage(noteKey: n.key), + ), + ); + }, + child: Text(n.key), + ), + ) + .toList(), cancelButton: CupertinoActionSheetAction( onPressed: () => Navigator.pop(ctx), child: const Text('取消'), @@ -437,22 +708,27 @@ class CtcSettingsPage extends ConsumerWidget { child: const Text('从剪贴板导入'), onPressed: () async { Navigator.pop(ctx); - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data?.text == null || data!.text!.isEmpty) { + final text = await ClipboardBridge.getData(); + if (text == null || text.isEmpty) { if (context.mounted) { showCupertinoDialog( context: context, builder: (c) => CupertinoAlertDialog( title: const Text('剪贴板为空'), content: const Text('请先复制JSON数据到剪贴板。'), - actions: [CupertinoDialogAction(child: const Text('确定'), onPressed: () => Navigator.pop(c))], + actions: [ + CupertinoDialogAction( + child: const Text('确定'), + onPressed: () => Navigator.pop(c), + ), + ], ), ); } return; } try { - final notes = CtcExportService.importFromJson(data.text!); + final notes = CtcExportService.importFromJson(text); final notifier = ref.read(ctcNoteListProvider.notifier); for (final note in notes) { await notifier.addNote(note.key, content: note.content); @@ -463,7 +739,12 @@ class CtcSettingsPage extends ConsumerWidget { builder: (c) => CupertinoAlertDialog( title: const Text('导入成功'), content: Text('已导入 ${notes.length} 条笔记。'), - actions: [CupertinoDialogAction(child: const Text('确定'), onPressed: () => Navigator.pop(c))], + actions: [ + CupertinoDialogAction( + child: const Text('确定'), + onPressed: () => Navigator.pop(c), + ), + ], ), ); } @@ -473,15 +754,26 @@ class CtcSettingsPage extends ConsumerWidget { context: context, builder: (c) => CupertinoAlertDialog( title: const Text('导入失败'), - content: Text('JSON格式错误: ${e.toString().substring(0, 100)}'), - actions: [CupertinoDialogAction(child: const Text('确定'), onPressed: () => Navigator.pop(c))], + content: Text( + 'JSON格式错误: ${e.toString().substring(0, 100)}', + ), + actions: [ + CupertinoDialogAction( + child: const Text('确定'), + onPressed: () => Navigator.pop(c), + ), + ], ), ); } } }, ), - CupertinoDialogAction(isDestructiveAction: true, child: const Text('取消'), onPressed: () => Navigator.pop(ctx)), + CupertinoDialogAction( + isDestructiveAction: true, + child: const Text('取消'), + onPressed: () => Navigator.pop(ctx), + ), ], ), ); @@ -496,7 +788,12 @@ class CtcSettingsPage extends ConsumerWidget { builder: (ctx) => CupertinoAlertDialog( title: const Text('暂无笔记'), content: const Text('没有可导出的笔记。'), - actions: [CupertinoDialogAction(child: const Text('确定'), onPressed: () => Navigator.pop(ctx))], + actions: [ + CupertinoDialogAction( + child: const Text('确定'), + onPressed: () => Navigator.pop(ctx), + ), + ], ), ); return; @@ -508,7 +805,12 @@ class CtcSettingsPage extends ConsumerWidget { builder: (ctx) => CupertinoAlertDialog( title: const Text('已复制到剪贴板'), content: Text('已将 ${notes.length} 条笔记导出为JSON格式,已复制到剪贴板。'), - actions: [CupertinoDialogAction(child: const Text('确定'), onPressed: () => Navigator.pop(ctx))], + actions: [ + CupertinoDialogAction( + child: const Text('确定'), + onPressed: () => Navigator.pop(ctx), + ), + ], ), ); } diff --git a/lib/features/discover/presentation/widgets/chat/chat_flow_readlater_sync_helper.dart b/lib/features/discover/presentation/widgets/chat/chat_flow_readlater_sync_helper.dart index c854be90..be8d2032 100644 --- a/lib/features/discover/presentation/widgets/chat/chat_flow_readlater_sync_helper.dart +++ b/lib/features/discover/presentation/widgets/chat/chat_flow_readlater_sync_helper.dart @@ -18,6 +18,7 @@ import 'package:xianyan/core/services/readlater/readlater_collab_service.dart'; import 'package:xianyan/core/services/readlater/readlater_device_sync_service.dart'; import 'package:xianyan/core/services/data/home_widget_service.dart'; import 'package:xianyan/core/services/clipboard_monitor_service.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'package:xianyan/features/discover/providers/chat_provider.dart'; import 'package:xianyan/shared/widgets/feedback/app_toast.dart'; import 'package:xianyan/l10n/translations.dart'; @@ -418,6 +419,8 @@ class ChatFlowReadlaterSyncHelper { onPressed: () async { Navigator.pop(ctx); await service.setEnabled(true); + // 启用后立即检查一次剪贴板 + await service.checkClipboardOnce(); AppToast.showSuccess(t.clipboardMonitorEnabled); }, child: Text(t.enableMonitor), @@ -434,11 +437,11 @@ class ChatFlowReadlaterSyncHelper { CupertinoActionSheetAction( onPressed: () async { Navigator.pop(ctx); - final text = await Clipboard.getData(Clipboard.kTextPlain); - if (text?.text?.isNotEmpty ?? false) { - final content = text!.text!.length > 100 - ? text.text!.substring(0, 100) - : text.text!; + final text = await ClipboardBridge.getData(); + if (text != null && text.isNotEmpty) { + final content = text.length > 100 + ? text.substring(0, 100) + : text; AppToast.show( t.clipboardContent.replaceAll('{content}', content), ); diff --git a/lib/features/discover/presentation/widgets/chat_input/link_input_sheet.dart b/lib/features/discover/presentation/widgets/chat_input/link_input_sheet.dart index 8702e61e..6293ce66 100644 --- a/lib/features/discover/presentation/widgets/chat_input/link_input_sheet.dart +++ b/lib/features/discover/presentation/widgets/chat_input/link_input_sheet.dart @@ -16,6 +16,7 @@ import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/services/network/og_metadata_service.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'package:xianyan/shared/widgets/containers/glass_container.dart'; /// 链接输入面板 — Cupertino风格底部弹窗 @@ -36,7 +37,8 @@ class LinkInputSheet extends StatefulWidget { String? description, String? imageUrl, String? sourceApp, - ) onSend; + ) + onSend; /// 显示链接输入面板 static Future show( @@ -47,7 +49,8 @@ class LinkInputSheet extends StatefulWidget { String? description, String? imageUrl, String? sourceApp, - ) onSend, + ) + onSend, }) { final ext = AppTheme.ext(context); return showCupertinoModalPopup( @@ -55,9 +58,7 @@ class LinkInputSheet extends StatefulWidget { builder: (_) => Container( decoration: BoxDecoration( color: ext.bgPrimary, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: LinkInputSheet(onSend: onSend), ), @@ -158,12 +159,12 @@ class _LinkInputSheetState extends State { /// 从剪贴板粘贴 Future _pasteFromClipboard() async { try { - final data = await Clipboard.getData(Clipboard.kTextPlain); - final text = data?.text?.trim() ?? ''; - if (text.isNotEmpty) { - _urlController.text = text; + final text = await ClipboardBridge.getData(); + if (text != null && text.trim().isNotEmpty) { + final trimmed = text.trim(); + _urlController.text = trimmed; _urlController.selection = TextSelection.fromPosition( - TextPosition(offset: text.length), + TextPosition(offset: trimmed.length), ); Log.d('从剪贴板粘贴链接', null, null, LogCategory.ui); } @@ -278,9 +279,7 @@ class _LinkInputSheetState extends State { controller: _urlController, focusNode: _urlFocusNode, placeholder: '粘贴或输入链接地址', - placeholderStyle: AppTypography.body.copyWith( - color: ext.textHint, - ), + placeholderStyle: AppTypography.body.copyWith(color: ext.textHint), style: AppTypography.body.copyWith(color: ext.textPrimary), decoration: BoxDecoration( color: ext.bgSecondary, @@ -415,11 +414,7 @@ class _LinkInputSheetState extends State { if (_ogMetadata!.siteName != null) Row( children: [ - Icon( - CupertinoIcons.globe, - size: 12, - color: ext.textHint, - ), + Icon(CupertinoIcons.globe, size: 12, color: ext.textHint), const SizedBox(width: AppSpacing.xs), Text( _ogMetadata!.siteName!, @@ -472,10 +467,7 @@ class _LinkInputSheetState extends State { /// OG图片缩略图 — 带错误降级 class _OgImageThumbnail extends StatelessWidget { - const _OgImageThumbnail({ - required this.imageUrl, - required this.ext, - }); + const _OgImageThumbnail({required this.imageUrl, required this.ext}); final String imageUrl; final AppThemeExtension ext; @@ -496,17 +488,11 @@ class _OgImageThumbnail extends StatelessWidget { height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Center( - child: Icon( - CupertinoIcons.link, - size: 24, - color: ext.textHint, - ), + child: Icon(CupertinoIcons.link, size: 24, color: ext.textHint), ), loadingBuilder: (_, child, loadingProgress) { if (loadingProgress == null) return child; - return const Center( - child: CupertinoActivityIndicator(radius: 8), - ); + return const Center(child: CupertinoActivityIndicator(radius: 8)); }, ), ); diff --git a/lib/features/file_transfer/collaboration/clipboard/clipboard_flow_page.dart b/lib/features/file_transfer/collaboration/clipboard/clipboard_flow_page.dart index fafaf9d1..f4a61140 100644 --- a/lib/features/file_transfer/collaboration/clipboard/clipboard_flow_page.dart +++ b/lib/features/file_transfer/collaboration/clipboard/clipboard_flow_page.dart @@ -18,6 +18,7 @@ import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; import 'package:xianyan/core/utils/data/extensions.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/shared/widgets/adaptive/adaptive_back_button.dart'; import 'package:xianyan/shared/widgets/feedback/app_toast.dart'; @@ -497,12 +498,12 @@ class _ClipboardFlowPageState extends ConsumerState { Future _handlePasteAndSync() async { try { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null || data.text == null || data.text!.isEmpty) { + final text = await ClipboardBridge.getData(); + if (text == null || text.isEmpty) { if (mounted) AppToast.showInfo('剪贴板为空'); return; } - await ref.read(clipboardProvider.notifier).sendToPeer(data.text!); + await ref.read(clipboardProvider.notifier).sendToPeer(text); if (mounted) AppToast.showSuccess('📋 已同步到剪贴板'); } catch (e) { Log.w('ClipboardFlow: pasteAndSync failed: $e'); diff --git a/lib/features/file_transfer/collaboration/clipboard/clipboard_manager_service.dart b/lib/features/file_transfer/collaboration/clipboard/clipboard_manager_service.dart index c30e7b01..89c85182 100644 --- a/lib/features/file_transfer/collaboration/clipboard/clipboard_manager_service.dart +++ b/lib/features/file_transfer/collaboration/clipboard/clipboard_manager_service.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — 剪贴板管理服务 // 创建时间: 2026-05-14 -// 更新时间: 2026-05-14 +// 更新时间: 2026-06-16 // 作用: 增强版剪贴板同步 — 图片同步/历史管理/置顶/搜索/信令通道 -// 上次更新: 初始创建 +// 上次更新: 使用ClipboardBridge统一入口,含隐私协议守卫 // ============================================================ import 'dart:async'; @@ -12,6 +12,7 @@ import 'package:drift/drift.dart'; import 'package:flutter/services.dart'; import 'package:xianyan/core/storage/database/app_database.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'package:xianyan/features/file_transfer/services/clipboard_sync_service.dart'; import 'package:xianyan/features/file_transfer/services/signaling_service.dart'; @@ -180,9 +181,9 @@ class ClipboardManagerService { Future syncNow() async { try { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null || data.text == null || data.text!.isEmpty) return; - await sendTextToPeer(data.text!); + final text = await ClipboardBridge.getData(); + if (text == null || text.isEmpty) return; + await sendTextToPeer(text); } catch (e) { Log.w('ClipboardManager: syncNow failed: $e'); } diff --git a/lib/features/file_transfer/services/clipboard_sync_service.dart b/lib/features/file_transfer/services/clipboard_sync_service.dart index 759c7b53..650839f3 100644 --- a/lib/features/file_transfer/services/clipboard_sync_service.dart +++ b/lib/features/file_transfer/services/clipboard_sync_service.dart @@ -1,15 +1,17 @@ // ============================================================ // 闲言APP — 剪贴板同步服务 // 创建时间: 2026-05-10 -// 更新时间: 2026-05-10 +// 更新时间: 2026-06-16 // 作用: 跨设备剪贴板文本同步 — 信令通道推送 -// 上次更新: 修复方法调用和空安全 +// 上次更新: 使用ClipboardBridge统一入口,含隐私协议守卫, +// 未同意协议时禁止读取剪贴板 // ============================================================ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'signaling_service.dart'; @@ -48,10 +50,9 @@ class ClipboardSyncService { Future _checkLocalClipboard() async { try { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null || data.text == null) return; + final text = await ClipboardBridge.getData(); + if (text == null || text.isEmpty) return; - final text = data.text!; if (text == _lastClipboardText) return; if (text.length > 10000) return; diff --git a/lib/features/home/presentation/providers/readlater_page.dart b/lib/features/home/presentation/providers/readlater_page.dart index a6db7dc8..65de2c4d 100644 --- a/lib/features/home/presentation/providers/readlater_page.dart +++ b/lib/features/home/presentation/providers/readlater_page.dart @@ -1,16 +1,15 @@ /// ============================================================ /// 闲言APP — 稍后读列表页面 /// 创建时间: 2026-04-28 -/// 更新时间: 2026-06-10 +/// 更新时间: 2026-06-16 /// 作用: 展示用户稍后读列表,支持搜索/筛选/排序/批量操作/富详情 -/// 上次更新: 添加剪贴板被动触发(用户进入稍后读页面时检查URL) +/// 上次更新: 移除自动剪贴板检查,改为用户主动点击按钮触发 /// ============================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:desktop_drop/desktop_drop.dart'; -import '../../../../core/services/clipboard_monitor_service.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../core/utils/platform/platform_helper.dart'; @@ -55,8 +54,7 @@ class _ReadLaterPageState extends ConsumerState @override void initState() { super.initState(); - // 被动触发:用户主动进入稍后读页面时检查剪贴板URL - ClipboardMonitorService.instance.checkClipboardOnce(); + // 剪贴板检查已改为用户主动点击触发,此处不再自动检查 } @override diff --git a/lib/features/profile/presentation/about_page.dart b/lib/features/profile/presentation/about_page.dart index 78567459..e5b7aa59 100644 --- a/lib/features/profile/presentation/about_page.dart +++ b/lib/features/profile/presentation/about_page.dart @@ -25,6 +25,7 @@ import '../../../l10n/translations.dart'; import '../../../shared/widgets/containers/glass_container.dart'; import '../../../shared/widgets/adaptive/adaptive_back_button.dart'; import '../../../shared/widgets/feedback/app_toast.dart'; +import '../../../shared/widgets/feedback/external_link_dialog.dart'; import '../../../shared/widgets/feedback/contact_email_sheet.dart'; import 'about_shared_widgets.dart'; @@ -374,33 +375,53 @@ class _FeedbackSection extends ConsumerWidget { if (pu.isIOS) { const appId = '6737492298'; final uri = Uri.parse('https://apps.apple.com/app/id$appId'); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - return; + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: uri, + appName: 'App Store', + ); } + return; } // 鸿蒙应用市场 if (pu.isOhos) { - final uri = Uri.parse('https://appgallery.huawei.com/app/C108129465'); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - return; + final uri = Uri.parse( + 'https://appgallery.huawei.com/app/detail?id=apps.xy.xianyan', + ); + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: uri, + appName: t.about.huaweiStore, + ); } + return; } // Android Google Play if (pu.isAndroid) { final uri = Uri.parse('market://details?id=apps.xy.xianyan'); if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: uri, + appName: 'Google Play', + ); + } return; } final webUri = Uri.parse( 'https://play.google.com/store/apps/details?id=apps.xy.xianyan', ); - if (await canLaunchUrl(webUri)) { - await launchUrl(webUri, mode: LaunchMode.externalApplication); - return; + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: webUri, + appName: 'Google Play', + ); } + return; } if (context.mounted) AppToast.showInfo(t.about.rateAppMenuDesc); } catch (e) { diff --git a/lib/features/profile/presentation/profile_page.dart b/lib/features/profile/presentation/profile_page.dart index 4192f60a..66cda026 100644 --- a/lib/features/profile/presentation/profile_page.dart +++ b/lib/features/profile/presentation/profile_page.dart @@ -6,7 +6,6 @@ /// 上次更新: ext参数内部化、Mixin抽取、下拉刷新、骨架屏、评分接入、退出逻辑迁移 /// ============================================================ - import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -27,6 +26,7 @@ import '../../../l10n/translations.dart'; import '../../../shared/widgets/containers/glass_container.dart'; import '../../../shared/widgets/adaptive/responsive_layout.dart'; import '../../../shared/widgets/feedback/app_toast.dart'; +import '../../../shared/widgets/feedback/external_link_dialog.dart'; import '../../../core/utils/platform/platform_utils.dart' as pu; import '../../../shared/widgets/input/setting_row.dart'; import '../../settings/providers/theme_settings_provider.dart'; @@ -157,34 +157,54 @@ class _ProfilePageState extends ConsumerState if (pu.isIOS) { const appId = '6737492298'; final uri = Uri.parse('https://apps.apple.com/app/id$appId'); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - return; + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: uri, + appName: 'App Store', + ); } + return; } // 鸿蒙应用市场 if (pu.isOhos) { - final uri = Uri.parse('https://appgallery.huawei.com/app/C108129465'); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - return; + final uri = Uri.parse( + 'https://appgallery.huawei.com/app/detail?id=apps.xy.xianyan', + ); + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: uri, + appName: t.about.huaweiStore, + ); } + return; } // Android Google Play if (pu.isAndroid) { final uri = Uri.parse('market://details?id=apps.xy.xianyan'); if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: uri, + appName: 'Google Play', + ); + } return; } // 降级:打开网页版 final webUri = Uri.parse( 'https://play.google.com/store/apps/details?id=apps.xy.xianyan', ); - if (await canLaunchUrl(webUri)) { - await launchUrl(webUri, mode: LaunchMode.externalApplication); - return; + if (context.mounted) { + await ExternalLinkDialog.launchWithConfirm( + context, + uri: webUri, + appName: 'Google Play', + ); } + return; } AppToast.showInfo(t.profile.appStoreNotFound); } catch (e) { diff --git a/lib/features/tool_center/leisure/presentation/pages/leisure_import_dialog.dart b/lib/features/tool_center/leisure/presentation/pages/leisure_import_dialog.dart index b3d73afb..25638be7 100644 --- a/lib/features/tool_center/leisure/presentation/pages/leisure_import_dialog.dart +++ b/lib/features/tool_center/leisure/presentation/pages/leisure_import_dialog.dart @@ -15,6 +15,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:xianyan/core/theme/app_theme.dart'; +import 'package:xianyan/core/utils/platform/clipboard_bridge.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; @@ -431,10 +432,10 @@ class _ImportDialogContentState extends ConsumerState<_ImportDialogContent> { /// 读取剪贴板 Future _readClipboard() async { try { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data?.text?.isNotEmpty ?? false) { + final text = await ClipboardBridge.getData(); + if (text != null && text.isNotEmpty) { setState(() { - _jsonContent = data!.text!; + _jsonContent = text; _preview = LeisureImportService.preview(_jsonContent); }); } else { diff --git a/lib/l10n/languages/fr.dart b/lib/l10n/languages/fr.dart index eedf3bc0..3ec671c9 100644 --- a/lib/l10n/languages/fr.dart +++ b/lib/l10n/languages/fr.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 法语翻译数据 /// 创建时间: 2026-05-29 -/// 更新时间: 2026-06-10 +/// 更新时间: 2026-06-16 /// 作用: 法语(fr)翻译文本 -/// 上次更新: 新增笔记模块翻译 +/// 上次更新: 补全defaultSentence翻译 /// ============================================================ import '../types/t.dart'; @@ -51,7 +51,8 @@ const fr = T( base: THomeBase( batteryCritical: 'Batterie très faible ! Rechargez', batteryLow: 'Batterie faible, pensez à recharger', - defaultSentence: '', + defaultSentence: + 'La vie n\'est pas d\'attendre que l\'orage passe, mais d\'apprendre à danser sous la pluie.', defaultFeedName: 'Xianyan', authorPrefix: '—', numberWan: 'w', diff --git a/lib/l10n/languages/hi.dart b/lib/l10n/languages/hi.dart index 71fe132b..032136cc 100644 --- a/lib/l10n/languages/hi.dart +++ b/lib/l10n/languages/hi.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 印地语翻译数据 /// 创建时间: 2026-05-29 -/// 更新时间: 2026-06-10 +/// 更新时间: 2026-06-16 /// 作用: 印地语(hi)翻译文本 -/// 上次更新: 新增笔记模块翻译 +/// 上次更新: 补全defaultSentence翻译 /// ============================================================ import '../types/t.dart'; @@ -51,7 +51,8 @@ const hi = T( base: THomeBase( batteryCritical: 'बैटरी बहुत कम! चार्ज करें', batteryLow: 'बैटरी कम है, चार्ज करना याद रखें', - defaultSentence: '', + defaultSentence: + 'जीवन तूफान के गुजरने का इंतजार नहीं, बल्कि बारिश में नाचना सीखना है।', defaultFeedName: 'Xianyan', authorPrefix: '— ', numberWan: 'w', diff --git a/lib/l10n/languages/it.dart b/lib/l10n/languages/it.dart index 694e0526..aec862b8 100644 --- a/lib/l10n/languages/it.dart +++ b/lib/l10n/languages/it.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 意大利语翻译数据 /// 创建时间: 2026-05-30 -/// 更新时间: 2026-06-10 +/// 更新时间: 2026-06-16 /// 作用: 意大利语(it)翻译文本 -/// 上次更新: 新增笔记模块翻译 +/// 上次更新: 补全defaultSentence翻译 /// ============================================================ import '../types/t.dart'; @@ -51,7 +51,8 @@ const it = T( base: THomeBase( batteryCritical: 'Batteria quasi esaurita! Ricarica', batteryLow: 'Batteria bassa, ricordati di ricaricare', - defaultSentence: '', + defaultSentence: + 'La vita non è aspettare che passi la tempesta, ma imparare a ballare sotto la pioggia.', defaultFeedName: 'Xianyan', authorPrefix: '— ', numberWan: 'w', diff --git a/lib/l10n/languages/pt.dart b/lib/l10n/languages/pt.dart index f944a7a3..04bc1cbc 100644 --- a/lib/l10n/languages/pt.dart +++ b/lib/l10n/languages/pt.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 葡萄牙语翻译数据 /// 创建时间: 2026-05-29 -/// 更新时间: 2026-06-10 +/// 更新时间: 2026-06-16 /// 作用: 葡萄牙语(pt)翻译文本 -/// 上次更新: 新增笔记模块翻译 +/// 上次更新: 补全defaultSentence翻译 /// ============================================================ import '../types/t.dart'; @@ -51,7 +51,8 @@ const pt = T( base: THomeBase( batteryCritical: 'Bateria muito baixa! Carregue agora', batteryLow: 'Bateria baixa, lembre-se de carregar', - defaultSentence: '', + defaultSentence: + 'A vida não é esperar a tempestade passar, é aprender a dançar na chuva.', defaultFeedName: 'Xianyan', authorPrefix: '— ', numberWan: 'w', diff --git a/lib/l10n/languages/ru.dart b/lib/l10n/languages/ru.dart index 9cb76970..658103f2 100644 --- a/lib/l10n/languages/ru.dart +++ b/lib/l10n/languages/ru.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 俄语翻译数据 /// 创建时间: 2026-05-29 -/// 更新时间: 2026-06-10 +/// 更新时间: 2026-06-16 /// 作用: 俄语(ru)翻译文本 -/// 上次更新: 新增笔记模块翻译 +/// 上次更新: 补全defaultSentence翻译 /// ============================================================ import '../types/t.dart'; @@ -51,7 +51,8 @@ const ru = T( base: THomeBase( batteryCritical: 'Батарея почти разряжена! Зарядите', batteryLow: 'Батарея разряжается, не забудьте зарядить', - defaultSentence: '', + defaultSentence: + 'Жизнь — это не ожидание, пока пройдет буря, а умение танцевать под дождем.', defaultFeedName: 'Xianyan', authorPrefix: '— ', numberWan: 'w', diff --git a/lib/l10n/translation_resolver.dart b/lib/l10n/translation_resolver.dart index 11e6d2be..cb62495e 100644 --- a/lib/l10n/translation_resolver.dart +++ b/lib/l10n/translation_resolver.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 翻译解析器与Provider /// 创建时间: 2026-05-29 -/// 更新时间: 2026-05-29 +/// 更新时间: 2026-06-16 /// 作用: 根据语言ID/系统语言解析翻译,提供Riverpod Provider -/// 上次更新: 从translations.dart拆分,引用改为公开语言常量 +/// 上次更新: 集成withFallback回退机制,翻译缺失时回退到英语 /// ============================================================ import 'package:flutter/widgets.dart'; @@ -27,48 +27,50 @@ import 'languages/ru.dart'; import 'languages/fr.dart'; import 't_func.dart'; -/// 根据语言ID获取翻译映射 +/// 根据语言ID获取翻译映射,非中文/英语语言自动回退到英语填充空字段 T getTranslations(String languageId) { switch (languageId) { + case 'zh_CN': + return zhCN; case 'en': return en; case 'ja': - return ja; + return T.withFallback(ja, en); case 'zh_TW': - return zhTW; + return T.withFallback(zhTW, zhCN); case 'ko': - return ko; + return T.withFallback(ko, en); case 'de': - return de; + return T.withFallback(de, en); case 'it': - return it; + return T.withFallback(it, en); case 'es': - return es; + return T.withFallback(es, en); case 'ar': - return ar; + return T.withFallback(ar, en); case 'bn': - return bn; + return T.withFallback(bn, en); case 'hi': - return hi; + return T.withFallback(hi, en); case 'pt': - return pt; + return T.withFallback(pt, en); case 'ru': - return ru; + return T.withFallback(ru, en); case 'fr': - return fr; + return T.withFallback(fr, en); default: return zhCN; } } -/// 解析系统语言对应的翻译 +/// 解析系统语言对应的翻译,非中文/英语语言自动回退到英语填充空字段 T _resolveSystemTranslations() { final systemLocale = WidgetsBinding.instance.platformDispatcher.locale; final langCode = systemLocale.languageCode; final countryCode = systemLocale.countryCode; if (langCode == 'zh') { if (countryCode == 'TW' || countryCode == 'HK' || countryCode == 'MO') { - return zhTW; + return T.withFallback(zhTW, zhCN); } return zhCN; } @@ -76,27 +78,27 @@ T _resolveSystemTranslations() { case 'en': return en; case 'ja': - return ja; + return T.withFallback(ja, en); case 'ko': - return ko; + return T.withFallback(ko, en); case 'de': - return de; + return T.withFallback(de, en); case 'it': - return it; + return T.withFallback(it, en); case 'es': - return es; + return T.withFallback(es, en); case 'ar': - return ar; + return T.withFallback(ar, en); case 'bn': - return bn; + return T.withFallback(bn, en); case 'hi': - return hi; + return T.withFallback(hi, en); case 'pt': - return pt; + return T.withFallback(pt, en); case 'ru': - return ru; + return T.withFallback(ru, en); case 'fr': - return fr; + return T.withFallback(fr, en); default: return zhCN; } diff --git a/lib/main.dart b/lib/main.dart index ae225669..1e65a531 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,7 +24,6 @@ import 'core/services/network/deep_link_service.dart'; import 'core/router/deep_link_resolver.dart'; import 'core/services/performance/performance_orchestrator.dart'; import 'core/services/error/crash_monitor.dart'; -import 'core/services/device/haptic_service.dart'; import 'core/storage/kv_storage.dart'; import 'core/services/error/global_error_handler.dart'; import 'core/services/post_agreement_initializer.dart'; @@ -95,8 +94,7 @@ Future _appMain() async { } // IOSScrollViewFlingVelocityTracker 时间戳乱序:Flutter框架已知bug,非致命 // iOS上触摸事件时间戳偶尔乱序导致velocity tracker断言失败,不影响功能 - if (msg.contains('smaller timestamp') && - msg.contains('predecessor')) { + if (msg.contains('smaller timestamp') && msg.contains('predecessor')) { Log.w( '⚠️ FlutterError: VelocityTracker timestamp out-of-order (已静默)', msg, @@ -153,8 +151,10 @@ Future _appMain() async { } try { - await HapticService.init(); - Log.i('触觉反馈服务初始化完成', null, null, LogCategory.haptic); + // HapticService.init() 已移至 PostAgreementInitializer + // Vibrate.canVibrate 会触发 flutter_vibrate 原生插件初始化, + // 未同意隐私协议前不应调用 + Log.i('触觉反馈服务延迟到协议同意后初始化', null, null, LogCategory.haptic); } catch (e, st) { Log.e('触觉反馈服务初始化失败', e, st, LogCategory.haptic); } @@ -288,7 +288,10 @@ Future _appMain() async { child: ProviderScope( overrides: [ authStateProvider.overrideWith((ref) => ref.watch(authProvider)), - logoutProvider.overrideWith((ref) => () => ref.read(authProvider.notifier).logout()), + logoutProvider.overrideWith( + (ref) => + () => ref.read(authProvider.notifier).logout(), + ), ], child: const XianyanApp(), ), @@ -299,7 +302,10 @@ Future _appMain() async { rootWidget: ProviderScope( overrides: [ authStateProvider.overrideWith((ref) => ref.watch(authProvider)), - logoutProvider.overrideWith((ref) => () => ref.read(authProvider.notifier).logout()), + logoutProvider.overrideWith( + (ref) => + () => ref.read(authProvider.notifier).logout(), + ), ], child: const XianyanApp(), ), diff --git a/lib/shared/widgets/feedback/app_toast.dart b/lib/shared/widgets/feedback/app_toast.dart index d6e37d18..eb40fe92 100644 --- a/lib/shared/widgets/feedback/app_toast.dart +++ b/lib/shared/widgets/feedback/app_toast.dart @@ -30,6 +30,7 @@ class AppToast { static bool get isInitialized => _isInitialized; static void markInitialized() { + if (_isInitialized) return; _isInitialized = true; debugPrint('[AppToast] BotToast 已标记为初始化完成'); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a73737b9..157145dd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,7 +17,7 @@ import flutter_app_group_directory import flutter_image_compress_macos import flutter_inappwebview_macos import flutter_local_notifications -import flutter_secure_storage_darwin +import flutter_secure_storage_macos import flutter_tts import flutter_webrtc import gal @@ -54,7 +54,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")) - FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) diff --git a/ohos/AppScope/app.json5 b/ohos/AppScope/app.json5 index 894d0226..45ad2cb1 100644 --- a/ohos/AppScope/app.json5 +++ b/ohos/AppScope/app.json5 @@ -2,9 +2,9 @@ "app": { "bundleName": "apps.xy.xianyan", "vendor": "example", - "versionCode": 1000000, - "versionName": "1.0.0", - "icon": "$media:app_icon", + "versionCode": 2606152, + "versionName": "6.6.16", + "icon": "$media:layered_image", "label": "$string:app_name" } } diff --git a/ohos/AppScope/resources/base/media/layered_image.json b/ohos/AppScope/resources/base/media/layered_image.json index f46276d8..746439a5 100644 --- a/ohos/AppScope/resources/base/media/layered_image.json +++ b/ohos/AppScope/resources/base/media/layered_image.json @@ -1,6 +1,6 @@ { "layered-image": { - "foreground": "$media:foreground_icon", - "background": "$media:background_icon" + "background": "$media:background_icon", + "foreground": "$media:foreground_icon" } } \ No newline at end of file diff --git a/ohos/entry/src/main/module.json5 b/ohos/entry/src/main/module.json5 index 84605e7d..0ae84ffc 100644 --- a/ohos/entry/src/main/module.json5 +++ b/ohos/entry/src/main/module.json5 @@ -33,7 +33,7 @@ "entity.system.home" ], "actions": [ - "action.system.home" + "ohos.want.action.home" ] }, { diff --git a/ohos/entry/src/main/resources/base/media/layered_image.json b/ohos/entry/src/main/resources/base/media/layered_image.json index f46276d8..746439a5 100644 --- a/ohos/entry/src/main/resources/base/media/layered_image.json +++ b/ohos/entry/src/main/resources/base/media/layered_image.json @@ -1,6 +1,6 @@ { "layered-image": { - "foreground": "$media:foreground_icon", - "background": "$media:background_icon" + "background": "$media:background_icon", + "foreground": "$media:foreground_icon" } } \ No newline at end of file diff --git a/pubspec.macos.yaml b/pubspec.macos.yaml index 6bed4016..c6eebcdf 100644 --- a/pubspec.macos.yaml +++ b/pubspec.macos.yaml @@ -21,7 +21,7 @@ name: xianyan description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP" publish_to: 'none' -version: 6.6.16+2606152 +version: 6.6.18+2606173 # 年月日-次 7位 environment: diff --git a/pubspec.ohos.yaml b/pubspec.ohos.yaml index cc84be6e..866bd0d5 100644 --- a/pubspec.ohos.yaml +++ b/pubspec.ohos.yaml @@ -20,7 +20,7 @@ name: xianyan description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP" publish_to: 'none' -version: 6.6.16+2606152 +version: 6.6.18+2606173 # 年月日-次 7位 environment: diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 39c008f6..a7f00426 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -111,3 +111,8 @@ install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) + +# Install prebuilt sqlite3.dll for Drift database +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/local_packages/sqlite3/prebuilt/sqlite3.dll" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee303..29136b17 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -2,6 +2,9 @@ #include +#include +#include + #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) @@ -36,6 +39,31 @@ bool FlutterWindow::OnCreate() { // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); + // Register Windows platform MethodChannel for theme control + const static std::string channel_name("com.xianyan.windows"); + + platform_channel_ = std::make_unique>( + flutter_controller_->engine()->messenger(), channel_name, + &flutter::StandardMethodCodec::GetInstance()); + + platform_channel_->SetMethodCallHandler( + [this](const flutter::MethodCall<>& call, + std::unique_ptr> result) { + if (call.method_name() == "setDarkMode") { + bool is_dark = false; + if (const auto* args = std::get_if(call.arguments())) { + auto it = args->find(flutter::EncodableValue("isDark")); + if (it != args->end() && std::holds_alternative(it->second)) { + is_dark = std::get(it->second); + } + } + Win32Window::SetDarkMode(GetHandle(), is_dark); + result->Success(); + } else { + result->NotImplemented(); + } + }); + return true; } diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h index 6da0652f..ead932f1 100644 --- a/windows/runner/flutter_window.h +++ b/windows/runner/flutter_window.h @@ -3,6 +3,7 @@ #include #include +#include #include @@ -28,6 +29,9 @@ class FlutterWindow : public Win32Window { // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; + + // Windows platform MethodChannel for theme control. + std::unique_ptr> platform_channel_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20caf6370ebb9253ad831cc31de4a9c965f6..e8740d1fb7a1f381221eff6de7a27208e039db87 100644 GIT binary patch literal 47957 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

kA9 znj(d%R6&6j=K=!fJVBk`EwbII1Gx<4mcl=KR~LPCCQazG4`0fQvHyh_vD8~kSHHxR zqQdt0iRhMT-UONgW}w;TF{wMFJDS7%cl{|ebnRhEP>PhpTch|ou$U)|whHjkR2WKz z+fO&D;8lw>85J(DVED+CpG@5Ju}1nBYg3Xb&I6I=BmVsP!>za*%sKn^e0#d(2`Z9h zZVjR#!pG06(rI>xIsu(qj0WKsaaJW>?Tb8SY<{zNAj*MIj#Kv$geC+GQ+SX>abXCO z`^U*jMl=2t{<1SgKtLdNd3B&YtLM5oi2GQfYPq%H?(}-mJTK3+DGtsX0s=251(oC- z@=#MSKoqVB*9)7A1^x;iN`N26wUJ6(+^DbDc%=*Vu4ApHX{xER+J5zl{2}!nII8Yl zw|z>IOT1Fei#DpyLu^q`nb&#);he$MKrEMIo=}%b%q3UbJs+q;WjPoJ5$64@qUMam z_l-@L!Y6qH(|7!+PTLRg0~LcHLQD*!3o$Qw^X4wO{z1a}pRhYUvMM9QN$p&v=tPUC zy^M2kcd>M0Vj1P5ggDpv`kBp3FA!_~f_6P5L|U=Y{YxxF#zZp88f0wB&k;F3xCQ!T zg=@zm-Eg0JZv0Bt`&smW1Qe>RYynS5vFdJFLlC{#A6iRvLi8{X504d4KSv&5t<%a? zlP?G*rg$iY(t0<9&^EVzT9EI~r71eZ-fxm}q&b>)7ULNeC(ETmRR@q{KJ{lO^vT~V z9Ek|CGQ?!xL``23PU1&tmvICjD*WjqjQ|M`rKLY_yF6jWI{q^gR=C)*=KgcqCRsSx z+AiphUno5kT2l;0rKSXV4EYWll&XC)0*8r5)g{((wHud&QNKz746$4oVrreht5ouj zbf(1RrFC10*F5tVG@o*dLi`vmi^1ZdXXlCc9TVo~DLovwUttw-ci|uRM}dv(fPp6U z$amNu(PgYfsdc@E6YE}zlz%z6 z%%i?7^pY|}6LN6}b#)`E+2dmljhHOp z`FPdr>e?lHzErb+jP~FwOqjhc+}YX5h>uQ)^Qw*>*(}U%!X7V0gam(OhWWNy7JOlL zh1&H|SwFQGg?;%&DRHsixzzzmV>dW1E}aR>G&^j`GH&W;N!E~yzopl#1g=`v+XAF* z(PPIT>c5rpE#{)P#Phan^!%|g(HcW{1UI-ASpe|C@P_&upH`sI_YdC#q3t9Flz7RMyF4;o(5f*UW_Db$( zD>3|@!*QeZ`(9r}1Ys9ZfyaD~-|n9?dVPJ3X0Kq5%C%?wjSbRQ3grp0oT(i>0#DmI z^V)JqZQZ*R^~d`5da}_K`@i1+r{%Rog%1@Z7JD!TQwigVJhEZpxAF`8*L zoZmAI=l6v7Ln%7c=!*V~9RA>MLbwSev&O^W?#FznMH(o?RU+Yox!Cl9dIeW#1ejz z*W26MSfqUFvFyx}s+@i7uK(mn=*G~Zmx!4WF^>DVK$cx2+BE`LqaG{VIAVA7IJ}9Y zI4qrM+n#^9HD!OE1v8axJ1eXGoOM9KyJN;DMA+I9E740;=l{DffdCIf0RC5)=sJ{| z0|147|5un?c*Av=NyNKu=KE~;$pQ_nP*{PZoV-x5|)vj zU2HKlG^EOzOh`f!d$zv~j;3hdh1WY|gT(3mwY5g8vPz;I!E7;aT~wdjPT~hU`o!>Awj3tMl1OK(atwQWFLUZggT=hQ7 zTGOvZde{3!jrP>;x5Lr4Ry&7_u44g^J;)p6xfufcGfF$ukO{U^)Au^kV5YIEX>E*2 zyCJOW)>G5;%$1XumjDMsNbE3wyvvf`j>(pP97(w8dvo}9>hvi4x^VY;Bl4=t=8~#= znM0L*9O!sUp3L!D^Xk0QDP5hX%(>t^dD@@uguOg~Crz90u(-Iyqa}v@Nr2jq6tzO5 z#sw^eIdN?|C384vNZX)Gl;Vu*GXc^LUAqT zl1@=<%Kukv&?(trBT z8C5vM6_|7-y!`cgc6f}?=m=PVg5`Jocs`!D(5pZ=mxC0hQ3TV@7iPLGrsfS%-LE;L zqtbA*-UZRXIx`s;Zfw6DaZxhq zx^6npuALamuH_>1v~Ry(o^UaiX4tmTkqN~4e!M-;hSgH3JKifZSN3YB@lYsf1SGWM zHuiJ!7?v|v=wbeJxYt}xyjOP@GeF?FAcxU3Y^;*)Ub~dIO%nM4nphx`z3%_`9HJ1{ z9rXT?+N&MJ(U~C}PhmQ*(yZ;fXa{v&-i))j#(Hk-YlmN3_hlwmH0Dn4KL*;Ce$6APub|QHSQL^Q~QKO*b($>Atq}TL|dL%v2sbF0ZZ42g9c( ztxC?cVLto<93?_^ekcj52El!G>)CW2_P)%eAr@b*?tIWz&N?0gveG~cqi$H~-x54F zHFriwdG0*Sbw|hS_Zt^VJj3EY8PPBf3WU*84(ueA=3uJ8T`aSLj>j07xJI*r7;5}2 z5sb&CtzuS_;ZGlm!hXxhmxWLo9(moKQ@np@VJ557<)ShgG6~VSh7U;yuiXyVzzfQ``N%Cn~*!CXX zsjh}3`DC3nd)1V(FnP~h(0K!Yg^~mQaQgvN(_xnJnqP{(8Q-@#eiHSj{NN?I(Jy%C zP)h-@Ufr%`!`StXB{O*bChKFR-OiC0WwiqO-A?gM3p|f177zKO6aDUiC_HD;?`SdF zNZjyYHdJ>iDd&HZRB@3FptaH17jzu?&5oCB|Lk?|QyOBq`gY&+Gbjn~q&4XKQtc|CTk`fB31e zjj_=a9ojW56+2}gW^ zaAHBesnIyE&9B#^{i7dqhI!9*t~kIqgh5TTj5~{BNjD}t<43Wj2?EzYUXtVlR+%Fw z!^Ncv*SqT`D01k6M&6z%?1u(omz6wGt}byA1;fiI4J@&rJS6rc&*rvUZb=fKoJ2&{(y#Gd|!~1mWRY{7?XBrlzK*55gvK-LUWMeB9zY6Vo7P zjY9;8K|gGpsPt23SkDQ6h)|n0;h1~_bau2$QK-^AD%$q}`*p*^EKi^4Xhm+SK}TZ^ zm&sBxfdm6>QR_W>uDDX5FNBEXEdE5)UXMY3ofdJ`B^Jd$w55=`IG5(^6Q#&uv#(wKlmX%dDw}V$QtN#WO2EgkzV;V+1Cd&X+dV z<8&1gbXK_d!U7Z zL_^&+&6`a(o>_qvEQ_^CWWl9{5fvcFR_Q5+uMgCwTj@-Tg(wgz_c|Ar^PNo2)Kt4C zAOZ+S6JF%}Kk(n+_Fi<$0BU3qc7)^PGjnzN4J#i176A(oL_yGg13X+?ZYFN+LGY$( zfL1?JebQ5E3Zc3OsRKJK-22Xry=;`3Jty1i(_{1T+y&~CGY^N0zYC$don_Zc+h(0O zG$8WUhDJIKji2fDoLuz~oe-}xtzSP`UM^9BlwTx${K3y)@8<7*6fzp4ih-o|@R2UC z3X%Mge+A|k3CfPAJ=x3~Q+w+7!Cn*Jgw`ddO)RnOoZH)1u-ash)hSIPI8yPtm2H@R zYGhb$uchMPnucF)n_Xy{xw^YH<#FXI)nmDJ7=BTgL|#f>jBlEQa$VMQy^r%jZbewf zyO3h;BEk1WzX)aOV12UJ7nE(lYpA!85~77!+E6BJm$88_TJZWYm4r3#^y=uWiKVxe zT}TZ5nPjghs1dMJCWvx#S?T!kYh*c8_{x77>Er$-qX@vUi2QCoge*;L)Bc~n>&ooJ zEe&Ye_VKKv-3!cxqP*UDe=TDapxQGwx#2;sGX#bdmGbC0<3TgSAhkM77glSDPqs~M zb6(3z+XhB~tXE}CJ=?q51zUU-2)r+=+Y+)STX4f-Vc2ab32{!z*`4u6BZ=_h$V z`5xaE7U2ElVe5ayE3gbpVnr5e#q>2)HC=B_ml2VET1sI9L@py+Rx;^Fn=^ec_Py)0 zw)Tt;bDsl&UFHI4^G+L(oxyoRKZ?3wjvh$=U>^L73_9wLRtkPUGRb6w&#e~*x1}GI zFN)Fw$(K-!lm>Dr=)ZL$k15o|C6*!iu@;32Dsbs1iCZt6UHs zwPuQ6m-yXb@>c2OBvg;~N=SA3hNwANPxY@tbE`3!-CY#Hl8^=erelT3_3Z4L611kh zYzxtCUn-$;UjaI)5g$RCN4`!=5(f&KNj31s*#ttTASOkO>V(A$0eVGN{=(7u@esZ2tMLPU^Utf<<)YhTq zdiH$ZnL=4<PC@OM#J=$a@iB{bBB+H5Cv}VqK3PG1knKqIBN7`G=Eaz*5X7A9--Rz?y>DD zmYX(N`M#I>YIWKtWs2fphcp53uxddm-{*+bH~Y9Y3>y?ykB>A>bgiDvC@sRml6ZndYwrwd32zTmoS$;>E22_0PBBWgt7{53b|^(U<}7A#wq!dzY?b!hY0k`(5E7}b}gQa+r% zB68fr+Z0uH!hPN_X!eXBWrNfOAF%!l9mwBoDO|oHpzdL=-_g0FS^R^Yg7Q1fgUCfh z|CDtGKHK&qt~pfedm9PQ#tagxZMW@U7daH*hEC(YY`xg<7_-$U1dDozOjtB7YAe*F-ZeK=iD6Ncezu!lmayeS`uP0odyP(7Lrj@8r7{Ow5(bRCaT zM!i+mG^z@8U^{f(2j{{z)PF71poGw!l9z-8Z9A6cq{GT7ZIc9Mm5;d@wlxX$aoA8+ zdquFZ@vo5d$bJ3Q1F!R$M>0VzF=?N@TD%cY(!BqcN^sm3cBe)D zZ&y9hn4mwA_RKWQ@ext$^~~Haq=xpo6>?QIXev33HSu*!@ilfon3JKTlVrxG$^gDm zzeE=EK&$KnX+igSLkK!SBG6sn4yCvVp`lsdna}Ip)KERc{w_Y`2K%a3FfGueLP$X0 z{uXMoAG2|bnJ;4n5ZHg>PZ$B+pu3xjff6FhqBG6<1 z38nL{68%CElsWCGCmg|wyhi%nmuPHQ2-6ZesBm)$l|s!g=!%6p!ZCv;{I zc2IJAVN_GE_VVLCKgnUp^$BP}>TOsSHQ~EsrycI6A~iQwru;C~DpsXZHY*uwoxG|% zmR?H#1+o_7L<(xL;fEt9g*}cPOZqt|;CCwYwu|O!vi-8uY?+F__Zey%2BE}57$?3u zIMO(k_sh;?>L!akj%Q!Bt`hPxMa=EJ+Kkmo3TbO+nOXvwL9M|*C518kCps;DjZqJ; zv{V*f*sTC9sU@Nq|PWG2x)s#WL#jb?f zxx=EhpRFjIn34&)k&zC-CHv$n!K6Lua3>CHKAvd&#&7}ZfA81Nkoctc`yXj{fC)*H zBLXLj_8(V7CH2{xKZbkuqTbq3ARLwb2EA2Sk>e*i`NBSgaHZ5B^;G>ZPc3Ke?V7|g zd9A6iVW##uU~SO~dU6s7LB0Zbx*g)niUgm$C$>Cnt*X3fHX8b=Hy%tx-eP8mJgl*7 zBp2~#ivGgNkSjArmSV=nAB^uFVp;z(letN~LbC#G^1IT#f%7TKQpsF|a+b^sE&9fe z3)ZW!GV3#lqW@yaMI`1WH!ZVx@RoX33%eX)^qiRH|9*p}*@*iGiSrGoDEEmfq@Ph_ zH<~N_4Yh58*#&7_Un#%1L8RP{PW<9alR;`PC}}TB%~!fxCcD-e+xof0b4#UXx2J$Y z5LLMNeQk!6(X_U=L^U*Z*^xJ&QMT<5Wdm6x!roJ{v%IQSD2s&T_&WMO(h#IFC4-^d z@|<~F{)$Qx+YWGO*)Q3|bOo!61TvOzEwdlvI&+zZdXp}T% z%XK|Ly<_=)HhX`(if!_o)DZ?Gvzo8JQGXX=y|w;(G$ejs{v3{Zj#>4eHWNo4?J`1YA-$F( z_Z9NLv}ly6QiLNfLZbXO@VpFgN0jn2w4&velsC=w-x$wEj?-^WT`LDEwiA6%^{H=v zbWzk{zQ$t)$(J!+UHr1$eCXoAkzrw>3+=++Pnaut&elhZpOsb3*%M`vPxNyp{_hgn-{eZ^hjx&NKHtU$&M=LPq<_W^j$reE_wv1nTh zM_P#nOAc@dut+3rP#OZ#CL_0~r0P4A@txk&w1Rsc=Vp(%kuh@gTvi7!YKOmG+Q)hl z5&MTo3}6jU)pgzQ32omSW7z0=y*zK)bkW%hV}A~W z;VEo#`pox15KB^X(a};SKWu$jtnoS2f2d1s-TwS|yqs+pTC=G=#QC4H4|ZJZ!8*+5 z2G~ND-+q>py=#{rvpR2no188be-$ZQYlXCFmyr&ngwgf79^_^&g-pG|!-L&TbCJH(k45^&YETJ0lgEC6tk<*QA`n1(;%e?g}>NvmY8P zZAria`qss-C7myBzB2IN-G(B!l~(&W*(YA`7E%fj07H;)LHc0vKmgqjQL2_1L72`` zfUVK(kXf?F{W$A*3k)1>IiGLG4L^R{Etab}p^ZR_0;;boEd(N45DTG~NK~Ze%=lq} zZO(w=f0kT=|A~{E!-5ZNqS#Zn1KWAeApJ|Qyum?}6ka-m-?C1^u9T(Ei6aDpRMhxAbtNVHKGJ>J{ z_quhxyzp;EoF77WXyPEAY4;eu=Yzzbvwv4;aS^x1F@}H`1OPj6Z$~#2}wSePUCn-T;GE2P%11^n=!E?`qFO27^;no&$6bk`ZAH!#>TGB9vBlS~7r*w_>ZS zJL=4(EK6q+{2L6eu5c#FGOO#8A7D>QHSnq0uLgSCS)XEMJqKEupC~WJA<7_l( z=&kF#s5YhNFiN!;@r42Xi>kb$q6P;C$8P(5`kz1f(|1YUzCa;MB*B;VHzuK$F7wGh zzwXZ#7eZAOc#76s)@|6Z7r_FSvJF^@xpM{I>GGx(W2DRk(Z?8phm|w%^mARW1ea^` z0<71~QuWlqN~8WlEV!GYVa>82`UC{NLTDkH(|c|pYPka6MZ>StxuQGNAw=pXrGd$`Gzx{@^`)Mi#6X{4k|&aY3eK7zow%JXZexL<#Oqz@){Ilji{M$31Ww9 zlqkg3D6l2IAGbN6kE2FkvCIT2z?)vJG0f>8H!a5Ab#wQ-&13B6Q|#r#x?PFIYC=@^ z3|X(zHUEJ8+aTv}Wv?HKqu&JyxAd?+k1X!&^ia=+U4Os%K85;{DxrF?%G&O@#5u%W z!{mT_L2uuh0^npH^-am9}uRj5I}&5I@8eH$U+qt8tghaK)!iR#wvKITA(Z|6Y~^kjl% zf(;KfFa-IXZ${%zR)KXbwwu(+4I}_^N9(+OXzt!5cA60x9i&|rJ9{N%E%jHM62iKb z2W}2@-UX$$*?GU}{O6gJd>9dNSHp-C-bQx|f#v*-|4Foc2sASrwFIihlc4P|wH9p5 z`7~+;*X+sKD26XK)}E^92Fhg{FCpUlOyj<|DAkjeC(?3?#_8`ygSNL9G0HddHYBXbifUa8{wD7=;!q#H? z-e1ipyhLy}$@-?#y!?Q`=S$7!pG$jyrpBgF(fZi7SCzcMlz)?`%et4~ z@L(E5um*cV--eN`u^=De{68bRpODxA8XRV)1LdM7e_=K@vNw%+0xVEkx zMe+TY&Ruo}RvMJXf%$%esL&7&e(SyPu(k+nUq>cVOz`pSUx}OBt!mn1-~parzJjar z_Rfd3@m)M;z!(&6p8-yLQQqI)#`~4>O+TEzUzaSeibL(`WAgILAKr{tub}Rn0sU$Q z?NUn;Y7gs$$(&5eMS!4lDVjv4;-rWI{k)_3WW2<;HRO&qepDyX&z$E8i?$l?3#)Ii z6YuUs+%W)0Z!k2)djKGFhs#RPmaRwWjPApuqu535H5s}&)tBBW{YPhrfiBZ!(5xA@ zs7srsJE*?OhqnSPAw6{Hw|)|U%^&1~>u|!XAHR>YdX*!jVIcfGQGNj^VKh_8J^}&1 zeVps?!{qVGf)mbCIrYmPTNFE1uKnL32T*MK0*=}8>)!YDTOE=;4Hw@J7(zX-R!m~6 zieS}^*mGO8%d?=g8u{42Oa88gy4?6KcoTmKZq!4*MpY-I7_EU}ld;A}=3mNGN5h0) z%f)a~pz0U>;)jZG{c4uq3GhWG?RSHSf|he0uFk z^d+t=05E|DnRA5;P2|zde^^d^CqTYML^U!ew#Y_JwrK8k*$xs+fHz`DPztjtS4-th ze1ywo+DNHwJ=mzf7E~l>yhGpb@o)Z5s=Q$@g8A#z(-SzI_ZJ>TlKbj@*+3CVq8x&s zhZK8;;e5=SbgY3z!`flDTd7h!vMV`21P4|q)+djJ=%8@inC>V~tzx6P{hwTX0DPom z_x0Cdxx%w$q#4X`$1^RfZja5Oe)c5Dol*b1+icHE1Wa^7ixYmZjw^fN3yvQIwtfLjF;1B|W2l2b=x(w~$x+tpN9X4tl)?GtXLUtX9Efg8Rxx zWt-Ze5Da8!z{n)%HdBmf_z~<#1gi>92>2~RhY(@Z3rl8+<_9r6;C4U3RF(Z_6km3N zJXyRgQmvT>IvB7^?6@HPEAI(yVCTM6DITQu>f`F+8%}V-)aVq z>3UY}TYa0HmA_pfVlz73{gf^Q*^Z@wGDPFBIK|Q@xNoEA70idkUj$mX!g`gy6H8#@ zk?wDy`QGd{h`>q;SmPM`$v~X$1+#b=QJ8*f?4fF4Qhm4GcL$kFC1((L;xn3%)ky46 z7!No%OOdH>M%Q9gFo!lo`;=Gm9pNESFof5VjwCy(Se1;xhrL4xa}Rg|P7P0B9Nk7` z^)df(H9WoZ{=yn(D1!|1>75C?hihhojPe0bE9BxR>@iKB^P=nRkfj}LqOW&;zJpxB znMt!g|NHdiA&>)o3zzWlo z5(}oCH+NpxP!9Uv22z-OClz!~Tg>vQ~pX6Q87wx?IYGp$Bw^s8TFlFrL=C47Nk|GT?m0+Hhb7LINqG=$$2z0Xt{^jE6M@!`(;Ui`p5 zH3IZ542zq8YilKTJ}2v3s*FQaIZ7BH*Kc7;NX0-elwNp8vw-t)$phQb_^1c{Rpi>> z<0duHTi8LU*z1jdzy}sJFIg%pKi4f)0FzbFsDz9kaZQ4bo zKPf3B9?8ivxM>0@ZytFLX1y3v>v2sL#>S~(tt|X+rXBIKR(Y}3bS~pS!rQU$2TkjH z4}VY86>69X9#{7~cF<1Sd38ZnBq`)dI)WVxVU}aLI_jFWq~210nnZL%tytfSGK8|9 z^w3V=D+qilHs}Lf&!jpjl6dq<#R!U}g5B(@N!phzkk|10dG|-fx?T6Xl(XLxf(aHp zI(60~!j-3PapQ^uxjmxvT>vxLm;+H|YvT*DF%OSj0uHFGB>QZNdmmZUXG^E&a15SX zegc*Zz+RP#{4jTTYO`J^H_l)>7@<1Eoz{dyLIhhObmz%R8APJv=aKtpLM$xmw@YBO ztZ5Mk&COdY|FU#3rq6;5J11-_ECBD}u+)rb=x=rai79v;uK4W&7jwp9bnu(&q`gs3 zd2+?bW?)u@C z&{w=fPCX)xf&x2q?KZ6cq{Zl};bVd&qPkDu!ia&3!gKzJFc@wXOwHtv3^WFZt+OGA zu)DI>xpM>36%tFGy54gM)3Ea1ZsyQxx3s+a40U!QeLX|uYN*Q)u~0)n6xgwzm-YjU zZn2_tmL{eMGil)k_Y>?qm_M{N$^z;2-NfPmrhMeGjoRpQc!{Fi6`8;(svQ4WbdS&S z#ftn1UQN=5>w|~&5nN=787f?Zy{+dM*EWi52l1?)dBv{FSX%>ymx#;TeCEGH$BcJn z$C@7g*nR?VD-!V6uHeRNDumD*<`~7eNCr;*K)-B_iubaQrgAPGNJGDWvSm&a*@*n1 z&i*`b7yx7&XlqY}LC3=E0vCy_n@^zX47oL`y~BGPFoqc^nWyJzfSbt;NIS=wlO5@7 z6jTI>uGui@vfe=OROy2wZ8&s@?Jp;g3PlZmIf{ImRM0o1w?|oi^7&dB-WTna^R7(j zW>sd=8{%uIaIS9Px}kR&T#6aj8X5qhzJG5yWO}2$a()!S6zLnyb`Te&`Zkun3*?1J z+}=K^1*WBCeZ={BkxrNVsDF%J`DUSYE`!#HRtyuGOY-qb^8m~V2nPeS7yY*o5>*b- ziq8BR%9ouXCZohCKQZAHbW4RV-5oF330LVAM+Np;Z_6?$V=9hEfDOEi2k=+XggYYWI37;QDI`uKoc%~+&M7LyOz7g>7YaJEWQ|4J6A zRocpPX>TSh;CiFa(IpWfvz|7Uu{#1+^rc0p!AN{Ls$P?AfwLkO#Q1~Zd$rDt&xrYG zXM&a9SS7Nwk0HDwXfG&E~XG2K==msy@Vt3@Z6M zXBe;7325}1{@*8}si zu9D}pF$g;Auh$g&8!DFpdRJ@g25b?f*0;FdrmJ@$P$^M6_q>JdT3oics73&JpMP_f z8UV+Zhm9l1O@KIpmW8hgjQr%dbc495ICJYA-Hs$7lsam{!y)grt+7FwP#7bnPxpLK z50D(mLyt`Nq;$3`^zD0lw}YrniFvlg?e5{#== z(_X=`43reKZP3;W;>S!Yf>N$)y%+UK=vi_>wsAUHx7DWlLQD5V+3Y92>#n5M7aE!sd~2_zhfQYZM|Cxsps`vON6N_uZb?lWq=3( z@v+Mf23y2=z?h5ezDT5s&|U;X;aPmAvlbEiunti}+92W^Ej9xIyjOzfXN)%{0RC9) z0f5vN<`kG}&VZ9g+fWTxp4QEyx)Q*ts}U~_?t|l+Ui6|Dx#vCac@%s@ZpEo-(nSX$ z$ho>T_w8DC8_0;cQpoA?T?j_DAD+34ctn8Yb;h^Oa3BBp$0^hqk&Q6mBH#=65YY9e zH@%6%c(Km}k)UfHTH~teS~43JvN+daI&@OnoKcV^!(>8`-1T(=Owoi0IjG@?q48b< zNi7>dO?_*a@jidPm$D<|7A7xTAU{Hh|NYcVOarRRIb_ z?ktLh z8+mYP8&}urg+&x1|zh#oO$R6a^+$BG~A*TEjcn;=S)ypo^y~RGSPS6 z4{ZR`>xR{kaeqqGM?Ufqcj+aUlEek%XXLR=D1#J;0GBze6it|2hgTdmKbMdJGGIv0a#qkG+-G-i*-HuPO?cEPP-Nv zhUp9dug@yvihTaAa%-p71}-Q$9~K>-G<4>fXOifTj7A6=g5Jr;eEr+M{hRy!?|<(; z_`wg-ZakS)M%I(-M}#P96C`7~|M~=r2(I#3 zl`RhpKxX(w`@e9?7ihy5A0sE{cTh-*>pg`lta5nVsCu@2L+5-$5RLG2CIRGv z7cweG>3MyguZi9*i3t2LK`lDS%RqJ^T?x zV^$2h+m>;b$H4y%^ETthU_?l8!kds{5@9pYAA{kq!Oe^;t(LVD3-P*ZucNKHu-fv% zDii(vLI3}TDZtPxa>hX zl^E&Eu)P)^++($XGaLbNaG)ndPb9uP;)o-t&1^iBM6h%*rlcwEr$rbnA|a(3Stjpy zK>MEaoaaz`VM)ghLKxO?9rIulQQOL0va>VEU zk7pmdmF`>CBRLdv3;2tHn9Rw!u3Aj2)0drl-no>qcyeNrQ~}%v((+-RF#I9S766D3 zaoO@Lu;_g0rI(UD00~d`-FIL2(3*$bgzG0Y=?Y0zr)7NGfZnQVPKy2@Wg2oY{wac$ z6lJy`+WA-L;&W(@<#b7D7koN!iW_XGpt+qj0YGAQved0yu`)ap#g~f%Ekf?VMAjB2IxMkpp81C1%g8O58^4sj zL_^nmY?ru|tu@Y9ua)CC6C-sk19-CrV_Zo6PG* z-eTK;wRKol>Br@dMg=GVIq_1TgaPec?|hd#^~abD_ z`9PiISNk`XAxz!J?qSRzKp8y$PfV5n`T(D3LNImF{M1Fp0xd-B7 zLki{+hg)!}^RZDcycrf^{k>$|^m|FBAPr3h)CSVHT<(uP%+(V4BUMD{y}G?!#P5>v zm{D7useK6oFbK{Bgrx%Fdl9nwh`#!(GFlq80JERoKv;K7*#`t8qNJef4O$MPh#pO3 zFHS9>fIKrZZgO&-JK=m8o||jC!@Cq`7hmktMRUasC{H{wV9X1`fN1 zJpj>*<72hdWz|0jq~2|mov?XOpM5+)TQ;MpE@e37?;#X;y@~vW7a&ZmasvYN!4BD? zBV=%HbbQn;8jr2<$%*i7q9hQ$oZtNVH}3MwE+=jdV;1ysJ)m-N(|vE9m6?OAcCw*~ z$3(_;%XaDS{JYhNmburaD#*U|Z=}n}iS;Q5COvnJa6?h-~`gBKx6}l8F5P`V0XLbhbt=@@;)U$;9dfRWBiFZP%KXdFG1yk;TUlOkOt)ObY+e1x1-fdVZ0M<{4w0( z;hsQ#+3%1qz)KDmVI~z|T^a!!t_;n2MF({|+SHXc4yd2eTuruzkqwIi7W?xrIG+ds zs}*g)E=IDnj{1!woN(39+nGRw{BNnKJ>ymRnv zwSFrw<_z0^agfIycicgzN4@4XuOYubV*h#5R%?1L$-NV9xcKq@#XPdJAW?aQt9PF3II1N)PekxW?xPra&9` zxSk;9C3f4%9djB|oHqr1wZMKaM>M)ZF|)83;E-(?N*`x0MKwd4t#I_*t+(9jKJ>v4 zk+BzWLZsU`9Y)RjKOr$)t8zai!-;0 z%+elEzf*to@RaE1bWQ4(#u&-6Qd~F~Z@q-2rI)UpJ$J1L)T7fgHS#0E?=%1Y8TXa1 zeuW}E5&MswfuH-_=Vf^v4PDxA+Gw~G`&pOisP~sVlQY<~Efjg5qwx@|^P?aBD8v+`kWwSU=qQp# z)MEdHKfp9K5rY`$+84dFb?{5w$_f&LKwPapSn7!;ENp1z?837NNe@EH`lWSW)&6`C z^#Aw&{vVxu#D~q|TnCD@bUk;@Ip>f{@aa!~n%ukS4F-IsK(!_ zN4f?h84aR*g&oZF#J#Y@F4KOBwvmAKff$phKeJXJe zmLgzOVl@K`QMZNLqw8w4Sy+)%_6_b+RDOXkIWlA!NG_!xOO?Sjcu$~HBm5bC$6rQH zxep`N{zj7uT!~g$vaNh*?5N+e3<7Zb?{9ZoZ@qOSvG51&#&u2)hMWK=o_L}W2`UWO z1!}OV8nj4jaRL3hq?*ijfj|Tjb3;T>-;kY2O+u^H)v^O5bdjjD>b_jGl5!F=n?-`7 zi39(|Nw8SJ_|1`jC3-mSgLuGe0OEmium{35kFa-9O_+b8)nZ&M9$Qx0Oy7o}4888Ocs$Dk_UmxAmb*dvaw+V$kdL$i>H; z94Xn_08#=72F`uhYp=Z`_j27J{!^~jhWvdjZFMne^=u=yyl(+NM@GQ524*KChuB7? zT@W`~ScT`ES?`wCA+OLApP+}bFS;%8L?(PB7)hU>*ljiCAtjMm*SH<9o%Bl%M0rMRjFwk_?s=br9e?|K&zIz&)nEP?p2v;&Et z5-{>f-M*pbXd`k)61CjDG)f|W9~0TB=_xn0rrz&AGFH3ythK>X4ZiQb+wN}D%{Hyu zYRJ&yKlxa;Je7@5vn8+r}mAoB!pS=zpnV#Z_x(BfvRsL@5Fx5PE&vm69wl$c~0 z=FH|~^4&?D6qb8%Y2@SsLUQ1bW#!f?0gos2`aAw`2Z{Me_WAnPzrihEzAQ)@corNG zh<^I%r@QmcJI}rP)vu<+liO~)Z4f&L*{jifKAkzKL@n!5zf~TBXJoKbGB06^<+`C} z_${xU#z${JtNP_|nW3)8S8W7RSt57F&J~c7X@|@xtcXa`ZleiNsrG2yv8z!C0HoxDI3T8f?|a{KS6p!g-GhiKi39HQ zJ^(op+CG5LUj&%|jy?8R_rVW-fXYI^2@HxreCF~X1UQBYgyO|7elaEgU|RsWq6)`Z zw?W$kQlfoV1n=@(sMjNqcE@o)v1sY{NIy|a>e=wyKdZ0jK2%Sy`Mamu#%bi}GgkkV z_oHz%Vp|#@_GAQ~yo{k?0@Lfe)?qU!8E;ktrJXF4k$Ioo)~SiJUJN>hC4w6ECbb%slIN#Qz8+_a-pNcb%#8BKv>Sv$0qakWu&H_ta0qbVwCsE5acAb} zeeqckDzU_gFh?8*AaO;g4)Q&X3sTKaNmF=_PB@a2IFRe1^j+iG)S{w$mG77z$${b( zxcHVPvh^JZ4V@gmf7A^m<+RY8U-0X z*bY=Bs*e;PE>00KgLgoNbA+kz1$3_=bfOm1Kt(`Ipe9arHQWkceXj1t%vX#xG` z8ca54ST~n~C@Ygud!WEfvmwR=GKEwvn>ENGL!i!?MZxzN8E?JyRzf-;I=lWDK_R;D zxZ{qrI}ENZcn4AZYj3BJ<^Y&v{%CQmULm&DO9alxh zyaX{zap}_W%R!s5F;^CIPqZCU&#b?m*mLjdWHLAX<|aC% z1?m|jG)f9OUFU&_@>6PCfz<7>1TwrU+7Zjod0RpxSr}ju3RW*rR$HJK^}`JViGvKA zH7Np7wJ$2pcdk#X?F0qZS$Q@bV!n~`3zN;~@v&rp1@Xo=y)hs!s|6{kMlMq3^awh6 z<};s3r_OT5UoA=lX+Q{&N(}oD{^ei(h1!JrLnyM6rgD0|Zp)yMD_8dc&msWoD_{8v z89490=WeBh8ze-hk2iO`vK9<2$@JXivlvk5RNOg^XFeIcPnh70Ac z=fqeE9QJ#`1sBliJnTM0Ih+gx6aU=jKG!|}`Ol{^R;^qWrR0uOmjqXX{>b^|alRbv z0UU)U%i!;nK77$43LXLx;8bK74{1%eEbFC^;-;2puXTOhXFU$sLXCDK4I9)Ms8-9D zEw8zb2><0d(Bi69k2RnAX?Zo!%B$c9MK0Ni}`);d*AE6`n9jRtFO75;uvsD z+fRP-lUk}7ubG1!lVE@oHUN627a!)qYg;~JMY%WZ2nt8S99bKHL;?``ypH{#{tm?% zxC2;&a??q;Eha$@iufI#<9KHINr0ud^54i-kW!gMI4Y1IMmcaQ`0M#VPVD64G~Q2o z(v#dj|MNeS0g#PIoDxjxlQR0M2N5w)-kQtIo1EuXuUbt>NwJ_1N(w|reH@L(fCUMI zA+EtIfVcz>8RlRiE8;7q=Vw(VLnm${3VJRgMDvHIAm#yZ%I8lZLhzasU+upC{j-Ru zauQVXhD_DblRVzJ51x~sl6Tm~0`VR8i16O@6zTi;@QJ@@+Xp`Q0XmQc?;wB+#2%*v zg8|?aCu|eu%|J451R=HxV3PMzW0xD>6r_l-C23f=6pWxH94rU2(xA`yfd~V@TZg$G zFLG(>m$G7WJys5X*=9M2k=UdBr+l5=L0|gPm&otWB0v8H*WG{r{oRK@{9(8K_S?JZ zwS7Ytb8{vFvR}TD;%n8X%AOm*lkkQ&ynzs(4+PPRpiXEyNed3`gE5h>2aC10Xh-!Z zq?96W7xh5Cqsy@@&~l8)3oO)mDDcuA4WF% zOxPdJ!+`G?k3m1E10pwtXmaN{YWpvT^t2lIKQZijV_dNkfh6*1wpV?y0H9JG@};0w1>duam& zr!A5Ts%~q?%?};g*`SC5dD*fL5gVdEBK2^T7w*+acc0s1k3HPm-u5L_3&5rNpBggz0(qG4knPR)Ul57~xkq0O5l4#n@WcLL92J1W`;10mZdk@4Qv^!@ zS-oAo@YYtfTMULiga9K6C5%i97cFoPt?mc@@a7Z%7A4vRXp0VP+q5(YloAF8c^>21 zwRP|m#utbMVu${I`O9A>q=T4krA;p(04cBtv6d8jUIoE@%s-5-IHwWWfG~!9q#F|g zJdb;5KL{Aohu!e28^|*Ri4NSD<_xAaX_%XZ^)5qxqcSD>$`gef1LEHkW`C4NdjSRp z-7(*=WT246ne@)KTy-KyVo=)!CUxHYdBo`vY=cDz4h#Jr`X5A99A|}n-_ZF0bez}x zd2VK{AT+|&V~J)GfcgxaZ=M#Ot0?j0H@@*rclzn4xrbIg)JQbJ<$<%X+t3p8#v{PwrMB?7o1!1(w$AvpI<8)t#sKz=4< z$Pe(@&wiE&`EI-JMu*H00Ut9g{~f{16QGlXDtjBh7o-QZ$3OzxYZ|)&Em1aUaYtoh zi(~2}mqthWt!^$J-%+cUbyN| zJ}klzVJ&X2Lqe0t$EpbDhhs5rx(+rv{`lkE<(FSZrv<}s#ybx|3_!5({5D%}LqrOI zwn}19vbR2P52?>1*vgo!)VP)~i-0h~3eEM&@xoYR+_4)HDgYbm&@Zk(BudpPW+nis z)t=FdCBYHT;dvY(0br*l#+^g9Wf^RLJOm7U3VAlSP?8&oqJX}zp* zEs!RpG#*Jly=Ed`Qv&E}1Bmk3iw-w}a16u_b^%R5S>0o^VL8suQA&)davAhqgAF7GrTwi_BT$l<|IUEC3j97+7oKcf5P{ zv!CrY6!_r}e@Jry0>EAvIfs}VsEOFx4B~}To3U=l!!T9b3mTwBbItQaNO2Mvr&4Zl z8J(!(M2vF+0~1SG)GZ(+G_0BpL!K~F=*f?)#@<4o-Qp^Y8*LCF8e9K8WrnK>R^a>Y zyN|>+Ofrmqpy)vUop;`uBJZ#x55UKHMePrU)i}9?X=&FKo?V|F*!YTeL8Qc%V7LWg zWa7GEiN<#vZi;{uxYjsuNiNJ>N^#u=elGQFsXEbChqY-&1Ns7FVhM($1S9`=<`bZCPjJhl&V*TQhiv;0~PjCq(kSp4Xu{=gK&DJA0f^U&7mp$y3^D543pTmXyeozSK=?Uu zex!8bZN5M_`1z5)4_kLRWHl#dMosr(q$C8~YSVZsr6joU+z5&MNDA`d4}I8uB-=buYK)n-R^tr-apeFp}~R}l_Xz@5F73(a0N(j!`lZ#f1>`Wx>Gg- z_U(t$mb_YGaxJ-6-g#v6>rWa^rI*HhUm578*4Fb4sfvw#yhBg|M`A)UfU3wFxIi@c z;1{c{^{_P8Lc?Rh?n5IuC`yE4>Gg8_EeYHJT1#$72%OIlPf`Ym;<81IO9?_sD#^qN z9Cm!74x|0#r#|UUIpq`*t6BHM_FMSl;p#(W6l^nIM*=pCC|sq`rDfeIJ=xsY;h$sT z0V&{F&w7^IX1i_NRaad_zJKWbNTjjruDcS4p~WrZ%zatBi%yPBN)+e5bjoDY0i@bs zaV#xWP*Mu)O5<(4qp^3zH#oFAxSp#TNrP_f@0ok-TiRQBqDt>3gcjLu-aa1k0jiYS z;cpMI_R{Pg^RWo=IiSn(+e^EEvY}b_nh3m?E`={c697h%WD*DVMHVI+v7QSh3$Z&T zUdlyBx%}rZpb0``7xvrZq!$2OPcM1NO9=UQ*kOk{GHh)f1E9lexgT*wZq|Ug9}r-_ zefOh}1ma5Gg~&1MQFD6j8L%5<0=t7{TV&$ToSnohtFTTPcoR4~>%sLy?d4QW zWJ`gziLkAP2|-C8w7RTuCzTU0YKNiEYa@K=ICdfcLA2s}+Bz*otxgsBKat@xAySLq zQXOEwGmsx~=J*D5*>@9C0~3!3S|*0;WL*u&bH*8G&`vaj(IS;I_TpnZE)0M$ z+@KCl^&A;&R6ezw>QZ7U52>HnPb?7`L1W%0&tqbG6Z)c1(jQ#v1t3fDG;PYax2tx9 zfMB9cKn(oTbz5%6s1HVfl_vI9*sFET8m|b^DtnJa7vjkFucBvV{(m8^n-WgyT}^P+ z`w`)6!M`8@bb*{fOLt5@V>ZUpW3`B!Ym1_|lw;3ZZ*dUEo8SCqGWcP02i$DX`=ym% zTTW%0a@^jE2Pg%(n5BKh4_^4wC{gK+5|UC_iQ6xCf=%H(3@S}Dq&W6284^zLB(RjG z6TSqsu^E375hpRvJ-w4tv@6xmzFV>XEQsAmRmMm)R}b|SrqYiRz!>UJ7C((O(MbY` z2Ak{Q)tVDUQj|!Squ+_=u~Wn%0yPk4j|3h#iH9TSD6)P8U7H z0*~Ox^nVf|TNXh}wqN}bg~V+e)LxSTss3qKV6rYh?!gr%kUg|FV;^V}hq*ocZclm^ zN)Rw1Eto`@1iXH(K}p#gOhgj~S&Z6z#${VlfrUpFkx#|}v<`rwt0WC)!o8vOL-~FZ zQuIXXSZW~Ig1_JX`?*67IV3zsx&3;zaV>BX-$tw5eQsJ2i=-MjT7@G{<2;NFhqpHf1M}>SWT7JwTgGEOnR z+HEBEC{Ct28=OBwq@u@iHWN!N8w66)h1H%JBQHhi_y^|R_cBD1PeR4gOfU)@gM{j1 zI~s(*ubblKksx#ojqSiW>#NxbTSg5aE$NyAb^()7XT&cA0P*R&VZ(>!1WNDcwnd_q z)YtuYc_3t`#jQm(o=pD3|FeisqVE#nb$c3;idd{%$1wW4Y=^#{dOlYU?~l-Z*FOIo za8zof+6c8m2Xs{_PlP&bGaBb;=?EKwM>efi$@ZIGF|`gGdcNJ!v|C(m@{(;pICHNb zu}GP#&9ZYY7(u196*9V~3>zf@4$aEMSDz!0L7yeZ?GE2ZdK8`+iEt^gNKOEY{f65_ zN&s4?)yM|PLq{cQ{VmdW^+dL5M@CpJmm$wNG<8$oPb+FJYZQ!zA|^E4$F_I5eW`oe z_7mEsnTm=5kike%4n8L2tWqgkKo9LfdNSpG(9xBleF@Nj5QAWr$s11F*y%q^9{fR>yf6n8g)e@dd znK+ZmvqD~lO4mR*!!jRTH$bZ*ZO>T0^|QPlBA1`%c3N%IhBPv;NE0g6Iwze=#?0-D1r^AKN?j)k z#Hd>l8B?Nx*PJgARxCv^e%?aVMy__TEix=1!;VqZZ$qbNgeTEzw019In=9fBjy&>6 z+Aso10Ld|^Eu1?+hRupGF8iJm19YYJ39D=$80g!PD!5BKz=Y}M$P1M<- z*Q{OBi8%Fqy_LvapvCV{XMt@e)FFs46WYc+2wOmeORBCEz5q-DkW&GO;<5!)ateXG zJlHbmAuB#<4kOF;lSPuD61P*Xjk<`wSQ?$?(xI;@z!p|)3+3=#EMP9N<_IoXmcM>vnP~avVmAL?zhvyWRHNxs4WW6wa?lSTiTEGx7-<5e1uy)y5wy{Rpk^m`nTZ6PgM5;$yDDWUhZZ#s2q}7wToYJea zgwJ!wsIxxRIKxm!g8_yMc#I|l8-_4=oHYig`Qo$NZo9jqkAAj0?zrRK7*3Hva*y%R z7{QEjVD5~KN}vDy=P5-N!j>UH5Q5B`Pi24zAPWuZ#5=d&e!Kg@4}L)T3~-Ipg-gU^ z$pXnlnb?PsWa&dUDn+Z+hM#zF(*4up$8F;@+}M)M>JQs(A}ctD{YDEZmQc(9G^;lwAY;zh%fM85ux%%FD41UW6^(kw46`bXDfc)YZQG+(Me5(j!2BVbGG z?C-KZsn=T#m+x~#mZFZ@k}A2DCF1gR%h;6@Rf|0y9IR2*=iUG9-~KI~c(e6ZTe}^1 z+R^Rxn7!QN_kTQP9O8tMYzGKjPf+79Mu^Du2UBjh?RFHZi~~ZkB^a5DN(dkJrAvE+ z-oK9fNV~W*^jYBm+d$ywAiM)Umw=nH<0(Ny}Y295~ z|3e2Ye3voWa>IwRNV6J|b!jEq)euJuLoDNhY@)%|^~XK#arDu^=lj(*fxZXAvUNz$ z05$;OFT{TASHS1yn{TG1rC9Djlv*E}G)#z3BIgFFHN($|rzPrk3E5b>M(%>h$0&P`QQWUqkD;1Q2_1Z5`1 z9tPy9|Kb3O2VG(4e1A{MAm%<-A1l@R>M)+D_cv=*D~vy5RgNEe4gJ-kW+gDTR*vosZ8yq2ci@2sQo>RktN|mT240j*1R?fgBNTN1r#$5; zl)DbV2Ve%#x*#WjlvVLqSWZzcJ0QT4FaI+mr&s&+{hPQ zbdh`6%U%|aWw+u@B=F|52>?j66Ju8ja7O{;x6yuAGLEvmj5@%~;MZaFm zArSyAHkt!3MQO2kO|j6BAWX3+6U-6~h7faehfr{urp5q<3JW-%1qyj(v>N-=t-abQ zRjDq6V@dBe0vOG;F_{P2wA^4eP=;v@Wp@C*k5(X%tojj_eEGoYNA7VBfm(ys$LwBb&u z0t|9OnA2kc@nXks=x*C>w_SKQqI|OiTIULFHb769lyZXWlgCj!M58sP3mE{BlvIh_RH$ zcx`Dr2pjq!1t1|RK70)O#Mnfin-3VK92iiBgNAsMswR}eSHx~zUaQ$6f&7}~<&4?K+Ir!3>vG4e39eD#UhuXbUOY z$qR=MY`}f^Y85a?2tGX6+IC;^CoB}P{>p|4nQAZ`a8bP$QD zKpJpQR*kTTF%EpLz@dj8N-6{55y&GkG8&x(WCdJVZO*#7*l~*XgAar4(O5d-ziqZySGS1vvg?D%n|PafI)O}5(koikdC?W@%j{|$;o+i ztQp$I?n<;92gbst5*Z5Bpa21Lqx5kqD!oQFUMu^NGAz=$H@=paF+_!|UHbNj`})B1bzf_QHMM?H%nKH%p(_KHJ83 zsl!PFC~qD0$tF0A6n#Tqp%Nfy2mnIBx-$p@&NO`LQ=dv{&>r=uN7aXog5!^hu`ldG zXy!vzx z7J9wEVMe9D8US*Hq~YfCeQ$&#t3?vVP3UN{&st1Jzi>PlG8@4di2efUF%G-zvJ0g> zJNoFO$zTYe90&;Z821N>2*T4q?$8uZl-WC>=R(>m#5~jddE+MEV~Gs}0K^N34=X>8@b%+%LdEy5as8oBYtn-G(MWPkqJ)Kgw!~Q~^AA#hSL0kLhEnPqKP1wznQLEzA?M~`Z#vt0e?z-zLoQi|Vk&SAl zRFacOqo1|y(!XN`_~>ie?ZPU`uWtC2``qU~M}-gy1>wk@KymO8{+Te4Wf4!?6_gakdnlZa9CNT@h6CpeQ+raKDa+PA9D>hh()d%Rd;ql575sraVbxT)knE-Q(12e5m3I1 zP+4$xa8-;0hb1#W0%Bs3s`>s&7Ry9__RuLGhuyGNCQ^n)7IXg_6S3K*n?-d}(vKM=0yU0u z((7uLxWDPH$rUXSU3xx38|CRP0{ON2!xIjI!-&uKF%D$-^y!~z_n{Oi1tE=)rh{Ze zWyoyFim-)+>{U6m2@;8pS%B!wHkyY0NkxbDFDFzlD+90`*UZFN$W!^V^|031a7lJ2 ze<~A)D2LsKN*$4XkZU>>@|%(#Qf-y}*54z-AQ(urM?nQJ!5BEW8?Xc<7ZxtV)@O}L zIJvzrO zl?+rLlV!CV|G@ z8B(h;QI*FAR(ubQ%GPkZ5(+% z5~aok${1(SC|agsgG}_E+B*limDniD)#Wr6zs`0e$Ae?gQxR&K@F)p|R2d`Vl$lgk zB5_KM(AQKulAwrMQd%@b@`kb1KNs+W8KqAgl2Z*sd5Q6qDi*gZNSFF-LM82_34o%G z!FZ+puL4mcswOF}=uo2?OC%E=lY^`cYO)P-5Zd(e1_G8s%3^K=Ej0*`AZ#fylE2Hv zD?KEV080@!;M@~GumM4!+-{usLbC@w~yTLz@(g$pMHm${6Xs`(x3c z;M9V)bks;(;YM0&g&|_5{J8ozi(DE(iXoxvH>kN9m#ptLp*u&h4!*Aq!XXKR2$5|N zBFXI%D$}fj^~;fkd*K=6lh^x;xkf`K_p349V%^mw6Q!@?o2UO`p}QoGOe_2 z8iYL|^8RGnPHkyTImP?n3l@26BfF&alM6-beL2WA!AmAXA%-c5_Vr84C>g!0?ItN} zG=nhF&^)vtP}H~5@lKVgAL|l0RGM&PB=bxmeg=b|+V38%$%646ICrGVwU+}aopoYP zVt$jZzm9-%(ndhbjV8r4Pt6-f9I2nG_q9l3Nf-_BnWF`D zaXr#lB{#zG^AT6q!A{b$E)RZS>B=}oSoSa51*!kFoB=3L+wBZGzFnyx+O?@I?2QDn zVr;aFQjSro&;0^2_oVTyr))I*x(Hf}2y{lWwZ_G2V~Jr;mXp8TY`-?3Ms*uT^!7+V zsjpcofMszsj6+gS&RJBWHu$ni7N@1#=S0(FIfhlgp>qqOq&)!vv4!|2eZ`GFiaPYO z4Y4){4UJ$(X{it*JzA+q;k%4+mvySOWx2h=qX}_6xSEa4K2bTsGbD*DL(~ST28y+8 z8U8GTZML0iW0=g`(*T2^CUt*f>`h5i^><7b$Z46PhlIY=uq=9=)HKj3UD~A3+TbIN zXhv{HZcXd*BZSq)G0O%aB#5MIPwL=eVKww_)vq8Ou+${Dec*Qfy`+d}A(u5KDg>1- zsr?G5k7MyU7eD7vkY@>)pxa?=f@&~?+}sr=%ShT$G*MwyprxI3snE4pfE>F>9m?dsRf}Yk13UvgH7tu7KNA3 zalg!gPL!z;vC&>bf=NLnD)ut%Qr;J;(Ws%Wmv8IqG6QA;nWdwTx}kFy?YJ^sCp1fa4d5WV$SdY9OB z7h?V`LD45@!Jdb)5uY$t^r21nPpQOVZd;g7EI0~+t<+w&^d7o_E6) zq@B`Yd30S+EY4N%XE4TaMq~|&Tn1|1AkF%u95_nspZ@8e+;_kGUCOTjK=O+#uDHUz z<>a@xgAP21o~0Dxsg!XN`Fvg2Fe48i`QHvR7{jSLZvg()Dah*0QLP5R45VyZTy7iQ zMTdhjwQp2lh@H~97$JO9R%wW*QFK&Jr{-IJw;Ko)(tqLjEAFu5oMwE$EEu=iAZwIL z>9kT$6F)2|B$OqjrN|CbRg?&2^UXG|@5!)W&gR2)`mkY)CC(N&<&;zCL>>?W92$bn zMaY2jjc^4ltiou#RQ!9R?}j}oCDEiH zHJ)9dm*K*)Osg)Wb;rDHwbfRnPH`%B`MmVm589sSzP1+?YSKv+AlL>3LZu8I4W!n( zj$s*(8Y?H>cN7Z|8Dgb{V2r~LKfFHN)E|%$`l<+*H2UFS(+_;$1C(us(}D3lZEND|sWf!Cx$Sz* zvW8pM^j?N2TWut%%jcosb-)Q+k-YUyZ+es4V~;)Ds+FtiOhPJAXl|`q*Y!FJ-<293 zh)jX!D}Oei(JoWWkocdV)+rsh@tiMr?bKQ_RNZ>(t;DTB0R9X$a9%`6066pm1c1*) z2pNXGXpXUzC)40WVVr)0XE~uLfR4!a2{1qi@D~I?a&YY`V@83x@fT+qUU=b!wp&l{ z$5(cc^##2FPKVa-O9H@z4w;XSKmK?k0K6qvO1uFQX}|zh#9*mAyA+ZHAaMH>5%5PI zu^*G)F{Om~<)|EeJu>2W!x9$ipP>(I5alxhb3Wf{B;HyJu@syiNQ;yMIpsPe z3wbP7m@C7*%JoTGK~DOLjKv!OR6`OVDFMl{E-nX2FL87f^2?Fm+AuQ7NsuD7v^z*Ug!Nw3 zCv}E>QdBcG0_E%zsj;!z+!zxjGDFOK?9yKoO zgu0jVxR_oXZ6+8ScfISczrMmkB1^lEa0?MjVxq0VlSvWl^84al%DNxvz{*%C%bT)}ZL}z45w?i4mIgFPL!tG4 zN_7a&DV3X<8U8-7bg8@c`m37+0B?~~Nv2HB5CE<~>-akAE?9n*5iAGLo%qng_}Tc@!AAVuAC&pqxt-~Nuf{#Vx%qC=8++S8s!hlwy3qxr3TfK4V`H`oM5 zjYOaSi(#6*DBcbuPYz}X_JIUaYl5j6MAloO2>=Y1-m~`LvIpH&SNp8Mp%6A8DKTBg zsn@l{4InBl41^ZfEvB}fDzbfH^{Uk&4?BvaW1o~rWa&uepcr*o#HtjuZfmI;vQJ4h zfW<#<^ zfuV$&p5w{BGa|?}!tZlkDUL=6gjffbw)Ba(a_oh^O|cL1j69Jv#gtr~TH~URR5Itp z@b!@hjxzjENe>L9GGu4gq2^ZKxL))A07gGk71V(0rd%Bg9O;@gZ~}pqTQ4-2GnsIb z3L$n{C*GDqs8?9A5wAX8jW-W_*Z?3%H-;-I_l|xxB4h>uFhxbJPimqluQ6iy48kA+ zKv+Z);udFI9v|yl{w?Ba>8#v7AoA$MXo%rRX7cgn0AZ%9>9R3XiB=*aq^;x}5h>lZ z>cm{VK6|WsW)>F{s*u`C=O@fS#F1sYRV1P$jBx=?DXfh+{7~L=Gic^t77GQB~pP@2Sa_vYYhTG z@5lj>#+OL0>@QqefQmS1sUgML^s`n6GV7wDqHwuMj^+A?Me~DdCFM$!+S z0$rD$Nann}2wDh(Pz@4Y#CN`qxje6nb1TcH(w*E zn^ISb3@L`sJ*AK74m28+nZFVjky&p6g30G4cWiJ%qwS&mUU?|0e6N9&iMBBT3hyYR z8W{z(NNl7V3*0#o;@lwY>RBnG>UIYkkVcj^5Xi);Wp-S+dBUF2Am&y-3fln)l8`J% zNr>{8EegE`(NNy9`nCEW83h%w3O!ZK(&|6sT7(O{CQ8(R%rhvF!nLzcv{hi}UR-ey z?^5ODf`)`1r#W*NC|Rx-*-WD0C4(*^YGbjBY{$CnNW^AQmh&i<7>BI36{NS#(YXK(l_=%gwQL}k2$=E===!xSAh^Y3nF9Iccj_1KlUMEM zGNZOCh7d)xiE|EuN|MvCWX{CQ`fIdXjf-xVrSogmHS!Md;Vdd$n|@Yj{mI#&B3oCu zl~x7X)St*^XGE?tjCk)~@@TLW1hLI-d~6*X+rCAmD=EeHNv(KT}Ct%s;#m-|7@ETU5e$AeL)bBhC5HC29qj)G>B=! zY7r_ue38e&P(>O;DG^wnIU&lhT&*St)V_OxT)8$7XdJQ5b?|Bm*ALO2$2SSdu6;IE+4WNm1*DAx(~(%(N@UDiFP1ec~*StK=NIJlXcZEvuF^ZSp;V zSdrykNf%VENU0UZM&>(|c?PtrQO%(RjrmrYc`a@j>hd0qM$svPU66T)LEAgzHNMXE zXwOU_OgchRv^DDmDFiL~NTOV-0z^a->o9pTkkQZx$?J<$AX`r(@scT`@^4FVn-N`7 z!9aNxl17Wt*WEy5gWATuPf&X+Ifax~YG}-Nn2JfV2Eyw*eSuXViqr0o%FFFYpb+uH##hk zOLE-ESF1$uq=X{4AGcFmtmVK(;-%tn4G}%HKnp~5y~Q@*1Gx-!G;mnD6(c~wh z&7yWH-Pi1=C6UDqXui`1ic4CIqcbF=gs472{Cc|YN-7NPDymwx!#N+>T{bL z6$@#C2@`LhO}%54p`R6tChVwlmAmkQ3*Gm=`#sul^W5W}>lQCw9CG?;qzZ$InSPI{7QNL)lkaIhZvsx$7OkJNoDYfQqZI)n7bmGRw z9t<5pB{J_yNrlVQ15cTCCdO%l%xR~c=05%DPt$(;Lk~ODZL-NGZfd%Y!Ixrt!~Wul z1jERT&rx7;Iz!|oHA~L34EYuL{X%(|R(@QlFM-p*m?-sSWpO5{vSt10cY;@l+fnkq zIH6kl%o{E}wYK!w6p7yAa%6n9q*)m!qooVU#k+dIq}1CnlaaQ+@YdjgBmgPqNCc+c zQ9krh>y=D7P0sB*LIg!bkTtBa7U%EEIeIpymX>sBjYiye;L!d)eU}K5AnC00bG!H5 zcVBnl0SCIn4m-?kxz(2aCTV0AQUw!L1flT0e@nltAMejaB;FdEV>nqyq_-=>o?b9S zgcK1Eb_|Ii8H>AYSCkL1RJ~F*s4(^;^;~sA3KvVB{bn{jMUg#4zN7X-$mb-YTk2UC zFbP|(Z}NH=W} zpUB%Gw!=n=JyoB0u7gp}D2tP`$TrAqxBVfl+m<4VG)5z}J*3fw(#>9;XfXkyaZ+M55sZ6X$A3>7pT>wgAS9KStFxc zA$6*>du(J21zU^EFvT& zec=8F+?HEy<<|I2HydrVk(-*DaSuFjzZ)GLp`3+CU4}jOc!xz`ZZ8iy;CHwE&aGUv z(mniP4|fX}ER2v(x0MfYSxaU3ZdG5OFu66(TkN@Z% zT)M;^bL_EnJ#bJT5{y%FCgx30imda_JCE}2|KbKl|Cw(t9VLd@|)q)Y*H2%7B#Iu5v&8;ScGZn{U3^9eU`Y zZkuhkaaUY^g}eBYi-}l4EqT|w-sKKB-~iW~j>SgY2g0Hp5Rm`;&;KNZMLVAP%x97@ z6L|&BKKpFY`GBm0yG54^C zJ98k-a|@+ifPaJ7B17~SzVHQi_dR#JBMv{p&7VKtU3~GyG`7fbc;JBt zy7#~T{cgA2cB46%8L5-kTI--^sHNJ_EZ1l77@Kg0b^ zhRMbIy2e;kYBbJBcA53@N|8y2*IJ!nN^1oESUPBUPXyvbgQtu+vb=$;u0EjY(T{$# zJL1SA+$^l2*-yPh-J3^X3 z&iyU6*rJ~((%VW{)MoJ>`}4u|U-hb2xx4PVi%#(YcSC+eAR<(@CqD6sfzw6eXw^49 zqF&_vyZrLY-5qz_;hy!ZXSpXI{$%%#n;JV)HowWcL`Uy%;D1 z&p`V4-uJ#oMC8O1Po(Q0H0V3d7eIP)j293-a9-lv({6fZ#*L4RyR*;!fqV6z9ZY@47!c8bL?YH8>O?{xN?w)wa6G^g|oR}m6jIrQDJ;OTbHJM2y`6_!+uwD3MoJ0h8pG6Los+!`?(S3s@Se>sWAAUrR8 z;R`AAF94$YD_{9acm4I(6Omw5K%YMVgakKz$2;CZ{YM623L_mGqYt?9Q=j@&_mY>s z#O<`xP9znujvj3FzHK02yYIfcd-~I#;f^}$sL&@I{(^Quy+AwJ7A4)nAZi#l5aAnc zxWT>io$sXadBrPUL0liO>1La`x4reP?uK98KqnQ=-*SF!YcZ_;C7{(|DOKjY1|@Z6 zaZDloAcn)-yMX{$URX;HnvF^UVUf!8OjbZkDrm>>H=EK$DqPuYRCuxfyH_IsIMw9j z1R)N%xGp#)t^uhIKICB1 z!GRnw+(0T=wrsh3_`^3N&ZfsUSb8A^OixjYFG>NnW{rEyV;)1|IogZgeCA?<1G4Jo zkNo0vVUPfzU;oE{{D=GRFa5WB&U2na*T40xZxK?112=SdHXy>AGBe|z{p@E`zt*f- zOGFfe=chmUDfI;ho#-l_^27!4&vR&SBtF(m zww?ZZos#>H#l3K&h3?+_?xmBFaN^Fk+ipwmPft&|nVA|X{h&pj1oTw~X#p_+FeU=Q z&{mX#-;``s8{GuQW<=qGMR9`gcfRu-cg?le&^WN6aL+yWByk(ZVClnLiJp3OV$q_F z+?{vc<-YvoFO$v=bpnJ9<)F{J9(8+|DAQO@Pt&t}0uTBDV<7tU(CXFH*0oa@BS8Sf z7&TPPs%ea4f|JS-C{@umkajH#*;B}>pHFde{j7CO$8xZQHYN|+;IrJcJ192*mVRGf zHhiekE}-=-e%5;c8oIr9@5y?M>_X%gGEzRJAo50t$-+Wo7Z{Gdf;bO=aSkFZM0?Z) zE)6|X8g^&_`MnY>a`8taUWlLsmz$b$lk+C&48Q;LfBp{{bx{V3Ta&OPs3ci#Eu`J>AGsk$@Mu`$=jnScPSnOYMFfHY!D!hrW7Ezo$d0Z?tI zs&YnRH8!R_Dr49)$D#0WQTeP!+|aH>5SA`ef0yfw#xALcT7BS@Qu3XQD~|tJ*Yv%B zxP~~N6p=)nzx?P8HLcQ%Du?R@A}(0CfJAp#@u7P|3V?+f;wz96IxE~#AQEIy4fM+Q z#s5YbTW+xh>HpB%!LiulFWbznG61{=mtA%l>HjbW!V`dUP!GQEu*3Gj?WA2#wJ*Bv zKb{5hLBwY63_=F(i@%U=U`dzywk`|O5bPN!14IDteesK5bl?8=x9OxIi2vk9^lcBX zvn1q$m`*+QRPsOn1H|(41o6Fv?Xw2Ams0V z|N9^oa@!?`q)5Q2x_o#a4nFu`>IaAiEdL+~(ErV~BqonJH0p-o59-ZlKJyv(;L-=l zs0p_v4lo4}ArO)%okck^LVdvdAcPo4^br76;-;H!B6|YH?Ed@i4-y1PVq$(Jp{|(8 z2#G_k3(F|pw&okmujP5k?fH56PL9jB*$^TrAe>WBW+Kvaqik{ucbD;{q1ij zQ5%Ll==4AS@sHgpr<~%p+G*P{r*^#p;rcvX)=sw_hwKvN)1 zVWJ4MAt8zjkp5bsj%Y((;840@Sp8l3S4Q{ND@ZS_N|-TVdf$(sOoAyhE59esitgP4Gqz3gQ~ zoKX)53$BB3h=r3h3RgNTJa^xKa5|7$fTixo!1y{C-7OD8N_yz4xIQc!;_I%v?pk3I z`2jXkl~f#zDFq2I2L)-siF7q#?T@0iGV$M0TMf!D-=$z3)ur6zaL|u$bns=@;<$aj zAqTw3`hyX+*N@?6(6I&pEN(XHa2u&Ce-2!uP9HNM!*oMg@KQ+i$&W4R_I}(zC_ed# zPf|GX7f$(t+kA`737O$?%(=37EQg!y(3NPFfpO!t2zBeW+ivS|AW$U8lg`)kyT4LmjpWKoABn;sqgJM^ z_y8GC9lz!yu#D4)Ba1G#iBoc=yc2S4M9+2fBE(M;>BX)&Mrz)uGvals5zl?2@e*nZ zkaU_{ccG?$pbeLo$hJhje(y_PIm2W?AvY>8CD?PmHz1viqB-Y{=I z*od|ldo?FmghP&OPXMQ;g4Y0gHzMwUWKaPhx4V@$9m|;FggUFu@oA+H@UhO^vL$p#c8lIbl=8h}}Ny=J67Pc2kCT@XzNq3|!Wi zgZOmj+$C;C_N6n@;eG8E;&$nU5b9U;$?KuLTDnnOHpB<`g&EE+uv7(4a9AqaY}3u$ zu8-Q42mtaK{N3OE9l6{n1X|=vkZs^T1}RN(tPrcJKh-y$NHNGreO1lo< zk7$34d=}@|Wy*H+thN;f@>tTbgs@>q?ev^&NVV7E{`f~K6ix>A+t$eq$Pxf_-e5>E zQ~+~Pl~kY&W7_&oy5rg@vZhne z5ss7^8!IAW79z<0NRHZYq3Z8#Yd{b~OM1wO;k`E76~AiOt28opMb4?@ZiHikDsUtj7j9YQwI~;;us3`~x^>35r$w+}UywVoTH>aog(cf4 zT!E=YWer>kx=1~tTo&;&??t+00ulU!eam)>L=(7@KEy)C!7K4t+8)BCcTpzsLiv<6)o8m#MKkqN!0 zjq7VJlcLgcp!5Lhc4^L^1BoE=cxa=cO~YfBVHkRG#t0QH6xFs+f1yu3%*EU` zMj%zMw9LzYvL9OXk92Xp2}%@RIw#>h-?pd>c{r&we)bH5FcaV^J);cvL7&ZW1AhAfZ&aytSc&>NqW_$j$h=)YR*(e=d8 zMB=L$qnvIj7pw7rc}-fTBLXT_4^@X~5CMBxG*M`FO8iY6@*x+uRVI&{6zdyEBcGp% zH&*HXMDx``hb`5nr3p)09nm^B%GB?0-U_w>Ae@@+B8D1~ zfW2O?0#!f=B@=}l^0$d1+KARhIW=gpxFK$e=s4z|8ti)EqZ(ewTERX%3i=wozE zv?G#ar0%Km!b|&z9xE;GTK&}v*R&BB_qbmT&acKVsej7!?uLGCwLNN=+_WY4+Z*qh zNUm))Ds!}Kd*J%U`3|~o)c^+QXr5cfZ_x4wDKGa9dvmaX3`AhTf(1QD{_yP3O7~&` z>37fz*hrwG)U1Js2BkyZ@?oIO7^mA&F9POx;P9~sCJ|nX-f2eLmkb(ut)b@*nv+mV zKwA1pwo^+i3K}Y&l0b4nO*8&q>K&~b_#fd~q>d!!(jtzL$S?Y4jX(Zn)w%(pSJ>t@ zC{^i7!Ak8Xe*>?vj|FXH!#{vlK6C>Z253v?+O;9olqGWL-`W})KM?D$U5SGhttI!7 z?Xg5~dA;8p&l&_Lk!44FrKJRHaet|whV?0eQkY{}DuA*fG<00W8KXM&82Nu%jMiK@ z(&$AH6358PKFG06^;_B*8hw>rRmYA;_6&pLC+n1B$6t-ZWTncM>Wig=OK9V?!Cuv( z8e1UOr1eZnIvJEmuoQssYf3aTIZ4+d0)TzgK18sG!GUMMOcj;+t_=}9IeHS>2A~a+ zIS46V~Jnz zC@1Hti^g{dL{AwJf$Z3$jQV7@p@GwgOS`BD$)tq;Lxsx>b7{&?< z^nkR9{g!2(LV10Hjz03%A8SIi&fZw`nn36#_&!QYI&(bSBMY#|F&7!QG^evtRt1xW zFb+v0ryw3&hBkhM>_I|0)BmFONus5Qx*U7j(yXlA!3db}%t++4q=(QB*+yZYwM6`e zgs6(Nh(b@TZ%b0O!pyB_Q&O%aO2XLI7A^JL0^aKZ+{Opx0lz^I!1Sp;t_)dc zkqiQcKu7?H^F~@FP7NJWG$(wax&!6_Siv!EzWz*^smYoR$W2&yMj|F(mIfge9gJDw5fwwG_p^c7^a9t z4obLd&)AP|sS+t-)>e-t6?J((Rh}*f*Et(B(zqjU0`ekkw9!UA{6<74&QA!|FE;-1 zVGl4ENLzs9;IiUUT_uR*^ux9vSQJs0DJ26*R7w_v7QgktHK=aor3mT8Q11uM&0J9F zx6C>5J8WXHL{ZIswHu0ccgeL4u~CcfDFYxAU<;`w$CbLiR#$YH;W@R)wItV&Skyd3 z{ZNuf>0Dc!vJ1Daq$n91R9ISm~~5!Ru}qT z4fkk6OVrUy< zUA8nJ4kEp30-FNhEExF8mCN0<-vSKxAKqmH;FF*HWH*8&lemcdY;{`ZT693+AOQGq zfig|jKu`_8NGxiXbIrun=c%jKt4W+z?ygb}#O?Ag5pZHkPH$b%;CSJAsemL2XPm?Wwu(x7JW&oHA=obi5G+)K*_L1XI)RTV=HooHnr9YK?{by=wJpcmKWjxs@wc zxQY4m+`LJgsW{RDL#Oi_{j)^AVekX6-aj@rMrr^)unoXs$mzpNgM;{Y8{#(>FT2eW zo`x>eRoB&aKP+yJ*;dG}T}!FHO3_#&KbIXXYJ)4L>=c%&Ajd(MMG{QLcxORM<&gFJ z2jg_x1Gv_-yO7_P{YimYPo!xjLcnJU`2qTUOF~VJM=2gz*DX5>*9#`S;=u>q8ie&D zd)4SDY|6?EMtwN_L4(t zAHM8#qpXamrWb2;EtA|dWy8pErAUkw+oD@_+nh)(bbsk;%#~&K$;gzGQ!64u;*meR zXW0f420>X~I152Af|Ow_g#gWN)t(SqAL1mO6rlm6)}j1uUALasYqJC|a^;rV+wchb z4e5Qw@)h*%*hG!TJ!D+M*_E93&jS$voVSjXZ*b;3LD7O zus@NHwBo}h*_aSbe1V=_suNkL=zA zk~>aK!bTq;KCa<-wDF0F9uAoW5kN9m#&tj-&vZgeRZ%by>;RAifao%p_=@Gr=?Ev- z10Ys#BFQ#U$7*nHfRZAU;TzWFrs^x<)?J{B#4}r<3F07NsRNv>RBiz`-&LY*NnH&O zN~YbG-Nk$^6U*OqT&j$uk^4O;bdd^5fr$9@G)eADeSQyQt;RIt5EZ2Q199LJtmP5x zBeH{t48p;WIEN0=LQ_*yJ#c-p35DGE$Fg1ESF_c|5K{TFc1+v)mPmXs~v;X(;yV0m=bPRdfytqwiA&hjHryrS^GjJ z@TR1#7FcVoO|8(^mMvdSoSzR!!O>rI;Hz^z*zcegz|#l7xt0u#qg1%?OD?&D1`!?t zybS7$10vR}SxXg-jE=g+8*NmdR*REq3Zt{Siqxrh^?>Cj#Nzl8(L*^Ml69uawEEFS zGScnpTwZ1Cy=`jIY^rWn8wb&6Q|)Xsi0OJtp6{s?c%^oNuF!RT>Gi4s*H*4tMK-!M z57kH1Q(va1-A0QSlkpx$zk#t^ZD_PcRtQ4@;s7QlCMviOdS0(v3o0`)PWAvC62XT! zT4m_6+a@Tff6HBIhHFg^n$#(!%T3oSxiM#aEz&WC?ZGNrf^K8Z@6XRk|8lNPeN6|xM5>AbQCWt6TT3#QPv2^* zztP_lArW1ALDaKRy?&RV4owbqS_ZoWf|N)TrChw&*5J>x)ay;IANjYWKb7`sw76QW zHkDp0AzYe3;3tB7?!EgSvd#67v~^-KFbUZ5pbkJ<=e}v=r?OI&B25@5$1i{ROXBqN z=g+SoO7PiYAOL)H6ru-75~%95!?K~e^A_bbH)-eso@`2=M!|5O00X~n&-F;U$+rhBn^R~Y(>~+OUJahhkm|M zcLxuPu47p4-?k10T|YUf48(6-OJsO2c{c?qbwf+R62O3$J@}xzhdBQVvi3vorz9W# z%!b8_7ZdW|cH3?4dCz;EyY<#vlVhGF0C*A91C~R$4WMVhs)&ya3x)*1dybbZd7yR? z!ecN*mXn4!uMnyjRJC0=D~ZMa-!Q=W(_91J)CWHB0g?h%uU_3l1u!tk zlwoMVS`dMI?{yE{|3JtrL?`R24(UNDrRYINHloS&d!^83gOIaj)~Vv+wb86Cf{)A9 zfi~Gba6NKt67Tj3BmxOd{V?sulL%C*tGCr(@@@*+B)ehPMPxtiVApEcBm5_X{*Rs z&IchbRY$A8wKh{@sZZAmM5Mqb!uJuF2d)lP8|8rk1L0-LZ=CK$62j7@y}kC@i|$=^ z*=6qWkAHl&j@C`-l5?MV=9yFhTnmr{qEm%?JvT8qNv?>c4=iza-Fc^5`XJ&5#0DZ` zvxpqj(#g}*)8s&w2%9nTSl2CGJ*86zZYg#%hL5t^^A5UHpY%r>}mr`KcKka)$s=yjx@1!BX^2d)o~ zTR?tD*Vt<@GD@O941joR<*JpvD}R4wcpu!q4X5%JI|0nak3RZnAasvC15gcQqv>?v znTo8Gs~>vEZL;a6ZqrRR31`Z2Ihy)e3qS%(8AE3d z-+$lzWaz_OOit7yza&V!Wq+$Jws1=xe9+x{-+k^0Pj~`t^hvq=hLQj@5xC%j3-G32PMXy=A z#x0mP-z{FW5%%Wv?z-a+_k=?arE< zpQKYqy@3L|%P7lmZ-jCQEV=)Fw`%1|w`lQ3Bn?bX&a3qqGGt(2d^DQkf+^~qQvv3Z z%M1!pC~rp&-UU)+4^yU9#=020*=TbLdIjF3Hc2lgeoWjmo&=@q<3w&SMu_`Bd>GPk zZ(`nLE%K8(hCGsWUHA?o{JeSd-Gj@Q_3pd-Zg;Rpet?YmFB$_k0DuUW#rxi_Xo}Z7r^xifBb336|jG%t{V(>(ZWT&rOTJPJMO*P9eK!; zg2kVA`*neVSM|H!{ccJt42lDz1HuhA+ibH64F?ej$wKg#H=dyLy~<6ksm~uI{bqcU zJPPn(V(&7<9f(ELajycDh`%{eEjLN21>1%c>q>GcMT|Bm5reyyUD~Fc1UDPDn%R~T zszTK&^}MB$0$?`-;=^VJwHP1d?Vp~RrhpzGx0DX}JL3}WgZsl12jk!qpZG+=z2lER ze&F>u^eRRJAPL|vvWzTRw5S3ygQ_8dkfQ=A_(2$$hzd?m*JqadK!(ZrlWyUHg+VF^ zR!ATa2{wcfXg&oBh0>jJjkPi0!3ElEV%08-D(cBgC!xq9@q zK_sglmqj+5_`7bCwtZl|VvPeAD7WT{I`J;-WsvK~#>Poi=0jSswmHfcfcJqLV3L&P zd)2BQ)CDl!*-&9S*#Im+xG*;aL}2&bckhAHZM^Zu6fi`&1pEe~-gJF5_Y6l6LjU&^ zjt(MOzQQeEww!#P6O$8e!TbdjdJJL!>W4DOXi_U#(fX9e75**1r>L^NA3e)u3^2&i zg5|ar5CMH%{;s7W`qtRaYt|iHi)HPU0Y}Dz^u%M`8Ke+jB-#W zY-?bM_uY3NrS!h@&O6<+pZ#o-z-I$yMUK@I{Ij3^%x$&RR`eSNLWCwRU$%_&7}E8z z$cU3W+Q!Jp%OVnW9%s_SuQr$+Un%vP(~m=eI%$7ZGfSQ zXmmZ%{oNF7$qodk*{+BHWO5?v@ei4!DKjpQr$Gp0xjow-V@hir1qmtCN4FyD4%RBK zTYL!l*RG-DoUnOeEC9^a!v};1W9|j5QL>rUY61Yv6O#1u<;#(umn7-~4mcpR58=I# zyoZHZRb;X80MSQi@TsSs%60+#uI5cnx@F6j)1XXGPt}78ge649NwP9ht|Ad>dTN>y zjW9LDBE+i09s^2#FbaZDdl4iN07VTQqci(r%B`1$UUXz}RqycvY%DyBvxs`=U5~Vt z&$$;CGjUTo>ZiWW0t2^EHaHxp+Kl0p7HK8`vOms3R?!zkm7x;e$j#>n)jQzo?>&lM z>^F-3Pji?iLhKX6A3_9A09YET$xcmN`0<$2@?VS{jAApyFqnw&r*1E?b6<`}E|nH6 z)Lr{x9a))1>RM>d1Ceb;dyZ6&5T4Y~YMQ>IPx$MEJ6jSrdT(^JpFcA&rmALueqO^o z_|tp9^*uo$asRHn?jo*_=a8=N(n~LO?|tuk-E6?DTe2XUy!gd0cDwDi8#Ndn19%7^ z#()q&C-9)L3Gy6++1Z4dy-1^p9f zU%j|v(Vvl05&&(i$r1o8wy3`0OYdF1JDN~d@FxBGUL@O*f3q%L0} zsw5PBPV05hD0vBU?KlH8^_hr-X((t6I&(t06dg;>1fgGlBz|*|sE@}>^nv0-#LO~b zA|@gU0OyiX+FeC-!|U^Ed>Cjv=#(hGX2QVL>jg+odoTR4ZAhEX?FdnwlF?E2sagu= zwao@TKem*|52`Q>?VO(jTl8mx>(40xV9=Zr8;KS|tXaG<-GvJTsl{lLFi?IVG&1SH z8+kGjdkJ)SF$h3v|F&vmAOLL~-7<>hxW9(LF~z%mDTT3wxm}C^f)Dx&H5ZJ#V%tAL zT^XaJE~mt(YP{Hwb7$g`QL|Fz<*qX^rFsG)GuN*%G)OM84m#zzMqEe`je4qP>|>+< zf~)r*YKoJzUOlo$(G(m zE#bCe{g>7U>nfsM(5Xp8WFx7NfKrAq`8{Wq*Y`t<6GPRu0OcHLWX`q0x+jT4RIjpc zG+$1;0`2GRYgOuc!wBkWh;Lh+C^rs8=I28!4Bs0ab@L}C$g_o?Fzmsy&jv(z)J{C{ zM0%F3{Bw!?RK8nBU@fMz&pz92z4g{a2`Q_|)D*`6R3Hu@2>6SSJcNYHi!A?)Bngg5 z_n5nS-OYTaWxzFIs_X6(fS`s*DaPxOAQZLQ;sr`|3Xdi(p)G=ISVS*HEcMvy@vrsp zvPF<8SJ$6{98u7cf!_CFNeHst;hxvQxxB`{jrw7G9I7TElLVkvD`P7X9)&@hiM4Di ze}}h<+}M+oNZZ|eV965lXo1NgE&u^N%<=U&%{B7Rxdb4A2L?t#aD4U_Ftt1K$Rk6# zu{CSf^q4DP7vbpWXocSpkvJJ?l)?$$IbupICVi4|5D=76s!auukn;q{XTl~YFUG>c zj!jaL3NjNf_DD(~J3#+8Tgo*uSZa}_F12xGE!Cv#27>~tcb3#*S>5F|PI?hbFEIh4 zYe-LD;rM!CEbzc-?5w(-Ky*{;;rT>{EBNt)H>2l9$H(1>ZyW3KoI4YnP3tMIbPq{`p|P;7$+nQ%(zWHil>Q$KSHd0MzhYEMP!}E%s3fIzRP{m>W)=*>V2y%DZ(JzWc6RyQxh5AF;#ccW-=p2fbB1O zPQ8RdPGavlpm+L#Y`qZd<9GLQKoA|SyJQI=I9u_VApZK-zjn9ZetYLlJb!c&fCfDo z_-hdZNC$@;atPTB$WMyEAfn_WJ#3dmOaL|(@rlKLu~fW(<>HYG+q%Z@%lkSkQ!XAE ze_$<8edH9~_X0|9wB8%R@5*%Qb-owOPBW*XY>Ty^DFaiji`0ni{pM|8+AGvqgkj8B z~*>}SpZsK;LuG3z-k#&cU!#5mkjT-{o4J)So zUnd8#0qOAof479qS9$n*@*o&Svt+M?R7$Cj6k*qQY|!wUC(zl$J#> z4jSSEO!>D&GA0uIosEtXp&2-xnO-=!&Wn{rRECfkMI5g#>a7!|TIbP$*!Z9nddHW; z`QPMOj2cPk!xF^wbolNPwiAh55_JE0Orcs$`TmY#XQ%LAOJb|*VgPdJzRMG)^&GRv zF8jx2@_0xj=I`}fpe`V2{aX@el*^mVW|*MY;!I0F?Z+P${y!0sXlPyl%$5KE002ovPDHLkV1nDpMxFow literal 33772 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 diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index 60608d0f..debae415 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -286,3 +286,9 @@ void Win32Window::UpdateTheme(HWND const window) { &enable_dark_mode, sizeof(enable_dark_mode)); } } + +void Win32Window::SetDarkMode(HWND const window, bool dark_mode) { + BOOL enable_dark_mode = dark_mode ? TRUE : FALSE; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h index e901dde6..34dc4ceb 100644 --- a/windows/runner/win32_window.h +++ b/windows/runner/win32_window.h @@ -55,6 +55,9 @@ class Win32Window { // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); + // Set the window frame's dark mode explicitly (called from Flutter). + static void SetDarkMode(HWND const window, bool dark_mode); + protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that