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/PrivacyPolicyDrawer.axaml b/LanMountainDesktop/Views/SettingsPages/PrivacyPolicyDrawer.axaml
new file mode 100644
index 0000000..572444b
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/PrivacyPolicyDrawer.axaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/PrivacyPolicyDrawer.axaml.cs b/LanMountainDesktop/Views/SettingsPages/PrivacyPolicyDrawer.axaml.cs
new file mode 100644
index 0000000..bb1d652
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/PrivacyPolicyDrawer.axaml.cs
@@ -0,0 +1,17 @@
+using Avalonia.Controls;
+using LanMountainDesktop.ViewModels;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+public partial class PrivacyPolicyDrawer : UserControl
+{
+ public PrivacyPolicyDrawer()
+ {
+ InitializeComponent();
+ }
+
+ public PrivacyPolicyDrawer(PrivacyPolicyViewModel viewModel) : this()
+ {
+ DataContext = viewModel;
+ }
+}
diff --git a/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml
index 2f6478e..42cc330 100644
--- a/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/PrivacySettingsPage.axaml
@@ -45,10 +45,13 @@
FontSize="12"
Opacity="0.7"
Margin="0,4,0,8" />
-
+ FontSize="12"
+ Focusable="False"
+ IsTabStop="False" />
+
+
+
+
+
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 @@
-
+
@@ -178,12 +181,18 @@
-
+
-
+
@@ -230,11 +239,14 @@
-
+ Text="{Binding ExcludedAlerts}"
+ Focusable="True"
+ IsTabStop="True" />