mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.6.3
优化了文本框焦点,优化了更新体验,优化了遥测,披露了收集的数据。
This commit is contained in:
@@ -67,6 +67,15 @@ public partial class App : Application
|
|||||||
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
|
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
|
||||||
(Current as App)?._hostApplicationLifecycle;
|
(Current as App)?._hostApplicationLifecycle;
|
||||||
|
|
||||||
|
// 隐私政策查看事件
|
||||||
|
public static event Action? CurrentPrivacyPolicyViewRequested;
|
||||||
|
|
||||||
|
// 触发隐私政策查看事件的方法
|
||||||
|
public static void RaisePrivacyPolicyViewRequested()
|
||||||
|
{
|
||||||
|
CurrentPrivacyPolicyViewRequested?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
||||||
public ISettingsFacadeService SettingsFacade => _settingsFacade;
|
public ISettingsFacadeService SettingsFacade => _settingsFacade;
|
||||||
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
||||||
|
|||||||
326
LanMountainDesktop/Assets/Documents/Privacy.md
Normal file
326
LanMountainDesktop/Assets/Documents/Privacy.md
Normal file
@@ -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!**
|
||||||
|
|
||||||
|
我们承诺保护您的隐私,并持续改进我们的隐私保护措施。
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Models\" />
|
<Folder Include="Models\" />
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
|
<EmbeddedResource Include="Assets\Documents\Privacy.md" />
|
||||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -99,6 +99,11 @@
|
|||||||
"settings.privacy.crash_upload_description": "Help us improve application stability.",
|
"settings.privacy.crash_upload_description": "Help us improve application stability.",
|
||||||
"settings.privacy.usage_upload_title": "Anonymous usage data uploads",
|
"settings.privacy.usage_upload_title": "Anonymous usage data uploads",
|
||||||
"settings.privacy.usage_upload_description": "Help us improve application features.",
|
"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.title": "Weather",
|
||||||
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
|
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
|
||||||
"settings.weather.location_source_header": "Location Source",
|
"settings.weather.location_source_header": "Location Source",
|
||||||
|
|||||||
@@ -98,6 +98,11 @@
|
|||||||
"settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。",
|
"settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。",
|
||||||
"settings.privacy.usage_upload_title": "匿名上传使用数据",
|
"settings.privacy.usage_upload_title": "匿名上传使用数据",
|
||||||
"settings.privacy.usage_upload_description": "帮助我们改善应用功能。",
|
"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.title": "天气",
|
||||||
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
|
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
|
||||||
"settings.weather.location_source_header": "位置来源",
|
"settings.weather.location_source_header": "位置来源",
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public string AppRenderMode { get; set; } = "Default";
|
public string AppRenderMode { get; set; } = "Default";
|
||||||
|
|
||||||
public bool AutoCheckUpdates { get; set; } = true;
|
|
||||||
|
|
||||||
public bool IncludePrereleaseUpdates { get; set; }
|
public bool IncludePrereleaseUpdates { get; set; }
|
||||||
|
|
||||||
public bool UploadAnonymousCrashData { get; set; }
|
public bool UploadAnonymousCrashData { get; set; }
|
||||||
@@ -72,6 +70,8 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public string? DeviceId { get; set; }
|
public string? DeviceId { get; set; }
|
||||||
|
|
||||||
|
public string? PersistentUserId { get; set; }
|
||||||
|
|
||||||
public string UpdateChannel { get; set; } = "stable";
|
public string UpdateChannel { get; set; } = "stable";
|
||||||
|
|
||||||
public string UpdateMode { get; set; } = "download_then_confirm";
|
public string UpdateMode { get; set; } = "download_then_confirm";
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public sealed class DeviceIdService
|
|||||||
{
|
{
|
||||||
private static DeviceIdService? _instance;
|
private static DeviceIdService? _instance;
|
||||||
private string? _deviceId;
|
private string? _deviceId;
|
||||||
|
private string? _persistentUserId; // 持久化的用户ID,用于关联设备
|
||||||
private readonly ISettingsFacadeService _settingsFacade;
|
private readonly ISettingsFacadeService _settingsFacade;
|
||||||
private bool _isInitialized;
|
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()
|
private void EnsureDeviceId()
|
||||||
{
|
{
|
||||||
if (_isInitialized)
|
if (_isInitialized)
|
||||||
@@ -56,13 +70,22 @@ public sealed class DeviceIdService
|
|||||||
{
|
{
|
||||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(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))
|
if (string.IsNullOrEmpty(snapshot.DeviceId))
|
||||||
{
|
{
|
||||||
snapshot.DeviceId = GenerateDeviceId();
|
snapshot.DeviceId = GenerateDeviceId();
|
||||||
_settingsFacade.Settings.SaveSnapshot(
|
_settingsFacade.Settings.SaveSnapshot(
|
||||||
SettingsScope.App,
|
SettingsScope.App,
|
||||||
snapshot,
|
snapshot,
|
||||||
changedKeys: [nameof(AppSettingsSnapshot.DeviceId)]);
|
changedKeys: [nameof(AppSettingsSnapshot.DeviceId), nameof(AppSettingsSnapshot.PersistentUserId)]);
|
||||||
_deviceId = snapshot.DeviceId;
|
_deviceId = snapshot.DeviceId;
|
||||||
AppLogger.Info("DeviceId", $"Generated new device ID: {_deviceId}");
|
AppLogger.Info("DeviceId", $"Generated new device ID: {_deviceId}");
|
||||||
}
|
}
|
||||||
@@ -75,6 +98,7 @@ public sealed class DeviceIdService
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_deviceId = GenerateDeviceId();
|
_deviceId = GenerateDeviceId();
|
||||||
|
_persistentUserId = GeneratePersistentUserId();
|
||||||
AppLogger.Warn("DeviceId", $"Failed to persist device ID, using generated ID: {_deviceId}", ex);
|
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));
|
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(deviceInfo));
|
||||||
return Convert.ToHexString(hash)[..32].ToLower();
|
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
|
public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||||
@@ -140,9 +173,18 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
|||||||
TimeSpan.FromSeconds(10),
|
TimeSpan.FromSeconds(10),
|
||||||
TimeSpan.FromSeconds(30));
|
TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
// 发送PostHog标准的$pageview事件用于统计日活(始终发送,不受开关影响)
|
||||||
|
CaptureEvent("$pageview", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "$current_url", "app://main" },
|
||||||
|
{ "$title", "LanMountainDesktop" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送应用启动事件(始终发送,用于统计用户数量)
|
||||||
CaptureEvent("app_online", new Dictionary<string, object>
|
CaptureEvent("app_online", new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "event_type", "app_start" }
|
{ "event_type", "app_start" },
|
||||||
|
{ "analytics_enabled", _isEnabled }
|
||||||
});
|
});
|
||||||
|
|
||||||
AppLogger.Info("UserBehaviorAnalytics", $"Analytics initialized. DeviceId={_deviceIdService.DeviceId}, Enabled={_isEnabled}");
|
AppLogger.Info("UserBehaviorAnalytics", $"Analytics initialized. DeviceId={_deviceIdService.DeviceId}, Enabled={_isEnabled}");
|
||||||
@@ -382,10 +424,22 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
|||||||
|
|
||||||
try
|
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
|
var eventData = new UserBehaviorEvent
|
||||||
{
|
{
|
||||||
Event = eventName,
|
Event = eventName,
|
||||||
DistinctId = _deviceIdService.DeviceId,
|
DistinctId = _deviceIdService.PersistentUserId, // 使用持久化用户ID
|
||||||
Timestamp = DateTimeOffset.UtcNow,
|
Timestamp = DateTimeOffset.UtcNow,
|
||||||
Properties = properties ?? new Dictionary<string, object>(),
|
Properties = properties ?? new Dictionary<string, object>(),
|
||||||
IncludeDetailedData = _isEnabled
|
IncludeDetailedData = _isEnabled
|
||||||
@@ -542,21 +596,29 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
|||||||
{
|
{
|
||||||
var userProperties = new Dictionary<string, object>
|
var userProperties = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "$device_id", distinctId },
|
|
||||||
{ "$app_version", GetAppVersion() },
|
{ "$app_version", GetAppVersion() },
|
||||||
{ "$os", GetOsName() },
|
{ "$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<string, object>
|
var requestBody = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "api_key", PostHogApiKey },
|
{ "api_key", PostHogApiKey },
|
||||||
{ "event", "$identify" },
|
{ "event", "$identify" },
|
||||||
|
{ "distinct_id", _deviceIdService.PersistentUserId }, // 使用持久化用户ID
|
||||||
{ "timestamp", DateTimeOffset.UtcNow.ToString("o") },
|
{ "timestamp", DateTimeOffset.UtcNow.ToString("o") },
|
||||||
{ "properties", new Dictionary<string, object>
|
{ "properties", new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "distinct_id", distinctId },
|
{ "$set", userProperties },
|
||||||
{ "$set", userProperties }
|
{ "$set_once", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "first_app_open", DateTimeOffset.UtcNow.ToString("o") }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -796,6 +858,9 @@ public sealed class CrashReportService
|
|||||||
});
|
});
|
||||||
|
|
||||||
ConfigureCrashReportingScope();
|
ConfigureCrashReportingScope();
|
||||||
|
|
||||||
|
// 显式开始会话跟踪
|
||||||
|
SentrySdk.StartSession();
|
||||||
|
|
||||||
AppLogger.Info("CrashReport", $"Sentry crash reporting initialized. DeviceId={_deviceIdService.DeviceId}");
|
AppLogger.Info("CrashReport", $"Sentry crash reporting initialized. DeviceId={_deviceIdService.DeviceId}");
|
||||||
|
|
||||||
@@ -849,7 +914,10 @@ public sealed class CrashReportService
|
|||||||
{
|
{
|
||||||
if (_isEnabled && _isInitialized)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ public sealed record PrivacySettingsState(
|
|||||||
bool UploadAnonymousCrashData,
|
bool UploadAnonymousCrashData,
|
||||||
bool UploadAnonymousUsageData);
|
bool UploadAnonymousUsageData);
|
||||||
public sealed record UpdateSettingsState(
|
public sealed record UpdateSettingsState(
|
||||||
bool AutoCheckUpdates,
|
|
||||||
bool IncludePrereleaseUpdates,
|
bool IncludePrereleaseUpdates,
|
||||||
string UpdateChannel,
|
string UpdateChannel,
|
||||||
string UpdateMode,
|
string UpdateMode,
|
||||||
|
|||||||
@@ -628,7 +628,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
snapshot.UpdateChannel,
|
snapshot.UpdateChannel,
|
||||||
snapshot.IncludePrereleaseUpdates);
|
snapshot.IncludePrereleaseUpdates);
|
||||||
return new UpdateSettingsState(
|
return new UpdateSettingsState(
|
||||||
snapshot.AutoCheckUpdates,
|
|
||||||
string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase),
|
string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase),
|
||||||
normalizedChannel,
|
normalizedChannel,
|
||||||
UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode),
|
UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode),
|
||||||
@@ -646,7 +645,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
|
var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
|
||||||
state.UpdateChannel,
|
state.UpdateChannel,
|
||||||
state.IncludePrereleaseUpdates);
|
state.IncludePrereleaseUpdates);
|
||||||
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
|
|
||||||
snapshot.IncludePrereleaseUpdates = string.Equals(
|
snapshot.IncludePrereleaseUpdates = string.Equals(
|
||||||
normalizedChannel,
|
normalizedChannel,
|
||||||
UpdateSettingsValues.ChannelPreview,
|
UpdateSettingsValues.ChannelPreview,
|
||||||
@@ -672,7 +670,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
|||||||
snapshot,
|
snapshot,
|
||||||
changedKeys:
|
changedKeys:
|
||||||
[
|
[
|
||||||
nameof(AppSettingsSnapshot.AutoCheckUpdates),
|
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
|
||||||
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
|
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
|
||||||
nameof(AppSettingsSnapshot.UpdateChannel),
|
nameof(AppSettingsSnapshot.UpdateChannel),
|
||||||
nameof(AppSettingsSnapshot.UpdateMode),
|
nameof(AppSettingsSnapshot.UpdateMode),
|
||||||
|
|||||||
@@ -131,13 +131,10 @@ public sealed class UpdateWorkflowService
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var state = _settingsFacade.Update.Get();
|
var state = _settingsFacade.Update.Get();
|
||||||
if (!state.AutoCheckUpdates)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Always check for updates on startup (removed AutoCheckUpdates check)
|
||||||
var result = await CheckForUpdatesAsync(currentVersion, cancellationToken);
|
var result = await CheckForUpdatesAsync(currentVersion, cancellationToken);
|
||||||
if (!result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
|
if (!result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
|
||||||
{
|
{
|
||||||
@@ -145,12 +142,14 @@ public sealed class UpdateWorkflowService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var normalizedMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode);
|
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);
|
||||||
}
|
}
|
||||||
|
// For "Manual" mode, just check but don't download
|
||||||
await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
92
LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs
Normal file
92
LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
|
|||||||
private readonly string _languageCode;
|
private readonly string _languageCode;
|
||||||
private bool _isInitializing;
|
private bool _isInitializing;
|
||||||
|
|
||||||
|
public event Action? ViewPrivacyPolicyRequested;
|
||||||
|
|
||||||
public PrivacySettingsPageViewModel(ISettingsFacadeService settingsFacade)
|
public PrivacySettingsPageViewModel(ISettingsFacadeService settingsFacade)
|
||||||
{
|
{
|
||||||
_settingsFacade = settingsFacade;
|
_settingsFacade = settingsFacade;
|
||||||
@@ -59,6 +61,12 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _refreshDeviceIdText = string.Empty;
|
private string _refreshDeviceIdText = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _viewPrivacyPolicyText = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _privacyPolicyHintPrefix = string.Empty;
|
||||||
|
|
||||||
public void Load()
|
public void Load()
|
||||||
{
|
{
|
||||||
var state = _settingsFacade.Privacy.Get();
|
var state = _settingsFacade.Privacy.Get();
|
||||||
@@ -130,6 +138,25 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
|
|||||||
DeviceIdHeader = L("settings.privacy.device_id_title", "Device ID");
|
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.");
|
DeviceIdDescription = L("settings.privacy.device_id_description", "Unique identifier for this device. Click refresh to regenerate.");
|
||||||
RefreshDeviceIdText = L("settings.privacy.refresh_device_id", "Refresh");
|
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)
|
private string L(string key, string fallback)
|
||||||
|
|||||||
@@ -1329,9 +1329,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
|||||||
LoadStateFromSettings();
|
LoadStateFromSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _autoCheckUpdates;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _selectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
|
private string _selectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
|
||||||
|
|
||||||
@@ -1380,9 +1377,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _preferencesDescription = string.Empty;
|
private string _preferencesDescription = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _autoCheckUpdatesLabel = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _updateChannelLabel = string.Empty;
|
private string _updateChannelLabel = string.Empty;
|
||||||
|
|
||||||
@@ -1520,16 +1514,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
|||||||
|
|
||||||
private bool IsBusy => IsCheckingForUpdates || IsDownloading;
|
private bool IsBusy => IsCheckingForUpdates || IsDownloading;
|
||||||
|
|
||||||
partial void OnAutoCheckUpdatesChanged(bool value)
|
|
||||||
{
|
|
||||||
if (_isInitializing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveUpdateSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSelectedUpdateChannelOptionChanged(SelectionOption? value)
|
partial void OnSelectedUpdateChannelOptionChanged(SelectionOption? value)
|
||||||
{
|
{
|
||||||
if (value is not null &&
|
if (value is not null &&
|
||||||
@@ -1729,7 +1713,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
|||||||
var current = _settingsFacade.Update.Get();
|
var current = _settingsFacade.Update.Get();
|
||||||
_settingsFacade.Update.Save(current with
|
_settingsFacade.Update.Save(current with
|
||||||
{
|
{
|
||||||
AutoCheckUpdates = AutoCheckUpdates,
|
|
||||||
IncludePrereleaseUpdates = string.Equals(
|
IncludePrereleaseUpdates = string.Equals(
|
||||||
SelectedUpdateChannelValue,
|
SelectedUpdateChannelValue,
|
||||||
UpdateSettingsValues.ChannelPreview,
|
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.");
|
StatusCardDescription = L("settings.update.status_card_description", "Check for updates and review the latest release information.");
|
||||||
PreferencesHeader = L("settings.update.preferences_header", "Update Preferences");
|
PreferencesHeader = L("settings.update.preferences_header", "Update Preferences");
|
||||||
PreferencesDescription = L("settings.update.preferences_description", "Choose your release channel, download source, behavior, and download speed.");
|
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");
|
UpdateChannelLabel = L("settings.update.channel_label", "Update Channel");
|
||||||
UpdateSourceLabel = L("settings.update.source_label", "Download Source");
|
UpdateSourceLabel = L("settings.update.source_label", "Download Source");
|
||||||
UpdateModeLabel = L("settings.update.mode_label", "Update Mode");
|
UpdateModeLabel = L("settings.update.mode_label", "Update Mode");
|
||||||
@@ -1870,7 +1852,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
var update = _settingsFacade.Update.Get();
|
var update = _settingsFacade.Update.Get();
|
||||||
_isInitializing = true;
|
_isInitializing = true;
|
||||||
AutoCheckUpdates = update.AutoCheckUpdates;
|
|
||||||
SelectedUpdateChannelValue = UpdateSettingsValues.NormalizeChannel(update.UpdateChannel, update.IncludePrereleaseUpdates);
|
SelectedUpdateChannelValue = UpdateSettingsValues.NormalizeChannel(update.UpdateChannel, update.IncludePrereleaseUpdates);
|
||||||
SelectedUpdateSourceValue = UpdateSettingsValues.NormalizeDownloadSource(update.UpdateDownloadSource);
|
SelectedUpdateSourceValue = UpdateSettingsValues.NormalizeDownloadSource(update.UpdateDownloadSource);
|
||||||
SelectedUpdateModeValue = UpdateSettingsValues.NormalizeMode(update.UpdateMode);
|
SelectedUpdateModeValue = UpdateSettingsValues.NormalizeMode(update.UpdateMode);
|
||||||
|
|||||||
@@ -473,6 +473,7 @@ public partial class MainWindow
|
|||||||
var latestWeatherState = _weatherSettingsService.Get();
|
var latestWeatherState = _weatherSettingsService.Get();
|
||||||
var latestUpdateState = _updateSettingsService.Get();
|
var latestUpdateState = _updateSettingsService.Get();
|
||||||
var latestThemeState = _themeSettingsService.Get();
|
var latestThemeState = _themeSettingsService.Get();
|
||||||
|
var latestPrivacyState = _settingsFacade.Privacy.Get();
|
||||||
return new AppSettingsSnapshot
|
return new AppSettingsSnapshot
|
||||||
{
|
{
|
||||||
GridShortSideCells = _targetShortSideCells,
|
GridShortSideCells = _targetShortSideCells,
|
||||||
@@ -504,7 +505,8 @@ public partial class MainWindow
|
|||||||
WeatherNoTlsRequests = latestWeatherState.NoTlsRequests,
|
WeatherNoTlsRequests = latestWeatherState.NoTlsRequests,
|
||||||
AutoStartWithWindows = _autoStartWithWindows,
|
AutoStartWithWindows = _autoStartWithWindows,
|
||||||
AppRenderMode = _selectedAppRenderMode,
|
AppRenderMode = _selectedAppRenderMode,
|
||||||
AutoCheckUpdates = latestUpdateState.AutoCheckUpdates,
|
UploadAnonymousCrashData = latestPrivacyState.UploadAnonymousCrashData,
|
||||||
|
UploadAnonymousUsageData = latestPrivacyState.UploadAnonymousUsageData,
|
||||||
IncludePrereleaseUpdates = latestUpdateState.IncludePrereleaseUpdates,
|
IncludePrereleaseUpdates = latestUpdateState.IncludePrereleaseUpdates,
|
||||||
UpdateChannel = latestUpdateState.UpdateChannel,
|
UpdateChannel = latestUpdateState.UpdateChannel,
|
||||||
UpdateMode = latestUpdateState.UpdateMode,
|
UpdateMode = latestUpdateState.UpdateMode,
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
<ui:SettingsExpander.Footer>
|
<ui:SettingsExpander.Footer>
|
||||||
<Grid ColumnDefinitions="*,Auto"
|
<Grid ColumnDefinitions="*,Auto"
|
||||||
ColumnSpacing="12">
|
ColumnSpacing="12">
|
||||||
<TextBox Text="{Binding SearchText}"
|
<TextBox x:Name="SearchTextBox"
|
||||||
Watermark="{Binding SearchPlaceholder}" />
|
Text="{Binding SearchText}"
|
||||||
|
Watermark="{Binding SearchPlaceholder}"
|
||||||
|
Focusable="True"
|
||||||
|
IsTabStop="True" />
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
Command="{Binding RefreshCommand}"
|
Command="{Binding RefreshCommand}"
|
||||||
Content="{Binding RefreshButtonText}" />
|
Content="{Binding RefreshButtonText}" />
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||||
|
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia"
|
||||||
|
xmlns:helpers="using:LanMountainDesktop.Helpers"
|
||||||
|
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||||
|
x:Class="LanMountainDesktop.Views.SettingsPages.PrivacyPolicyDrawer"
|
||||||
|
x:DataType="vm:PrivacyPolicyViewModel">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Classes="settings-page-container"
|
||||||
|
Margin="0,0,0,8">
|
||||||
|
<Border Classes="settings-section-card">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<Grid ColumnDefinitions="Auto,*"
|
||||||
|
ColumnSpacing="12">
|
||||||
|
<Border Classes="settings-section-card-icon-host"
|
||||||
|
Width="48"
|
||||||
|
Height="48">
|
||||||
|
<Viewbox Stretch="Uniform">
|
||||||
|
<fi:SymbolIcon Symbol="Document" />
|
||||||
|
</Viewbox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
Spacing="4"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="settings-card-header"
|
||||||
|
Margin="0"
|
||||||
|
Text="{Binding Title}" />
|
||||||
|
<TextBlock Classes="settings-item-description"
|
||||||
|
Text="{Binding Description}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="settings-section-card">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Classes="settings-item-description"
|
||||||
|
IsVisible="{Binding IsLoading}"
|
||||||
|
Text="{Binding LoadingText}" />
|
||||||
|
|
||||||
|
<TextBlock Classes="settings-item-description"
|
||||||
|
IsVisible="{Binding HasError}"
|
||||||
|
Text="{Binding ErrorText}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<mdxaml:MarkdownScrollViewer IsVisible="{Binding HasContent}"
|
||||||
|
Markdown="{Binding MarkdownContent}"
|
||||||
|
Engine="{x:Static helpers:PluginMarketMarkdownHelper.Engine}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,10 +45,13 @@
|
|||||||
FontSize="12"
|
FontSize="12"
|
||||||
Opacity="0.7"
|
Opacity="0.7"
|
||||||
Margin="0,4,0,8" />
|
Margin="0,4,0,8" />
|
||||||
<TextBox Text="{Binding DeviceId}"
|
<TextBox x:Name="DeviceIdTextBox"
|
||||||
|
Text="{Binding DeviceId}"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
FontFamily="Consolas"
|
FontFamily="Consolas"
|
||||||
FontSize="12" />
|
FontSize="12"
|
||||||
|
Focusable="False"
|
||||||
|
IsTabStop="False" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
Content="{Binding RefreshDeviceIdText}"
|
Content="{Binding RefreshDeviceIdText}"
|
||||||
@@ -58,6 +61,28 @@
|
|||||||
Classes="accent-button" />
|
Classes="accent-button" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
Spacing="4">
|
||||||
|
<TextBlock Text="{Binding PrivacyPolicyHintPrefix}"
|
||||||
|
FontSize="13"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<Button Content="{Binding ViewPrivacyPolicyText}"
|
||||||
|
Command="{Binding ViewPrivacyPolicyCommand}"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
Padding="0"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{DynamicResource SystemAccentColor}"
|
||||||
|
Cursor="Hand">
|
||||||
|
<Button.Styles>
|
||||||
|
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="TextBlock.Foreground" Value="{DynamicResource SystemAccentColorDark1}" />
|
||||||
|
</Style>
|
||||||
|
</Button.Styles>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -22,9 +22,17 @@ public partial class PrivacySettingsPage : SettingsPageBase
|
|||||||
public PrivacySettingsPage(PrivacySettingsPageViewModel viewModel)
|
public PrivacySettingsPage(PrivacySettingsPageViewModel viewModel)
|
||||||
{
|
{
|
||||||
ViewModel = viewModel;
|
ViewModel = viewModel;
|
||||||
|
ViewModel.ViewPrivacyPolicyRequested += OnViewPrivacyPolicyRequested;
|
||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PrivacySettingsPageViewModel ViewModel { get; }
|
public PrivacySettingsPageViewModel ViewModel { get; }
|
||||||
|
|
||||||
|
private void OnViewPrivacyPolicyRequested()
|
||||||
|
{
|
||||||
|
var privacyPolicyViewModel = new PrivacyPolicyViewModel();
|
||||||
|
var drawer = new PrivacyPolicyDrawer(privacyPolicyViewModel);
|
||||||
|
OpenDrawer(drawer, privacyPolicyViewModel.Title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,17 @@
|
|||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="Opacity" Value="0.68" />
|
<Setter Property="Opacity" Value="0.68" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
<Setter Property="MaxWidth" Value="200" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBlock.update-kv-value">
|
<Style Selector="TextBlock.update-kv-value">
|
||||||
<Setter Property="FontSize" Value="14" />
|
<Setter Property="FontSize" Value="14" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
<Setter Property="MaxWidth" Value="200" />
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
@@ -64,9 +69,9 @@
|
|||||||
Content="{Binding CheckForUpdatesButtonText}" />
|
Content="{Binding CheckForUpdatesButtonText}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,*"
|
<Grid ColumnDefinitions="Auto,*"
|
||||||
ColumnSpacing="14"
|
ColumnSpacing="20"
|
||||||
RowSpacing="12">
|
RowSpacing="16">
|
||||||
<StackPanel Grid.Column="0"
|
<StackPanel Grid.Column="0"
|
||||||
Spacing="4">
|
Spacing="4">
|
||||||
<TextBlock Classes="update-kv-label"
|
<TextBlock Classes="update-kv-label"
|
||||||
@@ -105,18 +110,23 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock Classes="settings-item-description"
|
<TextBlock Classes="settings-item-description"
|
||||||
Text="{Binding UpdateStatus}" />
|
Text="{Binding UpdateStatus}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
MaxWidth="500" />
|
||||||
|
|
||||||
<ProgressBar Minimum="0"
|
<ProgressBar Minimum="0"
|
||||||
Maximum="100"
|
Maximum="100"
|
||||||
Value="{Binding DownloadProgressValue}"
|
Value="{Binding DownloadProgressValue}"
|
||||||
IsVisible="{Binding IsDownloadProgressVisible}" />
|
IsVisible="{Binding IsDownloadProgressVisible}"
|
||||||
|
Margin="0,4,0,4" />
|
||||||
|
|
||||||
<TextBlock Classes="settings-item-description"
|
<TextBlock Classes="settings-item-description"
|
||||||
IsVisible="{Binding IsDownloadProgressVisible}"
|
IsVisible="{Binding IsDownloadProgressVisible}"
|
||||||
Text="{Binding DownloadProgressText}" />
|
Text="{Binding DownloadProgressText}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="0,4,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal"
|
<StackPanel Orientation="Horizontal"
|
||||||
@@ -210,15 +220,7 @@
|
|||||||
</ui:SettingsExpander.Footer>
|
</ui:SettingsExpander.Footer>
|
||||||
</ui:SettingsExpander>
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
<ui:SettingsExpander Classes="settings-expander-card"
|
|
||||||
Header="{Binding AutoCheckUpdatesLabel}">
|
|
||||||
<ui:SettingsExpander.IconSource>
|
|
||||||
<fi:SymbolIconSource Symbol="ClockAlarm" />
|
|
||||||
</ui:SettingsExpander.IconSource>
|
|
||||||
<ui:SettingsExpander.Footer>
|
|
||||||
<ToggleSwitch IsChecked="{Binding AutoCheckUpdates}" />
|
|
||||||
</ui:SettingsExpander.Footer>
|
|
||||||
</ui:SettingsExpander>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -96,8 +96,11 @@
|
|||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<Grid ColumnDefinitions="*,Auto"
|
<Grid ColumnDefinitions="*,Auto"
|
||||||
ColumnSpacing="12">
|
ColumnSpacing="12">
|
||||||
<TextBox Text="{Binding SearchKeyword}"
|
<TextBox x:Name="SearchKeywordTextBox"
|
||||||
Watermark="{Binding SearchPlaceholder}" />
|
Text="{Binding SearchKeyword}"
|
||||||
|
Watermark="{Binding SearchPlaceholder}"
|
||||||
|
Focusable="True"
|
||||||
|
IsTabStop="True" />
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
Command="{Binding SearchCommand}"
|
Command="{Binding SearchCommand}"
|
||||||
Content="{Binding SearchButtonText}" />
|
Content="{Binding SearchButtonText}" />
|
||||||
@@ -178,12 +181,18 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</ui:SettingsExpanderItem>
|
</ui:SettingsExpanderItem>
|
||||||
<ui:SettingsExpanderItem>
|
<ui:SettingsExpanderItem>
|
||||||
<TextBox Text="{Binding LocationKey}"
|
<TextBox x:Name="LocationKeyTextBox"
|
||||||
Watermark="{Binding LocationKeyPlaceholder}" />
|
Text="{Binding LocationKey}"
|
||||||
|
Watermark="{Binding LocationKeyPlaceholder}"
|
||||||
|
Focusable="True"
|
||||||
|
IsTabStop="True" />
|
||||||
</ui:SettingsExpanderItem>
|
</ui:SettingsExpanderItem>
|
||||||
<ui:SettingsExpanderItem>
|
<ui:SettingsExpanderItem>
|
||||||
<TextBox Text="{Binding LocationName}"
|
<TextBox x:Name="LocationNameTextBox"
|
||||||
Watermark="{Binding LocationNamePlaceholder}" />
|
Text="{Binding LocationName}"
|
||||||
|
Watermark="{Binding LocationNamePlaceholder}"
|
||||||
|
Focusable="True"
|
||||||
|
IsTabStop="True" />
|
||||||
</ui:SettingsExpanderItem>
|
</ui:SettingsExpanderItem>
|
||||||
</ui:SettingsExpander>
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
@@ -230,11 +239,14 @@
|
|||||||
<fi:SymbolIconSource Symbol="Warning" />
|
<fi:SymbolIconSource Symbol="Warning" />
|
||||||
</ui:SettingsExpander.IconSource>
|
</ui:SettingsExpander.IconSource>
|
||||||
<ui:SettingsExpander.Footer>
|
<ui:SettingsExpander.Footer>
|
||||||
<TextBox Width="360"
|
<TextBox x:Name="ExcludedAlertsTextBox"
|
||||||
|
Width="360"
|
||||||
MinHeight="120"
|
MinHeight="120"
|
||||||
AcceptsReturn="True"
|
AcceptsReturn="True"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Text="{Binding ExcludedAlerts}" />
|
Text="{Binding ExcludedAlerts}"
|
||||||
|
Focusable="True"
|
||||||
|
IsTabStop="True" />
|
||||||
</ui:SettingsExpander.Footer>
|
</ui:SettingsExpander.Footer>
|
||||||
</ui:SettingsExpander>
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user