Front End Audio Player Always Available

This commit is contained in:
daniel-c-harvey
2025-09-08 18:42:07 -04:00
parent a25d067dff
commit 73d4b0a9c5
15 changed files with 419 additions and 126 deletions
+7 -5
View File
@@ -1,12 +1,14 @@
<MudStack Row AlignItems="AlignItems.Center" Spacing="3" Class="mx-3"> <NavLink href="@Href" Match="@(Match ?? NavLinkMatch.Prefix)" class="nav-menu-item">
<div class="nav-item-content">
@if (Icon != null) @if (Icon != null)
{ {
<MudIcon Icon="@Icon" /> <MudIcon Icon="@Icon" class="nav-item-icon" />
} }
<NavLink href="@Href" Match="@(Match ?? NavLinkMatch.Prefix)"> <span class="nav-item-text">
@if (ChildContent != null) @if (ChildContent != null)
{ {
@ChildContent @ChildContent
} }
</NavLink> </span>
</MudStack> </div>
</NavLink>
@@ -1 +1,71 @@
 /* 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;
}
@@ -1,7 +1,24 @@
<MudPaper MaxWidth="1600px" Square="true"> @if (_isMinimized)
<MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="px-4 py-2"> {
<MudStack Class="pb-2"> <div class="deepdrft-minimized-player-dock">
<MudStack Row AlignItems="AlignItems.Center"> <MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary"
Size="Size.Large"
Class="deepdrft-minimized-player-button"
OnClick="@ToggleMinimized" />
</div>
}
else
{
@* Full-width outer container *@
<div class="deepdrft-player-outer-container">
<MudContainer MaxWidth="MaxWidth.Large" Class="deepdrft-player-inner-container">
<div class="deepdrft-audio-player-bar">
<div class="deepdrft-audio-player-content">
@* Controls section *@
<div class="deepdrft-audio-controls-section">
<div class="deepdrft-audio-buttons-row">
<MudIconButton Icon="@GetPlayIcon()" <MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary" Color="Color.Primary"
Size="Size.Large" Size="Size.Large"
@@ -9,13 +26,13 @@
Disabled="!IsLoaded"/> Disabled="!IsLoaded"/>
@if (IsLoaded) @if (IsLoaded)
{ {
<MudIconButton Icon="Icons.Material.Filled.Stop" <MudIconButton Icon="@Icons.Material.Filled.Stop"
Color="Color.Primary" Color="Color.Primary"
OnClick="@Stop" OnClick="@Stop"/>
Disabled="!IsLoaded"/>
} }
</MudStack> </div>
<MudStack Row AlignItems="AlignItems.Center">
<div class="deepdrft-audio-info-row">
<MudText Typo="Typo.body2" Class="font-monospace deepdrft-audio-time"> <MudText Typo="Typo.body2" Class="font-monospace deepdrft-audio-time">
@FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--") @FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--")
</MudText> </MudText>
@@ -23,9 +40,10 @@
{ {
<MudProgressCircular Color="Color.Tertiary" Value="@LoadProgress" Size="Size.Small"/> <MudProgressCircular Color="Color.Tertiary" Value="@LoadProgress" Size="Size.Small"/>
} }
</MudStack> </div>
</MudStack> </div>
@* Seek slider *@
<MudSlider T="double" <MudSlider T="double"
Min="0" Min="0"
Max="@(Duration ?? 0D)" Max="@(Duration ?? 0D)"
@@ -33,9 +51,10 @@
Value="@CurrentTime" Value="@CurrentTime"
ValueChanged="@OnSeek" ValueChanged="@OnSeek"
Disabled="!IsLoaded" Disabled="!IsLoaded"
Class="deepdrft-audio-slider-seek"/> Class="deepdrft-audio-seek-slider"/>
<div class="deepdrft-audio-controls"> @* Volume section *@
<div class="deepdrft-audio-volume-section">
<MudIcon Icon="@GetVolumeIcon()" Class="deepdrft-audio-volume-icon"/> <MudIcon Icon="@GetVolumeIcon()" Class="deepdrft-audio-volume-icon"/>
<MudSlider T="double" <MudSlider T="double"
Min="0" Min="0"
@@ -43,13 +62,26 @@
Step="0.01" Step="0.01"
Value="@Volume" Value="@Volume"
ValueChanged="@OnVolumeChange" ValueChanged="@OnVolumeChange"
Class="deepdrft-audio-slider"/> Class="deepdrft-audio-volume-slider"/>
</div> </div>
</MudStack> <div class="deepdrft-audio-minimize-section">
<MudIconButton Icon="@Icons.Material.Filled.Minimize"
Color="Color.Secondary"
Size="Size.Small"
OnClick="@ToggleMinimized"/>
</div>
</div>
@if (!string.IsNullOrEmpty(ErrorMessage)) @if (!string.IsNullOrEmpty(ErrorMessage))
{ {
<MudAlert Severity="Severity.Error" ShowCloseIcon="true" CloseIconClicked="ClearError"> <MudAlert Severity="Severity.Error" ShowCloseIcon="true" CloseIconClicked="ClearError" Class="ma-2">
@ErrorMessage @ErrorMessage
</MudAlert> </MudAlert>
} }
</MudPaper> </div>
</MudContainer>
</div>
@* Spacer div to maintain layout spacing *@
<div class="deepdrft-player-spacer"></div>
}
@@ -10,6 +10,7 @@ public partial class AudioPlayerBar : ComponentBase
{ {
[CascadingParameter] public required IPlayerService PlayerService { get; set; } [CascadingParameter] public required IPlayerService PlayerService { get; set; }
[Parameter] public bool ShowLoadProgress { get; set; } = true; [Parameter] public bool ShowLoadProgress { get; set; } = true;
private bool _isMinimized = true;
private bool IsLoaded => PlayerService.IsLoaded; private bool IsLoaded => PlayerService.IsLoaded;
private bool IsPlaying => PlayerService.IsPlaying; private bool IsPlaying => PlayerService.IsPlaying;
@@ -70,4 +71,10 @@ public partial class AudioPlayerBar : ComponentBase
PlayerService.ClearError(); PlayerService.ClearError();
} }
private void ToggleMinimized()
{
_isMinimized = !_isMinimized;
StateHasChanged();
}
} }
@@ -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;
}
}
@@ -1,5 +1,3 @@
@using DeepDrftWeb.Client.Services <CascadingValue Value="@(PlayerService)" IsFixed="true">
<CascadingValue Value="@(PlayerService)" IsFixed="true">
@ChildContent @ChildContent
</CascadingValue> </CascadingValue>
@@ -13,14 +13,6 @@ public partial class AudioPlayerService : ComponentBase
[Parameter] public RenderFragment? ChildContent { get; set; } [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) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
+5 -3
View File
@@ -8,7 +8,7 @@
<MudLayout> <MudLayout>
<AudioPlayerService> <AudioPlayerService>
@* <MudThemeManagerButton OnClick="@((e) => OpenThemeManager(true))" /> *@ @* <MudThemeManagerButton OnClick="@((e) => OpenThemeManager(true))" /> *@
<MudThemeManager Open="_themeManagerOpen" OpenChanged="OpenThemeManager" Theme="_themeManager" ThemeChanged="UpdateTheme" /> @* <MudThemeManager Open="_themeManagerOpen" OpenChanged="OpenThemeManager" Theme="_themeManager" ThemeChanged="UpdateTheme" /> *@
<MudAppBar Elevation="_themeManager.AppBarElevation"> <MudAppBar Elevation="_themeManager.AppBarElevation">
<MudAvatar Class="mr-2"> <MudAvatar Class="mr-2">
<MudImage Src="img/deepdrft-logo.jpg"></MudImage> <MudImage Src="img/deepdrft-logo.jpg"></MudImage>
@@ -16,10 +16,12 @@
<NavMenu /> <NavMenu />
<MudSpacer/> <MudSpacer/>
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle"/> <MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle"/>
<MudIconButton Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Edge="Edge.End"/> @* <MudIconButton Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Edge="Edge.End"/> *@
</MudAppBar> </MudAppBar>
<MudMainContent Class="pt-16 pa-4" Style="min-height: 100vh"> <MudMainContent Class="pt-16 deepdrft-layout-with-overlay-player">
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
@Body @Body
</MudContainer>
<AudioPlayerBar /> <AudioPlayerBar />
</MudMainContent> </MudMainContent>
</AudioPlayerService> </AudioPlayerService>
-6
View File
@@ -33,12 +33,6 @@
<Folder Include="wwwroot\js\" /> <Folder Include="wwwroot\js\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="Interop\webaudio.ts">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</TypeScriptCompile>
</ItemGroup>
<!-- Prevent TypeScript compilation issues during publish --> <!-- Prevent TypeScript compilation issues during publish -->
<PropertyGroup> <PropertyGroup>
<TypeScriptCompileOnSaveEnabled>false</TypeScriptCompileOnSaveEnabled> <TypeScriptCompileOnSaveEnabled>false</TypeScriptCompileOnSaveEnabled>
+19 -20
View File
@@ -293,29 +293,28 @@ body, p, span, div,
justify-content: center; justify-content: center;
} }
/* Audio Player Layout */
.deepdrft-audio-controls {
display: flex;
align-items: center;
width: 140px;
}
.deepdrft-audio-time { /* Layout with overlay audio player - Global layout class */
min-width: 120px; /*.deepdrft-layout-with-overlay-player {*/
} /* position: relative;*/
/* min-height: calc(100vh - 64px);*/
/* padding-bottom: 160px; !* Increased space for overlay player *!*/
/*}*/
.deepdrft-audio-volume-icon { /*!* Audio player overlay positioning - Global positioning *!*/
margin-right: 4px; /*.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 { /*.deepdrft-layout-with-overlay-player > *:last-child > * {*/
flex: 1; /* pointer-events: auto;*/
} /*}*/
.deepdrft-audio-slider-seek {
flex: 1;
margin-right: 8px;
}
/* Responsive Utilities */ /* Responsive Utilities */
@media (max-width: 768px) { @media (max-width: 768px) {
+4 -3
View File
@@ -4,6 +4,7 @@ ssh-add /c/.ssh/deepdrft_ed25519
CONTENT_PROJ="DeepDrftContent" CONTENT_PROJ="DeepDrftContent"
WEB_PROJ="DeepDrftWeb" WEB_PROJ="DeepDrftWeb"
WEB_SERVICES_PROJ="DeepDrftWeb.Services"
CONTENT_APP="deepdrft-content.tar.gz" CONTENT_APP="deepdrft-content.tar.gz"
WEB_APP="deepdrft-web.tar.gz" WEB_APP="deepdrft-web.tar.gz"
@@ -16,15 +17,15 @@ WEB_MIG="deepdrft-migrations.sql"
REMOTE="deepdrft@dch5.snailbird.net" REMOTE="deepdrft@dch5.snailbird.net"
WEB_APPROOT="/deepdrft/web" 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 "") 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 if [ "$LATEST_MIGRATION" != "$REMOTE_MIGRATION" ]; then
echo "Generating migration script from $REMOTE_MIGRATION to $LATEST_MIGRATION..." echo "Generating migration script from $REMOTE_MIGRATION to $LATEST_MIGRATION..."
if [ -z "$REMOTE_MIGRATION" ]; then 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 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 fi
APPLY_MIGRATIONS=true APPLY_MIGRATIONS=true
else else