From 9b8d4c0f018eac8d31592e2fd929e0ea40eb5e62 Mon Sep 17 00:00:00 2001 From: kyle Date: Tue, 30 Jun 2026 21:06:35 +0000 Subject: [PATCH] layout changes with media player and negative spacing at top of screen. updated sqlalchemy init for postgres deployment --- backend/app/models.py | 36 +++++++------- src/app/app.html | 2 +- src/app/app.scss | 47 ++++++++++++------- src/app/app.ts | 46 ++++++++++++++++-- .../media-player/media-player.component.scss | 5 ++ src/app/navbar/navbar.component.html | 2 + src/app/navbar/navbar.component.scss | 40 ++++++++++++++-- src/app/navbar/navbar.component.ts | 3 +- 8 files changed, 135 insertions(+), 46 deletions(-) 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 @@ + +