Skip to content

[WIP] Music Store ideas #133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@
<local:ViewLocator/>
</Application.DataTemplates>

<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Avalonia.MusicStore/Icons.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.MusicStore/Icons.axaml" />
</Application.Styles>
</Application>
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public override void OnFrameworkInitializationCompleted()
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
DataContext = new MainViewModel(),
};
}

Expand Down
22 changes: 11 additions & 11 deletions src/Avalonia.Samples/CompleteApps/Avalonia.MusicStore/Icons.axaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
<StackPanel Spacing="5">
<PathIcon Data="{StaticResource store_microsoft_regular}"></PathIcon>
<PathIcon Data="{StaticResource music_regular}"></PathIcon>
</StackPanel>
</Border>
</Design.PreviewWith>

<Style>
<Style.Resources>
<StreamGeometry x:Key="store_microsoft_regular">M11.5 9.5V13H8V9.5H11.5Z M11.5 17.5V14H8V17.5H11.5Z M16 9.5V13H12.5V9.5H16Z M16 17.5V14H12.5V17.5H16Z M8 6V3.75C8 2.7835 8.7835 2 9.75 2H14.25C15.2165 2 16 2.7835 16 3.75V6H21.25C21.6642 6 22 6.33579 22 6.75V18.25C22 19.7688 20.7688 21 19.25 21H4.75C3.23122 21 2 19.7688 2 18.25V6.75C2 6.33579 2.33579 6 2.75 6H8ZM9.5 3.75V6H14.5V3.75C14.5 3.61193 14.3881 3.5 14.25 3.5H9.75C9.61193 3.5 9.5 3.61193 9.5 3.75ZM3.5 18.25C3.5 18.9404 4.05964 19.5 4.75 19.5H19.25C19.9404 19.5 20.5 18.9404 20.5 18.25V7.5H3.5V18.25Z</StreamGeometry>
<StreamGeometry x:Key="music_regular">M11.5,2.75 C11.5,2.22634895 12.0230228,1.86388952 12.5133347,2.04775015 L18.8913911,4.43943933 C20.1598961,4.91511241 21.0002742,6.1277638 21.0002742,7.48252202 L21.0002742,10.7513533 C21.0002742,11.2750044 20.4772513,11.6374638 19.9869395,11.4536032 L13,8.83332147 L13,17.5 C13,17.5545945 12.9941667,17.6078265 12.9830895,17.6591069 C12.9940859,17.7709636 13,17.884807 13,18 C13,20.2596863 10.7242052,22 8,22 C5.27579485,22 3,20.2596863 3,18 C3,15.7403137 5.27579485,14 8,14 C9.3521238,14 10.5937815,14.428727 11.5015337,15.1368931 L11.5,2.75 Z M8,15.5 C6.02978478,15.5 4.5,16.6698354 4.5,18 C4.5,19.3301646 6.02978478,20.5 8,20.5 C9.97021522,20.5 11.5,19.3301646 11.5,18 C11.5,16.6698354 9.97021522,15.5 8,15.5 Z M13,3.83223733 L13,7.23159672 L19.5002742,9.669116 L19.5002742,7.48252202 C19.5002742,6.75303682 19.0477629,6.10007069 18.3647217,5.84393903 L13,3.83223733 Z</StreamGeometry>
</Style.Resources>
</Style>
</Styles>

<StreamGeometry x:Key="store_microsoft_regular">M11.5 9.5V13H8V9.5H11.5Z M11.5 17.5V14H8V17.5H11.5Z M16 9.5V13H12.5V9.5H16Z M16 17.5V14H12.5V17.5H16Z M8 6V3.75C8 2.7835 8.7835 2 9.75 2H14.25C15.2165 2 16 2.7835 16 3.75V6H21.25C21.6642 6 22 6.33579 22 6.75V18.25C22 19.7688 20.7688 21 19.25 21H4.75C3.23122 21 2 19.7688 2 18.25V6.75C2 6.33579 2.33579 6 2.75 6H8ZM9.5 3.75V6H14.5V3.75C14.5 3.61193 14.3881 3.5 14.25 3.5H9.75C9.61193 3.5 9.5 3.61193 9.5 3.75ZM3.5 18.25C3.5 18.9404 4.05964 19.5 4.75 19.5H19.25C19.9404 19.5 20.5 18.9404 20.5 18.25V7.5H3.5V18.25Z</StreamGeometry>
<StreamGeometry x:Key="music_regular">M11.5,2.75 C11.5,2.22634895 12.0230228,1.86388952 12.5133347,2.04775015 L18.8913911,4.43943933 C20.1598961,4.91511241 21.0002742,6.1277638 21.0002742,7.48252202 L21.0002742,10.7513533 C21.0002742,11.2750044 20.4772513,11.6374638 19.9869395,11.4536032 L13,8.83332147 L13,17.5 C13,17.5545945 12.9941667,17.6078265 12.9830895,17.6591069 C12.9940859,17.7709636 13,17.884807 13,18 C13,20.2596863 10.7242052,22 8,22 C5.27579485,22 3,20.2596863 3,18 C3,15.7403137 5.27579485,14 8,14 C9.3521238,14 10.5937815,14.428727 11.5015337,15.1368931 L11.5,2.75 Z M8,15.5 C6.02978478,15.5 4.5,16.6698354 4.5,18 C4.5,19.3301646 6.02978478,20.5 8,20.5 C9.97021522,20.5 11.5,19.3301646 11.5,18 C11.5,16.6698354 9.97021522,15.5 8,15.5 Z M13,3.83223733 L13,7.23159672 L19.5002742,9.669116 L19.5002742,7.48252202 C19.5002742,6.75303682 19.0477629,6.10007069 18.3647217,5.84393903 L13,3.83223733 Z</StreamGeometry>

</ResourceDictionary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace Avalonia.MusicStore.Messages;

public class NotificationMessage
{
public NotificationMessage(string message)
{
Message = message;
}
public string Message { get; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand All @@ -8,7 +9,7 @@

namespace Avalonia.MusicStore.Models
{
public class Album
public class Album : IEquatable<Album>
{
private static iTunesSearchManager s_SearchManager = new();
private static HttpClient s_httpClient = new();
Expand Down Expand Up @@ -67,7 +68,7 @@ public static async Task<IEnumerable<Album>> LoadCachedAsync()
if ((new DirectoryInfo(file).Extension) != ".json")
continue;

await using var fs = File.OpenRead(file);
await using var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read);
results.Add(await Album.LoadFromStream(fs).ConfigureAwait(false));
}

Expand All @@ -81,7 +82,7 @@ public async Task<Stream> LoadCoverBitmapAsync()
{
if (File.Exists(CachePath + ".bmp"))
{
return File.OpenRead(CachePath + ".bmp");
return File.Open(CachePath + ".bmp", FileMode.Open, FileAccess.Read, FileShare.Read);
}
else
{
Expand Down Expand Up @@ -135,5 +136,35 @@ private static async Task SaveToStreamAsync(Album data, Stream stream)
{
await JsonSerializer.SerializeAsync(stream, data).ConfigureAwait(false);
}

public bool Equals(Album? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Artist == other.Artist && Title == other.Title;
}

public override bool Equals(object? obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((Album)obj);
}

public override int GetHashCode()
{
return HashCode.Combine(Artist, Title);
}

public static bool operator ==(Album? left, Album? right)
{
return Equals(left, right);
}

public static bool operator !=(Album? left, Album? right)
{
return !Equals(left, right);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System.Threading.Tasks;
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using Avalonia.MusicStore.Models;
using CommunityToolkit.Mvvm.ComponentModel;

namespace Avalonia.MusicStore.ViewModels
{
public partial class AlbumViewModel : ViewModelBase
public partial class AlbumViewModel : ViewModelBase, IEquatable<AlbumViewModel>
{
private readonly Album _album;

Expand All @@ -18,16 +20,23 @@ public AlbumViewModel(Album album)

public string Title => _album.Title;

[ObservableProperty] public partial Bitmap? Cover { get; private set; }
public Task<Bitmap?> Cover => LoadCoverAsync();

/// <summary>
/// Asynchronously loads and decodes the album cover image, then assigns it to <see cref="Cover"/>.
/// </summary>
public async Task LoadCover()
private async Task<Bitmap?> LoadCoverAsync()
{
await using (var imageStream = await _album.LoadCoverBitmapAsync())
try
{
Cover = await Task.Run(() => Bitmap.DecodeToWidth(imageStream, 400));
await using (var imageStream = await _album.LoadCoverBitmapAsync())
{
return await Task.Run(() => Bitmap.DecodeToWidth(imageStream, 400));
}
}
catch
{
return null;
}
}

Expand All @@ -38,18 +47,36 @@ public async Task SaveToDiskAsync()
{
await _album.SaveAsync();

if (Cover != null)
if (await LoadCoverAsync() is Bitmap cover)
{
var bitmap = Cover;

await Task.Run(() =>
{
using (var fs = _album.SaveCoverBitmapStream())
{
bitmap.Save(fs);
cover.Save(fs);
}
});
}
}

public bool Equals(AlbumViewModel? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return _album.Equals(other._album);
}

public override bool Equals(object? obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((AlbumViewModel)obj);
}

public override int GetHashCode()
{
return _album.GetHashCode();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

namespace Avalonia.MusicStore.ViewModels
{
public partial class MainWindowViewModel : ObservableObject
public partial class MainViewModel : ObservableObject
{
public ObservableCollection<AlbumViewModel> Albums { get; } = new();

public MainWindowViewModel()
public MainViewModel()
{
LoadAlbums();
}
Expand All @@ -25,7 +25,16 @@ public MainWindowViewModel()
private async Task AddAlbumAsync()
{
var album = await WeakReferenceMessenger.Default.Send(new PurchaseAlbumMessage());
if (album is not null)

if (album is null)
{
WeakReferenceMessenger.Default.Send(new NotificationMessage("No Album Selected"));
}
else if (Albums.Contains(album))
{
WeakReferenceMessenger.Default.Send(new NotificationMessage("Album was already added"));
}
else
{
Albums.Add(album);
await album.SaveToDiskAsync();
Expand All @@ -42,8 +51,6 @@ private async void LoadAlbums()
{
Albums.Add(album);
}
var coverTasks = albums.Select(album => album.LoadCover());
await Task.WhenAll(coverTasks);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ public partial class MusicStoreViewModel : ViewModelBase
public partial bool IsBusy { get; private set; }

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(BuyMusicCommand))]
public partial AlbumViewModel? SelectedAlbum { get; set; }

public ObservableCollection<AlbumViewModel> SearchResults { get; } = new();

/// <summary>
/// This relay command sends a message indicating that the selected album has been purchased, which will notify music store view to close.
/// </summary>
[RelayCommand]
[RelayCommand (CanExecute = nameof(CanBuyMusic))]
private void BuyMusic()
{
if (SelectedAlbum != null)
Expand All @@ -38,6 +39,8 @@ private void BuyMusic()
}
}

private bool CanBuyMusic() => SelectedAlbum != null;

/// <summary>
/// Performs an asynchronous search for albums based on the provided term and updates the results.
/// </summary>
Expand All @@ -58,30 +61,9 @@ private async Task DoSearch(string? term)
SearchResults.Add(vm);
}

if (!cancellationToken.IsCancellationRequested)
{
LoadCovers(cancellationToken);
}

IsBusy = false;
}

/// <summary>
/// Asynchronously loads album cover images for each result, unless the operation is canceled.
/// </summary>
private async void LoadCovers(CancellationToken cancellationToken)
{
foreach (var album in SearchResults.ToList())
{
await album.LoadCover();

if (cancellationToken.IsCancellationRequested)
{
return;
}
}
}

/// <summary>
/// Triggered when the search text in music store view changes and initiates a new search operation.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<StackPanel Spacing="5" Width="200">
<Border CornerRadius="10" ClipToBounds="True">
<Panel Background="#7FFF22DD">
<Image Width="200" Stretch="Uniform" Source="{Binding Cover}" />
<Image Width="200" Stretch="Uniform" Source="{Binding Cover^}" />
<Panel Height="200" IsVisible="{Binding Cover, Converter={x:Static ObjectConverters.IsNull}}">
<PathIcon Height="75" Width="75" Data="{StaticResource music_regular}" />
</Panel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
Background="Transparent"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterScreen"
x:DataType="vm:MainWindowViewModel">
x:DataType="vm:MainViewModel">

<Panel>
<ExperimentalAcrylicBorder IsHitTestVisible="False">
Expand All @@ -24,6 +24,10 @@
MaterialOpacity="0.65" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>

<WindowNotificationManager x:Name="NotificationManager"
Position="TopRight" />

<Panel Margin="40">
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
Command="{Binding AddAlbumCommand}">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.MusicStore.Messages;
using Avalonia.MusicStore.ViewModels;
using CommunityToolkit.Mvvm.Messaging;
Expand All @@ -23,6 +25,11 @@ public MainWindow()

m.Reply(dialog.ShowDialog<AlbumViewModel?>(w));
});

WeakReferenceMessenger.Default.Register<MainWindow, NotificationMessage>(this, static (w, m) =>
{
w.NotificationManager.Show(m.Message, NotificationType.Warning, TimeSpan.FromSeconds(3));
});
}
}
}