This commit is contained in:
79
.gitea/workflows/ci.yml
Normal file
79
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Build App
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🏗 Setup repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: 🏗 Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: 🏗 Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: 🏗 Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: 🏗 Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: 🏗 Setup Expo and EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
eas-version: latest
|
||||
packager: pnpm
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: mobile
|
||||
|
||||
- name: 👷 Build app
|
||||
run: |
|
||||
eas build --local \
|
||||
--non-interactive \
|
||||
--output=./app-build \
|
||||
--platform=android \
|
||||
--profile=preview
|
||||
working-directory: mobile
|
||||
|
||||
# Neuer Schritt: Rename das Binary, damit es wie eine echte App aussieht
|
||||
- name: 📝 Rename build to APK
|
||||
run: mv mobile/app-build mobile/app-release.apk
|
||||
|
||||
- name: 📤 Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: android-preview-build
|
||||
path: mobile/app-release.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: 🏷 Create tag
|
||||
run: |
|
||||
TAG="build-$(git rev-parse --short HEAD)"
|
||||
git tag "$TAG"
|
||||
git push origin "$TAG"
|
||||
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: 🚀 Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
name: ${{ env.RELEASE_TAG }}
|
||||
files: mobile/app-release.apk
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
.kotlin/
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# generated native folders
|
||||
/ios
|
||||
/android
|
||||
279
App.js
Normal file
279
App.js
Normal file
@@ -0,0 +1,279 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BackHandler, Platform, StatusBar } from 'react-native';
|
||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||
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';
|
||||
|
||||
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 [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(() => {
|
||||
if (Platform.OS !== 'web') {
|
||||
ScreenOrientation.unlockAsync();
|
||||
}
|
||||
}, []);
|
||||
|
||||
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();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
33
app.json
Normal file
33
app.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Time Until",
|
||||
"slug": "time-until",
|
||||
"version": "1.0.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#0d0d0d"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"requireFullScreen": false
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#0d0d0d"
|
||||
},
|
||||
"edgeToEdgeEnabled": true
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png",
|
||||
"name": "Time Until",
|
||||
"themeColor": "#0d0d0d",
|
||||
"backgroundColor": "#0d0d0d"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
assets/adaptive-icon.png
Normal file
BIN
assets/adaptive-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/favicon.png
Normal file
BIN
assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/splash-icon.png
Normal file
BIN
assets/splash-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
30
eas.json
Normal file
30
eas.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 16.0.0",
|
||||
"appVersionSource": "local"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal",
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
},
|
||||
"ios": {
|
||||
"simulator": true
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal",
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true,
|
||||
"android": {
|
||||
"buildType": "app-bundle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
index.js
Normal file
8
index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerRootComponent } from 'expo';
|
||||
|
||||
import App from './App';
|
||||
|
||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||
// the environment is set up appropriately
|
||||
registerRootComponent(App);
|
||||
8910
package-lock.json
generated
Normal file
8910
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "time-until",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "~54.0.33",
|
||||
"expo-screen-orientation": "~9.0.8",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-web": "^0.21.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
40
src/components/CountdownRow.js
Normal file
40
src/components/CountdownRow.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
function pad(n) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
export default function CountdownRow({ styles, cd, accent, subText, pinkMode, numStyle, sepStyle }) {
|
||||
const outline = pinkMode
|
||||
? {
|
||||
borderColor: '#ff4fa3',
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 8,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<View style={styles.countdownRow}>
|
||||
{cd.hours > 0 && (
|
||||
<>
|
||||
<View style={styles.timeBlock}>
|
||||
<Text style={[numStyle, { color: accent }, outline]}>{pad(cd.hours)}</Text>
|
||||
<Text style={[styles.countdownUnit, { color: subText }]}>HRS</Text>
|
||||
</View>
|
||||
<Text style={[sepStyle, { color: accent }]}>:</Text>
|
||||
</>
|
||||
)}
|
||||
<View style={styles.timeBlock}>
|
||||
<Text style={[numStyle, { color: accent }, outline]}>{pad(cd.minutes)}</Text>
|
||||
<Text style={[styles.countdownUnit, { color: subText }]}>MIN</Text>
|
||||
</View>
|
||||
<Text style={[sepStyle, { color: accent }]}>:</Text>
|
||||
<View style={styles.timeBlock}>
|
||||
<Text style={[numStyle, { color: accent }, outline]}>{pad(cd.seconds)}</Text>
|
||||
<Text style={[styles.countdownUnit, { color: subText }]}>SEC</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
70
src/components/TopControls.js
Normal file
70
src/components/TopControls.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Platform, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
export default function TopControls({
|
||||
styles,
|
||||
accent,
|
||||
pink,
|
||||
darkMode,
|
||||
pinkMode,
|
||||
isFullscreen,
|
||||
showBackToMenu,
|
||||
showFocus,
|
||||
onBackToMenu,
|
||||
onToggleDark,
|
||||
onTogglePink,
|
||||
onToggleFullscreen,
|
||||
onFocus,
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.topRow}>
|
||||
{showBackToMenu && (
|
||||
<TouchableOpacity
|
||||
style={[styles.backMenuBtn, { borderColor: accent, backgroundColor: `${accent}1f` }]}
|
||||
onPress={onBackToMenu}
|
||||
>
|
||||
<Text style={[styles.toggleText, { color: accent }]}>Back To Menu</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={styles.toggleRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.toggleBtn, { borderColor: accent, backgroundColor: `${accent}1f` }]}
|
||||
onPress={onToggleDark}
|
||||
>
|
||||
<Text style={[styles.toggleText, { color: accent }]}>{darkMode ? 'Light' : 'Dark'}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.toggleBtn,
|
||||
{ borderColor: pink, backgroundColor: pinkMode ? `${pink}33` : 'transparent' },
|
||||
]}
|
||||
onPress={onTogglePink}
|
||||
>
|
||||
<Text style={[styles.toggleText, { color: pink }]}>Pink</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{Platform.OS === 'web' && (
|
||||
<TouchableOpacity
|
||||
style={[styles.toggleBtn, { borderColor: accent, backgroundColor: `${accent}14` }]}
|
||||
onPress={onToggleFullscreen}
|
||||
>
|
||||
<Text style={[styles.toggleText, { color: accent }]}>
|
||||
{isFullscreen ? 'Exit Full' : 'Fullscreen'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{showFocus && (
|
||||
<TouchableOpacity
|
||||
style={[styles.toggleBtn, { borderColor: accent, backgroundColor: `${accent}1f` }]}
|
||||
onPress={onFocus}
|
||||
>
|
||||
<Text style={[styles.toggleText, { color: accent }]}>Focus</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
86
src/screens/FocusScreen.js
Normal file
86
src/screens/FocusScreen.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { StatusBar, Text, TouchableOpacity, View } from 'react-native';
|
||||
import CountdownRow from '../components/CountdownRow';
|
||||
|
||||
export default function FocusScreen({
|
||||
styles,
|
||||
theme,
|
||||
screen,
|
||||
pinkMode,
|
||||
tuIsOver,
|
||||
tuCountdown,
|
||||
targetTime,
|
||||
timerDone,
|
||||
timerHr,
|
||||
timerMin,
|
||||
timerSec,
|
||||
onShowUI,
|
||||
}) {
|
||||
const pinkOutlineText = pinkMode
|
||||
? {
|
||||
borderColor: theme.pink,
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 8,
|
||||
}
|
||||
: {};
|
||||
|
||||
let focusContent = null;
|
||||
|
||||
if (screen === 'timeuntil') {
|
||||
focusContent = tuIsOver ? (
|
||||
<Text style={[styles.overText, styles.focusOverText, { color: pinkMode ? theme.pink : theme.danger }, pinkOutlineText]}>
|
||||
Time's Up!
|
||||
</Text>
|
||||
) : tuCountdown ? (
|
||||
<View style={styles.focusCountdown}>
|
||||
<Text style={[styles.focusLabel, { color: theme.subText }]}>
|
||||
until {targetTime?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</Text>
|
||||
<CountdownRow
|
||||
styles={styles}
|
||||
cd={tuCountdown}
|
||||
accent={theme.accent}
|
||||
subText={theme.subText}
|
||||
pinkMode={pinkMode}
|
||||
numStyle={styles.focusNum}
|
||||
sepStyle={styles.focusSep}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[styles.placeholder, { color: theme.subText }]}>No timer set</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen === 'timer') {
|
||||
focusContent = timerDone ? (
|
||||
<Text style={[styles.overText, styles.focusOverText, { color: pinkMode ? theme.pink : theme.danger }, pinkOutlineText]}>
|
||||
Done!
|
||||
</Text>
|
||||
) : (
|
||||
<View style={styles.focusCountdown}>
|
||||
<Text style={[styles.focusLabel, { color: theme.subText }]}>TIMER</Text>
|
||||
<CountdownRow
|
||||
styles={styles}
|
||||
cd={{ hours: timerHr, minutes: timerMin, seconds: timerSec }}
|
||||
accent={theme.accent}
|
||||
subText={theme.subText}
|
||||
pinkMode={pinkMode}
|
||||
numStyle={styles.focusNum}
|
||||
sepStyle={styles.focusSep}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.root, styles.focusRoot, { backgroundColor: theme.bg }]}>
|
||||
<StatusBar hidden />
|
||||
{focusContent}
|
||||
<TouchableOpacity style={[styles.focusExitBtn, { borderColor: theme.accent }]} onPress={onShowUI}>
|
||||
<Text style={[styles.focusExitText, { color: theme.accent }]}>Show UI</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
87
src/screens/HomeScreen.js
Normal file
87
src/screens/HomeScreen.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
import TopControls from '../components/TopControls';
|
||||
|
||||
export default function HomeScreen({
|
||||
styles,
|
||||
theme,
|
||||
now,
|
||||
pinkMode,
|
||||
darkMode,
|
||||
isFullscreen,
|
||||
onToggleDark,
|
||||
onTogglePink,
|
||||
onToggleFullscreen,
|
||||
onSelectTimeUntil,
|
||||
onSelectTimer,
|
||||
}) {
|
||||
return (
|
||||
<View style={[styles.root, { backgroundColor: theme.bg }]}>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
|
||||
<Text
|
||||
style={[
|
||||
styles.title,
|
||||
{ color: theme.accent },
|
||||
pinkMode && {
|
||||
borderColor: theme.accent,
|
||||
borderWidth: 2,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
Time Until
|
||||
</Text>
|
||||
|
||||
<TopControls
|
||||
styles={styles}
|
||||
accent={theme.accent}
|
||||
pink={theme.pink}
|
||||
darkMode={darkMode}
|
||||
pinkMode={pinkMode}
|
||||
isFullscreen={isFullscreen}
|
||||
showBackToMenu={false}
|
||||
showFocus={false}
|
||||
onBackToMenu={() => {}}
|
||||
onToggleDark={onToggleDark}
|
||||
onTogglePink={onTogglePink}
|
||||
onToggleFullscreen={onToggleFullscreen}
|
||||
onFocus={() => {}}
|
||||
/>
|
||||
|
||||
<Text style={[styles.modeSelectLabel, { color: theme.subText }]}>SELECT MODE</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.modeCard, { backgroundColor: theme.cardBg, borderColor: theme.accent }]}
|
||||
onPress={onSelectTimeUntil}
|
||||
activeOpacity={0.75}
|
||||
>
|
||||
<Text style={[styles.modeTitle, { color: theme.accent }]}>Mode 1: Time Until</Text>
|
||||
<Text style={[styles.modeDesc, { color: theme.subText }]}>
|
||||
Set a target clock time and count down to it
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.modeCard, { backgroundColor: theme.cardBg, borderColor: theme.border }]}
|
||||
onPress={onSelectTimer}
|
||||
activeOpacity={0.75}
|
||||
>
|
||||
<Text style={[styles.modeTitle, { color: theme.accent }]}>Mode 2: Timer</Text>
|
||||
<Text style={[styles.modeDesc, { color: theme.subText }]}>
|
||||
Set duration in hours, minutes, and seconds
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.clock, { color: theme.subText, marginTop: 32 }]}>
|
||||
{now.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
173
src/screens/TimeUntilScreen.js
Normal file
173
src/screens/TimeUntilScreen.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import React from 'react';
|
||||
import { ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import CountdownRow from '../components/CountdownRow';
|
||||
import TopControls from '../components/TopControls';
|
||||
|
||||
export default function TimeUntilScreen({
|
||||
styles,
|
||||
theme,
|
||||
now,
|
||||
darkMode,
|
||||
pinkMode,
|
||||
isFullscreen,
|
||||
targetTime,
|
||||
tuHour,
|
||||
tuMinute,
|
||||
tuIsOver,
|
||||
tuCountdown,
|
||||
onChangeHour,
|
||||
onChangeMinute,
|
||||
onSetTimer,
|
||||
onResetTimer,
|
||||
onBackToMenu,
|
||||
onToggleDark,
|
||||
onTogglePink,
|
||||
onToggleFullscreen,
|
||||
onFocus,
|
||||
}) {
|
||||
const isCountingDown = Boolean(tuCountdown) && !tuIsOver;
|
||||
|
||||
const pinkOutlineText = pinkMode
|
||||
? {
|
||||
borderColor: theme.pink,
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 8,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<View style={[styles.root, { backgroundColor: theme.bg }]}>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
|
||||
<Text
|
||||
style={[
|
||||
styles.title,
|
||||
{ color: theme.accent },
|
||||
pinkMode && {
|
||||
borderColor: theme.accent,
|
||||
borderWidth: 2,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
Time Until
|
||||
</Text>
|
||||
|
||||
<TopControls
|
||||
styles={styles}
|
||||
accent={theme.accent}
|
||||
pink={theme.pink}
|
||||
darkMode={darkMode}
|
||||
pinkMode={pinkMode}
|
||||
isFullscreen={isFullscreen}
|
||||
showBackToMenu
|
||||
showFocus={targetTime !== null || tuIsOver}
|
||||
onBackToMenu={onBackToMenu}
|
||||
onToggleDark={onToggleDark}
|
||||
onTogglePink={onTogglePink}
|
||||
onToggleFullscreen={onToggleFullscreen}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: theme.cardBg, borderColor: theme.border }]}>
|
||||
{!isCountingDown && (
|
||||
<>
|
||||
<Text style={[styles.inputLabel, { color: theme.subText }]}>Set target time (24h)</Text>
|
||||
<View style={styles.inputRow}>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.timeInput,
|
||||
{ color: theme.text, borderColor: theme.accent, backgroundColor: theme.inputBg },
|
||||
]}
|
||||
placeholder="HH"
|
||||
placeholderTextColor={darkMode ? '#5a6886' : '#98a1ba'}
|
||||
value={tuHour}
|
||||
onChangeText={onChangeHour}
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
/>
|
||||
<Text style={[styles.colon, { color: theme.accent }]}>:</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.timeInput,
|
||||
{ color: theme.text, borderColor: theme.accent, backgroundColor: theme.inputBg },
|
||||
]}
|
||||
placeholder="MM"
|
||||
placeholderTextColor={darkMode ? '#5a6886' : '#98a1ba'}
|
||||
value={tuMinute}
|
||||
onChangeText={onChangeMinute}
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.btnRow}>
|
||||
<TouchableOpacity style={[styles.setBtn, { backgroundColor: theme.accent }]} onPress={onSetTimer}>
|
||||
<Text style={styles.setBtnText}>Set Timer</Text>
|
||||
</TouchableOpacity>
|
||||
{targetTime && (
|
||||
<TouchableOpacity
|
||||
style={[styles.resetBtn, { borderColor: theme.accent }]}
|
||||
onPress={onResetTimer}
|
||||
>
|
||||
<Text style={[styles.resetBtnText, { color: theme.accent }]}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isCountingDown && targetTime && (
|
||||
<View style={styles.btnRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.resetBtn, { borderColor: theme.accent }]}
|
||||
onPress={onResetTimer}
|
||||
>
|
||||
<Text style={[styles.resetBtnText, { color: theme.accent }]}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{tuIsOver ? (
|
||||
<View style={styles.overContainer}>
|
||||
<Text style={[styles.overText, { color: pinkMode ? theme.pink : theme.danger }, pinkOutlineText]}>
|
||||
Time's Up!
|
||||
</Text>
|
||||
<Text style={[styles.overSub, { color: theme.subText }]}>
|
||||
{targetTime?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} has been reached
|
||||
</Text>
|
||||
</View>
|
||||
) : tuCountdown ? (
|
||||
<View style={styles.countdownContainer}>
|
||||
<Text style={[styles.countdownLabel, { color: theme.subText }]}>
|
||||
until {targetTime?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</Text>
|
||||
<CountdownRow
|
||||
styles={styles}
|
||||
cd={tuCountdown}
|
||||
accent={theme.accent}
|
||||
subText={theme.subText}
|
||||
pinkMode={pinkMode}
|
||||
numStyle={styles.countdownNum}
|
||||
sepStyle={styles.sep}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[styles.placeholder, { color: theme.subText }]}>Enter a time above to start counting down</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text style={[styles.clock, { color: theme.subText }]}>
|
||||
{now.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
195
src/screens/TimerScreen.js
Normal file
195
src/screens/TimerScreen.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import { ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import CountdownRow from '../components/CountdownRow';
|
||||
import TopControls from '../components/TopControls';
|
||||
|
||||
export default function TimerScreen({
|
||||
styles,
|
||||
theme,
|
||||
now,
|
||||
darkMode,
|
||||
pinkMode,
|
||||
isFullscreen,
|
||||
timerRunning,
|
||||
timerDone,
|
||||
timerRemaining,
|
||||
timerHInput,
|
||||
timerMInput,
|
||||
timerSInput,
|
||||
timerHr,
|
||||
timerMin,
|
||||
timerSec,
|
||||
onChangeH,
|
||||
onChangeM,
|
||||
onChangeS,
|
||||
onStart,
|
||||
onPause,
|
||||
onResume,
|
||||
onReset,
|
||||
onBackToMenu,
|
||||
onToggleDark,
|
||||
onTogglePink,
|
||||
onToggleFullscreen,
|
||||
onFocus,
|
||||
}) {
|
||||
const timerActive = timerRunning || timerRemaining > 0 || timerDone;
|
||||
const isCountingDown = timerRemaining > 0;
|
||||
const pinkOutlineText = pinkMode
|
||||
? {
|
||||
borderColor: theme.pink,
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 8,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<View style={[styles.root, { backgroundColor: theme.bg }]}>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
|
||||
<Text
|
||||
style={[
|
||||
styles.title,
|
||||
{ color: theme.accent },
|
||||
pinkMode && {
|
||||
borderColor: theme.accent,
|
||||
borderWidth: 2,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
Timer
|
||||
</Text>
|
||||
|
||||
<TopControls
|
||||
styles={styles}
|
||||
accent={theme.accent}
|
||||
pink={theme.pink}
|
||||
darkMode={darkMode}
|
||||
pinkMode={pinkMode}
|
||||
isFullscreen={isFullscreen}
|
||||
showBackToMenu
|
||||
showFocus={timerActive}
|
||||
onBackToMenu={onBackToMenu}
|
||||
onToggleDark={onToggleDark}
|
||||
onTogglePink={onTogglePink}
|
||||
onToggleFullscreen={onToggleFullscreen}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: theme.cardBg, borderColor: theme.border }]}>
|
||||
{!isCountingDown && !timerDone ? (
|
||||
<>
|
||||
<Text style={[styles.inputLabel, { color: theme.subText }]}>Set duration</Text>
|
||||
<View style={styles.inputRow}>
|
||||
<View style={styles.timerInputGroup}>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.timeInput,
|
||||
{ color: theme.text, borderColor: theme.accent, backgroundColor: theme.inputBg },
|
||||
]}
|
||||
placeholder="00"
|
||||
placeholderTextColor={darkMode ? '#5a6886' : '#98a1ba'}
|
||||
value={timerHInput}
|
||||
onChangeText={onChangeH}
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
/>
|
||||
<Text style={[styles.inputUnit, { color: theme.subText }]}>HRS</Text>
|
||||
</View>
|
||||
<Text style={[styles.colon, { color: theme.accent }]}>:</Text>
|
||||
<View style={styles.timerInputGroup}>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.timeInput,
|
||||
{ color: theme.text, borderColor: theme.accent, backgroundColor: theme.inputBg },
|
||||
]}
|
||||
placeholder="00"
|
||||
placeholderTextColor={darkMode ? '#5a6886' : '#98a1ba'}
|
||||
value={timerMInput}
|
||||
onChangeText={onChangeM}
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
/>
|
||||
<Text style={[styles.inputUnit, { color: theme.subText }]}>MIN</Text>
|
||||
</View>
|
||||
<Text style={[styles.colon, { color: theme.accent }]}>:</Text>
|
||||
<View style={styles.timerInputGroup}>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.timeInput,
|
||||
{ color: theme.text, borderColor: theme.accent, backgroundColor: theme.inputBg },
|
||||
]}
|
||||
placeholder="00"
|
||||
placeholderTextColor={darkMode ? '#5a6886' : '#98a1ba'}
|
||||
value={timerSInput}
|
||||
onChangeText={onChangeS}
|
||||
keyboardType="numeric"
|
||||
maxLength={2}
|
||||
/>
|
||||
<Text style={[styles.inputUnit, { color: theme.subText }]}>SEC</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={[styles.setBtn, { backgroundColor: theme.accent }]} onPress={onStart}>
|
||||
<Text style={styles.setBtnText}>Start</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : timerDone ? (
|
||||
<View style={styles.overContainer}>
|
||||
<Text style={[styles.overText, { color: pinkMode ? theme.pink : theme.danger }, pinkOutlineText]}>
|
||||
Done!
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.setBtn, { backgroundColor: theme.accent, marginTop: 20 }]}
|
||||
onPress={onReset}
|
||||
>
|
||||
<Text style={styles.setBtnText}>New Timer</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.countdownContainer}>
|
||||
<View>
|
||||
<CountdownRow
|
||||
styles={styles}
|
||||
cd={{ hours: timerHr, minutes: timerMin, seconds: timerSec }}
|
||||
accent={theme.accent}
|
||||
subText={theme.subText}
|
||||
pinkMode={pinkMode}
|
||||
numStyle={styles.countdownNum}
|
||||
sepStyle={styles.sep}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.btnRow}>
|
||||
{timerRunning ? (
|
||||
<TouchableOpacity style={[styles.resetBtn, { borderColor: theme.accent }]} onPress={onPause}>
|
||||
<Text style={[styles.resetBtnText, { color: theme.accent }]}>Pause</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity style={[styles.setBtn, { backgroundColor: theme.accent }]} onPress={onResume}>
|
||||
<Text style={styles.setBtnText}>Resume</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={[styles.resetBtn, { borderColor: theme.danger }]} onPress={onReset}>
|
||||
<Text style={[styles.resetBtnText, { color: theme.danger }]}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text style={[styles.clock, { color: theme.subText }]}>
|
||||
{now.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
161
src/styles.js
Normal file
161
src/styles.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export function createStyles() {
|
||||
return StyleSheet.create({
|
||||
root: { flex: 1 },
|
||||
scroll: {
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
paddingVertical: 48,
|
||||
},
|
||||
title: {
|
||||
fontSize: 44,
|
||||
fontWeight: '900',
|
||||
marginBottom: 20,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
topRow: {
|
||||
width: '100%',
|
||||
maxWidth: 640,
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
backMenuBtn: {
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
toggleRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 10,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
toggleBtn: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
toggleText: { fontSize: 14, fontWeight: '600' },
|
||||
modeSelectLabel: {
|
||||
fontSize: 12,
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 20,
|
||||
},
|
||||
modeCard: {
|
||||
width: '100%',
|
||||
maxWidth: 440,
|
||||
borderRadius: 22,
|
||||
padding: 28,
|
||||
borderWidth: 2,
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.18,
|
||||
shadowRadius: 14,
|
||||
elevation: 7,
|
||||
},
|
||||
modeTitle: {
|
||||
fontSize: 26,
|
||||
fontWeight: '800',
|
||||
marginBottom: 8,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
modeDesc: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
maxWidth: 440,
|
||||
borderRadius: 22,
|
||||
padding: 28,
|
||||
borderWidth: 1.5,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 16,
|
||||
elevation: 8,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 12,
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 10,
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 16,
|
||||
},
|
||||
timerInputGroup: { alignItems: 'center', gap: 4 },
|
||||
inputUnit: { fontSize: 11, fontWeight: '600', letterSpacing: 1.5 },
|
||||
timeInput: {
|
||||
width: 72,
|
||||
height: 58,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
textAlign: 'center',
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
},
|
||||
colon: { fontSize: 34, fontWeight: '900' },
|
||||
btnRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
setBtn: {
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
},
|
||||
setBtnText: { color: '#fff', fontSize: 16, fontWeight: '700', letterSpacing: 0.5 },
|
||||
resetBtn: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 13,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
resetBtnText: { fontSize: 15, fontWeight: '600' },
|
||||
overContainer: { alignItems: 'center', paddingVertical: 12, gap: 10 },
|
||||
overText: { fontSize: 52, fontWeight: '900', letterSpacing: 2 },
|
||||
overSub: { fontSize: 15 },
|
||||
countdownContainer: { alignItems: 'center', gap: 12 },
|
||||
countdownLabel: { fontSize: 13, letterSpacing: 0.5, textTransform: 'uppercase' },
|
||||
countdownRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 6, justifyContent: 'center' },
|
||||
timeBlock: { alignItems: 'center', gap: 4 },
|
||||
countdownNum: { fontSize: 58, fontWeight: '800', lineHeight: 66 },
|
||||
countdownUnit: { fontSize: 11, fontWeight: '600', letterSpacing: 1.5 },
|
||||
sep: { fontSize: 50, fontWeight: '800', marginTop: 4 },
|
||||
placeholder: { fontSize: 15, textAlign: 'center', paddingVertical: 24, lineHeight: 24 },
|
||||
clock: { marginTop: 24, fontSize: 13, letterSpacing: 1.5 },
|
||||
focusRoot: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 10 },
|
||||
focusCountdown: { alignItems: 'center', gap: 16, width: '100%' },
|
||||
focusLabel: { fontSize: 16, letterSpacing: 0.5, textTransform: 'uppercase' },
|
||||
focusNum: { fontSize: 72, fontWeight: '800', lineHeight: 80 },
|
||||
focusSep: { fontSize: 56, fontWeight: '800', marginTop: 8 },
|
||||
focusOverText: { fontSize: 72 },
|
||||
focusExitBtn: {
|
||||
position: 'absolute',
|
||||
bottom: 40,
|
||||
right: 32,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
focusExitText: { fontSize: 14, fontWeight: '600' },
|
||||
});
|
||||
}
|
||||
31
src/theme.js
Normal file
31
src/theme.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export function getTheme(darkMode, pinkMode) {
|
||||
const dark = {
|
||||
bg: '#0b1020',
|
||||
cardBg: '#131a2a',
|
||||
text: '#e6ecff',
|
||||
subText: '#93a0bf',
|
||||
accent: '#5fb0ff',
|
||||
border: '#24304a',
|
||||
inputBg: '#0f1727',
|
||||
};
|
||||
|
||||
const light = {
|
||||
bg: '#f7f8fc',
|
||||
cardBg: '#ffffff',
|
||||
text: '#1d2433',
|
||||
subText: '#5f6b85',
|
||||
accent: '#1f67ff',
|
||||
border: '#d5def2',
|
||||
inputBg: '#eef3ff',
|
||||
};
|
||||
|
||||
const base = darkMode ? dark : light;
|
||||
const accent = pinkMode ? '#ff4fa3' : base.accent;
|
||||
|
||||
return {
|
||||
...base,
|
||||
accent,
|
||||
danger: '#ef4444',
|
||||
pink: '#ff4fa3',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user