diff --git a/backend/app/models.py b/backend/app/models.py
index c87b053..2397642 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -13,6 +13,7 @@ from sqlalchemy import (
ForeignKey,
UniqueConstraint,
Index,
+ text,
)
from sqlalchemy.orm import DeclarativeBase, relationship
@@ -106,25 +107,26 @@ class StationConfig(Base):
app_store_embed_html = Column(Text, nullable=False, default="")
# Color theme — hex strings, e.g. "#1a3a5c"
- color_primary = Column(String(7), nullable=False, default="#1a3a5c")
- color_primary_light = Column(String(7), nullable=False, default="#2a5a8c")
- color_primary_dark = Column(String(7), nullable=False, default="#0e2440")
- color_primary_muted = Column(String(7), nullable=False, default="#3a6a9c")
- color_accent = Column(String(7), nullable=False, default="#e87a2e")
- color_accent_light = Column(String(7), nullable=False, default="#f09a4e")
- color_accent_dark = Column(String(7), nullable=False, default="#c85a1e")
- color_accent_muted = Column(String(7), nullable=False, default="#f0a86a")
- color_background = Column(String(7), nullable=False, default="#faf6f0")
- color_text = Column(String(7), nullable=False, default="#3a3632")
- color_success = Column(String(7), nullable=False, default="#4a9e4f")
- color_danger = Column(String(7), nullable=False, default="#c83030")
- color_info = Column(String(7), nullable=False, default="#3a8abf")
+ # server_default is required so PostgreSQL emits a proper DEFAULT in ALTER TABLE DDL
+ color_primary = Column(String(7), nullable=False, server_default=text("'#1a3a5c'"), default="#1a3a5c")
+ color_primary_light = Column(String(7), nullable=False, server_default=text("'#2a5a8c'"), default="#2a5a8c")
+ color_primary_dark = Column(String(7), nullable=False, server_default=text("'#0e2440'"), default="#0e2440")
+ color_primary_muted = Column(String(7), nullable=False, server_default=text("'#3a6a9c'"), default="#3a6a9c")
+ color_accent = Column(String(7), nullable=False, server_default=text("'#e87a2e'"), default="#e87a2e")
+ color_accent_light = Column(String(7), nullable=False, server_default=text("'#f09a4e'"), default="#f09a4e")
+ color_accent_dark = Column(String(7), nullable=False, server_default=text("'#c85a1e'"), default="#c85a1e")
+ color_accent_muted = Column(String(7), nullable=False, server_default=text("'#f0a86a'"), default="#f0a86a")
+ color_background = Column(String(7), nullable=False, server_default=text("'#faf6f0'"), default="#faf6f0")
+ color_text = Column(String(7), nullable=False, server_default=text("'#3a3632'"), default="#3a3632")
+ color_success = Column(String(7), nullable=False, server_default=text("'#4a9e4f'"), default="#4a9e4f")
+ color_danger = Column(String(7), nullable=False, server_default=text("'#c83030'"), default="#c83030")
+ color_info = Column(String(7), nullable=False, server_default=text("'#3a8abf'"), default="#3a8abf")
# Neutral colors
- color_white = Column(String(7), nullable=False, default="#ffffff")
- color_light = Column(String(7), nullable=False, default="#f0ebe3")
- color_medium = Column(String(7), nullable=False, default="#8a8580")
- color_black = Column(String(7), nullable=False, default="#1a1816")
+ color_white = Column(String(7), nullable=False, server_default=text("'#ffffff'"), default="#ffffff")
+ color_light = Column(String(7), nullable=False, server_default=text("'#f0ebe3'"), default="#f0ebe3")
+ color_medium = Column(String(7), nullable=False, server_default=text("'#8a8580'"), default="#8a8580")
+ color_black = Column(String(7), nullable=False, server_default=text("'#1a1816'"), default="#1a1816")
class HistoryEntry(Base):
diff --git a/src/app/app.html b/src/app/app.html
index 7a3b6d9..3d58163 100644
--- a/src/app/app.html
+++ b/src/app/app.html
@@ -1,6 +1,6 @@
-
+
diff --git a/src/app/app.scss b/src/app/app.scss
index 33db825..72b8298 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -1,8 +1,6 @@
@use '../styles/variables' as *;
-// Bar heights (keep in sync so player sits flush below navbar)
-$navbar-height-mobile: 64px;
-$navbar-height-desktop: 52px;
+// Player height (matches media-player component min-height)
$player-height: 48px;
app-navbar {
@@ -11,26 +9,39 @@ app-navbar {
left: 0;
right: 0;
z-index: 200;
+ overflow: visible;
}
+// App-level media player: shown on mobile (fixed strip), hidden on desktop (navbar instance shown)
app-media-player {
- position: fixed;
- top: $navbar-height-mobile;
- left: 0;
- right: 0;
- z-index: 199;
-}
-
-main {
- padding-top: calc(#{$navbar-height-mobile} + #{$player-height});
-}
-
-@media (min-width: #{$bp-md}) {
- app-media-player {
- top: $navbar-height-desktop;
+ // Mobile: fixed strip below navbar
+ @media (max-width: #{$bp-md - 1px}) {
+ position: fixed;
+ top: var(--navbar-height);
+ left: 0;
+ right: 0;
+ z-index: 199;
}
+ // Desktop: hidden (navbar contains the visible instance)
+ @media (min-width: $bp-md) {
+ display: none;
+ }
+}
+
+// Main content padding
+@media (max-width: #{$bp-md - 1px}) {
main {
- padding-top: calc(#{$navbar-height-desktop} + #{$player-height});
+ padding-top: calc(var(--navbar-height) + #{$player-height});
+
+ &.no-stream {
+ padding-top: var(--navbar-height);
+ }
+ }
+}
+
+@media (min-width: $bp-md) {
+ main {
+ padding-top: var(--navbar-height);
}
}
diff --git a/src/app/app.ts b/src/app/app.ts
index 9a03d87..0d997ae 100644
--- a/src/app/app.ts
+++ b/src/app/app.ts
@@ -1,4 +1,4 @@
-import { Component, effect, inject } from '@angular/core';
+import { Component, computed, effect, inject, OnInit, OnDestroy } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { RouterOutlet } from '@angular/router';
import { NavbarComponent } from './navbar/navbar.component';
@@ -13,23 +13,59 @@ import { StationConfigService } from './services/station-config.service';
templateUrl: './app.html',
styleUrl: './app.scss',
})
-export class App {
+export class App implements OnInit, OnDestroy {
private titleService = inject(Title);
private stationConfig = inject(StationConfigService);
+ readonly hasStream = computed(() => {
+ const cfg = this.stationConfig.config();
+ return !!cfg.stream_url && cfg.stream_url.trim().length > 0;
+ });
+
+ private navbarEl: HTMLElement | null = null;
+ private resizeObserver: ResizeObserver | null = null;
+
constructor() {
effect(() => {
const config = this.stationConfig.config();
const fullName = `${config.name_primary} ${config.name_secondary}`.trim();
this.titleService.setTitle(fullName);
- // Update favicon to use the station's logo_url.
- // Meta.updateTag() is unreliable for favicons in some browsers,
- // so we update the element directly.
const faviconEl = document.querySelector('link[rel="icon"]') as HTMLLinkElement | null;
if (faviconEl && config.logo_url) {
faviconEl.href = config.logo_url;
}
});
+
+ effect(() => {
+ if (this.hasStream()) {
+ document.body.classList.add('has-stream');
+ } else {
+ document.body.classList.remove('has-stream');
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ // Defer until the navbar is rendered in the DOM.
+ queueMicrotask(() => {
+ this.navbarEl = document.querySelector('app-navbar');
+ if (!this.navbarEl) return;
+
+ this.updateNavbarHeight();
+
+ this.resizeObserver = new ResizeObserver(() => this.updateNavbarHeight());
+ this.resizeObserver.observe(this.navbarEl!);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.resizeObserver?.disconnect();
+ }
+
+ private updateNavbarHeight(): void {
+ if (!this.navbarEl) return;
+ const h = this.navbarEl.offsetHeight;
+ document.documentElement.style.setProperty('--navbar-height', `${h}px`);
}
}
diff --git a/src/app/media-player/media-player.component.scss b/src/app/media-player/media-player.component.scss
index 8148c96..b201bd0 100644
--- a/src/app/media-player/media-player.component.scss
+++ b/src/app/media-player/media-player.component.scss
@@ -12,6 +12,11 @@
font-size: 0.85rem;
color: var(--color-light);
user-select: none;
+
+ // When inside navbar (desktop), remove the bottom border — navbar is one unit
+ :global(body.has-stream) & {
+ border-bottom: none;
+ }
}
.media-player__inner {
diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html
index 1c982b5..ad50eff 100644
--- a/src/app/navbar/navbar.component.html
+++ b/src/app/navbar/navbar.component.html
@@ -8,6 +8,8 @@
+
+