From 1a7dde34d0ff73aa9544bcb3c98fcbdbcd9fe626 Mon Sep 17 00:00:00 2001 From: lincube Date: Sun, 22 Mar 2026 02:53:31 +0800 Subject: [PATCH] 0.7.3.1 --- LanMountainDesktop/App.axaml.cs | 34 ++- LanMountainDesktop/Program.cs | 9 +- .../WeatherSettingsPageViewModel.cs | 67 ++++- .../Views/Components/BrowserWidget.axaml | 28 +- .../Views/Components/BrowserWidget.axaml.cs | 89 +++++- .../Components/CnrDailyNewsWidget.axaml.cs | 85 +++++- .../Components/DailyArtworkWidget.axaml.cs | 50 +++- .../Views/Components/DailyWordWidget.axaml.cs | 38 ++- .../Views/Components/WeatherWidget.axaml.cs | 34 ++- LanMountainDesktop/Views/MainWindow.axaml.cs | 191 +++++++++++- .../GeneratedPluginSettingsPage.axaml.cs | 275 +++++++++++++++++- .../PluginMarketSettingsPage.axaml.cs | 129 +++++++- .../PluginsSettingsPage.axaml.cs | 51 +++- .../WeatherSettingsPage.axaml.cs | 15 +- PRODUCT_BRIEF.md | 27 +- 15 files changed, 1058 insertions(+), 64 deletions(-) diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 729d424..94b2912 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -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( diff --git a/LanMountainDesktop/Program.cs b/LanMountainDesktop/Program.cs index 4cdb8e1..d1b4f07 100644 --- a/LanMountainDesktop/Program.cs +++ b/LanMountainDesktop/Program.cs @@ -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() .UsePlatformDetect() diff --git a/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs index b6f1b83..29c621c 100644 --- a/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs +++ b/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs @@ -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 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"); diff --git a/LanMountainDesktop/Views/Components/BrowserWidget.axaml b/LanMountainDesktop/Views/Components/BrowserWidget.axaml index 86e998d..1092a90 100644 --- a/LanMountainDesktop/Views/Components/BrowserWidget.axaml +++ b/LanMountainDesktop/Views/Components/BrowserWidget.axaml @@ -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"> - + - + + + + diff --git a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs index 81163f9..43bf1bf 100644 --- a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs @@ -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; diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs index 2ccc644..62c68ee 100644 --- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs @@ -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; diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs index 259820e..b800f4a 100644 --- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs @@ -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(); diff --git a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs index 26663f1..f326797 100644 --- a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs @@ -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(); diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs index 1371990..b1af769 100644 --- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs @@ -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; diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index e176156..0372d6f 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -160,7 +160,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 +175,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider InitializeComponent(); Icon = _appLogoService.CreateWindowIcon(); - InitializeTaskbarProfileFlyout(); _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( _componentRegistry, pluginRuntimeService, @@ -183,6 +184,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().FirstOrDefault(); _settingsService.Changed += OnSettingsChanged; _appearanceThemeService.Changed += OnAppearanceThemeChanged; @@ -196,6 +205,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 +404,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 +488,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(); diff --git a/LanMountainDesktop/Views/SettingsPages/GeneratedPluginSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/GeneratedPluginSettingsPage.axaml.cs index 20727f5..273d800 100644 --- a/LanMountainDesktop/Views/SettingsPages/GeneratedPluginSettingsPage.axaml.cs +++ b/LanMountainDesktop/Views/SettingsPages/GeneratedPluginSettingsPage.axaml.cs @@ -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 _values = new(StringComparer.OrdinalIgnoreCase); + + public event EventHandler? Changed; + + public T LoadSnapshot(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new() + => new(); + + public void SaveSnapshot( + SettingsScope scope, + T snapshot, + string? subjectId = null, + string? placementId = null, + string? sectionId = null, + IReadOnlyCollection? changedKeys = null) + { + RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys); + } + + public T LoadSection( + SettingsScope scope, + string subjectId, + string sectionId, + string? placementId = null) where T : new() + => new(); + + public void SaveSection( + SettingsScope scope, + string subjectId, + string sectionId, + T section, + string? placementId = null, + IReadOnlyCollection? 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( + 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(value) + : default; + } + + public void SetValue( + SettingsScope scope, + string key, + T value, + string? subjectId = null, + string? placementId = null, + string? sectionId = null, + IReadOnlyCollection? 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(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? 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() where T : new() + => _settingsService.LoadSnapshot(SettingsScope.ComponentInstance, ComponentId, PlacementId); + + public void SaveSnapshot(T snapshot, IReadOnlyCollection? changedKeys = null) + => _settingsService.SaveSnapshot(SettingsScope.ComponentInstance, snapshot, ComponentId, PlacementId, changedKeys: changedKeys); + + public T LoadSection(string sectionId) where T : new() + => _settingsService.LoadSection(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId); + + public void SaveSection(string sectionId, T section, IReadOnlyCollection? 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(string key) + => _settingsService.GetValue(SettingsScope.ComponentInstance, key, ComponentId, PlacementId); + + public void SetValue(string key, T value, IReadOnlyCollection? changedKeys = null) + => _settingsService.SetValue(SettingsScope.ComponentInstance, key, value, ComponentId, PlacementId, changedKeys: changedKeys); + } } diff --git a/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml.cs index cff8e8f..29385c8 100644 --- a/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml.cs +++ b/LanMountainDesktop/Views/SettingsPages/PluginMarketSettingsPage.axaml.cs @@ -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; + } } diff --git a/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml.cs index 5bed9db..a5e9966 100644 --- a/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml.cs +++ b/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml.cs @@ -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; + } } diff --git a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml.cs index 05f91f8..aede275 100644 --- a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml.cs +++ b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml.cs @@ -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; } } diff --git a/PRODUCT_BRIEF.md b/PRODUCT_BRIEF.md index 4f91e17..2b67f72 100644 --- a/PRODUCT_BRIEF.md +++ b/PRODUCT_BRIEF.md @@ -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 收集匿名数据(具体数据需后台查看) ---- - -**一句话总结**:阑山桌面是一款面向个人用户的可定制桌面工具,专注个人学习办公场景,通过组件化设计和插件生态提供轻量、开放、跨平台的桌面信息聚合方案。