diff --git a/App.tsx b/App.tsx index 8b62db0..3b5b2e9 100644 --- a/App.tsx +++ b/App.tsx @@ -50,18 +50,24 @@ const App = () => { ) useEffect(() => { - DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => { - console.log(`${Routes.name}.toast:`, {value}) - setSnackbar(value) - }) - if (AppDataSource.isInitialized) return setInitialized(true) - AppDataSource.initialize().then(async () => { - const settings = await settingsRepo.findOne({where: {}}) - console.log(`${App.name}.useEffect:`, {gotSettings: settings}) - setTheme(settings.theme) - setColor(settings.color) - setInitialized(true) - }) + const description = DeviceEventEmitter.addListener( + TOAST, + ({value}: {value: string}) => { + console.log(`${Routes.name}.toast:`, {value}) + setSnackbar(value) + }, + ) + if (AppDataSource.isInitialized) setInitialized(true) + else { + AppDataSource.initialize().then(async () => { + const settings = await settingsRepo.findOne({where: {}}) + console.log(`${App.name}.useEffect:`, {gotSettings: settings}) + setTheme(settings.theme) + setColor(settings.color) + setInitialized(true) + }) + } + return description.remove }, []) const paperTheme = useMemo(() => { diff --git a/Routes.tsx b/Routes.tsx index 23b2060..b8c1cac 100644 --- a/Routes.tsx +++ b/Routes.tsx @@ -7,6 +7,7 @@ import HomePage from './HomePage' import PlanPage from './PlanPage' import Route from './route' import SettingsPage from './SettingsPage' +import TimerPage from './TimerPage' import useDark from './use-dark' import WorkoutsPage from './WorkoutsPage' @@ -21,6 +22,7 @@ export default function Routes() { {name: 'Plans', component: PlanPage, icon: 'event'}, {name: 'Best', component: BestPage, icon: 'insights'}, {name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'}, + {name: 'Timer', component: TimerPage, icon: 'access-time'}, {name: 'Settings', component: SettingsPage, icon: 'settings'}, ], [], diff --git a/TimerPage.tsx b/TimerPage.tsx index 35d7c67..b07ca36 100644 --- a/TimerPage.tsx +++ b/TimerPage.tsx @@ -1,62 +1,45 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useState} from 'react'; -import {NativeModules, View} from 'react-native'; -import {Button, Subheading, Title} from 'react-native-paper'; -import DrawerHeader from './DrawerHeader'; -import MassiveFab from './MassiveFab'; -import Page from './Page'; -import {getSettings, updateSettings} from './settings.service'; -import {useSettings} from './use-settings'; +import React, {useEffect, useState} from 'react' +import {NativeEventEmitter, NativeModules, View} from 'react-native' +import {Button, Subheading, Title} from 'react-native-paper' +import {PADDING} from './constants' +import {settingsRepo} from './db' +import DrawerHeader from './DrawerHeader' +import MassiveFab from './MassiveFab' +import Settings from './settings' + +interface TickEvent { + minutes: string + seconds: string +} export default function TimerPage() { - const [remaining, setRemaining] = useState(0); - const {settings} = useSettings(); + const [minutes, setMinutes] = useState('00') + const [seconds, setSeconds] = useState('00') + const [settings, setSettings] = useState() - const minutes = Math.floor(remaining / 1000 / 60); - const seconds = Math.floor((remaining / 1000) % 60); - let interval = 0; - - const tick = useCallback(() => { - let newRemaining = 0; - getSettings().then(gotSettings => { - if (!gotSettings.nextAlarm) return; - const date = new Date(gotSettings.nextAlarm); - newRemaining = date.getTime() - new Date().getTime(); - if (newRemaining < 0) setRemaining(0); - else setRemaining(newRemaining); - }); - interval = setInterval(() => { - console.log({newRemaining}); - newRemaining -= 1000; - if (newRemaining > 0) return setRemaining(newRemaining); - clearInterval(interval); - setRemaining(0); - }, 1000); - return () => clearInterval(interval); - }, []); - - useFocusEffect(tick); + useEffect(() => { + settingsRepo.findOne({where: {}}).then(setSettings) + const emitter = new NativeEventEmitter() + const listener = emitter.addListener('tick', (event: TickEvent) => { + console.log(`${TimerPage.name}.tick:`, {event}) + setMinutes(event.minutes) + setSeconds(event.seconds) + }) + return listener.remove + }, []) const stop = () => { - NativeModules.AlarmModule.stop(); - clearInterval(interval); - setRemaining(0); - updateSettings({...settings, nextAlarm: undefined}); - }; + NativeModules.AlarmModule.stop() + } const add = async () => { - if (!settings.nextAlarm) return; - const date = new Date(settings.nextAlarm); - date.setTime(date.getTime() + 1000 * 60); - NativeModules.AlarmModule.add(date, settings.vibrate, settings.sound); - await updateSettings({...settings, nextAlarm: date.toISOString()}); - tick(); - }; + NativeModules.AlarmModule.add(settings.vibrate, settings.sound) + } return ( <> - + - + - ); + ) } diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index 289f3bd..99c4c35 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -16,10 +16,8 @@ import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import com.facebook.react.bridge.Callback -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.* +import com.facebook.react.modules.core.DeviceEventManagerModule import kotlin.math.floor @@ -27,39 +25,63 @@ class AlarmModule constructor(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { var countdownTimer: CountDownTimer? = null + var currentMs: Long = 0 + var running = false override fun getName(): String { return "AlarmModule" } - private val receiver = object : BroadcastReceiver() { + private val stopReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) override fun onReceive(context: Context?, intent: Intent?) { Log.d("AlarmModule", "Received broadcast intent") - Toast.makeText(reactApplicationContext, "called from test receiver", Toast.LENGTH_SHORT) - .show() + stop() } } + init { + reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) + } + + override fun onCatalystInstanceDestroy() { + reactApplicationContext.unregisterReceiver(stopReceiver) + super.onCatalystInstanceDestroy() + } + @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod - fun add(milliseconds: Int, vibrate: Boolean, sound: String?) { + fun add(vibrate: Boolean, sound: String?) { Log.d("AlarmModule", "Add 1 min to alarm.") - val addIntent = Intent(reactApplicationContext, TimerService::class.java) - addIntent.action = "add" - addIntent.putExtra("vibrate", vibrate) - addIntent.putExtra("sound", sound) - addIntent.data = Uri.parse("$milliseconds") - reactApplicationContext.startService(addIntent) + countdownTimer?.cancel() + val newMs = if (running) currentMs.toInt().plus(60000) else 60000 + countdownTimer = getTimer(newMs, vibrate, sound) + countdownTimer?.start() + running = true } @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod fun stop() { Log.d("AlarmModule", "Stop alarm.") - val timerIntent = Intent(reactApplicationContext, TimerService::class.java) - reactApplicationContext.stopService(timerIntent) - val alarmIntent = Intent(reactApplicationContext, AlarmService::class.java) - reactApplicationContext.stopService(alarmIntent) + countdownTimer?.cancel() + running = false + reactApplicationContext?.stopService( + Intent( + reactApplicationContext, + AlarmService::class.java + ) + ) + val manager = getManager() + manager.cancel(NOTIFICATION_ID_DONE) + manager.cancel(NOTIFICATION_ID_PENDING) + val params = Arguments.createMap().apply { + putString("minutes", "00") + putString("seconds", "00") + } + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("tick", params) } @RequiresApi(api = Build.VERSION_CODES.O) @@ -76,6 +98,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) : countdownTimer?.cancel() countdownTimer = getTimer(milliseconds, vibrate, sound) countdownTimer?.start() + running = true } @RequiresApi(Build.VERSION_CODES.M) @@ -114,6 +137,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) : return object : CountDownTimer(endMs.toLong(), 1000) { @RequiresApi(Build.VERSION_CODES.O) override fun onTick(current: Long) { + currentMs = current val seconds = floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0') val minutes = @@ -124,6 +148,13 @@ class AlarmModule constructor(context: ReactApplicationContext?) : NotificationCompat.PRIORITY_LOW val manager = getManager() manager.notify(NOTIFICATION_ID_PENDING, builder.build()) + val params = Arguments.createMap().apply { + putString("minutes", minutes) + putString("seconds", seconds) + } + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("tick", params) } @RequiresApi(Build.VERSION_CODES.O) @@ -149,6 +180,9 @@ class AlarmModule constructor(context: ReactApplicationContext?) : alarmIntent.putExtra("vibrate", vibrate) alarmIntent.putExtra("sound", sound) context.startService(alarmIntent) + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("finish", Arguments.createMap()) } } } @@ -160,11 +194,10 @@ class AlarmModule constructor(context: ReactApplicationContext?) : val contentIntent = Intent(context, MainActivity::class.java) val pendingContent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) - reactApplicationContext.registerReceiver(receiver, IntentFilter(STOP_BROADCAST)) - val stopIntent = Intent(STOP_BROADCAST) - stopIntent.flags =Intent.FLAG_ACTIVITY_NEW_TASK + val stopBroadcast = Intent(STOP_BROADCAST) + stopBroadcast.setPackage(reactApplicationContext.packageName) val pendingStop = - PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_MUTABLE) + PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE) return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting") .setContentIntent(pendingContent) diff --git a/drawer-param-list.ts b/drawer-param-list.ts index a7e8ac5..68cae78 100644 --- a/drawer-param-list.ts +++ b/drawer-param-list.ts @@ -4,4 +4,5 @@ export type DrawerParamList = { Best: {} Plans: {} Workouts: {} + Timer: {} }