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 @@
+
+
+
+
+ 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) {
+
+ }
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({