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 00000000..2d89b837 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_shortcut.png differ 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 00000000..e1cf72ef Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_shortcut.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_shortcut.png new file mode 100644 index 00000000..bc58a58e Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_shortcut.png differ 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 00000000..e8bf90f3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_shortcut.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_shortcut.png new file mode 100644 index 00000000..422c68f1 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_shortcut.png differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 1fe0d211..7102929b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ 桌面快捷专注计时 当前节气与对应诗词 连续签到天数和快捷签到 + 最新笔记内容预览 闲言数据管理 主题个性化 打开主题个性化设置 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 c04e20ca..e8740d1f 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ 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