跨平台WPF音乐商店应用程序

news2024/9/22 9:58:16

目录

一 简介

二 设计思路

三 源码


一 简介

支持在线检索音乐,支持实时浏览当前收藏的音乐及音乐数据的持久化。

二 设计思路

采用MVVM架构,前后端分离,子界面弹出始终位于主界面的中心。

三 源码

视窗引导启动源码:

namespace Avalonia.MusicStore
{
    public class ViewLocator : IDataTemplate
    {

        public Control? Build(object? data)
        {
            if (data is null)
                return null;

            var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
            var type = Type.GetType(name);

            if (type != null)
            {
                var control = (Control)Activator.CreateInstance(type)!;
                control.DataContext = data;
                return control;
            }

            return new TextBlock { Text = "Not Found: " + name };
        }

        public bool Match(object? data)
        {
            return data is ViewModelBase;
        }
    }
}







using Avalonia;
using Avalonia.ReactiveUI;
using System;

namespace Avalonia.MusicStore
{
    internal sealed 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.
        [STAThread]
        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()
                .WithInterFont()
                .LogToTrace()
                .UseReactiveUI();
    }
}

模型源码:

using iTunesSearch.Library;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace Avalonia.MusicStore.Models
{

    public class Album
    {
        private static iTunesSearchManager s_SearchManager = new();

        public string Artist { get; set; }
        public string Title { get; set; }
        public string CoverUrl { get; set; }

        public Album(string artist, string title, string coverUrl)
        {
            Artist = artist;
            Title = title;
            CoverUrl = coverUrl;
        }

        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")));
        }



        private static HttpClient s_httpClient = new();
        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 SaveCoverBitmapStream()
        {
            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;
        }
    }
}

模型视图源码:

using Avalonia.Media.Imaging;
using Avalonia.MusicStore.Models;
using ReactiveUI;
using System.Threading.Tasks;

namespace Avalonia.MusicStore.ViewModels
{
    public class AlbumViewModel : ViewModelBase
    {
        private readonly Album _album;
        public AlbumViewModel(Album album)
        {
            _album = album;
        }

        public string Artist => _album.Artist;
        public string Title => _album.Title;

        private Bitmap? _cover;

        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 async Task SaveToDiskAsync()
        {
            await _album.SaveAsync();

            if (Cover != null)
            {
                var bitmap = Cover;

                await Task.Run(() =>
                {
                    using (var fs = _album.SaveCoverBitmapStream())
                    {
                        bitmap.Save(fs);
                    }
                });
            }
        }
    }
}
using Avalonia.MusicStore.Models;
using ReactiveUI;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Windows.Input;

namespace Avalonia.MusicStore.ViewModels
{
    public class MainWindowViewModel : ViewModelBase
    {
        public ICommand BuyMusicCommand { get; }
        public Interaction<MusicStoreViewModel, AlbumViewModel?> ShowDialog { get; }
        public ObservableCollection<AlbumViewModel> Albums { get; } = new();


        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);
                    await result.SaveToDiskAsync();
                }
            });
            RxApp.MainThreadScheduler.Schedule(LoadAlbums);
        }

        private async void LoadAlbums()
        {
            var albums = (await Album.LoadCachedAsync()).Select(x => new AlbumViewModel(x));

            foreach (var album in albums)
            {
                Albums.Add(album);
            }

            foreach (var album in Albums.ToList())
            {
                await album.LoadCover();
            }
        }
    }
}
using Avalonia.MusicStore.Models;
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;

namespace Avalonia.MusicStore.ViewModels
{
    public class MusicStoreViewModel : ViewModelBase
    {
        private string? _searchText;
        private bool _isBusy;

        public string? SearchText
        {
            get => _searchText;
            set => this.RaiseAndSetIfChanged(ref _searchText, value);
        }

        public bool IsBusy
        {
            get => _isBusy;
            set => this.RaiseAndSetIfChanged(ref _isBusy, value);
        }

        private AlbumViewModel? _selectedAlbum;
        public ObservableCollection<AlbumViewModel> SearchResults { get; } = new();
        public AlbumViewModel? SelectedAlbum
        {
            get => _selectedAlbum;
            set => this.RaiseAndSetIfChanged(ref _selectedAlbum, value);
        }

        public MusicStoreViewModel()
        {
            this.WhenAnyValue(x => x.SearchText)
                .Throttle(TimeSpan.FromMilliseconds(400))
                .ObserveOn(RxApp.MainThreadScheduler)
                .Subscribe(DoSearch!);

            BuyMusicCommand = ReactiveCommand.Create(() =>
            {
                return SelectedAlbum;
            });
        }

        private async void DoSearch(string s)
        {
            IsBusy = true;
            SearchResults.Clear();

            _cancellationTokenSource?.Cancel();
            _cancellationTokenSource = new CancellationTokenSource();
            var cancellationToken = _cancellationTokenSource.Token;

            if (!string.IsNullOrWhiteSpace(s))
            {
                var albums = await Album.SearchAsync(s);

                foreach (var album in albums)
                {
                    var vm = new AlbumViewModel(album);

                    SearchResults.Add(vm);
                }

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

            IsBusy = false;
        }


        private async void LoadCovers(CancellationToken cancellationToken)
        {
            foreach (var album in SearchResults.ToList())
            {
                await album.LoadCover();

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

        private CancellationTokenSource? _cancellationTokenSource;
        public ReactiveCommand<Unit, AlbumViewModel?> BuyMusicCommand { get; }

    }
}
using ReactiveUI;

namespace Avalonia.MusicStore.ViewModels
{
    public class ViewModelBase : ReactiveObject
    {
    }
}

视图源码:

<UserControl
    x:Class="Avalonia.MusicStore.Views.AlbumView"
    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"
    xmlns:vm="using:Avalonia.MusicStore.ViewModels"
    Width="200"
    d:DesignHeight="450"
    d:DesignWidth="800"
    x:DataType="vm:AlbumViewModel"
    mc:Ignorable="d">
    <StackPanel Width="200" Spacing="5">
        <Border ClipToBounds="True" CornerRadius="10">
            <Panel Background="#7FFF22DD">
                <Image
                    Width="200"
                    Source="{Binding Cover}"
                    Stretch="Uniform" />
                <Panel Height="200" IsVisible="{Binding Cover, Converter={x:Static ObjectConverters.IsNull}}">
                    <PathIcon
                        Width="75"
                        Height="75"
                        Data="{StaticResource music_regular}" />
                </Panel>
            </Panel>
        </Border>
        <TextBlock HorizontalAlignment="Center" Text="{Binding Title}" />
        <TextBlock HorizontalAlignment="Center" Text="{Binding Artist}" />
    </StackPanel>
</UserControl>
<Window
    x:Class="Avalonia.MusicStore.Views.MainWindow"
    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"
    xmlns:views="clr-namespace:Avalonia.MusicStore.Views"
    xmlns:vm="using:Avalonia.MusicStore.ViewModels"
    Title="Avalonia.MusicStore"
    d:DesignHeight="450"
    d:DesignWidth="800"
    x:DataType="vm:MainWindowViewModel"
    Background="Transparent"
    ExtendClientAreaToDecorationsHint="True"
    Icon="/Assets/avalonia-logo.ico"
    TransparencyLevelHint="AcrylicBlur"
    WindowStartupLocation="CenterScreen"
    mc:Ignorable="d">

    <Panel>
        <ExperimentalAcrylicBorder IsHitTestVisible="False">
            <ExperimentalAcrylicBorder.Material>
                <ExperimentalAcrylicMaterial
                    BackgroundSource="Digger"
                    MaterialOpacity="0.65"
                    TintColor="Black"
                    TintOpacity="1" />
            </ExperimentalAcrylicBorder.Material>
        </ExperimentalAcrylicBorder>
        <Panel Margin="40">
            <Button
                HorizontalAlignment="Right"
                VerticalAlignment="Top"
                Command="{Binding BuyMusicCommand}">
                <PathIcon Data="{StaticResource store_microsoft_regular}" />
            </Button>
            <ItemsControl Margin="0,40,0,0" ItemsSource="{Binding Albums}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>

                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <views:AlbumView Margin="0,0,20,20" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Panel>
    </Panel>

</Window>
using Avalonia.MusicStore.ViewModels;
using Avalonia.ReactiveUI;
using ReactiveUI;
using System.Threading.Tasks;

namespace Avalonia.MusicStore.Views
{
    public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
    {
        public MainWindow()
        {
            InitializeComponent();
            this.WhenActivated(action => action(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);
        }
    }
}
<UserControl
    x:Class="Avalonia.MusicStore.Views.MusicStoreView"
    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"
    xmlns:vm="using:Avalonia.MusicStore.ViewModels"
    d:DesignHeight="450"
    d:DesignWidth="800"
    x:DataType="vm:MusicStoreViewModel"
    mc:Ignorable="d">
    <DockPanel>
        <StackPanel DockPanel.Dock="Top">
            <TextBox Text="{Binding SearchText}" Watermark="Search for Albums...." />
            <ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
        </StackPanel>
        <Button
            HorizontalAlignment="Center"
            Command="{Binding BuyMusicCommand}"
            Content="Buy Album"
            DockPanel.Dock="Bottom" />
        <ListBox
            Margin="0,20"
            Background="Transparent"
            ItemsSource="{Binding SearchResults}"
            SelectedItem="{Binding SelectedAlbum}">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </DockPanel>
</UserControl>
<Window
    x:Class="Avalonia.MusicStore.Views.MusicStoreWindow"
    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"
    xmlns:views="using:Avalonia.MusicStore.Views"
    Title="MusicStoreWindow"
    Width="1000"
    Height="550"
    ExtendClientAreaToDecorationsHint="True"
    TransparencyLevelHint="AcrylicBlur"
    WindowStartupLocation="CenterOwner"
    mc:Ignorable="d">
    <Panel>
        <ExperimentalAcrylicBorder IsHitTestVisible="False">
            <ExperimentalAcrylicBorder.Material>
                <ExperimentalAcrylicMaterial
                    BackgroundSource="Digger"
                    MaterialOpacity="0.65"
                    TintColor="Black"
                    TintOpacity="1" />
            </ExperimentalAcrylicBorder.Material>
        </ExperimentalAcrylicBorder>

        <Panel Margin="40">
            <views:MusicStoreView />
        </Panel>
    </Panel>
</Window>
using Avalonia.MusicStore.ViewModels;
using Avalonia.ReactiveUI;
using ReactiveUI;
using System;

namespace Avalonia.MusicStore.Views
{
    public partial class MusicStoreWindow : ReactiveWindow<MusicStoreViewModel>
    {
        public MusicStoreWindow()
        {
            InitializeComponent();
            this.WhenActivated(action => action(ViewModel!.BuyMusicCommand.Subscribe(Close)));
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1932941.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI、ML、DL 和 DS 之间的区别

人工智能 数据分析 数据科学 深度学习 机器学习 神经网络 在本文中&#xff0c;我们将了解 AI&#xff08;人工智能&#xff09;、ML&#xff08;机器学习&#xff09;、DL&#xff08;深度学习&#xff09;和 DS&#xff08;数据科学&#xff09;之间的区别。这些术语经常互换…

JVM基本知识——运行空间

JVM&#xff08;Java Virtual Machine&#xff09;即Java虚拟机&#xff0c;是负责读取java字节码&#xff0c;并在实际的硬件环境中运行。 JVM可以分为三部分&#xff1a;类装载器&#xff08;ClassLoader&#xff09;子系统、内存空间、执行引擎 内存空间&#xff08;运行时…

数仓建模详解:电商案例解析(第38天)

系列文章目录 一、需求分析 二、数据仓库设计 三、数据集成与处理 四、数据仓库实施与维护 五、实例&#xff1a;电商平台数据仓库建模 六、设计过程中遇到的常见问题有哪些 文章目录 系列文章目录前言常见数仓分层架构图一、需求分析二、数据仓库设计三、数据集成与处理四、数…

海外社媒矩阵为何会被关联?如何IP隔离?

在当今的数字时代&#xff0c;社交媒体已经成为人们日常生活中不可或缺的一部分。通过社交媒体&#xff0c;人们可以与朋友互动&#xff0c;分享生活&#xff0c;甚至进行业务推广和营销。然而&#xff0c;社交媒体账号关联问题逐渐受到广泛关注。社交媒体账号为何会关联&#…

Spring MVC 的常用注解

RequestMapping 和 RestController注解 上面两个注解&#xff0c;是Spring MCV最常用的注解。 RequestMapping &#xff0c; 他是用来注册接口的路由映射。 路由映射&#xff1a;当一个用户访问url时&#xff0c;将用户的请求对应到某个方法或类的过程叫做路由映射。 Reques…

学习008-01-02 Define the Data Model and Set the Initial Data(定义数据模型并设置初始数据 )

Define the Data Model and Set the Initial Data&#xff08;定义数据模型并设置初始数据 &#xff09; This topic explains how to implement entity classes for your application. It also describes the basics of automatic user interface construction based on a da…

SAP ABAP性能优化分析工具

SAP系统提供了许多性能调优的工具&#xff0c;重点介绍下最常用几种SM50, ST05, SAT等工具&#xff1a; 1.工具概况 1.1 SM50 / SM66 - 工作进程监视器 通过这两个T-code, 可以查看当前SAP AS实例上面的工作进程&#xff0c;当某一工作进程长时间处于running的状态时&#…

现场可重构CPLD芯片应用案例—蓝牙音箱

我司英尚微提供的高性能数模混合现场可重构IC、通用可配置的模数混合芯片内部集成丰富的模拟资源和数字资源&#xff0c;可轻松替代电路中的各种标准器件&#xff0c;并按照客户要求组合成最优小型ASIC&#xff0c;缩短开发周期&#xff0c;降低成本。下面介绍LS98002现场可重构…

Kafka Producer之ACKS应答机制

文章目录 1. 应答机制2. 等级03. 等级14. 等级all5. 设置等级6. ISR 1. 应答机制 异步发送的效率高&#xff0c;但是不安全&#xff0c;同步发送安全&#xff0c;但是效率低。 无论哪一种&#xff0c;有一个关键的步骤叫做回调&#xff0c;也就是ACKS应答机制。 其中ACKS也分…

AQS详解(详细图文)

目录 AQS详解1、AQS简介AbstractQueuedSynchronizer的继承结构和类属性AQS的静态内部类Node总结AQS的实现思想总结AQS的实现原理AQS和锁的关系 2、AQS的核心方法AQS管理共享资源的方式独占方式下&#xff0c;AQS获取资源的流程详解独占方式下&#xff0c;AQS释放资源的流程详解…

Django 请求和响应

1、请求 &#xff08;1&#xff09;get请求 用户直接在浏览器输入网址&#xff0c;参数直接在url中携带 http://127.0.0.1:8000/login/?a1&b%221243%22 &#xff08;2&#xff09;post请求 在html使用post,login.html <!DOCTYPE html> <html lang"en&…

文献阅读:tidyomics 生态系统:增强组学数据分析

文献介绍 文献题目&#xff1a; The tidyomics ecosystem: enhancing omic data analyses 研究团队&#xff1a; Stefano Mangiola&#xff08;澳大利亚沃尔特和伊丽莎霍尔医学研究所&#xff09;、Michael I. Love&#xff08;美国北卡罗来纳大学教堂山分校&#xff09;、Ant…

详细分析Springboot自定义启动界面(附Demo)

目录 前言1. banner.text1.1 配置文件关闭1.2 启动类关闭1.3 命令行关闭 2. 自定义Banner类3. 自动配置类4. 总结 前言 实现自定义启动动画是一项有趣的任务&#xff0c;虽然Spring Boot本身不提供内置的动画功能&#xff0c;但可以通过一些技巧来实现 以下主要以Demo的形式展…

windows docker nvidia wsl2

下载驱动(GeForce Experience里的也可以)https://www.nvidia.cn/Download/index.aspx 安装wsl2https://blog.csdn.net/qq_39942341/article/details/121512900?ops_request_misc%257B%2522request%255Fid%2522%253A%2522172122816816800227436617%2522%252C%2522scm%2522%253A…

【STM32】MPU内存保护单元

注&#xff1a;仅在F7和M7系列上使用介绍 功能&#xff1a; 设置不同存储区域的存储器访问权限&#xff08;管理员、用户&#xff09; 设置存储器&#xff08;内存和外设&#xff09;属性&#xff08;可缓冲、可缓存、可共享&#xff09; 优点&#xff1a;提高嵌入式系统的健壮…

用了6年git,不知道cherry-pick是啥意思

背景 可能是测试开发角色原因&#xff0c;平时很少有代码冲突或多人协同的编码场景。今天有个协同项目&#xff0c;需要提交自己的代码到其它业务的代码库中&#xff0c;这个代码库是分支开发分支上线模式&#xff0c;同时会有多个同事提交代码&#xff0c;然后模块负责的同学…

队列(Queue),循环队列,双端队列(Deque)and LeetCode刷题

队列&#xff08;Queue&#xff09;&#xff0c;循环队列&#xff0c;双端队列&#xff08;Deque&#xff09;and LeetCode刷题 1. 队列的概念2.队列的使用3. 队列的模拟实现3.1 用链式结构实现队列3.2 用顺序结构实现队列 4. 循环队列5. 双端队列&#xff08;Deque&#xff09…

探索AI播客:PocketPod——定制化音频内容的未来引领者

在数字内容日新月异的今天,从文字、图片到视频、音乐,每一种媒介形式都在不断进化,以满足人们日益增长的多元化需求。而现在,一个全新的领域正蓄势待发——AI生成的播客内容,正由创新先锋PocketPod平台引领,为音频世界带来前所未有的变革。 一、产品概览:PocketPod——…

百度网盘Android一二面凉经(2024)

百度网盘Android一二面凉经(2024) 笔者作为一名双非二本毕业7年老Android, 最近面试了不少公司, 目前已告一段落, 整理一下各家的面试问题, 打算陆续发布出来, 供有缘人参考。今天给大家带来的是《百度网盘Android一二面凉经(2024)》。 面试职位: 网盘主端研发组_Android高级研…

Spring Data Jpa 原生SQL联表查询返回自定义DTO

Spring Data Jpa 原生SQL联表查询返回自定义DTO 方案一&#xff1a;返回Map 这个就不说了 方案二&#xff1a;实体定义成接口的形式 该方式最直观&#xff01;&#xff01;推荐&#xff01;&#xff01;&#xff01; 注意&#xff1a;XxxDto是interface接口&#xff0c;而…