Compare commits

...

2 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
33 changed files with 1704 additions and 131 deletions

View File

@@ -101,6 +101,11 @@ public partial class App : Application
public App()
{
if (Design.IsDesignMode)
{
return;
}
_settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
}
@@ -108,9 +113,16 @@ public partial class App : Application
public override void Initialize()
{
AppLogger.Info("App", "Initializing application resources.");
AvaloniaXamlLoader.Load(this);
if (Design.IsDesignMode)
{
ApplyDesignTimeTheme();
return;
}
ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default);
AvaloniaXamlLoader.Load(this);
ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService();
@@ -119,6 +131,12 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
if (Design.IsDesignMode)
{
base.OnFrameworkInitializationCompleted();
return;
}
AppLogger.Info("App", "Framework initialization completed.");
RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled();
@@ -127,6 +145,20 @@ public partial class App : Application
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()
{
_desktopShellHost ??= new DesktopShellHost(

View File

@@ -38,6 +38,27 @@
"settings.wallpaper.title": "Wallpaper",
"settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.",
"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_desc": "Adjust how the image fills the desktop.",
"settings.wallpaper.pick_button": "Browse Files",
@@ -217,7 +238,14 @@
"schedule.settings.unnamed": "Unnamed Schedule",
"schedule.settings.delete": "Delete",
"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.desc": "Choose a time zone for each of the four clocks.",
"worldclock.settings.clock_1": "Clock 1",

View File

@@ -41,6 +41,23 @@
"settings.wallpaper.type_label": "壁纸类型",
"settings.wallpaper.type.image": "图片",
"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.custom_color_tooltip": "自定义颜色",
"settings.wallpaper.custom_color_apply": "应用",
@@ -216,7 +233,14 @@
"schedule.settings.unnamed": "未命名课表",
"schedule.settings.delete": "删除",
"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.desc": "分别为四个时钟选择时区。",
"worldclock.settings.clock_1": "时钟 1",

View File

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

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
namespace LanMountainDesktop.Models;
@@ -12,6 +13,10 @@ public sealed class ComponentSettingsSnapshot
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 StudyEnvironmentShowDbfs { get; set; }

View File

@@ -11,7 +11,7 @@ using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop;
sealed class Program
public sealed class Program
{
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>()
.UsePlatformDetect()

View File

@@ -12,7 +12,7 @@ namespace LanMountainDesktop.Services;
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(
ClassIslandScheduleSnapshot snapshot,
@@ -43,7 +43,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
.IgnoreUnmatchedProperties()
.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>();
try
@@ -73,11 +73,11 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
ClassIslandScheduleSnapshot snapshot;
if (source.SourceKind == ScheduleSourceKind.Cses)
{
snapshot = ParseCsesSnapshot(source);
snapshot = ParseCsesSnapshot(source, semesterStartDate, semesterWeekCycle);
}
else
{
var cycleRule = ParseCycleRule(source.SettingsPath, warnings);
var cycleRule = ParseCycleRule(source.SettingsPath, warnings, semesterStartDate, semesterWeekCycle);
var profileJson = ReadJson(source.ProfilePath);
snapshot = ParseProfileSnapshot(profileJson.RootElement, source, cycleRule);
}
@@ -412,22 +412,50 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
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.");
return new ClassIslandScheduleCycleRule(null, 4, new List<int> { -1, -1, 0, 0, 0 });
using var json = ReadJson(settingsPath);
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);
var root = json.RootElement;
var singleWeekStartDate = TryReadDateOnly(root, "SingleWeekStartTime");
var maxCycle = TryReadInt(root, "MultiWeekRotationMaxCycle", 4);
var offsetList = ReadIntList(root, "MultiWeekRotationOffset");
if (offsetList.Count < 2)
if (maxCycle < 2)
{
offsetList = new List<int> { -1, -1, 0, 0, 0 };
maxCycle = 2;
}
while (offsetList.Count <= maxCycle)
{
offsetList.Add(0);
}
return new ClassIslandScheduleCycleRule(
@@ -469,7 +497,10 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
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 csesProfile = CsesDeserializer.Deserialize<CsesProfileDto>(yaml) ?? new CsesProfileDto();
@@ -600,12 +631,19 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
[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(
SourceRootPath: source.SourceRootPath,
ProfilePath: source.ProfilePath,
ProfileFileName: source.ProfileFileName,
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,
TempClassPlanGroupId: null,
IsTempClassPlanGroupEnabled: false,

View File

@@ -16,7 +16,13 @@ public enum WallpaperMediaType
}
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(
bool IsNightMode,
string? ThemeColor,

View File

@@ -101,7 +101,9 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
: snapshot.WallpaperPath,
normalizedType,
snapshot.WallpaperColor,
snapshot.WallpaperPlacement);
snapshot.WallpaperPlacement,
CustomColor: null,
SystemWallpaperRefreshIntervalSeconds: NormalizeRefreshInterval(snapshot.SystemWallpaperRefreshIntervalSeconds));
}
public void Save(WallpaperSettingsState state)
@@ -128,6 +130,7 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
? "Fill"
: state.Placement.Trim();
snapshot.SystemWallpaperRefreshIntervalSeconds = NormalizeRefreshInterval(state.SystemWallpaperRefreshIntervalSeconds);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
@@ -136,9 +139,21 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
nameof(AppSettingsSnapshot.WallpaperPath),
nameof(AppSettingsSnapshot.WallpaperType),
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

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

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

View File

@@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@@ -15,6 +16,7 @@ namespace LanMountainDesktop.ViewModels;
public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly ISystemWallpaperProvider _systemWallpaperProvider;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _isInitializing;
@@ -22,9 +24,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
public WallpaperSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_systemWallpaperProvider = HostSystemWallpaperProvider.GetOrCreate();
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
WallpaperPlacements = CreateWallpaperPlacements();
WallpaperTypes = CreateWallpaperTypes();
RefreshIntervals = CreateRefreshIntervals();
PresetColors = CreatePresetColors();
RefreshLocalizedText();
@@ -35,8 +39,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
public IReadOnlyList<SelectionOption> WallpaperPlacements { get; }
public IReadOnlyList<SelectionOption> WallpaperTypes { get; }
public IReadOnlyList<SelectionOption> RefreshIntervals { get; }
public IReadOnlyList<string> PresetColors { get; }
public bool IsSystemWallpaperSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
[ObservableProperty]
private string _wallpaperPath = string.Empty;
@@ -49,6 +56,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private SelectionOption _selectedWallpaperPlacement = null!;
[ObservableProperty]
private SelectionOption _selectedRefreshInterval = null!;
[ObservableProperty]
private string _wallpaperHeader = string.Empty;
@@ -73,6 +83,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
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]
private bool _isImageOrVideo;
@@ -82,13 +104,15 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private bool _isImage;
[ObservableProperty]
private bool _isSystemWallpaper;
[ObservableProperty]
private Bitmap? _previewImage;
[ObservableProperty]
private IBrush? _previewBrush;
// 自定义颜色持久化
[ObservableProperty]
private Color _customColor = Colors.White;
@@ -110,7 +134,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
?? 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))
{
CustomColor = customColor;
@@ -119,6 +147,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
UpdateVisibility();
UpdatePreviewFromCurrentSelection();
UpdateSystemWallpaperStatus();
}
partial void OnSelectedWallpaperTypeChanged(SelectionOption value)
@@ -132,8 +161,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private void UpdateVisibility()
{
IsImage = SelectedWallpaperType?.Value == "Image";
IsImageOrVideo = IsImage;
IsImageOrVideo = IsImage || SelectedWallpaperType?.Value == "SystemWallpaper";
IsSolidColor = SelectedWallpaperType?.Value == "SolidColor";
IsSystemWallpaper = SelectedWallpaperType?.Value == "SystemWallpaper";
}
partial void OnSelectedColorChanged(string? value)
@@ -145,13 +175,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
partial void OnCustomColorChanged(Color value)
{
CustomColorBrush = new SolidColorBrush(value);
// 将自定义颜色应用到壁纸
var colorHex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}";
SelectedColor = colorHex;
if (_isInitializing) return;
SaveWallpaper();
}
partial void OnSelectedRefreshIntervalChanged(SelectionOption value)
{
if (_isInitializing) return;
SaveWallpaper();
}
public async Task ImportWallpaperAsync(string sourcePath)
{
var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
@@ -170,6 +205,12 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private void UpdatePreviewFromCurrentSelection()
{
if (IsSystemWallpaper)
{
UpdateSystemWallpaperPreview();
return;
}
if (!IsImage)
{
ClearPreviewImage();
@@ -180,10 +221,24 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
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;
if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path))
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
previousPreview?.Dispose();
PreviewImage = null;
@@ -193,7 +248,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
try
{
using var stream = System.IO.File.OpenRead(path);
using var stream = File.OpenRead(path);
var bitmap = new Bitmap(stream);
PreviewImage = bitmap;
PreviewBrush = WallpaperImageBrushFactory.Create(bitmap, SelectedWallpaperPlacement?.Value);
@@ -215,9 +270,21 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
previousPreview?.Dispose();
}
private void UpdateSystemWallpaperStatus()
{
if (!IsSystemWallpaper) return;
UpdateSystemWallpaperPreview();
}
[RelayCommand]
private void RefreshSystemWallpaper()
{
UpdateSystemWallpaperPreview();
}
partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
{
if (IsImage && PreviewImage is not null)
if ((IsImage || IsSystemWallpaper) && PreviewImage is not null)
{
PreviewBrush = WallpaperImageBrushFactory.Create(PreviewImage, value?.Value);
}
@@ -236,16 +303,46 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
{
var selectedType = SelectedWallpaperType?.Value ?? "Image";
var selectedPlacement = SelectedWallpaperPlacement?.Value ?? WallpaperImageBrushFactory.Fill;
var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath)
? null
: WallpaperPath;
var refreshIntervalSeconds = GetIntervalSeconds(SelectedRefreshInterval?.Value);
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}";
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
normalizedPath,
selectedType,
SelectedColor,
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()
@@ -262,10 +359,36 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private IReadOnlyList<SelectionOption> CreateWallpaperTypes()
{
return
[
var types = new List<SelectionOption>
{
new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")),
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.");
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import 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)

View File

@@ -27,7 +27,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
ISettingsFacadeService settingsFacade,
LocalizationService localizationService,
ILocationService locationService,
WeatherLocationRefreshService weatherLocationRefreshService)
WeatherLocationRefreshService weatherLocationRefreshService,
bool enableStartupPreviewRefresh = true)
{
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
@@ -52,7 +53,10 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
? LocationReadyText
: LocationUnsupportedText;
_ = RefreshPreviewAsync();
if (enableStartupPreviewRefresh)
{
_ = RefreshPreviewAsync();
}
}
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()
{
PageTitle = L("settings.weather.title", "Weather");

View File

@@ -2,6 +2,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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"
x:Class="LanMountainDesktop.Views.ComponentEditors.ClassScheduleComponentEditor">
<StackPanel Spacing="16">
@@ -36,6 +38,45 @@
</StackPanel>
</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"
Padding="20">
<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");
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");
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)
? FollowSystemColorSchemeItem
: 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;
}
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)
{
_ = sender;
@@ -106,6 +127,39 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
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)
{
_ = sender;
@@ -122,7 +176,15 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
AllowMultiple = false,
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"]
}

View File

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

View File

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

View File

@@ -253,7 +253,11 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
var today = DateOnly.FromDateTime(now);
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)
{
_courseItems = Array.Empty<CourseItemViewModel>();

View File

@@ -44,6 +44,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromMinutes(30)
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
@@ -102,12 +103,19 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
News2TitleTextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
RefreshButton.Click += OnRefreshButtonClick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyCellSize(_currentCellSize);
UpdateLanguageCode();
@@ -226,6 +234,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (_isRefreshing)
{
return;
@@ -242,6 +256,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnNewsItem1PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -253,6 +273,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnNewsItem2PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -264,6 +290,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnExtraNewsItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed ||
sender is not Control control ||
control.Tag is not int index)
@@ -408,6 +440,55 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
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()
{
return 2;

View File

@@ -60,6 +60,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromHours(6)
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
@@ -85,10 +86,17 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
ArtistTextBlock.FontFamily = MiSansFontFamily;
YearTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ApplyCellSize(_currentCellSize);
UpdateLanguageCode();
@@ -177,6 +185,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
private void OnArtworkPanelPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -188,6 +201,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
private void OnInfoPanelPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -420,6 +438,36 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
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()
{
var scale = ResolveScale();

View File

@@ -31,6 +31,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
Interval = TimeSpan.FromHours(6)
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
@@ -55,12 +56,19 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
ExampleTranslationTextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
RefreshButton.Click += OnRefreshButtonClick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyCellSize(_currentCellSize);
UpdateLanguageCode();
@@ -175,6 +183,12 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (_isRefreshing)
{
return;
@@ -284,6 +298,26 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
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()
{
var scale = ResolveScale();

View File

@@ -10,7 +10,7 @@
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
<Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusIsland}"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Background="#2D5A8E"
ClipToBounds="True"
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);
}
var leftWidthFactor = Math.Clamp(leftContentWidth / 122d, 0.48, 1.35);
TimeTextBlock.FontSize = Math.Clamp((metrics.PrimaryTemperatureFont * 0.74) * scale * compactFactor * leftWidthFactor, 10, 62);
DateTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * compactFactor * leftWidthFactor, 8, 30);
var weatherIconSize = Math.Clamp(metrics.IconFont * scale * compactFactor * leftWidthFactor, 9, 32);
var timeTextWidth = leftContentWidth * 0.92;
var timeCharCount = 5;
var maxTimeFontSize = timeTextWidth / (timeCharCount * 0.58);
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.Height = weatherIconSize;
@@ -226,11 +233,11 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
LeftStack.Width = leftContentWidth;
LeftStack.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;
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
? weatherIconSize + DateWeatherStack.Spacing
@@ -477,14 +484,22 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
: CreateBrush("#F8FAFF");
AnalogDialBorder.BorderBrush = CreateBrush(isNightMode ? "#34DDE7FF" : "#12000000");
TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
isNightMode ? "#F8FBFF" : "#10131A",
backgroundSamples,
WeatherTypographyAccessibility.WcagLargeTextContrast);
DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
isNightMode ? "#BCC8DD" : "#7A7E87",
backgroundSamples,
WeatherTypographyAccessibility.WcagNormalTextContrast);
if (isNightMode)
{
TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
"#F8FBFF",
backgroundSamples,
WeatherTypographyAccessibility.WcagLargeTextContrast);
DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
"#BCC8DD",
backgroundSamples,
WeatherTypographyAccessibility.WcagNormalTextContrast);
}
else
{
TimeTextBlock.Foreground = CreateBrush("#10131A");
DateTextBlock.Foreground = CreateBrush("#7A7E87");
}
_hourHandLine.Stroke = CreateBrush(isNightMode ? "#F1F5FF" : "#232938");
_minuteHandLine.Stroke = CreateBrush(isNightMode ? "#D6E0F2" : "#2F3749");

View File

@@ -95,6 +95,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
Interval = FluttermotionToken.WeatherAnimationFrameInterval
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
@@ -128,11 +129,19 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
InitializeComponent();
InitializeMotionTransform();
SizeChanged += OnSizeChanged;
if (_isDesignModePreview)
{
InitializeParticleVisuals();
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
_backgroundAnimationTimer.Tick += OnBackgroundAnimationTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
InitializeParticleVisuals();
ApplyVisualTheme(WeatherVisualKind.ClearDay);
@@ -512,6 +521,29 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
_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)
{
_activeVisualKind = kind;

View File

@@ -215,17 +215,21 @@ public partial class MainWindow
string? savedWallpaperPath,
string? type = null,
string? color = null,
string? placement = null)
string? placement = null,
int systemWallpaperRefreshIntervalSeconds = 300)
{
_wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath;
_wallpaperType = string.IsNullOrWhiteSpace(type) ? "Image" : type.Trim();
_wallpaperPlacement = WallpaperImageBrushFactory.NormalizePlacement(placement);
_wallpaperSolidColor = TryParseColor(color, out var parsedColor) ? parsedColor : null;
_wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured;
_systemWallpaperRefreshIntervalSeconds = systemWallpaperRefreshIntervalSeconds;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
StopSystemWallpaperTimer();
if (string.Equals(_wallpaperType, "SolidColor", StringComparison.OrdinalIgnoreCase))
{
_wallpaperMediaType = WallpaperMediaType.SolidColor;
@@ -235,6 +239,14 @@ public partial class MainWindow
return;
}
if (string.Equals(_wallpaperType, "SystemWallpaper", StringComparison.OrdinalIgnoreCase))
{
_wallpaperMediaType = WallpaperMediaType.Image;
LoadSystemWallpaper();
StartSystemWallpaperTimer();
return;
}
if (string.IsNullOrWhiteSpace(_wallpaperPath))
{
_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()
{
DesktopWallpaperImageLayer.Background = null;
@@ -480,7 +555,8 @@ public partial class MainWindow
snapshot.WallpaperPath,
snapshot.WallpaperType,
snapshot.WallpaperColor,
snapshot.WallpaperPlacement);
snapshot.WallpaperPlacement,
snapshot.SystemWallpaperRefreshIntervalSeconds);
if (!snapshot.IsNightMode.HasValue)
{
_isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold;
@@ -523,6 +599,7 @@ public partial class MainWindow
? latestWallpaperState.Color
: null,
WallpaperPlacement = latestWallpaperState.Placement,
SystemWallpaperRefreshIntervalSeconds = latestWallpaperState.SystemWallpaperRefreshIntervalSeconds,
LanguageCode = _languageCode,
TimeZoneId = _timeZoneService.CurrentTimeZone.Id,
WeatherLocationMode = latestWeatherState.LocationMode,

View File

@@ -122,6 +122,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private Color? _wallpaperSolidColor;
private string? _wallpaperPath;
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> _monetColors = Array.Empty<Color>();
private Color _selectedThemeColor = Color.Parse("#FF3B82F6");
@@ -160,7 +163,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
public MainWindow()
{
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
var pluginRuntimeService = Design.IsDesignMode
? null
: (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_settingsService = _settingsFacade.Settings;
_gridSettingsService = _settingsFacade.Grid;
@@ -173,7 +178,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
InitializeComponent();
Icon = _appLogoService.CreateWindowIcon();
InitializeTaskbarProfileFlyout();
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService,
@@ -183,6 +187,14 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
pluginRuntimeService);
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
_componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade);
if (Design.IsDesignMode)
{
ApplyDesignTimePreview();
return;
}
InitializeTaskbarProfileFlyout();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
_settingsService.Changed += OnSettingsChanged;
_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)
{
if (sender is not ToggleButton toggleButton)
@@ -231,6 +407,14 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (Design.IsDesignMode)
{
ConfigureDesignTimeDesktopGrid();
PopulateDesignTimeDesktopSurface();
return;
}
SyncSettingsWindowState();
_suppressSettingsPersistence = true;
@@ -307,6 +491,12 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
protected override void OnClosed(EventArgs e)
{
if (Design.IsDesignMode)
{
base.OnClosed(e);
return;
}
var wasVisible = IsVisible;
var windowState = WindowState.ToString();

View File

@@ -77,10 +77,10 @@
<Setter Property="Margin" Value="2" />
</Style>
</WrapPanel.Styles>
<HyperlinkButton NavigateUri="https://github.com/Lincube/LanMountainDesktop">
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop">
<TextBlock Text="GitHub 仓库" />
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://github.com/Lincube/LanMountainDesktop/issues">
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop/issues">
<TextBlock Text="问题反馈" />
</HyperlinkButton>
</WrapPanel>

View File

@@ -12,12 +12,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class GeneratedPluginSettingsPage : SettingsPageBase
{
public GeneratedPluginSettingsPage()
: this(
new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US")))
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{
}
@@ -223,4 +218,272 @@ public partial class GeneratedPluginSettingsPage : SettingsPageBase
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.Services;
using LanMountainDesktop.Services.PluginMarket;
@@ -17,7 +19,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginMarketSettingsPage : SettingsPageBase
{
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)
{
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync();
}
@@ -48,6 +55,113 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
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)
{
RequestRestart(reason ?? ViewModel.RestartRequiredMessage);
@@ -60,4 +174,17 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
OpenDrawer(drawer, detailViewModel.DrawerTitle);
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.Services.Settings;
using LanMountainDesktop.ViewModels;
@@ -15,7 +16,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginsSettingsPage : SettingsPageBase
{
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)
{
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync();
}
@@ -38,4 +44,47 @@ public partial class PluginsSettingsPage : SettingsPageBase
{
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

@@ -29,6 +29,11 @@
<!-- 纯色预览 -->
<Border Background="{Binding SelectedColor}"
IsVisible="{Binding IsSolidColor}" />
<!-- 系统壁纸预览 -->
<Border Background="#FFF6F7F9"
IsVisible="{Binding IsSystemWallpaper}">
<Border Background="{Binding PreviewBrush}" />
</Border>
</Panel>
</Border>
</Viewbox>
@@ -135,6 +140,19 @@
</Button>
</UniformGrid>
</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>
<Separator Classes="settings-separator" Margin="0,0,0,24" />
@@ -183,10 +201,60 @@
</ui:SettingsExpander.Footer>
</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}"
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">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Maximize" />

View File

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

View File

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