This commit is contained in:
Dan Walmsley 2021-03-05 15:33:06 +00:00
Коммит b56116bf15
24 изменённых файлов: 1803 добавлений и 0 удалений

7
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.DS_Store
.idea

Просмотреть файл

@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using iTunesSearch.Library;
namespace Avalonia.MusicStore.Backend
{
public class Album
{
private static HttpClient s_httpClient = new();
private static iTunesSearchManager s_SearchManager = new();
public Album(string artist, string title, string coverUrl)
{
Artist = artist;
Title = title;
CoverUrl = coverUrl;
}
public string Artist { get; set; }
public string Title { get; set; }
public string CoverUrl { get; set; }
private string CachePath => $"./Cache/{Artist} - {Title}";
public async Task<Stream> LoadCoverBitmapAsync()
{
if (File.Exists(CachePath + ".bmp"))
{
return File.OpenRead(CachePath + ".bmp");
}
else
{
var data = await s_httpClient.GetByteArrayAsync(CoverUrl);
return new MemoryStream(data);
}
}
public async Task SaveAsync()
{
if (!Directory.Exists("./Cache"))
{
Directory.CreateDirectory("./Cache");
}
using (var fs = File.OpenWrite(CachePath))
{
await SaveToStreamAsync(this, fs);
}
}
public Stream SaveCoverBitmapSteam()
{
return File.OpenWrite(CachePath + ".bmp");
}
private static async Task SaveToStreamAsync(Album data, Stream stream)
{
await JsonSerializer.SerializeAsync(stream, data).ConfigureAwait(false);
}
public static async Task<Album> LoadFromStream(Stream stream)
{
return (await JsonSerializer.DeserializeAsync<Album>(stream).ConfigureAwait(false))!;
}
public static async Task<IEnumerable<Album>> LoadCachedAsync()
{
if (!Directory.Exists("./Cache"))
{
Directory.CreateDirectory("./Cache");
}
var results = new List<Album>();
foreach (var file in Directory.EnumerateFiles("./Cache"))
{
if (!string.IsNullOrWhiteSpace(new DirectoryInfo(file).Extension)) continue;
await using var fs = File.OpenRead(file);
results.Add(await Album.LoadFromStream(fs).ConfigureAwait(false));
}
return results;
}
public static async Task<IEnumerable<Album>> SearchAsync(string searchTerm)
{
var query = await s_SearchManager.GetAlbumsAsync(searchTerm).ConfigureAwait(false);
return query.Albums.Select(x =>
new Album(x.ArtistName, x.CollectionName, x.ArtworkUrl100.Replace("100x100bb", "600x600bb")));
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="iTunesSearch" Version="1.0.44" />
</ItemGroup>
</Project>

22
Avalonia.MusicStore.sln Normal file
Просмотреть файл

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.MusicStore", "Avalonia.MusicStore\Avalonia.MusicStore.csproj", "{EE95FC68-9062-4CC2-8E9E-9F9B846E07C1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.MusicStore.Backend", "Avalonia.MusicStore.Backend\Avalonia.MusicStore.Backend.csproj", "{6F986AEB-CA11-4E42-ACCF-CA3D1681B121}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EE95FC68-9062-4CC2-8E9E-9F9B846E07C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE95FC68-9062-4CC2-8E9E-9F9B846E07C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE95FC68-9062-4CC2-8E9E-9F9B846E07C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE95FC68-9062-4CC2-8E9E-9F9B846E07C1}.Release|Any CPU.Build.0 = Release|Any CPU
{6F986AEB-CA11-4E42-ACCF-CA3D1681B121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F986AEB-CA11-4E42-ACCF-CA3D1681B121}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F986AEB-CA11-4E42-ACCF-CA3D1681B121}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F986AEB-CA11-4E42-ACCF-CA3D1681B121}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

8
Avalonia.MusicStore/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,8 @@
.idea/
.vscode/
.vs/
bin/
obj/
*.user

Просмотреть файл

@ -0,0 +1,13 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Avalonia.MusicStore"
x:Class="Avalonia.MusicStore.App">
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme Mode="Dark"/>
<StyleInclude Source="avares://Avalonia.MusicStore/Icons.axaml" />
</Application.Styles>
</Application>

Просмотреть файл

@ -0,0 +1,29 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.MusicStore.ViewModels;
using Avalonia.MusicStore.Views;
namespace Avalonia.MusicStore
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

Двоичные данные
Avalonia.MusicStore/Assets/avalonia-logo.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 172 KiB

Просмотреть файл

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.0" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.0" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.0" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.MusicStore.Backend\Avalonia.MusicStore.Backend.csproj" />
</ItemGroup>
</Project>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,20 @@
using Avalonia.ReactiveUI;
namespace Avalonia.MusicStore
{
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.MusicStore.ViewModels;
namespace Avalonia.MusicStore
{
public class ViewLocator : IDataTemplate
{
public bool SupportsRecycling => false;
public IControl Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control) Activator.CreateInstance(type)!;
}
else
{
return new TextBlock {Text = "Not Found: " + name};
}
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}

Просмотреть файл

@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Avalonia.Media.Imaging;
using Avalonia.MusicStore.Backend;
using ReactiveUI;
namespace Avalonia.MusicStore.ViewModels
{
public class AlbumViewModel : ViewModelBase
{
private Bitmap? _cover;
private readonly Album _album;
public AlbumViewModel(Album album)
{
_album = album;
}
public string Artist => _album.Artist;
public string Title => _album.Title;
public Bitmap? Cover
{
get => _cover;
private set => this.RaiseAndSetIfChanged(ref _cover, value);
}
public async Task LoadCover()
{
await using (var imageStream =await _album.LoadCoverBitmapAsync())
{
Cover = await Task.Run(() => Bitmap.DecodeToWidth(imageStream, 400));
}
}
public static async Task<IEnumerable<AlbumViewModel>> LoadCached()
{
return (await Album.LoadCachedAsync()).Select(x => new AlbumViewModel(x));
}
public async Task SaveToDiskAsync()
{
await _album.SaveAsync();
if (Cover != null)
{
await Task.Run(() =>
{
using (var fs = _album.SaveCoverBitmapSteam())
{
Cover.Save(fs);
}
});
}
}
}
}

Просмотреть файл

@ -0,0 +1,57 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Windows.Input;
using ReactiveUI;
namespace Avalonia.MusicStore.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
ShowDialog = new Interaction<MusicStoreViewModel, AlbumViewModel?>();
BuyMusicCommand = ReactiveCommand.CreateFromTask(async () =>
{
var store = new MusicStoreViewModel();
var result = await ShowDialog.Handle(store);
if (result != null)
{
Albums.Add(result);
}
});
RxApp.MainThreadScheduler.Schedule(LoadAlbums);
}
private async void LoadAlbums()
{
var albums = await AlbumViewModel.LoadCached();
foreach (var album in albums)
{
Albums.Add(album);
}
LoadCovers();
}
private async void LoadCovers()
{
foreach (var album in Albums.ToList())
{
await album.LoadCover();
}
}
public ObservableCollection<AlbumViewModel> Albums { get; } = new();
public ICommand BuyMusicCommand { get; }
public Interaction<MusicStoreViewModel, AlbumViewModel?> ShowDialog { get; }
}
}

Просмотреть файл

@ -0,0 +1,100 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using Avalonia.MusicStore.Backend;
using ReactiveUI;
namespace Avalonia.MusicStore.ViewModels
{
public class MusicStoreViewModel : ViewModelBase
{
private string? _searchText;
private bool _isBusy;
private CancellationTokenSource? _cancellationTokenSource;
private AlbumViewModel? _selectedAlbum;
public MusicStoreViewModel()
{
this.WhenAnyValue(x => x.SearchText)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Throttle(TimeSpan.FromMilliseconds(400))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(DoSearch!);
BuyMusicCommand = ReactiveCommand.CreateFromTask(async () =>
{
if (SelectedAlbum is { })
{
await SelectedAlbum.SaveToDiskAsync();
return SelectedAlbum;
}
return null;
});
}
public string? SearchText
{
get => _searchText;
set => this.RaiseAndSetIfChanged(ref _searchText, value);
}
public bool IsBusy
{
get => _isBusy;
set => this.RaiseAndSetIfChanged(ref _isBusy, value);
}
public ReactiveCommand<Unit, AlbumViewModel?> BuyMusicCommand { get; }
public ObservableCollection<AlbumViewModel> SearchResults { get; } = new();
public AlbumViewModel? SelectedAlbum
{
get => _selectedAlbum;
set => this.RaiseAndSetIfChanged(ref _selectedAlbum, value);
}
private async void DoSearch(string s)
{
IsBusy = true;
SearchResults.Clear();
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
var albums = await Album.SearchAsync(s);
foreach (var album in albums)
{
var vm = new AlbumViewModel(album);
SearchResults.Add(vm);
}
if (!_cancellationTokenSource.IsCancellationRequested)
{
LoadCovers(_cancellationTokenSource.Token);
}
IsBusy = false;
}
private async void LoadCovers(CancellationToken cancellationToken)
{
foreach (var album in SearchResults.ToList())
{
await album.LoadCover();
if (cancellationToken.IsCancellationRequested)
{
return;
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using ReactiveUI;
namespace Avalonia.MusicStore.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

Просмотреть файл

@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.MusicStore.Views.AlbumView">
<StackPanel Spacing="5" Width="200">
<Border CornerRadius="10" ClipToBounds="True">
<Panel Background="#7FFF22DD">
<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>
</Panel>
</Border>
<TextBlock Text="{Binding Title}" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Artist}" HorizontalAlignment="Center" />
</StackPanel>
</UserControl>

Просмотреть файл

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Avalonia.MusicStore.Views
{
public class AlbumView : UserControl
{
public AlbumView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

Просмотреть файл

@ -0,0 +1,45 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Avalonia.MusicStore.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.MusicStore.Views.MainWindow"
WindowStartupLocation="CenterScreen"
Background="Transparent"
TransparencyLevelHint="AcrylicBlur"
ExtendClientAreaToDecorationsHint="True"
Icon="/Assets/avalonia-logo.ico"
Title="Avalonia.MusicStore">
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Panel>
<ExperimentalAcrylicBorder IsHitTestVisible="False">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
BackgroundSource="Digger"
TintColor="Black"
TintOpacity="1"
MaterialOpacity="0.65" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<Panel Margin="40">
<Button Command="{Binding BuyMusicCommand}" HorizontalAlignment="Right" VerticalAlignment="Top">
<PathIcon Data="{StaticResource store_microsoft_regular}" />
</Button>
<ItemsControl Items="{Binding Albums}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Panel>
</Panel>
</Window>

Просмотреть файл

@ -0,0 +1,37 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.MusicStore.ViewModels;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Avalonia.MusicStore.Views
{
public class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public MainWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
this.WhenActivated(d => d(ViewModel.ShowDialog.RegisterHandler(DoShowDialogAsync)));
}
private async Task DoShowDialogAsync(InteractionContext<MusicStoreViewModel, AlbumViewModel?> interaction)
{
var dialog = new MusicStoreWindow();
dialog.DataContext = interaction.Input;
var result = await dialog.ShowDialog<AlbumViewModel?>(this);
interaction.SetOutput(result);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.MusicStore.Views.MusicStoreView">
<DockPanel>
<StackPanel DockPanel.Dock="Top">
<TextBox Text="{Binding SearchText}" Watermark="Search for Albums...." />
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
</StackPanel>
<Button Command="{Binding BuyMusicCommand}" Content="Buy Album" DockPanel.Dock="Bottom" HorizontalAlignment="Center" />
<ListBox Items="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" Background="Transparent" Margin="0 20">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</DockPanel>
</UserControl>

Просмотреть файл

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Avalonia.MusicStore.Views
{
public class MusicStoreView : UserControl
{
public MusicStoreView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.MusicStore.Views.MusicStoreWindow"
xmlns:local="using:Avalonia.MusicStore.Views"
Width="1000" Height="550" WindowStartupLocation="CenterOwner"
Background="Transparent"
TransparencyLevelHint="AcrylicBlur"
ExtendClientAreaToDecorationsHint="True"
Title="MusicStoreWindow">
<Panel>
<ExperimentalAcrylicBorder IsHitTestVisible="False">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
BackgroundSource="Digger"
TintColor="Black"
TintOpacity="1"
MaterialOpacity="0.65" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<Panel Margin="40">
<local:MusicStoreView />
</Panel>
</Panel>
</Window>

Просмотреть файл

@ -0,0 +1,26 @@
using Avalonia.Markup.Xaml;
using Avalonia.MusicStore.ViewModels;
using Avalonia.ReactiveUI;
using ReactiveUI;
using System;
namespace Avalonia.MusicStore.Views
{
public class MusicStoreWindow : ReactiveWindow<MusicStoreViewModel>
{
public MusicStoreWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
this.WhenActivated(d => d(ViewModel.BuyMusicCommand.Subscribe(Close)));
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}