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 =>
|
||||
(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;
|
||||
|
||||
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>
|
||||
<Folder Include="Models\" />
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<EmbeddedResource Include="Assets\Documents\Privacy.md" />
|
||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "位置来源",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<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))
|
||||
{
|
||||
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<string, object>
|
||||
{
|
||||
{ "$current_url", "app://main" },
|
||||
{ "$title", "LanMountainDesktop" }
|
||||
});
|
||||
|
||||
// 发送应用启动事件(始终发送,用于统计用户数量)
|
||||
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}");
|
||||
@@ -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<string, object>(),
|
||||
IncludeDetailedData = _isEnabled
|
||||
@@ -542,21 +596,29 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
{
|
||||
var userProperties = new Dictionary<string, object>
|
||||
{
|
||||
{ "$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<string, object>
|
||||
{
|
||||
{ "api_key", PostHogApiKey },
|
||||
{ "event", "$identify" },
|
||||
{ "distinct_id", _deviceIdService.PersistentUserId }, // 使用持久化用户ID
|
||||
{ "timestamp", DateTimeOffset.UtcNow.ToString("o") },
|
||||
{ "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();
|
||||
|
||||
// 显式开始会话跟踪
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ public sealed record PrivacySettingsState(
|
||||
bool UploadAnonymousCrashData,
|
||||
bool UploadAnonymousUsageData);
|
||||
public sealed record UpdateSettingsState(
|
||||
bool AutoCheckUpdates,
|
||||
bool IncludePrereleaseUpdates,
|
||||
string UpdateChannel,
|
||||
string UpdateMode,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
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 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="12">
|
||||
<TextBox Text="{Binding SearchText}"
|
||||
Watermark="{Binding SearchPlaceholder}" />
|
||||
<TextBox x:Name="SearchTextBox"
|
||||
Text="{Binding SearchText}"
|
||||
Watermark="{Binding SearchPlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding RefreshCommand}"
|
||||
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"
|
||||
Opacity="0.7"
|
||||
Margin="0,4,0,8" />
|
||||
<TextBox Text="{Binding DeviceId}"
|
||||
<TextBox x:Name="DeviceIdTextBox"
|
||||
Text="{Binding DeviceId}"
|
||||
IsReadOnly="True"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12" />
|
||||
FontSize="12"
|
||||
Focusable="False"
|
||||
IsTabStop="False" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Content="{Binding RefreshDeviceIdText}"
|
||||
@@ -58,6 +61,28 @@
|
||||
Classes="accent-button" />
|
||||
</Grid>
|
||||
</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>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,17 @@
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Opacity" Value="0.68" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="MaxWidth" Value="200" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.update-kv-value">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="MaxWidth" Value="200" />
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
@@ -64,9 +69,9 @@
|
||||
Content="{Binding CheckForUpdatesButtonText}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
ColumnSpacing="14"
|
||||
RowSpacing="12">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="20"
|
||||
RowSpacing="16">
|
||||
<StackPanel Grid.Column="0"
|
||||
Spacing="4">
|
||||
<TextBlock Classes="update-kv-label"
|
||||
@@ -105,18 +110,23 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding UpdateStatus}" />
|
||||
Text="{Binding UpdateStatus}"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="500" />
|
||||
|
||||
<ProgressBar Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{Binding DownloadProgressValue}"
|
||||
IsVisible="{Binding IsDownloadProgressVisible}" />
|
||||
IsVisible="{Binding IsDownloadProgressVisible}"
|
||||
Margin="0,4,0,4" />
|
||||
|
||||
<TextBlock Classes="settings-item-description"
|
||||
IsVisible="{Binding IsDownloadProgressVisible}"
|
||||
Text="{Binding DownloadProgressText}" />
|
||||
Text="{Binding DownloadProgressText}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
@@ -210,15 +220,7 @@
|
||||
</ui:SettingsExpander.Footer>
|
||||
</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>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -96,8 +96,11 @@
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="12">
|
||||
<TextBox Text="{Binding SearchKeyword}"
|
||||
Watermark="{Binding SearchPlaceholder}" />
|
||||
<TextBox x:Name="SearchKeywordTextBox"
|
||||
Text="{Binding SearchKeyword}"
|
||||
Watermark="{Binding SearchPlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding SearchCommand}"
|
||||
Content="{Binding SearchButtonText}" />
|
||||
@@ -178,12 +181,18 @@
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBox Text="{Binding LocationKey}"
|
||||
Watermark="{Binding LocationKeyPlaceholder}" />
|
||||
<TextBox x:Name="LocationKeyTextBox"
|
||||
Text="{Binding LocationKey}"
|
||||
Watermark="{Binding LocationKeyPlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBox Text="{Binding LocationName}"
|
||||
Watermark="{Binding LocationNamePlaceholder}" />
|
||||
<TextBox x:Name="LocationNameTextBox"
|
||||
Text="{Binding LocationName}"
|
||||
Watermark="{Binding LocationNamePlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
@@ -230,11 +239,14 @@
|
||||
<fi:SymbolIconSource Symbol="Warning" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<TextBox Width="360"
|
||||
<TextBox x:Name="ExcludedAlertsTextBox"
|
||||
Width="360"
|
||||
MinHeight="120"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding ExcludedAlerts}" />
|
||||
Text="{Binding ExcludedAlerts}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user