make default reader settings changeable (#217)

* [#214] Move "reader settings" util functions into new file

* [#214] Extract "options" of "ReaderSettings" to new component

* [#214] Add component for changing the default reader settings

* [#214] Add default "ReaderSettings" to "Settings"

* [#214] Use default "ReaderSettings" stored on the server

Wait for both "manga" and "defaultSettings" to be loaded.
Otherwise, by the time the "manga" response was received, the "defaultSettings" might be still loading and thus, the local fallback "default" settings will get used as the "default settings"

* [#214] Save and update "ReaderSettings" options in case they are missing

- Manga gets read the first time => save settings in metadata
- Default settings get opened the first time => save settings in global metadata
This commit is contained in:
Daniel
2023-01-06 17:34:16 +01:00
committed by GitHub
parent 46ac5d5531
commit dca968fdb7
7 changed files with 287 additions and 98 deletions

View File

@@ -24,6 +24,7 @@ import Settings from 'screens/Settings';
import About from 'screens/settings/About'; import About from 'screens/settings/About';
import Backup from 'screens/settings/Backup'; import Backup from 'screens/settings/Backup';
import Categories from 'screens/settings/Categories'; import Categories from 'screens/settings/Categories';
import DefaultReaderSettings from 'screens/settings/DefaultReaderSettings';
import SourceConfigure from 'screens/SourceConfigure'; import SourceConfigure from 'screens/SourceConfigure';
import SourceMangas from 'screens/SourceMangas'; import SourceMangas from 'screens/SourceMangas';
import Sources from 'screens/Sources'; import Sources from 'screens/Sources';
@@ -60,6 +61,9 @@ const App: React.FC = () => (
<Route path="/settings/categories"> <Route path="/settings/categories">
<Categories /> <Categories />
</Route> </Route>
<Route path="/settings/defaultReaderSettings">
<DefaultReaderSettings />
</Route>
<Route path="/settings/backup"> <Route path="/settings/backup">
<Backup /> <Backup />
</Route> </Route>

View File

@@ -17,18 +17,15 @@ import { useHistory, Link, useLocation } from 'react-router-dom';
import Slide from '@mui/material/Slide'; import Slide from '@mui/material/Slide';
import Fade from '@mui/material/Fade'; import Fade from '@mui/material/Fade';
import Zoom from '@mui/material/Zoom'; import Zoom from '@mui/material/Zoom';
import { Divider, Switch } from '@mui/material'; import { Divider } from '@mui/material';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem'; import ListItem from '@mui/material/ListItem';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { styled } from '@mui/system'; import { styled } from '@mui/system';
import useBackTo from 'util/useBackTo'; import useBackTo from 'util/useBackTo';
import { getMetadataFrom } from 'util/metadata'; import ReaderSettingsOptions from '../reader/ReaderSettingsOptions';
const Root = styled('div')(({ theme }) => ({ const Root = styled('div')(({ theme }) => ({
top: 0, top: 0,
@@ -106,27 +103,6 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
}, },
})); }));
const defaultReaderSettings = () => ({
staticNav: false,
showPageNumber: true,
continuesPageGap: false,
loadNextonEnding: false,
readerType: 'ContinuesVertical',
} as IReaderSettings);
const getReaderSettingsFromMetadata = (
meta?: IMetadata,
): IReaderSettings => ({
...getMetadataFrom(
{ meta },
Object.entries(defaultReaderSettings()) as MetadataKeyValuePair[],
) as unknown as IReaderSettings,
});
export const getReaderSettingsFor = (
{ meta }: IMangaCard | IManga,
): IReaderSettings => getReaderSettingsFromMetadata(meta);
interface IProps { interface IProps {
settings: IReaderSettings settings: IReaderSettings
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void
@@ -276,75 +252,13 @@ export default function ReaderNavBar(props: IProps) {
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit> <Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
<List> <ReaderSettingsOptions
<ListItem> setSettingValue={updateSettingValue}
<ListItemText primary="Static Navigation" /> staticNav={settings.staticNav}
<ListItemSecondaryAction> showPageNumber={settings.showPageNumber}
<Switch loadNextonEnding={settings.loadNextonEnding}
edge="end" readerType={settings.readerType}
checked={settings.staticNav} />
onChange={(e) => updateSettingValue('staticNav', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Show page number" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.showPageNumber}
onChange={(e) => updateSettingValue('showPageNumber', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Load next chapter at ending" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={settings.loadNextonEnding}
onChange={(e) => updateSettingValue('loadNextonEnding', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Reader Type" />
<Select
variant="standard"
value={settings.readerType}
onChange={(e) => updateSettingValue('readerType', e.target.value)}
sx={{ p: 0 }}
>
<MenuItem value="SingleLTR">
Single Page (LTR)
</MenuItem>
<MenuItem value="SingleRTL">
Single Page (RTL)
</MenuItem>
{/* <MenuItem value="SingleVertical">
Vertical(WIP)
</MenuItem> */}
<MenuItem value="DoubleLTR">
Double Page (LTR)
</MenuItem>
<MenuItem value="DoubleRTL">
Double Page (RTL)
</MenuItem>
<MenuItem value="Webtoon">
Webtoon
</MenuItem>
<MenuItem value="ContinuesVertical">
Continues Vertical
</MenuItem>
<MenuItem value="ContinuesHorizontalLTR">
Horizontal (LTR)
</MenuItem>
<MenuItem value="ContinuesHorizontalRTL">
Horizontal (RTL)
</MenuItem>
</Select>
</ListItem>
</List>
</Collapse> </Collapse>
<Divider sx={{ my: 1, mx: 2 }} /> <Divider sx={{ my: 1, mx: 2 }} />
<Navigation> <Navigation>

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import {
List, ListItem, ListItemText, Switch,
} from '@mui/material';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import React from 'react';
interface IProps extends IReaderSettings {
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void
}
export default function ReaderSettingsOptions({
staticNav, loadNextonEnding, readerType, showPageNumber, setSettingValue,
}: IProps) {
return (
<>
<List>
<ListItem>
<ListItemText primary="Static Navigation" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={staticNav}
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Show page number" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={showPageNumber}
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Load next chapter at ending" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={loadNextonEnding}
onChange={(e) => setSettingValue('loadNextonEnding', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemText primary="Reader Type" />
<Select
variant="standard"
value={readerType}
onChange={(e) => setSettingValue('readerType', e.target.value)}
sx={{ p: 0 }}
>
<MenuItem value="SingleLTR">
Single Page (LTR)
</MenuItem>
<MenuItem value="SingleRTL">
Single Page (RTL)
</MenuItem>
{/* <MenuItem value="SingleVertical">
Vertical(WIP)
</MenuItem> */}
<MenuItem value="DoubleLTR">
Double Page (LTR)
</MenuItem>
<MenuItem value="DoubleRTL">
Double Page (RTL)
</MenuItem>
<MenuItem value="Webtoon">
Webtoon
</MenuItem>
<MenuItem value="ContinuesVertical">
Continues Vertical
</MenuItem>
<MenuItem value="ContinuesHorizontalLTR">
Horizontal (LTR)
</MenuItem>
<MenuItem value="ContinuesHorizontalRTL">
Horizontal (RTL)
</MenuItem>
</Select>
</ListItem>
</List>
</>
);
}

View File

@@ -15,12 +15,17 @@ import PageNumber from 'components/reader/PageNumber';
import PagedPager from 'components/reader/pager/PagedPager'; import PagedPager from 'components/reader/pager/PagedPager';
import DoublePagedPager from 'components/reader/pager/DoublePagedPager'; import DoublePagedPager from 'components/reader/pager/DoublePagedPager';
import VerticalPager from 'components/reader/pager/VerticalPager'; import VerticalPager from 'components/reader/pager/VerticalPager';
import ReaderNavBar, { getReaderSettingsFor } from 'components/navbar/ReaderNavBar'; import ReaderNavBar from 'components/navbar/ReaderNavBar';
import NavbarContext from 'components/context/NavbarContext'; import NavbarContext from 'components/context/NavbarContext';
import client from 'util/client'; import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import { requestUpdateMangaMetadata } from 'util/metadata'; import { requestUpdateMangaMetadata } from 'util/metadata';
import {
checkAndHandleMissingStoredReaderSettings,
getReaderSettingsFor,
useDefaultReaderSettings,
} from 'util/readerSettings';
import makeToast from '../components/util/Toast'; import makeToast from '../components/util/Toast';
const getReaderComponent = (readerType: ReaderType) => { const getReaderComponent = (readerType: ReaderType) => {
@@ -68,13 +73,25 @@ export default function Reader() {
const [curPage, setCurPage] = useState<number>(0); const [curPage, setCurPage] = useState<number>(0);
const { setOverride, setTitle } = useContext(NavbarContext); const { setOverride, setTitle } = useContext(NavbarContext);
const [settings, setSettings] = useState(getReaderSettingsFor(manga)); const {
settings: defaultSettings,
loading: areDefaultSettingsLoading,
} = useDefaultReaderSettings();
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
const [isMangaLoading, setIsMangaLoading] = useState(true);
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => { const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
setSettings({ ...settings, [key]: value }); setSettings({ ...settings, [key]: value });
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() => makeToast('Failed to save the reader settings to the server', 'warning')); requestUpdateMangaMetadata(manga, [[key, value]]).catch(() => makeToast('Failed to save the reader settings to the server', 'warning'));
}; };
useEffect(() => {
if (!areDefaultSettingsLoading && !isMangaLoading) {
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(() => {});
setSettings(getReaderSettingsFor(manga, defaultSettings));
}
}, [areDefaultSettingsLoading, isMangaLoading]);
useEffect(() => { useEffect(() => {
// set the custom navbar // set the custom navbar
setOverride( setOverride(
@@ -97,13 +114,14 @@ export default function Reader() {
}, [manga, chapter, settings, curPage, chapterIndex]); }, [manga, chapter, settings, curPage, chapterIndex]);
useEffect(() => { useEffect(() => {
setIsMangaLoading(true);
setTitle('Reader'); setTitle('Reader');
client.get(`/api/v1/manga/${mangaId}/`) client.get(`/api/v1/manga/${mangaId}/`)
.then((response) => response.data) .then((response) => response.data)
.then((data: IManga) => { .then((data: IManga) => {
setManga(data); setManga(data);
setTitle(data.title); setTitle(data.title);
setSettings(getReaderSettingsFor(data)); setIsMangaLoading(false);
}); });
}, [mangaId]); }, [mangaId]);

View File

@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import AutoStoriesIcon from '@mui/icons-material/AutoStories';
import List from '@mui/material/List'; import List from '@mui/material/List';
import ListAltIcon from '@mui/icons-material/ListAlt'; import ListAltIcon from '@mui/icons-material/ListAlt';
import BackupIcon from '@mui/icons-material/Backup'; import BackupIcon from '@mui/icons-material/Backup';
@@ -96,6 +97,12 @@ export default function Settings() {
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Categories" /> <ListItemText primary="Categories" />
</ListItemLink> </ListItemLink>
<ListItemLink to="/settings/defaultReaderSettings">
<ListItemIcon>
<AutoStoriesIcon />
</ListItemIcon>
<ListItemText primary="Default Reader Settings" />
</ListItemLink>
<ListItemLink to="/settings/backup"> <ListItemLink to="/settings/backup">
<ListItemIcon> <ListItemIcon>
<BackupIcon /> <BackupIcon />

View File

@@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import React, { useContext, useEffect } from 'react';
import NavbarContext from 'components/context/NavbarContext';
import { Box } from '@mui/system';
import CircularProgress from '@mui/material/CircularProgress';
import { requestUpdateServerMetadata } from 'util/metadata';
import makeToast from 'components/util/Toast';
import {
checkAndHandleMissingStoredReaderSettings,
getDefaultSettings,
useDefaultReaderSettings,
} from 'util/readerSettings';
import ReaderSettingsOptions from 'components/reader/ReaderSettingsOptions';
export default function DefaultReaderSettings() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => {
setTitle('Default Reader Settings');
setAction(<></>);
}, []);
const { metadata, settings, loading } = useDefaultReaderSettings();
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => makeToast('Failed to save the default reader settings to the server', 'warning'));
};
if (loading) {
return (
<Box sx={{
height: '100vh', width: '100vw', display: 'grid', placeItems: 'center',
}}
>
<CircularProgress thickness={5} />
</Box>
);
}
checkAndHandleMissingStoredReaderSettings({ meta: metadata }, 'server', getDefaultSettings())
.catch(() => {});
return (
<ReaderSettingsOptions
setSettingValue={setSettingValue}
staticNav={settings.staticNav}
showPageNumber={settings.showPageNumber}
loadNextonEnding={settings.loadNextonEnding}
readerType={settings.readerType}
/>
);
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { getMetadataFrom, requestUpdateMangaMetadata, requestUpdateServerMetadata } from 'util/metadata';
import { useQuery } from './client';
export const getDefaultSettings = (forceUndefined: boolean = false) => ({
staticNav: forceUndefined ? undefined : false,
showPageNumber: forceUndefined ? undefined : true,
continuesPageGap: forceUndefined ? undefined : false,
loadNextonEnding: forceUndefined ? undefined : false,
readerType: forceUndefined ? undefined : 'ContinuesVertical',
} as IReaderSettings);
const getReaderSettingsWithDefaultValueFallback = (
meta?: IMetadata,
defaultSettings?: IReaderSettings,
): IReaderSettings => ({
...getMetadataFrom(
{ meta },
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
) as unknown as IReaderSettings,
});
export const getReaderSettingsFromMetadata = (
meta?: IMetadata,
defaultSettings?: IReaderSettings,
): IReaderSettings => ({
...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings),
});
export const getReaderSettingsFor = (
{ meta }: IMetadataHolder,
defaultSettings?: IReaderSettings,
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings);
export const useDefaultReaderSettings = (): {
metadata?: IMetadata,
settings: IReaderSettings,
loading: boolean
} => {
const { data: meta, loading } = useQuery<IMetadata>('/api/v1/meta');
const settings = getReaderSettingsWithDefaultValueFallback(meta);
return { metadata: meta, settings, loading };
};
/**
* Saves all missing reader settings from the metadata to the server
*
* @param metadataHolder
* @param metadataHolderType
* @param defaultSettings
*/
export const checkAndHandleMissingStoredReaderSettings = async (
metadataHolder: IManga | IMetadataHolder,
metadataHolderType: 'manga' | 'server',
defaultSettings: IReaderSettings,
): Promise<void | void[]> => {
const meta = metadataHolder.meta ?? metadataHolder as IMetadata;
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true));
const newSettings = getReaderSettingsFor({ meta }, defaultSettings);
const undefinedSettings = Object.entries(settingsToCheck)
.filter((setting) => setting[1] === undefined);
const settingsToUpdate: MetadataKeyValuePair[] = [];
undefinedSettings.forEach((setting) => {
const key = setting[0] as keyof IReaderSettings;
settingsToUpdate.push([key, newSettings[key]]);
});
if (!undefinedSettings.length) {
return;
}
if (metadataHolderType === 'manga') {
await requestUpdateMangaMetadata(metadataHolder as IManga, settingsToUpdate);
return;
}
await requestUpdateServerMetadata(meta, settingsToUpdate);
};