Add mobile remote control interface
This commit is contained in:
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-19
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+19
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user