diff --git a/DeepDrftWeb.Client/Layout/MainLayout.razor b/DeepDrftWeb.Client/Layout/MainLayout.razor index 29df2ae..56589fb 100644 --- a/DeepDrftWeb.Client/Layout/MainLayout.razor +++ b/DeepDrftWeb.Client/Layout/MainLayout.razor @@ -20,6 +20,17 @@ + diff --git a/DeepDrftWeb.Client/Layout/MainLayout.razor.css b/DeepDrftWeb.Client/Layout/MainLayout.razor.css index 60cec92..286b3ef 100644 --- a/DeepDrftWeb.Client/Layout/MainLayout.razor.css +++ b/DeepDrftWeb.Client/Layout/MainLayout.razor.css @@ -18,3 +18,53 @@ right: 0.75rem; top: 0.5rem; } + +/* ── SITE FOOTER ── */ +.deepdrft-footer { + background: var(--deepdrft-white); + border-top: 1px solid var(--deepdrft-border); + padding: 3rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.deepdrft-footer-logo { + font-family: var(--deepdrft-font-display); + font-size: 1.5rem; + font-weight: 400; + color: var(--deepdrft-navy); +} + +.deepdrft-footer-logo span { + font-style: italic; + color: var(--deepdrft-green); +} + +.deepdrft-footer-links { + display: flex; + gap: 2rem; + list-style: none; + margin: 0; + padding: 0; +} + +.deepdrft-footer-links a { + font-family: var(--deepdrft-font-mono); + font-size: 0.62rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--deepdrft-muted); + text-decoration: none; + transition: color 0.2s; +} + +.deepdrft-footer-links a:hover { color: var(--deepdrft-navy); } + +.deepdrft-footer-copy { + font-family: var(--deepdrft-font-mono); + font-size: 0.58rem; + letter-spacing: 0.12em; + color: var(--deepdrft-muted); +} diff --git a/DeepDrftWeb.Client/Pages/Home.razor b/DeepDrftWeb.Client/Pages/Home.razor index ebbbe92..d24385b 100644 --- a/DeepDrftWeb.Client/Pages/Home.razor +++ b/DeepDrftWeb.Client/Pages/Home.razor @@ -1,226 +1,230 @@ -@page "/" +@page "/" +@using DeepDrftWeb.Client.Services -Deep DRFT- Electronic Music Collective +Deep DRFT - Electronic Music Collective - - - @* Hero Section *@ - +@* Hero - split 50/50 *@ +
+
+
Charleston, South Carolina
+

Deep
Drft

+

Electronic Music Collective

+

+ We craft immersive electronic soundscapes — live, unscripted, and built from synthesizers, drum machines, and raw intention. No sets. No loops. Pure drift. +

+ +
- - - - DEEP DRFT - +
+ @* Pulsing rings *@ +
+
+
- - ELECTRONIC MUSIC COLLECTIVE - +
+ @* Now-Playing card *@ +
+
Now Playing
+
@(Player?.CurrentTrack?.TrackName ?? "Nothing playing")
+
+ @(Player?.CurrentTrack != null + ? $"{Player.CurrentTrack.Artist} · {Player.CurrentTrack.Album ?? "Single"}" + : "Select a track to begin") +
- - Live electronic music from Charleston, South Carolina - + @if (Player?.IsLoaded == true) + { +
+ @* 20 bars - approximate the wireframe's varied animation timings *@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } + else + { +
+ } +
- - - START STREAMING - + @* Stat row - hard-coded for now. TODO Phase 2: wire to real track count / identity model. *@ +
+
+
47+
+
Live Sessions
+
+
+
2
+
Members
+
+
+
+
Drift Points
+
+
+
+
+
- - - BROWSE TRACKS - - - -
+@* Divider *@ +
+
+
The Sound
+
+
- @* About Section *@ - - - - - - The Collective - - - DeepDrft is a two-member electronic music collective based in Charleston, South Carolina. - We create immersive soundscapes that blend the energy of underground club culture with - the precision of live electronic performance. - - - Using an arsenal of synthesizers, drum machines, and grooveboxes, we craft dynamic live - performances that span house, techno, trance, and intelligent dance music (IDM). - Every session is a journey through sound and rhythm. - - - - - - - - - Our Sound - - - House - Techno - Trance - IDM - Progressive - Ambient - - - From deep, driving basslines to ethereal atmospheric textures, our music explores the - full spectrum of electronic expression. We believe in the power of live performance - to create unique, unrepeatable moments that connect artist and audience through rhythm and melody. - - - - +@* Sound section *@ +
+
+
+ +

Every
Frequency
Explored

+
+

+ From the hypnotic pulse of deep house to the fractal complexity of IDM, Deep DRFT refuses to be categorized. We treat genre as a palette — not a cage. Each session begins as something and ends as something else entirely. +

+
- @* Features Section *@ - - - Experience DeepDrft - - - - - - - - High-Quality Streaming - - - Crystal-clear audio streaming with lossless quality for the best listening experience - - - - - - - - - Live Sessions - - - Join us for live streaming sessions and experience electronic music as it's created - - - - - - - - - Video Content - - - Watch behind-the-scenes content and live performance videos (coming soon) - - - - - - - - - Growing Archive - - - Explore our expanding collection of tracks, mixes, and live recordings - - - - - +
+ @* TODO Phase 2.2: wire each genre to /genres/{slug} *@ +
+
House
+
Foundation
+
+
+
Techno
+
Architecture
+
+
+
Trance
+
Ascension
+
+
+
IDM
+
Intelligence
+
+
+
Progressive
+
Journey
+
+
+
Ambient
+
Atmosphere
+
+
+
- @* Location & Connect Section *@ - - - - - - Charleston, SC - - - Based in the vibrant music scene of Charleston, South Carolina, DeepDrft draws inspiration - from both the city's rich cultural heritage and the cutting-edge sounds of global electronic music. - - - The Holy City's unique blend of tradition and innovation provides the perfect backdrop - for our electronic explorations, where historic charm meets futuristic beats. - - - - - - - - - Connect With Us - - - Stay connected with DeepDrft for the latest releases, live session announcements, - and updates from our studio in Charleston. - - - - Newsletter - - - Live Alerts - - - - - +@* Dark features section *@ +
+ +

+ Built for the
Committed Listener +

- @* Call to Action *@ - - - Ready to Dive Deep? - - - Immerse yourself in the electronic soundscapes of DeepDrft - - - - - EXPLORE MUSIC - - - - LIVE SCHEDULE - - - +
+
+
+ +
+
Lossless Audio Streaming
+
Crystal-clear sound delivered at full resolution. We don't compress the music — or the experience.
+
+
+
+ +
+
Live Sessions Broadcast
+
Join us in real time. Every session is unrepeatable — a singular moment sculpted between performer and listener.
+
+
+
+ +
+
Studio Video Content
+
Behind-the-machine footage. Watch synthesis happen in real time, gear and all — no performance, just process.
+
+
+
+ +
+
Growing Archive
+
A living catalogue of sessions, mixes, and experiments — indexed, searchable, and always expanding.
+
+
+
-
+@* Split: origin + connect *@ +
+
+
Our Origin
+

Where the Holy City
Meets the Future

+

+ Charleston, South Carolina holds centuries of culture in its streets. We carry that weight into the studio — tradition as tension against the forward pull of electronic sound. The result is music that feels both ancient and unimagined. +

+
+
+
Stay Connected
+

Never Miss
a Session

+
+ @* TODO: wire to subscription system when newsletter/alerts backend exists *@ +
+
+ +
+
+
Newsletter
+
New releases & studio dispatches
+
+
+
+
+ +
+
+
Live Alerts
+
Know the moment we go live
+
+
+
+ @* TODO: subscription form lives behind this CTA *@ + +
+
+ +@* CTA banner *@ +
+
+

Ready to
Drift Deeper?

+

Immerse yourself. The current is always running.

+
+
+ Explore the Archive + @* TODO: route to /schedule when live-session schedule page exists *@ + +
+
+ +@code { + [CascadingParameter] public IPlayerService? Player { get; set; } +} diff --git a/DeepDrftWeb.Client/Pages/Home.razor.css b/DeepDrftWeb.Client/Pages/Home.razor.css new file mode 100644 index 0000000..46b97ad --- /dev/null +++ b/DeepDrftWeb.Client/Pages/Home.razor.css @@ -0,0 +1,721 @@ +/* Home.razor scoped styles - wireframe-faithful marketing page. */ + +/* ── Animations ── */ +@keyframes fade-up { + from { opacity: 0; transform: translateY(24px); } + to { opacity: 1; transform: none; } +} + +@keyframes pulse-ring { + 0%, 100% { opacity: 0.15; transform: translate(-50%, -50%) scale(1); } + 50% { opacity: 0.4; transform: translate(-50%, -50%) scale(1.03); } +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} + +@keyframes wave-dance { + from { height: var(--h-lo, 4px); } + to { height: var(--h-hi, 20px); } +} + +.fade-up { + opacity: 0; + animation: fade-up 0.8s ease forwards; +} + +/* ── HERO ── */ +.hero { + min-height: 100vh; + display: grid; + grid-template-columns: 1fr 1fr; + overflow: hidden; +} + +.hero-left { + display: flex; + flex-direction: column; + justify-content: center; + padding: 6rem 3rem; + position: relative; + background: var(--deepdrft-white); +} + +.hero-eyebrow { + font-family: var(--deepdrft-font-mono); + font-size: 0.65rem; + letter-spacing: 0.28em; + color: var(--deepdrft-green-accent); + text-transform: uppercase; + margin-bottom: 1.8rem; + display: flex; + align-items: center; + gap: 1rem; + animation-delay: 0.1s; +} + +.hero-eyebrow::before { + content: ''; + display: block; + width: 2.5rem; + height: 1px; + background: var(--deepdrft-green-accent); +} + +.hero-title { + font-family: var(--deepdrft-font-display); + font-size: clamp(4.5rem, 8vw, 8.5rem); + font-weight: 300; + line-height: 0.92; + letter-spacing: -0.02em; + color: var(--deepdrft-navy); + margin-bottom: 0.5rem; + animation-delay: 0.22s; +} + +.hero-title em { + font-style: italic; + color: var(--deepdrft-green); +} + +.hero-subtitle { + font-family: var(--deepdrft-font-display); + font-size: clamp(1rem, 2vw, 1.35rem); + font-weight: 300; + font-style: italic; + color: var(--deepdrft-muted); + margin-bottom: 3rem; + letter-spacing: 0.04em; + animation-delay: 0.34s; +} + +.hero-desc { + font-family: var(--deepdrft-font-body); + font-size: 0.92rem; + line-height: 1.75; + color: var(--deepdrft-navy); + opacity: 0.7; + max-width: 36ch; + margin-bottom: 3rem; + animation-delay: 0.44s; +} + +.hero-actions { + display: flex; + gap: 1rem; + align-items: center; + animation-delay: 0.54s; +} + +.btn-primary { + font-family: var(--deepdrft-font-mono); + font-size: 0.68rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--deepdrft-white); + background: var(--deepdrft-navy); + border: none; + padding: 1rem 2.2rem; + cursor: pointer; + text-decoration: none; + transition: background 0.25s, transform 0.2s; + display: inline-block; +} + +.btn-primary:hover { + background: var(--deepdrft-green); + transform: translateY(-1px); +} + +.btn-ghost { + font-family: var(--deepdrft-font-mono); + font-size: 0.68rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--deepdrft-navy); + background: transparent; + border: 1px solid var(--deepdrft-border); + padding: 1rem 2.2rem; + cursor: pointer; + text-decoration: none; + transition: border-color 0.25s, color 0.25s; + display: inline-block; +} + +.btn-ghost:hover { border-color: var(--deepdrft-navy); } + +.hero-right { + background: var(--deepdrft-navy); + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-end; + overflow: hidden; +} + +.circle-deco { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + border: 1px solid rgba(61, 122, 104, 0.3); + animation: pulse-ring 4s ease-in-out infinite; + pointer-events: none; +} + +.circle-deco:nth-child(1) { width: 320px; height: 320px; animation-delay: 0s; } +.circle-deco:nth-child(2) { width: 520px; height: 520px; animation-delay: 0.8s; } +.circle-deco:nth-child(3) { width: 720px; height: 720px; animation-delay: 1.6s; } + +.hero-right-content { + position: relative; + z-index: 2; + padding: 3rem; +} + +.now-playing { + background: rgba(250, 250, 248, 0.06); + border: 1px solid rgba(250, 250, 248, 0.12); + padding: 1.5rem; + margin-bottom: 1.5rem; + backdrop-filter: blur(8px); +} + +.np-label { + font-family: var(--deepdrft-font-mono); + font-size: 0.6rem; + letter-spacing: 0.25em; + color: var(--deepdrft-green-accent); + text-transform: uppercase; + margin-bottom: 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.np-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--deepdrft-green-accent); + animation: blink 1.2s ease-in-out infinite; +} + +.np-title { + font-family: var(--deepdrft-font-display); + font-size: 1.5rem; + font-weight: 400; + color: var(--deepdrft-white); + margin-bottom: 0.25rem; +} + +.np-sub { + font-family: var(--deepdrft-font-body); + font-size: 0.75rem; + color: rgba(250, 250, 248, 0.45); + letter-spacing: 0.08em; +} + +.waveform-bars { + display: flex; + align-items: center; + gap: 3px; + margin-top: 1.2rem; +} + +.waveform-bar { + width: 3px; + background: var(--deepdrft-green-accent); + border-radius: 2px; + animation: wave-dance var(--dur, 1s) ease-in-out infinite alternate; +} + +.waveform-placeholder { + margin-top: 1.2rem; + height: 1px; + background: rgba(250, 250, 248, 0.12); +} + +.hero-stat-row { + display: flex; + gap: 1.5rem; +} + +.hero-stat { + flex: 1; + background: rgba(250, 250, 248, 0.04); + border: 1px solid rgba(250, 250, 248, 0.08); + padding: 1.2rem; +} + +.hero-stat-num { + font-family: var(--deepdrft-font-display); + font-size: 2rem; + font-weight: 300; + color: var(--deepdrft-white); + line-height: 1; +} + +.hero-stat-label { + font-family: var(--deepdrft-font-mono); + font-size: 0.58rem; + letter-spacing: 0.2em; + color: rgba(250, 250, 248, 0.4); + text-transform: uppercase; + margin-top: 0.4rem; +} + +/* ── DIVIDER ── */ +.section-divider { + display: flex; + align-items: center; + gap: 2rem; + padding: 2rem 3rem; + background: var(--deepdrft-white); +} + +.divider-line { + flex: 1; + height: 1px; + background: var(--deepdrft-border); +} + +.divider-tag { + font-family: var(--deepdrft-font-mono); + font-size: 0.6rem; + letter-spacing: 0.25em; + color: var(--deepdrft-muted); + text-transform: uppercase; + white-space: nowrap; +} + +/* ── SECTION (sound) ── */ +.section { + padding: 7rem 3rem; + background: var(--deepdrft-white); +} + +.section-header { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 4rem; + margin-bottom: 5rem; + align-items: end; +} + +.section-label { + font-family: var(--deepdrft-font-mono); + font-size: 0.62rem; + letter-spacing: 0.28em; + color: var(--deepdrft-green-accent); + text-transform: uppercase; + margin-bottom: 1.2rem; +} + +.section-title { + font-family: var(--deepdrft-font-display); + font-size: clamp(2.8rem, 5vw, 4.5rem); + font-weight: 300; + line-height: 1; + color: var(--deepdrft-navy); +} + +.section-title em { + font-style: italic; + color: var(--deepdrft-green); +} + +.section-body { + font-family: var(--deepdrft-font-body); + font-size: 0.9rem; + line-height: 1.8; + color: var(--deepdrft-navy); + opacity: 0.65; + max-width: 52ch; + align-self: end; +} + +/* Genre grid */ +.genre-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 1px; + background: var(--deepdrft-border); + border: 1px solid var(--deepdrft-border); + margin-bottom: 4rem; +} + +.genre-card { + background: var(--deepdrft-white); + padding: 2rem 1.5rem; + transition: background 0.3s, color 0.3s; + cursor: pointer; + position: relative; + overflow: hidden; + text-decoration: none; + display: block; +} + +.genre-card::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: var(--deepdrft-green-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.3s; +} + +.genre-card:hover::after { transform: scaleX(1); } +.genre-card:hover { background: #f3f6f4; } + +.genre-name { + font-family: var(--deepdrft-font-display); + font-size: 1.5rem; + font-weight: 400; + color: var(--deepdrft-navy); + margin-bottom: 0.5rem; +} + +.genre-count { + font-family: var(--deepdrft-font-mono); + font-size: 0.58rem; + letter-spacing: 0.2em; + color: var(--deepdrft-muted); + text-transform: uppercase; +} + +/* ── DARK FEATURE SECTION ── */ +.section-dark { + background: var(--deepdrft-navy); + padding: 7rem 3rem; + color: var(--deepdrft-white); +} + +.section-label-dark { + color: var(--deepdrft-green-accent); +} + +.section-title-dark { + color: var(--deepdrft-white); + margin-top: 0.5rem; +} + +.section-title-dark em { + font-style: italic; + color: var(--deepdrft-green-accent); +} + +.features-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0; + border: 1px solid rgba(250, 250, 248, 0.08); + margin-top: 4rem; +} + +.feature-card { + padding: 2.5rem; + border-right: 1px solid rgba(250, 250, 248, 0.08); + transition: background 0.3s; +} + +.feature-card:last-child { border-right: none; } + +.feature-card:hover { background: rgba(250, 250, 248, 0.04); } + +.feature-icon { + width: 2.5rem; + height: 2.5rem; + border: 1px solid rgba(250, 250, 248, 0.15); + margin-bottom: 1.8rem; + display: flex; + align-items: center; + justify-content: center; +} + +.feature-icon svg { + width: 1rem; + height: 1rem; + stroke: var(--deepdrft-green-accent); + fill: none; + stroke-width: 1.5; +} + +.feature-title { + font-family: var(--deepdrft-font-display); + font-size: 1.3rem; + font-weight: 400; + color: var(--deepdrft-white); + margin-bottom: 0.8rem; + line-height: 1.2; +} + +.feature-desc { + font-family: var(--deepdrft-font-body); + font-size: 0.8rem; + line-height: 1.7; + color: rgba(250, 250, 248, 0.45); +} + +/* ── SPLIT: ORIGIN + CONNECT ── */ +.section-split { + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 60vh; +} + +.split-left { + background: var(--deepdrft-green); + padding: 6rem 4rem; + display: flex; + flex-direction: column; + justify-content: center; + position: relative; + overflow: hidden; +} + +.split-left::before { + content: ''; + position: absolute; + top: -100px; + right: -100px; + width: 400px; + height: 400px; + border-radius: 50%; + background: rgba(61, 122, 104, 0.3); +} + +.split-right { + padding: 6rem 4rem; + display: flex; + flex-direction: column; + justify-content: center; + background: var(--deepdrft-white); +} + +.split-eyebrow { + font-family: var(--deepdrft-font-mono); + font-size: 0.62rem; + letter-spacing: 0.28em; + color: rgba(250, 250, 248, 0.6); + text-transform: uppercase; + margin-bottom: 1.5rem; + position: relative; + z-index: 1; +} + +.split-title { + font-family: var(--deepdrft-font-display); + font-size: clamp(2.5rem, 4vw, 3.8rem); + font-weight: 300; + color: var(--deepdrft-white); + line-height: 1.05; + margin-bottom: 1.5rem; + position: relative; + z-index: 1; +} + +.split-title em { + font-style: italic; + opacity: 0.65; +} + +.split-body { + font-family: var(--deepdrft-font-body); + font-size: 0.88rem; + line-height: 1.8; + color: rgba(250, 250, 248, 0.6); + max-width: 42ch; + position: relative; + z-index: 1; +} + +.connect-label { + font-family: var(--deepdrft-font-mono); + font-size: 0.62rem; + letter-spacing: 0.28em; + color: var(--deepdrft-green-accent); + text-transform: uppercase; + margin-bottom: 1.5rem; +} + +.connect-title { + font-family: var(--deepdrft-font-display); + font-size: clamp(2rem, 3.5vw, 3rem); + font-weight: 300; + color: var(--deepdrft-navy); + line-height: 1.05; + margin-bottom: 2rem; +} + +.connect-title em { + font-style: italic; + color: var(--deepdrft-green); +} + +.connect-options { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 2.5rem; +} + +.connect-option { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 1.25rem; + border: 1px solid var(--deepdrft-border); + cursor: pointer; + transition: border-color 0.25s, background 0.25s; + text-decoration: none; +} + +.connect-option:hover { + border-color: var(--deepdrft-green-accent); + background: #f3f6f4; +} + +.option-icon { + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + background: var(--deepdrft-navy); + flex-shrink: 0; +} + +.option-icon svg { + width: 0.9rem; + height: 0.9rem; + stroke: var(--deepdrft-white); + fill: none; + stroke-width: 1.5; +} + +.option-text-label { + font-family: var(--deepdrft-font-mono); + font-size: 0.65rem; + letter-spacing: 0.15em; + color: var(--deepdrft-navy); + text-transform: uppercase; +} + +.option-text-sub { + font-family: var(--deepdrft-font-body); + font-size: 0.75rem; + color: var(--deepdrft-muted); + margin-top: 0.1rem; +} + +.connect-cta { + align-self: flex-start; +} + +/* ── CTA BANNER ── */ +.cta-banner { + background: var(--deepdrft-navy); + padding: 6rem 3rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 3rem; + position: relative; + overflow: hidden; +} + +.cta-banner::before { + content: 'DRFT'; + position: absolute; + right: -2rem; + top: 50%; + transform: translateY(-50%); + font-family: var(--deepdrft-font-display); + font-size: 22rem; + font-weight: 300; + color: rgba(250, 250, 248, 0.03); + line-height: 1; + pointer-events: none; + user-select: none; + letter-spacing: -0.05em; +} + +.cta-text { + position: relative; + z-index: 1; +} + +.cta-headline { + font-family: var(--deepdrft-font-display); + font-size: clamp(2.5rem, 5vw, 5rem); + font-weight: 300; + color: var(--deepdrft-white); + line-height: 1; + margin-bottom: 1rem; +} + +.cta-headline em { + font-style: italic; + color: var(--deepdrft-green-accent); +} + +.cta-sub { + font-family: var(--deepdrft-font-body); + font-size: 0.88rem; + color: rgba(250, 250, 248, 0.4); + letter-spacing: 0.05em; +} + +.cta-actions { + display: flex; + gap: 1rem; + align-items: center; + position: relative; + z-index: 1; + flex-shrink: 0; +} + +.btn-white { + font-family: var(--deepdrft-font-mono); + font-size: 0.68rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--deepdrft-navy); + background: var(--deepdrft-white); + border: none; + padding: 1rem 2.2rem; + cursor: pointer; + text-decoration: none; + transition: background 0.25s, color 0.25s; + display: inline-block; +} + +.btn-white:hover { + background: var(--deepdrft-green-accent); + color: var(--deepdrft-white); +} + +.btn-outline-white { + font-family: var(--deepdrft-font-mono); + font-size: 0.68rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--deepdrft-white); + background: transparent; + border: 1px solid rgba(250, 250, 248, 0.3); + padding: 1rem 2.2rem; + cursor: pointer; + text-decoration: none; + transition: border-color 0.25s; + display: inline-block; +} + +.btn-outline-white:hover { border-color: var(--deepdrft-white); } diff --git a/DeepDrftWeb.Client/Services/AudioPlayerService.cs b/DeepDrftWeb.Client/Services/AudioPlayerService.cs index 08151de..2118cc0 100644 --- a/DeepDrftWeb.Client/Services/AudioPlayerService.cs +++ b/DeepDrftWeb.Client/Services/AudioPlayerService.cs @@ -24,6 +24,15 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable public double Volume { get; protected set; } = 0.8; public double LoadProgress { get; protected set; } = 0; public string? ErrorMessage { get; protected set; } + /// + /// The currently selected track. In the streaming subclass this property is managed + /// exclusively by : set in + /// LoadTrackStreaming after ResetToIdle clears it, and cleared again + /// by ResetToIdle on stop/unload/dispose. Base-class subclasses that take the + /// / path are responsible for managing + /// it themselves. + /// + public TrackEntity? CurrentTrack { get; protected set; } // Events public EventCallback? OnStateChanged { get; set; } @@ -68,12 +77,12 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable public virtual async Task SelectTrack(TrackEntity track) { await EnsureInitializedAsync(); - + await NotifyStateChanged(); - + if (OnTrackSelected.HasValue) await OnTrackSelected.Value.InvokeAsync(); - + await LoadTrack(track); await NotifyStateChanged(); } diff --git a/DeepDrftWeb.Client/Services/IPlayerService.cs b/DeepDrftWeb.Client/Services/IPlayerService.cs index 120af1e..acbd2ff 100644 --- a/DeepDrftWeb.Client/Services/IPlayerService.cs +++ b/DeepDrftWeb.Client/Services/IPlayerService.cs @@ -17,7 +17,8 @@ public interface IPlayerService double Volume { get; } double LoadProgress { get; } string? ErrorMessage { get; } - + TrackEntity? CurrentTrack { get; } + // Events for UI updates EventCallback? OnStateChanged { get; set; } EventCallback? OnTrackSelected { get; set; } diff --git a/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs b/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs index d6eb8d7..c5a7279 100644 --- a/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs +++ b/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs @@ -70,6 +70,9 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS // Save track ID for seek operations _currentTrackId = track.EntryKey; + // Expose to UI immediately — Now-Playing surfaces should reflect the selected + // track while it's still loading, not only after playback starts. + CurrentTrack = track; // Create new cancellation token for this streaming operation _streamingCancellation = new CancellationTokenSource(); @@ -434,6 +437,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS Duration = null; LoadProgress = 0; ErrorMessage = null; + CurrentTrack = null; // 4. Reset streaming-specific state IsStreamingMode = false;