Files
time-until/App.js
Luna b01f3d2dab
All checks were successful
Pull Request Check / validate (pull_request) Successful in 23s
feat: use local alert sound instead of broken URL
2026-03-30 20:36:30 +02:00

344 lines
9.5 KiB
JavaScript

import React, { useEffect, useMemo, useRef, useState } from 'react';
import { BackHandler, Platform, StatusBar, Modal, View, Image, StyleSheet, Button } from 'react-native';
import * as ScreenOrientation from 'expo-screen-orientation';
import * as SplashScreen from 'expo-splash-screen';
import { Accelerometer } from 'expo-sensors';
import { Audio } from 'expo-av';
import FocusScreen from './src/screens/FocusScreen';
import HomeScreen from './src/screens/HomeScreen';
import TimeUntilScreen from './src/screens/TimeUntilScreen';
import TimerScreen from './src/screens/TimerScreen';
import { createStyles } from './src/styles';
import { getTheme } from './src/theme';
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync().catch(() => {
/* reloading the app might cause this to error in dev */
});
export default function App() {
const styles = useMemo(() => createStyles(), []);
const [screen, setScreen] = useState('home');
const [focusMode, setFocusMode] = useState(false);
const [darkMode, setDarkMode] = useState(true);
const [pinkMode, setPinkMode] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [now, setNow] = useState(new Date());
const [showMinion, setShowMinion] = useState(false);
const [targetTime, setTargetTime] = useState(null);
const [tuHour, setTuHour] = useState('');
const [tuMinute, setTuMinute] = useState('');
const [tuIsOver, setTuIsOver] = useState(false);
const [timerRunning, setTimerRunning] = useState(false);
const [timerDone, setTimerDone] = useState(false);
const [timerRemaining, setTimerRemaining] = useState(0);
const [timerHInput, setTimerHInput] = useState('');
const [timerMInput, setTimerMInput] = useState('');
const [timerSInput, setTimerSInput] = useState('');
const timerRef = useRef(null);
const theme = getTheme(darkMode, pinkMode);
useEffect(() => {
// Hide splash screen after initialization
SplashScreen.hideAsync().catch(() => {});
}, []);
useEffect(() => {
if (Platform.OS !== 'web') {
ScreenOrientation.unlockAsync();
}
}, []);
useEffect(() => {
if (Platform.OS === 'android' || Platform.OS === 'ios') {
Accelerometer.setUpdateInterval(500);
const subscription = Accelerometer.addListener(({ x, y, z }) => {
const acceleration = Math.sqrt(x * x + y * y + z * z);
if (acceleration > 2.5) {
setShowMinion(true);
setTimeout(() => setShowMinion(false), 3000);
}
});
return () => subscription.remove();
}
}, []);
useEffect(() => {
const interval = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (targetTime && now >= targetTime) {
setTuIsOver(true);
} else {
setTuIsOver(false);
}
}, [now, targetTime]);
useEffect(() => {
if (timerRunning) {
timerRef.current = setInterval(() => {
setTimerRemaining((r) => {
if (r <= 1) {
clearInterval(timerRef.current);
setTimerRunning(false);
setTimerDone(true);
return 0;
}
return r - 1;
});
}, 1000);
}
return () => clearInterval(timerRef.current);
}, [timerRunning]);
useEffect(() => {
if (Platform.OS !== 'android') return undefined;
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
if (focusMode) {
setFocusMode(false);
return true;
}
if (screen !== 'home') {
setScreen('home');
return true;
}
return false;
});
return () => sub.remove();
}, [focusMode, screen]);
const toggleFullscreen = () => {
if (Platform.OS !== 'web') return;
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
setIsFullscreen(true);
} else {
document.exitFullscreen();
setIsFullscreen(false);
}
};
const setTuTimer = () => {
const h = parseInt(tuHour, 10);
const m = parseInt(tuMinute, 10);
if (isNaN(h) || isNaN(m) || h < 0 || h > 23 || m < 0 || m > 59) return;
const target = new Date();
target.setHours(h, m, 0, 0);
if (target <= new Date()) {
target.setDate(target.getDate() + 1);
}
setTargetTime(target);
setTuIsOver(false);
};
const resetTuTimer = () => {
setTargetTime(null);
setTuIsOver(false);
setTuHour('');
setTuMinute('');
};
const getTuCountdown = () => {
if (!targetTime) return null;
const diff = targetTime - now;
if (diff <= 0) return null;
const t = Math.floor(diff / 1000);
return {
hours: Math.floor(t / 3600),
minutes: Math.floor((t % 3600) / 60),
seconds: t % 60,
};
};
const startTimer = () => {
const h = parseInt(timerHInput, 10) || 0;
const m = parseInt(timerMInput, 10) || 0;
const s = parseInt(timerSInput, 10) || 0;
const total = h * 3600 + m * 60 + s;
if (total <= 0) return;
setTimerRemaining(total);
setTimerDone(false);
setTimerRunning(true);
};
const resetTimerState = () => {
clearInterval(timerRef.current);
setTimerRunning(false);
setTimerDone(false);
setTimerRemaining(0);
setTimerHInput('');
setTimerMInput('');
setTimerSInput('');
};
const timerHr = Math.floor(timerRemaining / 3600);
const timerMin = Math.floor((timerRemaining % 3600) / 60);
const timerSec = timerRemaining % 60;
const tuCountdown = getTuCountdown();
const playSound = async () => {
try {
const { sound } = await Audio.Sound.createAsync(
require('./assets/alert.mp3'),
{ shouldPlay: true, volume: 1.0 }
);
sound.setOnPlaybackStatusUpdate((status) => {
if (status.didJustFinish) {
sound.unloadAsync();
}
});
} catch (error) {
console.log('Error playing sound:', error);
}
};
useEffect(() => {
if (timerDone || tuIsOver) {
playSound();
}
}, [timerDone, tuIsOver]);
if (focusMode) {
return (
<FocusScreen
styles={styles}
theme={theme}
screen={screen}
pinkMode={pinkMode}
tuIsOver={tuIsOver}
tuCountdown={tuCountdown}
targetTime={targetTime}
timerDone={timerDone}
timerHr={timerHr}
timerMin={timerMin}
timerSec={timerSec}
onShowUI={() => setFocusMode(false)}
/>
);
}
const barStyle = darkMode ? 'light-content' : 'dark-content';
if (screen === 'home') {
return (
<>
<StatusBar barStyle={barStyle} backgroundColor={theme.bg} />
<HomeScreen
styles={styles}
theme={theme}
now={now}
darkMode={darkMode}
pinkMode={pinkMode}
isFullscreen={isFullscreen}
onToggleDark={() => setDarkMode((d) => !d)}
onTogglePink={() => setPinkMode((p) => !p)}
onToggleFullscreen={toggleFullscreen}
onSelectTimeUntil={() => setScreen('timeuntil')}
onSelectTimer={() => setScreen('timer')}
/>
</>
);
}
if (screen === 'timeuntil') {
return (
<>
<StatusBar barStyle={barStyle} backgroundColor={theme.bg} />
<TimeUntilScreen
styles={styles}
theme={theme}
now={now}
darkMode={darkMode}
pinkMode={pinkMode}
isFullscreen={isFullscreen}
targetTime={targetTime}
tuHour={tuHour}
tuMinute={tuMinute}
tuIsOver={tuIsOver}
tuCountdown={tuCountdown}
onChangeHour={setTuHour}
onChangeMinute={setTuMinute}
onSetTimer={setTuTimer}
onResetTimer={resetTuTimer}
onBackToMenu={() => setScreen('home')}
onToggleDark={() => setDarkMode((d) => !d)}
onTogglePink={() => setPinkMode((p) => !p)}
onToggleFullscreen={toggleFullscreen}
onFocus={() => setFocusMode(true)}
/>
</>
);
}
return (
<>
<Modal visible={showMinion} transparent={true} animationType="fade">
<View style={StyleSheet.absoluteFill}>
<Image
source={{ uri: 'https://shx.reversed.dev/u/XHuDcA.jpg' }}
style={{ flex: 1 }}
resizeMode="contain"
/>
<View style={{ position: 'absolute', top: 50, right: 20 }}>
<Button title="Dismiss" onPress={() => setShowMinion(false)} />
</View>
</View>
</Modal>
<StatusBar barStyle={barStyle} backgroundColor={theme.bg} />
<TimerScreen
styles={styles}
theme={theme}
now={now}
darkMode={darkMode}
pinkMode={pinkMode}
isFullscreen={isFullscreen}
timerRunning={timerRunning}
timerDone={timerDone}
timerRemaining={timerRemaining}
timerHInput={timerHInput}
timerMInput={timerMInput}
timerSInput={timerSInput}
timerHr={timerHr}
timerMin={timerMin}
timerSec={timerSec}
onChangeH={setTimerHInput}
onChangeM={setTimerMInput}
onChangeS={setTimerSInput}
onStart={startTimer}
onPause={() => setTimerRunning(false)}
onResume={() => {
if (timerRemaining > 0) {
setTimerRunning(true);
}
}}
onReset={resetTimerState}
onBackToMenu={() => setScreen('home')}
onToggleDark={() => setDarkMode((d) => !d)}
onTogglePink={() => setPinkMode((p) => !p)}
onToggleFullscreen={toggleFullscreen}
onFocus={() => setFocusMode(true)}
/>
</>
);
}