Merge pull request #168 from ben-mathu/feature/sleeper-timer

Timer to Stop Music
This commit is contained in:
Lei Nelissen
2024-01-29 00:24:19 +01:00
committed by GitHub
16 changed files with 1399 additions and 12446 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View 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

View File

@@ -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;
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -69,3 +69,4 @@ export type LocaleKeys = 'play-next'
| 'color-scheme-dark'
| 'artists'
| 'privacy-policy'
| 'sleep-timer'

View File

@@ -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`

View 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>
);
}

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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);

View 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;

View File

@@ -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) => {

View File

@@ -8,7 +8,6 @@
],
},
"types": [
"@types/styled-components-react-native",
"@types/node"
],
},