From dadd132b4f30b67a9640561bf8429fe44817bfbf Mon Sep 17 00:00:00 2001 From: lincube Date: Tue, 17 Mar 2026 01:01:48 +0800 Subject: [PATCH] 0.6.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化了文本框焦点,优化了更新体验,优化了遥测,披露了收集的数据。 --- LanMountainDesktop/App.axaml.cs | 9 + .../Assets/Documents/Privacy.md | 326 ++++++++++++++++++ LanMountainDesktop/LanMountainDesktop.csproj | 1 + LanMountainDesktop/Localization/en-US.json | 5 + LanMountainDesktop/Localization/zh-CN.json | 5 + .../Models/AppSettingsSnapshot.cs | 4 +- .../Services/CrashReportService.cs | 84 ++++- .../Services/Settings/SettingsContracts.cs | 1 - .../Settings/SettingsDomainServices.cs | 4 +- .../Services/UpdateWorkflowService.cs | 15 +- .../ViewModels/PrivacyPolicyViewModel.cs | 92 +++++ .../PrivacySettingsPageViewModel.cs | 27 ++ .../ViewModels/SettingsViewModels.cs | 19 - .../Views/MainWindow.SettingsHardCut.Stubs.cs | 4 +- .../PluginMarketSettingsPage.axaml | 7 +- .../SettingsPages/PrivacyPolicyDrawer.axaml | 55 +++ .../PrivacyPolicyDrawer.axaml.cs | 17 + .../SettingsPages/PrivacySettingsPage.axaml | 29 +- .../PrivacySettingsPage.axaml.cs | 8 + .../SettingsPages/UpdateSettingsPage.axaml | 34 +- .../SettingsPages/WeatherSettingsPage.axaml | 28 +- 21 files changed, 704 insertions(+), 70 deletions(-) create mode 100644 LanMountainDesktop/Assets/Documents/Privacy.md create mode 100644 LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs create mode 100644 LanMountainDesktop/Views/SettingsPages/PrivacyPolicyDrawer.axaml create mode 100644 LanMountainDesktop/Views/SettingsPages/PrivacyPolicyDrawer.axaml.cs diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 03082ac..779ea45 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -67,6 +67,15 @@ public partial class App : Application internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle => (Current as App)?._hostApplicationLifecycle; + // 隐私政策查看事件 + public static event Action? CurrentPrivacyPolicyViewRequested; + + // 触发隐私政策查看事件的方法 + public static void RaisePrivacyPolicyViewRequested() + { + CurrentPrivacyPolicyViewRequested?.Invoke(); + } + public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService; public ISettingsFacadeService SettingsFacade => _settingsFacade; public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle; diff --git a/LanMountainDesktop/Assets/Documents/Privacy.md b/LanMountainDesktop/Assets/Documents/Privacy.md new file mode 100644 index 0000000..8d0d827 --- /dev/null +++ b/LanMountainDesktop/Assets/Documents/Privacy.md @@ -0,0 +1,326 @@ +# LanMountainDesktop 隐私政策 + +**最后更新日期:2024年** + +--- + +## 引言 + +欢迎使用 LanMountainDesktop!我们非常重视您的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的数据。 + +**请在使用本应用前仔细阅读本隐私政策。使用本应用即表示您同意本政策的条款。** + +--- + +## 1. 数据收集范围 + +### 1.1 我们收集的数据 + +当您启用匿名数据收集功能时,我们会收集以下数据: + +#### 匿名崩溃数据 +- **崩溃报告**:应用崩溃时的错误日志和堆栈跟踪 +- **设备信息**:操作系统版本、设备型号、架构(x64/x86) +- **应用版本**:当前使用的应用版本号 +- **设备标识符**:匿名生成的唯一设备ID(不包含个人信息) + +#### 匿名使用数据 +- **应用启动和关闭事件**:记录应用何时启动和关闭 +- **功能使用统计**:哪些功能被使用、使用频率 +- **设置变更**:用户更改了哪些设置(不包含具体设置值) +- **界面交互**:点击了哪些按钮、访问了哪些页面 +- **设备信息**:操作系统、应用版本、设备类型 + +### 1.2 始终收集的基础数据 + +**重要说明:** 为了统计应用的用户数量和日活跃用户,即使您关闭了匿名数据收集开关,我们仍会收集以下基础数据: + +- ✅ **应用启动事件**:用于统计日活跃用户 +- ✅ **设备标识符**:用于区分不同用户(不包含个人信息) +- ✅ **应用版本**:用于统计版本分布 + +**这些基础数据不包含任何个人身份信息,仅用于统计用户数量和应用使用情况。** + +### 1.3 我们不收集的数据 + +我们**明确承诺不收集**以下数据: + +- ❌ 个人身份信息(姓名、邮箱、电话等) +- ❌ 真实姓名或用户名 +- ❌ 地理位置信息(精确位置) +- ❌ 文件内容或文档数据 +- ❌ 密码或凭据信息 +- ❌ 网络浏览历史 +- ❌ 联系人信息 +- ❌ 照片、视频或音频文件 + +--- + +## 2. 数据收集目的 + +我们收集数据的目的如下: + +### 2.1 基础数据用途(始终收集) +- **统计用户数量**:了解应用的用户规模 +- **统计日活跃用户**:了解应用的活跃程度 +- **版本分布统计**:了解用户使用的版本情况 + +### 2.2 崩溃数据用途 +- **提高应用稳定性**:识别和修复崩溃问题 +- **优化性能**:分析性能瓶颈 +- **改进用户体验**:了解应用在不同设备上的表现 + +### 2.3 使用数据用途 +- **功能优化**:了解哪些功能最受欢迎,优先改进 +- **用户体验改进**:优化界面设计和交互流程 +- **统计分析**:了解用户规模和使用趋势 +- **产品决策**:基于数据做出产品发展方向决策 + +--- + +## 3. 数据存储和处理 + +### 3.1 数据存储位置 + +我们使用以下第三方服务存储和处理数据: + +#### Sentry(崩溃报告) +- **用途**:崩溃数据收集和分析 +- **位置**:美国 +- **官网**:https://sentry.io +- **隐私政策**:https://sentry.io/privacy/ + +#### PostHog(使用分析) +- **用途**:用户行为分析和统计 +- **位置**:美国 +- **官网**:https://posthog.com +- **隐私政策**:https://posthog.com/privacy + +### 3.2 数据保留期限 + +- **崩溃数据**:保留90天后自动删除 +- **使用数据**:保留12个月后自动删除 +- **设备标识符**:永久保留(用于统计日活用户) + +### 3.3 数据安全措施 + +我们采取以下安全措施保护您的数据: + +- ✅ 数据传输使用HTTPS加密 +- ✅ 数据存储使用加密技术 +- ✅ 访问权限严格控制 +- ✅ 定期安全审计 + +--- + +## 4. 数据共享 + +### 4.1 我们不会出售您的数据 + +我们**明确承诺**: +- ❌ 不会出售您的个人数据 +- ❌ 不会将您的数据用于广告目的 +- ❌ 不会与第三方共享可识别个人的数据 + +### 4.2 必要的共享 + +我们仅在以下情况下共享数据: + +- **服务提供商**:与Sentry和PostHog共享数据以提供服务 +- **法律要求**:在法律要求或政府机构合法要求时 + +--- + +## 5. 您的权利 + +### 5.1 选择权 + +您完全控制详细数据收集: + +- **匿名崩溃数据**:可在设置中开启或关闭 +- **匿名使用数据**:可在设置中开启或关闭 +- **基础数据**:始终收集(用于统计用户数量) + +**注意:** 即使关闭所有开关,我们仍会收集基础数据(应用启动事件和设备标识符)以统计用户数量。 + +### 5.2 数据访问权 + +您可以: +- 查看我们收集的数据类型 +- 了解数据的使用目的 +- 了解数据的存储位置 + +### 5.3 数据删除权 + +您可以: +- 随时关闭详细数据收集功能 +- 删除本地存储的设备标识符 +- 联系我们删除已收集的数据 + +--- + +## 6. 设备标识符 + +### 6.1 什么是设备标识符? + +设备标识符是一个随机生成的唯一字符串,用于: +- 统计日活用户数量 +- 区分不同设备 +- 分析用户使用趋势 + +### 6.2 设备标识符的特点 + +- **匿名性**:不包含任何个人信息 +- **随机性**:通过SHA256算法生成 +- **唯一性**:每个设备有唯一标识符 +- **持久性**:即使刷新设备ID,仍能关联到同一用户 +- **可重置**:您可以在设置中刷新设备标识符 + +### 6.3 设备标识符刷新 + +当您刷新设备标识符时: +- 生成新的设备ID +- **持久用户ID保持不变**,确保仍能关联到同一用户 +- 统计数据不会丢失 + +### 6.4 设备标识符示例 + +``` +a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 +``` + +--- + +## 7. 儿童隐私保护 + +本应用不面向13岁以下儿童。我们不会故意收集儿童的个人信息。如果您发现我们无意中收集了儿童的数据,请联系我们,我们将立即删除相关数据。 + +--- + +## 8. 国际数据传输 + +由于我们的服务提供商位于美国,您的数据可能会被传输到美国。我们确保: + +- 数据传输符合相关法律法规 +- 服务提供商遵守GDPR等隐私法规 +- 采取适当的安全措施保护数据 + +--- + +## 9. 隐私政策更新 + +我们可能会不时更新本隐私政策。更新时,我们将: + +- 在本页面更新"最后更新日期" +- 在应用内通知您重大变更 +- 继续使用应用即表示您同意更新后的政策 + +--- + +## 10. 联系我们 + +如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们: + +- **GitHub Issues**:https://github.com/wwiinnddyy/LanMountainDesktop/issues +- **电子邮件**:[您的邮箱地址] + +--- + +## 11. 法律依据 + +### 11.1 GDPR合规 + +如果您位于欧洲经济区(EEA),我们的数据处理基于: + +- **同意**:您明确同意数据收集 +- **合法利益**:改进应用性能和用户体验 + +### 11.2 CCPA合规 + +如果您是加州居民,您有权: + +- 知道我们收集了哪些数据 +- 要求删除您的数据 +- 选择退出数据销售(我们不销售数据) + +--- + +## 12. 第三方链接 + +本应用可能包含第三方网站链接。我们不对这些网站的隐私政策负责。请阅读这些网站的隐私政策。 + +--- + +## 13. 数据收集示例 + +### 13.1 崩溃报告示例 + +```json +{ + "event_id": "abc123", + "timestamp": "2024-01-01T12:00:00Z", + "device_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "app_version": "1.0.0", + "os_name": "Windows", + "os_version": "10.0.19041", + "error_message": "NullReferenceException", + "stack_trace": "..." +} +``` + +### 13.2 使用数据示例 + +```json +{ + "event": "app_online", + "timestamp": "2024-01-01T12:00:00Z", + "distinct_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "properties": { + "app_version": "1.0.0", + "os_name": "Windows", + "event_type": "app_start", + "analytics_enabled": true + } +} +``` + +### 13.3 基础数据示例(始终收集) + +```json +{ + "event": "$pageview", + "timestamp": "2024-01-01T12:00:00Z", + "distinct_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "properties": { + "$current_url": "app://main", + "$title": "LanMountainDesktop" + } +} +``` + +--- + +## 14. 您的同意 + +使用本应用即表示您: + +- ✅ 已阅读并理解本隐私政策 +- ✅ 同意我们按照本政策收集和使用数据 +- ✅ 了解您可以随时撤回同意(详细数据收集) +- ✅ 了解基础数据将始终收集以统计用户数量 + +--- + +## 15. 免责声明 + +本隐私政策仅适用于 LanMountainDesktop 应用。我们不对以下情况负责: + +- 第三方服务的隐私政策 +- 您自行分享的数据 +- 不可抗力导致的数据泄露 + +--- + +**感谢您信任 LanMountainDesktop!** + +我们承诺保护您的隐私,并持续改进我们的隐私保护措施。 diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 006549f..3b616f8 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -21,6 +21,7 @@ + diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index bbd595c..b5302e7 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -99,6 +99,11 @@ "settings.privacy.crash_upload_description": "Help us improve application stability.", "settings.privacy.usage_upload_title": "Anonymous usage data uploads", "settings.privacy.usage_upload_description": "Help us improve application features.", + "settings.privacy.device_id_title": "Device ID", + "settings.privacy.device_id_description": "Unique identifier for this device. Click refresh to regenerate.", + "settings.privacy.refresh_device_id": "Refresh", + "settings.privacy.policy_hint_prefix": "For more details, please ", + "settings.privacy.view_policy": "view our privacy policy", "settings.weather.title": "Weather", "settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.", "settings.weather.location_source_header": "Location Source", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index f866438..d849bd6 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -98,6 +98,11 @@ "settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。", "settings.privacy.usage_upload_title": "匿名上传使用数据", "settings.privacy.usage_upload_description": "帮助我们改善应用功能。", + "settings.privacy.device_id_title": "设备标识符", + "settings.privacy.device_id_description": "此设备的唯一标识符。点击刷新以重新生成。", + "settings.privacy.refresh_device_id": "刷新", + "settings.privacy.policy_hint_prefix": "了解更多详情,请", + "settings.privacy.view_policy": "查看我们的隐私政策", "settings.weather.title": "天气", "settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。", "settings.weather.location_source_header": "位置来源", diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index 5be34b1..3880637 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -62,8 +62,6 @@ public sealed class AppSettingsSnapshot public string AppRenderMode { get; set; } = "Default"; - public bool AutoCheckUpdates { get; set; } = true; - public bool IncludePrereleaseUpdates { get; set; } public bool UploadAnonymousCrashData { get; set; } @@ -72,6 +70,8 @@ public sealed class AppSettingsSnapshot public string? DeviceId { get; set; } + public string? PersistentUserId { get; set; } + public string UpdateChannel { get; set; } = "stable"; public string UpdateMode { get; set; } = "download_then_confirm"; diff --git a/LanMountainDesktop/Services/CrashReportService.cs b/LanMountainDesktop/Services/CrashReportService.cs index 83a2880..b26ed15 100644 --- a/LanMountainDesktop/Services/CrashReportService.cs +++ b/LanMountainDesktop/Services/CrashReportService.cs @@ -15,6 +15,7 @@ public sealed class DeviceIdService { private static DeviceIdService? _instance; private string? _deviceId; + private string? _persistentUserId; // 持久化的用户ID,用于关联设备 private readonly ISettingsFacadeService _settingsFacade; private bool _isInitialized; @@ -43,6 +44,19 @@ public sealed class DeviceIdService } } + // 持久化的用户ID,用于跨设备关联用户 + public string PersistentUserId + { + get + { + if (_persistentUserId is null) + { + throw new InvalidOperationException("PersistentUserId not initialized"); + } + return _persistentUserId; + } + } + private void EnsureDeviceId() { if (_isInitialized) @@ -56,13 +70,22 @@ public sealed class DeviceIdService { var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App); + // 初始化或生成持久化用户ID(只生成一次,永不改变) + if (string.IsNullOrEmpty(snapshot.PersistentUserId)) + { + snapshot.PersistentUserId = GeneratePersistentUserId(); + AppLogger.Info("DeviceId", $"Generated new persistent user ID: {snapshot.PersistentUserId}"); + } + _persistentUserId = snapshot.PersistentUserId; + + // 初始化或生成设备ID(可以刷新) if (string.IsNullOrEmpty(snapshot.DeviceId)) { snapshot.DeviceId = GenerateDeviceId(); _settingsFacade.Settings.SaveSnapshot( SettingsScope.App, snapshot, - changedKeys: [nameof(AppSettingsSnapshot.DeviceId)]); + changedKeys: [nameof(AppSettingsSnapshot.DeviceId), nameof(AppSettingsSnapshot.PersistentUserId)]); _deviceId = snapshot.DeviceId; AppLogger.Info("DeviceId", $"Generated new device ID: {_deviceId}"); } @@ -75,6 +98,7 @@ public sealed class DeviceIdService catch (Exception ex) { _deviceId = GenerateDeviceId(); + _persistentUserId = GeneratePersistentUserId(); AppLogger.Warn("DeviceId", $"Failed to persist device ID, using generated ID: {_deviceId}", ex); } } @@ -87,6 +111,15 @@ public sealed class DeviceIdService var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(deviceInfo)); return Convert.ToHexString(hash)[..32].ToLower(); } + + private static string GeneratePersistentUserId() + { + // 生成一个永久性的用户ID,基于机器名和用户名的哈希 + var userInfo = $"{Environment.MachineName}|{Environment.UserName}|LanMountainDesktop"; + using var sha = System.Security.Cryptography.SHA256.Create(); + var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(userInfo)); + return Convert.ToHexString(hash)[..32].ToLower(); + } } public sealed class UserBehaviorAnalyticsService : IDisposable @@ -140,9 +173,18 @@ public sealed class UserBehaviorAnalyticsService : IDisposable TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)); + // 发送PostHog标准的$pageview事件用于统计日活(始终发送,不受开关影响) + CaptureEvent("$pageview", new Dictionary + { + { "$current_url", "app://main" }, + { "$title", "LanMountainDesktop" } + }); + + // 发送应用启动事件(始终发送,用于统计用户数量) CaptureEvent("app_online", new Dictionary { - { "event_type", "app_start" } + { "event_type", "app_start" }, + { "analytics_enabled", _isEnabled } }); AppLogger.Info("UserBehaviorAnalytics", $"Analytics initialized. DeviceId={_deviceIdService.DeviceId}, Enabled={_isEnabled}"); @@ -382,10 +424,22 @@ public sealed class UserBehaviorAnalyticsService : IDisposable try { + // 基础事件($pageview, app_online, app_shutdown等)始终发送,用于统计用户数量 + bool isBasicEvent = eventName.StartsWith("$") || + eventName == "app_online" || + eventName == "app_shutdown" || + eventName == "$identify"; + + // 非基础事件只有在启用时才发送 + if (!isBasicEvent && !_isEnabled) + { + return; + } + var eventData = new UserBehaviorEvent { Event = eventName, - DistinctId = _deviceIdService.DeviceId, + DistinctId = _deviceIdService.PersistentUserId, // 使用持久化用户ID Timestamp = DateTimeOffset.UtcNow, Properties = properties ?? new Dictionary(), IncludeDetailedData = _isEnabled @@ -542,21 +596,29 @@ public sealed class UserBehaviorAnalyticsService : IDisposable { var userProperties = new Dictionary { - { "$device_id", distinctId }, { "$app_version", GetAppVersion() }, { "$os", GetOsName() }, - { "$os_version", GetOsVersion() } + { "$os_version", GetOsVersion() }, + { "$device_type", GetDeviceModel() }, + { "$device_id", _deviceIdService.DeviceId } // 当前设备ID }; + // PostHog正确的$identify格式 + // 使用PersistentUserId作为distinct_id,确保设备ID刷新后仍能关联到同一用户 var requestBody = new Dictionary { { "api_key", PostHogApiKey }, { "event", "$identify" }, + { "distinct_id", _deviceIdService.PersistentUserId }, // 使用持久化用户ID { "timestamp", DateTimeOffset.UtcNow.ToString("o") }, { "properties", new Dictionary { - { "distinct_id", distinctId }, - { "$set", userProperties } + { "$set", userProperties }, + { "$set_once", new Dictionary + { + { "first_app_open", DateTimeOffset.UtcNow.ToString("o") } + } + } } } }; @@ -796,6 +858,9 @@ public sealed class CrashReportService }); ConfigureCrashReportingScope(); + + // 显式开始会话跟踪 + SentrySdk.StartSession(); AppLogger.Info("CrashReport", $"Sentry crash reporting initialized. DeviceId={_deviceIdService.DeviceId}"); @@ -849,7 +914,10 @@ public sealed class CrashReportService { if (_isEnabled && _isInitialized) { - AppLogger.Info("CrashReport", $"Shutdown event will be sent via Sentry. DeviceId={_deviceIdService.DeviceId}"); + // 结束Sentry会话 + SentrySdk.EndSession(); + SentrySdk.Flush(TimeSpan.FromSeconds(3)); + AppLogger.Info("CrashReport", $"Shutdown event sent via Sentry. DeviceId={_deviceIdService.DeviceId}"); return; } diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index cab1afd..1dabba4 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -47,7 +47,6 @@ public sealed record PrivacySettingsState( bool UploadAnonymousCrashData, bool UploadAnonymousUsageData); public sealed record UpdateSettingsState( - bool AutoCheckUpdates, bool IncludePrereleaseUpdates, string UpdateChannel, string UpdateMode, diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index d23f42f..beca663 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -628,7 +628,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl snapshot.UpdateChannel, snapshot.IncludePrereleaseUpdates); return new UpdateSettingsState( - snapshot.AutoCheckUpdates, string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase), normalizedChannel, UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode), @@ -646,7 +645,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl var normalizedChannel = UpdateSettingsValues.NormalizeChannel( state.UpdateChannel, state.IncludePrereleaseUpdates); - snapshot.AutoCheckUpdates = state.AutoCheckUpdates; snapshot.IncludePrereleaseUpdates = string.Equals( normalizedChannel, UpdateSettingsValues.ChannelPreview, @@ -672,7 +670,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl snapshot, changedKeys: [ - nameof(AppSettingsSnapshot.AutoCheckUpdates), + nameof(AppSettingsSnapshot.IncludePrereleaseUpdates), nameof(AppSettingsSnapshot.IncludePrereleaseUpdates), nameof(AppSettingsSnapshot.UpdateChannel), nameof(AppSettingsSnapshot.UpdateMode), diff --git a/LanMountainDesktop/Services/UpdateWorkflowService.cs b/LanMountainDesktop/Services/UpdateWorkflowService.cs index 091bfea..3babbeb 100644 --- a/LanMountainDesktop/Services/UpdateWorkflowService.cs +++ b/LanMountainDesktop/Services/UpdateWorkflowService.cs @@ -131,13 +131,10 @@ public sealed class UpdateWorkflowService CancellationToken cancellationToken = default) { var state = _settingsFacade.Update.Get(); - if (!state.AutoCheckUpdates) - { - return; - } try { + // Always check for updates on startup (removed AutoCheckUpdates check) var result = await CheckForUpdatesAsync(currentVersion, cancellationToken); if (!result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null) { @@ -145,12 +142,14 @@ public sealed class UpdateWorkflowService } var normalizedMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode); - if (string.Equals(normalizedMode, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase)) + + // For "Silent Download" and "Silent Install" modes, automatically download the update + if (string.Equals(normalizedMode, UpdateSettingsValues.ModeDownloadThenConfirm, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedMode, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase)) { - return; + await DownloadReleaseAsync(result, cancellationToken: cancellationToken); } - - await DownloadReleaseAsync(result, cancellationToken: cancellationToken); + // For "Manual" mode, just check but don't download } catch (OperationCanceledException) { diff --git a/LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs b/LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs new file mode 100644 index 0000000..767efd4 --- /dev/null +++ b/LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.ViewModels; + +public sealed partial class PrivacyPolicyViewModel : ViewModelBase +{ + private readonly LocalizationService _localizationService = new(); + private readonly string _languageCode; + + [ObservableProperty] + private string _title = string.Empty; + + [ObservableProperty] + private string _description = string.Empty; + + [ObservableProperty] + private string _loadingText = string.Empty; + + [ObservableProperty] + private string _errorText = string.Empty; + + [ObservableProperty] + private string _markdownContent = string.Empty; + + [ObservableProperty] + private bool _isLoading = true; + + [ObservableProperty] + private bool _hasError; + + [ObservableProperty] + private bool _hasContent; + + public PrivacyPolicyViewModel() + { + _languageCode = "zh-CN"; + RefreshLocalizedText(); + LoadPrivacyPolicy(); + } + + private void RefreshLocalizedText() + { + Title = L("settings.privacy.policy_title", "Privacy Policy"); + Description = L("settings.privacy.policy_description", "Learn how we collect, use, and protect your data."); + LoadingText = L("settings.privacy.policy_loading", "Loading privacy policy..."); + } + + private async void LoadPrivacyPolicy() + { + try + { + IsLoading = true; + HasError = false; + HasContent = false; + + // 从嵌入资源加载隐私政策Markdown文件 + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "LanMountainDesktop.Assets.Documents.Privacy.md"; + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream is null) + { + throw new FileNotFoundException($"Privacy policy resource not found: {resourceName}"); + } + + using var reader = new StreamReader(stream); + var markdown = await reader.ReadToEndAsync(); + + MarkdownContent = markdown; + IsLoading = false; + HasContent = true; + + AppLogger.Info("PrivacyPolicy", "Privacy policy loaded successfully."); + } + catch (Exception ex) + { + AppLogger.Warn("PrivacyPolicy", "Failed to load privacy policy.", ex); + IsLoading = false; + HasError = true; + ErrorText = L("settings.privacy.policy_error", "Failed to load privacy policy. Please try again later."); + } + } + + private string L(string key, string fallback) + => _localizationService.GetString(_languageCode, key, fallback); +} diff --git a/LanMountainDesktop/ViewModels/PrivacySettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/PrivacySettingsPageViewModel.cs index 29391ef..039d501 100644 --- a/LanMountainDesktop/ViewModels/PrivacySettingsPageViewModel.cs +++ b/LanMountainDesktop/ViewModels/PrivacySettingsPageViewModel.cs @@ -15,6 +15,8 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase private readonly string _languageCode; private bool _isInitializing; + public event Action? ViewPrivacyPolicyRequested; + public PrivacySettingsPageViewModel(ISettingsFacadeService settingsFacade) { _settingsFacade = settingsFacade; @@ -59,6 +61,12 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase [ObservableProperty] private string _refreshDeviceIdText = string.Empty; + [ObservableProperty] + private string _viewPrivacyPolicyText = string.Empty; + + [ObservableProperty] + private string _privacyPolicyHintPrefix = string.Empty; + public void Load() { var state = _settingsFacade.Privacy.Get(); @@ -130,6 +138,25 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase DeviceIdHeader = L("settings.privacy.device_id_title", "Device ID"); DeviceIdDescription = L("settings.privacy.device_id_description", "Unique identifier for this device. Click refresh to regenerate."); RefreshDeviceIdText = L("settings.privacy.refresh_device_id", "Refresh"); + PrivacyPolicyHintPrefix = L("settings.privacy.policy_hint_prefix", "For more details, please "); + ViewPrivacyPolicyText = L("settings.privacy.view_policy", "view our privacy policy"); + } + + [RelayCommand] + private void ViewPrivacyPolicy() + { + try + { + // 触发隐私政策查看事件 + AppLogger.Info("PrivacySettings", "User requested to view privacy policy."); + + // 发送事件通知显示隐私政策 + ViewPrivacyPolicyRequested?.Invoke(); + } + catch (Exception ex) + { + AppLogger.Warn("PrivacySettings", "Failed to view privacy policy.", ex); + } } private string L(string key, string fallback) diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs index 7e065f2..0ac8640 100644 --- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs +++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs @@ -1329,9 +1329,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase LoadStateFromSettings(); } - [ObservableProperty] - private bool _autoCheckUpdates; - [ObservableProperty] private string _selectedUpdateChannelValue = UpdateSettingsValues.ChannelStable; @@ -1380,9 +1377,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase [ObservableProperty] private string _preferencesDescription = string.Empty; - [ObservableProperty] - private string _autoCheckUpdatesLabel = string.Empty; - [ObservableProperty] private string _updateChannelLabel = string.Empty; @@ -1520,16 +1514,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase private bool IsBusy => IsCheckingForUpdates || IsDownloading; - partial void OnAutoCheckUpdatesChanged(bool value) - { - if (_isInitializing) - { - return; - } - - SaveUpdateSettings(); - } - partial void OnSelectedUpdateChannelOptionChanged(SelectionOption? value) { if (value is not null && @@ -1729,7 +1713,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase var current = _settingsFacade.Update.Get(); _settingsFacade.Update.Save(current with { - AutoCheckUpdates = AutoCheckUpdates, IncludePrereleaseUpdates = string.Equals( SelectedUpdateChannelValue, UpdateSettingsValues.ChannelPreview, @@ -1841,7 +1824,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase StatusCardDescription = L("settings.update.status_card_description", "Check for updates and review the latest release information."); PreferencesHeader = L("settings.update.preferences_header", "Update Preferences"); PreferencesDescription = L("settings.update.preferences_description", "Choose your release channel, download source, behavior, and download speed."); - AutoCheckUpdatesLabel = L("settings.update.auto_check_toggle", "Automatically check for updates on startup"); UpdateChannelLabel = L("settings.update.channel_label", "Update Channel"); UpdateSourceLabel = L("settings.update.source_label", "Download Source"); UpdateModeLabel = L("settings.update.mode_label", "Update Mode"); @@ -1870,7 +1852,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase { var update = _settingsFacade.Update.Get(); _isInitializing = true; - AutoCheckUpdates = update.AutoCheckUpdates; SelectedUpdateChannelValue = UpdateSettingsValues.NormalizeChannel(update.UpdateChannel, update.IncludePrereleaseUpdates); SelectedUpdateSourceValue = UpdateSettingsValues.NormalizeDownloadSource(update.UpdateDownloadSource); SelectedUpdateModeValue = UpdateSettingsValues.NormalizeMode(update.UpdateMode); diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs index 1cdca75..3abb52f 100644 --- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs +++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs @@ -473,6 +473,7 @@ public partial class MainWindow var latestWeatherState = _weatherSettingsService.Get(); var latestUpdateState = _updateSettingsService.Get(); var latestThemeState = _themeSettingsService.Get(); + var latestPrivacyState = _settingsFacade.Privacy.Get(); return new AppSettingsSnapshot { GridShortSideCells = _targetShortSideCells, @@ -504,7 +505,8 @@ public partial class MainWindow WeatherNoTlsRequests = latestWeatherState.NoTlsRequests, AutoStartWithWindows = _autoStartWithWindows, AppRenderMode = _selectedAppRenderMode, - AutoCheckUpdates = latestUpdateState.AutoCheckUpdates, + UploadAnonymousCrashData = latestPrivacyState.UploadAnonymousCrashData, + UploadAnonymousUsageData = latestPrivacyState.UploadAnonymousUsageData, IncludePrereleaseUpdates = latestUpdateState.IncludePrereleaseUpdates, UpdateChannel = latestUpdateState.UpdateChannel, UpdateMode = latestUpdateState.UpdateMode, diff --git a/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml index ed4879a..ede0001 100644 --- a/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml @@ -16,8 +16,11 @@ - + + diff --git a/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml.cs index b455de3..c6c99e1 100644 --- a/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml.cs +++ b/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml.cs @@ -22,9 +22,17 @@ public partial class PrivacySettingsPage : SettingsPageBase public PrivacySettingsPage(PrivacySettingsPageViewModel viewModel) { ViewModel = viewModel; + ViewModel.ViewPrivacyPolicyRequested += OnViewPrivacyPolicyRequested; DataContext = ViewModel; InitializeComponent(); } public PrivacySettingsPageViewModel ViewModel { get; } + + private void OnViewPrivacyPolicyRequested() + { + var privacyPolicyViewModel = new PrivacyPolicyViewModel(); + var drawer = new PrivacyPolicyDrawer(privacyPolicyViewModel); + OpenDrawer(drawer, privacyPolicyViewModel.Title); + } } diff --git a/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml index 871fcae..5f06263 100644 --- a/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml @@ -21,12 +21,17 @@ + + @@ -64,9 +69,9 @@ Content="{Binding CheckForUpdatesButtonText}" /> - + - + + Text="{Binding UpdateStatus}" + TextWrapping="Wrap" + MaxWidth="500" /> + IsVisible="{Binding IsDownloadProgressVisible}" + Margin="0,4,0,4" /> + Text="{Binding DownloadProgressText}" + TextWrapping="Wrap" + Margin="0,4,0,0" /> - - - - - - - - + diff --git a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml index 7ee16ad..977313b 100644 --- a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml @@ -96,8 +96,11 @@ - +