Compare commits

...

3 Commits

Author SHA1 Message Date
lincube
c3db5af923 0.7.4
首先我加了CI课程表json的读取,然后把天气时钟这个老问题也修了。
2026-03-22 04:57:19 +08:00
lincube
1a7dde34d0 0.7.3.1 2026-03-22 02:53:31 +08:00
lincube
73cdefe296 0.7.3
修东西
2026-03-21 22:40:07 +08:00
37 changed files with 1741 additions and 190 deletions

View File

@@ -101,6 +101,11 @@ public partial class App : Application
public App() public App()
{ {
if (Design.IsDesignMode)
{
return;
}
_settingsFacade.Settings.Changed += OnSettingsChanged; _settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged; _appearanceThemeService.Changed += OnAppearanceThemeChanged;
} }
@@ -108,9 +113,16 @@ public partial class App : Application
public override void Initialize() public override void Initialize()
{ {
AppLogger.Info("App", "Initializing application resources."); AppLogger.Info("App", "Initializing application resources.");
AvaloniaXamlLoader.Load(this);
if (Design.IsDesignMode)
{
ApplyDesignTimeTheme();
return;
}
ConfigureWebViewUserDataFolder(); ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default); AvaloniaWebViewBuilder.Initialize(default);
AvaloniaXamlLoader.Load(this);
ApplyThemeFromSettings(); ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings(); ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService(); EnsureSettingsWindowService();
@@ -119,6 +131,12 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (Design.IsDesignMode)
{
base.OnFrameworkInitializationCompleted();
return;
}
AppLogger.Info("App", "Framework initialization completed."); AppLogger.Info("App", "Framework initialization completed.");
RegisterUiUnhandledExceptionGuard(); RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled(); LinuxDesktopEntryInstaller.EnsureInstalled();
@@ -127,6 +145,20 @@ public partial class App : Application
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
private void ApplyDesignTimeTheme()
{
RequestedThemeVariant = ThemeVariant.Light;
try
{
ApplyAdaptiveThemeResources();
}
catch (Exception ex)
{
AppLogger.Warn("Previewer", "Failed to apply adaptive theme resources in design mode.", ex);
}
}
private void InitializeDesktopShell() private void InitializeDesktopShell()
{ {
_desktopShellHost ??= new DesktopShellHost( _desktopShellHost ??= new DesktopShellHost(

View File

@@ -38,6 +38,27 @@
"settings.wallpaper.title": "Wallpaper", "settings.wallpaper.title": "Wallpaper",
"settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.", "settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.",
"settings.wallpaper.current_label": "Current Wallpaper", "settings.wallpaper.current_label": "Current Wallpaper",
"settings.wallpaper.type_label": "Wallpaper Type",
"settings.wallpaper.type.image": "Image",
"settings.wallpaper.type.solid_color": "Solid Color",
"settings.wallpaper.type.system": "System Wallpaper",
"settings.wallpaper.system.label": "System Wallpaper",
"settings.wallpaper.system.unavailable": "Unable to read system wallpaper",
"settings.wallpaper.refresh_interval": "Refresh Interval",
"settings.wallpaper.refresh_now": "Refresh Now",
"settings.wallpaper.refresh.30s": "30 seconds",
"settings.wallpaper.refresh.1m": "1 minute",
"settings.wallpaper.refresh.5m": "5 minutes",
"settings.wallpaper.refresh.10m": "10 minutes",
"settings.wallpaper.refresh.15m": "15 minutes",
"settings.wallpaper.refresh.30m": "30 minutes",
"settings.wallpaper.refresh.1h": "1 hour",
"settings.wallpaper.refresh.2h": "2 hours",
"settings.wallpaper.refresh.4h": "4 hours",
"settings.wallpaper.refresh.8h": "8 hours",
"settings.wallpaper.refresh.12h": "12 hours",
"settings.wallpaper.refresh.24h": "24 hours",
"settings.wallpaper.color_label": "Wallpaper Color",
"settings.wallpaper.placement_label": "Placement", "settings.wallpaper.placement_label": "Placement",
"settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.", "settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.",
"settings.wallpaper.pick_button": "Browse Files", "settings.wallpaper.pick_button": "Browse Files",
@@ -217,7 +238,14 @@
"schedule.settings.unnamed": "Unnamed Schedule", "schedule.settings.unnamed": "Unnamed Schedule",
"schedule.settings.delete": "Delete", "schedule.settings.delete": "Delete",
"schedule.settings.picker_title": "Select ClassIsland schedule file", "schedule.settings.picker_title": "Select ClassIsland schedule file",
"schedule.settings.picker_file_type": "ClassIsland CSES schedule", "schedule.settings.picker_file_type.all": "ClassIsland Schedule Files",
"schedule.settings.picker_file_type.json": "ClassIsland Profile (JSON)",
"schedule.settings.picker_file_type.cses": "CSES Schedule (YAML)",
"schedule.settings.semester.title": "Semester Settings",
"schedule.settings.semester.start_date": "Semester Start Date",
"schedule.settings.semester.week_cycle": "Week Cycle",
"schedule.settings.semester.week_cycle_desc": "Set the week rotation cycle for multi-week schedules (e.g., 2 for odd/even weeks).",
"schedule.settings.semester.week_cycle_format": "{0}-week rotation",
"worldclock.settings.title": "World Clock Settings", "worldclock.settings.title": "World Clock Settings",
"worldclock.settings.desc": "Choose a time zone for each of the four clocks.", "worldclock.settings.desc": "Choose a time zone for each of the four clocks.",
"worldclock.settings.clock_1": "Clock 1", "worldclock.settings.clock_1": "Clock 1",

View File

@@ -41,6 +41,23 @@
"settings.wallpaper.type_label": "壁纸类型", "settings.wallpaper.type_label": "壁纸类型",
"settings.wallpaper.type.image": "图片", "settings.wallpaper.type.image": "图片",
"settings.wallpaper.type.solid_color": "纯色", "settings.wallpaper.type.solid_color": "纯色",
"settings.wallpaper.type.system": "系统壁纸",
"settings.wallpaper.system.label": "系统壁纸",
"settings.wallpaper.system.unavailable": "无法读取系统壁纸",
"settings.wallpaper.refresh_interval": "刷新频率",
"settings.wallpaper.refresh_now": "立即刷新",
"settings.wallpaper.refresh.30s": "30 秒",
"settings.wallpaper.refresh.1m": "1 分钟",
"settings.wallpaper.refresh.5m": "5 分钟",
"settings.wallpaper.refresh.10m": "10 分钟",
"settings.wallpaper.refresh.15m": "15 分钟",
"settings.wallpaper.refresh.30m": "30 分钟",
"settings.wallpaper.refresh.1h": "1 小时",
"settings.wallpaper.refresh.2h": "2 小时",
"settings.wallpaper.refresh.4h": "4 小时",
"settings.wallpaper.refresh.8h": "8 小时",
"settings.wallpaper.refresh.12h": "12 小时",
"settings.wallpaper.refresh.24h": "24 小时",
"settings.wallpaper.color_label": "壁纸颜色", "settings.wallpaper.color_label": "壁纸颜色",
"settings.wallpaper.custom_color_tooltip": "自定义颜色", "settings.wallpaper.custom_color_tooltip": "自定义颜色",
"settings.wallpaper.custom_color_apply": "应用", "settings.wallpaper.custom_color_apply": "应用",
@@ -216,7 +233,14 @@
"schedule.settings.unnamed": "未命名课表", "schedule.settings.unnamed": "未命名课表",
"schedule.settings.delete": "删除", "schedule.settings.delete": "删除",
"schedule.settings.picker_title": "选择 ClassIsland 课表文件", "schedule.settings.picker_title": "选择 ClassIsland 课表文件",
"schedule.settings.picker_file_type": "ClassIsland CSES 课表", "schedule.settings.picker_file_type.all": "ClassIsland 课表文件",
"schedule.settings.picker_file_type.json": "ClassIsland 档案 (JSON)",
"schedule.settings.picker_file_type.cses": "CSES 课表 (YAML)",
"schedule.settings.semester.title": "学期设置",
"schedule.settings.semester.start_date": "学期开始日期",
"schedule.settings.semester.week_cycle": "周循环",
"schedule.settings.semester.week_cycle_desc": "设置多周课表轮换周期,用于计算当前是第几周。",
"schedule.settings.semester.week_cycle_format": "{0} 周轮换",
"worldclock.settings.title": "世界时钟设置", "worldclock.settings.title": "世界时钟设置",
"worldclock.settings.desc": "分别为四个时钟选择时区。", "worldclock.settings.desc": "分别为四个时钟选择时区。",
"worldclock.settings.clock_1": "时钟 1", "worldclock.settings.clock_1": "时钟 1",

View File

@@ -33,6 +33,8 @@ public sealed class AppSettingsSnapshot
public string WallpaperPlacement { get; set; } = "Fill"; public string WallpaperPlacement { get; set; } = "Fill";
public int SystemWallpaperRefreshIntervalSeconds { get; set; } = 300;
public int SettingsTabIndex { get; set; } = 0; public int SettingsTabIndex { get; set; } = 0;
public string? SettingsTabTag { get; set; } public string? SettingsTabTag { get; set; }

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace LanMountainDesktop.Models; namespace LanMountainDesktop.Models;
@@ -12,6 +13,10 @@ public sealed class ComponentSettingsSnapshot
public string ActiveImportedClassScheduleId { get; set; } = string.Empty; public string ActiveImportedClassScheduleId { get; set; } = string.Empty;
public DateOnly? SemesterStartDate { get; set; }
public int SemesterWeekCycle { get; set; } = 1;
public bool StudyEnvironmentShowDisplayDb { get; set; } = true; public bool StudyEnvironmentShowDisplayDb { get; set; } = true;
public bool StudyEnvironmentShowDbfs { get; set; } public bool StudyEnvironmentShowDbfs { get; set; }

View File

@@ -11,7 +11,7 @@ using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop; namespace LanMountainDesktop;
sealed class Program public sealed class Program
{ {
internal static string StartupRenderMode { get; private set; } = AppRenderingModeHelper.Default; internal static string StartupRenderMode { get; private set; } = AppRenderingModeHelper.Default;
@@ -67,7 +67,12 @@ sealed class Program
} }
} }
public static AppBuilder BuildAvaloniaApp(string renderMode = AppRenderingModeHelper.Default) public static AppBuilder BuildAvaloniaApp()
{
return BuildAvaloniaApp(AppRenderingModeHelper.Default);
}
public static AppBuilder BuildAvaloniaApp(string renderMode)
{ {
var builder = AppBuilder.Configure<App>() var builder = AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()

View File

@@ -12,7 +12,7 @@ namespace LanMountainDesktop.Services;
public interface IClassIslandScheduleDataService public interface IClassIslandScheduleDataService
{ {
ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null); ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null, DateOnly? semesterStartDate = null, int semesterWeekCycle = 1);
bool TryResolveClassPlanForDate( bool TryResolveClassPlanForDate(
ClassIslandScheduleSnapshot snapshot, ClassIslandScheduleSnapshot snapshot,
@@ -43,7 +43,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
.IgnoreUnmatchedProperties() .IgnoreUnmatchedProperties()
.Build(); .Build();
public ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null) public ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null, DateOnly? semesterStartDate = null, int semesterWeekCycle = 1)
{ {
var warnings = new List<string>(); var warnings = new List<string>();
try try
@@ -73,11 +73,11 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
ClassIslandScheduleSnapshot snapshot; ClassIslandScheduleSnapshot snapshot;
if (source.SourceKind == ScheduleSourceKind.Cses) if (source.SourceKind == ScheduleSourceKind.Cses)
{ {
snapshot = ParseCsesSnapshot(source); snapshot = ParseCsesSnapshot(source, semesterStartDate, semesterWeekCycle);
} }
else else
{ {
var cycleRule = ParseCycleRule(source.SettingsPath, warnings); var cycleRule = ParseCycleRule(source.SettingsPath, warnings, semesterStartDate, semesterWeekCycle);
var profileJson = ReadJson(source.ProfilePath); var profileJson = ReadJson(source.ProfilePath);
snapshot = ParseProfileSnapshot(profileJson.RootElement, source, cycleRule); snapshot = ParseProfileSnapshot(profileJson.RootElement, source, cycleRule);
} }
@@ -412,22 +412,50 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
return null; return null;
} }
private static ClassIslandScheduleCycleRule ParseCycleRule(string? settingsPath, List<string> warnings) private static ClassIslandScheduleCycleRule ParseCycleRule(
string? settingsPath,
List<string> warnings,
DateOnly? semesterStartDate = null,
int semesterWeekCycle = 1)
{ {
if (string.IsNullOrWhiteSpace(settingsPath) || !File.Exists(settingsPath)) DateOnly? singleWeekStartDate = semesterStartDate;
int maxCycle = semesterWeekCycle > 1 ? semesterWeekCycle : 4;
var offsetList = new List<int> { -1, -1, 0, 0, 0, 0, 0, 0 };
if (!string.IsNullOrWhiteSpace(settingsPath) && File.Exists(settingsPath))
{ {
warnings.Add("ClassIsland Settings.json not found, using default cycle rule."); using var json = ReadJson(settingsPath);
return new ClassIslandScheduleCycleRule(null, 4, new List<int> { -1, -1, 0, 0, 0 }); var root = json.RootElement;
if (!singleWeekStartDate.HasValue)
{
singleWeekStartDate = TryReadDateOnly(root, "SingleWeekStartTime");
}
if (semesterWeekCycle <= 1)
{
maxCycle = TryReadInt(root, "MultiWeekRotationMaxCycle", 4);
}
var settingsOffsetList = ReadIntList(root, "MultiWeekRotationOffset");
if (settingsOffsetList.Count >= 2)
{
offsetList = settingsOffsetList;
}
}
else
{
warnings.Add("ClassIsland Settings.json not found, using semester settings from component.");
} }
using var json = ReadJson(settingsPath); if (maxCycle < 2)
var root = json.RootElement;
var singleWeekStartDate = TryReadDateOnly(root, "SingleWeekStartTime");
var maxCycle = TryReadInt(root, "MultiWeekRotationMaxCycle", 4);
var offsetList = ReadIntList(root, "MultiWeekRotationOffset");
if (offsetList.Count < 2)
{ {
offsetList = new List<int> { -1, -1, 0, 0, 0 }; maxCycle = 2;
}
while (offsetList.Count <= maxCycle)
{
offsetList.Add(0);
} }
return new ClassIslandScheduleCycleRule( return new ClassIslandScheduleCycleRule(
@@ -469,7 +497,10 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
ClassPlanGroups: groups); ClassPlanGroups: groups);
} }
private static ClassIslandScheduleSnapshot ParseCsesSnapshot(ResolvedSource source) private static ClassIslandScheduleSnapshot ParseCsesSnapshot(
ResolvedSource source,
DateOnly? semesterStartDate = null,
int semesterWeekCycle = 1)
{ {
var yaml = File.ReadAllText(source.ProfilePath); var yaml = File.ReadAllText(source.ProfilePath);
var csesProfile = CsesDeserializer.Deserialize<CsesProfileDto>(yaml) ?? new CsesProfileDto(); var csesProfile = CsesDeserializer.Deserialize<CsesProfileDto>(yaml) ?? new CsesProfileDto();
@@ -600,12 +631,19 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
[GlobalClassPlanGroupId] = new ClassIslandClassPlanGroup(GlobalClassPlanGroupId, "Global", IsGlobal: true) [GlobalClassPlanGroupId] = new ClassIslandClassPlanGroup(GlobalClassPlanGroupId, "Global", IsGlobal: true)
}; };
var maxCycle = semesterWeekCycle > 1 ? semesterWeekCycle : 4;
var offsetList = new List<int> { -1, -1, 0, 0, 0, 0, 0, 0 };
while (offsetList.Count <= maxCycle)
{
offsetList.Add(0);
}
return new ClassIslandScheduleSnapshot( return new ClassIslandScheduleSnapshot(
SourceRootPath: source.SourceRootPath, SourceRootPath: source.SourceRootPath,
ProfilePath: source.ProfilePath, ProfilePath: source.ProfilePath,
ProfileFileName: source.ProfileFileName, ProfileFileName: source.ProfileFileName,
LoadedAt: DateTimeOffset.Now, LoadedAt: DateTimeOffset.Now,
CycleRule: new ClassIslandScheduleCycleRule(null, 4, new List<int> { -1, -1, 0, 0, 0 }), CycleRule: new ClassIslandScheduleCycleRule(semesterStartDate, Math.Clamp(maxCycle, 2, 32), offsetList),
SelectedClassPlanGroupId: DefaultClassPlanGroupId, SelectedClassPlanGroupId: DefaultClassPlanGroupId,
TempClassPlanGroupId: null, TempClassPlanGroupId: null,
IsTempClassPlanGroupEnabled: false, IsTempClassPlanGroupEnabled: false,

View File

@@ -337,6 +337,14 @@ public sealed class PostHogUsageTelemetryService : IDisposable
["timestamp"] = timestamp.ToString("o"), ["timestamp"] = timestamp.ToString("o"),
["properties"] = new Dictionary<string, object?> ["properties"] = new Dictionary<string, object?>
{ {
["install_id"] = installId,
["app_version"] = TelemetryEnvironmentInfo.GetAppVersion(),
["os_name"] = TelemetryEnvironmentInfo.GetOsName(),
["os_version"] = TelemetryEnvironmentInfo.GetOsVersion(),
["device_model"] = TelemetryEnvironmentInfo.GetDeviceModel(),
["device_arch"] = TelemetryEnvironmentInfo.GetDeviceArchitecture(),
["runtime_version"] = TelemetryEnvironmentInfo.GetRuntimeVersion(),
["language"] = TelemetryEnvironmentInfo.GetSystemLanguage(),
["launch_time_utc"] = timestamp.ToString("o") ["launch_time_utc"] = timestamp.ToString("o")
} }
}; };

View File

@@ -16,7 +16,13 @@ public enum WallpaperMediaType
} }
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent); public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
public sealed record WallpaperSettingsState(string? WallpaperPath, string Type, string? Color, string Placement, string? CustomColor = null); public sealed record WallpaperSettingsState(
string? WallpaperPath,
string Type,
string? Color,
string Placement,
string? CustomColor = null,
int SystemWallpaperRefreshIntervalSeconds = 300);
public sealed record ThemeAppearanceSettingsState( public sealed record ThemeAppearanceSettingsState(
bool IsNightMode, bool IsNightMode,
string? ThemeColor, string? ThemeColor,

View File

@@ -101,7 +101,9 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
: snapshot.WallpaperPath, : snapshot.WallpaperPath,
normalizedType, normalizedType,
snapshot.WallpaperColor, snapshot.WallpaperColor,
snapshot.WallpaperPlacement); snapshot.WallpaperPlacement,
CustomColor: null,
SystemWallpaperRefreshIntervalSeconds: NormalizeRefreshInterval(snapshot.SystemWallpaperRefreshIntervalSeconds));
} }
public void Save(WallpaperSettingsState state) public void Save(WallpaperSettingsState state)
@@ -128,6 +130,7 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement) snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
? "Fill" ? "Fill"
: state.Placement.Trim(); : state.Placement.Trim();
snapshot.SystemWallpaperRefreshIntervalSeconds = NormalizeRefreshInterval(state.SystemWallpaperRefreshIntervalSeconds);
_settingsService.SaveSnapshot( _settingsService.SaveSnapshot(
SettingsScope.App, SettingsScope.App,
snapshot, snapshot,
@@ -136,9 +139,21 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
nameof(AppSettingsSnapshot.WallpaperPath), nameof(AppSettingsSnapshot.WallpaperPath),
nameof(AppSettingsSnapshot.WallpaperType), nameof(AppSettingsSnapshot.WallpaperType),
nameof(AppSettingsSnapshot.WallpaperColor), nameof(AppSettingsSnapshot.WallpaperColor),
nameof(AppSettingsSnapshot.WallpaperPlacement) nameof(AppSettingsSnapshot.WallpaperPlacement),
nameof(AppSettingsSnapshot.SystemWallpaperRefreshIntervalSeconds)
]); ]);
} }
private static int NormalizeRefreshInterval(int seconds)
{
return seconds switch
{
<= 0 => 300,
< 30 => 30,
> 86400 => 86400,
_ => seconds
};
}
} }
internal sealed class WallpaperMediaService : IWallpaperMediaService internal sealed class WallpaperMediaService : IWallpaperMediaService

View File

@@ -0,0 +1,65 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Avalonia.Media.Imaging;
using Microsoft.Win32;
namespace LanMountainDesktop.Services;
public interface ISystemWallpaperProvider
{
bool IsSupported { get; }
string? GetWallpaperPath();
event EventHandler? WallpaperChanged;
}
internal sealed class SystemWallpaperProvider : ISystemWallpaperProvider, IDisposable
{
public bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public event EventHandler? WallpaperChanged;
public string? GetWallpaperPath()
{
if (!IsSupported)
{
return null;
}
try
{
using var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Desktop");
var wallpaperPath = key?.GetValue("Wallpaper") as string;
if (string.IsNullOrWhiteSpace(wallpaperPath))
{
return null;
}
if (!File.Exists(wallpaperPath))
{
return null;
}
return wallpaperPath;
}
catch
{
return null;
}
}
public void Dispose()
{
}
}
public static class HostSystemWallpaperProvider
{
private static ISystemWallpaperProvider? _instance;
public static ISystemWallpaperProvider GetOrCreate()
{
return _instance ??= new SystemWallpaperProvider();
}
}

View File

@@ -78,25 +78,6 @@ public sealed class TelemetryIdentityService
} }
} }
public string RefreshTelemetryId()
{
lock (_syncRoot)
{
EnsureInitialized();
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
snapshot.TelemetryId = GenerateId();
_settingsFacade.Settings.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys: [nameof(AppSettingsSnapshot.TelemetryId)]);
_telemetryId = snapshot.TelemetryId ?? GenerateId();
AppLogger.Info("TelemetryIdentity", $"Telemetry id refreshed. TelemetryId={_telemetryId}");
return _telemetryId;
}
}
public bool MarkBaselineReported() public bool MarkBaselineReported()
{ {
lock (_syncRoot) lock (_syncRoot)

View File

@@ -5,13 +5,13 @@ using Avalonia.Media.Imaging;
namespace LanMountainDesktop.Services; namespace LanMountainDesktop.Services;
internal static class WallpaperImageBrushFactory public static class WallpaperImageBrushFactory
{ {
internal const string Fill = "Fill"; public const string Fill = "Fill";
internal const string Fit = "Fit"; public const string Fit = "Fit";
internal const string StretchMode = "Stretch"; public const string StretchMode = "Stretch";
internal const string Center = "Center"; public const string Center = "Center";
internal const string Tile = "Tile"; public const string Tile = "Tile";
public static string NormalizePlacement(string? placement) public static string NormalizePlacement(string? placement)
{ {

View File

@@ -58,9 +58,6 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string _telemetryIdDescription = string.Empty; private string _telemetryIdDescription = string.Empty;
[ObservableProperty]
private string _refreshTelemetryIdText = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _viewPrivacyPolicyText = string.Empty; private string _viewPrivacyPolicyText = string.Empty;
@@ -75,27 +72,6 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
TelemetryId = TelemetryServices.Identity?.TelemetryId ?? string.Empty; TelemetryId = TelemetryServices.Identity?.TelemetryId ?? string.Empty;
} }
[RelayCommand]
private void RefreshTelemetryId()
{
try
{
var identity = TelemetryServices.Identity;
if (identity is null)
{
AppLogger.Warn("PrivacySettings", "Telemetry identity service is unavailable.");
return;
}
TelemetryId = identity.RefreshTelemetryId();
AppLogger.Info("PrivacySettings", $"Telemetry ID refreshed: {TelemetryId}");
}
catch (Exception ex)
{
AppLogger.Warn("PrivacySettings", "Failed to refresh telemetry ID.", ex);
}
}
partial void OnUploadAnonymousCrashDataChanged(bool value) partial void OnUploadAnonymousCrashDataChanged(bool value)
{ {
if (_isInitializing) if (_isInitializing)
@@ -137,8 +113,7 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
TelemetryIdHeader = L("settings.privacy.telemetry_id_title", "Telemetry ID"); TelemetryIdHeader = L("settings.privacy.telemetry_id_title", "Telemetry ID");
TelemetryIdDescription = L( TelemetryIdDescription = L(
"settings.privacy.telemetry_id_description", "settings.privacy.telemetry_id_description",
"A refreshable anonymous identifier used for detailed telemetry sessions."); "An anonymous identifier used for detailed telemetry sessions.");
RefreshTelemetryIdText = L("settings.privacy.refresh_telemetry_id", "Refresh");
PrivacyPolicyHintPrefix = L("settings.privacy.policy_hint_prefix", "For more details, please "); PrivacyPolicyHintPrefix = L("settings.privacy.policy_hint_prefix", "For more details, please ");
ViewPrivacyPolicyText = L("settings.privacy.view_policy", "view our privacy policy"); ViewPrivacyPolicyText = L("settings.privacy.view_policy", "view our privacy policy");
} }

View File

@@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
@@ -15,6 +16,7 @@ namespace LanMountainDesktop.ViewModels;
public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
{ {
private readonly ISettingsFacadeService _settingsFacade; private readonly ISettingsFacadeService _settingsFacade;
private readonly ISystemWallpaperProvider _systemWallpaperProvider;
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly string _languageCode; private readonly string _languageCode;
private bool _isInitializing; private bool _isInitializing;
@@ -22,9 +24,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
public WallpaperSettingsPageViewModel(ISettingsFacadeService settingsFacade) public WallpaperSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{ {
_settingsFacade = settingsFacade; _settingsFacade = settingsFacade;
_systemWallpaperProvider = HostSystemWallpaperProvider.GetOrCreate();
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode); _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
WallpaperPlacements = CreateWallpaperPlacements(); WallpaperPlacements = CreateWallpaperPlacements();
WallpaperTypes = CreateWallpaperTypes(); WallpaperTypes = CreateWallpaperTypes();
RefreshIntervals = CreateRefreshIntervals();
PresetColors = CreatePresetColors(); PresetColors = CreatePresetColors();
RefreshLocalizedText(); RefreshLocalizedText();
@@ -35,8 +39,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
public IReadOnlyList<SelectionOption> WallpaperPlacements { get; } public IReadOnlyList<SelectionOption> WallpaperPlacements { get; }
public IReadOnlyList<SelectionOption> WallpaperTypes { get; } public IReadOnlyList<SelectionOption> WallpaperTypes { get; }
public IReadOnlyList<SelectionOption> RefreshIntervals { get; }
public IReadOnlyList<string> PresetColors { get; } public IReadOnlyList<string> PresetColors { get; }
public bool IsSystemWallpaperSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
[ObservableProperty] [ObservableProperty]
private string _wallpaperPath = string.Empty; private string _wallpaperPath = string.Empty;
@@ -49,6 +56,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private SelectionOption _selectedWallpaperPlacement = null!; private SelectionOption _selectedWallpaperPlacement = null!;
[ObservableProperty]
private SelectionOption _selectedRefreshInterval = null!;
[ObservableProperty] [ObservableProperty]
private string _wallpaperHeader = string.Empty; private string _wallpaperHeader = string.Empty;
@@ -73,6 +83,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string _filePickerTitle = string.Empty; private string _filePickerTitle = string.Empty;
[ObservableProperty]
private string _systemWallpaperLabel = string.Empty;
[ObservableProperty]
private string _refreshIntervalLabel = string.Empty;
[ObservableProperty]
private string _refreshButtonTooltip = string.Empty;
[ObservableProperty]
private string _systemWallpaperStatus = string.Empty;
[ObservableProperty] [ObservableProperty]
private bool _isImageOrVideo; private bool _isImageOrVideo;
@@ -82,13 +104,15 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private bool _isImage; private bool _isImage;
[ObservableProperty]
private bool _isSystemWallpaper;
[ObservableProperty] [ObservableProperty]
private Bitmap? _previewImage; private Bitmap? _previewImage;
[ObservableProperty] [ObservableProperty]
private IBrush? _previewBrush; private IBrush? _previewBrush;
// 自定义颜色持久化
[ObservableProperty] [ObservableProperty]
private Color _customColor = Colors.White; private Color _customColor = Colors.White;
@@ -110,7 +134,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase)) string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
?? WallpaperPlacements[0]; ?? WallpaperPlacements[0];
// 加载自定义颜色 var refreshIntervalSeconds = wallpaper.SystemWallpaperRefreshIntervalSeconds;
SelectedRefreshInterval = RefreshIntervals.FirstOrDefault(option =>
GetIntervalSeconds(option.Value) == refreshIntervalSeconds)
?? RefreshIntervals[2];
if (!string.IsNullOrWhiteSpace(wallpaper.CustomColor) && Color.TryParse(wallpaper.CustomColor, out var customColor)) if (!string.IsNullOrWhiteSpace(wallpaper.CustomColor) && Color.TryParse(wallpaper.CustomColor, out var customColor))
{ {
CustomColor = customColor; CustomColor = customColor;
@@ -119,6 +147,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
UpdateVisibility(); UpdateVisibility();
UpdatePreviewFromCurrentSelection(); UpdatePreviewFromCurrentSelection();
UpdateSystemWallpaperStatus();
} }
partial void OnSelectedWallpaperTypeChanged(SelectionOption value) partial void OnSelectedWallpaperTypeChanged(SelectionOption value)
@@ -132,8 +161,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private void UpdateVisibility() private void UpdateVisibility()
{ {
IsImage = SelectedWallpaperType?.Value == "Image"; IsImage = SelectedWallpaperType?.Value == "Image";
IsImageOrVideo = IsImage; IsImageOrVideo = IsImage || SelectedWallpaperType?.Value == "SystemWallpaper";
IsSolidColor = SelectedWallpaperType?.Value == "SolidColor"; IsSolidColor = SelectedWallpaperType?.Value == "SolidColor";
IsSystemWallpaper = SelectedWallpaperType?.Value == "SystemWallpaper";
} }
partial void OnSelectedColorChanged(string? value) partial void OnSelectedColorChanged(string? value)
@@ -145,13 +175,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
partial void OnCustomColorChanged(Color value) partial void OnCustomColorChanged(Color value)
{ {
CustomColorBrush = new SolidColorBrush(value); CustomColorBrush = new SolidColorBrush(value);
// 将自定义颜色应用到壁纸
var colorHex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}"; var colorHex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}";
SelectedColor = colorHex; SelectedColor = colorHex;
if (_isInitializing) return; if (_isInitializing) return;
SaveWallpaper(); SaveWallpaper();
} }
partial void OnSelectedRefreshIntervalChanged(SelectionOption value)
{
if (_isInitializing) return;
SaveWallpaper();
}
public async Task ImportWallpaperAsync(string sourcePath) public async Task ImportWallpaperAsync(string sourcePath)
{ {
var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath); var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
@@ -170,6 +205,12 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private void UpdatePreviewFromCurrentSelection() private void UpdatePreviewFromCurrentSelection()
{ {
if (IsSystemWallpaper)
{
UpdateSystemWallpaperPreview();
return;
}
if (!IsImage) if (!IsImage)
{ {
ClearPreviewImage(); ClearPreviewImage();
@@ -180,10 +221,24 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
UpdatePreviewImage(WallpaperPath); UpdatePreviewImage(WallpaperPath);
} }
private void UpdatePreviewImage(string path) private void UpdateSystemWallpaperPreview()
{
var systemPath = _systemWallpaperProvider.GetWallpaperPath();
if (string.IsNullOrWhiteSpace(systemPath))
{
ClearPreviewImage();
SystemWallpaperStatus = L("settings.wallpaper.system.unavailable", "Unable to read system wallpaper");
return;
}
SystemWallpaperStatus = systemPath;
UpdatePreviewImage(systemPath);
}
private void UpdatePreviewImage(string? path)
{ {
var previousPreview = PreviewImage; var previousPreview = PreviewImage;
if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path)) if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{ {
previousPreview?.Dispose(); previousPreview?.Dispose();
PreviewImage = null; PreviewImage = null;
@@ -193,7 +248,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
try try
{ {
using var stream = System.IO.File.OpenRead(path); using var stream = File.OpenRead(path);
var bitmap = new Bitmap(stream); var bitmap = new Bitmap(stream);
PreviewImage = bitmap; PreviewImage = bitmap;
PreviewBrush = WallpaperImageBrushFactory.Create(bitmap, SelectedWallpaperPlacement?.Value); PreviewBrush = WallpaperImageBrushFactory.Create(bitmap, SelectedWallpaperPlacement?.Value);
@@ -215,9 +270,21 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
previousPreview?.Dispose(); previousPreview?.Dispose();
} }
private void UpdateSystemWallpaperStatus()
{
if (!IsSystemWallpaper) return;
UpdateSystemWallpaperPreview();
}
[RelayCommand]
private void RefreshSystemWallpaper()
{
UpdateSystemWallpaperPreview();
}
partial void OnSelectedWallpaperPlacementChanged(SelectionOption value) partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
{ {
if (IsImage && PreviewImage is not null) if ((IsImage || IsSystemWallpaper) && PreviewImage is not null)
{ {
PreviewBrush = WallpaperImageBrushFactory.Create(PreviewImage, value?.Value); PreviewBrush = WallpaperImageBrushFactory.Create(PreviewImage, value?.Value);
} }
@@ -236,16 +303,46 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
{ {
var selectedType = SelectedWallpaperType?.Value ?? "Image"; var selectedType = SelectedWallpaperType?.Value ?? "Image";
var selectedPlacement = SelectedWallpaperPlacement?.Value ?? WallpaperImageBrushFactory.Fill; var selectedPlacement = SelectedWallpaperPlacement?.Value ?? WallpaperImageBrushFactory.Fill;
var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath) var refreshIntervalSeconds = GetIntervalSeconds(SelectedRefreshInterval?.Value);
? null
: WallpaperPath; string? normalizedPath;
if (selectedType == "SolidColor" || selectedType == "SystemWallpaper")
{
normalizedPath = null;
}
else
{
normalizedPath = string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath;
}
var customColorHex = $"#{CustomColor.A:X2}{CustomColor.R:X2}{CustomColor.G:X2}{CustomColor.B:X2}"; var customColorHex = $"#{CustomColor.A:X2}{CustomColor.R:X2}{CustomColor.G:X2}{CustomColor.B:X2}";
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState( _settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
normalizedPath, normalizedPath,
selectedType, selectedType,
SelectedColor, SelectedColor,
selectedPlacement, selectedPlacement,
customColorHex)); customColorHex,
refreshIntervalSeconds));
}
private static int GetIntervalSeconds(string? value)
{
return value switch
{
"30s" => 30,
"1m" => 60,
"5m" => 300,
"10m" => 600,
"15m" => 900,
"30m" => 1800,
"1h" => 3600,
"2h" => 7200,
"4h" => 14400,
"8h" => 28800,
"12h" => 43200,
"24h" => 86400,
_ => 300
};
} }
private IReadOnlyList<SelectionOption> CreateWallpaperPlacements() private IReadOnlyList<SelectionOption> CreateWallpaperPlacements()
@@ -262,10 +359,36 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private IReadOnlyList<SelectionOption> CreateWallpaperTypes() private IReadOnlyList<SelectionOption> CreateWallpaperTypes()
{ {
return var types = new List<SelectionOption>
[ {
new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")), new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")),
new SelectionOption("SolidColor", L("settings.wallpaper.type.solid_color", "Solid Color")) new SelectionOption("SolidColor", L("settings.wallpaper.type.solid_color", "Solid Color"))
};
if (IsSystemWallpaperSupported)
{
types.Add(new SelectionOption("SystemWallpaper", L("settings.wallpaper.type.system", "System Wallpaper")));
}
return types;
}
private IReadOnlyList<SelectionOption> CreateRefreshIntervals()
{
return
[
new SelectionOption("30s", L("settings.wallpaper.refresh.30s", "30 seconds")),
new SelectionOption("1m", L("settings.wallpaper.refresh.1m", "1 minute")),
new SelectionOption("5m", L("settings.wallpaper.refresh.5m", "5 minutes")),
new SelectionOption("10m", L("settings.wallpaper.refresh.10m", "10 minutes")),
new SelectionOption("15m", L("settings.wallpaper.refresh.15m", "15 minutes")),
new SelectionOption("30m", L("settings.wallpaper.refresh.30m", "30 minutes")),
new SelectionOption("1h", L("settings.wallpaper.refresh.1h", "1 hour")),
new SelectionOption("2h", L("settings.wallpaper.refresh.2h", "2 hours")),
new SelectionOption("4h", L("settings.wallpaper.refresh.4h", "4 hours")),
new SelectionOption("8h", L("settings.wallpaper.refresh.8h", "8 hours")),
new SelectionOption("12h", L("settings.wallpaper.refresh.12h", "12 hours")),
new SelectionOption("24h", L("settings.wallpaper.refresh.24h", "24 hours"))
]; ];
} }
@@ -289,6 +412,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop."); WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop.");
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper"); ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
FilePickerTitle = L("filepicker.title", "Select wallpaper"); FilePickerTitle = L("filepicker.title", "Select wallpaper");
SystemWallpaperLabel = L("settings.wallpaper.system.label", "System Wallpaper");
RefreshIntervalLabel = L("settings.wallpaper.refresh_interval", "Refresh Interval");
RefreshButtonTooltip = L("settings.wallpaper.refresh_now", "Refresh Now");
} }
private string L(string key, string fallback) private string L(string key, string fallback)

View File

@@ -27,7 +27,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
ISettingsFacadeService settingsFacade, ISettingsFacadeService settingsFacade,
LocalizationService localizationService, LocalizationService localizationService,
ILocationService locationService, ILocationService locationService,
WeatherLocationRefreshService weatherLocationRefreshService) WeatherLocationRefreshService weatherLocationRefreshService,
bool enableStartupPreviewRefresh = true)
{ {
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade)); _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
@@ -52,7 +53,10 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
? LocationReadyText ? LocationReadyText
: LocationUnsupportedText; : LocationUnsupportedText;
_ = RefreshPreviewAsync(); if (enableStartupPreviewRefresh)
{
_ = RefreshPreviewAsync();
}
} }
public IReadOnlyList<SelectionOption> LocationModes { get; } public IReadOnlyList<SelectionOption> LocationModes { get; }
@@ -476,6 +480,65 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
} }
} }
internal void ApplyDesignTimePreview()
{
_isInitializing = true;
var previewLocation = new WeatherLocation(
"Shenzhen Nanshan",
"101280601",
22.5431,
114.0579,
"Guangdong, China");
var alternateLocation = new WeatherLocation(
"Shanghai Pudong",
"101020600",
31.2304,
121.4737,
"Shanghai, China");
SelectedLocationMode = LocationModes.FirstOrDefault(option =>
string.Equals(option.Value, "CitySearch", StringComparison.OrdinalIgnoreCase))
?? LocationModes[0];
SearchKeyword = "shenzhen";
SelectedSearchResult = previewLocation;
SearchResults.Clear();
SearchResults.Add(previewLocation);
SearchResults.Add(alternateLocation);
Latitude = previewLocation.Latitude;
Longitude = previewLocation.Longitude;
LocationKey = previewLocation.LocationKey;
LocationName = previewLocation.Name;
AutoRefreshLocation = true;
ExcludedAlerts = "Heat\nThunderstorm";
NoTlsRequests = false;
IsLocationSupported = true;
IsRefreshingLocation = false;
IsRefreshingPreview = false;
_isInitializing = false;
UpdateModeVisibility();
UpdateCurrentLocationSummary();
var preview = XiaomiWeatherVisualResolver.Resolve(
"Partly cloudy",
4,
isNight: false,
_languageCode);
SearchStatus = "2 sample locations are shown for design preview.";
LocationActionStatus = "Using mocked Windows location support in design mode.";
PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(preview.PrimaryIconAsset);
PreviewLocation = previewLocation.Name;
PreviewTemperature = "24 deg C";
PreviewCondition = preview.DisplayText;
PreviewUpdated = "Updated 09:42";
PreviewStatus = "Preview data is mocked for Avalonia design mode.";
}
private void RefreshLocalizedText() private void RefreshLocalizedText()
{ {
PageTitle = L("settings.weather.title", "Weather"); PageTitle = L("settings.weather.title", "Weather");

View File

@@ -2,6 +2,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:material="clr-namespace:Material.Styles;assembly=Material.Styles"
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
mc:Ignorable="d" mc:Ignorable="d"
x:Class="LanMountainDesktop.Views.ComponentEditors.ClassScheduleComponentEditor"> x:Class="LanMountainDesktop.Views.ComponentEditors.ClassScheduleComponentEditor">
<StackPanel Spacing="16"> <StackPanel Spacing="16">
@@ -36,6 +38,45 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border Classes="component-editor-card"
Padding="20">
<StackPanel Spacing="16">
<TextBlock x:Name="SemesterSettingsHeaderTextBlock"
Classes="component-editor-section-title" />
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="16">
<StackPanel Spacing="8">
<TextBlock x:Name="SemesterStartDateLabel"
Classes="component-editor-secondary-text" />
<CalendarDatePicker x:Name="SemesterStartDatePicker"
HorizontalAlignment="Stretch"
SelectedDateChanged="OnSemesterStartDateChanged" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8" Width="160">
<TextBlock x:Name="WeekCycleLabel"
Classes="component-editor-secondary-text" />
<ComboBox x:Name="WeekCycleComboBox"
Classes="component-editor-select"
HorizontalAlignment="Stretch"
SelectionChanged="OnWeekCycleSelectionChanged">
<ComboBoxItem Content="1" Tag="1" />
<ComboBoxItem Content="2" Tag="2" />
<ComboBoxItem Content="3" Tag="3" />
<ComboBoxItem Content="4" Tag="4" />
<ComboBoxItem Content="5" Tag="5" />
<ComboBoxItem Content="6" Tag="6" />
<ComboBoxItem Content="7" Tag="7" />
</ComboBox>
</StackPanel>
</Grid>
<TextBlock x:Name="WeekCycleDescription"
Classes="component-editor-secondary-text"
TextWrapping="Wrap"
Opacity="0.7" />
</StackPanel>
</Border>
<Border Classes="component-editor-card" <Border Classes="component-editor-card"
Padding="20"> Padding="20">
<StackPanel Spacing="12"> <StackPanel Spacing="12">

View File

@@ -76,6 +76,11 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
FollowSystemColorSchemeItem.Content = L("component.color_scheme.follow_system", "Follow system color scheme"); FollowSystemColorSchemeItem.Content = L("component.color_scheme.follow_system", "Follow system color scheme");
UseNativeColorSchemeItem.Content = L("component.color_scheme.native", "Use component custom color scheme"); UseNativeColorSchemeItem.Content = L("component.color_scheme.native", "Use component custom color scheme");
SemesterSettingsHeaderTextBlock.Text = L("schedule.settings.semester.title", "Semester Settings");
SemesterStartDateLabel.Text = L("schedule.settings.semester.start_date", "Semester Start Date");
WeekCycleLabel.Text = L("schedule.settings.semester.week_cycle", "Week Cycle");
WeekCycleDescription.Text = L("schedule.settings.semester.week_cycle_desc", "Set the week rotation cycle for multi-week schedules (e.g., 2 for odd/even weeks).");
AddScheduleButton.Content = L("schedule.settings.add", "Add Schedule"); AddScheduleButton.Content = L("schedule.settings.add", "Add Schedule");
EmptyStateTextBlock.Text = L("schedule.settings.empty", "No imported schedules yet."); EmptyStateTextBlock.Text = L("schedule.settings.empty", "No imported schedules yet.");
@@ -85,9 +90,25 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
string.Equals(colorSchemeSource, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase) string.Equals(colorSchemeSource, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase)
? FollowSystemColorSchemeItem ? FollowSystemColorSchemeItem
: UseNativeColorSchemeItem; : UseNativeColorSchemeItem;
if (snapshot.SemesterStartDate.HasValue)
{
SemesterStartDatePicker.SelectedDate = snapshot.SemesterStartDate.Value.ToDateTime(TimeOnly.MinValue);
}
var weekCycle = Math.Clamp(snapshot.SemesterWeekCycle, 1, 7);
WeekCycleComboBox.SelectedIndex = weekCycle - 1;
UpdateWeekCycleDescription(weekCycle);
_suppressEvents = false; _suppressEvents = false;
} }
private void UpdateWeekCycleDescription(int weekCycle)
{
var format = L("schedule.settings.semester.week_cycle_format", "{0}-week rotation");
WeekCycleDescription.Text = string.Format(format, weekCycle);
}
private void OnColorSchemeSelectionChanged(object? sender, SelectionChangedEventArgs e) private void OnColorSchemeSelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
_ = sender; _ = sender;
@@ -106,6 +127,39 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource)); SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource));
} }
private void OnSemesterStartDateChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressEvents)
{
return;
}
var snapshot = LoadSnapshot();
if (SemesterStartDatePicker.SelectedDate.HasValue)
{
snapshot.SemesterStartDate = DateOnly.FromDateTime(SemesterStartDatePicker.SelectedDate.Value);
}
else
{
snapshot.SemesterStartDate = null;
}
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.SemesterStartDate));
}
private void OnWeekCycleSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressEvents)
{
return;
}
var weekCycle = WeekCycleComboBox.SelectedIndex + 1;
var snapshot = LoadSnapshot();
snapshot.SemesterWeekCycle = weekCycle;
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.SemesterWeekCycle));
UpdateWeekCycleDescription(weekCycle);
}
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e) private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
{ {
_ = sender; _ = sender;
@@ -122,7 +176,15 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
AllowMultiple = false, AllowMultiple = false,
FileTypeFilter = FileTypeFilter =
[ [
new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES Schedule")) new FilePickerFileType(L("schedule.settings.picker_file_type.all", "ClassIsland Schedule Files"))
{
Patterns = ["*.json", "*.cses", "*.yaml", "*.yml"]
},
new FilePickerFileType(L("schedule.settings.picker_file_type.json", "ClassIsland Profile (JSON)"))
{
Patterns = ["*.json"]
},
new FilePickerFileType(L("schedule.settings.picker_file_type.cses", "CSES Schedule (YAML)"))
{ {
Patterns = ["*.cses", "*.yaml", "*.yml"] Patterns = ["*.cses", "*.yaml", "*.yml"]
} }

View File

@@ -3,7 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fi="using:FluentIcons.Avalonia" xmlns:fi="using:FluentIcons.Avalonia"
xmlns:webview="clr-namespace:AvaloniaWebView;assembly=Avalonia.WebView"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="480" d:DesignWidth="480"
d:DesignHeight="480" d:DesignHeight="480"
@@ -24,20 +23,27 @@
BorderBrush="#22000000" BorderBrush="#22000000"
BorderThickness="1"> BorderThickness="1">
<Grid> <Grid>
<webview:WebView x:Name="BrowserWebView" /> <Grid x:Name="WebViewPresenter" />
<Border x:Name="UnavailableOverlay" <Border x:Name="UnavailableOverlay"
IsVisible="False" IsVisible="False"
Background="#CC0F172A" Background="#CC0F172A"
Padding="16"> Padding="16">
<TextBlock x:Name="UnavailableMessageTextBlock" <StackPanel HorizontalAlignment="Center"
Foreground="#F8FAFC" VerticalAlignment="Center"
TextWrapping="Wrap" Spacing="10"
TextAlignment="Center" MaxWidth="360">
HorizontalAlignment="Center" <fi:SymbolIcon Symbol="Desktop"
VerticalAlignment="Center" FontSize="28"
MaxWidth="360" HorizontalAlignment="Center"
FontSize="13" Foreground="#F8FAFC" />
Text="Browser runtime unavailable." /> <TextBlock x:Name="UnavailableMessageTextBlock"
Foreground="#F8FAFC"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
FontSize="13"
Text="Browser runtime unavailable." />
</StackPanel>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>

View File

@@ -17,6 +17,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
{ {
private static readonly Uri DefaultHomeUri = new("https://www.bing.com"); private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private double _currentCellSize = 48; private double _currentCellSize = 48;
private string _componentId = BuiltInComponentIds.DesktopBrowser; private string _componentId = BuiltInComponentIds.DesktopBrowser;
private string _placementId = string.Empty; private string _placementId = string.Empty;
@@ -27,6 +28,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool _isEditMode; private bool _isEditMode;
private bool _isWebViewActive = true; private bool _isWebViewActive = true;
private bool _isWebViewFaulted; private bool _isWebViewFaulted;
private WebView? _browserWebView;
private readonly WebView2RuntimeAvailability _runtimeAvailability; private readonly WebView2RuntimeAvailability _runtimeAvailability;
private bool _isDisposed; private bool _isDisposed;
@@ -41,10 +43,15 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
ApplyTheme(force: true); ApplyTheme(force: true);
_runtimeAvailability = WebView2RuntimeProbe.GetAvailability(); _runtimeAvailability = _isDesignModePreview
? new WebView2RuntimeAvailability(
IsAvailable: false,
Version: null,
Message: "WebView preview is disabled in Avalonia design mode.")
: WebView2RuntimeProbe.GetAvailability();
if (_runtimeAvailability.IsAvailable) if (_runtimeAvailability.IsAvailable)
{ {
BrowserWebView.NavigationStarting += OnBrowserWebViewNavigationStarting; EnsureWebViewCreated();
} }
else else
{ {
@@ -69,9 +76,9 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
AttachedToVisualTree -= OnAttachedToVisualTree; AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree; DetachedFromVisualTree -= OnDetachedFromVisualTree;
if (_runtimeAvailability.IsAvailable) if (_browserWebView is not null)
{ {
BrowserWebView.NavigationStarting -= OnBrowserWebViewNavigationStarting; _browserWebView.NavigationStarting -= OnBrowserWebViewNavigationStarting;
} }
} }
@@ -300,6 +307,13 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void UpdateWebViewActiveState() private void UpdateWebViewActiveState()
{ {
if (_isDesignModePreview)
{
_isWebViewActive = false;
ApplyRuntimeUnavailableState();
return;
}
if (!_runtimeAvailability.IsAvailable || _isWebViewFaulted) if (!_runtimeAvailability.IsAvailable || _isWebViewFaulted)
{ {
_isWebViewActive = false; _isWebViewActive = false;
@@ -325,14 +339,21 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void ActivateWebView() private void ActivateWebView()
{ {
EnsureWebViewCreated();
if (_isWebViewFaulted || !_runtimeAvailability.IsAvailable) if (_isWebViewFaulted || !_runtimeAvailability.IsAvailable)
{ {
ApplyRuntimeUnavailableState(); ApplyRuntimeUnavailableState();
return; return;
} }
BrowserWebView.IsVisible = true; if (_browserWebView is null)
BrowserWebView.IsHitTestVisible = true; {
ApplyRuntimeUnavailableState();
return;
}
_browserWebView.IsVisible = true;
_browserWebView.IsHitTestVisible = true;
RefreshButton.IsEnabled = true; RefreshButton.IsEnabled = true;
GoButton.IsEnabled = true; GoButton.IsEnabled = true;
AddressTextBox.IsEnabled = true; AddressTextBox.IsEnabled = true;
@@ -341,8 +362,11 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void DeactivateWebView(bool clearUrl) private void DeactivateWebView(bool clearUrl)
{ {
BrowserWebView.IsHitTestVisible = false; if (_browserWebView is not null)
BrowserWebView.IsVisible = false; {
_browserWebView.IsHitTestVisible = false;
_browserWebView.IsVisible = false;
}
if (clearUrl) if (clearUrl)
{ {
@@ -352,9 +376,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool TryReloadWebView(string action) private bool TryReloadWebView(string action)
{ {
if (_browserWebView is null)
{
return false;
}
try try
{ {
BrowserWebView.Reload(); _browserWebView.Reload();
return true; return true;
} }
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex)) catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
@@ -366,9 +395,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool TryNavigate(Uri uri, string action) private bool TryNavigate(Uri uri, string action)
{ {
if (_browserWebView is null)
{
return false;
}
try try
{ {
BrowserWebView.Url = uri; _browserWebView.Url = uri;
return true; return true;
} }
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex)) catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
@@ -380,9 +414,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void TryClearWebViewUrl() private void TryClearWebViewUrl()
{ {
if (_browserWebView is null)
{
return;
}
try try
{ {
BrowserWebView.Url = null; _browserWebView.Url = null;
} }
catch catch
{ {
@@ -392,14 +431,20 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool CanUseWebView() private bool CanUseWebView()
{ {
return _runtimeAvailability.IsAvailable && !_isWebViewFaulted && _isWebViewActive; return _runtimeAvailability.IsAvailable &&
!_isWebViewFaulted &&
_isWebViewActive &&
_browserWebView is not null;
} }
private void ApplyRuntimeUnavailableState() private void ApplyRuntimeUnavailableState()
{ {
_isWebViewActive = false; _isWebViewActive = false;
BrowserWebView.IsVisible = false; if (_browserWebView is not null)
BrowserWebView.IsHitTestVisible = false; {
_browserWebView.IsVisible = false;
_browserWebView.IsHitTestVisible = false;
}
RefreshButton.IsEnabled = false; RefreshButton.IsEnabled = false;
GoButton.IsEnabled = false; GoButton.IsEnabled = false;
@@ -414,6 +459,22 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
UnavailableOverlay.IsVisible = true; UnavailableOverlay.IsVisible = true;
} }
private void EnsureWebViewCreated()
{
if (_browserWebView is not null || _isDesignModePreview || !_runtimeAvailability.IsAvailable)
{
return;
}
_browserWebView = new WebView
{
IsVisible = false,
IsHitTestVisible = false
};
_browserWebView.NavigationStarting += OnBrowserWebViewNavigationStarting;
WebViewPresenter.Children.Insert(0, _browserWebView);
}
private void EnterFaultedState(string action, Exception ex) private void EnterFaultedState(string action, Exception ex)
{ {
_isWebViewFaulted = true; _isWebViewFaulted = true;

View File

@@ -253,7 +253,11 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
var today = DateOnly.FromDateTime(now); var today = DateOnly.FromDateTime(now);
var importedSchedulePath = ResolveImportedSchedulePath(componentSettings); var importedSchedulePath = ResolveImportedSchedulePath(componentSettings);
var readResult = _scheduleService.Load(importedSchedulePath); var readResult = _scheduleService.Load(
importedSchedulePath,
profileFileName: null,
semesterStartDate: componentSettings.SemesterStartDate,
semesterWeekCycle: componentSettings.SemesterWeekCycle);
if (!readResult.Success || readResult.Snapshot is null) if (!readResult.Success || readResult.Snapshot is null)
{ {
_courseItems = Array.Empty<CourseItemViewModel>(); _courseItems = Array.Empty<CourseItemViewModel>();

View File

@@ -44,6 +44,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromMinutes(30) Interval = TimeSpan.FromMinutes(30)
}; };
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings; private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate(); private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
@@ -102,12 +103,19 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
News2TitleTextBlock.FontFamily = MiSansFontFamily; News2TitleTextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily; StatusTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick; _refreshTimer.Tick += OnRefreshTimerTick;
RefreshButton.Click += OnRefreshButtonClick; RefreshButton.Click += OnRefreshButtonClick;
AttachedToVisualTree += OnAttachedToVisualTree; AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
UpdateLanguageCode(); UpdateLanguageCode();
@@ -226,6 +234,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e) private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{ {
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (_isRefreshing) if (_isRefreshing)
{ {
return; return;
@@ -242,6 +256,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnNewsItem1PointerPressed(object? sender, PointerPressedEventArgs e) private void OnNewsItem1PointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
return; return;
@@ -253,6 +273,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnNewsItem2PointerPressed(object? sender, PointerPressedEventArgs e) private void OnNewsItem2PointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
return; return;
@@ -264,6 +290,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnExtraNewsItemPointerPressed(object? sender, PointerPressedEventArgs e) private void OnExtraNewsItemPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed ||
sender is not Control control || sender is not Control control ||
control.Tag is not int index) control.Tag is not int index)
@@ -408,6 +440,55 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
UpdateAdaptiveLayout(); UpdateAdaptiveLayout();
} }
private void ApplyDesignTimePreview()
{
_isNightVisual = ResolveNightMode();
_activeNewsItems =
[
new DailyNewsItemSnapshot(
"LanMountain preview mode now shows mocked widget content in Rider.",
null,
"https://example.com/news/preview-1",
null,
"09:30"),
new DailyNewsItemSnapshot(
"Weather, artwork, and plugin market cards render without live network calls.",
null,
"https://example.com/news/preview-2",
null,
"09:10"),
new DailyNewsItemSnapshot(
"Design-time mocks make isolated widget layout tuning much faster.",
null,
"https://example.com/news/preview-3",
null,
"08:55")
];
_newsUrls.Clear();
foreach (var item in _activeNewsItems)
{
_newsUrls.Add(item.Url);
}
UpdateHotHeadlineText(_activeNewsItems[0].Title);
News2TitleTextBlock.Text = NormalizeCompactText(_activeNewsItems[1].Title);
StatusTextBlock.Text = string.Empty;
StatusTextBlock.IsVisible = false;
SetNewsBitmap(0, null);
SetNewsBitmap(1, null);
RenderExtraNewsRows(_activeNewsItems.Skip(2).ToArray());
UpdateNewsInteractionState();
RefreshButton.IsEnabled = false;
RefreshButton.Opacity = 1.0;
RefreshGlyphIcon.Opacity = 0.82;
RefreshLabelTextBlock.Opacity = 0.82;
UpdateAdaptiveLayout();
}
private int ResolveDesiredNewsItemCount() private int ResolveDesiredNewsItemCount()
{ {
return 2; return 2;

View File

@@ -60,6 +60,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromHours(6) Interval = TimeSpan.FromHours(6)
}; };
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
@@ -85,10 +86,17 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
ArtistTextBlock.FontFamily = MiSansFontFamily; ArtistTextBlock.FontFamily = MiSansFontFamily;
YearTextBlock.FontFamily = MiSansFontFamily; YearTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick; _refreshTimer.Tick += OnRefreshTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree; AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
UpdateLanguageCode(); UpdateLanguageCode();
@@ -177,6 +185,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
private void OnArtworkPanelPointerPressed(object? sender, PointerPressedEventArgs e) private void OnArtworkPanelPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (_isDesignModePreview)
{
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
return; return;
@@ -188,6 +201,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
private void OnInfoPanelPointerPressed(object? sender, PointerPressedEventArgs e) private void OnInfoPanelPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (_isDesignModePreview)
{
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
return; return;
@@ -420,6 +438,36 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
UpdateAdaptiveLayout(); UpdateAdaptiveLayout();
} }
private void ApplyDesignTimePreview()
{
DisposeArtworkBitmap();
_currentArtworkSourceUrl = null;
_currentArtworkImageUrl = null;
RootBorder.Background = new SolidColorBrush(Color.Parse("#C6B08B"));
ArtworkPanel.Background = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops = new GradientStops
{
new GradientStop(Color.Parse("#AA8B69"), 0),
new GradientStop(Color.Parse("#5F4B3B"), 1)
}
};
InfoPanel.Background = new SolidColorBrush(Color.Parse("#15181D"));
DateTextBlock.Text = "03/22";
WeekdayTextBlock.Text = "Sunday";
PaintingTitleTextBlock.Text = BuildQuotedTitle("The Starry Night");
ArtistTextBlock.Text = NormalizeCompactText("Vincent van Gogh");
YearTextBlock.Text = "1889 | MoMA";
StatusTextBlock.IsVisible = false;
StatusTextBlock.Text = string.Empty;
UpdateAdaptiveLayout();
}
private void UpdateAdaptiveLayout() private void UpdateAdaptiveLayout()
{ {
var scale = ResolveScale(); var scale = ResolveScale();

View File

@@ -31,6 +31,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
Interval = TimeSpan.FromHours(6) Interval = TimeSpan.FromHours(6)
}; };
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings; private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate(); private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
@@ -55,12 +56,19 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
ExampleTranslationTextBlock.FontFamily = MiSansFontFamily; ExampleTranslationTextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily; StatusTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick; _refreshTimer.Tick += OnRefreshTimerTick;
RefreshButton.Click += OnRefreshButtonClick; RefreshButton.Click += OnRefreshButtonClick;
AttachedToVisualTree += OnAttachedToVisualTree; AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
UpdateLanguageCode(); UpdateLanguageCode();
@@ -175,6 +183,12 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e) private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{ {
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (_isRefreshing) if (_isRefreshing)
{ {
return; return;
@@ -284,6 +298,26 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
UpdateAdaptiveLayout(); UpdateAdaptiveLayout();
} }
private void ApplyDesignTimePreview()
{
_isNightVisual = ResolveNightMode();
ApplyNightModeVisual();
WordTextBlock.Text = "serendipity";
PronunciationTextBlock.Text = "UK /,seren'dipiti/ | US /,seren'dipiti/";
MeaningTextBlock.Text = "n. finding something valuable by accident; a pleasant surprise.";
ExampleTextBlock.Text = "The widget preview became useful by pure serendipity.";
ExampleTranslationTextBlock.Text = "A mocked sample sentence shown only in design mode.";
StatusTextBlock.Text = string.Empty;
StatusTextBlock.IsVisible = false;
RefreshButton.IsEnabled = false;
RefreshButton.Opacity = 1.0;
RefreshIcon.Opacity = 0.82;
UpdateAdaptiveLayout();
}
private void UpdateAdaptiveLayout() private void UpdateAdaptiveLayout()
{ {
var scale = ResolveScale(); var scale = ResolveScale();

View File

@@ -10,7 +10,7 @@
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget"> x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
<Border x:Name="RootBorder" <Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusIsland}" CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Background="#2D5A8E" Background="#2D5A8E"
ClipToBounds="True" ClipToBounds="True"
BorderThickness="0" BorderThickness="0"

View File

@@ -213,10 +213,17 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
ContentGrid.ColumnDefinitions[1].Width = new GridLength(showDial ? dialSize : 0, GridUnitType.Pixel); ContentGrid.ColumnDefinitions[1].Width = new GridLength(showDial ? dialSize : 0, GridUnitType.Pixel);
} }
var leftWidthFactor = Math.Clamp(leftContentWidth / 122d, 0.48, 1.35); var timeTextWidth = leftContentWidth * 0.92;
TimeTextBlock.FontSize = Math.Clamp((metrics.PrimaryTemperatureFont * 0.74) * scale * compactFactor * leftWidthFactor, 10, 62); var timeCharCount = 5;
DateTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * compactFactor * leftWidthFactor, 8, 30); var maxTimeFontSize = timeTextWidth / (timeCharCount * 0.58);
var weatherIconSize = Math.Clamp(metrics.IconFont * scale * compactFactor * leftWidthFactor, 9, 32); var baseTimeFontSize = Math.Clamp(maxTimeFontSize, 12, 48);
var timeFontSize = Math.Clamp(baseTimeFontSize * scale * compactFactor, 10, 48);
TimeTextBlock.FontSize = timeFontSize;
var dateFontSize = Math.Clamp(timeFontSize * 0.48, 8, 22);
DateTextBlock.FontSize = dateFontSize;
var weatherIconSize = Math.Clamp(dateFontSize * 1.1, 10, 24);
WeatherIconImage.Width = weatherIconSize; WeatherIconImage.Width = weatherIconSize;
WeatherIconImage.Height = weatherIconSize; WeatherIconImage.Height = weatherIconSize;
@@ -226,11 +233,11 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
LeftStack.Width = leftContentWidth; LeftStack.Width = leftContentWidth;
LeftStack.MaxWidth = leftContentWidth; LeftStack.MaxWidth = leftContentWidth;
DateWeatherStack.MaxWidth = leftContentWidth; DateWeatherStack.MaxWidth = leftContentWidth;
TimeTextBlock.MaxWidth = leftContentWidth; TimeTextBlock.MaxWidth = timeTextWidth;
var showDateLine = leftContentWidth >= Math.Max(40, TimeTextBlock.FontSize * 1.72); var showDateLine = leftContentWidth >= Math.Max(36, timeFontSize * 1.4) && contentHeight >= 38;
DateWeatherStack.IsVisible = showDateLine; DateWeatherStack.IsVisible = showDateLine;
WeatherIconImage.IsVisible = showDateLine && leftContentWidth >= Math.Max(56, DateTextBlock.FontSize * 2.4); WeatherIconImage.IsVisible = showDateLine && leftContentWidth >= Math.Max(48, dateFontSize * 3.2);
var dateReservedWidth = WeatherIconImage.IsVisible var dateReservedWidth = WeatherIconImage.IsVisible
? weatherIconSize + DateWeatherStack.Spacing ? weatherIconSize + DateWeatherStack.Spacing
@@ -477,14 +484,22 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
: CreateBrush("#F8FAFF"); : CreateBrush("#F8FAFF");
AnalogDialBorder.BorderBrush = CreateBrush(isNightMode ? "#34DDE7FF" : "#12000000"); AnalogDialBorder.BorderBrush = CreateBrush(isNightMode ? "#34DDE7FF" : "#12000000");
TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush( if (isNightMode)
isNightMode ? "#F8FBFF" : "#10131A", {
backgroundSamples, TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
WeatherTypographyAccessibility.WcagLargeTextContrast); "#F8FBFF",
DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush( backgroundSamples,
isNightMode ? "#BCC8DD" : "#7A7E87", WeatherTypographyAccessibility.WcagLargeTextContrast);
backgroundSamples, DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
WeatherTypographyAccessibility.WcagNormalTextContrast); "#BCC8DD",
backgroundSamples,
WeatherTypographyAccessibility.WcagNormalTextContrast);
}
else
{
TimeTextBlock.Foreground = CreateBrush("#10131A");
DateTextBlock.Foreground = CreateBrush("#7A7E87");
}
_hourHandLine.Stroke = CreateBrush(isNightMode ? "#F1F5FF" : "#232938"); _hourHandLine.Stroke = CreateBrush(isNightMode ? "#F1F5FF" : "#232938");
_minuteHandLine.Stroke = CreateBrush(isNightMode ? "#D6E0F2" : "#2F3749"); _minuteHandLine.Stroke = CreateBrush(isNightMode ? "#D6E0F2" : "#2F3749");

View File

@@ -95,6 +95,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
Interval = FluttermotionToken.WeatherAnimationFrameInterval Interval = FluttermotionToken.WeatherAnimationFrameInterval
}; };
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings; private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate(); private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
@@ -128,11 +129,19 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
InitializeComponent(); InitializeComponent();
InitializeMotionTransform(); InitializeMotionTransform();
SizeChanged += OnSizeChanged;
if (_isDesignModePreview)
{
InitializeParticleVisuals();
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick; _refreshTimer.Tick += OnRefreshTimerTick;
_backgroundAnimationTimer.Tick += OnBackgroundAnimationTick; _backgroundAnimationTimer.Tick += OnBackgroundAnimationTick;
AttachedToVisualTree += OnAttachedToVisualTree; AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
InitializeParticleVisuals(); InitializeParticleVisuals();
ApplyVisualTheme(WeatherVisualKind.ClearDay); ApplyVisualTheme(WeatherVisualKind.ClearDay);
@@ -512,6 +521,29 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
_latestSnapshot = null; _latestSnapshot = null;
} }
private void ApplyDesignTimePreview()
{
const WeatherVisualKind previewVisualKind = WeatherVisualKind.PartlyCloudyDay;
_languageCode = "en-US";
_latestSnapshot = null;
ApplyVisualTheme(previewVisualKind);
SetWeatherIcon(
HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.PartlyCloudyDay),
previewVisualKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = "Shenzhen Bay";
ConditionTextBlock.Text = "Partly cloudy";
TemperatureTextBlock.Text = "24°";
RangeTextBlock.Text = "28°/20°";
ResetAnimationState();
ResetParticles();
ApplyAdaptiveTypography();
}
private void ApplyVisualTheme(WeatherVisualKind kind) private void ApplyVisualTheme(WeatherVisualKind kind)
{ {
_activeVisualKind = kind; _activeVisualKind = kind;

View File

@@ -215,17 +215,21 @@ public partial class MainWindow
string? savedWallpaperPath, string? savedWallpaperPath,
string? type = null, string? type = null,
string? color = null, string? color = null,
string? placement = null) string? placement = null,
int systemWallpaperRefreshIntervalSeconds = 300)
{ {
_wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath; _wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath;
_wallpaperType = string.IsNullOrWhiteSpace(type) ? "Image" : type.Trim(); _wallpaperType = string.IsNullOrWhiteSpace(type) ? "Image" : type.Trim();
_wallpaperPlacement = WallpaperImageBrushFactory.NormalizePlacement(placement); _wallpaperPlacement = WallpaperImageBrushFactory.NormalizePlacement(placement);
_wallpaperSolidColor = TryParseColor(color, out var parsedColor) ? parsedColor : null; _wallpaperSolidColor = TryParseColor(color, out var parsedColor) ? parsedColor : null;
_wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured; _wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured;
_systemWallpaperRefreshIntervalSeconds = systemWallpaperRefreshIntervalSeconds;
_wallpaperBitmap?.Dispose(); _wallpaperBitmap?.Dispose();
_wallpaperBitmap = null; _wallpaperBitmap = null;
StopSystemWallpaperTimer();
if (string.Equals(_wallpaperType, "SolidColor", StringComparison.OrdinalIgnoreCase)) if (string.Equals(_wallpaperType, "SolidColor", StringComparison.OrdinalIgnoreCase))
{ {
_wallpaperMediaType = WallpaperMediaType.SolidColor; _wallpaperMediaType = WallpaperMediaType.SolidColor;
@@ -235,6 +239,14 @@ public partial class MainWindow
return; return;
} }
if (string.Equals(_wallpaperType, "SystemWallpaper", StringComparison.OrdinalIgnoreCase))
{
_wallpaperMediaType = WallpaperMediaType.Image;
LoadSystemWallpaper();
StartSystemWallpaperTimer();
return;
}
if (string.IsNullOrWhiteSpace(_wallpaperPath)) if (string.IsNullOrWhiteSpace(_wallpaperPath))
{ {
_wallpaperMediaType = WallpaperMediaType.None; _wallpaperMediaType = WallpaperMediaType.None;
@@ -273,6 +285,69 @@ public partial class MainWindow
} }
} }
private void LoadSystemWallpaper()
{
var systemPath = _systemWallpaperProvider.GetWallpaperPath();
if (string.IsNullOrWhiteSpace(systemPath) || !File.Exists(systemPath))
{
_wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
return;
}
try
{
using var stream = File.OpenRead(systemPath);
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = new Bitmap(stream);
_wallpaperPath = systemPath;
_wallpaperDisplayState = WallpaperDisplayState.CurrentValidWallpaper;
CacheLastValidWallpaperBitmap(systemPath);
}
catch
{
_wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
}
}
private void StartSystemWallpaperTimer()
{
StopSystemWallpaperTimer();
var intervalSeconds = Math.Clamp(_systemWallpaperRefreshIntervalSeconds, 30, 86400);
_systemWallpaperRefreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(intervalSeconds)
};
_systemWallpaperRefreshTimer.Tick += OnSystemWallpaperRefreshTimerTick;
_systemWallpaperRefreshTimer.Start();
}
private void StopSystemWallpaperTimer()
{
if (_systemWallpaperRefreshTimer is not null)
{
_systemWallpaperRefreshTimer.Stop();
_systemWallpaperRefreshTimer.Tick -= OnSystemWallpaperRefreshTimerTick;
_systemWallpaperRefreshTimer = null;
}
}
private void OnSystemWallpaperRefreshTimerTick(object? sender, EventArgs e)
{
if (!string.Equals(_wallpaperType, "SystemWallpaper", StringComparison.OrdinalIgnoreCase))
{
StopSystemWallpaperTimer();
return;
}
LoadSystemWallpaper();
ApplyWallpaperBrush();
}
private void ApplyWallpaperBrush() private void ApplyWallpaperBrush()
{ {
DesktopWallpaperImageLayer.Background = null; DesktopWallpaperImageLayer.Background = null;
@@ -480,7 +555,8 @@ public partial class MainWindow
snapshot.WallpaperPath, snapshot.WallpaperPath,
snapshot.WallpaperType, snapshot.WallpaperType,
snapshot.WallpaperColor, snapshot.WallpaperColor,
snapshot.WallpaperPlacement); snapshot.WallpaperPlacement,
snapshot.SystemWallpaperRefreshIntervalSeconds);
if (!snapshot.IsNightMode.HasValue) if (!snapshot.IsNightMode.HasValue)
{ {
_isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold; _isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold;
@@ -523,6 +599,7 @@ public partial class MainWindow
? latestWallpaperState.Color ? latestWallpaperState.Color
: null, : null,
WallpaperPlacement = latestWallpaperState.Placement, WallpaperPlacement = latestWallpaperState.Placement,
SystemWallpaperRefreshIntervalSeconds = latestWallpaperState.SystemWallpaperRefreshIntervalSeconds,
LanguageCode = _languageCode, LanguageCode = _languageCode,
TimeZoneId = _timeZoneService.CurrentTimeZone.Id, TimeZoneId = _timeZoneService.CurrentTimeZone.Id,
WeatherLocationMode = latestWeatherState.LocationMode, WeatherLocationMode = latestWeatherState.LocationMode,

View File

@@ -122,6 +122,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private Color? _wallpaperSolidColor; private Color? _wallpaperSolidColor;
private string? _wallpaperPath; private string? _wallpaperPath;
private string _wallpaperStatus = "Current background uses solid color."; private string _wallpaperStatus = "Current background uses solid color.";
private int _systemWallpaperRefreshIntervalSeconds = 300;
private DispatcherTimer? _systemWallpaperRefreshTimer;
private readonly ISystemWallpaperProvider _systemWallpaperProvider = HostSystemWallpaperProvider.GetOrCreate();
private IReadOnlyList<Color> _recommendedColors = Array.Empty<Color>(); private IReadOnlyList<Color> _recommendedColors = Array.Empty<Color>();
private IReadOnlyList<Color> _monetColors = Array.Empty<Color>(); private IReadOnlyList<Color> _monetColors = Array.Empty<Color>();
private Color _selectedThemeColor = Color.Parse("#FF3B82F6"); private Color _selectedThemeColor = Color.Parse("#FF3B82F6");
@@ -160,7 +163,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
public MainWindow() public MainWindow()
{ {
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService; var pluginRuntimeService = Design.IsDesignMode
? null
: (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService); _componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_settingsService = _settingsFacade.Settings; _settingsService = _settingsFacade.Settings;
_gridSettingsService = _settingsFacade.Grid; _gridSettingsService = _settingsFacade.Grid;
@@ -173,7 +178,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
InitializeComponent(); InitializeComponent();
Icon = _appLogoService.CreateWindowIcon(); Icon = _appLogoService.CreateWindowIcon();
InitializeTaskbarProfileFlyout();
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry, _componentRegistry,
pluginRuntimeService, pluginRuntimeService,
@@ -183,6 +187,14 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
pluginRuntimeService); pluginRuntimeService);
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry); _componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
_componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade); _componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade);
if (Design.IsDesignMode)
{
ApplyDesignTimePreview();
return;
}
InitializeTaskbarProfileFlyout();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault(); _fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
_settingsService.Changed += OnSettingsChanged; _settingsService.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged; _appearanceThemeService.Changed += OnAppearanceThemeChanged;
@@ -196,6 +208,170 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
} }
} }
private void ApplyDesignTimePreview()
{
Title = "LanMountainDesktop Preview";
ShowInTaskbar = false;
DesktopWallpaperLayer.Background = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops = new GradientStops
{
new GradientStop(Color.Parse("#FFF6F8FB"), 0d),
new GradientStop(Color.Parse("#FFE9EEF7"), 0.55d),
new GradientStop(Color.Parse("#FFDCE5F3"), 1d)
}
};
DesktopWallpaperImageLayer.IsVisible = false;
LauncherPagePanel.IsVisible = false;
ComponentLibraryWindow.IsVisible = false;
BackToWindowsTextBlock.Text = "Back to Windows";
ComponentLibraryTitleTextBlock.Text = "Widgets";
ComponentLibraryBackTextBlock.Text = "Back";
TaskbarProfileDisplayNameTextBlock.Text = "Preview User";
TaskbarProfileSettingsActionTextBlock.Text = "Settings";
TaskbarProfileDesktopEditActionTextBlock.Text = "Edit Desktop";
TaskbarProfileAvatarFallbackText.Text = "P";
TaskbarProfileHeaderAvatarFallbackText.Text = "P";
TaskbarProfileButton.IsEnabled = false;
TaskbarProfilePopup.IsOpen = false;
ClockWidget.IsVisible = true;
ClockWidget.SetDisplayFormat(ClockDisplayFormat.HourMinute);
ClockWidget.SetTransparentBackground(false);
ConfigureDesignTimeDesktopGrid();
PopulateDesignTimeDesktopSurface();
}
private void ConfigureDesignTimeDesktopGrid()
{
const int previewRows = 7;
const int previewColumns = 12;
DesktopGrid.RowDefinitions.Clear();
DesktopGrid.ColumnDefinitions.Clear();
for (var row = 0; row < previewRows; row++)
{
DesktopGrid.RowDefinitions.Add(new RowDefinition(new GridLength(1, GridUnitType.Star)));
}
for (var column = 0; column < previewColumns; column++)
{
DesktopGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
}
DesktopGrid.Margin = new Thickness(28);
DesktopGrid.RowSpacing = 14;
DesktopGrid.ColumnSpacing = 14;
DesktopGrid.Width = double.NaN;
DesktopGrid.Height = double.NaN;
Grid.SetRow(TopStatusBarHost, 0);
Grid.SetColumn(TopStatusBarHost, 0);
Grid.SetRowSpan(TopStatusBarHost, 1);
Grid.SetColumnSpan(TopStatusBarHost, previewColumns);
Grid.SetRow(DesktopPagesViewport, 1);
Grid.SetColumn(DesktopPagesViewport, 0);
Grid.SetRowSpan(DesktopPagesViewport, previewRows - 2);
Grid.SetColumnSpan(DesktopPagesViewport, previewColumns);
Grid.SetRow(BottomTaskbarContainer, previewRows - 1);
Grid.SetColumn(BottomTaskbarContainer, 0);
Grid.SetRowSpan(BottomTaskbarContainer, 1);
Grid.SetColumnSpan(BottomTaskbarContainer, previewColumns);
DesktopPagesHost.ColumnDefinitions.Clear();
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
ClockWidget.ApplyCellSize(72);
}
private void PopulateDesignTimeDesktopSurface()
{
DesktopPagesContainer.Children.Clear();
DesktopPagesContainer.Width = double.NaN;
DesktopPagesContainer.Height = double.NaN;
DesktopPagesContainer.Children.Add(CreateDesignTimePreviewCard(
"Focus Clock",
"Compact widget preview",
32,
32,
300,
170,
"#FFFFFFFF",
"#FFE8EEF8"));
DesktopPagesContainer.Children.Add(CreateDesignTimePreviewCard(
"Weather",
"26°C Qingdao",
360,
86,
260,
132,
"#FFF8FBFF",
"#FFDDE8F6"));
DesktopPagesContainer.Children.Add(CreateDesignTimePreviewCard(
"Study Session",
"Deep work · 48 min",
210,
248,
340,
144,
"#FFFDFEFF",
"#FFE7EEF7"));
}
private static Border CreateDesignTimePreviewCard(
string title,
string subtitle,
double left,
double top,
double width,
double height,
string backgroundColor,
string borderColor)
{
return new Border
{
Width = width,
Height = height,
Margin = new Thickness(left, top, 0, 0),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Background = new SolidColorBrush(Color.Parse(backgroundColor)),
BorderBrush = new SolidColorBrush(Color.Parse(borderColor)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(28),
Child = new StackPanel
{
Margin = new Thickness(20),
Spacing = 8,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
new TextBlock
{
Text = title,
FontSize = 20,
FontWeight = FontWeight.SemiBold,
Foreground = new SolidColorBrush(Color.Parse("#FF1E293B"))
},
new TextBlock
{
Text = subtitle,
FontSize = 13,
Foreground = new SolidColorBrush(Color.Parse("#FF64748B"))
}
}
}
};
}
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e) private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
{ {
if (sender is not ToggleButton toggleButton) if (sender is not ToggleButton toggleButton)
@@ -231,6 +407,14 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
protected override void OnOpened(EventArgs e) protected override void OnOpened(EventArgs e)
{ {
base.OnOpened(e); base.OnOpened(e);
if (Design.IsDesignMode)
{
ConfigureDesignTimeDesktopGrid();
PopulateDesignTimeDesktopSurface();
return;
}
SyncSettingsWindowState(); SyncSettingsWindowState();
_suppressSettingsPersistence = true; _suppressSettingsPersistence = true;
@@ -307,6 +491,12 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{ {
if (Design.IsDesignMode)
{
base.OnClosed(e);
return;
}
var wasVisible = IsVisible; var wasVisible = IsVisible;
var windowState = WindowState.ToString(); var windowState = WindowState.ToString();

View File

@@ -62,15 +62,34 @@
</ui:InfoBar.IconSource> </ui:InfoBar.IconSource>
</ui:InfoBar> </ui:InfoBar>
<Border Classes="settings-section-card"> <!-- 版权声明 - 放在渲染显示前面 -->
<StackPanel Spacing="12"> <ui:SettingsExpander Header="版权声明"
<controls:IconText Icon="WindowConsole" IsExpanded="True">
Text="{Binding RenderBackendLabel}" /> <ui:SettingsExpander.IconSource>
<TextBlock Classes="settings-item-description" <fi:SymbolIconSource Symbol="Document" />
Text="{Binding RenderBackendText}" </ui:SettingsExpander.IconSource>
TextWrapping="Wrap" /> <ui:SettingsExpanderItem>
</StackPanel> <ui:SettingsExpanderItem.Footer>
</Border> <WrapPanel>
<WrapPanel.Styles>
<Style Selector="HyperlinkButton">
<Setter Property="Padding" Value="4" />
<Setter Property="Margin" Value="2" />
</Style>
</WrapPanel.Styles>
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop">
<TextBlock Text="GitHub 仓库" />
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop/issues">
<TextBlock Text="问题反馈" />
</HyperlinkButton>
</WrapPanel>
</ui:SettingsExpanderItem.Footer>
<TextBlock>
<Run Text="Copyright (c) 2024-" /><Run Text="2025" /> Lincube
</TextBlock>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -12,12 +12,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class GeneratedPluginSettingsPage : SettingsPageBase public partial class GeneratedPluginSettingsPage : SettingsPageBase
{ {
public GeneratedPluginSettingsPage() public GeneratedPluginSettingsPage()
: this( : this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US")))
{ {
} }
@@ -223,4 +218,272 @@ public partial class GeneratedPluginSettingsPage : SettingsPageBase
return textBox; return textBox;
} }
private static PluginGeneratedSettingsPageViewModel CreateDefaultViewModel()
{
return new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US"));
}
private static PluginGeneratedSettingsPageViewModel CreateDesignTimeViewModel()
{
const string pluginId = "preview.plugin";
var settingsService = new DesignTimeSettingsService();
var section = new PluginSettingsSectionRegistration(
"desktop_preview",
"Preview Widget Settings",
[
new SettingsOptionDefinition(
"enable_glow",
SettingsOptionType.Toggle,
"Enable glow",
"Adds a soft highlight around the preview widget.",
true),
new SettingsOptionDefinition(
"refresh_minutes",
SettingsOptionType.Number,
"Refresh interval",
"How often the plugin refreshes its cached content.",
30d,
minimum: 5d,
maximum: 120d),
new SettingsOptionDefinition(
"layout_density",
SettingsOptionType.Select,
"Layout density",
"Choose how compact the widget layout should feel.",
"balanced",
[
new SettingsOptionChoice("compact", "Compact"),
new SettingsOptionChoice("balanced", "Balanced"),
new SettingsOptionChoice("comfortable", "Comfortable")
]),
new SettingsOptionDefinition(
"content_path",
SettingsOptionType.Path,
"Content folder",
"Local folder used by the plugin for mock assets.",
@"C:\Preview\PluginAssets"),
new SettingsOptionDefinition(
"keywords",
SettingsOptionType.List,
"Pinned keywords",
"Comma-separated topics that will be emphasized in the widget.",
new[] { "avalonia", "preview", "design-time" })
],
"Mock plugin settings shown only in Avalonia design mode.");
settingsService.SetValue(
SettingsScope.Plugin,
"enable_glow",
true,
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"refresh_minutes",
30d,
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"layout_density",
"balanced",
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"content_path",
@"C:\Preview\PluginAssets",
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"keywords",
new[] { "avalonia", "preview", "design-time" },
pluginId,
sectionId: section.Id);
return new PluginGeneratedSettingsPageViewModel(
settingsService,
pluginId,
section,
new PluginLocalizer(AppContext.BaseDirectory, "en-US"));
}
private sealed class DesignTimeSettingsService : ISettingsService
{
private readonly Dictionary<string, object?> _values = new(StringComparer.OrdinalIgnoreCase);
public event EventHandler<SettingsChangedEvent>? Changed;
public T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new()
=> new();
public void SaveSnapshot<T>(
SettingsScope scope,
T snapshot,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys);
}
public T LoadSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null) where T : new()
=> new();
public void SaveSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
T section,
string? placementId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys);
}
public void DeleteSection(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null)
{
var prefix = BuildStorageKey(scope, subjectId, placementId, sectionId, key: null);
foreach (var existingKey in _values.Keys.Where(key => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToArray())
{
_values.Remove(existingKey);
}
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys: null);
}
public T? GetValue<T>(
SettingsScope scope,
string key,
string? subjectId = null,
string? placementId = null,
string? sectionId = null)
{
return _values.TryGetValue(BuildStorageKey(scope, subjectId, placementId, sectionId, key), out var value)
? ConvertValue<T>(value)
: default;
}
public void SetValue<T>(
SettingsScope scope,
string key,
T value,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
_values[BuildStorageKey(scope, subjectId, placementId, sectionId, key)] = value;
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys ?? [key]);
}
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
{
return new DesignTimeComponentSettingsAccessor(this, componentId, placementId);
}
private static T? ConvertValue<T>(object? value)
{
if (value is null)
{
return default;
}
if (value is T typedValue)
{
return typedValue;
}
var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
try
{
return (T?)Convert.ChangeType(value, targetType);
}
catch
{
return default;
}
}
private static string BuildStorageKey(
SettingsScope scope,
string? subjectId,
string? placementId,
string? sectionId,
string? key)
{
return string.Join(
"|",
scope,
subjectId ?? string.Empty,
placementId ?? string.Empty,
sectionId ?? string.Empty,
key ?? string.Empty);
}
private void RaiseChanged(
SettingsScope scope,
string? subjectId,
string? placementId,
string? sectionId,
IReadOnlyCollection<string>? changedKeys)
{
Changed?.Invoke(this, new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
}
}
private sealed class DesignTimeComponentSettingsAccessor : IComponentSettingsAccessor
{
private readonly DesignTimeSettingsService _settingsService;
public DesignTimeComponentSettingsAccessor(
DesignTimeSettingsService settingsService,
string componentId,
string? placementId)
{
_settingsService = settingsService;
ComponentId = componentId;
PlacementId = placementId;
}
public string ComponentId { get; }
public string? PlacementId { get; }
public T LoadSnapshot<T>() where T : new()
=> _settingsService.LoadSnapshot<T>(SettingsScope.ComponentInstance, ComponentId, PlacementId);
public void SaveSnapshot<T>(T snapshot, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SaveSnapshot(SettingsScope.ComponentInstance, snapshot, ComponentId, PlacementId, changedKeys: changedKeys);
public T LoadSection<T>(string sectionId) where T : new()
=> _settingsService.LoadSection<T>(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
public void SaveSection<T>(string sectionId, T section, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SaveSection(SettingsScope.ComponentInstance, ComponentId, sectionId, section, PlacementId, changedKeys);
public void DeleteSection(string sectionId)
=> _settingsService.DeleteSection(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
public T? GetValue<T>(string key)
=> _settingsService.GetValue<T>(SettingsScope.ComponentInstance, key, ComponentId, PlacementId);
public void SetValue<T>(string key, T value, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SetValue(SettingsScope.ComponentInstance, key, value, ComponentId, PlacementId, changedKeys: changedKeys);
}
} }

View File

@@ -1,3 +1,5 @@
using System;
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket; using LanMountainDesktop.Services.PluginMarket;
@@ -17,7 +19,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginMarketSettingsPage : SettingsPageBase public partial class PluginMarketSettingsPage : SettingsPageBase
{ {
public PluginMarketSettingsPage() public PluginMarketSettingsPage()
: this(CreateDefaultViewModel()) : this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{ {
} }
@@ -34,6 +36,11 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
public override async void OnNavigatedTo(object? parameter) public override async void OnNavigatedTo(object? parameter)
{ {
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync(); await ViewModel.InitializeAsync();
} }
@@ -48,6 +55,113 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
new AirAppMarketReadmeService()); new AirAppMarketReadmeService());
} }
private static PluginMarketSettingsPageViewModel CreateDesignTimeViewModel()
{
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
var localizationService = new LocalizationService();
var viewModel = new PluginMarketSettingsPageViewModel(
settingsFacade,
localizationService,
new AirAppMarketIconService(),
new AirAppMarketReadmeService());
var previewHostVersion = new Version(1, 2, 0);
var items = new[]
{
CreateMarketItem(
new PluginMarketPluginInfo(
"news-tiles",
"News Tiles",
"Brings editorial news cards and ticker rows to the desktop.",
"LanMountain Labs",
"1.2.0",
"1.0.0",
"1.0.0",
"https://example.com/news-tiles.zip",
"v1.2.0",
"news-tiles.zip",
string.Empty,
"https://example.com/news-tiles/readme",
"https://example.com/news-tiles",
"https://example.com/news-tiles/repo",
["news", "widgets"],
[],
DateTimeOffset.Now.AddDays(-8),
DateTimeOffset.Now.AddDays(-2)),
localizationService,
installedPlugin: null,
previewHostVersion),
CreateMarketItem(
new PluginMarketPluginInfo(
"workspace-pulse",
"Workspace Pulse",
"Tracks active projects and shows a compact productivity summary.",
"Studio North",
"2.4.0",
"1.0.0",
"1.0.0",
"https://example.com/workspace-pulse.zip",
"v2.4.0",
"workspace-pulse.zip",
string.Empty,
"https://example.com/workspace-pulse/readme",
"https://example.com/workspace-pulse",
"https://example.com/workspace-pulse/repo",
["dashboard", "productivity"],
[],
DateTimeOffset.Now.AddDays(-30),
DateTimeOffset.Now.AddDays(-1)),
localizationService,
new InstalledPluginInfo(
new PluginManifest(
"workspace-pulse",
"Workspace Pulse",
"WorkspacePulse.dll",
"Tracks active projects and shows a compact productivity summary.",
"Studio North",
"2.1.0"),
true,
true,
true,
null),
previewHostVersion),
CreateMarketItem(
new PluginMarketPluginInfo(
"glass-panels",
"Glass Panels",
"Adds experimental acrylic surfaces for plugin-powered widgets.",
"Aster Team",
"0.8.0",
"1.0.0",
"9.0.0",
"https://example.com/glass-panels.zip",
"v0.8.0",
"glass-panels.zip",
string.Empty,
"https://example.com/glass-panels/readme",
"https://example.com/glass-panels",
"https://example.com/glass-panels/repo",
["theme", "experimental"],
[],
DateTimeOffset.Now.AddDays(-12),
DateTimeOffset.Now.AddDays(-3)),
localizationService,
installedPlugin: null,
previewHostVersion)
};
foreach (var item in items)
{
viewModel.MarketPlugins.Add(item);
viewModel.FilteredPlugins.Add(item);
}
viewModel.ShowEmptyState = false;
viewModel.EmptyStateText = string.Empty;
viewModel.StatusMessage = "Showing 3 mocked marketplace plugins for Avalonia design mode.";
return viewModel;
}
private void OnRestartRequested(string? reason) private void OnRestartRequested(string? reason)
{ {
RequestRestart(reason ?? ViewModel.RestartRequiredMessage); RequestRestart(reason ?? ViewModel.RestartRequiredMessage);
@@ -60,4 +174,17 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
OpenDrawer(drawer, detailViewModel.DrawerTitle); OpenDrawer(drawer, detailViewModel.DrawerTitle);
await detailViewModel.InitializeAsync(); await detailViewModel.InitializeAsync();
} }
private static PluginMarketItemViewModel CreateMarketItem(
PluginMarketPluginInfo plugin,
LocalizationService localizationService,
InstalledPluginInfo? installedPlugin,
Version hostVersion)
{
var languageCode = localizationService.NormalizeLanguageCode(
HostSettingsFacadeProvider.GetOrCreate().Region.Get().LanguageCode);
var item = new PluginMarketItemViewModel(plugin, localizationService, languageCode);
item.ApplyInstallState(installedPlugin, hostVersion);
return item;
}
} }

View File

@@ -1,3 +1,4 @@
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
@@ -15,7 +16,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginsSettingsPage : SettingsPageBase public partial class PluginsSettingsPage : SettingsPageBase
{ {
public PluginsSettingsPage() public PluginsSettingsPage()
: this(new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate())) : this(Design.IsDesignMode ? CreateDesignTimeViewModel() : new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{ {
} }
@@ -31,6 +32,11 @@ public partial class PluginsSettingsPage : SettingsPageBase
public override async void OnNavigatedTo(object? parameter) public override async void OnNavigatedTo(object? parameter)
{ {
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync(); await ViewModel.InitializeAsync();
} }
@@ -38,4 +44,47 @@ public partial class PluginsSettingsPage : SettingsPageBase
{ {
RequestRestart(ViewModel.RestartRequiredMessage); RequestRestart(ViewModel.RestartRequiredMessage);
} }
private static PluginsSettingsPageViewModel CreateDesignTimeViewModel()
{
var viewModel = new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate());
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"calendar-plus",
"Calendar Plus",
"CalendarPlus.dll",
"Adds a compact agenda widget and richer date cards.",
"LanMountain Labs",
"1.4.0"),
true,
true,
true,
null)));
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"focus-mode",
"Focus Mode",
"FocusMode.dll",
"Provides a distraction-free overlay and quick toggles.",
"Studio North",
"0.9.2"),
true,
false,
true,
null)));
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"notes-dock",
"Notes Dock",
"NotesDock.dll",
"Pins short markdown notes directly on the desktop.",
"Aster Team",
"2.1.0"),
false,
false,
true,
null)));
viewModel.StatusMessage = "Loaded 3 mocked plugins for Avalonia design mode.";
return viewModel;
}
} }

View File

@@ -51,14 +51,9 @@
FontFamily="Consolas" FontFamily="Consolas"
FontSize="12" FontSize="12"
Focusable="False" Focusable="False"
IsTabStop="False" /> IsTabStop="False"
HorizontalAlignment="Stretch" />
</StackPanel> </StackPanel>
<Button Grid.Column="1"
Content="{Binding RefreshTelemetryIdText}"
Command="{Binding RefreshTelemetryIdCommand}"
VerticalAlignment="Center"
Margin="16,0,0,0"
Classes="accent-button" />
</Grid> </Grid>
</Border> </Border>

View File

@@ -29,6 +29,11 @@
<!-- 纯色预览 --> <!-- 纯色预览 -->
<Border Background="{Binding SelectedColor}" <Border Background="{Binding SelectedColor}"
IsVisible="{Binding IsSolidColor}" /> IsVisible="{Binding IsSolidColor}" />
<!-- 系统壁纸预览 -->
<Border Background="#FFF6F7F9"
IsVisible="{Binding IsSystemWallpaper}">
<Border Background="{Binding PreviewBrush}" />
</Border>
</Panel> </Panel>
</Border> </Border>
</Viewbox> </Viewbox>
@@ -135,6 +140,19 @@
</Button> </Button>
</UniformGrid> </UniformGrid>
</StackPanel> </StackPanel>
<!-- 右侧:系统壁纸状态 -->
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="12" IsVisible="{Binding IsSystemWallpaper}">
<TextBlock Text="{Binding SystemWallpaperLabel}"
FontSize="14"
FontWeight="SemiBold"
Opacity="0.8" />
<TextBlock Text="{Binding SystemWallpaperStatus}"
FontSize="12"
Opacity="0.7"
TextWrapping="Wrap"
MaxWidth="280" />
</StackPanel>
</Grid> </Grid>
<Separator Classes="settings-separator" Margin="0,0,0,24" /> <Separator Classes="settings-separator" Margin="0,0,0,24" />
@@ -183,10 +201,60 @@
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
<!-- 填充方式 --> <!-- 系统壁纸刷新设置 -->
<ui:SettingsExpander Header="{Binding RefreshIntervalLabel}"
IsVisible="{Binding IsSystemWallpaper}"
Margin="0,4,0,0">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Clock" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8">
<ComboBox Width="140"
ItemsSource="{Binding RefreshIntervals}"
SelectedItem="{Binding SelectedRefreshInterval}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Classes="settings-accent-button"
Command="{Binding RefreshSystemWallpaperCommand}"
ToolTip.Tip="{Binding RefreshButtonTooltip}"
VerticalAlignment="Center"
Padding="12,8">
<fi:SymbolIcon Symbol="ArrowSync" IconVariant="Regular" />
</Button>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<!-- 填充方式(图片和系统壁纸都显示) -->
<ui:SettingsExpander Header="{Binding WallpaperPlacementLabel}" <ui:SettingsExpander Header="{Binding WallpaperPlacementLabel}"
Description="{Binding WallpaperPlacementDescription}" Description="{Binding WallpaperPlacementDescription}"
IsVisible="{Binding IsImage}" IsVisible="{Binding IsImageOrVideo}"
Margin="0,4,0,0">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Maximize" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="200"
ItemsSource="{Binding WallpaperPlacements}"
SelectedItem="{Binding SelectedWallpaperPlacement}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<!-- 系统壁纸填充方式 -->
<ui:SettingsExpander Header="{Binding WallpaperPlacementLabel}"
Description="{Binding WallpaperPlacementDescription}"
IsVisible="{Binding IsSystemWallpaper}"
Margin="0,4,0,0"> Margin="0,4,0,0">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Maximize" /> <fi:SymbolIconSource Symbol="Maximize" />

View File

@@ -1,3 +1,4 @@
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
@@ -16,7 +17,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class WeatherSettingsPage : SettingsPageBase public partial class WeatherSettingsPage : SettingsPageBase
{ {
public WeatherSettingsPage() public WeatherSettingsPage()
: this(CreateDefaultViewModel()) : this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{ {
} }
@@ -29,7 +30,7 @@ public partial class WeatherSettingsPage : SettingsPageBase
public WeatherSettingsPageViewModel ViewModel { get; } public WeatherSettingsPageViewModel ViewModel { get; }
private static WeatherSettingsPageViewModel CreateDefaultViewModel() private static WeatherSettingsPageViewModel CreateDefaultViewModel(bool enableStartupPreviewRefresh = true)
{ {
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
var localizationService = new LocalizationService(); var localizationService = new LocalizationService();
@@ -42,6 +43,14 @@ public partial class WeatherSettingsPage : SettingsPageBase
settingsFacade, settingsFacade,
localizationService, localizationService,
locationService, locationService,
weatherLocationRefreshService); weatherLocationRefreshService,
enableStartupPreviewRefresh);
}
private static WeatherSettingsPageViewModel CreateDesignTimeViewModel()
{
var viewModel = CreateDefaultViewModel(enableStartupPreviewRefresh: false);
viewModel.ApplyDesignTimePreview();
return viewModel;
} }
} }

View File

@@ -8,12 +8,12 @@
## 2. 使用场景 ## 2. 使用场景
| 场景 | 说明 | | 场景 | 说明 |
|-----|------| | ---- | ---------------------- |
| 学习辅助 | 查看课程表、记录自习时长、获取每日诗词单词 | | 学习辅助 | 查看课程表、记录自习时长、获取每日诗词单词 |
| 办公效率 | 查看日历日程、快速访问最近文档、获取新闻资讯 | | 办公效率 | 查看日历日程、快速访问最近文档、获取新闻资讯 |
| 信息聚合 | 桌面一站式查看天气、日历、热搜、新闻 | | 信息聚合 | 桌面一站式查看天气、日历、热搜、新闻 |
| 个性美化 | 自由定制桌面组件布局、主题、壁纸 | | 个性美化 | 自由定制桌面组件布局、主题、壁纸 |
## 3. 解决方案 ## 3. 解决方案
@@ -27,20 +27,17 @@
## 4. 解决的问题 ## 4. 解决的问题
| 痛点 | 解决方案 | | 痛点 | 解决方案 |
|-----|---------| | -------------- | -------------------- |
| 信息分散,需打开多个应用 | 桌面聚合展示天气、日历、新闻等信息 | | 信息分散,需打开多个应用 | 桌面聚合展示天气、日历、新闻等信息 |
| 桌面单调,缺乏个性化 | 丰富的组件和主题自由定制 | | 桌面单调,缺乏个性化 | 丰富的组件和主题自由定制 |
| 学习管理不便 | 课程表、自习监测专为学生设计 | | 学习管理不便 | 课程表、自习监测专为学生设计 |
| 功能单一,需安装多个独立应用 | 一个应用整合考试看板、噪音监测等多种功能 | | 功能单一,需安装多个独立应用 | 一个应用整合考试看板、噪音监测等多种功能 |
| 功能无法满足个性需求 | 插件系统支持无限扩展 | | 功能无法满足个性需求 | 插件系统支持无限扩展 |
## 5. 产品进度 ## 5. 产品进度
- **当前版本**v0.7.0(插件 API 3.0.0 - **当前版本**v0.7.0(插件 API 3.0.0
- **开发状态**核心功能开发中,预计 v1.0 正式发布 - **开发状态**:功能开发中,预计 1\~2 个月内发布 v1.0 正式
- **用户统计**:通过 PostHog 收集匿名数据(具体数据需后台查看) - **用户统计**:通过 PostHog 收集匿名数据(具体数据需后台查看)
---
**一句话总结**:阑山桌面是一款面向个人用户的可定制桌面工具,专注个人学习办公场景,通过组件化设计和插件生态提供轻量、开放、跨平台的桌面信息聚合方案。