new app store links feature
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12
-1
@@ -23,6 +23,9 @@ def _migrate_add_missing_columns(sync_conn):
|
|||||||
pending = [
|
pending = [
|
||||||
("station_config", "stream_url", sa.String(500), ""),
|
("station_config", "stream_url", sa.String(500), ""),
|
||||||
("station_config", "stream_metadata_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
|
# Determine dialect
|
||||||
@@ -38,13 +41,21 @@ def _migrate_add_missing_columns(sync_conn):
|
|||||||
|
|
||||||
print(f" → Migrating: adding column {table_name}.{col_name}")
|
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":
|
if dialect == "sqlite":
|
||||||
sync_conn.execute(
|
sync_conn.execute(
|
||||||
sa.text(f'ALTER TABLE {table_name} ADD COLUMN "{col_name}" TEXT NOT NULL DEFAULT "{default}"')
|
sa.text(f'ALTER TABLE {table_name} ADD COLUMN "{col_name}" TEXT NOT NULL DEFAULT "{default}"')
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sync_conn.execute(
|
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}")
|
print(f" ✓ Added {table_name}.{col_name}")
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ class StationConfig(Base):
|
|||||||
stream_url = Column(String(500), nullable=False, default="")
|
stream_url = Column(String(500), nullable=False, default="")
|
||||||
stream_metadata_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):
|
class HistoryEntry(Base):
|
||||||
__tablename__ = "history_entries"
|
__tablename__ = "history_entries"
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ class StationConfigResponse(BaseModel):
|
|||||||
donation_url: str
|
donation_url: str
|
||||||
stream_url: str
|
stream_url: str
|
||||||
stream_metadata_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}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
@@ -99,6 +102,9 @@ class StationConfigUpdate(BaseModel):
|
|||||||
donation_url: Optional[str] = None
|
donation_url: Optional[str] = None
|
||||||
stream_url: Optional[str] = None
|
stream_url: Optional[str] = None
|
||||||
stream_metadata_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 ──────────────────────────────────────────
|
# ── HistoryEntry ──────────────────────────────────────────
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ STATION_CONFIG: dict = {
|
|||||||
"donation_url": "",
|
"donation_url": "",
|
||||||
"stream_url": "",
|
"stream_url": "",
|
||||||
"stream_metadata_url": "",
|
"stream_metadata_url": "",
|
||||||
|
"play_store_icon_url": "",
|
||||||
|
"play_store_url": "",
|
||||||
|
"app_store_embed_html": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
HISTORY_ENTRIES: list[dict] = [
|
HISTORY_ENTRIES: list[dict] = [
|
||||||
|
|||||||
@@ -154,6 +154,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile App -->
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Mobile App</h3>
|
||||||
|
|
||||||
|
<h4>Google Play Store</h4>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="play_store_icon_url">Play Store Badge (PNG)</label>
|
||||||
|
<div class="url-with-upload">
|
||||||
|
<input id="play_store_icon_url" type="text" [(ngModel)]="play_store_icon_url" name="play_store_icon_url"
|
||||||
|
placeholder="uploads/…">
|
||||||
|
<label class="btn btn-upload">
|
||||||
|
Upload
|
||||||
|
<input type="file" accept="image/png" (change)="onImageUpload($event, 'play_store_icon_url')" [disabled]="loading">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="play_store_url">Play Store URL</label>
|
||||||
|
<input id="play_store_url" type="url" [(ngModel)]="play_store_url" name="play_store_url"
|
||||||
|
placeholder="https://play.google.com/store/apps/details?id=…">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Apple App Store</h4>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="app_store_embed_html">App Store Embed HTML</label>
|
||||||
|
<textarea id="app_store_embed_html" [(ngModel)]="app_store_embed_html" name="app_store_embed_html"
|
||||||
|
rows="6" placeholder="Paste the embed HTML from Apple Marketing Resources"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn btn-cancel" (click)="onClose()">Cancel</button>
|
<button type="button" class="btn btn-cancel" (click)="onClose()">Cancel</button>
|
||||||
<button type="submit" class="btn btn-save" [disabled]="saving">
|
<button type="submit" class="btn btn-save" [disabled]="saving">
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export class AdminStationFormComponent implements OnChanges {
|
|||||||
donation_url = '';
|
donation_url = '';
|
||||||
stream_url = '';
|
stream_url = '';
|
||||||
stream_metadata_url = '';
|
stream_metadata_url = '';
|
||||||
|
play_store_icon_url = '';
|
||||||
|
play_store_url = '';
|
||||||
|
app_store_embed_html = '';
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
saving = false;
|
saving = false;
|
||||||
@@ -73,6 +76,9 @@ export class AdminStationFormComponent implements OnChanges {
|
|||||||
this.donation_url = config.donation_url;
|
this.donation_url = config.donation_url;
|
||||||
this.stream_url = config.stream_url;
|
this.stream_url = config.stream_url;
|
||||||
this.stream_metadata_url = config.stream_metadata_url;
|
this.stream_metadata_url = config.stream_metadata_url;
|
||||||
|
this.play_store_icon_url = config.play_store_icon_url;
|
||||||
|
this.play_store_url = config.play_store_url;
|
||||||
|
this.app_store_embed_html = config.app_store_embed_html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset form with current service values. */
|
/** Reset form with current service values. */
|
||||||
@@ -161,6 +167,9 @@ export class AdminStationFormComponent implements OnChanges {
|
|||||||
donation_url: this.donation_url,
|
donation_url: this.donation_url,
|
||||||
stream_url: this.stream_url,
|
stream_url: this.stream_url,
|
||||||
stream_metadata_url: this.stream_metadata_url,
|
stream_metadata_url: this.stream_metadata_url,
|
||||||
|
play_store_icon_url: this.play_store_icon_url,
|
||||||
|
play_store_url: this.play_store_url,
|
||||||
|
app_store_embed_html: this.app_store_embed_html,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -300,6 +300,14 @@
|
|||||||
<span class="config-label">Donation URL</span>
|
<span class="config-label">Donation URL</span>
|
||||||
<span class="config-value">{{ config.donation_url || '(not set)' }}</span>
|
<span class="config-value">{{ config.donation_url || '(not set)' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<span class="config-label">Play Store URL</span>
|
||||||
|
<span class="config-value">{{ config.play_store_url || '(not set)' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<span class="config-label">App Store Embed</span>
|
||||||
|
<span class="config-value">{{ config.app_store_embed_html ? 'Configured' : '(not set)' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,25 @@
|
|||||||
}
|
}
|
||||||
<a routerLink="/schedule" class="btn btn-hero-secondary">View Schedule</a>
|
<a routerLink="/schedule" class="btn btn-hero-secondary">View Schedule</a>
|
||||||
</div>
|
</div>
|
||||||
|
@if (config().play_store_url || config().app_store_embed_html) {
|
||||||
|
<div class="hero-download">
|
||||||
|
<span class="download-label">Download our app</span>
|
||||||
|
<div class="download-buttons">
|
||||||
|
@if (config().play_store_url) {
|
||||||
|
<a [href]="config().play_store_url" target="_blank" rel="noopener noreferrer" class="store-link">
|
||||||
|
@if (config().play_store_icon_url) {
|
||||||
|
<img [src]="resolveImageUrl(config().play_store_icon_url)" alt="Get it on Google Play" class="store-badge">
|
||||||
|
} @else {
|
||||||
|
<span class="store-text">Google Play</span>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (config().app_store_embed_html) {
|
||||||
|
<div class="appstore-embed" [innerHTML]="config().app_store_embed_html"></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="hero-listeners">
|
<div class="hero-listeners">
|
||||||
<span class="listeners-dot"></span>
|
<span class="listeners-dot"></span>
|
||||||
<span class="listeners-text">Live on-air now — Stream free worldwide</span>
|
<span class="listeners-text">Live on-air now — Stream free worldwide</span>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
.hero-content {
|
.hero-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 4;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $spacing-3xl $spacing-lg;
|
padding: $spacing-3xl $spacing-lg;
|
||||||
|
|
||||||
@@ -368,6 +368,90 @@
|
|||||||
letter-spacing: 0.03em;
|
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 <a>/<img> — constrain it
|
||||||
|
.appstore-embed {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
// Apple's embeds typically wrap an <a> around an <img>
|
||||||
|
> 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 ──────────────────────────────────────────────
|
// ── Responsive ──────────────────────────────────────────────
|
||||||
|
|
||||||
@media (max-width: #{$bp-md - 1px}) {
|
@media (max-width: #{$bp-md - 1px}) {
|
||||||
|
|||||||
@@ -20,4 +20,7 @@ export interface StationConfig {
|
|||||||
donation_url: string;
|
donation_url: string;
|
||||||
stream_url: string;
|
stream_url: string;
|
||||||
stream_metadata_url: string;
|
stream_metadata_url: string;
|
||||||
|
play_store_icon_url: string;
|
||||||
|
play_store_url: string;
|
||||||
|
app_store_embed_html: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const DEFAULT_CONFIG: StationConfig = {
|
|||||||
donation_url: '',
|
donation_url: '',
|
||||||
stream_url: '',
|
stream_url: '',
|
||||||
stream_metadata_url: '',
|
stream_metadata_url: '',
|
||||||
|
play_store_icon_url: '',
|
||||||
|
play_store_url: '',
|
||||||
|
app_store_embed_html: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
|||||||
Reference in New Issue
Block a user