From 73d4b0a9c5f18a55a9b26f740eb0b51fa05a5691 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 8 Sep 2025 18:42:07 -0400 Subject: [PATCH] Front End Audio Player Always Available --- DeepDrftWeb.Client/Controls/AppNavLink.razor | 22 +- .../Controls/AppNavLink.razor.css | 72 ++++++- .../Controls/AudioPlayerBar.razor | 138 +++++++----- .../Controls/AudioPlayerBar.razor.cs | 7 + .../Controls/AudioPlayerBar.razor.css | 196 ++++++++++++++++++ .../Controls/AudioPlayerService.razor | 4 +- .../Controls/AudioPlayerService.razor.cs | 8 - DeepDrftWeb.Client/Layout/MainLayout.razor | 10 +- .../20250904233927_Initial.Designer.cs | 0 .../Migrations/20250904233927_Initial.cs | 0 .../DeepDrftContextModelSnapshot.cs | 0 DeepDrftWeb/DeepDrftWeb.csproj | 6 - DeepDrftWeb/Interop/webaudio.ts | 36 ++-- .../wwwroot/styles/deepdrft-styles.css | 39 ++-- dch5-publish-deploy.sh | 7 +- 15 files changed, 419 insertions(+), 126 deletions(-) create mode 100644 DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.css rename {DeepDrftWeb => DeepDrftWeb.Services}/Migrations/20250904233927_Initial.Designer.cs (100%) rename {DeepDrftWeb => DeepDrftWeb.Services}/Migrations/20250904233927_Initial.cs (100%) rename {DeepDrftWeb => DeepDrftWeb.Services}/Migrations/DeepDrftContextModelSnapshot.cs (100%) diff --git a/DeepDrftWeb.Client/Controls/AppNavLink.razor b/DeepDrftWeb.Client/Controls/AppNavLink.razor index f06fff0..9d817a2 100644 --- a/DeepDrftWeb.Client/Controls/AppNavLink.razor +++ b/DeepDrftWeb.Client/Controls/AppNavLink.razor @@ -1,12 +1,14 @@ - - @if (Icon != null) - { - - } - - @if (ChildContent != null) + + + \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AppNavLink.razor.css b/DeepDrftWeb.Client/Controls/AppNavLink.razor.css index 5f28270..8a4ba6e 100644 --- a/DeepDrftWeb.Client/Controls/AppNavLink.razor.css +++ b/DeepDrftWeb.Client/Controls/AppNavLink.razor.css @@ -1 +1,71 @@ - \ No newline at end of file +/* Navigation menu item styling */ +.nav-menu-item { + display: block; + padding: 8px 16px; + margin: 4px 8px; + border-radius: 8px; + text-decoration: none; + color: inherit; + transition: all 0.3s ease; + position: relative; +} + +.nav-item-content { + display: flex; + align-items: center; + gap: 12px; + margin: 0 12px; +} + +.nav-item-icon { + width: 20px; + height: 20px; + transition: all 0.3s ease; +} + +.nav-item-text { + font-weight: 500; + font-size: 14px; + transition: all 0.3s ease; +} + +/* Hover state */ +.nav-menu-item:hover { + background-color: rgba(138, 43, 226, 0.08); + color: #8A2BE2; + text-decoration: none; +} + +.nav-menu-item:hover .nav-item-content { + color: var(--mud-palette-tertiary); +} + +.nav-menu-item:hover .nav-item-text { + font-weight: 600; +} + +/* Active state */ +.nav-menu-item.active { + background-color: rgba(138, 43, 226, 0.12); + color: var(--mud-palette-primary); + border-left: 4px solid #8A2BE2; + padding-left: 12px; +} + +.nav-menu-item.active .nav-item-content { + color: var(--mud-palette-primary); +} + +.nav-menu-item.active .nav-item-icon { + color: var(--mud-palette-primary); +} + +.nav-menu-item.active .nav-item-text { + font-weight: 600; +} + +/* Focus state */ +.nav-menu-item:focus { + outline: 2px solid #8A2BE2; + outline-offset: 2px; +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor index 782bf8f..a746318 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor @@ -1,55 +1,87 @@ - - - - - - @if (IsLoaded) - { - - } - - - - @FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--") - - @if (!IsLoaded) - { - - } - - +@if (_isMinimized) +{ +
+ +
+} +else +{ + @* Full-width outer container *@ +
+ +
+
+ + @* Controls section *@ +
+
+ + @if (IsLoaded) + { + + } +
+ +
+ + @FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--") + + @if (!IsLoaded) + { + + } +
+
- + @* Seek slider *@ + -
- - -
- - @if (!string.IsNullOrEmpty(ErrorMessage)) - { - - @ErrorMessage - - } - + @* Volume section *@ +
+ + +
+
+ +
+
+ + @if (!string.IsNullOrEmpty(ErrorMessage)) + { + + @ErrorMessage + + } +
+
+
+ + @* Spacer div to maintain layout spacing *@ +
+} diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.cs index 35b137e..ff1688c 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.cs +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.cs @@ -10,6 +10,7 @@ public partial class AudioPlayerBar : ComponentBase { [CascadingParameter] public required IPlayerService PlayerService { get; set; } [Parameter] public bool ShowLoadProgress { get; set; } = true; + private bool _isMinimized = true; private bool IsLoaded => PlayerService.IsLoaded; private bool IsPlaying => PlayerService.IsPlaying; @@ -70,4 +71,10 @@ public partial class AudioPlayerBar : ComponentBase PlayerService.ClearError(); } + private void ToggleMinimized() + { + _isMinimized = !_isMinimized; + StateHasChanged(); + } + } \ 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..36bf92e --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar.razor.css @@ -0,0 +1,196 @@ +/* AudioPlayerBar Component - Scoped Styles */ + +/* Outer container - full width, fixed to bottom */ +.deepdrft-player-outer-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + width: 100%; + z-index: 1200; + padding: 0; + margin: 0; +} + +/* Inner container wrapper */ +.deepdrft-player-inner-container { + padding: 1rem; + padding-bottom: 1.5rem; + margin: 0 auto; +} + +/* Player bar with rounded corners and semi-opaque background */ +.deepdrft-audio-player-bar { + position: relative; + background: rgba(var(--mud-palette-surface-rgb), 0.75); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-radius: 1rem; + border: 1px solid rgba(var(--mud-palette-divider-rgb), 0.9); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; + color: var(--mud-palette-text-primary); + overflow: hidden; + margin-bottom: 1rem; +} + +/* Spacer div to maintain layout spacing */ +.deepdrft-player-spacer { + height: 140px; + width: 100%; + flex-shrink: 0; +} + +/* Minimized floating dock */ +.deepdrft-minimized-player-dock { + position: fixed; + bottom: 60px; + right: 60px; + z-index: 1300; + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, + rgba(var(--mud-palette-primary-rgb), 0.95) 0%, + rgba(var(--mud-palette-secondary-rgb), 0.9) 50%, + rgba(var(--mud-palette-tertiary-rgb), 0.95) 100% + ); + backdrop-filter: blur(15px); + border: 2px solid rgba(var(--mud-palette-secondary-rgb), 0.7); + box-shadow: 0 4px 20px rgba(var(--mud-palette-primary-rgb), 0.6), + 0 2px 10px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; + cursor: pointer; +} + +.deepdrft-minimized-player-dock:hover { + transform: scale(1.1); + box-shadow: 0 6px 25px rgba(var(--mud-palette-primary-rgb), 0.8), + 0 3px 15px rgba(0, 0, 0, 0.4); +} + +.deepdrft-minimized-player-button { + border-radius: 50%; + background: transparent !important; + color: white; + transition: all 0.3s ease; + box-shadow: none !important; + border: none !important; + width: 48px !important; + height: 48px !important; +} + +/* Layout containers */ +.deepdrft-audio-player-content { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1.25rem; +} + +.deepdrft-audio-controls-section { + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 200px; +} + +.deepdrft-audio-buttons-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.deepdrft-audio-info-row { + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: 0.25rem; +} + +.deepdrft-audio-volume-section { + display: flex; + align-items: center; + gap: 0.25rem; + width: 140px; +} + +.deepdrft-audio-minimize-section { + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +/* Control elements */ +.deepdrft-audio-seek-slider { + flex: 1; + margin-right: 0.5rem; +} + +.deepdrft-audio-volume-slider { + width: 100px; +} + +.deepdrft-audio-time { + min-width: 120px; +} + +.deepdrft-audio-volume-icon { + margin-right: 4px; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .deepdrft-minimized-player-dock { + bottom: 15px; + right: 15px; + width: 56px; + height: 56px; + } + + .deepdrft-minimized-player-button { + width: 44px !important; + height: 44px !important; + } + + .deepdrft-player-inner-container { + padding: 0.75rem; + padding-bottom: 1.25rem; + } + + .deepdrft-audio-player-bar { + border-radius: 1rem; + margin-bottom: 1.25rem; + } + + .deepdrft-audio-player-content { + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem 1rem; + } + + .deepdrft-audio-controls-section { + align-items: center; + width: 100%; + } + + .deepdrft-audio-seek-slider { + margin-right: 0; + width: 100%; + } + + .deepdrft-audio-volume-section { + width: 100%; + justify-content: center; + } + + .deepdrft-player-spacer { + height: 160px; + } +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor b/DeepDrftWeb.Client/Controls/AudioPlayerService.razor index 86ceffa..6292516 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor +++ b/DeepDrftWeb.Client/Controls/AudioPlayerService.razor @@ -1,5 +1,3 @@ -@using DeepDrftWeb.Client.Services - - + @ChildContent \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerService.razor.cs index 69d4b2c..66c7bdf 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor.cs +++ b/DeepDrftWeb.Client/Controls/AudioPlayerService.razor.cs @@ -13,14 +13,6 @@ public partial class AudioPlayerService : ComponentBase [Parameter] public RenderFragment? ChildContent { get; set; } - protected override void OnInitialized() - { - base.OnInitialized(); - - // PlayerService is already created as a field, so it's immediately available to cascading components - // It will be in uninitialized state until OnAfterRenderAsync when AudioPlaybackEngine is ready - } - protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) diff --git a/DeepDrftWeb.Client/Layout/MainLayout.razor b/DeepDrftWeb.Client/Layout/MainLayout.razor index dbe5167..ea6638e 100644 --- a/DeepDrftWeb.Client/Layout/MainLayout.razor +++ b/DeepDrftWeb.Client/Layout/MainLayout.razor @@ -8,7 +8,7 @@ @* *@ - + @* *@ @@ -16,10 +16,12 @@ - + @* *@ - - @Body + + + @Body + diff --git a/DeepDrftWeb/Migrations/20250904233927_Initial.Designer.cs b/DeepDrftWeb.Services/Migrations/20250904233927_Initial.Designer.cs similarity index 100% rename from DeepDrftWeb/Migrations/20250904233927_Initial.Designer.cs rename to DeepDrftWeb.Services/Migrations/20250904233927_Initial.Designer.cs diff --git a/DeepDrftWeb/Migrations/20250904233927_Initial.cs b/DeepDrftWeb.Services/Migrations/20250904233927_Initial.cs similarity index 100% rename from DeepDrftWeb/Migrations/20250904233927_Initial.cs rename to DeepDrftWeb.Services/Migrations/20250904233927_Initial.cs diff --git a/DeepDrftWeb/Migrations/DeepDrftContextModelSnapshot.cs b/DeepDrftWeb.Services/Migrations/DeepDrftContextModelSnapshot.cs similarity index 100% rename from DeepDrftWeb/Migrations/DeepDrftContextModelSnapshot.cs rename to DeepDrftWeb.Services/Migrations/DeepDrftContextModelSnapshot.cs diff --git a/DeepDrftWeb/DeepDrftWeb.csproj b/DeepDrftWeb/DeepDrftWeb.csproj index 1f2023f..26dd075 100644 --- a/DeepDrftWeb/DeepDrftWeb.csproj +++ b/DeepDrftWeb/DeepDrftWeb.csproj @@ -33,12 +33,6 @@ - - - PreserveNewest - - - false diff --git a/DeepDrftWeb/Interop/webaudio.ts b/DeepDrftWeb/Interop/webaudio.ts index 738e2fa..dd7446b 100644 --- a/DeepDrftWeb/Interop/webaudio.ts +++ b/DeepDrftWeb/Interop/webaudio.ts @@ -72,12 +72,12 @@ class AudioPlayer { try { this.bufferChunks.push(audioBlock); this.currentSize += audioBlock.length; - + if (this.expectedSize > 0 && this.onLoadProgressCallback) { const progress = (this.currentSize / this.expectedSize) * 100; this.onLoadProgressCallback(Math.min(progress, 100)); } - + return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; @@ -89,24 +89,24 @@ class AudioPlayer { const arrayBuffer = new ArrayBuffer(this.currentSize); const view = new Uint8Array(arrayBuffer); let offset = 0; - + for (const chunk of this.bufferChunks) { view.set(chunk, offset); offset += chunk.length; } - + this.audioBuffer = await this.audioContext!.decodeAudioData(arrayBuffer); this.duration = this.audioBuffer.duration; - + this.bufferChunks = []; this.currentSize = 0; - + if (this.onLoadProgressCallback) { this.onLoadProgressCallback(100); } - - return { - success: true, + + return { + success: true, duration: this.duration, sampleRate: this.audioBuffer.sampleRate, numberOfChannels: this.audioBuffer.numberOfChannels, @@ -202,18 +202,18 @@ class AudioPlayer { try { const wasPlaying = this.isPlaying; - + if (this.isPlaying) { this.source!.stop(); } this.pauseOffset = position; - + if (wasPlaying) { this.source = this.audioContext!.createBufferSource(); this.source.buffer = this.audioBuffer; this.source.connect(this.gainNode!); - + this.source.onended = () => { this.isPlaying = false; this.isPaused = false; @@ -424,11 +424,11 @@ const DeepDrftAudio = { if (!player) { return { success: false, error: "Player not found" }; } - + player.setOnProgressCallback((currentTime: number) => { dotNetObjectReference.invokeMethodAsync(methodName, currentTime); }); - + return { success: true }; }, @@ -437,11 +437,11 @@ const DeepDrftAudio = { if (!player) { return { success: false, error: "Player not found" }; } - + player.setOnEndCallback(() => { dotNetObjectReference.invokeMethodAsync(methodName); }); - + return { success: true }; }, @@ -450,11 +450,11 @@ const DeepDrftAudio = { if (!player) { return { success: false, error: "Player not found" }; } - + player.setOnLoadProgressCallback((progress: number) => { dotNetObjectReference.invokeMethodAsync(methodName, progress); }); - + return { success: true }; }, diff --git a/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css b/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css index 81eee79..7faea0a 100644 --- a/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css +++ b/DeepDrftWeb/wwwroot/styles/deepdrft-styles.css @@ -293,29 +293,28 @@ body, p, span, div, justify-content: center; } -/* Audio Player Layout */ -.deepdrft-audio-controls { - display: flex; - align-items: center; - width: 140px; -} -.deepdrft-audio-time { - min-width: 120px; -} +/* Layout with overlay audio player - Global layout class */ +/*.deepdrft-layout-with-overlay-player {*/ +/* position: relative;*/ +/* min-height: calc(100vh - 64px);*/ +/* padding-bottom: 160px; !* Increased space for overlay player *!*/ +/*}*/ -.deepdrft-audio-volume-icon { - margin-right: 4px; -} +/*!* Audio player overlay positioning - Global positioning *!*/ +/*.deepdrft-layout-with-overlay-player > .AudioPlayerBar,*/ +/*.deepdrft-layout-with-overlay-player > *:last-child {*/ +/* position: fixed;*/ +/* bottom: 0;*/ +/* left: 0;*/ +/* right: 0;*/ +/* z-index: 1000;*/ +/* pointer-events: none;*/ +/*}*/ -.deepdrft-audio-slider { - flex: 1; -} - -.deepdrft-audio-slider-seek { - flex: 1; - margin-right: 8px; -} +/*.deepdrft-layout-with-overlay-player > *:last-child > * {*/ +/* pointer-events: auto;*/ +/*}*/ /* Responsive Utilities */ @media (max-width: 768px) { diff --git a/dch5-publish-deploy.sh b/dch5-publish-deploy.sh index ad54f6f..aaa969c 100644 --- a/dch5-publish-deploy.sh +++ b/dch5-publish-deploy.sh @@ -4,6 +4,7 @@ ssh-add /c/.ssh/deepdrft_ed25519 CONTENT_PROJ="DeepDrftContent" WEB_PROJ="DeepDrftWeb" +WEB_SERVICES_PROJ="DeepDrftWeb.Services" CONTENT_APP="deepdrft-content.tar.gz" WEB_APP="deepdrft-web.tar.gz" @@ -16,15 +17,15 @@ WEB_MIG="deepdrft-migrations.sql" REMOTE="deepdrft@dch5.snailbird.net" WEB_APPROOT="/deepdrft/web" -LATEST_MIGRATION=$(dotnet ef migrations list --project $WEB_PROJ --context DeepDrftContext --no-build | tail -1) +LATEST_MIGRATION=$(dotnet ef migrations list --project $WEB_SERVICES_PROJ --context DeepDrftContext --no-build | tail -1) REMOTE_MIGRATION=$(ssh $REMOTE "sqlite3 $WEB_APPROOT/Database/deepdrft.db 'SELECT MigrationId FROM __EFMigrationsHistory ORDER BY MigrationId DESC LIMIT 1;'" 2>/dev/null || echo "") if [ "$LATEST_MIGRATION" != "$REMOTE_MIGRATION" ]; then echo "Generating migration script from $REMOTE_MIGRATION to $LATEST_MIGRATION..." if [ -z "$REMOTE_MIGRATION" ]; then - dotnet ef migrations script --project $WEB_PROJ --context DeepDrftContext --output $WEB_MIG --verbose --no-build + dotnet ef migrations script --project $WEB_SERVICES_PROJ --context DeepDrftContext --output $WEB_MIG --verbose --no-build else - dotnet ef migrations script $REMOTE_MIGRATION --project $WEB_PROJ --context DeepDrftContext --output $WEB_MIG --verbose --no-build + dotnet ef migrations script $REMOTE_MIGRATION --project $WEB_SERVICES_PROJ --context DeepDrftContext --output $WEB_MIG --verbose --no-build fi APPLY_MIGRATIONS=true else