using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.SettingsPages; internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable { private static readonly IBrush SurfaceBrush = new SolidColorBrush(Color.Parse("#14000000")); private static readonly IBrush SelectedSurfaceBrush = new SolidColorBrush(Color.Parse("#1A0EA5E9")); private static readonly IBrush CardBorderBrush = new SolidColorBrush(Color.Parse("#24FFFFFF")); private static readonly IBrush SelectedBorderBrush = new SolidColorBrush(Color.Parse("#7C0EA5E9")); private static readonly IBrush IconSurfaceBrush = new SolidColorBrush(Color.Parse("#221E3A8A")); private static readonly IBrush ChipBrush = new SolidColorBrush(Color.Parse("#22000000")); private static readonly IBrush MutedBrush = new SolidColorBrush(Color.Parse("#CC94A3B8")); private static readonly IBrush SuccessBrush = new SolidColorBrush(Color.Parse("#FF0F766E")); private static readonly IBrush WarningBrush = new SolidColorBrush(Color.Parse("#FF9A6700")); private static readonly IBrush ErrorBrush = new SolidColorBrush(Color.Parse("#FFC42B1C")); private readonly AppSettingsService _appSettingsService = new(); private readonly LocalizationService _localizationService = new(); private readonly PluginRuntimeService _runtime; private readonly AirAppMarketIndexService _indexService; private readonly AirAppMarketInstallService _installService; private readonly AirAppMarketReadmeService _readmeService; private readonly AirAppMarketIconService _iconService; private readonly Version? _hostVersion; private readonly TextBox _searchTextBox; private readonly Button _refreshButton; private readonly TextBlock _statusTextBlock; private readonly StackPanel _pluginListHost; private readonly Border _detailBorder; private AirAppMarketIndexDocument? _document; private AirAppMarketPluginEntry? _selectedPlugin; private Dictionary _installedPlugins = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _readmeContents = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _readmeErrors = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _iconBitmaps = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _loadingIconPluginIds = new(StringComparer.OrdinalIgnoreCase); private string _marketSourceDisplay = AirAppMarketDefaults.DefaultIndexUrl; private string? _loadingReadmePluginId; private string? _installingPluginId; private bool _isRefreshing; private bool _isInstalling; private bool _hasLoadedOnce; public PluginMarketEmbeddedView(PluginRuntimeService runtime) { _runtime = runtime; var dataDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LanMountainDesktop", "AirAppMarket"); _indexService = new AirAppMarketIndexService(new AirAppMarketCacheService(dataDirectory)); _installService = new AirAppMarketInstallService(runtime, dataDirectory); _readmeService = new AirAppMarketReadmeService(); _iconService = new AirAppMarketIconService(); _hostVersion = typeof(App).Assembly.GetName().Version; _searchTextBox = new TextBox { MinWidth = 260, Watermark = T("market.toolbar.search_placeholder", "Search plugins") }; _searchTextBox.PropertyChanged += (_, e) => { if (e.Property == TextBox.TextProperty) { RebuildSurface(); } }; _refreshButton = new Button { Content = T("market.toolbar.refresh", "Refresh"), HorizontalAlignment = HorizontalAlignment.Right }; _refreshButton.Click += OnRefreshClick; _statusTextBlock = new TextBlock { Text = T("market.status.loading", "Loading the official plugin market..."), TextWrapping = TextWrapping.Wrap, Foreground = WarningBrush }; _pluginListHost = new StackPanel { Spacing = 10 }; _detailBorder = CreatePanelShell(18); Content = BuildLayout(); AttachedToVisualTree += async (_, _) => { if (_hasLoadedOnce) { return; } _hasLoadedOnce = true; await RefreshAsync(); }; } public void RefreshInstalledSnapshot() { _installedPlugins = _runtime.Catalog .ToDictionary(entry => entry.Manifest.Id, StringComparer.OrdinalIgnoreCase); RebuildSurface(); } public void RefreshLocalization() { _searchTextBox.Watermark = T("market.toolbar.search_placeholder", "Search plugins"); _refreshButton.Content = T("market.toolbar.refresh", "Refresh"); RebuildSurface(); } public void Dispose() { foreach (var bitmap in _iconBitmaps.Values) { bitmap?.Dispose(); } _iconBitmaps.Clear(); _iconService.Dispose(); _readmeService.Dispose(); _installService.Dispose(); _indexService.Dispose(); } private Control BuildLayout() { var root = new Grid { RowDefinitions = new RowDefinitions("Auto,*"), RowSpacing = 16 }; var toolbar = new Grid { RowDefinitions = new RowDefinitions("Auto,Auto"), RowSpacing = 8 }; var actionRow = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto"), ColumnSpacing = 12 }; actionRow.Children.Add(_searchTextBox); actionRow.Children.Add(_refreshButton); Grid.SetColumn(_refreshButton, 1); toolbar.Children.Add(actionRow); toolbar.Children.Add(_statusTextBlock); Grid.SetRow(_statusTextBlock, 1); var contentGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("430,*"), ColumnSpacing = 16 }; var listShell = CreatePanelShell(14); listShell.Child = new ScrollViewer { Content = _pluginListHost, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled }; contentGrid.Children.Add(listShell); contentGrid.Children.Add(_detailBorder); Grid.SetColumn(_detailBorder, 1); root.Children.Add(toolbar); root.Children.Add(contentGrid); Grid.SetRow(contentGrid, 1); return root; } private async void OnRefreshClick(object? sender, RoutedEventArgs e) { await RefreshAsync(); } private async Task RefreshAsync() { if (_isRefreshing) { return; } _isRefreshing = true; _refreshButton.IsEnabled = false; SetStatus(T("market.status.loading", "Loading the official plugin market..."), WarningBrush); try { RefreshInstalledSnapshot(); var result = await _indexService.LoadAsync(); if (!result.Success || result.Document is null) { _document = null; _selectedPlugin = null; SetStatus( F( "market.status.load_failed_format", "Failed to load the plugin market: {0}", result.ErrorMessage ?? T("market.detail.unknown", "Unknown")), ErrorBrush); RebuildSurface(); return; } _document = result.Document; _marketSourceDisplay = result.SourceLocation ?? AirAppMarketDefaults.DefaultIndexUrl; _selectedPlugin = ResolveSelectedPlugin(_selectedPlugin?.Id, result.Document.Plugins); var statusMessage = result.Source == AirAppMarketLoadSource.Cache ? F( "market.status.loaded_cache_format", "Official source unavailable. Loaded {0} plugin(s) from cache. Reason: {1}", result.Document.Plugins.Count, result.WarningMessage ?? T("market.detail.unknown", "Unknown")) : F( "market.status.loaded_network_format", "Loaded {0} plugin(s) from the official source.", result.Document.Plugins.Count); SetStatus(statusMessage, result.Source == AirAppMarketLoadSource.Cache ? WarningBrush : SuccessBrush); RebuildSurface(); await EnsureReadmeLoadedAsync(_selectedPlugin); } finally { _isRefreshing = false; _refreshButton.IsEnabled = true; } } private void RebuildSurface() { var filteredPlugins = GetFilteredPlugins(); _selectedPlugin = filteredPlugins.Count > 0 ? ResolveSelectedPlugin(_selectedPlugin?.Id, filteredPlugins) : null; BuildPluginList(filteredPlugins); BuildDetailPanel(); _ = EnsureReadmeLoadedAsync(_selectedPlugin); foreach (var plugin in filteredPlugins) { _ = EnsureIconLoadedAsync(plugin); } } private List GetFilteredPlugins() { if (_document is null) { return []; } var query = (_searchTextBox.Text ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(query)) { return _document.Plugins.ToList(); } return _document.Plugins .Where(plugin => plugin.Name.Contains(query, StringComparison.OrdinalIgnoreCase) || plugin.Description.Contains(query, StringComparison.OrdinalIgnoreCase) || plugin.Author.Contains(query, StringComparison.OrdinalIgnoreCase) || plugin.Id.Contains(query, StringComparison.OrdinalIgnoreCase) || plugin.Tags.Any(tag => tag.Contains(query, StringComparison.OrdinalIgnoreCase))) .ToList(); } private void BuildPluginList(IReadOnlyList plugins) { _pluginListHost.Children.Clear(); if (_document is null) { _pluginListHost.Children.Add(CreateEmptyState(T("market.list.empty", "The plugin market has not been loaded yet."))); return; } if (plugins.Count == 0) { _pluginListHost.Children.Add(CreateEmptyState(T("market.list.no_results", "No plugins match the current search."))); return; } foreach (var plugin in plugins) { _pluginListHost.Children.Add(CreatePluginListItem(plugin)); } } private Control CreatePluginListItem(AirAppMarketPluginEntry plugin) { var installState = ResolveInstallState(plugin, out var installedPlugin); var isCompatible = IsCompatibleWithHost(plugin); var isSelected = string.Equals(_selectedPlugin?.Id, plugin.Id, StringComparison.OrdinalIgnoreCase); var titleBlock = new TextBlock { Text = plugin.Name, FontSize = 16, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap }; var subtitleBlock = new TextBlock { Text = F("market.card.subtitle_format", "{0} | v{1}", plugin.Author, plugin.Version), Foreground = MutedBrush, TextWrapping = TextWrapping.Wrap }; var descriptionBlock = new TextBlock { Text = plugin.Description, TextWrapping = TextWrapping.Wrap, MaxHeight = 40 }; var chips = CreateChipWrapPanel( CreateStateChip(T(StateKey(installState), StateFallback(installState)))); if (installedPlugin is not null) { chips.Children.Add(CreateStateChip(installedPlugin.IsLoaded ? T("market.card.loaded", "Loaded") : T("market.card.pending_restart", "Restart required"))); } foreach (var tag in plugin.Tags.Take(3)) { chips.Children.Add(CreateStateChip(tag)); } var summaryStack = new StackPanel { Spacing = 6, VerticalAlignment = VerticalAlignment.Center, Children = { titleBlock, subtitleBlock, descriptionBlock, chips } }; var selectGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*"), ColumnSpacing = 14, Children = { CreatePluginIcon(plugin, 56), summaryStack } }; Grid.SetColumn(summaryStack, 1); var selectButton = new Button { Background = Brushes.Transparent, BorderThickness = new Thickness(0), Padding = new Thickness(0), HorizontalContentAlignment = HorizontalAlignment.Stretch, Content = selectGrid }; selectButton.Click += async (_, _) => await SelectPluginAsync(plugin); var rightPanel = new StackPanel { Spacing = 8, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top, Children = { CreateInstallButton(plugin, installState, isCompatible, 96), new TextBlock { Text = installedPlugin is null ? string.Empty : installedPlugin.IsLoaded ? T("market.card.loaded", "Loaded") : T("market.card.pending_restart", "Restart required"), FontSize = 12, Foreground = MutedBrush, HorizontalAlignment = HorizontalAlignment.Right, IsVisible = installedPlugin is not null } } }; var layoutGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto"), ColumnSpacing = 14, Children = { selectButton, rightPanel } }; Grid.SetColumn(rightPanel, 1); return new Border { Background = isSelected ? SelectedSurfaceBrush : SurfaceBrush, BorderBrush = isSelected ? SelectedBorderBrush : CardBorderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(18), Padding = new Thickness(14), Child = layoutGrid }; } private void BuildDetailPanel() { if (_selectedPlugin is null) { _detailBorder.Child = CreateEmptyState(T("market.detail.placeholder", "Select a plugin on the left to inspect details.")); return; } var plugin = _selectedPlugin; var installState = ResolveInstallState(plugin, out var installedPlugin); var isCompatible = IsCompatibleWithHost(plugin); var headerSummary = new StackPanel { Spacing = 6, Children = { new TextBlock { Text = plugin.Name, FontSize = 26, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap }, new TextBlock { Text = F("market.detail.author_subtitle_format", "By {0}", plugin.Author), Foreground = MutedBrush, TextWrapping = TextWrapping.Wrap }, new TextBlock { Text = plugin.Description, TextWrapping = TextWrapping.Wrap } } }; var headerChips = CreateChipWrapPanel( CreateStateChip(T(StateKey(installState), StateFallback(installState))), CreateStateChip(plugin.GetVersionSummary())); foreach (var tag in plugin.Tags) { headerChips.Children.Add(CreateStateChip(tag)); } headerSummary.Children.Add(headerChips); var headerGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"), ColumnSpacing = 16, Children = { CreatePluginIcon(plugin, 76), headerSummary, CreateInstallButton(plugin, installState, isCompatible, 120) } }; Grid.SetColumn(headerSummary, 1); Grid.SetColumn(headerGrid.Children[2], 2); var detailPanel = new StackPanel { Spacing = 18, Children = { headerGrid } }; if (!isCompatible) { detailPanel.Children.Add(new Border { Background = new SolidColorBrush(Color.Parse("#24FFC42B1C")), CornerRadius = new CornerRadius(14), Padding = new Thickness(12), Child = new TextBlock { Text = F( "market.status.host_incompatible_format", "This host is too old. Version {0} or newer is required.", plugin.MinHostVersion), Foreground = ErrorBrush, TextWrapping = TextWrapping.Wrap } }); } detailPanel.Children.Add(CreateSectionTitle(T("market.detail.readme", "README"))); detailPanel.Children.Add(new Border { Background = SurfaceBrush, BorderBrush = CardBorderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(16), Padding = new Thickness(16), Child = new TextBlock { Text = GetReadmeContent(plugin), TextWrapping = TextWrapping.Wrap } }); detailPanel.Children.Add(CreateSectionTitle(T("market.detail.plugin_information", "Plugin Information"))); detailPanel.Children.Add(CreatePluginInfoSection(plugin, installedPlugin, installState)); _detailBorder.Child = new ScrollViewer { Content = detailPanel, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled }; } private Control CreatePluginInfoSection( AirAppMarketPluginEntry plugin, PluginCatalogEntry? installedPlugin, AirAppMarketInstallState installState) { var infoPanel = new StackPanel { Spacing = 14 }; var cardWrap = new WrapPanel { Orientation = Orientation.Horizontal }; foreach (var card in new[] { CreateInfoCard(T("market.detail.version", "Version"), $"v{plugin.Version}"), CreateInfoCard(T("market.detail.installed_version", "Installed Version"), installedPlugin?.Manifest.Version ?? T("market.detail.not_installed", "Not installed")), CreateInfoCard(T("market.detail.api_version", "API Version"), plugin.ApiVersion), CreateInfoCard(T("market.detail.min_host_version", "Minimum Host Version"), plugin.MinHostVersion), CreateInfoCard(T("market.detail.package_size", "Package Size"), FormatPackageSize(plugin.PackageSizeBytes)), CreateInfoCard(T("market.detail.published_at", "Published At"), FormatTimestamp(plugin.PublishedAt)), CreateInfoCard(T("market.detail.updated_at", "Updated At"), FormatTimestamp(plugin.UpdatedAt)), CreateInfoCard(T("market.detail.state", "Install State"), T(StateKey(installState), StateFallback(installState))) }) { cardWrap.Children.Add(card); } infoPanel.Children.Add(cardWrap); infoPanel.Children.Add(CreateInfoRow(T("market.detail.tags", "Tags"), plugin.Tags.Count == 0 ? T("market.detail.unknown", "Unknown") : string.Join(", ", plugin.Tags))); infoPanel.Children.Add(CreateInfoRow(T("market.detail.project", "Project"), plugin.ProjectUrl)); infoPanel.Children.Add(CreateInfoRow(T("market.detail.homepage", "Homepage"), plugin.HomepageUrl)); infoPanel.Children.Add(CreateInfoRow(T("market.detail.repository", "Repository"), plugin.RepositoryUrl)); infoPanel.Children.Add(CreateInfoRow(T("market.detail.market_source", "Market Source"), _marketSourceDisplay)); if (!string.IsNullOrWhiteSpace(plugin.ReleaseNotes)) { infoPanel.Children.Add(CreateInfoRow(T("market.detail.release_notes", "Release Notes"), plugin.ReleaseNotes)); } return infoPanel; } private Button CreateInstallButton( AirAppMarketPluginEntry plugin, AirAppMarketInstallState installState, bool isCompatible, double minWidth) { var isThisInstalling = _isInstalling && string.Equals(_installingPluginId, plugin.Id, StringComparison.OrdinalIgnoreCase); var button = new Button { Content = isThisInstalling ? T("market.button.installing", "Installing...") : T(ButtonKey(installState), ButtonFallback(installState)), IsEnabled = !_isInstalling && isCompatible && installState != AirAppMarketInstallState.Installed, MinWidth = minWidth, HorizontalAlignment = HorizontalAlignment.Right }; button.Click += async (_, _) => { _selectedPlugin = plugin; RebuildSurface(); await EnsureReadmeLoadedAsync(plugin); await InstallSelectedPluginAsync(plugin); }; return button; } private async Task SelectPluginAsync(AirAppMarketPluginEntry plugin) { _selectedPlugin = plugin; RebuildSurface(); await EnsureReadmeLoadedAsync(plugin); } private async Task InstallSelectedPluginAsync(AirAppMarketPluginEntry plugin) { if (_isInstalling) { return; } _isInstalling = true; _installingPluginId = plugin.Id; BuildDetailPanel(); BuildPluginList(GetFilteredPlugins()); SetStatus( F("market.status.installing_format", "Downloading and staging plugin '{0}'...", plugin.Name), WarningBrush); try { var result = await _installService.InstallAsync(plugin); if (!result.Success || result.Manifest is null) { SetStatus( F( "market.status.install_failed_format", "Failed to install plugin: {0}", result.ErrorMessage ?? T("market.detail.unknown", "Unknown")), ErrorBrush); return; } RefreshInstalledSnapshot(); SetStatus( F( "market.status.install_success_format", "Plugin '{0}' has been staged. Restart the app to apply it.", result.Manifest.Name), SuccessBrush); RebuildSurface(); } finally { _isInstalling = false; _installingPluginId = null; RebuildSurface(); } } private async Task EnsureReadmeLoadedAsync(AirAppMarketPluginEntry? plugin) { if (plugin is null || _readmeContents.ContainsKey(plugin.Id) || string.Equals(_loadingReadmePluginId, plugin.Id, StringComparison.OrdinalIgnoreCase)) { return; } _loadingReadmePluginId = plugin.Id; _readmeErrors.Remove(plugin.Id); BuildDetailPanel(); try { var readme = await _readmeService.LoadAsync(plugin); _readmeContents[plugin.Id] = string.IsNullOrWhiteSpace(readme) ? T("market.detail.readme_empty", "README is empty.") : readme.Trim(); } catch (Exception ex) { _readmeErrors[plugin.Id] = ex.Message; } finally { _loadingReadmePluginId = null; if (string.Equals(_selectedPlugin?.Id, plugin.Id, StringComparison.OrdinalIgnoreCase)) { BuildDetailPanel(); } } } private async Task EnsureIconLoadedAsync(AirAppMarketPluginEntry? plugin) { if (plugin is null || _iconBitmaps.ContainsKey(plugin.Id) || !_loadingIconPluginIds.Add(plugin.Id)) { return; } try { _iconBitmaps[plugin.Id] = await _iconService.LoadAsync(plugin); } catch { _iconBitmaps[plugin.Id] = null; } finally { _loadingIconPluginIds.Remove(plugin.Id); RebuildSurface(); } } private string GetReadmeContent(AirAppMarketPluginEntry plugin) { if (_readmeContents.TryGetValue(plugin.Id, out var readme)) { return readme; } if (_readmeErrors.TryGetValue(plugin.Id, out var error)) { return F( "market.detail.readme_error_format", "README could not be loaded: {0}", error); } if (string.Equals(_loadingReadmePluginId, plugin.Id, StringComparison.OrdinalIgnoreCase)) { return T("market.detail.readme_loading", "Loading README..."); } return plugin.ReleaseNotes; } private AirAppMarketPluginEntry? ResolveSelectedPlugin( string? selectedPluginId, IReadOnlyList plugins) { if (plugins.Count == 0) { return null; } if (!string.IsNullOrWhiteSpace(selectedPluginId)) { var existing = plugins.FirstOrDefault(plugin => string.Equals(plugin.Id, selectedPluginId, StringComparison.OrdinalIgnoreCase)); if (existing is not null) { return existing; } } return plugins[0]; } private AirAppMarketInstallState ResolveInstallState( AirAppMarketPluginEntry plugin, out PluginCatalogEntry? installedPlugin) { if (!_installedPlugins.TryGetValue(plugin.Id, out installedPlugin)) { return AirAppMarketInstallState.NotInstalled; } return CompareVersions(plugin.Version, installedPlugin.Manifest.Version) > 0 ? AirAppMarketInstallState.UpdateAvailable : AirAppMarketInstallState.Installed; } private bool IsCompatibleWithHost(AirAppMarketPluginEntry plugin) { if (_hostVersion is null || !AirAppMarketIndexDocument.TryParseVersion(plugin.MinHostVersion, out var minHostVersion) || minHostVersion is null) { return true; } return _hostVersion >= minHostVersion; } private void SetStatus(string message, IBrush foreground) { _statusTextBlock.Text = message; _statusTextBlock.Foreground = foreground; } private static int CompareVersions(string? left, string? right) { if (!AirAppMarketIndexDocument.TryParseVersion(left, out var leftVersion)) { leftVersion = new Version(0, 0, 0); } if (!AirAppMarketIndexDocument.TryParseVersion(right, out var rightVersion)) { rightVersion = new Version(0, 0, 0); } return (leftVersion ?? new Version(0, 0, 0)).CompareTo(rightVersion ?? new Version(0, 0, 0)); } private Border CreatePanelShell(double padding) { return new Border { Background = SurfaceBrush, CornerRadius = new CornerRadius(18), Padding = new Thickness(padding) }; } private Control CreateEmptyState(string text) { return new Border { Background = SurfaceBrush, CornerRadius = new CornerRadius(16), BorderBrush = CardBorderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(18), Child = new TextBlock { Text = text, TextWrapping = TextWrapping.Wrap } }; } private Control CreatePluginIcon(AirAppMarketPluginEntry plugin, double size) { if (!_iconBitmaps.ContainsKey(plugin.Id)) { _ = EnsureIconLoadedAsync(plugin); } Control iconChild; if (_iconBitmaps.TryGetValue(plugin.Id, out var bitmap) && bitmap is not null) { iconChild = new Image { Source = bitmap, Stretch = Stretch.UniformToFill }; } else { var glyph = string.IsNullOrWhiteSpace(plugin.Name) ? "?" : plugin.Name.Trim()[0].ToString().ToUpperInvariant(); iconChild = new TextBlock { Text = glyph, FontSize = Math.Max(18, size * 0.32), FontWeight = FontWeight.Bold, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, TextAlignment = TextAlignment.Center }; } return new Border { Width = size, Height = size, CornerRadius = new CornerRadius(Math.Max(12, size * 0.24)), ClipToBounds = true, Background = IconSurfaceBrush, Child = iconChild }; } private WrapPanel CreateChipWrapPanel(params Control[] chips) { var panel = new WrapPanel { Orientation = Orientation.Horizontal }; foreach (var chip in chips) { chip.Margin = new Thickness(0, 0, 8, 8); panel.Children.Add(chip); } return panel; } private Border CreateStateChip(string text) { return new Border { Background = ChipBrush, CornerRadius = new CornerRadius(999), Padding = new Thickness(10, 4), Child = new TextBlock { Text = text, FontSize = 12 } }; } private TextBlock CreateSectionTitle(string text) { return new TextBlock { Text = text, FontSize = 18, FontWeight = FontWeight.SemiBold }; } private Border CreateInfoCard(string label, string value) { return new Border { Width = 190, Margin = new Thickness(0, 0, 12, 12), Background = SurfaceBrush, BorderBrush = CardBorderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(14), Child = new StackPanel { Spacing = 6, Children = { new TextBlock { Text = label, FontSize = 12, Foreground = MutedBrush }, new TextBlock { Text = string.IsNullOrWhiteSpace(value) ? T("market.detail.unknown", "Unknown") : value, TextWrapping = TextWrapping.Wrap } } } }; } private Control CreateInfoRow(string label, string value) { return new Border { Background = SurfaceBrush, BorderBrush = CardBorderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(14), Child = new StackPanel { Spacing = 6, Children = { new TextBlock { Text = label, FontSize = 12, Foreground = MutedBrush }, new TextBlock { Text = string.IsNullOrWhiteSpace(value) ? T("market.detail.unknown", "Unknown") : value, TextWrapping = TextWrapping.Wrap } } } }; } private static string FormatPackageSize(long packageSizeBytes) { var size = packageSizeBytes; string[] units = ["B", "KB", "MB", "GB"]; var unitIndex = 0; decimal display = size; while (display >= 1024 && unitIndex < units.Length - 1) { display /= 1024; unitIndex++; } return string.Format( CultureInfo.CurrentCulture, display >= 10 || unitIndex == 0 ? "{0:0} {1}" : "{0:0.0} {1}", display, units[unitIndex]); } private static string FormatTimestamp(DateTimeOffset timestamp) { if (timestamp == default) { return string.Empty; } return timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm", CultureInfo.CurrentCulture); } private string T(string key, string fallback) { var snapshot = _appSettingsService.Load(); return _localizationService.GetString(snapshot.LanguageCode, key, fallback); } private string F(string key, string fallback, params object[] args) { return string.Format(CultureInfo.CurrentCulture, T(key, fallback), args); } private static string StateKey(AirAppMarketInstallState state) { return state switch { AirAppMarketInstallState.UpdateAvailable => "market.detail.state.update_available", AirAppMarketInstallState.Installed => "market.detail.state.installed", _ => "market.detail.state.not_installed" }; } private static string StateFallback(AirAppMarketInstallState state) { return state switch { AirAppMarketInstallState.UpdateAvailable => "Update available", AirAppMarketInstallState.Installed => "Installed", _ => "Not installed" }; } private static string ButtonKey(AirAppMarketInstallState state) { return state switch { AirAppMarketInstallState.UpdateAvailable => "market.button.update", AirAppMarketInstallState.Installed => "market.button.installed", _ => "market.button.install" }; } private static string ButtonFallback(AirAppMarketInstallState state) { return state switch { AirAppMarketInstallState.UpdateAvailable => "Update", AirAppMarketInstallState.Installed => "Installed", _ => "Install" }; } }