diff --git a/DeepDrftWeb.Client/Controls/AppNavLink.razor b/DeepDrftWeb.Client/Controls/AppNavLink.razor new file mode 100644 index 0000000..f06fff0 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AppNavLink.razor @@ -0,0 +1,12 @@ + + @if (Icon != null) + { + + } + + @if (ChildContent != null) + { + @ChildContent + } + + \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AppNavLink.razor.cs b/DeepDrftWeb.Client/Controls/AppNavLink.razor.cs new file mode 100644 index 0000000..1fc9fa8 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AppNavLink.razor.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; + +namespace DeepDrftWeb.Client.Controls; + +public partial class AppNavLink : ComponentBase +{ + [Parameter] public required string Href { get; set; } + [Parameter] public NavLinkMatch? Match { get; set; } + [Parameter] public string? Icon { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AppNavLink.razor.css b/DeepDrftWeb.Client/Controls/AppNavLink.razor.css new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AppNavLink.razor.css @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor new file mode 100644 index 0000000..6733bd4 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor @@ -0,0 +1,51 @@ + + + + + + + @if (IsLoaded) + { + + } + + + @FormatTime(CurrentTime) / @FormatTime(Duration) + + + + + +
+ + +
+
+ @if (!string.IsNullOrEmpty(ErrorMessage)) + { + + @ErrorMessage + + } +
+
\ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.cs new file mode 100644 index 0000000..6939e42 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.cs @@ -0,0 +1,257 @@ +using Microsoft.AspNetCore.Components; +using DeepDrftWeb.Client.Services; +using MudBlazor; + +namespace DeepDrftWeb.Client.Controls; + +public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable +{ + [Parameter] public string? AudioUrl { get; set; } + [Parameter] public bool ShowLoadProgress { get; set; } = true; + [Parameter] public EventCallback OnProgressChanged { get; set; } + [Parameter] public EventCallback OnPlaybackEnded { get; set; } + + [Inject] public required AudioInteropService AudioInterop { get; set; } + + private string PlayerId = Guid.NewGuid().ToString(); + private bool IsLoaded = false; + private bool IsPlaying = false; + private bool IsPaused = false; + private double CurrentTime = 0; + private double Duration = 0; + private double Volume = 0.8; + private double LoadProgress = 0; + private string? ErrorMessage; + private Timer? progressTimer; + + protected override async Task OnInitializedAsync() + { + var result = await AudioInterop.CreatePlayerAsync(PlayerId); + if (!result.Success) + { + ErrorMessage = $"Failed to initialize audio player: {result.Error}"; + return; + } + + await AudioInterop.SetOnProgressCallbackAsync(PlayerId, OnProgress); + await AudioInterop.SetOnEndCallbackAsync(PlayerId, OnPlaybackEnd); + await AudioInterop.SetOnLoadProgressCallbackAsync(PlayerId, OnLoadProgress); + + await AudioInterop.SetVolumeAsync(PlayerId, Volume); + } + + protected override async Task OnParametersSetAsync() + { + if (IsLoaded) return; + + try + { + AudioLoadResult? loadResult = null; + + if (!string.IsNullOrEmpty(AudioUrl)) + { + loadResult = await AudioInterop.LoadAudioFromUrlAsync(PlayerId, AudioUrl); + } + + if (loadResult?.Success == true) + { + IsLoaded = true; + Duration = loadResult.Duration; + LoadProgress = loadResult.LoadProgress; + ErrorMessage = null; + StateHasChanged(); + } + else + { + ErrorMessage = $"Failed to load audio: {loadResult?.Error ?? "No audio source provided"}"; + } + } + catch (Exception ex) + { + ErrorMessage = $"Error loading audio: {ex.Message}"; + } + } + + private async Task TogglePlayPause() + { + if (!IsLoaded) return; + + try + { + AudioOperationResult result; + + if (IsPlaying) + { + result = await AudioInterop.PauseAsync(PlayerId); + if (result.Success) + { + IsPlaying = false; + IsPaused = true; + } + } + else + { + result = await AudioInterop.PlayAsync(PlayerId); + if (result.Success) + { + IsPlaying = true; + IsPaused = false; + } + } + + if (!result.Success) + { + ErrorMessage = $"Playback error: {result.Error}"; + } + else + { + ErrorMessage = null; + } + } + catch (Exception ex) + { + ErrorMessage = $"Error controlling playback: {ex.Message}"; + } + + StateHasChanged(); + } + + private async Task Stop() + { + if (!IsLoaded) return; + + try + { + var result = await AudioInterop.StopAsync(PlayerId); + if (result.Success) + { + IsPlaying = false; + IsPaused = false; + CurrentTime = 0; + ErrorMessage = null; + } + else + { + ErrorMessage = $"Stop error: {result.Error}"; + } + } + catch (Exception ex) + { + ErrorMessage = $"Error stopping playback: {ex.Message}"; + } + + StateHasChanged(); + } + + private async Task OnSeek(double position) + { + if (!IsLoaded) return; + + try + { + var result = await AudioInterop.SeekAsync(PlayerId, position); + if (result.Success) + { + CurrentTime = position; + ErrorMessage = null; + } + else + { + ErrorMessage = $"Seek error: {result.Error}"; + } + } + catch (Exception ex) + { + ErrorMessage = $"Error seeking: {ex.Message}"; + } + + StateHasChanged(); + } + + private async Task OnVolumeChange(double volume) + { + Volume = volume; + + if (IsLoaded) + { + try + { + var result = await AudioInterop.SetVolumeAsync(PlayerId, volume); + if (!result.Success) + { + ErrorMessage = $"Volume error: {result.Error}"; + StateHasChanged(); + } + } + catch (Exception ex) + { + ErrorMessage = $"Error setting volume: {ex.Message}"; + StateHasChanged(); + } + } + } + + private async Task OnProgress(double currentTime) + { + CurrentTime = currentTime; + if (OnProgressChanged.HasDelegate) + { + await OnProgressChanged.InvokeAsync(currentTime); + } + await InvokeAsync(StateHasChanged); + } + + private async Task OnPlaybackEnd() + { + IsPlaying = false; + IsPaused = false; + CurrentTime = 0; + + if (OnPlaybackEnded.HasDelegate) + { + await OnPlaybackEnded.InvokeAsync(); + } + + await InvokeAsync(StateHasChanged); + } + + private async Task OnLoadProgress(double progress) + { + LoadProgress = progress; + await InvokeAsync(StateHasChanged); + } + + private string GetPlayIcon() + { + return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow; + } + + private string GetVolumeIcon() + { + if (Volume == 0) return Icons.Material.Filled.VolumeOff; + if (Volume < 0.5) return Icons.Material.Filled.VolumeDown; + return Icons.Material.Filled.VolumeUp; + } + + private static string FormatTime(double seconds) + { + var timeSpan = TimeSpan.FromSeconds(seconds); + return timeSpan.ToString(timeSpan.TotalHours >= 1 ? @"h\:mm\:ss" : @"m\:ss"); + } + + private void ClearError() + { + ErrorMessage = null; + StateHasChanged(); + } + + public async ValueTask DisposeAsync() + { + progressTimer?.Dispose(); + + if (IsLoaded) + { + await AudioInterop.DisposePlayerAsync(PlayerId); + } + } +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.css b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.css new file mode 100644 index 0000000..feff813 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.css @@ -0,0 +1,10 @@ +.bottom-bar { + justify-self: center; + max-width: 1800px; + position: fixed; + bottom: 0; + left: var(--mud-drawer-width-left); + right: 0; + margin: 0 1.5rem 1.5rem 1.5rem; + z-index: 1000; +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/TrackPlayer.razor b/DeepDrftWeb.Client/Controls/TrackCard.razor similarity index 78% rename from DeepDrftWeb.Client/Controls/TrackPlayer.razor rename to DeepDrftWeb.Client/Controls/TrackCard.razor index f7b265a..d85255d 100644 --- a/DeepDrftWeb.Client/Controls/TrackPlayer.razor +++ b/DeepDrftWeb.Client/Controls/TrackCard.razor @@ -1,10 +1,10 @@  - @if (!string.IsNullOrEmpty(Track?.ImagePath)) + @if (!string.IsNullOrEmpty(TrackModel?.ImagePath)) {
@@ -25,45 +25,45 @@ Color="Color.Surface" Style="margin-bottom: 4px;" Class="text-truncate"> - @Track?.TrackName + @TrackModel?.TrackName - @Track?.Artist + @TrackModel?.Artist
- @if (!string.IsNullOrEmpty(Track?.Album)) + @if (!string.IsNullOrEmpty(TrackModel?.Album)) { - @Track.Album + @TrackModel.Album } - @if (!string.IsNullOrEmpty(Track?.Genre)) + @if (!string.IsNullOrEmpty(TrackModel?.Genre)) { - @Track.Genre + @TrackModel.Genre }
- @if (Track?.ReleaseDate.HasValue == true) + @if (TrackModel?.ReleaseDate.HasValue == true) { - @Track.ReleaseDate.Value.Year + @TrackModel.ReleaseDate.Value.Year } else @@ -73,9 +73,8 @@ - + StartIcon="@PlayPauseIcon" + OnClick="@PlayClick"/>
diff --git a/DeepDrftWeb.Client/Controls/TrackCard.razor.cs b/DeepDrftWeb.Client/Controls/TrackCard.razor.cs new file mode 100644 index 0000000..1db47fb --- /dev/null +++ b/DeepDrftWeb.Client/Controls/TrackCard.razor.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Components; +using DeepDrftModels.Entities; +using DeepDrftWeb.Client.Clients; +using MudBlazor; + +namespace DeepDrftWeb.Client.Controls; + +public partial class TrackCard : ComponentBase +{ + [Parameter] public required TrackEntity TrackModel { get; set; } + [Parameter] public EventCallback OnPlay { get; set; } + + private bool _isPlaying = false; + private string PlayPauseIcon => _isPlaying ? Icons.Material.Filled.MusicNote : Icons.Material.Filled.PlayArrow; + + private async Task PlayClick() + { + if (!_isPlaying) + { + _isPlaying = true; + await OnPlay.InvokeAsync(); + } + } +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/TrackPlayer.razor.cs b/DeepDrftWeb.Client/Controls/TrackPlayer.razor.cs deleted file mode 100644 index 566034d..0000000 --- a/DeepDrftWeb.Client/Controls/TrackPlayer.razor.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Components; -using DeepDrftModels.Entities; -using DeepDrftWeb.Client.Clients; -using MudBlazor; - -namespace DeepDrftWeb.Client.Controls; - -public partial class TrackPlayer : ComponentBase -{ - [Parameter] public required TrackEntity Track { get; set; } - [Inject] public required TrackMediaClient Client { get; set; } - - private Stream? _audioStream = null; - private bool _isPlaying = false; - private string _playPauseIcon => _isPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow; - private async Task HandlePlayClick() - { - if (_audioStream == null) - { - _audioStream = await Client.GetTrackMedia(Track.EntryKey); - PlayAudio(); - } - } - - private void PlayAudio() - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/TracksGallery.razor b/DeepDrftWeb.Client/Controls/TracksGallery.razor index a6d1ba6..564e158 100644 --- a/DeepDrftWeb.Client/Controls/TracksGallery.razor +++ b/DeepDrftWeb.Client/Controls/TracksGallery.razor @@ -1,10 +1,10 @@ - + @foreach (var track in Tracks) {
- +
} diff --git a/DeepDrftWeb.Client/Controls/TracksGallery.razor.cs b/DeepDrftWeb.Client/Controls/TracksGallery.razor.cs index 4fffc55..73056bd 100644 --- a/DeepDrftWeb.Client/Controls/TracksGallery.razor.cs +++ b/DeepDrftWeb.Client/Controls/TracksGallery.razor.cs @@ -1,9 +1,28 @@ using Microsoft.AspNetCore.Components; using DeepDrftModels.Entities; +using DeepDrftWeb.Client.Clients; namespace DeepDrftWeb.Client.Controls; public partial class TracksGallery : ComponentBase { + private Stream? _audioStream = null; [Parameter] public IEnumerable Tracks { get; set; } = Enumerable.Empty(); + + [Inject] public required TrackMediaClient Client { get; set; } + + private async Task HandlePlayClick(TrackEntity track) + { + if (_audioStream == null) + { + _audioStream = await Client.GetTrackMedia(track.EntryKey); + PlayAudio(); + } + } + + private void PlayAudio() + { + throw new NotImplementedException(); + } + } \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/TracksGallery.razor.css b/DeepDrftWeb.Client/Controls/TracksGallery.razor.css new file mode 100644 index 0000000..9ac440b --- /dev/null +++ b/DeepDrftWeb.Client/Controls/TracksGallery.razor.css @@ -0,0 +1,5 @@ +.tracks-gallery-container { + padding: 16px; + height: 100%; + box-sizing: border-box; +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/DeepDrftWeb.Client.csproj b/DeepDrftWeb.Client/DeepDrftWeb.Client.csproj index af5ab3c..06b458c 100644 --- a/DeepDrftWeb.Client/DeepDrftWeb.Client.csproj +++ b/DeepDrftWeb.Client/DeepDrftWeb.Client.csproj @@ -13,10 +13,17 @@ + + + + true + + + diff --git a/DeepDrftWeb.Client/Layout/MainLayout.razor b/DeepDrftWeb.Client/Layout/MainLayout.razor index f82b129..b908487 100644 --- a/DeepDrftWeb.Client/Layout/MainLayout.razor +++ b/DeepDrftWeb.Client/Layout/MainLayout.razor @@ -1,21 +1,22 @@ @inherits LayoutComponentBase - + - - - Application - - - - - + + + + + + - - + + + + + @Body @@ -30,18 +31,21 @@ @code { private bool _drawerOpen = true; private bool _isDarkMode = true; - private MudTheme? _theme = null; + // private MudTheme? _theme = null; protected override void OnInitialized() { base.OnInitialized(); - - _theme = new() + _themeManager = new ThemeManagerTheme { - PaletteLight = _lightPalette, - PaletteDark = _darkPalette, - LayoutProperties = new LayoutProperties() + Theme = + { + PaletteDark = _darkPalette, + PaletteLight = _lightPalette + } }; + + StateHasChanged(); } private void DrawerToggle() @@ -53,6 +57,20 @@ { _isDarkMode = !_isDarkMode; } + + private ThemeManagerTheme _themeManager; + public bool _themeManagerOpen = false; + + void OpenThemeManager(bool value) + { + _themeManagerOpen = value; + } + + void UpdateTheme(ThemeManagerTheme value) + { + _themeManager = value; + StateHasChanged(); + } private readonly PaletteLight _lightPalette = new() { diff --git a/DeepDrftWeb.Client/Layout/NavMenu.razor b/DeepDrftWeb.Client/Layout/NavMenu.razor index 30a1ecb..9c06af9 100644 --- a/DeepDrftWeb.Client/Layout/NavMenu.razor +++ b/DeepDrftWeb.Client/Layout/NavMenu.razor @@ -1,8 +1,8 @@ - - - Home - Track Gallery - Audio Test - +@using DeepDrftWeb.Client.Controls + +Home +Track Gallery +Audio Test + diff --git a/DeepDrftWeb.Client/Pages/TracksView.razor b/DeepDrftWeb.Client/Pages/TracksView.razor index 16a86c2..ec6cfd4 100644 --- a/DeepDrftWeb.Client/Pages/TracksView.razor +++ b/DeepDrftWeb.Client/Pages/TracksView.razor @@ -1,46 +1,37 @@ @page "/tracks" -@using DeepDrftModels.DTOs -@using DeepDrftModels.Entities + @using DeepDrftWeb.Client.Controls -

Track Gallery

- -@if (ViewModel.Page != null) -{ - @* *@ - @* *@ - @* Name *@ - @* Artist *@ - @* Album *@ - @* Genre *@ - @* Released *@ - @* Actions *@ - @* *@ - @* *@ - @* @context.TrackName *@ - @* @context.Artist *@ - @* @context.Album *@ - @* @context.Genre *@ - @* @context.ReleaseDate *@ - @* *@ - @* View *@ - @* *@ - @* *@ - @* *@ - - - - - - -} -else -{ - - - -} +DeepDrft Track Gallery + +
+
+ @if (ViewModel.Page != null) + { +
+ +
+ +
+ + + +
+ } + else + { +
+ +
+
+ +
+ } +
+ + +
\ No newline at end of file diff --git a/DeepDrftWeb.Client/Pages/TracksView.razor.css b/DeepDrftWeb.Client/Pages/TracksView.razor.css new file mode 100644 index 0000000..f816847 --- /dev/null +++ b/DeepDrftWeb.Client/Pages/TracksView.razor.css @@ -0,0 +1,28 @@ +.tracks-page-wrapper { + display: flex; + flex-direction: column; + height: calc(100dvh - 64px); /* Subtract app bar height (pt-16 = 4rem = 64px) */ + margin: -16px; /* Counteract MudMainContent padding */ + padding-top: 16px; /* Restore top padding for spacing */ +} + +.tracks-view-container { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; + padding: 0 16px; /* Horizontal padding only */ +} + +.tracks-content { + flex: 1 1 auto; + overflow-y: scroll; + min-height: 0; + padding-top: 16px; +} + +.tracks-pagination { + flex: 0 0 auto; + padding: 8px 0; +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/_Imports.razor b/DeepDrftWeb.Client/_Imports.razor index 3aa70bc..de75392 100644 --- a/DeepDrftWeb.Client/_Imports.razor +++ b/DeepDrftWeb.Client/_Imports.razor @@ -8,3 +8,4 @@ @using Microsoft.JSInterop @using MudBlazor @using MudBlazor.Services +@using MudBlazor.ThemeManager diff --git a/DeepDrftWeb/Components/App.razor b/DeepDrftWeb/Components/App.razor index 3affce3..b1043d3 100644 --- a/DeepDrftWeb/Components/App.razor +++ b/DeepDrftWeb/Components/App.razor @@ -4,11 +4,15 @@ - + + + + + - + diff --git a/DeepDrftWeb/wwwroot/deepdrft-logo.ico b/DeepDrftWeb/wwwroot/deepdrft-logo.ico new file mode 100644 index 0000000..52b5d0a Binary files /dev/null and b/DeepDrftWeb/wwwroot/deepdrft-logo.ico differ