layout changes with media player and negative spacing at top of screen. updated sqlalchemy init for postgres deployment
This commit is contained in:
+19
-17
@@ -13,6 +13,7 @@ from sqlalchemy import (
|
|||||||
ForeignKey,
|
ForeignKey,
|
||||||
UniqueConstraint,
|
UniqueConstraint,
|
||||||
Index,
|
Index,
|
||||||
|
text,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import DeclarativeBase, relationship
|
from sqlalchemy.orm import DeclarativeBase, relationship
|
||||||
|
|
||||||
@@ -106,25 +107,26 @@ class StationConfig(Base):
|
|||||||
app_store_embed_html = Column(Text, nullable=False, default="")
|
app_store_embed_html = Column(Text, nullable=False, default="")
|
||||||
|
|
||||||
# Color theme — hex strings, e.g. "#1a3a5c"
|
# Color theme — hex strings, e.g. "#1a3a5c"
|
||||||
color_primary = Column(String(7), nullable=False, default="#1a3a5c")
|
# server_default is required so PostgreSQL emits a proper DEFAULT in ALTER TABLE DDL
|
||||||
color_primary_light = Column(String(7), nullable=False, default="#2a5a8c")
|
color_primary = Column(String(7), nullable=False, server_default=text("'#1a3a5c'"), default="#1a3a5c")
|
||||||
color_primary_dark = Column(String(7), nullable=False, default="#0e2440")
|
color_primary_light = Column(String(7), nullable=False, server_default=text("'#2a5a8c'"), default="#2a5a8c")
|
||||||
color_primary_muted = Column(String(7), nullable=False, default="#3a6a9c")
|
color_primary_dark = Column(String(7), nullable=False, server_default=text("'#0e2440'"), default="#0e2440")
|
||||||
color_accent = Column(String(7), nullable=False, default="#e87a2e")
|
color_primary_muted = Column(String(7), nullable=False, server_default=text("'#3a6a9c'"), default="#3a6a9c")
|
||||||
color_accent_light = Column(String(7), nullable=False, default="#f09a4e")
|
color_accent = Column(String(7), nullable=False, server_default=text("'#e87a2e'"), default="#e87a2e")
|
||||||
color_accent_dark = Column(String(7), nullable=False, default="#c85a1e")
|
color_accent_light = Column(String(7), nullable=False, server_default=text("'#f09a4e'"), default="#f09a4e")
|
||||||
color_accent_muted = Column(String(7), nullable=False, default="#f0a86a")
|
color_accent_dark = Column(String(7), nullable=False, server_default=text("'#c85a1e'"), default="#c85a1e")
|
||||||
color_background = Column(String(7), nullable=False, default="#faf6f0")
|
color_accent_muted = Column(String(7), nullable=False, server_default=text("'#f0a86a'"), default="#f0a86a")
|
||||||
color_text = Column(String(7), nullable=False, default="#3a3632")
|
color_background = Column(String(7), nullable=False, server_default=text("'#faf6f0'"), default="#faf6f0")
|
||||||
color_success = Column(String(7), nullable=False, default="#4a9e4f")
|
color_text = Column(String(7), nullable=False, server_default=text("'#3a3632'"), default="#3a3632")
|
||||||
color_danger = Column(String(7), nullable=False, default="#c83030")
|
color_success = Column(String(7), nullable=False, server_default=text("'#4a9e4f'"), default="#4a9e4f")
|
||||||
color_info = Column(String(7), nullable=False, default="#3a8abf")
|
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
|
# Neutral colors
|
||||||
color_white = Column(String(7), nullable=False, default="#ffffff")
|
color_white = Column(String(7), nullable=False, server_default=text("'#ffffff'"), default="#ffffff")
|
||||||
color_light = Column(String(7), nullable=False, default="#f0ebe3")
|
color_light = Column(String(7), nullable=False, server_default=text("'#f0ebe3'"), default="#f0ebe3")
|
||||||
color_medium = Column(String(7), nullable=False, default="#8a8580")
|
color_medium = Column(String(7), nullable=False, server_default=text("'#8a8580'"), default="#8a8580")
|
||||||
color_black = Column(String(7), nullable=False, default="#1a1816")
|
color_black = Column(String(7), nullable=False, server_default=text("'#1a1816'"), default="#1a1816")
|
||||||
|
|
||||||
|
|
||||||
class HistoryEntry(Base):
|
class HistoryEntry(Base):
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<app-navbar></app-navbar>
|
<app-navbar></app-navbar>
|
||||||
<app-media-player></app-media-player>
|
<app-media-player></app-media-player>
|
||||||
<main>
|
<main [class.no-stream]="!hasStream()">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
|
|||||||
+29
-18
@@ -1,8 +1,6 @@
|
|||||||
@use '../styles/variables' as *;
|
@use '../styles/variables' as *;
|
||||||
|
|
||||||
// Bar heights (keep in sync so player sits flush below navbar)
|
// Player height (matches media-player component min-height)
|
||||||
$navbar-height-mobile: 64px;
|
|
||||||
$navbar-height-desktop: 52px;
|
|
||||||
$player-height: 48px;
|
$player-height: 48px;
|
||||||
|
|
||||||
app-navbar {
|
app-navbar {
|
||||||
@@ -11,26 +9,39 @@ app-navbar {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App-level media player: shown on mobile (fixed strip), hidden on desktop (navbar instance shown)
|
||||||
app-media-player {
|
app-media-player {
|
||||||
position: fixed;
|
// Mobile: fixed strip below navbar
|
||||||
top: $navbar-height-mobile;
|
@media (max-width: #{$bp-md - 1px}) {
|
||||||
left: 0;
|
position: fixed;
|
||||||
right: 0;
|
top: var(--navbar-height);
|
||||||
z-index: 199;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Desktop: hidden (navbar contains the visible instance)
|
||||||
|
@media (min-width: $bp-md) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main content padding
|
||||||
|
@media (max-width: #{$bp-md - 1px}) {
|
||||||
main {
|
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
@@ -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 { Title } from '@angular/platform-browser';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { NavbarComponent } from './navbar/navbar.component';
|
import { NavbarComponent } from './navbar/navbar.component';
|
||||||
@@ -13,23 +13,59 @@ import { StationConfigService } from './services/station-config.service';
|
|||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss',
|
styleUrl: './app.scss',
|
||||||
})
|
})
|
||||||
export class App {
|
export class App implements OnInit, OnDestroy {
|
||||||
private titleService = inject(Title);
|
private titleService = inject(Title);
|
||||||
private stationConfig = inject(StationConfigService);
|
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() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const config = this.stationConfig.config();
|
const config = this.stationConfig.config();
|
||||||
const fullName = `${config.name_primary} ${config.name_secondary}`.trim();
|
const fullName = `${config.name_primary} ${config.name_secondary}`.trim();
|
||||||
this.titleService.setTitle(fullName);
|
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;
|
const faviconEl = document.querySelector('link[rel="icon"]') as HTMLLinkElement | null;
|
||||||
if (faviconEl && config.logo_url) {
|
if (faviconEl && config.logo_url) {
|
||||||
faviconEl.href = 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;
|
font-size: 0.85rem;
|
||||||
color: var(--color-light);
|
color: var(--color-light);
|
||||||
user-select: none;
|
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 {
|
.media-player__inner {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<app-media-player></app-media-player>
|
||||||
|
|
||||||
<button class="navbar-toggle" aria-label="Toggle menu" (click)="toggleMenu()">
|
<button class="navbar-toggle" aria-label="Toggle menu" (click)="toggleMenu()">
|
||||||
<span class="bar"></span>
|
<span class="bar"></span>
|
||||||
<span class="bar"></span>
|
<span class="bar"></span>
|
||||||
|
|||||||
@@ -4,14 +4,47 @@
|
|||||||
.navbar {
|
.navbar {
|
||||||
background: var(--color-primary-dark);
|
background: var(--color-primary-dark);
|
||||||
padding: $spacing-sm 0;
|
padding: $spacing-sm 0;
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: $z-sticky;
|
|
||||||
box-shadow: $shadow-md;
|
box-shadow: $shadow-md;
|
||||||
|
|
||||||
|
:global(body.has-stream) & {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inner {
|
.navbar-inner {
|
||||||
@include flex-between;
|
@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 {
|
.navbar-brand {
|
||||||
@@ -181,7 +214,6 @@
|
|||||||
// Responsive
|
// Responsive
|
||||||
@media (max-width: #{$bp-md - 1px}) {
|
@media (max-width: #{$bp-md - 1px}) {
|
||||||
.navbar {
|
.navbar {
|
||||||
height: 64px;
|
|
||||||
padding: $spacing-xs 0;
|
padding: $spacing-xs 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CommonModule, UpperCasePipe } from '@angular/common';
|
|||||||
import { RouterLink, RouterLinkActive, Router } from '@angular/router';
|
import { RouterLink, RouterLinkActive, Router } from '@angular/router';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
|
import { MediaPlayerComponent } from '../media-player/media-player.component';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { StationConfigService } from '../services/station-config.service';
|
import { StationConfigService } from '../services/station-config.service';
|
||||||
import { resolveImageUrl } from '../services/app-config.service';
|
import { resolveImageUrl } from '../services/app-config.service';
|
||||||
@@ -10,7 +11,7 @@ import { resolveImageUrl } from '../services/app-config.service';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, UpperCasePipe, RouterLink, RouterLinkActive],
|
imports: [CommonModule, UpperCasePipe, RouterLink, RouterLinkActive, MediaPlayerComponent],
|
||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
styleUrl: './navbar.component.scss',
|
styleUrl: './navbar.component.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user