Adding initial playback bar logic.

This commit is contained in:
2025-03-02 13:46:33 -05:00
parent fc28004c89
commit 1a9c1a5478
16 changed files with 374 additions and 25 deletions

View File

@@ -1,12 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
xmlns:control="clr-namespace:Harmonia.UI.Controls"
xmlns:semi="https://irihi.tech/semi"
x:Class="Harmonia.UI.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
<!--<FluentTheme />-->
<semi:SemiTheme Locale="en-US"/>
</Application.Styles>
<Application.Resources>

View File

@@ -0,0 +1,32 @@
using Avalonia.Data.Converters;
using System;
using System.Collections.Generic;
using System.Globalization;
namespace Harmonia.UI.Converters;
public class ArtistsToStringConverter : IValueConverter
{
public ArtistsToStringConverter()
{
}
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not List<string>)
return string.Empty;
List<string> artists = (List<string>)value;
if (artists == null)
return string.Empty;
return string.Join(" / ", artists);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return new List<string>();
}
}

View File

@@ -0,0 +1,29 @@
using Avalonia.Data.Converters;
using System;
using System.Collections;
using System.Globalization;
namespace Harmonia.UI.Converters;
public sealed class NullVisibilityConverter : IValueConverter
{
public NullVisibilityConverter()
{
}
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is IList list)
{
return list.Count > 0;
}
return value is not null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,37 @@
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace Harmonia.UI.Converters;
public sealed class SecondsToStringConverter : IValueConverter
{
public SecondsToStringConverter()
{
}
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not double doubleValue)
return null;
TimeSpan timeSpan = TimeSpan.FromSeconds(doubleValue);
if (timeSpan.Hours >= 1)
return timeSpan.ToString(@"%h\:mm\:ss");
return timeSpan.ToString(@"%m\:ss");
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value == null)
return null;
if (value is not string stringValue)
return null;
return TimeSpan.Parse(stringValue);
}
}

View File

@@ -0,0 +1,27 @@
using Avalonia.Data.Converters;
using Harmonia.Core.Models;
using System;
using System.Globalization;
namespace Harmonia.UI.Converters;
public sealed class SongTitleConverter : IValueConverter
{
public SongTitleConverter()
{
}
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not Song song)
return null;
return string.IsNullOrWhiteSpace(song.Title) ? song.ShortFileName : song.Title;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -11,14 +11,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
<PackageReference Include="Avalonia" Version="11.2.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.5" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageReference Include="Semi.Avalonia" Version="11.2.1.5" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,10 +1,14 @@
using Avalonia.Media.Imaging;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Harmonia.Core.Caching;
using Harmonia.Core.Imaging;
using Harmonia.Core.Models;
using Harmonia.Core.Player;
using Harmonia.Core.Playlists;
using Harmonia.Core.Scanner;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -14,8 +18,8 @@ public partial class PlaybackBarViewModel : ViewModelBase
{
private readonly IAudioPlayer _audioPlayer;
private readonly IAudioImageCache _audioImageCache;
private readonly DispatcherTimer _timer;
private bool _isPositionChangeInProgress;
private CancellationTokenSource? _audioImageCancellationTokenSource;
private Song? _song;
@@ -95,6 +99,17 @@ public partial class PlaybackBarViewModel : ViewModelBase
}
}
private bool _isPositionChangeInProgress;
public bool IsPositionChangeInProgress
{
get { return _isPositionChangeInProgress; }
set
{
_isPositionChangeInProgress = value;
OnPropertyChanged();
}
}
public bool IsRandom
{
get
@@ -110,12 +125,14 @@ public partial class PlaybackBarViewModel : ViewModelBase
public string Greeting => "Welcome to Harmonia!";
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache)
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner)
{
_audioPlayer = audioPlayer;
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
_audioImageCache = audioImageCache;
_timer = new(TimeSpan.FromMilliseconds(100), DispatcherPriority.Default, TickTock);
}
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
@@ -149,6 +166,16 @@ public partial class PlaybackBarViewModel : ViewModelBase
SongImageSource = new(songPictureInfo.Stream);
}
private void TickTock(object? sender, object e)
{
Position = _audioPlayer.Position;
if (IsPositionChangeInProgress)
return;
CurrentPosition = _audioPlayer.Position;
}
public void Play()
{
_audioPlayer.Play();

View File

@@ -3,15 +3,91 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
xmlns:converter="clr-namespace:Harmonia.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
DataContext="{x:Static vm:ViewModelLocator.PlaybackBarViewModel}"
x:Class="Harmonia.UI.Views.PlaybackBar"
x:DataType="vm:PlaybackBarViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainViewModel />
</Design.DataContext>
<UserControl.Resources>
<converter:SecondsToStringConverter x:Key="SecondsToString" />
<converter:ArtistsToStringConverter x:Key="ArtistsToString" />
<converter:NullVisibilityConverter x:Key="NullVisibility" />
<converter:SongTitleConverter x:Key="SongTitle" />
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<!-- Slider -->
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Center" Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Foreground="#aaaaaa" Text="{Binding Position, Converter={StaticResource SecondsToString}}" FontSize="13" Margin="0 0 6 0" VerticalAlignment="Center"></TextBlock>
<Slider Name="TrackSlider" Loaded="Slider_Loaded" Grid.Column="1" Value="{Binding CurrentPosition, Mode=TwoWay, UpdateSourceTrigger=Explicit}" Minimum="0" Maximum="{Binding MaxPosition, Mode=OneWay}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Foreground="#aaaaaa" Text="{Binding MaxPosition, Converter={StaticResource SecondsToString}}" FontSize="13" Margin="6 0 0 0" VerticalAlignment="Center"></TextBlock>
</Grid>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Song Info -->
<Grid Grid.Row="1" Grid.Column="0" Name="PlayingSongGrid">
<StackPanel Orientation="Horizontal">
<Grid Margin="0 0 10 0">
<!--<Image Name="PlayingSongImage" Width="80" Height="80"></Image>-->
<Image Source="{Binding SongImageSource, Mode=TwoWay}" Width="80" Height="80"></Image>
<Canvas Background="#19000000"></Canvas>
</Grid>
<StackPanel VerticalAlignment="Center">
<TextBlock Foreground="#dddddd" FontWeight="SemiBold" FontSize="16" LineHeight="20" Text="{Binding Song, Converter={StaticResource SongTitle}}"></TextBlock>
<TextBlock Foreground="#aaaaaa" FontSize="14" LineHeight="20" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}"></TextBlock>
<TextBlock Foreground="#aaaaaa" FontSize="14" LineHeight="20" Text="{Binding Song.Album}"></TextBlock>
<TextBlock Foreground="#777" FontSize="12" IsVisible="{Binding Song, Converter={StaticResource NullVisibility}}" FontWeight="Normal" Opacity="1.0">
<Run Text="{Binding Song.FileType}"></Run>
<Run Text=" - "></Run>
<Run Text="{Binding Song.BitRate}"></Run>
<Run Text=" kbps - "></Run>
<Run Text="{Binding Song.SampleRate}"></Run>
<Run Text=" Hz"></Run>
</TextBlock>
</StackPanel>
</StackPanel>
</Grid>
<!-- Action Buttons -->
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Button Margin="8 0 8 0" Click="PreviousSongButton_Click" Theme="{StaticResource BorderlessButton}">
<Button.Content>
<StackPanel Orientation="Horizontal">
<PathIcon Data="{StaticResource SemiIconBackward}" Margin="0 0 0 0"></PathIcon>
<TextBlock Text="" FontSize="18"/>
</StackPanel>
</Button.Content>
</Button>
<Button Margin="8 0 8 0" Click="StopButton_Click">
<TextBlock Text="&#x0042;" FontSize="18"></TextBlock>
</Button>
<Button Margin="8 0 8 0" Click="PlayButton_Click">
<TextBlock Text="&#x0041;" FontSize="36"></TextBlock>
</Button>
<Button Margin="8 0 8 0" Click="PauseButton_Click">
<TextBlock Text="&#x0043;" FontSize="18"></TextBlock>
</Button>
<Button Margin="8 0 8 0" Click="NextSongButton_Click">
<TextBlock Text="&#x0045;" FontSize="18"></TextBlock>
</Button>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -1,11 +1,73 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Harmonia.UI.ViewModels;
namespace Harmonia.UI.Views;
public partial class PlaybackBar : UserControl
{
private readonly PlaybackBarViewModel _viewModel;
public PlaybackBar()
{
InitializeComponent();
_viewModel = (PlaybackBarViewModel)DataContext!;
}
private void Slider_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (sender is not Slider slider)
return;
slider.AddHandler(PointerPressedEvent, OnSliderPointerPressed, RoutingStrategies.Tunnel);
slider.AddHandler(PointerReleasedEvent, OnSliderPointerReleased, RoutingStrategies.Tunnel);
}
private void OnSliderPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is not Slider slider)
return;
PointerPoint currentPoint = e.GetCurrentPoint(slider);
if (currentPoint.Properties.IsLeftButtonPressed == false)
return;
_viewModel.IsPositionChangeInProgress = true;
}
private void OnSliderPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender is not Slider slider)
return;
_viewModel.CurrentPosition = slider.Value;
_viewModel.IsPositionChangeInProgress = false;
}
private void PlayButton_Click(object? sender, RoutedEventArgs e)
{
_viewModel.Play();
}
private void StopButton_Click(object? sender, RoutedEventArgs e)
{
_viewModel.Stop();
}
private void PauseButton_Click(object? sender, RoutedEventArgs e)
{
_viewModel.Pause();
}
private void PreviousSongButton_Click(object? sender, RoutedEventArgs e)
{
_viewModel.Previous();
}
private void NextSongButton_Click(object sender, RoutedEventArgs e)
{
_viewModel.Next();
}
}