diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceExtensions.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceExtensions.cs new file mode 100644 index 0000000..879c5d9 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/PluginAppearanceExtensions.cs @@ -0,0 +1,87 @@ +using Avalonia; + +namespace LanMountainDesktop.PluginSdk; + +public static class PluginAppearanceExtensions +{ + public static CornerRadius ResolveCornerRadius( + this PluginAppearanceSnapshot snapshot, + PluginCornerRadiusPreset preset) + { + ArgumentNullException.ThrowIfNull(snapshot); + var value = snapshot.CornerRadiusTokens.Get(preset); + return new CornerRadius(Math.Max(0d, value)); + } + + public static CornerRadius ResolveCornerRadius( + this PluginAppearanceSnapshot snapshot, + PluginCornerRadiusPreset preset, + CornerRadius fallback) + { + ArgumentNullException.ThrowIfNull(snapshot); + var value = snapshot.CornerRadiusTokens.Get(preset); + if (!double.IsFinite(value) || value < 0) + { + return fallback; + } + return new CornerRadius(value); + } + + public static CornerRadius ResolveCornerRadius( + this IPluginAppearanceContext context, + PluginCornerRadiusPreset preset) + { + ArgumentNullException.ThrowIfNull(context); + var value = context.ResolveCornerRadius(preset); + return new CornerRadius(Math.Max(0d, value)); + } + + public static CornerRadius ResolveCornerRadius( + this IPluginAppearanceContext context, + PluginCornerRadiusPreset preset, + double minimum, + double maximum) + { + ArgumentNullException.ThrowIfNull(context); + var value = context.ResolveCornerRadius(preset, minimum, maximum); + return new CornerRadius(Math.Max(0d, value)); + } + + public static CornerRadius ResolveScaledCornerRadius( + this IPluginAppearanceContext context, + double baseRadius) + { + ArgumentNullException.ThrowIfNull(context); + var value = context.ResolveScaledCornerRadius(baseRadius); + return new CornerRadius(Math.Max(0d, value)); + } + + public static CornerRadius ResolveScaledCornerRadius( + this IPluginAppearanceContext context, + double baseRadius, + double minimum, + double maximum) + { + ArgumentNullException.ThrowIfNull(context); + var value = context.ResolveScaledCornerRadius(baseRadius, minimum, maximum); + return new CornerRadius(Math.Max(0d, value)); + } + + public static CornerRadius ResolveCornerRadius( + this PluginDesktopComponentContext context, + PluginCornerRadiusPreset preset, + double minimum, + double maximum) + { + ArgumentNullException.ThrowIfNull(context); + var value = context.ResolveCornerRadius(preset, minimum, maximum); + return new CornerRadius(Math.Max(0d, value)); + } + + public static PluginAppearanceSnapshot GetAppearanceSnapshot( + this PluginDesktopComponentContext context) + { + ArgumentNullException.ThrowIfNull(context); + return context.Appearance.Snapshot; + } +} diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml index 23c14cd..2e69f63 100644 --- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml +++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml @@ -1,4 +1,4 @@ - -/// 插件设置 -/// -public sealed class PluginSettings -{ - /// - /// API 地址 - /// - public string ApiUrl { get; set; } = "https://voicehub.lao-shui.top/api/songs/public"; - - /// - /// 是否显示点歌人 - /// - public bool ShowRequester { get; set; } = true; - - /// - /// 是否显示投票数 - /// - public bool ShowVoteCount { get; set; } = false; - - /// - /// 刷新间隔(分钟) - /// - public int RefreshIntervalMinutes { get; set; } = 60; -} diff --git a/VoiceHubLanDesktop/Models/SongModels.cs b/VoiceHubLanDesktop/Models/SongModels.cs deleted file mode 100644 index 9f3f195..0000000 --- a/VoiceHubLanDesktop/Models/SongModels.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Text.Json.Serialization; - -namespace VoiceHubLanDesktop.Models; - -/// -/// 歌曲信息 -/// -public sealed class Song -{ - /// - /// 歌曲标题 - /// - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; - - /// - /// 艺术家/歌手 - /// - [JsonPropertyName("artist")] - public string Artist { get; set; } = string.Empty; - - /// - /// 点歌人 - /// - [JsonPropertyName("requester")] - public string Requester { get; set; } = string.Empty; - - /// - /// 投票数/热度 - /// - [JsonPropertyName("voteCount")] - public int VoteCount { get; set; } -} - -/// -/// 排期歌曲项目 -/// -public sealed class SongItem -{ - /// - /// 播放日期 (yyyy-MM-dd) - /// - [JsonPropertyName("playDate")] - public string PlayDate { get; set; } = string.Empty; - - /// - /// 播放序号 - /// - [JsonPropertyName("sequence")] - public int Sequence { get; set; } - - /// - /// 歌曲信息 - /// - [JsonPropertyName("song")] - public Song Song { get; set; } = new(); - - /// - /// 获取播放日期 - /// - public DateTime GetPlayDate() - { - if (string.IsNullOrWhiteSpace(PlayDate)) - { - return DateTime.MinValue; - } - - if (DateTime.TryParseExact(PlayDate, "yyyy-MM-dd", null, - System.Globalization.DateTimeStyles.None, out var result)) - { - return result; - } - - return DateTime.MinValue; - } -} - -/// -/// 组件状态 -/// -public enum ComponentState -{ - /// - /// 加载中 - /// - Loading, - - /// - /// 正常显示 - /// - Normal, - - /// - /// 网络错误 - /// - NetworkError, - - /// - /// 暂无排期 - /// - NoSchedule -} - -/// -/// 显示数据 -/// -public sealed class DisplayData -{ - public ComponentState State { get; set; } - public IReadOnlyList Songs { get; set; } = []; - public DateTime? DisplayDate { get; set; } - public string ErrorMessage { get; set; } = string.Empty; -} diff --git a/VoiceHubLanDesktop/README.md b/VoiceHubLanDesktop/README.md deleted file mode 100644 index 22e23dc..0000000 --- a/VoiceHubLanDesktop/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# VoiceHubLanDesktop - -VoiceHub 广播站排期插件,用于 LanMountainDesktop 桌面应用。 - -## 功能特性 - -- 📻 **排期显示**:展示 VoiceHub 广播站当日排期歌曲 -- 🔄 **自动刷新**:支持自定义刷新间隔(5分钟 ~ 2小时) -- ⚙️ **灵活配置**:可自定义 API 地址、显示选项 -- 🌐 **多语言支持**:支持中文和英文 - -## 安装 - -将 `.laapp` 包放入 LanMountainDesktop 的插件目录: -``` -%LocalAppData%\LanMountainDesktop\Extensions\Plugins\ -``` - -## 配置 - -在 LanMountainDesktop 设置中找到 "VoiceHub 设置": - -| 选项 | 说明 | 默认值 | -|-----|------|--------| -| API 地址 | VoiceHub 后端 API 地址 | `https://voicehub.lao-shui.top/api/songs/public` | -| 显示点歌人 | 是否显示点歌人信息 | 是 | -| 显示投票数 | 是否显示歌曲投票数 | 否 | -| 刷新间隔 | 自动刷新时间间隔 | 1小时 | - -## 组件规格 - -- **最小尺寸**:3 × 4 网格 -- **缩放模式**:等比例缩放 -- **放置位置**:桌面 - -## 开发 - -### 构建 - -```bash -cd VoiceHubLanDesktop -dotnet build -``` - -### 打包 - -```bash -dotnet pack -# 或使用脚本 -../scripts/Pack-PluginPackages.ps1 -``` - -## 技术栈 - -- .NET 10 -- Avalonia UI 11.3.12 -- LanMountainDesktop.PluginSdk 4.0.0 -- CommunityToolkit.Mvvm 8.2.1 - -## 许可证 - -MIT License diff --git a/VoiceHubLanDesktop/Services/VoiceHubApiService.cs b/VoiceHubLanDesktop/Services/VoiceHubApiService.cs deleted file mode 100644 index c3f10fe..0000000 --- a/VoiceHubLanDesktop/Services/VoiceHubApiService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Net.Http; -using System.Text.Json; -using VoiceHubLanDesktop.Models; - -namespace VoiceHubLanDesktop.Services; - -/// -/// VoiceHub API 服务 -/// -public sealed class VoiceHubApiService : IDisposable -{ - private readonly HttpClient _httpClient; - private readonly JsonSerializerOptions _jsonOptions; - - private const string DefaultApiUrl = "https://voicehub.lao-shui.top/api/songs/public"; - private const int MaxRetryCount = 3; - private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); - - public VoiceHubApiService() - { - _httpClient = new HttpClient - { - Timeout = TimeSpan.FromSeconds(30) - }; - - _jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - } - - /// - /// 获取公开排期数据 - /// - public async Task>> GetPublicScheduleAsync( - string? apiUrl = null, - CancellationToken cancellationToken = default) - { - var url = string.IsNullOrWhiteSpace(apiUrl) ? DefaultApiUrl : apiUrl.Trim(); - - for (var attempt = 0; attempt < MaxRetryCount; attempt++) - { - try - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(_requestTimeout); - - var jsonResponse = await _httpClient.GetStringAsync(url, cts.Token); - var items = JsonSerializer.Deserialize>(jsonResponse, _jsonOptions); - - if (items is null) - { - return ApiResult>.Failure("数据解析失败"); - } - - return ApiResult>.Success(items); - } - catch (HttpRequestException ex) - { - if (attempt == MaxRetryCount - 1) - { - return ApiResult>.Failure($"网络错误: {ex.Message}"); - } - } - catch (TaskCanceledException) - { - if (attempt == MaxRetryCount - 1) - { - return ApiResult>.Failure("请求超时"); - } - } - catch (JsonException ex) - { - return ApiResult>.Failure($"数据格式错误: {ex.Message}"); - } - catch (Exception ex) - { - return ApiResult>.Failure($"未知错误: {ex.Message}"); - } - - // 指数退避 - await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken); - } - - return ApiResult>.Failure("获取数据失败"); - } - - public void Dispose() - { - _httpClient.Dispose(); - } -} - -/// -/// API 结果 -/// -public sealed class ApiResult -{ - public bool IsSuccess { get; } - public T? Data { get; } - public string? ErrorMessage { get; } - - private ApiResult(bool isSuccess, T? data, string? errorMessage) - { - IsSuccess = isSuccess; - Data = data; - ErrorMessage = errorMessage; - } - - public static ApiResult Success(T data) => new(true, data, null); - public static ApiResult Failure(string errorMessage) => new(false, default, errorMessage); -} diff --git a/VoiceHubLanDesktop/Services/VoiceHubScheduleService.cs b/VoiceHubLanDesktop/Services/VoiceHubScheduleService.cs deleted file mode 100644 index 1fe9893..0000000 --- a/VoiceHubLanDesktop/Services/VoiceHubScheduleService.cs +++ /dev/null @@ -1,164 +0,0 @@ -using VoiceHubLanDesktop.Models; - -namespace VoiceHubLanDesktop.Services; - -/// -/// 排期管理服务 -/// -public sealed class VoiceHubScheduleService -{ - private readonly VoiceHubApiService _apiService; - private readonly VoiceHubSettingsService _settingsService; - private IReadOnlyList _cachedSchedule = []; - private DateTime _cacheTime = DateTime.MinValue; - private readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(5); - - public event EventHandler? ScheduleUpdated; - - public VoiceHubScheduleService(VoiceHubApiService apiService, VoiceHubSettingsService settingsService) - { - _apiService = apiService; - _settingsService = settingsService; - } - - /// - /// 获取今日排期 - /// - public async Task GetTodayScheduleAsync(CancellationToken cancellationToken = default) - { - var settings = _settingsService.GetSettings(); - - // 检查缓存 - if (_cachedSchedule.Count > 0 && DateTime.Now - _cacheTime < _cacheExpiry) - { - return BuildDisplayData(_cachedSchedule); - } - - // 从 API 获取 - var result = await _apiService.GetPublicScheduleAsync(settings.ApiUrl, cancellationToken); - - if (!result.IsSuccess) - { - return new DisplayData - { - State = ComponentState.NetworkError, - ErrorMessage = result.ErrorMessage ?? "获取排期失败" - }; - } - - var items = result.Data ?? []; - - // 更新缓存 - _cachedSchedule = items; - _cacheTime = DateTime.Now; - - return BuildDisplayData(items); - } - - /// - /// 强制刷新 - /// - public async Task RefreshAsync(CancellationToken cancellationToken = default) - { - _cachedSchedule = []; - _cacheTime = DateTime.MinValue; - return await GetTodayScheduleAsync(cancellationToken); - } - - /// - /// 清除缓存 - /// - public void ClearCache() - { - _cachedSchedule = []; - _cacheTime = DateTime.MinValue; - } - - private DisplayData BuildDisplayData(IReadOnlyList items) - { - if (items.Count == 0) - { - return new DisplayData - { - State = ComponentState.NoSchedule, - ErrorMessage = "暂无排期数据" - }; - } - - // 过滤有效日期 - var validItems = items.Where(s => s.GetPlayDate() != DateTime.MinValue).ToList(); - - if (validItems.Count == 0) - { - return new DisplayData - { - State = ComponentState.NoSchedule, - ErrorMessage = "暂无有效排期数据" - }; - } - - // 找到今天或最近未来的排期 - var today = DateTime.Today; - var todaySchedule = validItems - .Where(s => s.GetPlayDate() == today) - .OrderBy(s => s.Sequence) - .ToList(); - - List displayItems; - DateTime actualDate; - - if (todaySchedule.Count > 0) - { - displayItems = todaySchedule; - actualDate = today; - } - else - { - // 找最近的未来排期 - var futureSchedule = validItems - .Where(s => s.GetPlayDate() > today) - .GroupBy(s => s.GetPlayDate()) - .OrderBy(g => g.Key) - .FirstOrDefault(); - - if (futureSchedule != null) - { - displayItems = futureSchedule.OrderBy(s => s.Sequence).ToList(); - actualDate = futureSchedule.Key; - } - else - { - return new DisplayData - { - State = ComponentState.NoSchedule, - ErrorMessage = "暂无排期数据" - }; - } - } - - // 触发更新事件 - ScheduleUpdated?.Invoke(this, new ScheduleUpdatedEventArgs(displayItems, actualDate)); - - return new DisplayData - { - State = ComponentState.Normal, - Songs = displayItems, - DisplayDate = actualDate - }; - } -} - -/// -/// 排期更新事件参数 -/// -public sealed class ScheduleUpdatedEventArgs : EventArgs -{ - public IReadOnlyList Songs { get; } - public DateTime DisplayDate { get; } - - public ScheduleUpdatedEventArgs(IReadOnlyList songs, DateTime displayDate) - { - Songs = songs; - DisplayDate = displayDate; - } -} diff --git a/VoiceHubLanDesktop/Services/VoiceHubSettingsService.cs b/VoiceHubLanDesktop/Services/VoiceHubSettingsService.cs deleted file mode 100644 index 13fe04b..0000000 --- a/VoiceHubLanDesktop/Services/VoiceHubSettingsService.cs +++ /dev/null @@ -1,97 +0,0 @@ -using LanMountainDesktop.PluginSdk; -using VoiceHubLanDesktop.Models; - -namespace VoiceHubLanDesktop.Services; - -/// -/// 插件设置服务 -/// -public sealed class VoiceHubSettingsService -{ - private readonly IPluginSettingsService _settingsService; - private const string SettingsSectionId = "voicehub-settings"; - private PluginSettings? _cachedSettings; - - public event EventHandler? SettingsChanged; - - public VoiceHubSettingsService(IPluginSettingsService settingsService) - { - _settingsService = settingsService; - } - - /// - /// 获取设置 - /// - public PluginSettings GetSettings() - { - if (_cachedSettings != null) - { - return _cachedSettings; - } - - var settings = new PluginSettings(); - - try - { - var apiUrl = _settingsService.GetValue(SettingsScope.Plugin, "apiUrl", SettingsSectionId); - if (!string.IsNullOrWhiteSpace(apiUrl)) - { - settings.ApiUrl = apiUrl; - } - - var showRequester = _settingsService.GetValue(SettingsScope.Plugin, "showRequester", SettingsSectionId); - if (showRequester.HasValue) - { - settings.ShowRequester = showRequester.Value; - } - - var showVoteCount = _settingsService.GetValue(SettingsScope.Plugin, "showVoteCount", SettingsSectionId); - if (showVoteCount.HasValue) - { - settings.ShowVoteCount = showVoteCount.Value; - } - - var refreshInterval = _settingsService.GetValue(SettingsScope.Plugin, "refreshInterval", SettingsSectionId); - if (!string.IsNullOrWhiteSpace(refreshInterval) && int.TryParse(refreshInterval, out var minutes)) - { - settings.RefreshIntervalMinutes = minutes; - } - } - catch - { - // 使用默认值 - } - - _cachedSettings = settings; - return settings; - } - - /// - /// 保存设置 - /// - public void SaveSettings(PluginSettings settings) - { - try - { - _settingsService.SetValue(SettingsScope.Plugin, "apiUrl", settings.ApiUrl, sectionId: SettingsSectionId); - _settingsService.SetValue(SettingsScope.Plugin, "showRequester", settings.ShowRequester, sectionId: SettingsSectionId); - _settingsService.SetValue(SettingsScope.Plugin, "showVoteCount", settings.ShowVoteCount, sectionId: SettingsSectionId); - _settingsService.SetValue(SettingsScope.Plugin, "refreshInterval", settings.RefreshIntervalMinutes.ToString(), sectionId: SettingsSectionId); - - _cachedSettings = settings; - SettingsChanged?.Invoke(this, settings); - } - catch - { - // 忽略保存错误 - } - } - - /// - /// 清除缓存 - /// - public void ClearCache() - { - _cachedSettings = null; - } -} diff --git a/VoiceHubLanDesktop/SongModels.cs b/VoiceHubLanDesktop/SongModels.cs deleted file mode 100644 index e027c12..0000000 --- a/VoiceHubLanDesktop/SongModels.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.Json.Serialization; - -namespace VoiceHubLanDesktop; - -/// -/// 歌曲信息 -/// -public sealed class Song -{ - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; - - [JsonPropertyName("artist")] - public string Artist { get; set; } = string.Empty; - - [JsonPropertyName("requester")] - public string Requester { get; set; } = string.Empty; - - [JsonPropertyName("voteCount")] - public int VoteCount { get; set; } -} - -/// -/// 排期歌曲项目 -/// -public sealed class SongItem -{ - [JsonPropertyName("playDate")] - public string PlayDate { get; set; } = string.Empty; - - [JsonPropertyName("sequence")] - public int Sequence { get; set; } - - [JsonPropertyName("song")] - public Song Song { get; set; } = new(); - - public DateTime GetPlayDate() - { - if (string.IsNullOrWhiteSpace(PlayDate)) - { - return DateTime.MinValue; - } - - if (DateTime.TryParseExact(PlayDate, "yyyy-MM-dd", null, - System.Globalization.DateTimeStyles.None, out var result)) - { - return result; - } - - return DateTime.MinValue; - } -} diff --git a/VoiceHubLanDesktop/Views/VoiceHubScheduleControl.axaml b/VoiceHubLanDesktop/Views/VoiceHubScheduleControl.axaml deleted file mode 100644 index 9291085..0000000 --- a/VoiceHubLanDesktop/Views/VoiceHubScheduleControl.axaml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -