diff --git a/HarmonyWinUI/.editorconfig b/HarmonyWinUI/.editorconfig index ed024d0..83c28bd 100644 --- a/HarmonyWinUI/.editorconfig +++ b/HarmonyWinUI/.editorconfig @@ -4,6 +4,11 @@ root = true # All files [*] trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 +tab_width = 4 +end_of_line = lf +insert_final_newline = true # C# files [*.cs] diff --git a/HarmonyWinUI/HarmonyWinUI.csproj b/HarmonyWinUI/HarmonyWinUI.csproj index 8ba262f..881f798 100644 --- a/HarmonyWinUI/HarmonyWinUI.csproj +++ b/HarmonyWinUI/HarmonyWinUI.csproj @@ -36,6 +36,7 @@ + @@ -89,4 +90,10 @@ + + + MSBuild:Compile + + + diff --git a/HarmonyWinUI/HarmonyWinUI.csproj.user b/HarmonyWinUI/HarmonyWinUI.csproj.user index b80b9f8..73cd313 100644 --- a/HarmonyWinUI/HarmonyWinUI.csproj.user +++ b/HarmonyWinUI/HarmonyWinUI.csproj.user @@ -17,6 +17,9 @@ Designer + + Designer + Designer diff --git a/HarmonyWinUI/MainWindow.xaml b/HarmonyWinUI/MainWindow.xaml index c444bf0..4c194f1 100644 --- a/HarmonyWinUI/MainWindow.xaml +++ b/HarmonyWinUI/MainWindow.xaml @@ -30,8 +30,10 @@ PaneDisplayMode="Left" PaneTitle="Harmony" x:Name="MainNavigation" - SelectionChanged="MainNavigation_SelectionChanged"> - + SelectionChanged="MainNavigation_SelectionChanged" + IsBackButtonVisible="Visible" + BackRequested="MainNavigation_BackRequested"> + @@ -40,7 +42,7 @@ - + @@ -50,7 +52,8 @@ - + @@ -100,7 +103,7 @@ RadiusY="6" VerticalAlignment="Center" HorizontalAlignment="Center" - Fill="{ThemeResource SystemControlBackgroundBaseMediumBrush}"/> + Fill="{ThemeResource SystemControlBackgroundBaseMediumBrush}"/> + Margin="12,0,0,6"/> diff --git a/HarmonyWinUI/MainWindow.xaml.cs b/HarmonyWinUI/MainWindow.xaml.cs index a6accdb..0295883 100644 --- a/HarmonyWinUI/MainWindow.xaml.cs +++ b/HarmonyWinUI/MainWindow.xaml.cs @@ -69,6 +69,19 @@ namespace HarmonyWinUI break; } } + + private void MainNavigation_BackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args) + { + if (MainNavigationContentFrame.CanGoBack) + { + MainNavigationContentFrame.GoBack(); + } + } + + private void MainNavigationContentFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e) + { + MainNavigation.IsBackEnabled = MainNavigationContentFrame.CanGoBack; + } } public class PlayListItem diff --git a/HarmonyWinUI/Models/MusicBrainz.cs b/HarmonyWinUI/Models/MusicBrainz.cs new file mode 100644 index 0000000..6aec890 --- /dev/null +++ b/HarmonyWinUI/Models/MusicBrainz.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace HarmonyWinUI.Models +{ + class MusicBrainz + { + private String URLprefix = "https://musicbrainz.org/ws/2/"; + private static readonly HttpClient httpClient = createHttpClient(); + + public async Task> getArtists(String query) + { + ArgumentException.ThrowIfNullOrWhiteSpace(query); + + string searchQuery = Uri.EscapeDataString($"artist:{query}"); + MusicBrainzArtistResponse? response = await getData( + $"artist?query={searchQuery}&fmt=json&limit=10" + ); + + return response?.Artists ?? new List(); + } + + private async Task getData(String relativeURL) + { + Uri requestURL = new Uri(new Uri(URLprefix), relativeURL); + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestURL); + using HttpResponseMessage response = await httpClient.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead + ); + response.EnsureSuccessStatusCode(); + + await using Stream jsonStream = await response.Content.ReadAsStreamAsync(); + return await JsonSerializer.DeserializeAsync(jsonStream); + } + + private static HttpClient createHttpClient() + { + HttpClient client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + client.DefaultRequestHeaders.UserAgent.ParseAdd( + "HarmonyWinUI/1.0 (https://harmony.tpsoft.org/)" + ); + + return client; + } + + private sealed class MusicBrainzArtistResponse + { + [JsonPropertyName("artists")] + public List? Artists { get; set; } + } + } + + public class MusicBrainzArtist + { + public string? ID { get; set; } + public string? Type { get; set; } + [JsonPropertyName("type-id")] + public string? TypeID { get; set; } + public int? Score { get; set; } + public string? Name { get; set; } = ""; + public string? Gender { get; set; } + public string? Country { get; set; } + public string? ImageURL { get; set; } + + public MusicBrainzArtist() { } + + public MusicBrainzArtist( + string? id, + string? type, + string? typeID, + int? score, + string? name, + string? gender, + string? country) + { + this.ID = id; + this.Type = type; + this.TypeID = typeID; + this.Score = score; + this.Name = name; + this.Gender = gender; + this.Country = country; + } + + } +} diff --git a/HarmonyWinUI/Models/YTDLP.cs b/HarmonyWinUI/Models/YTDLP.cs new file mode 100644 index 0000000..96db544 --- /dev/null +++ b/HarmonyWinUI/Models/YTDLP.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; + +namespace HarmonyWinUI.Models +{ + class YTDLP + { + private String binPath = string.Empty; + + public YTDLP() + { + this.binPath = this.findBinnary(); + System.Diagnostics.Debug.WriteLine("binPath = " + this.binPath); + if (this.binPath.Equals(string.Empty)) + { + throw new Exception("Binnary for YT-DLP not found."); + } + } + + private String findBinnary() + { + DirectoryInfo? currentDirectory = new DirectoryInfo(AppContext.BaseDirectory); + + while (currentDirectory != null) + { + string candidatePath = Path.Combine( + currentDirectory.FullName, + "vendor", + "yt-dlp.exe" + ); + + if (File.Exists(candidatePath)) + { + return candidatePath; + } + + currentDirectory = currentDirectory.Parent; + } + + return string.Empty; + } + } +} diff --git a/HarmonyWinUI/Pages/Dashboard.xaml b/HarmonyWinUI/Pages/Dashboard.xaml index 57dc36e..a0e102f 100644 --- a/HarmonyWinUI/Pages/Dashboard.xaml +++ b/HarmonyWinUI/Pages/Dashboard.xaml @@ -19,14 +19,16 @@ + x:Name="SearchBox" + PlaceholderText="🔎 Search artists, albums and songs..." + HorizontalAlignment="Stretch" + KeyDown="SearchBox_KeyDown"/> + Text="Last artists" + Style="{StaticResource TitleTextBlockStyle}" /> @@ -36,30 +38,30 @@ + Text="Last albums" + Style="{StaticResource TitleTextBlockStyle}" /> - + + Text="Last tracks" + Style="{StaticResource TitleTextBlockStyle}" /> - + + Text="New releases" + Style="{StaticResource TitleTextBlockStyle}" /> diff --git a/HarmonyWinUI/Pages/Dashboard.xaml.cs b/HarmonyWinUI/Pages/Dashboard.xaml.cs index 256b9ff..3e464c6 100644 --- a/HarmonyWinUI/Pages/Dashboard.xaml.cs +++ b/HarmonyWinUI/Pages/Dashboard.xaml.cs @@ -1,17 +1,6 @@ -using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; +using Windows.System; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -27,5 +16,22 @@ namespace HarmonyWinUI.Pages { InitializeComponent(); } + + private void SearchBox_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Enter) + { + string searchText = SearchBox.Text; + Search(searchText); + } + } + + private void Search(string searchText) + { + // Implement your search logic here + // For example, you can navigate to a search results page or filter a list of items + System.Diagnostics.Debug.WriteLine($"Searching for: {searchText}"); + Frame.Navigate(typeof(Search), searchText); + } } } diff --git a/HarmonyWinUI/Pages/Search.xaml b/HarmonyWinUI/Pages/Search.xaml new file mode 100644 index 0000000..15541d1 --- /dev/null +++ b/HarmonyWinUI/Pages/Search.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HarmonyWinUI/Pages/Search.xaml.cs b/HarmonyWinUI/Pages/Search.xaml.cs new file mode 100644 index 0000000..a623537 --- /dev/null +++ b/HarmonyWinUI/Pages/Search.xaml.cs @@ -0,0 +1,114 @@ +using HarmonyWinUI.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace HarmonyWinUI.Pages; + +public sealed class ImageUrlToImageSourceConverter : IValueConverter +{ + public object? Convert(object value, Type targetType, object parameter, string language) + { + if (value is string imageUrl + && Uri.TryCreate(imageUrl, UriKind.Absolute, out Uri? imageUri)) + { + return new BitmapImage(imageUri); + } + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotSupportedException(); + } +} + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class Search : Page +{ + public ObservableCollection Artists { get; } = new(); + private string? pendingSearchText; + + public Search() + { + InitializeComponent(); + Loaded += Search_Loaded; + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + pendingSearchText = e.Parameter as string; + } + + private async void Search_Loaded(object sender, RoutedEventArgs e) + { + if (pendingSearchText is not string searchText) + { + return; + } + + pendingSearchText = null; + await SearchText(searchText); + } + + private async Task SearchText(string searchText) + { + TextBlockQuery.Text = $"Query: '{searchText}'"; + var mb = new MusicBrainz(); + + while (true) + { + try + { + var artists = await mb.getArtists(searchText); + + this.Artists.Clear(); + foreach (var artist in artists) + { + this.Artists.Add(artist); + } + + return; + } + catch (Exception exception) when ( + exception is HttpRequestException + or IOException + or JsonException + or TaskCanceledException + ) + { + ContentDialog errorDialog = new ContentDialog + { + XamlRoot = XamlRoot, + Title = "MusicBrainz connection failed", + Content = $"{exception.GetType().Name}: {exception.Message}", + PrimaryButtonText = "Retry", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + + ContentDialogResult result = await errorDialog.ShowAsync(); + if (result != ContentDialogResult.Primary) + { + this.Artists.Clear(); + return; + } + } + } + } +}