Merge pull request #168 from ben-mathu/feature/sleeper-timer
Timer to Stop Music
This commit is contained in:
@@ -447,6 +447,8 @@ PODS:
|
||||
- React-perflogger (= 0.71.15)
|
||||
- RNCAsyncStorage (1.17.11):
|
||||
- React-Core
|
||||
- RNDateTimePicker (7.6.2):
|
||||
- React-Core
|
||||
- RNFastImage (8.6.3):
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.11.1)
|
||||
@@ -552,6 +554,7 @@ DEPENDENCIES:
|
||||
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNFastImage (from `../node_modules/react-native-fast-image`)
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
@@ -670,6 +673,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
RNCAsyncStorage:
|
||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNFastImage:
|
||||
:path: "../node_modules/react-native-fast-image"
|
||||
RNFS:
|
||||
@@ -746,6 +751,7 @@ SPEC CHECKSUMS:
|
||||
React-runtimeexecutor: 8f2ddd9db7874ec7de84f5c55d73aeaaf82908e2
|
||||
ReactCommon: 309d965cb51f058d07dea65bc04dcf462911f0a4
|
||||
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
||||
RNDateTimePicker: fc2e4f2795877d45e84d85659bebe627eba5c931
|
||||
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
|
||||
|
||||
13614
package-lock.json
generated
13614
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
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/datetimepicker": "^7.6.0",
|
||||
"@react-native-community/netinfo": "^9.3.6",
|
||||
"@react-navigation/bottom-tabs": "^6.4.0",
|
||||
"@react-navigation/elements": "^1.3.17",
|
||||
@@ -43,6 +44,7 @@
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "^2.9.0",
|
||||
"react-native-localize": "^2.2.4",
|
||||
"react-native-modal-datetime-picker": "^17.0.0",
|
||||
"react-native-reanimated": "^3.6.2",
|
||||
"react-native-safe-area-context": "^4.4.1",
|
||||
"react-native-screens": "^3.18.2",
|
||||
@@ -57,7 +59,7 @@
|
||||
"redux-flipper": "^2.0.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
"styled-components": "^5.3.6"
|
||||
"styled-components": "^6.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
@@ -70,9 +72,8 @@
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"@types/redux-logger": "^3.0.9",
|
||||
"@types/styled-components-react-native": "^5.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"babel-jest": "^29.3.1",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"eslint": "^8.27.0",
|
||||
@@ -83,7 +84,7 @@
|
||||
"metro-react-native-babel-transformer": "^0.73.3",
|
||||
"react-native-codegen": "^0.72.0",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "react-native",
|
||||
|
||||
5
src/assets/icons/timer-icon.svg
Normal file
5
src/assets/icons/timer-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M7.99498 13.1613C10.8527 13.1613 13.1931 10.8209 13.1931 7.96316C13.1931 5.11048 10.8577 2.77008 8 2.77008C7.68861 2.77008 7.51283 2.95591 7.51283 3.25725V5.10546C7.51283 5.3616 7.69364 5.56249 7.94977 5.56249C8.20591 5.56249 8.38672 5.3616 8.38672 5.10546V3.81472C10.5112 4.01059 12.1484 5.7885 12.1484 7.96316C12.1484 10.2634 10.3052 12.1216 7.99498 12.1216C5.68973 12.1216 3.83649 10.2634 3.84152 7.96316C3.84654 6.97376 4.19308 6.05468 4.77567 5.34653C4.96652 5.08537 4.99665 4.80914 4.78069 4.59318C4.56473 4.37722 4.21819 4.39731 3.99219 4.69363C3.2539 5.5876 2.8019 6.72767 2.8019 7.96316C2.8019 10.8209 5.1423 13.1613 7.99498 13.1613ZM8.80357 8.7366C9.2154 8.29966 9.13002 7.70702 8.63783 7.37053L6.08649 5.61774C5.78515 5.41684 5.49386 5.71316 5.69475 6.0145L7.44754 8.56082C7.78404 9.05803 8.37667 9.14843 8.80357 8.7366Z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 957 B |
@@ -1,9 +1,9 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { PropsWithChildren, useCallback } from 'react';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||
import useDefaultStyles from './Colors';
|
||||
|
||||
interface Props {
|
||||
interface Props extends PropsWithChildren {
|
||||
fullSize?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,5 +70,6 @@
|
||||
"color-scheme-light": "Light Mode",
|
||||
"color-scheme-dark": "Dark Mode",
|
||||
"artists": "Artists",
|
||||
"privacy-policy": "Privacy Policy"
|
||||
"privacy-policy": "Privacy Policy",
|
||||
"sleep-timer": "Sleep timer"
|
||||
}
|
||||
|
||||
@@ -70,5 +70,6 @@
|
||||
"color-scheme-system": "Systeem",
|
||||
"color-scheme-light": "Lichte modus",
|
||||
"color-scheme-dark": "Donkere modus",
|
||||
"privacy-policy": "Privacybeleid"
|
||||
"privacy-policy": "Privacybeleid",
|
||||
"sleep-timer": "Slaaptimer"
|
||||
}
|
||||
|
||||
@@ -69,3 +69,4 @@ export type LocaleKeys = 'play-next'
|
||||
| 'color-scheme-dark'
|
||||
| 'artists'
|
||||
| 'privacy-policy'
|
||||
| 'sleep-timer'
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import useQueue from '@/utility/useQueue';
|
||||
import { View, StyleSheet, ListRenderItemInfo } from 'react-native';
|
||||
import { View, StyleSheet, ListRenderItemInfo, FlatList } from 'react-native';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import useCurrentTrack from '@/utility/useCurrentTrack';
|
||||
import TouchableHandler from '@/components/TouchableHandler';
|
||||
@@ -18,7 +18,7 @@ import ticksToDuration from '@/utility/ticksToDuration';
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
|
||||
const Container = styled.FlatList<Track>`
|
||||
const Container = styled(FlatList<Track>)`
|
||||
|
||||
`;
|
||||
|
||||
@@ -27,7 +27,7 @@ const Header = styled.View`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 52px;
|
||||
padding-top: 27px;
|
||||
`;
|
||||
|
||||
const IconButton = styled.TouchableOpacity`
|
||||
|
||||
136
src/screens/modals/Player/components/Timer.tsx
Normal file
136
src/screens/modals/Player/components/Timer.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import DateTimePickerModal from 'react-native-modal-datetime-picker';
|
||||
import styled from 'styled-components/native';
|
||||
import { THEME_COLOR } from '@/CONSTANTS';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTypedSelector } from '@/store';
|
||||
import TimerIcon from '@/assets/icons/timer-icon.svg';
|
||||
import { setTimerDate } from '@/store/sleep-timer';
|
||||
import ticksToDuration from '@/utility/ticksToDuration';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { t } from '@/localisation';
|
||||
|
||||
const Container = styled.View`
|
||||
align-self: flex-start;
|
||||
align-items: flex-start;
|
||||
margin-top: 52px;
|
||||
padding: 8px;
|
||||
margin-left: -8px;
|
||||
flex: 0 1 auto;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
const View = styled.View`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const Label = styled.Text`
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
export default function Timer() {
|
||||
// Keep an incrementing counter to force the component to update when the
|
||||
// interval is active.
|
||||
const [, setCounter] = useState(0);
|
||||
|
||||
// Show the picker or not
|
||||
const [showPicker, setShowPicker] = useState<boolean>(false);
|
||||
|
||||
// Retrieve Redux state and methods
|
||||
const date = useTypedSelector(state => state.sleepTimer.date);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Retrieve styles
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// Deal with a date being selected
|
||||
const handleConfirm = useCallback((date: Date) => {
|
||||
// GUARD: If the date is in the past, we need to add 24 hours to it to
|
||||
// ensure it is in the future
|
||||
if (date.getTime() < new Date().getTime()) {
|
||||
date = new Date(date.getTime() + 24 * 60 * 60 * 1_000);
|
||||
}
|
||||
|
||||
// Only accept minutes and hours
|
||||
date.setSeconds(0);
|
||||
|
||||
// Set the date and close the picker
|
||||
dispatch(setTimerDate(date));
|
||||
setShowPicker(false);
|
||||
}, [dispatch]);
|
||||
|
||||
// Close the picker when it is canceled
|
||||
const handleCancelDatePicker = useCallback(() => {
|
||||
setShowPicker(false);
|
||||
}, []);
|
||||
|
||||
// Show it when it should be opened
|
||||
const showDatePicker = useCallback(() => {
|
||||
setShowPicker(!showPicker);
|
||||
}, [showPicker]);
|
||||
|
||||
// Periodically trigger updates
|
||||
useEffect(() => {
|
||||
// GUARD: If there's no date, there's no need to update
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set an interval that periodically increments the counter when a date
|
||||
// is active
|
||||
const interval = setInterval(() => {
|
||||
setCounter((i) => i + 1);
|
||||
}, 1_000);
|
||||
|
||||
// Clean up the interval on re-renders
|
||||
return () => clearInterval(interval);
|
||||
}, [date]);
|
||||
|
||||
// Calculate the remaining time by subtracting it from the current date
|
||||
const remainingTime = date && date - new Date().getTime();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={showDatePicker}
|
||||
activeOpacity={0.6}
|
||||
style={{ flexGrow: 0 }}
|
||||
>
|
||||
<Container
|
||||
style={{ backgroundColor: showPicker || date
|
||||
? defaultStyles.activeBackground.backgroundColor
|
||||
: undefined
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<TimerIcon
|
||||
fill={showPicker || date
|
||||
? THEME_COLOR
|
||||
: defaultStyles.textHalfOpacity.color
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
style={{ color: showPicker || date
|
||||
? THEME_COLOR
|
||||
: defaultStyles.textHalfOpacity.color
|
||||
}}
|
||||
>
|
||||
{!remainingTime
|
||||
? t('sleep-timer')
|
||||
: ticksToDuration(remainingTime * 10_000)
|
||||
}
|
||||
</Label>
|
||||
<DateTimePickerModal
|
||||
isVisible={showPicker}
|
||||
mode='time'
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancelDatePicker}
|
||||
/>
|
||||
</View>
|
||||
</Container>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import StreamStatus from './components/StreamStatus';
|
||||
import { Platform } from 'react-native';
|
||||
import BackButton from './components/Backbutton';
|
||||
import Timer from './components/Timer';
|
||||
|
||||
export default function Player() {
|
||||
return (
|
||||
@@ -20,6 +21,7 @@ export default function Player() {
|
||||
<StreamStatus />
|
||||
<ProgressBar />
|
||||
<MediaControls />
|
||||
<Timer />
|
||||
</>
|
||||
)} />
|
||||
</GestureHandlerRootView>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { THEME_COLOR } from '@/CONSTANTS';
|
||||
import { t } from '@/localisation';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import { Text } from '@/components/Typography';
|
||||
import { useAppDispatch } from '@/store';
|
||||
import { AppState, useAppDispatch } from '@/store';
|
||||
|
||||
|
||||
export default function SetJellyfinServer() {
|
||||
@@ -23,9 +23,11 @@ export default function SetJellyfinServer() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
// Save creedentials to store and close the modal
|
||||
const saveCredentials = useCallback((credentials) => {
|
||||
dispatch(setJellyfinCredentials(credentials));
|
||||
navigation.dispatch(StackActions.popToTop());
|
||||
const saveCredentials = useCallback((credentials: AppState['settings']['jellyfin']) => {
|
||||
if (credentials) {
|
||||
dispatch(setJellyfinCredentials(credentials));
|
||||
navigation.dispatch(StackActions.popToTop());
|
||||
}
|
||||
}, [navigation, dispatch]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ import music, { initialState as musicInitialState } from './music';
|
||||
import downloads, { initialState as downloadsInitialState } from './downloads';
|
||||
import { PersistState } from 'redux-persist/es/types';
|
||||
import { ColorScheme } from './settings/types';
|
||||
import sleepTimer from './sleep-timer';
|
||||
|
||||
const persistConfig: PersistConfig<Omit<AppState, '_persist'>> = {
|
||||
key: 'root',
|
||||
@@ -55,6 +56,15 @@ const persistConfig: PersistConfig<Omit<AppState, '_persist'>> = {
|
||||
}
|
||||
};
|
||||
},
|
||||
// @ts-expect-error migrations are poorly typed
|
||||
4: (state: AppState) => {
|
||||
return {
|
||||
...state,
|
||||
sleepTimer: {
|
||||
date: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
@@ -62,6 +72,7 @@ const reducers = combineReducers({
|
||||
settings,
|
||||
music: music.reducer,
|
||||
downloads: downloads.reducer,
|
||||
sleepTimer: sleepTimer.reducer,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, reducers);
|
||||
|
||||
23
src/store/sleep-timer/index.ts
Normal file
23
src/store/sleep-timer/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
export interface State {
|
||||
date: number | null;
|
||||
}
|
||||
|
||||
export const initialState: State = {
|
||||
date: null,
|
||||
};
|
||||
|
||||
const sleepTimer = createSlice({
|
||||
name: 'sleep-timer',
|
||||
initialState,
|
||||
reducers: {
|
||||
setTimerDate(state, action: PayloadAction<Date | null>) {
|
||||
state.date = action.payload?.getTime() || null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const { setTimerDate } = sleepTimer.actions;
|
||||
|
||||
export default sleepTimer;
|
||||
@@ -10,6 +10,7 @@
|
||||
import TrackPlayer, { Event, State } from 'react-native-track-player';
|
||||
import store from '@/store';
|
||||
import { sendPlaybackEvent } from './JellyfinApi';
|
||||
import { setTimerDate } from '@/store/sleep-timer';
|
||||
|
||||
export default async function() {
|
||||
TrackPlayer.addEventListener(Event.RemotePlay, () => {
|
||||
@@ -53,12 +54,18 @@ export default async function() {
|
||||
|
||||
TrackPlayer.addEventListener(Event.PlaybackProgressUpdated, () => {
|
||||
// Retrieve the current settings from the Redux store
|
||||
const settings = store.getState().settings;
|
||||
const { settings, sleepTimer } = store.getState();
|
||||
|
||||
// GUARD: Only report playback when the settings is enabled
|
||||
if (settings.enablePlaybackReporting) {
|
||||
sendPlaybackEvent('/Sessions/Playing/Progress', settings.jellyfin);
|
||||
}
|
||||
|
||||
// check if timerDate is undefined, otherwise start timer
|
||||
if (sleepTimer.date && sleepTimer.date < new Date().valueOf()) {
|
||||
TrackPlayer.pause();
|
||||
store.dispatch(setTimerDate(null));
|
||||
}
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener(Event.PlaybackState, (event) => {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
],
|
||||
},
|
||||
"types": [
|
||||
"@types/styled-components-react-native",
|
||||
"@types/node"
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user