layout changes with media player and negative spacing at top of screen. updated sqlalchemy init for postgres deployment

This commit is contained in:
2026-06-30 21:06:35 +00:00
parent 87c9b0b8ab
commit 9b8d4c0f01
8 changed files with 135 additions and 46 deletions
+19 -17
View File
@@ -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):
+1 -1
View File
@@ -1,6 +1,6 @@
<app-navbar></app-navbar>
<app-media-player></app-media-player>
<main>
<main [class.no-stream]="!hasStream()">
<router-outlet></router-outlet>
</main>
<app-footer></app-footer>
+29 -18
View File
@@ -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);
}
}
+41 -5
View File
@@ -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 <link> 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`);
}
}
@@ -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 {
+2
View File
@@ -8,6 +8,8 @@
</span>
</a>
<app-media-player></app-media-player>
<button class="navbar-toggle" aria-label="Toggle menu" (click)="toggleMenu()">
<span class="bar"></span>
<span class="bar"></span>
+36 -4
View File
@@ -4,14 +4,47 @@
.navbar {
background: var(--color-primary-dark);
padding: $spacing-sm 0;
position: sticky;
top: 0;
z-index: $z-sticky;
box-shadow: $shadow-md;
:global(body.has-stream) & {
box-shadow: none;
}
}
.navbar-inner {
@include flex-between;
// Mobile: media player inside navbar is hidden (app.html instance shown as fixed strip)
@media (max-width: #{$bp-md - 1px}) {
app-media-player {
display: none;
}
}
// Desktop: grid layout with media player inline
@media (min-width: $bp-md) {
display: grid;
align-items: center;
grid-template-columns: auto 1fr auto;
grid-template-areas: 'brand player menu';
gap: $spacing-lg;
.navbar-brand {
grid-area: brand;
}
app-media-player {
grid-area: player;
}
.navbar-menu {
grid-area: menu;
}
.navbar-toggle {
display: none;
}
}
}
.navbar-brand {
@@ -181,7 +214,6 @@
// Responsive
@media (max-width: #{$bp-md - 1px}) {
.navbar {
height: 64px;
padding: $spacing-xs 0;
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { CommonModule, UpperCasePipe } from '@angular/common';
import { RouterLink, RouterLinkActive, Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MediaPlayerComponent } from '../media-player/media-player.component';
import { AuthService } from '../services/auth.service';
import { StationConfigService } from '../services/station-config.service';
import { resolveImageUrl } from '../services/app-config.service';
@@ -10,7 +11,7 @@ import { resolveImageUrl } from '../services/app-config.service';
@Component({
selector: 'app-navbar',
standalone: true,
imports: [CommonModule, UpperCasePipe, RouterLink, RouterLinkActive],
imports: [CommonModule, UpperCasePipe, RouterLink, RouterLinkActive, MediaPlayerComponent],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss',
})