From dcae0b604b526c58f2add7ac52323d35687034f2 Mon Sep 17 00:00:00 2001
From: kfj001
Date: Tue, 28 Apr 2026 01:03:44 +0000
Subject: [PATCH 1/7] Transitions UI to react.js
Co-authored-by: Copilot
---
README.md | 47 +++++++++-
app/globals.css | 118 ++++++++++++++++++++++++
app/layout.tsx | 25 ++++++
app/page.tsx | 215 ++++++++++++++++++++++++++++++++++++++++++++
capacitor.config.ts | 2 +-
next-env.d.ts | 6 ++
next.config.ts | 11 +++
package.json | 15 +++-
tsconfig.json | 36 ++++++++
www/css/style.css | 61 -------------
www/index.html | 25 ------
www/js/app.js | 133 ---------------------------
12 files changed, 469 insertions(+), 225 deletions(-)
create mode 100644 app/globals.css
create mode 100644 app/layout.tsx
create mode 100644 app/page.tsx
create mode 100644 next-env.d.ts
create mode 100644 next.config.ts
create mode 100644 tsconfig.json
delete mode 100644 www/css/style.css
delete mode 100644 www/index.html
delete mode 100644 www/js/app.js
diff --git a/README.md b/README.md
index d1aa83d..070be9a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,48 @@
# kryz-go
-KRYZ Go! Mobile radio app
\ No newline at end of file
+KRYZ Go! mobile radio app built with Next.js + React and packaged with Capacitor.
+
+## Stack
+
+- Next.js (App Router, static export)
+- React
+- Capacitor (Android and iOS)
+- `@anuradev/capacitor-background-mode` for improved background playback reliability
+
+## Install
+
+```bash
+npm install
+```
+
+## Develop Frontend
+
+```bash
+npm run dev
+```
+
+## Build Web Assets For Capacitor
+
+```bash
+npm run build
+```
+
+This exports static files into `out/`, which is the Capacitor `webDir`.
+
+## Sync Native Projects
+
+```bash
+npm run sync
+```
+
+## Run Android
+
+```bash
+npm run android
+```
+
+## Open Android Studio
+
+```bash
+npm run open:android
+```
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..578df61
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,118 @@
+:root {
+ --bg-1: #f8f2e6;
+ --bg-2: #dce9f4;
+ --card: #fff9f0;
+ --ink: #1f2633;
+ --muted: #586173;
+ --primary: #0a5f94;
+ --primary-press: #084d79;
+ --disabled: #9fa9b8;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ min-height: 100%;
+}
+
+body {
+ color: var(--ink);
+ font-family: 'Trebuchet MS', 'Avenir Next', 'Segoe UI', sans-serif;
+ background:
+ radial-gradient(circle at 16% 18%, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 44%),
+ linear-gradient(140deg, var(--bg-1), var(--bg-2));
+}
+
+.page-shell {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ padding: 1.25rem;
+}
+
+.player-card {
+ width: min(30rem, 100%);
+ border-radius: 1.25rem;
+ background: var(--card);
+ box-shadow:
+ 0 1.5rem 3rem rgba(18, 50, 73, 0.17),
+ inset 0 0.1rem 0.25rem rgba(255, 255, 255, 0.6);
+ padding: 1.4rem;
+ text-align: center;
+ animation: card-enter 420ms ease-out;
+}
+
+.eyebrow {
+ margin: 0;
+ text-transform: uppercase;
+ letter-spacing: 0.14em;
+ font-weight: 700;
+ font-size: 0.72rem;
+ color: var(--muted);
+}
+
+h1 {
+ margin: 0.4rem 0 0.6rem;
+ font-size: clamp(2rem, 8vw, 2.8rem);
+ line-height: 1.1;
+}
+
+.status {
+ margin: 0 0 1.2rem;
+ color: var(--muted);
+ font-size: 1rem;
+ min-height: 1.3rem;
+}
+
+.controls {
+ display: grid;
+ gap: 0.75rem;
+ grid-template-columns: 1fr 1fr;
+}
+
+button {
+ appearance: none;
+ border: none;
+ border-radius: 0.7rem;
+ background: var(--primary);
+ color: #ffffff;
+ font-size: 1rem;
+ font-weight: 700;
+ padding: 0.78rem 1rem;
+ cursor: pointer;
+ transition: transform 120ms ease, background 120ms ease;
+}
+
+button:hover:not(:disabled) {
+ background: var(--primary-press);
+}
+
+button:active:not(:disabled) {
+ transform: translateY(1px);
+}
+
+button:disabled {
+ cursor: not-allowed;
+ background: var(--disabled);
+}
+
+@keyframes card-enter {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@media (max-width: 560px) {
+ .controls {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..a220118
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,25 @@
+import type { Metadata } from 'next';
+import './globals.css';
+
+export const metadata: Metadata = {
+ title: 'kryz-go',
+ description: 'KRYZ Go! mobile radio app',
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+ {children}
+
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..728b4d2
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,215 @@
+'use client';
+
+import { useEffect, useMemo, useRef, useState } from 'react';
+
+type PermissionState = 'granted' | 'denied' | 'prompt';
+
+type BackgroundModePlugin = {
+ checkMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
+ requestMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
+ checkNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
+ requestNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
+ checkBatteryOptimizations: () => Promise<{ enabled: boolean }>;
+ requestDisableBatteryOptimizations: () => Promise;
+ disableWebViewOptimizations: () => Promise;
+ enable: (options: {
+ title: string;
+ text: string;
+ channelName: string;
+ channelDescription: string;
+ resume: boolean;
+ silent: boolean;
+ disableWebViewOptimization: boolean;
+ }) => Promise;
+ addListener: (eventName: string, listener: () => void) => Promise;
+};
+
+type CapacitorRuntime = {
+ Plugins?: {
+ BackgroundMode?: BackgroundModePlugin;
+ };
+};
+
+type CapacitorWindow = Window & {
+ Capacitor?: CapacitorRuntime;
+};
+
+const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_b';
+
+export default function HomePage() {
+ const audioRef = useRef(null);
+ const [status, setStatus] = useState('Stream is stopped.');
+ const [isPlaying, setIsPlaying] = useState(false);
+
+ const backgroundMode = useMemo(() => {
+ if (typeof window === 'undefined') {
+ return undefined;
+ }
+ return (window as CapacitorWindow).Capacitor?.Plugins?.BackgroundMode;
+ }, []);
+
+ useEffect(() => {
+ const audio = audioRef.current;
+ if (!audio) {
+ return;
+ }
+
+ const handleEnded = () => {
+ setIsPlaying(false);
+ setStatus('Stream ended.');
+ };
+
+ const handleError = () => {
+ setIsPlaying(false);
+ setStatus('Stream error.');
+ };
+
+ audio.addEventListener('ended', handleEnded);
+ audio.addEventListener('error', handleError);
+
+ return () => {
+ audio.removeEventListener('ended', handleEnded);
+ audio.removeEventListener('error', handleError);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!backgroundMode) {
+ console.warn('BackgroundMode plugin is not available in this runtime.');
+ return;
+ }
+
+ const initializeBackgroundMode = async () => {
+ try {
+ const notificationPermission = await backgroundMode.checkNotificationsPermission();
+ if (notificationPermission.notifications !== 'granted') {
+ await backgroundMode.requestNotificationsPermission();
+ }
+
+ const microphonePermission = await backgroundMode.checkMicrophonePermission();
+ if (microphonePermission.microphone !== 'granted') {
+ const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
+ if (requestedMicrophonePermission.microphone !== 'granted') {
+ console.warn(
+ 'BackgroundMode microphone permission denied; background playback may stop when screen is off.'
+ );
+ }
+ }
+
+ const batteryOptimizations = await backgroundMode.checkBatteryOptimizations();
+ if (batteryOptimizations.enabled) {
+ console.warn(
+ 'Battery optimizations are enabled; requesting exemption for reliable background playback.'
+ );
+ await backgroundMode.requestDisableBatteryOptimizations();
+ }
+
+ await backgroundMode.disableWebViewOptimizations();
+
+ await backgroundMode.enable({
+ title: 'KRYZ-Go!',
+ text: 'Background mode is active',
+ channelName: 'kryz-go background',
+ channelDescription: 'Keeps kryz-go running while the app is in background',
+ resume: true,
+ silent: false,
+ disableWebViewOptimization: true,
+ });
+
+ await backgroundMode.addListener('appInBackground', () => {
+ console.log('kryz-go moved to background');
+ });
+
+ await backgroundMode.addListener('appInForeground', () => {
+ console.log('kryz-go returned to foreground');
+ });
+
+ console.log('BackgroundMode plugin enabled.');
+ } catch (error) {
+ console.error('Failed to initialize BackgroundMode plugin:', error);
+ }
+ };
+
+ void initializeBackgroundMode();
+ }, [backgroundMode]);
+
+ const ensureBackgroundPlaybackPermissions = async () => {
+ if (!backgroundMode) {
+ return true;
+ }
+
+ const microphonePermission = await backgroundMode.checkMicrophonePermission();
+ if (microphonePermission.microphone === 'granted') {
+ return true;
+ }
+
+ const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
+ if (requestedMicrophonePermission.microphone === 'granted') {
+ return true;
+ }
+
+ setStatus('Enable microphone permission in Android settings for screen-off playback.');
+ console.warn('Background playback blocked: microphone permission not granted.');
+ return false;
+ };
+
+ const startStream = async () => {
+ const audio = audioRef.current;
+ if (!audio) {
+ return;
+ }
+
+ try {
+ const hasBackgroundPermissions = await ensureBackgroundPlaybackPermissions();
+ if (!hasBackgroundPermissions) {
+ return;
+ }
+
+ audio.src = STREAM_URL;
+ await audio.play();
+ setIsPlaying(true);
+ setStatus('Stream is playing.');
+ } catch (error) {
+ setIsPlaying(false);
+ setStatus('Unable to start stream.');
+ console.error('Failed to start stream:', error);
+ }
+ };
+
+ const stopStream = () => {
+ const audio = audioRef.current;
+ if (!audio) {
+ return;
+ }
+
+ audio.pause();
+ audio.removeAttribute('src');
+ audio.load();
+
+ setIsPlaying(false);
+ setStatus('Stream is stopped.');
+ };
+
+ return (
+
+
+ KRYZ Go!
+ Live Radio
+
+ {status}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/capacitor.config.ts b/capacitor.config.ts
index f33194a..f2164a9 100644
--- a/capacitor.config.ts
+++ b/capacitor.config.ts
@@ -3,7 +3,7 @@ import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.kryzgo.app',
appName: 'kryz-go',
- webDir: 'www',
+ webDir: 'out',
android: {
adjustMarginsForEdgeToEdge: 'force',
buildOptions: {
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/next.config.ts b/next.config.ts
new file mode 100644
index 0000000..4597eb8
--- /dev/null
+++ b/next.config.ts
@@ -0,0 +1,11 @@
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {
+ output: 'export',
+ trailingSlash: true,
+ images: {
+ unoptimized: true,
+ },
+};
+
+export default nextConfig;
diff --git a/package.json b/package.json
index 5add92f..ce6a37b 100644
--- a/package.json
+++ b/package.json
@@ -3,19 +3,26 @@
"version": "1.0.0",
"description": "Capacitor mobile app",
"scripts": {
- "build": "echo 'Add your web build command here'",
- "sync": "cap sync",
- "android": "cap run android",
+ "dev": "next dev",
+ "build": "next build",
+ "sync": "npm run build && cap sync",
+ "android": "npm run sync && cap run android",
"open:android": "cap open android"
},
"dependencies": {
"@anuradev/capacitor-background-mode": "^7.2.1",
"@capacitor/android": "^7.0.0",
"@capacitor/core": "^7.0.0",
- "@capacitor/ios": "^7.0.0"
+ "@capacitor/ios": "^7.0.0",
+ "next": "^16.2.4",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5"
},
"devDependencies": {
"@capacitor/cli": "^7.0.0",
+ "@types/node": "^25.6.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
"typescript": "^6.0.3"
}
}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..79b1981
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/www/css/style.css b/www/css/style.css
deleted file mode 100644
index 96501e1..0000000
--- a/www/css/style.css
+++ /dev/null
@@ -1,61 +0,0 @@
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: #ffffff;
- color: #1a1a1a;
-}
-
-#app {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 1rem;
- min-height: 100vh;
- padding: 1rem;
- text-align: center;
-}
-
-h1 {
- font-size: 2rem;
-}
-
-#stream-status {
- font-size: 1rem;
-}
-
-.controls {
- display: flex;
- gap: 0.75rem;
-}
-
-button {
- border: none;
- border-radius: 0.5rem;
- padding: 0.75rem 1rem;
- font-size: 1rem;
- font-weight: 600;
- color: #ffffff;
- background: #1456a7;
-}
-
-button:disabled {
- background: #9da8b3;
- color: #f5f5f5;
-}
-
-@media (max-width: 480px) {
- .controls {
- width: 100%;
- flex-direction: column;
- }
-
- button {
- width: 100%;
- }
-}
diff --git a/www/index.html b/www/index.html
deleted file mode 100644
index da4e8bd..0000000
--- a/www/index.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
- kryz-go
-
-
-
-
-
kryz-go
-
Stream is stopped.
-
-
-
-
-
-
-
-
-
diff --git a/www/js/app.js b/www/js/app.js
deleted file mode 100644
index 1f6a506..0000000
--- a/www/js/app.js
+++ /dev/null
@@ -1,133 +0,0 @@
-// kryz-go app entry point
-window.addEventListener('DOMContentLoaded', () => {
- console.log('kryz-go loaded');
- initializeStreamPlayer();
- initializeBackgroundMode();
-});
-
-function initializeStreamPlayer() {
- const streamUrl = 'https://kryz.out.airtime.pro/kryz_b';
- const audio = document.getElementById('stream-player');
- const startButton = document.getElementById('start-stream');
- const stopButton = document.getElementById('stop-stream');
- const status = document.getElementById('stream-status');
-
- if (!audio || !startButton || !stopButton || !status) {
- console.error('Stream player controls are missing from the page.');
- return;
- }
-
- const setStatus = (isPlaying, text) => {
- startButton.disabled = isPlaying;
- stopButton.disabled = !isPlaying;
- status.textContent = text;
- };
-
- const ensureBackgroundPlaybackPermissions = async () => {
- const backgroundMode = window.Capacitor?.Plugins?.BackgroundMode;
-
- if (!backgroundMode) {
- return true;
- }
-
- const microphonePermission = await backgroundMode.checkMicrophonePermission();
- if (microphonePermission.microphone === 'granted') {
- return true;
- }
-
- const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
- if (requestedMicrophonePermission.microphone === 'granted') {
- return true;
- }
-
- setStatus(false, 'Enable microphone permission in Android settings for screen-off playback.');
- console.warn('Background playback blocked: microphone permission not granted.');
- return false;
- };
-
- const startStream = async () => {
- try {
- const hasBackgroundPermissions = await ensureBackgroundPlaybackPermissions();
- if (!hasBackgroundPermissions) {
- return;
- }
-
- audio.src = streamUrl;
- await audio.play();
- setStatus(true, 'Stream is playing.');
- } catch (error) {
- setStatus(false, 'Unable to start stream.');
- console.error('Failed to start stream:', error);
- }
- };
-
- const stopStream = () => {
- audio.pause();
- audio.removeAttribute('src');
- audio.load();
- setStatus(false, 'Stream is stopped.');
- };
-
- startButton.addEventListener('click', startStream);
- stopButton.addEventListener('click', stopStream);
- audio.addEventListener('ended', () => {
- setStatus(false, 'Stream ended.');
- });
- audio.addEventListener('error', () => {
- setStatus(false, 'Stream error.');
- });
-}
-
-async function initializeBackgroundMode() {
- const backgroundMode = window.Capacitor?.Plugins?.BackgroundMode;
-
- if (!backgroundMode) {
- console.warn('BackgroundMode plugin is not available in this runtime.');
- return;
- }
-
- try {
- const notificationPermission = await backgroundMode.checkNotificationsPermission();
- if (notificationPermission.notifications !== 'granted') {
- await backgroundMode.requestNotificationsPermission();
- }
-
- const microphonePermission = await backgroundMode.checkMicrophonePermission();
- if (microphonePermission.microphone !== 'granted') {
- const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
- if (requestedMicrophonePermission.microphone !== 'granted') {
- console.warn('BackgroundMode microphone permission denied; background playback may stop when screen is off.');
- }
- }
-
- const batteryOptimizations = await backgroundMode.checkBatteryOptimizations();
- if (batteryOptimizations.enabled) {
- console.warn('Battery optimizations are enabled; requesting exemption for reliable background playback.');
- await backgroundMode.requestDisableBatteryOptimizations();
- }
-
- await backgroundMode.disableWebViewOptimizations();
-
- await backgroundMode.enable({
- title: 'KRYZ-Go!',
- text: 'Background mode is active',
- channelName: 'kryz-go background',
- channelDescription: 'Keeps kryz-go running while the app is in background',
- resume: true,
- silent: false,
- disableWebViewOptimization: true,
- });
-
- await backgroundMode.addListener('appInBackground', () => {
- console.log('kryz-go moved to background');
- });
-
- await backgroundMode.addListener('appInForeground', () => {
- console.log('kryz-go returned to foreground');
- });
-
- console.log('BackgroundMode plugin enabled.');
- } catch (error) {
- console.error('Failed to initialize BackgroundMode plugin:', error);
- }
-}
--
2.54.0
From 2b0b043d3de6b612f59bb0d61d1430ece6e71d46 Mon Sep 17 00:00:00 2001
From: kfj001
Date: Tue, 28 Apr 2026 01:37:13 +0000
Subject: [PATCH 2/7] Refactors music playback and boilerplate into AudioPlayer
class
Co-authored-by: Copilot
---
app/AudioPlayer.ts | 172 ++++++++++++++++++++++++++++++++++++++++
app/page.tsx | 190 +++++----------------------------------------
2 files changed, 191 insertions(+), 171 deletions(-)
create mode 100644 app/AudioPlayer.ts
diff --git a/app/AudioPlayer.ts b/app/AudioPlayer.ts
new file mode 100644
index 0000000..e453823
--- /dev/null
+++ b/app/AudioPlayer.ts
@@ -0,0 +1,172 @@
+type PermissionState = 'granted' | 'denied' | 'prompt';
+
+export type BackgroundModePlugin = {
+ checkMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
+ requestMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
+ checkNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
+ requestNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
+ checkBatteryOptimizations: () => Promise<{ enabled: boolean }>;
+ requestDisableBatteryOptimizations: () => Promise;
+ disableWebViewOptimizations: () => Promise;
+ enable: (options: {
+ title: string;
+ text: string;
+ channelName: string;
+ channelDescription: string;
+ resume: boolean;
+ silent: boolean;
+ disableWebViewOptimization: boolean;
+ }) => Promise;
+ addListener: (eventName: string, listener: () => void) => Promise;
+};
+
+export type CapacitorRuntime = {
+ Plugins?: {
+ BackgroundMode?: BackgroundModePlugin;
+ };
+};
+
+export type CapacitorWindow = Window & {
+ Capacitor?: CapacitorRuntime;
+};
+
+export type PlaybackState = 'stopped' | 'playing' | 'error';
+
+export const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_b';
+
+export class AudioPlayer {
+ private url: string;
+ private onStateChange: (state: PlaybackState, status: string) => void;
+ private element: HTMLAudioElement | null = null;
+ private playbackState: PlaybackState = 'stopped';
+
+ constructor(url: string, onStateChange: (state: PlaybackState, status: string) => void) {
+ this.url = url;
+ this.onStateChange = onStateChange;
+ this.handleEnded = this.handleEnded.bind(this);
+ this.handleError = this.handleError.bind(this);
+ }
+
+ setElement(el: HTMLAudioElement): void {
+ this.element = el;
+ el.addEventListener('ended', this.handleEnded);
+ el.addEventListener('error', this.handleError);
+ }
+
+ private handleEnded(): void {
+ this.playbackState = 'stopped';
+ this.onStateChange('stopped', 'Stream ended.');
+ }
+
+ private handleError(): void {
+ this.playbackState = 'error';
+ this.onStateChange('error', 'Stream error.');
+ }
+
+ async initBackgroundMode(backgroundMode: BackgroundModePlugin): Promise {
+ try {
+ const notificationPermission = await backgroundMode.checkNotificationsPermission();
+ if (notificationPermission.notifications !== 'granted') {
+ await backgroundMode.requestNotificationsPermission();
+ }
+
+ const microphonePermission = await backgroundMode.checkMicrophonePermission();
+ if (microphonePermission.microphone !== 'granted') {
+ const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
+ if (requestedMicrophonePermission.microphone !== 'granted') {
+ console.warn(
+ 'BackgroundMode microphone permission denied; background playback may stop when screen is off.'
+ );
+ }
+ }
+
+ const batteryOptimizations = await backgroundMode.checkBatteryOptimizations();
+ if (batteryOptimizations.enabled) {
+ console.warn(
+ 'Battery optimizations are enabled; requesting exemption for reliable background playback.'
+ );
+ await backgroundMode.requestDisableBatteryOptimizations();
+ }
+
+ await backgroundMode.disableWebViewOptimizations();
+
+ await backgroundMode.enable({
+ title: 'KRYZ-Go!',
+ text: 'Background mode is active',
+ channelName: 'kryz-go background',
+ channelDescription: 'Keeps kryz-go running while the app is in background',
+ resume: true,
+ silent: false,
+ disableWebViewOptimization: true,
+ });
+
+ await backgroundMode.addListener('appInBackground', () => {
+ console.log('kryz-go moved to background');
+ });
+
+ await backgroundMode.addListener('appInForeground', () => {
+ console.log('kryz-go returned to foreground');
+ });
+
+ console.log('BackgroundMode plugin enabled.');
+ } catch (error) {
+ console.error('Failed to initialize BackgroundMode plugin:', error);
+ }
+ }
+
+ private async ensureBackgroundPermissions(backgroundMode: BackgroundModePlugin): Promise {
+ const microphonePermission = await backgroundMode.checkMicrophonePermission();
+ if (microphonePermission.microphone === 'granted') {
+ return true;
+ }
+
+ const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
+ if (requestedMicrophonePermission.microphone === 'granted') {
+ return true;
+ }
+
+ this.onStateChange(
+ this.playbackState,
+ 'Enable microphone permission in Android settings for screen-off playback.'
+ );
+ console.warn('Background playback blocked: microphone permission not granted.');
+ return false;
+ }
+
+ async play(backgroundMode?: BackgroundModePlugin): Promise {
+ if (!this.element) {
+ return;
+ }
+
+ try {
+ if (backgroundMode) {
+ const hasPermissions = await this.ensureBackgroundPermissions(backgroundMode);
+ if (!hasPermissions) {
+ return;
+ }
+ }
+
+ this.element.src = this.url;
+ await this.element.play();
+ this.playbackState = 'playing';
+ this.onStateChange('playing', 'Stream is playing.');
+ } catch (error) {
+ this.playbackState = 'error';
+ this.onStateChange('error', 'Unable to start stream.');
+ console.error('Failed to start stream:', error);
+ }
+ }
+
+ stop(): void {
+ if (!this.element) {
+ return;
+ }
+
+ this.element.pause();
+ this.element.removeAttribute('src');
+ this.element.load();
+
+ this.playbackState = 'stopped';
+ this.onStateChange('stopped', 'Stream is stopped.');
+ }
+}
diff --git a/app/page.tsx b/app/page.tsx
index 728b4d2..7fd79d1 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,40 +1,7 @@
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
-
-type PermissionState = 'granted' | 'denied' | 'prompt';
-
-type BackgroundModePlugin = {
- checkMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
- requestMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
- checkNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
- requestNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
- checkBatteryOptimizations: () => Promise<{ enabled: boolean }>;
- requestDisableBatteryOptimizations: () => Promise;
- disableWebViewOptimizations: () => Promise;
- enable: (options: {
- title: string;
- text: string;
- channelName: string;
- channelDescription: string;
- resume: boolean;
- silent: boolean;
- disableWebViewOptimization: boolean;
- }) => Promise;
- addListener: (eventName: string, listener: () => void) => Promise;
-};
-
-type CapacitorRuntime = {
- Plugins?: {
- BackgroundMode?: BackgroundModePlugin;
- };
-};
-
-type CapacitorWindow = Window & {
- Capacitor?: CapacitorRuntime;
-};
-
-const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_b';
+import { AudioPlayer, CapacitorWindow, STREAM_URL } from './AudioPlayer';
export default function HomePage() {
const audioRef = useRef(null);
@@ -48,147 +15,28 @@ export default function HomePage() {
return (window as CapacitorWindow).Capacitor?.Plugins?.BackgroundMode;
}, []);
+ const player = useMemo(
+ () =>
+ new AudioPlayer(STREAM_URL, (state, msg) => {
+ setIsPlaying(state === 'playing');
+ setStatus(msg);
+ }),
+ []
+ );
+
useEffect(() => {
- const audio = audioRef.current;
- if (!audio) {
- return;
+ if (audioRef.current) {
+ player.setElement(audioRef.current);
}
-
- const handleEnded = () => {
- setIsPlaying(false);
- setStatus('Stream ended.');
- };
-
- const handleError = () => {
- setIsPlaying(false);
- setStatus('Stream error.');
- };
-
- audio.addEventListener('ended', handleEnded);
- audio.addEventListener('error', handleError);
-
- return () => {
- audio.removeEventListener('ended', handleEnded);
- audio.removeEventListener('error', handleError);
- };
- }, []);
+ }, [player]);
useEffect(() => {
if (!backgroundMode) {
console.warn('BackgroundMode plugin is not available in this runtime.');
return;
}
-
- const initializeBackgroundMode = async () => {
- try {
- const notificationPermission = await backgroundMode.checkNotificationsPermission();
- if (notificationPermission.notifications !== 'granted') {
- await backgroundMode.requestNotificationsPermission();
- }
-
- const microphonePermission = await backgroundMode.checkMicrophonePermission();
- if (microphonePermission.microphone !== 'granted') {
- const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
- if (requestedMicrophonePermission.microphone !== 'granted') {
- console.warn(
- 'BackgroundMode microphone permission denied; background playback may stop when screen is off.'
- );
- }
- }
-
- const batteryOptimizations = await backgroundMode.checkBatteryOptimizations();
- if (batteryOptimizations.enabled) {
- console.warn(
- 'Battery optimizations are enabled; requesting exemption for reliable background playback.'
- );
- await backgroundMode.requestDisableBatteryOptimizations();
- }
-
- await backgroundMode.disableWebViewOptimizations();
-
- await backgroundMode.enable({
- title: 'KRYZ-Go!',
- text: 'Background mode is active',
- channelName: 'kryz-go background',
- channelDescription: 'Keeps kryz-go running while the app is in background',
- resume: true,
- silent: false,
- disableWebViewOptimization: true,
- });
-
- await backgroundMode.addListener('appInBackground', () => {
- console.log('kryz-go moved to background');
- });
-
- await backgroundMode.addListener('appInForeground', () => {
- console.log('kryz-go returned to foreground');
- });
-
- console.log('BackgroundMode plugin enabled.');
- } catch (error) {
- console.error('Failed to initialize BackgroundMode plugin:', error);
- }
- };
-
- void initializeBackgroundMode();
- }, [backgroundMode]);
-
- const ensureBackgroundPlaybackPermissions = async () => {
- if (!backgroundMode) {
- return true;
- }
-
- const microphonePermission = await backgroundMode.checkMicrophonePermission();
- if (microphonePermission.microphone === 'granted') {
- return true;
- }
-
- const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
- if (requestedMicrophonePermission.microphone === 'granted') {
- return true;
- }
-
- setStatus('Enable microphone permission in Android settings for screen-off playback.');
- console.warn('Background playback blocked: microphone permission not granted.');
- return false;
- };
-
- const startStream = async () => {
- const audio = audioRef.current;
- if (!audio) {
- return;
- }
-
- try {
- const hasBackgroundPermissions = await ensureBackgroundPlaybackPermissions();
- if (!hasBackgroundPermissions) {
- return;
- }
-
- audio.src = STREAM_URL;
- await audio.play();
- setIsPlaying(true);
- setStatus('Stream is playing.');
- } catch (error) {
- setIsPlaying(false);
- setStatus('Unable to start stream.');
- console.error('Failed to start stream:', error);
- }
- };
-
- const stopStream = () => {
- const audio = audioRef.current;
- if (!audio) {
- return;
- }
-
- audio.pause();
- audio.removeAttribute('src');
- audio.load();
-
- setIsPlaying(false);
- setStatus('Stream is stopped.');
- };
+ void player.initBackgroundMode(backgroundMode);
+ }, [player, backgroundMode]);
return (
@@ -200,11 +48,11 @@ export default function HomePage() {
-
--
2.54.0
From 4bbcc7d537ba43bb0ba597534b91ab6aaf20e061 Mon Sep 17 00:00:00 2001
From: kfj001
Date: Tue, 28 Apr 2026 02:22:25 +0000
Subject: [PATCH 3/7] Replaces words with emoji. Formatting enhancements for
play/pause buttons. Switched to lower fidelity audio stream for better
performance.
---
app/AudioPlayer.ts | 2 +-
app/globals.css | 7 +++++++
app/page.tsx | 8 ++++----
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/app/AudioPlayer.ts b/app/AudioPlayer.ts
index e453823..201975e 100644
--- a/app/AudioPlayer.ts
+++ b/app/AudioPlayer.ts
@@ -32,7 +32,7 @@ export type CapacitorWindow = Window & {
export type PlaybackState = 'stopped' | 'playing' | 'error';
-export const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_b';
+export const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_a';
export class AudioPlayer {
private url: string;
diff --git a/app/globals.css b/app/globals.css
index 578df61..63cd5b2 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -87,6 +87,13 @@ button {
transition: transform 120ms ease, background 120ms ease;
}
+button.playpause {
+ width: 100%;
+ aspect-ratio: 1;
+ font-size: clamp(2rem, 25vw, 6em);
+ padding: 0;
+}
+
button:hover:not(:disabled) {
background: var(--primary-press);
}
diff --git a/app/page.tsx b/app/page.tsx
index 7fd79d1..111066b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -48,11 +48,11 @@ export default function HomePage() {
- void player.play(backgroundMode)} disabled={isPlaying}>
- Play
+ void player.play(backgroundMode)} disabled={isPlaying}>
+ ▶️
- player.stop()} disabled={!isPlaying}>
- Stop
+ player.stop()} disabled={!isPlaying}>
+ ⏹️
--
2.54.0
From eab0abf800697e35f6e20535923f657713fb69db Mon Sep 17 00:00:00 2001
From: kfj001
Date: Tue, 28 Apr 2026 02:41:47 +0000
Subject: [PATCH 4/7] New redesign
Co-authored-by: Copilot
---
app/components/RadioControls.tsx | 36 +++++++
app/components/WebsitePanel.tsx | 27 ++++++
app/config/sites.ts | 15 +++
app/globals.css | 161 +++++++++++++++++++++----------
app/layout.tsx | 2 +-
app/page.tsx | 35 ++++---
next-env.d.ts | 2 +-
7 files changed, 208 insertions(+), 70 deletions(-)
create mode 100644 app/components/RadioControls.tsx
create mode 100644 app/components/WebsitePanel.tsx
create mode 100644 app/config/sites.ts
diff --git a/app/components/RadioControls.tsx b/app/components/RadioControls.tsx
new file mode 100644
index 0000000..c54f5ce
--- /dev/null
+++ b/app/components/RadioControls.tsx
@@ -0,0 +1,36 @@
+type RadioControlsProps = {
+ status: string;
+ isPlaying: boolean;
+ onPlay: () => void | Promise;
+ onStop: () => void;
+};
+
+export function RadioControls({ status, isPlaying, onPlay, onStop }: RadioControlsProps) {
+ return (
+
+
+ {status}
+
+
+ void onPlay()}
+ disabled={isPlaying}
+ aria-label="Start live stream"
+ >
+ ▶️
+
+
+ ⏹️
+
+
+
+ );
+}
diff --git a/app/components/WebsitePanel.tsx b/app/components/WebsitePanel.tsx
new file mode 100644
index 0000000..4cb0906
--- /dev/null
+++ b/app/components/WebsitePanel.tsx
@@ -0,0 +1,27 @@
+type WebsitePanelProps = {
+ label: string;
+ url: string;
+};
+
+export function WebsitePanel({ label, url }: WebsitePanelProps) {
+ return (
+
+ );
+}
diff --git a/app/config/sites.ts b/app/config/sites.ts
new file mode 100644
index 0000000..c25bfdc
--- /dev/null
+++ b/app/config/sites.ts
@@ -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];
diff --git a/app/globals.css b/app/globals.css
index 63cd5b2..ab30a0c 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -2,11 +2,14 @@
--bg-1: #f8f2e6;
--bg-2: #dce9f4;
--card: #fff9f0;
+ --panel: #f9f8f3;
+ --panel-line: #d8d1c4;
--ink: #1f2633;
--muted: #586173;
--primary: #0a5f94;
--primary-press: #084d79;
--disabled: #9fa9b8;
+ --controls-height: 10dvh;
}
* {
@@ -16,7 +19,7 @@
html,
body {
margin: 0;
- min-height: 100%;
+ height: 100%;
}
body {
@@ -25,56 +28,106 @@ body {
background:
radial-gradient(circle at 16% 18%, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 44%),
linear-gradient(140deg, var(--bg-1), var(--bg-2));
+ overflow: hidden;
}
-.page-shell {
- min-height: 100vh;
- display: grid;
- place-items: center;
- padding: 1.25rem;
+.home-layout {
+ height: 100dvh;
+ width: 100%;
+ padding: 1rem;
+ padding-bottom: calc(var(--controls-height) + 1rem);
}
-.player-card {
- width: min(30rem, 100%);
- border-radius: 1.25rem;
- background: var(--card);
+.website-zone {
+ height: calc(100dvh - var(--controls-height) - 1rem);
+ max-height: 90dvh;
+ min-height: 0;
+}
+
+.website-panel {
+ height: 100%;
+ border-radius: 1rem;
+ border: 1px solid var(--panel-line);
+ background: var(--panel);
box-shadow:
- 0 1.5rem 3rem rgba(18, 50, 73, 0.17),
- inset 0 0.1rem 0.25rem rgba(255, 255, 255, 0.6);
- padding: 1.4rem;
- text-align: center;
- animation: card-enter 420ms ease-out;
+ 0 1.2rem 2.2rem rgba(18, 50, 73, 0.12),
+ inset 0 0.1rem 0.2rem rgba(255, 255, 255, 0.7);
+ display: grid;
+ grid-template-rows: auto 1fr;
+ overflow: hidden;
}
-.eyebrow {
+.website-panel__header {
+ border-bottom: 1px solid var(--panel-line);
+ padding: 0.75rem 1rem;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(247, 242, 233, 0.9));
+}
+
+.website-panel__label {
margin: 0;
text-transform: uppercase;
- letter-spacing: 0.14em;
+ letter-spacing: 0.12em;
+ font-size: 0.68rem;
font-weight: 700;
- font-size: 0.72rem;
color: var(--muted);
}
-h1 {
- margin: 0.4rem 0 0.6rem;
- font-size: clamp(2rem, 8vw, 2.8rem);
- line-height: 1.1;
+.website-panel__url {
+ margin: 0.25rem 0 0;
+ font-size: clamp(0.82rem, 2.2vw, 0.96rem);
+ color: var(--ink);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.status {
- margin: 0 0 1.2rem;
- color: var(--muted);
- font-size: 1rem;
- min-height: 1.3rem;
+.website-panel__content {
+ min-height: 0;
}
-.controls {
+.website-panel__frame {
+ width: 100%;
+ height: 100%;
+ border: none;
+ background: #ffffff;
+}
+
+.radio-controls-wrap {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: var(--controls-height);
+ padding: 0.5rem 1rem;
+ padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));
+ border-top: 1px solid rgba(28, 41, 62, 0.16);
+ background: linear-gradient(180deg, rgba(255, 250, 242, 0.95), rgba(239, 235, 225, 0.98));
+ backdrop-filter: blur(7px);
+}
+
+.radio-controls {
+ height: 100%;
+ max-width: 48rem;
+ margin: 0 auto;
display: grid;
- gap: 0.75rem;
- grid-template-columns: 1fr 1fr;
+ gap: 0.55rem;
+ 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);
+}
+
+.radio-controls__buttons {
+ display: grid;
+ grid-auto-flow: column;
+ gap: 0.6rem;
+}
+
+.radio-controls button {
appearance: none;
border: none;
border-radius: 0.7rem;
@@ -87,39 +140,47 @@ button {
transition: transform 120ms ease, background 120ms ease;
}
-button.playpause {
- width: 100%;
- aspect-ratio: 1;
- font-size: clamp(2rem, 25vw, 6em);
+.radio-controls button.playpause {
+ width: clamp(2.9rem, 9vw, 4rem);
+ aspect-ratio: 1 / 1;
+ font-size: clamp(1.15rem, 4vw, 1.7rem);
padding: 0;
}
-button:hover:not(:disabled) {
+.radio-controls button:hover:not(:disabled) {
background: var(--primary-press);
}
-button:active:not(:disabled) {
+.radio-controls button:active:not(:disabled) {
transform: translateY(1px);
}
-button:disabled {
+.radio-controls button:disabled {
cursor: not-allowed;
background: var(--disabled);
}
-@keyframes card-enter {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
@media (max-width: 560px) {
- .controls {
+ .home-layout {
+ padding: 0.65rem;
+ padding-bottom: calc(var(--controls-height) + 0.65rem);
+ }
+
+ .website-zone {
+ height: calc(100dvh - var(--controls-height) - 0.65rem);
+ }
+
+ .radio-controls-wrap {
+ padding-left: 0.65rem;
+ padding-right: 0.65rem;
+ }
+
+ .radio-controls {
grid-template-columns: 1fr;
+ gap: 0.35rem;
+ }
+
+ .radio-controls__buttons {
+ justify-content: start;
}
}
diff --git a/app/layout.tsx b/app/layout.tsx
index a220118..d097d45 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -16,7 +16,7 @@ export default function RootLayout({
{children}
diff --git a/app/page.tsx b/app/page.tsx
index 111066b..d51e2b7 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -2,6 +2,9 @@
import { useEffect, useMemo, useRef, useState } from 'react';
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() {
const audioRef = useRef(null);
@@ -39,25 +42,21 @@ export default function HomePage() {
}, [player, backgroundMode]);
return (
-
-
- KRYZ Go!
- Live Radio
-
- {status}
-
-
-
- void player.play(backgroundMode)} disabled={isPlaying}>
- ▶️
-
- player.stop()} disabled={!isPlaying}>
- ⏹️
-
-
-
-
+
+
+
+
+ player.play(backgroundMode)}
+ onStop={() => player.stop()}
+ />
+
+
+
);
}
diff --git a/next-env.d.ts b/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
--
2.54.0
From 695d3ded97ccdd4c4eef8226006c376b6283c5b4 Mon Sep 17 00:00:00 2001
From: kfj001
Date: Tue, 28 Apr 2026 02:58:28 +0000
Subject: [PATCH 5/7] css changes to ensure lower media controls don't end up
cut off
Co-authored-by: Copilot
---
app/globals.css | 33 +++++++++++++++++----------------
1 file changed, 17 insertions(+), 16 deletions(-)
diff --git a/app/globals.css b/app/globals.css
index ab30a0c..7438c38 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,7 +1,6 @@
:root {
--bg-1: #f8f2e6;
--bg-2: #dce9f4;
- --card: #fff9f0;
--panel: #f9f8f3;
--panel-line: #d8d1c4;
--ink: #1f2633;
@@ -9,7 +8,9 @@
--primary: #0a5f94;
--primary-press: #084d79;
--disabled: #9fa9b8;
- --controls-height: 10dvh;
+ --controls-height: 10svh;
+ --controls-safe: env(safe-area-inset-bottom);
+ --controls-total-height: calc(var(--controls-height) + var(--controls-safe));
}
* {
@@ -32,15 +33,16 @@ body {
}
.home-layout {
- height: 100dvh;
+ position: relative;
+ height: 100svh;
width: 100%;
padding: 1rem;
- padding-bottom: calc(var(--controls-height) + 1rem);
+ padding-bottom: calc(var(--controls-total-height) + 1rem);
+ overflow: hidden;
}
.website-zone {
- height: calc(100dvh - var(--controls-height) - 1rem);
- max-height: 90dvh;
+ height: 100%;
min-height: 0;
}
@@ -97,9 +99,9 @@ body {
left: 0;
right: 0;
bottom: 0;
- height: var(--controls-height);
+ height: var(--controls-total-height);
padding: 0.5rem 1rem;
- padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));
+ padding-bottom: calc(0.5rem + var(--controls-safe));
border-top: 1px solid rgba(28, 41, 62, 0.16);
background: linear-gradient(180deg, rgba(255, 250, 242, 0.95), rgba(239, 235, 225, 0.98));
backdrop-filter: blur(7px);
@@ -119,6 +121,9 @@ body {
margin: 0;
color: var(--muted);
font-size: clamp(0.8rem, 2.2vw, 1rem);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.radio-controls__buttons {
@@ -163,11 +168,7 @@ body {
@media (max-width: 560px) {
.home-layout {
padding: 0.65rem;
- padding-bottom: calc(var(--controls-height) + 0.65rem);
- }
-
- .website-zone {
- height: calc(100dvh - var(--controls-height) - 0.65rem);
+ padding-bottom: calc(var(--controls-total-height) + 0.65rem);
}
.radio-controls-wrap {
@@ -176,11 +177,11 @@ body {
}
.radio-controls {
- grid-template-columns: 1fr;
- gap: 0.35rem;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 0.5rem;
}
.radio-controls__buttons {
- justify-content: start;
+ justify-content: end;
}
}
--
2.54.0
From bf4dd174b0614337cb2bf1bfd86e8e1998cc6903 Mon Sep 17 00:00:00 2001
From: kfj001
Date: Tue, 28 Apr 2026 03:05:40 +0000
Subject: [PATCH 6/7] locks the app to device portrait orientation
---
android/app/src/main/AndroidManifest.xml | 1 +
ios/App/App/Info.plist | 4 ----
next-env.d.ts | 2 +-
3 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 340e7df..c7fa7a0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818..9edff1c 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
--
2.54.0
From e847c667b3507f5f73216a8e685ce25d17dd32c5 Mon Sep 17 00:00:00 2001
From: kfj001
Date: Tue, 28 Apr 2026 19:54:09 +0000
Subject: [PATCH 7/7] Removes java development tools for vscode
---
.devcontainer/devcontainer.json | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 1ade0ad..3582058 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -6,12 +6,10 @@
"customizations": {
"vscode": {
"extensions": [
- "vscjava.vscode-java-pack",
"ms-vscode.vscode-typescript-next"
],
"settings": {
- "terminal.integrated.defaultProfile.linux": "bash",
- "java.home": "/usr/lib/jvm/java-17-openjdk-amd64"
+ "terminal.integrated.defaultProfile.linux": "bash"
}
}
},
--
2.54.0