diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc index 905da32..0bcc23a 100644 Binary files a/backend/app/__pycache__/main.cpython-312.pyc and b/backend/app/__pycache__/main.cpython-312.pyc differ diff --git a/backend/app/__pycache__/models.cpython-312.pyc b/backend/app/__pycache__/models.cpython-312.pyc index cc520cf..f1f7ced 100644 Binary files a/backend/app/__pycache__/models.cpython-312.pyc and b/backend/app/__pycache__/models.cpython-312.pyc differ diff --git a/backend/app/__pycache__/schemas.cpython-312.pyc b/backend/app/__pycache__/schemas.cpython-312.pyc index 9d19e86..4394e61 100644 Binary files a/backend/app/__pycache__/schemas.cpython-312.pyc and b/backend/app/__pycache__/schemas.cpython-312.pyc differ diff --git a/backend/app/main.py b/backend/app/main.py index 1e63e80..1ac648a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -23,6 +23,9 @@ def _migrate_add_missing_columns(sync_conn): pending = [ ("station_config", "stream_url", sa.String(500), ""), ("station_config", "stream_metadata_url", sa.String(500), ""), + ("station_config", "play_store_icon_url", sa.String(500), ""), + ("station_config", "play_store_url", sa.String(500), ""), + ("station_config", "app_store_embed_html", sa.Text, ""), ] # Determine dialect @@ -38,13 +41,21 @@ def _migrate_add_missing_columns(sync_conn): print(f" → Migrating: adding column {table_name}.{col_name}") + # Determine SQL type from the column type object + if isinstance(col_type, sa.Text): + sql_type = "TEXT" + elif isinstance(col_type, sa.String): + sql_type = "VARCHAR(500)" + else: + sql_type = "TEXT" + if dialect == "sqlite": sync_conn.execute( sa.text(f'ALTER TABLE {table_name} ADD COLUMN "{col_name}" TEXT NOT NULL DEFAULT "{default}"') ) else: sync_conn.execute( - sa.text(f'ALTER TABLE {table_name} ADD COLUMN "{col_name}" VARCHAR(500) NOT NULL DEFAULT "{default}"') + sa.text(f'ALTER TABLE {table_name} ADD COLUMN "{col_name}" {sql_type} NOT NULL DEFAULT "{default}"') ) print(f" ✓ Added {table_name}.{col_name}") diff --git a/backend/app/models.py b/backend/app/models.py index 0c4b842..8de3ac1 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -98,6 +98,11 @@ class StationConfig(Base): stream_url = Column(String(500), nullable=False, default="") stream_metadata_url = Column(String(500), nullable=False, default="") + # Mobile app download links + play_store_icon_url = Column(String(500), nullable=False, default="") + play_store_url = Column(String(500), nullable=False, default="") + app_store_embed_html = Column(Text, nullable=False, default="") + class HistoryEntry(Base): __tablename__ = "history_entries" diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 379102a..f8515ae 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -75,6 +75,9 @@ class StationConfigResponse(BaseModel): donation_url: str stream_url: str stream_metadata_url: str + play_store_icon_url: str + play_store_url: str + app_store_embed_html: str model_config = {"from_attributes": True} @@ -99,6 +102,9 @@ class StationConfigUpdate(BaseModel): donation_url: Optional[str] = None stream_url: Optional[str] = None stream_metadata_url: Optional[str] = None + play_store_icon_url: Optional[str] = None + play_store_url: Optional[str] = None + app_store_embed_html: Optional[str] = None # ── HistoryEntry ────────────────────────────────────────── diff --git a/backend/seed.py b/backend/seed.py index f2a6f54..1b29a51 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -214,6 +214,9 @@ STATION_CONFIG: dict = { "donation_url": "", "stream_url": "", "stream_metadata_url": "", + "play_store_icon_url": "", + "play_store_url": "", + "app_store_embed_html": "", } HISTORY_ENTRIES: list[dict] = [ diff --git a/src/app/admin/admin-station-form.component.html b/src/app/admin/admin-station-form.component.html index f7a85b7..3232d46 100644 --- a/src/app/admin/admin-station-form.component.html +++ b/src/app/admin/admin-station-form.component.html @@ -154,6 +154,36 @@ + +
+

Mobile App

+ +

Google Play Store

+
+ +
+ + +
+
+
+ + +
+ +

Apple App Store

+
+ + +
+
+
+
+ Play Store URL + {{ config.play_store_url || '(not set)' }} +
+
+ App Store Embed + {{ config.app_store_embed_html ? 'Configured' : '(not set)' }} +
} diff --git a/src/app/hero/hero.component.html b/src/app/hero/hero.component.html index 329a888..8969442 100644 --- a/src/app/hero/hero.component.html +++ b/src/app/hero/hero.component.html @@ -30,6 +30,25 @@ } View Schedule + @if (config().play_store_url || config().app_store_embed_html) { +
+ Download our app +
+ @if (config().play_store_url) { + + @if (config().play_store_icon_url) { + Get it on Google Play + } @else { + Google Play + } + + } + @if (config().app_store_embed_html) { +
+ } +
+
+ }
Live on-air now — Stream free worldwide diff --git a/src/app/hero/hero.component.scss b/src/app/hero/hero.component.scss index 90479b7..3348068 100644 --- a/src/app/hero/hero.component.scss +++ b/src/app/hero/hero.component.scss @@ -38,7 +38,7 @@ .hero-content { position: relative; - z-index: 2; + z-index: 4; text-align: center; padding: $spacing-3xl $spacing-lg; @@ -368,6 +368,90 @@ letter-spacing: 0.03em; } +// ── Download CTAs ─────────────────────────────────────────── + +.hero-download { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-sm; + margin: $spacing-lg 0 $spacing-md; +} + +.download-label { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.65); + letter-spacing: 0.03em; +} + +.download-buttons { + display: flex; + gap: $spacing-md; + justify-content: center; + flex-wrap: wrap; +} + +// Both store badges are constrained to the same size (160x52) +.store-link { + display: inline-block; + text-decoration: none; +} + +.store-badge { + width: 160px; + height: 52px; + object-fit: contain; + background: rgba(255, 255, 255, 0.1); + border-radius: $radius-sm; + padding: 4px; + transition: opacity $transition-fast; + + &:hover { + opacity: 0.85; + } +} + +.store-text { + color: var(--color-white); + text-decoration: underline; + font-size: 0.95rem; + padding: $spacing-xs $spacing-sm; + border-radius: $radius-sm; + background: rgba(255, 255, 255, 0.1); + + &:hover { + background: rgba(255, 255, 255, 0.2); + } +} + +// Apple's embed HTML contains its own / — constrain it +.appstore-embed { + display: inline-flex; + align-items: center; + + // Apple's embeds typically wrap an around an + > a, + a { + display: inline-block; + text-decoration: none; + } + + img { + width: 160px; + height: 52px; + object-fit: contain; + display: block; + } + + // If the embed is an iframe (Apple's new style), constrain that too + iframe { + width: 160px; + height: 52px; + border: none; + border-radius: $radius-sm; + } +} + // ── Responsive ────────────────────────────────────────────── @media (max-width: #{$bp-md - 1px}) { diff --git a/src/app/interfaces/station-config.ts b/src/app/interfaces/station-config.ts index 2dd09a2..19413ee 100644 --- a/src/app/interfaces/station-config.ts +++ b/src/app/interfaces/station-config.ts @@ -20,4 +20,7 @@ export interface StationConfig { donation_url: string; stream_url: string; stream_metadata_url: string; + play_store_icon_url: string; + play_store_url: string; + app_store_embed_html: string; } diff --git a/src/app/services/station-config.service.ts b/src/app/services/station-config.service.ts index 1fd612b..11b19da 100644 --- a/src/app/services/station-config.service.ts +++ b/src/app/services/station-config.service.ts @@ -28,6 +28,9 @@ const DEFAULT_CONFIG: StationConfig = { donation_url: '', stream_url: '', stream_metadata_url: '', + play_store_icon_url: '', + play_store_url: '', + app_store_embed_html: '', }; @Injectable({