Compare commits

..
8 Commits
9 changed files with 231 additions and 77 deletions
+1 -3
View File
@@ -6,12 +6,10 @@
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
"vscjava.vscode-java-pack",
"ms-vscode.vscode-typescript-next" "ms-vscode.vscode-typescript-next"
], ],
"settings": { "settings": {
"terminal.integrated.defaultProfile.linux": "bash", "terminal.integrated.defaultProfile.linux": "bash"
"java.home": "/usr/lib/jvm/java-17-openjdk-amd64"
} }
} }
}, },
+1
View File
@@ -11,6 +11,7 @@
<activity <activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:screenOrientation="portrait"
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/title_activity_main" android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch" android:theme="@style/AppTheme.NoActionBarLaunch"
+36
View File
@@ -0,0 +1,36 @@
type RadioControlsProps = {
status: string;
isPlaying: boolean;
onPlay: () => void | Promise<void>;
onStop: () => void;
};
export function RadioControls({ status, isPlaying, onPlay, onStop }: RadioControlsProps) {
return (
<section className="radio-controls" aria-label="Radio playback controls">
<p className="radio-controls__status" aria-live="polite">
{status}
</p>
<div className="radio-controls__buttons">
<button
type="button"
className="playpause"
onClick={() => void onPlay()}
disabled={isPlaying}
aria-label="Start live stream"
>
</button>
<button
type="button"
className="playpause"
onClick={onStop}
disabled={!isPlaying}
aria-label="Stop live stream"
>
</button>
</div>
</section>
);
}
+27
View File
@@ -0,0 +1,27 @@
type WebsitePanelProps = {
label: string;
url: string;
};
export function WebsitePanel({ label, url }: WebsitePanelProps) {
return (
<section className="website-panel" aria-label={label}>
<header className="website-panel__header">
<p className="website-panel__label">{label}</p>
<p className="website-panel__url" title={url}>
{url}
</p>
</header>
<div className="website-panel__content">
<iframe
className="website-panel__frame"
src={url}
title={label}
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
/>
</div>
</section>
);
}
+15
View File
@@ -0,0 +1,15 @@
export type SiteConfig = {
id: string;
label: string;
url: string;
};
export const SITES: SiteConfig[] = [
{
id: 'kryz-home',
label: 'Station Website',
url: 'https://kryzradio.org/',
},
];
export const ACTIVE_SITE = SITES[0];
+133 -51
View File
@@ -1,12 +1,19 @@
:root { :root {
--bg-1: #f8f2e6; --bg-1: #f8f2e6;
--bg-2: #dce9f4; --bg-2: #dce9f4;
--card: #fff9f0; --panel: #f9f8f3;
--panel-line: #d8d1c4;
--ink: #1f2633; --ink: #1f2633;
--muted: #586173; --muted: #586173;
--primary: #0a5f94; --primary: #0a5f94;
--primary-press: #084d79; --primary-press: #084d79;
--disabled: #9fa9b8; --disabled: #9fa9b8;
--white: #ffffff;
--shadow-base: #12324a;
--border-dark: #1c293e;
--controls-height: 10svh;
--controls-safe: env(safe-area-inset-bottom);
--controls-total-height: calc(var(--controls-height) + var(--controls-safe));
} }
* { * {
@@ -16,70 +23,124 @@
html, html,
body { body {
margin: 0; margin: 0;
min-height: 100%; height: 100%;
} }
body { body {
color: var(--ink); color: var(--ink);
font-family: 'Trebuchet MS', 'Avenir Next', 'Segoe UI', sans-serif; font-family: 'Trebuchet MS', 'Avenir Next', 'Segoe UI', sans-serif;
background: background:
radial-gradient(circle at 16% 18%, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 44%), radial-gradient(circle at 16% 18%, color-mix(in srgb, var(--white) 85%, transparent) 0%, transparent 44%),
linear-gradient(140deg, var(--bg-1), var(--bg-2)); linear-gradient(140deg, var(--bg-1), var(--bg-2));
overflow: hidden;
} }
.page-shell { .home-layout {
min-height: 100vh; position: relative;
display: grid; height: 100svh;
place-items: center; width: 100%;
padding: 1.25rem; padding: 1rem;
padding-bottom: calc(var(--controls-total-height) + 1rem);
overflow: hidden;
} }
.player-card { .website-zone {
width: min(30rem, 100%); height: 100%;
border-radius: 1.25rem; min-height: 0;
background: var(--card); }
.website-panel {
height: 100%;
border-radius: 1rem;
border: 1px solid var(--panel-line);
background: var(--panel);
box-shadow: box-shadow:
0 1.5rem 3rem rgba(18, 50, 73, 0.17), 0 1.2rem 2.2rem color-mix(in srgb, var(--shadow-base) 12%, transparent),
inset 0 0.1rem 0.25rem rgba(255, 255, 255, 0.6); inset 0 0.1rem 0.2rem color-mix(in srgb, var(--white) 70%, transparent);
padding: 1.4rem; display: grid;
text-align: center; grid-template-rows: auto 1fr;
animation: card-enter 420ms ease-out; overflow: hidden;
} }
.eyebrow { .website-panel__header {
border-bottom: 1px solid var(--panel-line);
padding: 0.75rem 1rem;
background: linear-gradient(180deg, color-mix(in srgb, var(--white) 70%, transparent), color-mix(in srgb, var(--bg-1) 90%, transparent));
}
.website-panel__label {
margin: 0; margin: 0;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.14em; letter-spacing: 0.12em;
font-size: 0.68rem;
font-weight: 700; font-weight: 700;
font-size: 0.72rem;
color: var(--muted); color: var(--muted);
} }
h1 { .website-panel__url {
margin: 0.4rem 0 0.6rem; margin: 0.25rem 0 0;
font-size: clamp(2rem, 8vw, 2.8rem); font-size: clamp(0.82rem, 2.2vw, 0.96rem);
line-height: 1.1; color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.status { .website-panel__content {
margin: 0 0 1.2rem; min-height: 0;
color: var(--muted);
font-size: 1rem;
min-height: 1.3rem;
} }
.controls { .website-panel__frame {
width: 100%;
height: 100%;
border: none;
background: var(--white);
}
.radio-controls-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: var(--controls-total-height);
padding: 0.5rem 1rem;
padding-bottom: calc(0.5rem + var(--controls-safe));
border-top: 1px solid color-mix(in srgb, var(--border-dark) 16%, transparent);
background: linear-gradient(180deg, color-mix(in srgb, var(--bg-1) 95%, transparent), color-mix(in srgb, var(--bg-1) 98%, var(--panel-line)));
backdrop-filter: blur(7px);
}
.radio-controls {
height: 100%;
max-width: 48rem;
margin: 0 auto;
display: grid; display: grid;
gap: 0.75rem; gap: 0.55rem;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr auto;
align-items: center;
} }
button { .radio-controls__status {
margin: 0;
color: var(--muted);
font-size: clamp(0.8rem, 2.2vw, 1rem);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.radio-controls__buttons {
display: grid;
grid-auto-flow: column;
gap: 0.6rem;
}
.radio-controls button {
appearance: none; appearance: none;
border: none; border: none;
border-radius: 0.7rem; border-radius: 0.7rem;
background: var(--primary); background: var(--primary);
color: #ffffff; color: var(--white);
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
padding: 0.78rem 1rem; padding: 0.78rem 1rem;
@@ -87,39 +148,60 @@ button {
transition: transform 120ms ease, background 120ms ease; transition: transform 120ms ease, background 120ms ease;
} }
button.playpause { .radio-controls button.playpause {
width: 100%; width: clamp(2.9rem, 9vw, 4rem);
aspect-ratio: 1; aspect-ratio: 1 / 1;
font-size: clamp(2rem, 25vw, 6em); font-size: clamp(1.15rem, 4vw, 1.7rem);
padding: 0; padding: 0;
} }
button:hover:not(:disabled) { .radio-controls button:hover:not(:disabled) {
background: var(--primary-press); background: var(--primary-press);
} }
button:active:not(:disabled) { .radio-controls button:active:not(:disabled) {
transform: translateY(1px); transform: translateY(1px);
} }
button:disabled { .radio-controls button:disabled {
cursor: not-allowed; cursor: not-allowed;
background: var(--disabled); background: var(--disabled);
} }
@keyframes card-enter { @media (prefers-color-scheme: dark) {
from { :root {
opacity: 0; --bg-1: #0d1420;
transform: translateY(10px); --bg-2: #111c2e;
} --panel: #16202f;
to { --panel-line: #253043;
opacity: 1; --ink: #dde4f0;
transform: translateY(0); --muted: #8fb2eb;
--primary: #2d8fd6;
--primary-press: #237bbf;
--disabled: #34455c;
--white: #929292;
--shadow-base: #020507;
--border-dark: #060d18;
} }
} }
@media (max-width: 560px) { @media (max-width: 560px) {
.controls { .home-layout {
grid-template-columns: 1fr; padding: 0.65rem;
padding-bottom: calc(var(--controls-total-height) + 0.65rem);
}
.radio-controls-wrap {
padding-left: 0.65rem;
padding-right: 0.65rem;
}
.radio-controls {
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.5rem;
}
.radio-controls__buttons {
justify-content: end;
} }
} }
+1 -1
View File
@@ -16,7 +16,7 @@ export default function RootLayout({
<head> <head>
<meta <meta
httpEquiv="Content-Security-Policy" httpEquiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; media-src 'self' https://kryz.out.airtime.pro" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; media-src 'self' https://kryz.out.airtime.pro; frame-src 'self' https:; child-src 'self' https:"
/> />
</head> </head>
<body>{children}</body> <body>{children}</body>
+14 -15
View File
@@ -2,6 +2,9 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { AudioPlayer, CapacitorWindow, STREAM_URL } from './AudioPlayer'; import { AudioPlayer, CapacitorWindow, STREAM_URL } from './AudioPlayer';
import { RadioControls } from './components/RadioControls';
import { WebsitePanel } from './components/WebsitePanel';
import { ACTIVE_SITE } from './config/sites';
export default function HomePage() { export default function HomePage() {
const audioRef = useRef<HTMLAudioElement | null>(null); const audioRef = useRef<HTMLAudioElement | null>(null);
@@ -39,25 +42,21 @@ export default function HomePage() {
}, [player, backgroundMode]); }, [player, backgroundMode]);
return ( return (
<main className="page-shell"> <main className="home-layout">
<section className="player-card" aria-label="KRYZ live stream player"> <section className="website-zone">
<p className="eyebrow">KRYZ Go!</p> <WebsitePanel label={ACTIVE_SITE.label} url={ACTIVE_SITE.url} />
<h1>Live Radio</h1> </section>
<p className="status" aria-live="polite">
{status}
</p>
<div className="controls"> <div className="radio-controls-wrap">
<button type="button" className='playpause' onClick={() => void player.play(backgroundMode)} disabled={isPlaying}> <RadioControls
status={status}
</button> isPlaying={isPlaying}
<button type="button" className='playpause' onClick={() => player.stop()} disabled={!isPlaying}> onPlay={() => player.play(backgroundMode)}
onStop={() => player.stop()}
</button> />
</div> </div>
<audio ref={audioRef} preload="none" /> <audio ref={audioRef} preload="none" />
</section>
</main> </main>
); );
} }
-4
View File
@@ -33,15 +33,11 @@
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true/> <true/>