diff --git a/package-lock.json b/package-lock.json index 16c944c..8b219fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@react-native-async-storage/async-storage": "^1.17.11", "@react-native-community/blur": "^4.3.0", + "@react-native-community/checkbox": "^0.5.13", "@react-native-community/netinfo": "^9.3.6", "@react-navigation/bottom-tabs": "^6.4.0", "@react-navigation/elements": "^1.3.17", @@ -2677,6 +2678,21 @@ "react-native": "*" } }, + "node_modules/@react-native-community/checkbox": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@react-native-community/checkbox/-/checkbox-0.5.16.tgz", + "integrity": "sha512-j4fmWe77EAayGnKJ52BljlN8apLT3xjxG/pJOA6HZ4ew63FiXmnY7VtxTzmvDKgSPrETdQc2lmx5mdXTAufJnw==", + "peerDependencies": { + "react": "*", + "react-native": ">= 0.62", + "react-native-windows": ">=0.62" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, "node_modules/@react-native-community/cli": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-10.2.6.tgz", @@ -4409,23 +4425,15 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "node_modules/@types/react": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", - "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "version": "18.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz", + "integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==", "dependencies": { "@types/prop-types": "*", + "@types/scheduler": "*", "csstype": "^3.0.2" } }, - "node_modules/@types/react-native": { - "version": "0.70.6", - "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.6.tgz", - "integrity": "sha512-ynQ2jj0km9d7dbnyKqVdQ6Nti7VQ8SLTA/KKkkS5+FnvGyvij2AOo1/xnkBR/jnSNXuzrvGVzw2n0VWfppmfKw==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-test-renderer": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz", @@ -4444,6 +4452,11 @@ "redux": "^4.0.0" } }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -4477,6 +4490,15 @@ "@types/styled-components": "*" } }, + "node_modules/@types/styled-components-react-native/node_modules/@types/react-native": { + "version": "0.70.14", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.14.tgz", + "integrity": "sha512-Kwc+BYBrnDqvacNxKp1UtcZJnJJnTih2NYmi/ieAKlHdxEPN6sYMwmIwgHdoLHggvml6bf3DYRaH2jt+gzaLjw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -18163,6 +18185,11 @@ "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-4.3.0.tgz", "integrity": "sha512-d6phh39kKcbZ4IluDftiVWqfeFOgjl1AbQWzN47x+hLKQ5GvQJ6QhRvgAuDZ+xbJksrbXgNpMjVYkjsbcVehxg==" }, + "@react-native-community/checkbox": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@react-native-community/checkbox/-/checkbox-0.5.16.tgz", + "integrity": "sha512-j4fmWe77EAayGnKJ52BljlN8apLT3xjxG/pJOA6HZ4ew63FiXmnY7VtxTzmvDKgSPrETdQc2lmx5mdXTAufJnw==" + }, "@react-native-community/cli": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-10.2.6.tgz", @@ -19446,23 +19473,15 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", - "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "version": "18.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz", + "integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==", "requires": { "@types/prop-types": "*", + "@types/scheduler": "*", "csstype": "^3.0.2" } }, - "@types/react-native": { - "version": "0.70.6", - "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.6.tgz", - "integrity": "sha512-ynQ2jj0km9d7dbnyKqVdQ6Nti7VQ8SLTA/KKkkS5+FnvGyvij2AOo1/xnkBR/jnSNXuzrvGVzw2n0VWfppmfKw==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, "@types/react-test-renderer": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz", @@ -19481,6 +19500,11 @@ "redux": "^4.0.0" } }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -19512,6 +19536,17 @@ "@types/react": "*", "@types/react-native": "^0.70", "@types/styled-components": "*" + }, + "dependencies": { + "@types/react-native": { + "version": "0.70.14", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.14.tgz", + "integrity": "sha512-Kwc+BYBrnDqvacNxKp1UtcZJnJJnTih2NYmi/ieAKlHdxEPN6sYMwmIwgHdoLHggvml6bf3DYRaH2jt+gzaLjw==", + "dev": true, + "requires": { + "@types/react": "*" + } + } } }, "@types/use-sync-external-store": { diff --git a/package.json b/package.json index bcbc2a5..9a35c6f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@react-native-async-storage/async-storage": "^1.17.11", "@react-native-community/blur": "^4.3.0", + "@react-native-community/checkbox": "^0.5.13", "@react-native-community/netinfo": "^9.3.6", "@react-navigation/bottom-tabs": "^6.4.0", "@react-navigation/elements": "^1.3.17", diff --git a/src/screens/Settings/index.tsx b/src/screens/Settings/index.tsx index 3782895..2d5fa28 100644 --- a/src/screens/Settings/index.tsx +++ b/src/screens/Settings/index.tsx @@ -33,18 +33,18 @@ export function SettingsList() { const { sleepTime } = useTypedSelector(state => state.settings); const getTime = () => { + let hours = 0; + let minutes = 0; if (!Number.isNaN(sleepTime)) { - const hours = Math.round(sleepTime / 3600); + hours = Math.round(sleepTime / 3600); const timeRemaining = sleepTime % 3600; - let minutes = 0; - if (timeRemaining > 60) { + if (timeRemaining >= 60) { minutes = Math.round(timeRemaining / 60); } - return `${hours} hrs ${minutes} min`; - } else { - return 0; } + + return `${hours} hrs ${minutes} min`; } return ( diff --git a/src/screens/Settings/stacks/timer/Timer.tsx b/src/screens/Settings/stacks/timer/Timer.tsx index b02bcfc..acf14e1 100644 --- a/src/screens/Settings/stacks/timer/Timer.tsx +++ b/src/screens/Settings/stacks/timer/Timer.tsx @@ -4,7 +4,7 @@ import { Text } from '@/components/Typography'; import { InputContainer } from '../../components/Input'; import Input from '@/components/Input'; import useDefaultStyles from '@/components/Colors'; -import { setSleepTime } from '@/store/settings/actions'; +import { setEnabledSleeper, setSleepTime } from '@/store/settings/actions'; import { useNavigation } from '@react-navigation/native'; import { useDispatch } from 'react-redux'; import { ScrollView, ToastAndroid, View } from 'react-native'; @@ -16,12 +16,16 @@ import AlbumIcon from '@/assets/icons/collection.svg'; import TrackIcon from '@/assets/icons/note.svg'; import SelectDropdown from 'react-native-select-dropdown'; import { time } from 'console'; +import CheckBox from '@react-native-community/checkbox'; +import TrackPlayer from 'react-native-track-player'; function Timer() { const { sleepTime } = useTypedSelector(state => state.settings); + const enabledSleeper = useTypedSelector((state) => state.settings.enabledSleeper); const [minutes, setMinutes] = useState(); const [hours, setHours] = useState(); + const [enableSleeper, setEnableSleeper] = useState(enabledSleeper); const timerStyles = generateTimerStyles(); @@ -37,51 +41,72 @@ function Timer() { }, [navigation, dispatch]); const getTime = () => { + let hours = 0; + let minutes = 0; if (!Number.isNaN(sleepTime)) { - const hours = Math.round(sleepTime / 3600); + hours = Math.round(sleepTime / 3600); const timeRemaining = sleepTime % 3600; - let minutes = 0; - if (timeRemaining > 60) { + if (timeRemaining >= 60) { minutes = Math.round(timeRemaining / 60); } - return `${hours} hrs ${minutes} min`; - } else { - return 0; } + + return `${hours} hrs ${minutes} min`; } + const handleEnabledSleeper = useCallback((value: boolean) => { + dispatch(setEnabledSleeper(value)); + setEnableSleeper(value); + + // If value is true sleeper has been enabled, pause then play tack + // to trigger play state and start sleeper timer + if (value) { + TrackPlayer.pause() + TrackPlayer.play() + } + }, [dispatch]); + return ( Set Sleep Timer. Time Set Previously: {getTime()} - - - H: - { - setSleeper(parseInt(value), 'Hours'); - }} - /> - - - M: - { - setSleeper(parseInt(value), 'Minutes'); - }} - /> - + + handleEnabledSleeper(value)} + /> + Enable Sleeper + + + + + H: + { + setSleeper(parseInt(value), 'Hours'); + }} + /> + + + M: + { + setSleeper(parseInt(value), 'Minutes'); + }} + /> + + + Set this to automatically stop the audio when time runs out. - Set this to automatically stop the audio when time runs out. ); diff --git a/src/screens/Settings/stacks/timer/styles.tsx b/src/screens/Settings/stacks/timer/styles.tsx index 78f0c44..d1f93b7 100644 --- a/src/screens/Settings/stacks/timer/styles.tsx +++ b/src/screens/Settings/stacks/timer/styles.tsx @@ -16,6 +16,18 @@ export function generateTimerStyles() { flexDirection: 'row', alignItems: 'center', gap: 3 + }, + checkbox: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center' + }, + timerSetting: { + marginStart: 30 + }, + timerSettingsDisabled: { + color: '#cbcbcb', + marginStart: 30 } }) } \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index c5aa4fa..71e8134 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -63,7 +63,8 @@ const persistConfig: PersistConfig> = { ...state.settings, enableSleepTimer: false, sleepTime: 0, - remainingSleepTime: 0 + remainingSleepTime: 0, + enabledSleeper: false } }; }, diff --git a/src/store/settings/actions.ts b/src/store/settings/actions.ts index 029ef86..97ea6a1 100644 --- a/src/store/settings/actions.ts +++ b/src/store/settings/actions.ts @@ -10,3 +10,4 @@ export const setColorScheme = createAction('SET_COLOR_SCHEME'); export const setSleepTime = createAction('SET_SLEEP_TIME'); export const setEnableSleepTimer = createAction('SET_ENABLE_SLEEP_TIMER'); export const setRemainingSleepTime = createAction('SET_REMAINING_SLEEP_TIME'); +export const setEnabledSleeper = createAction('SET_ENABLE_SLEEPER'); diff --git a/src/store/settings/index.ts b/src/store/settings/index.ts index 6e0dd32..7248d44 100644 --- a/src/store/settings/index.ts +++ b/src/store/settings/index.ts @@ -1,5 +1,5 @@ import { createReducer } from '@reduxjs/toolkit'; -import { setReceivedErrorReportingAlert, setBitrate, setJellyfinCredentials, setOnboardingStatus, setEnablePlaybackReporting, setColorScheme, setSleepTime, setEnableSleepTimer, setRemainingSleepTime } from './actions'; +import { setReceivedErrorReportingAlert, setBitrate, setJellyfinCredentials, setOnboardingStatus, setEnablePlaybackReporting, setColorScheme, setSleepTime, setEnableSleepTimer, setRemainingSleepTime, setEnabledSleeper } from './actions'; import { ColorScheme } from './types'; interface State { @@ -15,6 +15,7 @@ interface State { enablePlaybackReporting: boolean; colorScheme: ColorScheme; sleepTime: number; + enabledSleeper: boolean } const initialState: State = { @@ -24,6 +25,7 @@ const initialState: State = { enablePlaybackReporting: true, colorScheme: ColorScheme.System, sleepTime: 60, + enabledSleeper: false, }; const settings = createReducer(initialState, builder => { @@ -63,6 +65,10 @@ const settings = createReducer(initialState, builder => { ...state, remainingSleepTime: action.payload, })); + builder.addCase(setEnabledSleeper, (state, action) => ({ + ...state, + enabledSleeper: action.payload, + })); }); export default settings; \ No newline at end of file diff --git a/src/utility/PlaybackService.ts b/src/utility/PlaybackService.ts index 4ae9dc5..a170543 100644 --- a/src/utility/PlaybackService.ts +++ b/src/utility/PlaybackService.ts @@ -8,10 +8,15 @@ */ import TrackPlayer, { Event, State } from 'react-native-track-player'; -import store from '@/store'; +import store, { useTypedSelector } from '@/store'; import { sendPlaybackEvent } from './JellyfinApi'; +import { useDispatch } from 'react-redux'; +import { setSleepTime } from '@/store/settings/actions'; +import internal from 'stream'; export default async function() { + let interval = setInterval(() => {}, 0); + TrackPlayer.addEventListener(Event.RemotePlay, () => { TrackPlayer.play(); }); @@ -59,6 +64,11 @@ export default async function() { if (settings.enablePlaybackReporting) { sendPlaybackEvent('/Sessions/Playing/Progress', settings.jellyfin); } + + // regularly check if sleeper is enabled, then disable the timer + if (!settings.enabledSleeper) { + clearInterval(interval); + } }); TrackPlayer.addEventListener(Event.PlaybackState, (event) => { @@ -72,6 +82,24 @@ export default async function() { sendPlaybackEvent('/Sessions/Playing/Stopped', settings.jellyfin); } } + + // Handle is playback state is playing + if (event.state === State.Playing) { + const settings = store.getState().settings; + + // Start timer is sleeper is enabled + if (settings.enabledSleeper) { + let time = settings.sleepTime; + interval = setInterval(() => { + if (time > 0) { + time -= 1; + } else { + TrackPlayer.pause(); + clearInterval(interval); + } + }, 1000); + } + } }); } \ No newline at end of file