mirror of
https://github.com/Heretek-AI/not-zune.git
synced 2026-10-12 02:04:04 +00:00
fix(ui/shell): Settings pivot-gating bug + Now Playing button hover/pressed states + CDView disc art + mini-player showlist/volume + cross-collection search + real About panel
- Settings pivot-gating regression: SoftwarePivot setter was missing 5 OnPropertyChanged notifications for the Phase 3 sub-pivots (Podcasts / FileTypes / Privacy / Photos / General). Switching between them left the previous panel visible. Fix + regression test. - CDView: bind CDLANDSHINE / CDRIPBURNGLOW / CDARTSHADOW assets (already extracted in the disassembly corpus, never wired). - MainShell Now Playing button: ENTER / HOVER / PRESSED variants and HOVER.FRAME0N / PRESSED.FRAME0N per-frame variants — Avalonia 11 PointerEntered / PointerExited / PointerPressed wired through the VM. - Compact mini-player: showlist toggle (NowPlayingVM.ToggleShowlistCommand) + compact volume slider; mini-player Width bumped 420 -> 480 to fit. - MainShell header search: cross-collection matches (podcasts + videos) fetched asynchronously with cancellation, debounced via a per-keystroke CancellationTokenSource, deduped against the synchronous collection suggestions. - About sub-pivot: full Zune-style product panel — branded logo, ProductName / ProductTagline / ProductVersion / ProductRuntime / PlatformInfo / ProductBuildId / ProductCopyright / MIT license / EULA link. AppInfo expanded with BuildIdentifier and RuntimeIdentifier populated at Desktop startup. - Reusable test fakes (FakeMediaLibraryService / FakeDeviceSyncService) under tests/NotZune.Tests.Application/TestFakes/.
This commit is contained in:
@@ -6,8 +6,18 @@ namespace NotZune.Application;
|
||||
public static class AppInfo
|
||||
{
|
||||
public const string Version = "1.1.0";
|
||||
public const string ProductName = "Not-Zune";
|
||||
public const string Tagline = "The Modern Spiritual Successor to Microsoft Zune Desktop";
|
||||
public const string CopyrightLine = "© 2026 Heretek-AI. Built on the shoulders of the Zune team.";
|
||||
public const string EulaLink = "https://github.com/Heretek-AI/not-zune/blob/main/LICENSE";
|
||||
|
||||
public static string VersionDisplay => $"Not-Zune v{Version} (True-Parity Engine)";
|
||||
public static string VersionDisplay => $"{ProductName} v{Version} (True-Parity Engine)";
|
||||
|
||||
/// <summary>Build identifier (commit + date) — filled by the platform layer when available.</summary>
|
||||
public static string BuildIdentifier { get; set; } = "local-dev";
|
||||
|
||||
/// <summary>Runtime identifier (e.g. .NET 8 on Linux X11) — filled by the platform layer.</summary>
|
||||
public static string RuntimeIdentifier { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Highlights shown in the What's New dialog when the version changes.</summary>
|
||||
public static IReadOnlyList<string> WhatsNewHighlights { get; } = new List<string>
|
||||
@@ -20,3 +30,4 @@ public static class AppInfo
|
||||
"Search autocomplete, back-stack navigation (Escape / back arrow), and Mixview like/hate/info/add tiles"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NotZune.Application;
|
||||
using NotZune.Application.Interfaces;
|
||||
using NotZune.Application.Services;
|
||||
using NotZune.Domain.Enums;
|
||||
@@ -34,6 +35,13 @@ public partial class App : Avalonia.Application
|
||||
// Seed initial rich demo data
|
||||
SeedDemoData(_serviceProvider);
|
||||
|
||||
// Populate AppInfo for the About page: runtime + commit identifier.
|
||||
AppInfo.RuntimeIdentifier =
|
||||
$"{System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription} on " +
|
||||
$"{System.Runtime.InteropServices.RuntimeInformation.OSDescription}";
|
||||
AppInfo.BuildIdentifier =
|
||||
$"Built on {DateTime.UtcNow:yyyy-MM-dd HH:mm 'UTC'}";
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var shellVm = _serviceProvider.GetRequiredService<MainShellViewModel>();
|
||||
|
||||
@@ -179,9 +179,9 @@ public partial class MainWindow : Window
|
||||
{
|
||||
_preCompactWidth = Width;
|
||||
_preCompactHeight = Height;
|
||||
MinWidth = 360;
|
||||
MinWidth = 480;
|
||||
MinHeight = 110;
|
||||
Width = 420;
|
||||
Width = 480;
|
||||
Height = 130;
|
||||
Topmost = (DataContext is MainShellViewModel vm) ? vm.SettingsVM.CompactModeAlwaysOnTop : true;
|
||||
CanResize = false;
|
||||
|
||||
@@ -34,6 +34,8 @@ public class MainShellViewModel : ViewModelBase
|
||||
private readonly IDeviceSyncService _deviceSyncService;
|
||||
private readonly ISoundEffectService? _soundEffectService;
|
||||
private readonly IUserStatsService? _userStatsService;
|
||||
private readonly IPodcastService? _podcastService;
|
||||
private readonly IVideoLibraryService? _videoLibraryService;
|
||||
|
||||
private NavigationPivot _activePivot = NavigationPivot.Quickplay;
|
||||
private readonly Stack<NavigationPivot> _navigationHistory = new();
|
||||
@@ -45,6 +47,9 @@ public class MainShellViewModel : ViewModelBase
|
||||
|
||||
private int _equalizerFrame = 1;
|
||||
private readonly DispatcherTimer? _equalizerTimer;
|
||||
private bool _isNowPlayingButtonHovered;
|
||||
private bool _isNowPlayingButtonPressed;
|
||||
private bool _isNowPlayingPlaying;
|
||||
private string _nowPlayingIconSource = "avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.ENTER.PNG";
|
||||
|
||||
public QuickplayViewModel QuickplayVM { get; }
|
||||
@@ -91,6 +96,53 @@ public class MainShellViewModel : ViewModelBase
|
||||
private set => SetProperty(ref _nowPlayingIconSource, value);
|
||||
}
|
||||
|
||||
/// <summary>Code-behind hook: pointer entered the Now Playing button.</summary>
|
||||
public void NotifyNowPlayingButtonHover(bool isHovering) => SetNowPlayingHoverState(isHovering);
|
||||
|
||||
/// <summary>Code-behind hook: pointer pressed/released the Now Playing button.</summary>
|
||||
public void NotifyNowPlayingButtonPressed(bool isPressed) => SetNowPlayingPressedState(isPressed);
|
||||
|
||||
private void SetNowPlayingHoverState(bool isHovering)
|
||||
{
|
||||
if (_isNowPlayingButtonHovered == isHovering) return;
|
||||
_isNowPlayingButtonHovered = isHovering;
|
||||
RefreshNowPlayingIcon();
|
||||
}
|
||||
|
||||
private void SetNowPlayingPressedState(bool isPressed)
|
||||
{
|
||||
if (_isNowPlayingButtonPressed == isPressed) return;
|
||||
_isNowPlayingButtonPressed = isPressed;
|
||||
RefreshNowPlayingIcon();
|
||||
}
|
||||
|
||||
/// <summary>Compute the right Now Playing icon asset for the current (hover/pressed/playing/frame) state.</summary>
|
||||
private void RefreshNowPlayingIcon()
|
||||
{
|
||||
string? variant = _isNowPlayingButtonPressed ? "PRESSED"
|
||||
: _isNowPlayingButtonHovered ? "HOVER"
|
||||
: null;
|
||||
|
||||
if (!_isNowPlayingPlaying)
|
||||
{
|
||||
NowPlayingIconSource = variant switch
|
||||
{
|
||||
"PRESSED" => "avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.ENTER.PRESSED.PNG",
|
||||
"HOVER" => "avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.ENTER.HOVER.PNG",
|
||||
_ => "avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.ENTER.PNG",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Playing: cycle through 10 frames, with the hover/pressed variant.
|
||||
NowPlayingIconSource = variant switch
|
||||
{
|
||||
"PRESSED" => $"avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.PRESSED.FRAME{_equalizerFrame:D2}.PNG",
|
||||
"HOVER" => $"avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.HOVER.FRAME{_equalizerFrame:D2}.PNG",
|
||||
_ => $"avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.FRAME{_equalizerFrame:D2}.PNG",
|
||||
};
|
||||
}
|
||||
|
||||
private string _headerSearchQuery = string.Empty;
|
||||
public string HeaderSearchQuery
|
||||
{
|
||||
@@ -116,6 +168,8 @@ public class MainShellViewModel : ViewModelBase
|
||||
|
||||
public bool HasSearchSuggestions => SearchSuggestions.Count > 0 && !string.IsNullOrWhiteSpace(HeaderSearchQuery);
|
||||
|
||||
private CancellationTokenSource? _searchCts;
|
||||
|
||||
private void UpdateSearchSuggestions(string query)
|
||||
{
|
||||
SearchSuggestions.Clear();
|
||||
@@ -130,24 +184,87 @@ public class MainShellViewModel : ViewModelBase
|
||||
|
||||
suggestions.AddRange(CollectionVM.Artists
|
||||
.Where(a => a.Name.ToLowerInvariant().Contains(lower))
|
||||
.Take(3)
|
||||
.Take(2)
|
||||
.Select(a => a.Name));
|
||||
|
||||
suggestions.AddRange(CollectionVM.Albums
|
||||
.Where(a => a.Title.ToLowerInvariant().Contains(lower))
|
||||
.Take(3)
|
||||
.Take(2)
|
||||
.Select(a => a.Title));
|
||||
|
||||
suggestions.AddRange(CollectionVM.Songs
|
||||
.Where(s => s.Title.ToLowerInvariant().Contains(lower))
|
||||
.Take(3)
|
||||
.Take(2)
|
||||
.Select(s => s.Title));
|
||||
|
||||
foreach (var suggestion in suggestions.Distinct().Take(8))
|
||||
foreach (var suggestion in suggestions.Distinct().Take(6))
|
||||
{
|
||||
SearchSuggestions.Add(suggestion);
|
||||
}
|
||||
|
||||
// Async podcast + video matches (debounced via cancellation token)
|
||||
_searchCts?.Cancel();
|
||||
_searchCts = new CancellationTokenSource();
|
||||
_ = SearchExtendedAsync(query, _searchCts.Token);
|
||||
|
||||
OnPropertyChanged(nameof(HasSearchSuggestions));
|
||||
}
|
||||
|
||||
private async Task SearchExtendedAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var lower = query.Trim().ToLowerInvariant();
|
||||
var added = new List<string>();
|
||||
|
||||
if (_podcastService != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var podcasts = await _podcastService.GetAllPodcastsAsync();
|
||||
foreach (var series in podcasts)
|
||||
{
|
||||
if (ct.IsCancellationRequested) return;
|
||||
if (series.Title?.ToLowerInvariant().Contains(lower) == true)
|
||||
{
|
||||
added.Add(series.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Search is best-effort; ignore service failures
|
||||
}
|
||||
}
|
||||
|
||||
if (_videoLibraryService != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var videos = await _videoLibraryService.GetAllVideosAsync();
|
||||
foreach (var video in videos)
|
||||
{
|
||||
if (ct.IsCancellationRequested) return;
|
||||
if (video.Title?.ToLowerInvariant().Contains(lower) == true)
|
||||
{
|
||||
added.Add(video.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
if (ct.IsCancellationRequested || added.Count == 0) return;
|
||||
|
||||
// Append the additional matches (deduped) without evicting the synchronous ones.
|
||||
var existing = new HashSet<string>(SearchSuggestions, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var s in added.Where(s => !existing.Contains(s)).Take(4))
|
||||
{
|
||||
SearchSuggestions.Add(s);
|
||||
if (SearchSuggestions.Count >= 10) break;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasSearchSuggestions));
|
||||
}
|
||||
|
||||
@@ -386,13 +503,14 @@ public class MainShellViewModel : ViewModelBase
|
||||
_deviceSyncService = deviceSyncService;
|
||||
_soundEffectService = soundEffectService ?? new SoundEffectService();
|
||||
_userStatsService = userStatsService ?? new UserStatsService(libraryService);
|
||||
var podService = podcastService ?? new PodcastService(playerCoordinator);
|
||||
_podcastService = podcastService ?? new PodcastService(playerCoordinator);
|
||||
_videoLibraryService = videoLibraryService;
|
||||
|
||||
// Child ViewModels
|
||||
QuickplayVM = new QuickplayViewModel(playerCoordinator, libraryService, smartDJService);
|
||||
CollectionVM = new CollectionViewModel(playerCoordinator, libraryService, podService, smartDJService, artworkCacheService, metadataService, smartPlaylistService, videoLibraryService, videoEngine, photoLibraryService);
|
||||
CollectionVM = new CollectionViewModel(playerCoordinator, libraryService, _podcastService, smartDJService, artworkCacheService, metadataService, smartPlaylistService, videoLibraryService, videoEngine, photoLibraryService);
|
||||
NowPlayingVM = new NowPlayingViewModel(playerCoordinator, libraryService, enrichmentService, audioEngine, videoLibraryService, videoEngine);
|
||||
DeviceVM = new DeviceViewModel(deviceSyncService, libraryService, syncEngine, settingsStore, videoLibraryService, photoLibraryService, podService, _soundEffectService, syncGroupService);
|
||||
DeviceVM = new DeviceViewModel(deviceSyncService, libraryService, syncEngine, settingsStore, videoLibraryService, photoLibraryService, _podcastService, _soundEffectService, syncGroupService);
|
||||
SettingsVM = new SettingsViewModel(_soundEffectService, folderPickerService, _libraryService, playerCoordinator, deviceSyncService, settingsStore);
|
||||
|
||||
// Onboarding (FIRSTLAUNCH + WHATSNEW parity): wizard on first run, What's New on version change.
|
||||
@@ -554,7 +672,7 @@ public class MainShellViewModel : ViewModelBase
|
||||
_equalizerTimer.Tick += (s, e) =>
|
||||
{
|
||||
_equalizerFrame = (_equalizerFrame % 10) + 1;
|
||||
NowPlayingIconSource = $"avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.FRAME{_equalizerFrame:D2}.PNG";
|
||||
RefreshNowPlayingIcon();
|
||||
};
|
||||
BeginFirstConnectOnAlreadyConnected();
|
||||
}
|
||||
@@ -712,12 +830,14 @@ public class MainShellViewModel : ViewModelBase
|
||||
|
||||
if (IsPlaying)
|
||||
{
|
||||
_isNowPlayingPlaying = true;
|
||||
_equalizerTimer?.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_isNowPlayingPlaying = false;
|
||||
_equalizerTimer?.Stop();
|
||||
NowPlayingIconSource = "avares://NotZune.UI/Assets/Zune/Transport/ICON.NOWPLAYING.ENTER.PNG";
|
||||
RefreshNowPlayingIcon();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,10 +138,15 @@ public class SettingsViewModel : ViewModelBase
|
||||
{
|
||||
OnPropertyChanged(nameof(IsCollectionSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsPlaybackSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsPodcastsSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsFileTypesSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsPrivacySubPivotActive));
|
||||
OnPropertyChanged(nameof(IsPhotosSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsRipSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsBurnSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsMetadataSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsDisplaySubPivotActive));
|
||||
OnPropertyChanged(nameof(IsGeneralSubPivotActive));
|
||||
OnPropertyChanged(nameof(IsAboutSubPivotActive));
|
||||
}
|
||||
}
|
||||
@@ -994,6 +999,15 @@ public class SettingsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
public string ProductName => NotZune.Application.AppInfo.ProductName;
|
||||
public string ProductTagline => NotZune.Application.AppInfo.Tagline;
|
||||
public string ProductCopyright => NotZune.Application.AppInfo.CopyrightLine;
|
||||
public string ProductEulaLink => NotZune.Application.AppInfo.EulaLink;
|
||||
public string ProductVersion => NotZune.Application.AppInfo.Version;
|
||||
public string ProductRuntime => string.IsNullOrWhiteSpace(NotZune.Application.AppInfo.RuntimeIdentifier)
|
||||
? PlatformInfo
|
||||
: NotZune.Application.AppInfo.RuntimeIdentifier;
|
||||
public string ProductBuildId => NotZune.Application.AppInfo.BuildIdentifier;
|
||||
public string PlatformInfo => $"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} ({System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture})";
|
||||
public string VersionInfo => NotZune.Application.AppInfo.VersionDisplay;
|
||||
|
||||
|
||||
@@ -21,15 +21,44 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- Disc Session Summary Box -->
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource ZuneSurfaceElevatedBrush}"
|
||||
BorderBrush="{DynamicResource ZuneBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="0"
|
||||
Padding="24,16"
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource ZuneSurfaceElevatedBrush}"
|
||||
BorderBrush="{DynamicResource ZuneBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="0"
|
||||
Padding="24,16"
|
||||
Margin="0,0,0,24">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<!-- Authentic CDLand disc + shine + glow stack (CDLANDSHINE always; CDRIPBURNGLOW during active session). -->
|
||||
<Grid Grid.Column="0" Width="120" Height="120" Margin="0,0,24,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/CD/CDARTSHADOW.PNG"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
<Border Width="100" Height="100"
|
||||
Background="#202020"
|
||||
BorderBrush="{DynamicResource ZuneSubtleBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="♫" FontSize="42" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource ZuneTextDimmedBrush}" />
|
||||
</Border>
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/CD/CDLANDSHINE.PNG"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False" />
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/CD/CDRIPBURNGLOW.PNG"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
IsVisible="{Binding IsRipping, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding DiscTitle}" Classes="hero" FontSize="26" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding DiscArtist}" Classes="body" Foreground="{DynamicResource ZuneAccentBrush}" />
|
||||
@@ -41,17 +70,17 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="8">
|
||||
<Button Command="{Binding RipCdCommand}"
|
||||
Classes="zune-action"
|
||||
IsVisible="{Binding IsRipMode}"
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center" Spacing="8">
|
||||
<Button Command="{Binding RipCdCommand}"
|
||||
Classes="zune-action"
|
||||
IsVisible="{Binding IsRipMode}"
|
||||
IsEnabled="{Binding !IsRipping}">
|
||||
<TextBlock Text="START RIP" Classes="section-header" FontWeight="Bold" />
|
||||
</Button>
|
||||
|
||||
<Button Command="{Binding BurnCdCommand}"
|
||||
Classes="zune-action"
|
||||
IsVisible="{Binding IsBurnMode}"
|
||||
<Button Command="{Binding BurnCdCommand}"
|
||||
Classes="zune-action"
|
||||
IsVisible="{Binding IsBurnMode}"
|
||||
IsEnabled="{Binding !IsBurning}">
|
||||
<TextBlock Text="START BURN" Classes="section-header" FontWeight="Bold" />
|
||||
</Button>
|
||||
@@ -66,13 +95,13 @@
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,16" Spacing="6" IsVisible="{Binding IsRipping}">
|
||||
<TextBlock Text="{Binding RipStatusText}" Classes="caption" Foreground="{DynamicResource ZuneAccentBrush}" />
|
||||
<ProgressBar Value="{Binding RipProgress}" Minimum="0.0" Maximum="1.0" Height="4" CornerRadius="0"
|
||||
Foreground="{DynamicResource ZuneAccentBrush}" Background="{DynamicResource ZuneSurfaceBrush}" />
|
||||
Foreground="{DynamicResource ZuneAccentBrush}" Background="{DynamicResource ZuneSurfaceElevatedBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,16" Spacing="6" IsVisible="{Binding IsBurning}">
|
||||
<TextBlock Text="{Binding BurnStatusText}" Classes="caption" Foreground="{DynamicResource ZuneAccentBrush}" />
|
||||
<ProgressBar Value="{Binding BurnProgress}" Minimum="0.0" Maximum="1.0" Height="4" CornerRadius="0"
|
||||
Foreground="{DynamicResource ZuneAccentBrush}" Background="{DynamicResource ZuneSurfaceBrush}" />
|
||||
Foreground="{DynamicResource ZuneAccentBrush}" Background="{DynamicResource ZuneSurfaceElevatedBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Track Listing -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:NotZune.UI.ViewModels;assembly=NotZune.UI"
|
||||
mc:Ignorable="d" d:DesignWidth="420" d:DesignHeight="130"
|
||||
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="130"
|
||||
x:Class="NotZune.UI.Views.CompactMiniPlayerView"
|
||||
x:DataType="vm:MainShellViewModel"
|
||||
Background="{DynamicResource ZuneSurfaceElevatedBrush}">
|
||||
@@ -45,46 +45,51 @@
|
||||
Background="{DynamicResource ZuneInputBackgroundBrush}" />
|
||||
|
||||
<!-- 3. Player Content & Transport Strip -->
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto" Margin="12,8,12,6" VerticalAlignment="Center">
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto,Auto" Margin="12,8,12,6" VerticalAlignment="Center">
|
||||
<!-- Left: Mini Square Album Art & Rating Badge -->
|
||||
<Border Grid.Column="0"
|
||||
Width="44" Height="44"
|
||||
Background="{DynamicResource ZuneCardBrush}"
|
||||
BorderBrush="{DynamicResource ZuneBorderBrush}"
|
||||
BorderThickness="1"
|
||||
<Border Grid.Column="0"
|
||||
Width="44" Height="44"
|
||||
Background="{DynamicResource ZuneCardBrush}"
|
||||
BorderBrush="{DynamicResource ZuneBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="0">
|
||||
<Grid>
|
||||
<TextBlock Text="♫" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{DynamicResource ZuneTextDimmedBrush}" />
|
||||
<!-- Mini Heart badge if favorite -->
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/Rating/RATING.LIKEIT.SELECTED.PNG"
|
||||
IsVisible="{Binding IsFavorite}"
|
||||
Width="12" Height="11"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/Rating/RATING.LIKEIT.SELECTED.PNG"
|
||||
IsVisible="{Binding IsFavorite}"
|
||||
Width="12" Height="11"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,2,2" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Center: Track Title & Artist Metadata -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="10,0,10,0" Spacing="2">
|
||||
<TextBlock Text="{Binding CurrentTrack.Title, FallbackValue='Not Playing'}"
|
||||
Classes="body"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
<TextBlock Text="{Binding CurrentTrack.Title, FallbackValue='Not Playing'}"
|
||||
Classes="body"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxWidth="170" />
|
||||
<TextBlock Text="{Binding CurrentTrack.ArtistName, FallbackValue='Zune Player'}"
|
||||
Classes="caption"
|
||||
Foreground="{DynamicResource ZuneTextSecondaryBrush}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
<TextBlock Text="{Binding CurrentTrack.ArtistName, FallbackValue='Zune Player'}"
|
||||
Classes="caption"
|
||||
Foreground="{DynamicResource ZuneTextSecondaryBrush}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxWidth="170" />
|
||||
<TextBlock Text="{Binding ElapsedTimeText}"
|
||||
Classes="caption"
|
||||
Foreground="{DynamicResource ZuneTextDimmedBrush}"
|
||||
<TextBlock Text="{Binding ElapsedTimeText}"
|
||||
Classes="caption"
|
||||
Foreground="{DynamicResource ZuneTextDimmedBrush}"
|
||||
FontSize="8" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right: Compact Transport Buttons -->
|
||||
<!-- Showlist Toggle + Compact Transport -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<!-- Showlist toggle (upcoming queue) -->
|
||||
<Button Command="{Binding NowPlayingVM.ToggleShowlistCommand}" Padding="4" ToolTip.Tip="Showlist (upcoming queue)">
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/Transport/TRANSPORT.SHOWLIST.ON.PNG" Width="20" Height="20" />
|
||||
</Button>
|
||||
|
||||
<!-- Previous -->
|
||||
<Button Command="{Binding PreviousCommand}" Padding="4" ToolTip.Tip="Previous">
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/Transport/TRANSPORT.BACK.PNG" Width="20" Height="20" />
|
||||
@@ -103,6 +108,16 @@
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/Transport/TRANSPORT.FORWARD.PNG" Width="20" Height="20" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Compact volume slider -->
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center" Margin="6,0,0,0">
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/Transport/TRANSPORT.MUTE.OFF.PNG" Width="14" Height="12" VerticalAlignment="Center" />
|
||||
<Slider Width="60"
|
||||
Value="{Binding Volume}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -219,19 +219,22 @@
|
||||
</Panel>
|
||||
|
||||
<!-- Animated Now Playing Dancing Equalizer Button -->
|
||||
<Button Command="{Binding ToggleNowPlayingCommand}" VerticalAlignment="Center">
|
||||
<Border BorderBrush="{DynamicResource ZuneAccentBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="12,4"
|
||||
Background="{DynamicResource ZuneCardBrush}"
|
||||
<Button Command="{Binding ToggleNowPlayingCommand}" VerticalAlignment="Center"
|
||||
PointerEntered="OnNowPlayingButtonPointerEntered"
|
||||
PointerExited="OnNowPlayingButtonPointerExited"
|
||||
PointerPressed="OnNowPlayingButtonPointerPressed">
|
||||
<Border BorderBrush="{DynamicResource ZuneAccentBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="12,4"
|
||||
Background="{DynamicResource ZuneCardBrush}"
|
||||
CornerRadius="0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Image Source="{Binding NowPlayingIconSource}" Width="18" Height="17" VerticalAlignment="Center" />
|
||||
<TextBlock Text="NOW PLAYING"
|
||||
Classes="caption"
|
||||
FontWeight="SemiBold"
|
||||
<TextBlock Text="NOW PLAYING"
|
||||
Classes="caption"
|
||||
FontWeight="SemiBold"
|
||||
LetterSpacing="1"
|
||||
Foreground="{DynamicResource ZuneTextActiveBrush}"
|
||||
Foreground="{DynamicResource ZuneTextActiveBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -76,4 +76,29 @@ public partial class MainShellView : UserControl
|
||||
var window = TopLevel.GetTopLevel(this) as Window;
|
||||
window?.Close();
|
||||
}
|
||||
|
||||
private void OnNowPlayingButtonPointerEntered(object? sender, Avalonia.Input.PointerEventArgs e)
|
||||
{
|
||||
if (DataContext is ViewModels.MainShellViewModel vm)
|
||||
{
|
||||
vm.NotifyNowPlayingButtonHover(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNowPlayingButtonPointerExited(object? sender, Avalonia.Input.PointerEventArgs e)
|
||||
{
|
||||
if (DataContext is ViewModels.MainShellViewModel vm)
|
||||
{
|
||||
vm.NotifyNowPlayingButtonHover(false);
|
||||
vm.NotifyNowPlayingButtonPressed(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNowPlayingButtonPointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||
{
|
||||
if (DataContext is ViewModels.MainShellViewModel vm)
|
||||
{
|
||||
vm.NotifyNowPlayingButtonPressed(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,20 +628,58 @@
|
||||
|
||||
<!-- SUB-PANEL: ABOUT -->
|
||||
<StackPanel Spacing="24" IsVisible="{Binding IsAboutSubPivotActive}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding VersionInfo}" Classes="hero" />
|
||||
<TextBlock Text="The Modern Spiritual Successor to Microsoft Zune Desktop" Classes="section-header" Foreground="{DynamicResource ZuneAccentBrush}" />
|
||||
<TextBlock Text="{Binding PlatformInfo}" Classes="caption" Foreground="{DynamicResource ZuneTextDimmedBrush}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="20">
|
||||
<!-- Authentic Zune brand mark -->
|
||||
<Image Source="avares://NotZune.UI/Assets/Zune/Branding/ZUNECOLORLOGO.PNG"
|
||||
Width="80" Height="80"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<StackPanel Spacing="6" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding ProductName}" Classes="hero" FontSize="48" />
|
||||
<TextBlock Text="{Binding ProductTagline}" Classes="section-header" Foreground="{DynamicResource ZuneAccentBrush}" LetterSpacing="1" />
|
||||
<TextBlock Text="{Binding VersionInfo}" Classes="body" Foreground="{DynamicResource ZuneTextActiveBrush}" />
|
||||
<TextBlock Text="{Binding ProductBuildId}" Classes="caption" Foreground="{DynamicResource ZuneTextDimmedBrush}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Border Background="{DynamicResource ZuneSurfaceBrush}"
|
||||
BorderBrush="{DynamicResource ZuneBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="0"
|
||||
Padding="20"
|
||||
MaxWidth="650">
|
||||
<Border Background="{DynamicResource ZuneSurfaceElevatedBrush}"
|
||||
BorderBrush="{DynamicResource ZuneBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="0"
|
||||
Padding="20"
|
||||
MaxWidth="700">
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="VERSION" Classes="caption" FontWeight="SemiBold" Foreground="{DynamicResource ZuneTextSecondaryBrush}" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ProductVersion}" Classes="body" Margin="0,0,0,6" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="RUNTIME" Classes="caption" FontWeight="SemiBold" Foreground="{DynamicResource ZuneTextSecondaryBrush}" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ProductRuntime}" Classes="body" Margin="0,0,0,6" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="PLATFORM" Classes="caption" FontWeight="SemiBold" Foreground="{DynamicResource ZuneTextSecondaryBrush}" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding PlatformInfo}" Classes="body" Margin="0,0,0,6" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="BUILD" Classes="caption" FontWeight="SemiBold" Foreground="{DynamicResource ZuneTextSecondaryBrush}" />
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding ProductBuildId}" Classes="body" Margin="0,0,0,6" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="COPYRIGHT" Classes="caption" FontWeight="SemiBold" Foreground="{DynamicResource ZuneTextSecondaryBrush}" />
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding ProductCopyright}" Classes="body" Margin="0,0,0,6" />
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Text="LICENSE" Classes="caption" FontWeight="SemiBold" Foreground="{DynamicResource ZuneTextSecondaryBrush}" />
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" Text="MIT License" Classes="body" Margin="0,0,0,6" />
|
||||
|
||||
<TextBlock Grid.Row="6" Grid.Column="0" Text="EULA & SOURCE" Classes="caption" FontWeight="SemiBold" Foreground="{DynamicResource ZuneTextSecondaryBrush}" />
|
||||
<TextBlock Grid.Row="6" Grid.Column="1" Text="{Binding ProductEulaLink}" Classes="caption" Foreground="{DynamicResource ZuneAccentBrush}" TextDecorations="Underline" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Background="{DynamicResource ZuneSurfaceElevatedBrush}"
|
||||
BorderBrush="{DynamicResource ZuneBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="0"
|
||||
Padding="20"
|
||||
MaxWidth="700">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="DESIGN INVARIANTS & PRINCIPLES" Classes="section-header" />
|
||||
<TextBlock Text="DESIGN INVARIANTS & PRINCIPLES" Classes="section-header" Foreground="{DynamicResource ZuneAccentBrush}" />
|
||||
<TextBlock Text="• Strict Metro / Iris Design Language: Zero Corner Radius on all controls and surfaces." Classes="body" />
|
||||
<TextBlock Text="• Content Before Chrome: Eliminating unnecessary borders, bevels, and artificial depth." Classes="body" />
|
||||
<TextBlock Text="• Typography as Art: Segoe UI, Selawik, and clear hierarchical scale establishing visual rhythm." Classes="body" />
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using Avalonia.Headless.XUnit;
|
||||
using NotZune.Application.Services;
|
||||
using NotZune.Tests.Application.TestFakes;
|
||||
using NotZune.UI.ViewModels;
|
||||
using Xunit;
|
||||
|
||||
namespace NotZune.Tests.Application;
|
||||
|
||||
public class NowPlayingIconTests
|
||||
{
|
||||
[AvaloniaFact]
|
||||
public void Default_IconIsEnterStatic()
|
||||
{
|
||||
var vm = new MainShellViewModel(
|
||||
new PlaybackQueueCoordinator(),
|
||||
new FakeMediaLibraryService(),
|
||||
new FakeDeviceSyncService(),
|
||||
new SmartDJEngine());
|
||||
|
||||
Assert.EndsWith("ICON.NOWPLAYING.ENTER.PNG", vm.NowPlayingIconSource);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Hover_SwitchesToHoverVariant()
|
||||
{
|
||||
var vm = new MainShellViewModel(
|
||||
new PlaybackQueueCoordinator(),
|
||||
new FakeMediaLibraryService(),
|
||||
new FakeDeviceSyncService(),
|
||||
new SmartDJEngine());
|
||||
|
||||
vm.NotifyNowPlayingButtonHover(true);
|
||||
Assert.EndsWith("ICON.NOWPLAYING.ENTER.HOVER.PNG", vm.NowPlayingIconSource);
|
||||
|
||||
vm.NotifyNowPlayingButtonHover(false);
|
||||
Assert.EndsWith("ICON.NOWPLAYING.ENTER.PNG", vm.NowPlayingIconSource);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Pressed_OverridesHoverVariant()
|
||||
{
|
||||
var vm = new MainShellViewModel(
|
||||
new PlaybackQueueCoordinator(),
|
||||
new FakeMediaLibraryService(),
|
||||
new FakeDeviceSyncService(),
|
||||
new SmartDJEngine());
|
||||
|
||||
vm.NotifyNowPlayingButtonHover(true);
|
||||
vm.NotifyNowPlayingButtonPressed(true);
|
||||
Assert.EndsWith("ICON.NOWPLAYING.ENTER.PRESSED.PNG", vm.NowPlayingIconSource);
|
||||
|
||||
vm.NotifyNowPlayingButtonPressed(false);
|
||||
Assert.EndsWith("ICON.NOWPLAYING.ENTER.HOVER.PNG", vm.NowPlayingIconSource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Avalonia.Headless.XUnit;
|
||||
using NotZune.UI.ViewModels;
|
||||
using Xunit;
|
||||
|
||||
namespace NotZune.Tests.Application;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the Settings pivot-gating bug:
|
||||
/// the SoftwarePivot setter must raise OnPropertyChanged for every
|
||||
/// composed IsXSubPivotActive flag, including the Phase 3 pages.
|
||||
/// </summary>
|
||||
public class SettingsPivotGatingTests
|
||||
{
|
||||
[AvaloniaFact]
|
||||
public void SoftwarePivot_ChangeBetweenPhase3Pages_RaisesAllComposedFlags()
|
||||
{
|
||||
var vm = new SettingsViewModel();
|
||||
|
||||
var fired = 0;
|
||||
void OnProp(object? _, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(SettingsViewModel.IsPodcastsSubPivotActive)
|
||||
or nameof(SettingsViewModel.IsFileTypesSubPivotActive)
|
||||
or nameof(SettingsViewModel.IsPrivacySubPivotActive)
|
||||
or nameof(SettingsViewModel.IsPhotosSubPivotActive)
|
||||
or nameof(SettingsViewModel.IsGeneralSubPivotActive))
|
||||
{
|
||||
fired++;
|
||||
}
|
||||
}
|
||||
vm.PropertyChanged += OnProp;
|
||||
|
||||
vm.SoftwarePivot = SoftwareSubPivot.Podcasts;
|
||||
vm.SoftwarePivot = SoftwareSubPivot.FileTypes;
|
||||
vm.SoftwarePivot = SoftwareSubPivot.Privacy;
|
||||
vm.SoftwarePivot = SoftwareSubPivot.Photos;
|
||||
vm.SoftwarePivot = SoftwareSubPivot.General;
|
||||
|
||||
vm.PropertyChanged -= OnProp;
|
||||
|
||||
// 5 transitions × 5 distinct sub-pivot flags = 25 fired events.
|
||||
// (Each transition raises all 5 flags because the flag depends on
|
||||
// SoftwarePivot; the bug was that notifications weren't fired.)
|
||||
Assert.Equal(25, fired);
|
||||
|
||||
// Final state: General is active.
|
||||
Assert.True(vm.IsGeneralSubPivotActive);
|
||||
Assert.False(vm.IsPodcastsSubPivotActive);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using NotZune.Application.Interfaces;
|
||||
using NotZune.Domain.Enums;
|
||||
using NotZune.Domain.Models;
|
||||
|
||||
namespace NotZune.Tests.Application.TestFakes;
|
||||
|
||||
internal class FakeMediaLibraryService : IMediaLibraryService
|
||||
{
|
||||
public Task<IReadOnlyList<Track>> GetAllTracksAsync() => Task.FromResult<IReadOnlyList<Track>>(new List<Track>());
|
||||
public Task<IReadOnlyList<Album>> GetAllAlbumsAsync() => Task.FromResult<IReadOnlyList<Album>>(new List<Album>());
|
||||
public Task<IReadOnlyList<Artist>> GetAllArtistsAsync() => Task.FromResult<IReadOnlyList<Artist>>(new List<Artist>());
|
||||
public Task<IReadOnlyList<Playlist>> GetAllPlaylistsAsync() => Task.FromResult<IReadOnlyList<Playlist>>(new List<Playlist>());
|
||||
public Task<IReadOnlyList<PlayHistoryEntry>> GetRecentHistoryAsync(int count = 20) => Task.FromResult<IReadOnlyList<PlayHistoryEntry>>(new List<PlayHistoryEntry>());
|
||||
public Task<IReadOnlyList<Album>> GetRecentlyAddedAlbumsAsync(int count = 12) => Task.FromResult<IReadOnlyList<Album>>(new List<Album>());
|
||||
public Task<IReadOnlyList<Track>> SearchAsync(string query) => Task.FromResult<IReadOnlyList<Track>>(new List<Track>());
|
||||
public Task SetTrackRatingAsync(Guid trackId, HeartRating rating) => Task.CompletedTask;
|
||||
public Task ScanDirectoryAsync(string directoryPath, IProgress<double>? progress = null) => Task.CompletedTask;
|
||||
public Task ClearDemoDataAsync() => Task.CompletedTask;
|
||||
public Task<Playlist> CreatePlaylistAsync(string name, string? description = null) => Task.FromResult(new Playlist { Name = name });
|
||||
public Task DeletePlaylistAsync(Guid playlistId) => Task.CompletedTask;
|
||||
public Task AddTrackToPlaylistAsync(Guid playlistId, Guid trackId) => Task.CompletedTask;
|
||||
public Task RemoveTrackFromPlaylistAsync(Guid playlistId, Guid trackId) => Task.CompletedTask;
|
||||
public Task<IReadOnlyList<Track>> GetPlaylistTracksAsync(Guid playlistId) => Task.FromResult<IReadOnlyList<Track>>(new List<Track>());
|
||||
public Task ExportPlaylistToZplAsync(Guid playlistId, string targetFilePath) => Task.CompletedTask;
|
||||
public Task UpdateTrackMetadataAsync(Guid trackId, string title, string artistName, string albumTitle, int? year, string genre, int trackNumber, int discNumber) => Task.CompletedTask;
|
||||
public Task SetAlbumArtworkAsync(Guid albumId, string? artworkUri) => Task.CompletedTask;
|
||||
public Task SetArtistMetadataAsync(string artistName, string? biography, string? thumbnailUri, string? backgroundImageUri, string? musicBrainzId) => Task.CompletedTask;
|
||||
public Task<IReadOnlyList<Album>> GetPinnedAlbumsAsync() => Task.FromResult<IReadOnlyList<Album>>(new List<Album>());
|
||||
public Task PinAlbumAsync(Guid albumId) => Task.CompletedTask;
|
||||
public Task UnpinAlbumAsync(Guid albumId) => Task.CompletedTask;
|
||||
public void StartDirectoryWatcher(string directoryPath) { }
|
||||
public void StopDirectoryWatcher() { }
|
||||
#pragma warning disable CS0067
|
||||
public event EventHandler? LibraryUpdated;
|
||||
#pragma warning restore CS0067
|
||||
}
|
||||
|
||||
internal class FakeDeviceSyncService : IDeviceSyncService
|
||||
{
|
||||
public IReadOnlyList<ZuneDevice> ConnectedDevices => new List<ZuneDevice>();
|
||||
public Task StartMonitoringAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task SyncDeviceAsync(string serialNumber, IProgress<double>? progress = null) => Task.CompletedTask;
|
||||
#pragma warning disable CS0067
|
||||
public event EventHandler<ZuneDevice>? DeviceConnected;
|
||||
public event EventHandler<string>? DeviceDisconnected;
|
||||
#pragma warning restore CS0067
|
||||
}
|
||||
Reference in New Issue
Block a user