All checks were successful
Pull Request Check / validate (pull_request) Successful in 23s
344 lines
9.5 KiB
JavaScript
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)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|