feat: Add Time-Tracking Toggle and Inactivity Timeout (#1890)

* feat: implement on-off switch and inactivity timeout for time-tracking

* chore: remove stray console.log
This commit is contained in:
Ecasept
2026-07-23 05:40:14 +02:00
committed by GitHub
parent d2385b8dc2
commit 4a4208e336
12 changed files with 371 additions and 54 deletions
+22
View File
@@ -1,3 +1,19 @@
window.onUserInteraction = (() => {
let groupStart = 0;
// Group multiple interactions within a short time frame to avoid excessive calls
const groupTime = 3000;
return () => {
let now = Date.now();
if (now - groupStart < groupTime) {
// We're inside the group
} else {
// We're the first in a while
groupStart = now;
reader.post({ type: 'interaction' });
}
}
})();
/* eslint-disable no-console */
window.reader = new (function () {
const {
@@ -100,6 +116,7 @@ window.reader = new (function () {
});
document.onscrollend = () => {
onUserInteraction();
if (!this.generalSettings.val.pageReader) {
this.post({
type: 'save',
@@ -111,6 +128,10 @@ window.reader = new (function () {
}
};
document.onpointerdown = () => onUserInteraction();
document.onpointermove = () => onUserInteraction();
document.onpointerup = () => onUserInteraction();
if (DEBUG) {
// eslint-disable-next-line no-global-assign, no-new-object
console = new Object();
@@ -505,6 +526,7 @@ window.pageReader = new (function () {
};
this.movePage = destPage => {
onUserInteraction();
if (this.chapterEndingVisible.val) {
if (destPage < 0) {
this.showChapterEnding(false);
+14
View File
@@ -80,6 +80,14 @@ export interface AppSettings {
hideBackdrop: boolean;
defaultChapterSort: ChapterOrderKey;
/**
* Time-tracking settings
*/
/** Whether to actually enable time tracking */
timeTrackingEnabled: boolean;
/** The timeout after which to consider the user inactive and not track time */
inactivityTimeoutMs?: number;
}
export interface BrowseSettings {
@@ -188,6 +196,12 @@ const initialAppSettings: AppSettings = {
hideBackdrop: false,
defaultChapterSort: 'positionAsc',
/**
* Time-tracking settings
*/
timeTrackingEnabled: true,
inactivityTimeoutMs: 5 * 60 * 1000, // 5 minutes
};
const initialBrowseSettings: BrowseSettings = {
+6 -3
View File
@@ -65,7 +65,7 @@ export const ChapterContent = ({
openDrawer,
}: ChapterContentProps) => {
const { left, right } = useSafeAreaInsets();
const { novel, chapter } = useChapterContext();
const { novel, chapter, onUserInteraction } = useChapterContext();
const readerSheetRef = useRef<BottomSheetModalMethods>(null);
const theme = useTheme();
const { pageReader = false, keepScreenOn } = useChapterGeneralSettings();
@@ -135,7 +135,8 @@ export const ChapterContent = ({
`);
}, [hidden, searchVisible, webViewRef]);
const scrollToStart = () =>
const scrollToStart = () => {
onUserInteraction();
requestAnimationFrame(() => {
webViewRef?.current?.injectJavaScript(
!pageReader
@@ -147,7 +148,8 @@ export const ChapterContent = ({
document.querySelector("chapter").style.transform = 'translate(0%)';
})()`,
);
});
})
};
const openDrawerI = useCallback(() => {
openDrawer();
@@ -161,6 +163,7 @@ export const ChapterContent = ({
}, [searchVisible]);
const handleReaderPress = useCallback(() => {
onUserInteraction();
if (searchVisible) {
setSearchVisible(false);
return;
@@ -95,6 +95,8 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
nextChapter,
prevChapter,
webViewRef,
onUserInteraction,
isTTSReadingRef
} = useChapterContext();
const theme = useTheme();
const initialReaderSettings = useMemo(
@@ -121,7 +123,6 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
const pluginCustomCSS = `file://${PLUGIN_STORAGE}/${plugin?.id}/custom.css`;
const nextChapterScreenVisible = useRef<boolean>(false);
const autoStartTTSRef = useRef<boolean>(false);
const isTTSReadingRef = useRef<boolean>(false);
const appStateRef = useRef(AppState.currentState);
const ttsQueueRef = useRef<string[]>([]);
const ttsQueueIndexRef = useRef<number>(0);
@@ -210,7 +211,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
isPlaying: isTTSReadingRef.current,
});
}
}, [novel?.name, novel?.cover, chapter.name]);
}, [novel?.name, novel?.cover, chapter.name, isTTSReadingRef]);
useEffect(() => {
return () => {
@@ -301,11 +302,12 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
});
return () => subscription.remove();
}, [webViewRef]);
}, [webViewRef, isTTSReadingRef]);
const speakText = (text: string) => {
Speech.speak(text, {
onDone() {
onUserInteraction();
const isBackground =
appStateRef.current === 'background' ||
appStateRef.current === 'inactive';
@@ -505,10 +507,14 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({
if (event.data && typeof event.data === 'object') {
const data = event.data as { isReading?: boolean };
const isReading = data.isReading === true;
onUserInteraction();
isTTSReadingRef.current = isReading;
updateTTSPlaybackState(isReading);
}
break;
case 'interaction':
onUserInteraction();
break;
}
}}
source={{
@@ -5,6 +5,7 @@ import NativeFile from '@modules/native-file'
const mockUseNovelActions = jest.fn();
const mockUseChapterGeneralSettings = jest.fn();
const mockUseLibrarySettings = jest.fn();
const mockUseAppSettings = jest.fn();
const mockUseTracker = jest.fn();
const mockUseTrackedNovel = jest.fn();
const mockUseFullscreenMode = jest.fn();
@@ -30,6 +31,7 @@ jest.mock('@screens/novel/NovelContext', () => ({
jest.mock('@hooks/persisted', () => ({
useChapterGeneralSettings: () => mockUseChapterGeneralSettings(),
useLibrarySettings: () => mockUseLibrarySettings(),
useAppSettings: () => mockUseAppSettings(),
useTracker: () => mockUseTracker(),
useTrackedNovel: (...args: unknown[]) => mockUseTrackedNovel(...args),
}));
@@ -154,6 +156,7 @@ describe('useChapter', () => {
volumeButtonsOffset: 100,
});
mockUseLibrarySettings.mockReturnValue({ incognitoMode: false });
mockUseAppSettings.mockReturnValue({ timeTrackingEnabled: true, inactivityTimeoutMs: 60000 });
mockUseTracker.mockReturnValue({ tracker: { id: 'tracker' } });
mockUseTrackedNovel.mockReturnValue({
trackedNovel: { progress: 1 },
@@ -24,50 +24,37 @@ describe('useTimeTracking', () => {
jest.useRealTimers();
});
it('flushes elapsed time on the 60s interval', () => {
renderHook(() => useTimeTracking(1, false, increaseTimeSpent));
act(() => {
jest.advanceTimersByTime(60000);
});
expect(increaseTimeSpent).toHaveBeenCalledWith(1, expect.any(Number));
const elapsed = increaseTimeSpent.mock.calls[0][1];
expect(elapsed).toBeGreaterThanOrEqual(60000);
});
it('flushes when the app goes to the background', () => {
renderHook(() => useTimeTracking(1, false, increaseTimeSpent));
renderHook(() => useTimeTracking(1, false, 60000, true, increaseTimeSpent));
act(() => {
jest.advanceTimersByTime(5000);
appStateHandler('background');
});
expect(increaseTimeSpent).toHaveBeenCalledWith(1, expect.any(Number));
expect(increaseTimeSpent).toHaveBeenCalledWith(1, 5000);
});
it('does not double count after backgrounding and foregrounding', () => {
renderHook(() => useTimeTracking(1, false, increaseTimeSpent));
renderHook(() => useTimeTracking(1, false, 60000, true, increaseTimeSpent));
act(() => {
jest.advanceTimersByTime(5000);
appStateHandler('background'); // flush 5s
appStateHandler('background');
jest.advanceTimersByTime(20000);
appStateHandler('active');
jest.advanceTimersByTime(3000);
appStateHandler('background'); // flush 3s
});
const firstElapsed = increaseTimeSpent.mock.calls[0][1];
expect(firstElapsed).toBeLessThan(6000); // 5s flush
const secondElapsed = increaseTimeSpent.mock.calls[1][1];
expect(secondElapsed).toBeLessThan(4000); // 3s flush
expect(increaseTimeSpent).toHaveBeenNthCalledWith(1, 1, 5000);
expect(increaseTimeSpent).toHaveBeenNthCalledWith(2, 1, 3000);
expect(increaseTimeSpent).toHaveBeenCalledTimes(2);
});
it('flushes remaining time on unmount', () => {
const { unmount } = renderHook(() =>
useTimeTracking(1, false, increaseTimeSpent),
useTimeTracking(1, false, 60000, true, increaseTimeSpent),
);
act(() => {
@@ -76,13 +63,13 @@ describe('useTimeTracking', () => {
unmount();
expect(increaseTimeSpent).toHaveBeenLastCalledWith(1, expect.any(Number));
expect(increaseTimeSpent).toHaveBeenLastCalledWith(1, 10000);
});
it('flushes for the previous chapter id when chapterId changes', () => {
const { rerender } = renderHook(
({ chapterId }: { chapterId: number }) =>
useTimeTracking(chapterId, false, increaseTimeSpent),
useTimeTracking(chapterId, false, 60000, true, increaseTimeSpent),
{ initialProps: { chapterId: 1 } },
);
@@ -92,17 +79,175 @@ describe('useTimeTracking', () => {
rerender({ chapterId: 2 });
expect(increaseTimeSpent).toHaveBeenCalledWith(1, expect.any(Number));
expect(increaseTimeSpent).toHaveBeenCalledWith(1, 10000);
expect(increaseTimeSpent).not.toHaveBeenCalledWith(2, expect.any(Number));
});
it('does not track time in incognito mode', () => {
renderHook(() => useTimeTracking(1, true, increaseTimeSpent));
it('flushes after user interaction', () => {
const onUserInteraction = renderHook(() =>
useTimeTracking(1, false, 60000, true, increaseTimeSpent),
).result.current.onUserInteraction;
act(() => {
jest.advanceTimersByTime(60000);
jest.advanceTimersByTime(10000);
onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenCalledWith(1, 10000);
});
it('does not track time when time tracking is turned off', () => {
const onUserInteraction = renderHook(() => useTimeTracking(1, false, 60000, false, increaseTimeSpent)).result.current.onUserInteraction;
act(() => {
jest.advanceTimersByTime(6000);
onUserInteraction();
});
expect(increaseTimeSpent).not.toHaveBeenCalled();
});
it('does not track time when the user is inactive for too long', () => {
const onUserInteraction = renderHook(() => useTimeTracking(1, false, 60000, true, increaseTimeSpent)).result.current.onUserInteraction;
act(() => {
jest.advanceTimersByTime(70000);
onUserInteraction();
});
expect(increaseTimeSpent).not.toHaveBeenCalled();
});
it('resets inactivity timeout correctly', () => {
const onUserInteraction = renderHook(() => useTimeTracking(1, false, 60000, true, increaseTimeSpent)).result.current.onUserInteraction;
act(() => {
jest.advanceTimersByTime(30000);
onUserInteraction();
jest.advanceTimersByTime(30000);
onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenNthCalledWith(1, 1, 30000);
expect(increaseTimeSpent).toHaveBeenNthCalledWith(2, 1, 30000);
expect(increaseTimeSpent).toHaveBeenCalledTimes(2);
});
it('tracks time accurately when the user interacts multiple times in quick succession', () => {
const onUserInteraction = renderHook(() => useTimeTracking(1, false, 60000, true, increaseTimeSpent)).result.current.onUserInteraction;
act(() => {
jest.advanceTimersByTime(30000);
onUserInteraction();
onUserInteraction(); // clock didn't advance, so shouldn't call increaseTimeSpent again
jest.advanceTimersByTime(2);
onUserInteraction();
jest.advanceTimersByTime(20000);
onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenNthCalledWith(1, 1, 30000);
expect(increaseTimeSpent).toHaveBeenNthCalledWith(2, 1, 2);
expect(increaseTimeSpent).toHaveBeenNthCalledWith(3, 1, 20000);
expect(increaseTimeSpent).toHaveBeenCalledTimes(3);
});
it('tracks time accurately even when the user goes inactive for a while', () => {
const onUserInteraction = renderHook(() => useTimeTracking(1, false, 60000, true, increaseTimeSpent)).result.current.onUserInteraction;
act(() => {
jest.advanceTimersByTime(30000);
onUserInteraction();
jest.advanceTimersByTime(10000);
onUserInteraction();
jest.advanceTimersByTime(50000);
onUserInteraction();
// Inactivity
jest.advanceTimersByTime(100000);
onUserInteraction(); // shouldn't do anything
jest.advanceTimersByTime(30000);
onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenNthCalledWith(1, 1, 30000);
expect(increaseTimeSpent).toHaveBeenNthCalledWith(2, 1, 10000);
expect(increaseTimeSpent).toHaveBeenNthCalledWith(3, 1, 50000);
expect(increaseTimeSpent).toHaveBeenNthCalledWith(4, 1, 30000);
expect(increaseTimeSpent).toHaveBeenCalledTimes(4);
});
it('does not track time in incognito mode', () => {
const onUserInteraction = renderHook(() => useTimeTracking(1, true, 60000, true, increaseTimeSpent)).result.current.onUserInteraction;
act(() => {
jest.advanceTimersByTime(6000);
onUserInteraction();
});
expect(increaseTimeSpent).not.toHaveBeenCalled();
});
it('tracks time when TTS is reading even if the user is inactive for too long', () => {
const { result } = renderHook(() =>
useTimeTracking(1, false, 60000, true, increaseTimeSpent),
);
act(() => {
result.current.isTTSReadingRef.current = true;
jest.advanceTimersByTime(70000);
result.current.onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenCalledWith(1, 70000);
});
it('tracks time when TTS is reading even if the app goes to the background', () => {
const { result } = renderHook(() =>
useTimeTracking(1, false, 60000, true, increaseTimeSpent),
);
act(() => {
result.current.isTTSReadingRef.current = true;
jest.advanceTimersByTime(70000);
appStateHandler('background');
jest.advanceTimersByTime(10000);
result.current.onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenNthCalledWith(1, 1, 70000); // on background
expect(increaseTimeSpent).toHaveBeenNthCalledWith(2, 1, 10000); // on user interaction after background
});
it('does not double-count when onUserInteraction is called back-to-back during TTS', () => {
const { result } = renderHook(() =>
useTimeTracking(1, false, 60000, true, increaseTimeSpent),
);
act(() => {
result.current.isTTSReadingRef.current = true;
jest.advanceTimersByTime(5000);
result.current.onUserInteraction();
result.current.onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenNthCalledWith(1, 1, 5000);
expect(increaseTimeSpent).toHaveBeenCalledTimes(1);
});
it('resumes normal inactivity gating once TTS stops', () => {
const { result } = renderHook(() =>
useTimeTracking(1, false, 60000, true, increaseTimeSpent),
);
act(() => {
result.current.isTTSReadingRef.current = true;
jest.advanceTimersByTime(5000);
result.current.onUserInteraction();
result.current.isTTSReadingRef.current = false;
jest.advanceTimersByTime(70000);
result.current.onUserInteraction();
});
expect(increaseTimeSpent).toHaveBeenNthCalledWith(1, 1, 5000);
expect(increaseTimeSpent).toHaveBeenCalledTimes(1);
});
});
+7 -1
View File
@@ -8,6 +8,7 @@ import {
import { insertHistory } from '@database/queries/HistoryQueries';
import { ChapterInfo, NovelInfo } from '@database/types';
import {
useAppSettings,
useChapterGeneralSettings,
useLibrarySettings,
useTrackedNovel,
@@ -70,12 +71,13 @@ export default function useChapter(
volumeButtonsOffset,
} = useChapterGeneralSettings();
const { incognitoMode } = useLibrarySettings();
const { timeTrackingEnabled, inactivityTimeoutMs } = useAppSettings();
const [error, setError] = useState<string>();
const { tracker } = useTracker();
const { trackedNovel, updateAllTrackedNovels } = useTrackedNovel(novel.id);
const { setImmersiveMode, showStatusAndNavBar } = useFullscreenMode();
useTimeTracking(chapter.id, incognitoMode || false, increaseTimeSpent);
const { onUserInteraction, isTTSReadingRef } = useTimeTracking(chapter.id, incognitoMode || false, inactivityTimeoutMs, timeTrackingEnabled, increaseTimeSpent);
const connectVolumeButton = useCallback(() => {
const offset = defaultTo(
@@ -411,6 +413,8 @@ export default function useChapter(
setChapter,
setLoading,
getChapter,
onUserInteraction,
isTTSReadingRef,
}),
[
hidden,
@@ -431,6 +435,8 @@ export default function useChapter(
setChapter,
setLoading,
getChapter,
onUserInteraction,
isTTSReadingRef,
],
);
}
+25 -17
View File
@@ -3,33 +3,33 @@ import { AppState, AppStateStatus } from 'react-native';
/**
* Starts tracking time spent on the chapter, and flushes the data to the database:
* - periodically every minute
* - when the app goes in the background
* - when the chapter changes
* Starts tracking time spent on the chapter and returns a callback to be called on user interaction (to prevent the inactivity timeout from being triggered).
*/
export default function useTimeTracking(
chapterId: number,
incognitoMode: boolean,
inactivityTimeoutMs: number | undefined,
turnedOn: boolean,
increaseTimeSpent: (chapterId: number, elapsed: number) => void,
) {
const startTimestampRef = useRef(Date.now());
const lastUserInteractionRef = useRef(Date.now());
const isTTSReadingRef = useRef(false);
/** Increases the time spent on the current chapter since the last time this function was called */
/** Increases the time spent on the current chapter since the last user interaction, as long as it was not too long ago */
const flushElapsedTime = useCallback(() => {
if (!incognitoMode) {
const elapsed = Date.now() - startTimestampRef.current;
const enabled = turnedOn && !incognitoMode;
const isInactive = inactivityTimeoutMs !== undefined && Date.now() - lastUserInteractionRef.current > inactivityTimeoutMs;
if (enabled && (!isInactive || isTTSReadingRef.current)) {
const elapsed = Date.now() - lastUserInteractionRef.current;
if (elapsed > 0) {
increaseTimeSpent(chapterId, elapsed);
}
}
startTimestampRef.current = Date.now();
}, [chapterId, incognitoMode, increaseTimeSpent]);
}, [chapterId, incognitoMode, inactivityTimeoutMs, turnedOn, increaseTimeSpent]);
// Flush the elapsed time every minute
useEffect(() => {
const intervalId = setInterval(flushElapsedTime, 60000);
return () => clearInterval(intervalId);
const onUserInteraction = useCallback(() => {
flushElapsedTime();
lastUserInteractionRef.current = Date.now();
}, [flushElapsedTime]);
// Flush the elapsed time when the app goes in the background
@@ -39,9 +39,16 @@ export default function useTimeTracking(
if (appState === 'active' && nextState.match(/inactive|background/)) {
// Save the elapsed time when the app goes in the background
flushElapsedTime();
lastUserInteractionRef.current = Date.now();
} else if (appState.match(/inactive|background/) && nextState === 'active') {
// Reset the start timestamp when the app comes back
startTimestampRef.current = Date.now();
// Reset the start timestamp when the app comes back and the TTS isn't on
if (!isTTSReadingRef.current) {
lastUserInteractionRef.current = Date.now();
} else {
// If TTS is on, we want to count the time spent in the background as well
flushElapsedTime();
lastUserInteractionRef.current = Date.now();
}
}
appState = nextState;
};
@@ -51,9 +58,10 @@ export default function useTimeTracking(
// Flush the elapsed time when the chapter changes
useEffect(() => {
startTimestampRef.current = Date.now();
lastUserInteractionRef.current = Date.now();
return () => {
flushElapsedTime();
};
}, [chapterId, flushElapsedTime]);
return { onUserInteraction, isTTSReadingRef };
}
@@ -24,6 +24,7 @@ import NovelBadgesModal from './modals/NovelBadgesModal';
import { NavigationState } from '@react-navigation/native';
import { getString } from '@strings/translations';
import SettingSwitch from '../components/SettingSwitch';
import InactivityTimeoutModal from './modals/InactivityTimeoutModal';
interface GenralSettingsProps {
navigation: NavigationState;
@@ -60,6 +61,8 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
disableHapticFeedback,
useLibraryFAB,
chapterDownloadCooldownMs,
timeTrackingEnabled,
inactivityTimeoutMs,
setAppSettings,
} = useAppSettings();
@@ -106,6 +109,10 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
* Download Cooldown Modal
*/
const downloadCooldownModalRef = useBoolean();
/**
* Inactivity Timeout Modal
*/
const inactivityTimeoutModalRef = useBoolean();
return (
<SafeAreaView excludeTop>
<Appbar
@@ -228,6 +235,27 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
{getString('generalSettingsScreen.timeTracking')}
</List.SubHeader>
<SettingSwitch
label={getString('generalSettingsScreen.enableTimeTracking')}
value={timeTrackingEnabled}
description={getString('generalSettingsScreen.enableTimeTrackingDesc')}
onPress={() =>
setAppSettings({ timeTrackingEnabled: !timeTrackingEnabled })
}
theme={theme}
/>
<List.Item
title={getString('generalSettingsScreen.inactivityTimeout')}
description={inactivityTimeoutMs === undefined
? getString('generalSettingsScreen.inactivityTimeoutNever')
: getString('time.minutes', { count: inactivityTimeoutMs / 60000 })}
onPress={inactivityTimeoutModalRef.setTrue}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
{getString('generalSettings')}
</List.SubHeader>
@@ -299,6 +327,12 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
hideModal={downloadCooldownModalRef.setFalse}
theme={theme}
/>
<InactivityTimeoutModal
inactivityTimeoutMs={inactivityTimeoutMs}
modalVisible={inactivityTimeoutModalRef.value}
hideModal={inactivityTimeoutModalRef.setFalse}
theme={theme}
/>
</SafeAreaView>
);
};
@@ -0,0 +1,64 @@
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { Portal } from 'react-native-paper';
import { RadioButton } from '@components/RadioButton/RadioButton';
import { ThemeColors } from '@theme/types';
import { useAppSettings } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { Modal } from '@components';
interface InactivityTimeoutModalProps {
inactivityTimeoutMs: number | undefined;
modalVisible: boolean;
hideModal: () => void;
theme: ThemeColors;
}
const InactivityTimeoutModal: React.FC<InactivityTimeoutModalProps> = ({
theme,
inactivityTimeoutMs,
hideModal,
modalVisible,
}) => {
const { setAppSettings } = useAppSettings();
return (
<Portal>
<Modal visible={modalVisible} onDismiss={hideModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
{getString('generalSettingsScreen.inactivityTimeout')}
</Text>
<Text style={{ color: theme.onSurface }}>
{getString('generalSettingsScreen.inactivityTimeoutDesc')}
</Text>
<RadioButton
key={0}
status={inactivityTimeoutMs === undefined}
label={getString('generalSettingsScreen.inactivityTimeoutNever')}
onPress={() => setAppSettings({ inactivityTimeoutMs: undefined })}
theme={theme}
/>
{[1, 3, 5, 10, 15, 30].map(time => (
<RadioButton
key={time}
status={inactivityTimeoutMs === time * 60 * 1000}
onPress={() => setAppSettings({ inactivityTimeoutMs: time * 60 * 1000 })}
label={`${getString('time.minutes', { count: time })}`}
theme={theme}
/>
))}
</Modal>
</Portal>
);
};
export default InactivityTimeoutModal;
const styles = StyleSheet.create({
modalHeader: {
fontSize: 24,
marginBottom: 10,
},
});
+9 -3
View File
@@ -305,7 +305,13 @@
"updateLibraryDesc": "Not recommended for low devices",
"updateOngoing": "Only update ongoing novels",
"updateTime": "Show last update time",
"useFAB": "Use FAB in Library"
"useFAB": "Use FAB in Library",
"timeTracking": "Time Tracking",
"enableTimeTracking": "Enable time tracking",
"enableTimeTrackingDesc": "Tracks time spent reading and displays it in the statistics screen",
"inactivityTimeout": "Inactivity Timeout",
"inactivityTimeoutNever": "Never",
"inactivityTimeoutDesc": "Automatically pause time tracking after a period of inactivity"
},
"globalSearch": {
"allSources": "all sources",
@@ -543,8 +549,8 @@
"topNovelsByTimeSpent": "Top novels by time spent",
"topCategoriesByTimeSpent": "Top categories by time spent",
"totalTimeSpent": "Total time spent",
"showNovels": "Group by novel",
"showCategories": "Group by category"
"showNovels": "Group by novel",
"showCategories": "Group by category"
},
"tracking": "Tracking",
"trackingScreen": {
+6
View File
@@ -269,6 +269,12 @@ export interface StringMap {
'generalSettingsScreen.updateOngoing': 'string';
'generalSettingsScreen.updateTime': 'string';
'generalSettingsScreen.useFAB': 'string';
'generalSettingsScreen.timeTracking': 'string';
'generalSettingsScreen.enableTimeTracking': 'string';
'generalSettingsScreen.enableTimeTrackingDesc': 'string';
'generalSettingsScreen.inactivityTimeout': 'string';
'generalSettingsScreen.inactivityTimeoutNever': 'string';
'generalSettingsScreen.inactivityTimeoutDesc': 'string';
'globalSearch.allSources': 'string';
'globalSearch.searchIn': 'string';
'history': 'string';