Add mobile remote control interface

This commit is contained in:
Andre
2026-02-27 10:01:19 +01:00
parent bf0e3a9038
commit 193f094795
6 changed files with 153 additions and 22 deletions
File diff suppressed because one or more lines are too long
-19
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+19
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -25,8 +25,8 @@
min-height: 100%; min-height: 100%;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-Cax2IsI8.js"></script> <script type="module" crossorigin src="/assets/index-DKJsxQTf.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CxSjc_eW.css"> <link rel="stylesheet" crossorigin href="/assets/index-CFG2ZxhD.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+131
View File
@@ -6,6 +6,10 @@ import { soundEngine } from './utils/sound';
type TimerMode = 'clock' | 'countdown' | 'interval'; type TimerMode = 'clock' | 'countdown' | 'interval';
type FocusTarget = 'menu-clock' | 'menu-countdown' | 'menu-interval' | 'menu-sound' | 'start' | 'pause' | 'resume' | 'stop' | 'skip-back' | 'skip-next' | 'work-dec' | 'work-inc' | 'rest-dec' | 'rest-inc' | 'rounds-dec' | 'rounds-inc' | null; type FocusTarget = 'menu-clock' | 'menu-countdown' | 'menu-interval' | 'menu-sound' | 'start' | 'pause' | 'resume' | 'stop' | 'skip-back' | 'skip-next' | 'work-dec' | 'work-inc' | 'rest-dec' | 'rest-inc' | 'rounds-dec' | 'rounds-inc' | null;
function isMobileDevice(): boolean {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth < 768;
}
function formatTime(seconds: number): string { function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
@@ -38,6 +42,11 @@ export default function App() {
const [clockTime, setClockTime] = useState(new Date()); const [clockTime, setClockTime] = useState(new Date());
const [showControls, setShowControls] = useState(true); const [showControls, setShowControls] = useState(true);
const [focusedEl, setFocusedEl] = useState<FocusTarget>('start'); const [focusedEl, setFocusedEl] = useState<FocusTarget>('start');
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
setIsMobile(isMobileDevice());
}, []);
const timerMode: 'interval' | 'countdown' = mode === 'clock' ? 'interval' : mode; const timerMode: 'interval' | 'countdown' = mode === 'clock' ? 'interval' : mode;
@@ -610,6 +619,128 @@ export default function App() {
</div> </div>
</div> </div>
)} )}
{/* Mobile Remote Control */}
{isMobile && (
<div className="fixed bottom-0 left-0 right-0 bg-[#0F172A] border-t border-[#334155] p-4 pb-8 z-40">
<div className="flex flex-col gap-4">
{/* Mode Selection */}
<div className="flex justify-center gap-2">
{(['clock', 'countdown', 'interval'] as TimerMode[]).map((m) => (
<button
key={m}
onClick={() => handleModeChange(m)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-all ${
mode === m
? 'bg-[#34D399] text-[#0F172A]'
: 'bg-[#1E293B] text-[#94A3B8] border border-[#334155]'
}`}
>
{m === 'clock' ? 'UHR' : m === 'countdown' ? 'COUNTDOWN' : 'INTERVAL'}
</button>
))}
</div>
{/* Timer Display */}
<div className="text-center">
<div className="text-sm text-[#94A3B8]">{getPhaseLabel(phase)}</div>
<div className="text-5xl font-bold text-white my-2">{formatTime(secondsRemaining)}</div>
{mode === 'interval' && phase !== 'idle' && (
<div className="text-[#94A3B8]">RUNDE {currentRound} / {config.rounds}</div>
)}
</div>
{/* Controls */}
<div className="flex justify-center gap-4">
{phase === 'idle' || phase === 'complete' ? (
<button
onClick={() => { soundEngine.init(); start(); }}
className="flex-1 bg-[#34D399] text-[#0F172A] py-4 rounded-xl font-bold text-xl"
>
START
</button>
) : isRunning ? (
<button
onClick={pause}
className="flex-1 bg-[#F59E0B] text-[#0F172A] py-4 rounded-xl font-bold text-xl"
>
PAUSE
</button>
) : (
<button
onClick={() => { soundEngine.init(); resume(); }}
className="flex-1 bg-[#34D399] text-[#0F172A] py-4 rounded-xl font-bold text-xl"
>
WEITER
</button>
)}
{phase !== 'idle' && (
<button
onClick={stop}
className="px-8 bg-red-600 text-white py-4 rounded-xl font-bold text-xl"
>
STOPP
</button>
)}
</div>
{/* Settings */}
{mode !== 'clock' && phase === 'idle' && (
<div className="bg-[#1E293B] rounded-xl p-4">
{mode === 'interval' ? (
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-[#94A3B8]">ARBEIT</span>
<div className="flex items-center gap-2">
<button onClick={() => handleConfigChange('workSeconds', Math.max(5, config.workSeconds - 5))} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">-</button>
<span className="w-16 text-center text-white font-bold">{config.workSeconds}s</span>
<button onClick={() => handleConfigChange('workSeconds', config.workSeconds + 5)} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">+</button>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-[#94A3B8]">PAUSE</span>
<div className="flex items-center gap-2">
<button onClick={() => handleConfigChange('restSeconds', Math.max(0, config.restSeconds - 5))} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">-</button>
<span className="w-16 text-center text-white font-bold">{config.restSeconds}s</span>
<button onClick={() => handleConfigChange('restSeconds', config.restSeconds + 5)} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">+</button>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-[#94A3B8]">RUNDEN</span>
<div className="flex items-center gap-2">
<button onClick={() => handleConfigChange('rounds', Math.max(1, config.rounds - 1))} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">-</button>
<span className="w-16 text-center text-white font-bold">{config.rounds}</span>
<button onClick={() => handleConfigChange('rounds', config.rounds + 1)} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">+</button>
</div>
</div>
</div>
) : (
<div className="flex justify-between items-center">
<span className="text-[#94A3B8]">DAUER</span>
<div className="flex items-center gap-2">
<button onClick={() => handleConfigChange('workSeconds', Math.max(5, config.workSeconds - 5))} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">-</button>
<span className="w-20 text-center text-white font-bold">{config.workSeconds}s</span>
<button onClick={() => handleConfigChange('workSeconds', config.workSeconds + 5)} className="w-10 h-10 bg-[#334155] rounded-lg text-white text-xl font-bold">+</button>
</div>
</div>
)}
</div>
)}
{/* Sound Toggle */}
<div className="flex justify-center">
<button
onClick={() => setSoundEnabled(!soundEnabled)}
className={`px-6 py-2 rounded-lg font-medium ${
soundEnabled ? 'bg-[#34D399] text-[#0F172A]' : 'bg-[#334155] text-[#94A3B8]'
}`}
>
SOUND {soundEnabled ? 'AN' : 'AUS'}
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }