Set manga thumbnail as background image

This commit is contained in:
schroda
2024-09-10 14:45:08 +02:00
parent b3a8821023
commit a9e0853e34
6 changed files with 83 additions and 4 deletions

View File

@@ -819,6 +819,10 @@
} }
}, },
"appearance": { "appearance": {
"manga_thumbnail_backdrop": {
"description": "Sets the manga thumbnail as the background image on the manga page",
"title": "Manga thumbnail as background"
},
"theme": { "theme": {
"create": { "create": {
"action": "Creating theme…", "action": "Creating theme…",

View File

@@ -37,6 +37,7 @@ import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts
import { MangaType, SourceType } from '@/lib/graphql/generated/graphql.ts'; import { MangaType, SourceType } from '@/lib/graphql/generated/graphql.ts';
import { useLocalStorage } from '@/util/useStorage.tsx'; import { useLocalStorage } from '@/util/useStorage.tsx';
import { useResizeObserver } from '@/util/useResizeObserver.tsx'; import { useResizeObserver } from '@/util/useResizeObserver.tsx';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
const DetailsWrapper = styled('div')(({ theme }) => ({ const DetailsWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@@ -50,7 +51,29 @@ const DetailsWrapper = styled('div')(({ theme }) => ({
}, },
})); }));
const TopContentWrapper = styled('div')(() => ({})); const TopContentWrapper = styled('div', {
shouldForwardProp: (prop) => !['url', 'mangaThumbnailBackdrop'].includes(prop as string),
})<{
url: string;
mangaThumbnailBackdrop: boolean;
}>(({ theme, url, mangaThumbnailBackdrop }) => ({
position: 'relative',
backgroundImage: mangaThumbnailBackdrop ? `url(${url})` : undefined,
backgroundRepeat: mangaThumbnailBackdrop ? 'no-repeat' : undefined,
backgroundSize: mangaThumbnailBackdrop ? 'cover' : undefined,
borderRadius: mangaThumbnailBackdrop ? theme.shape.borderRadius : undefined,
'&::before': mangaThumbnailBackdrop && {
position: 'absolute',
display: 'inline-block',
content: '""',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: `linear-gradient(to top, ${theme.palette.background.default}, transparent 100%, transparent 1px),linear-gradient(to right, ${theme.palette.background.default}, transparent 50%, transparent 1px),linear-gradient(to bottom, ${theme.palette.background.default}, transparent 50%, transparent 1px),linear-gradient(to left, ${theme.palette.background.default}, transparent 50%, transparent 1px)`,
backdropFilter: 'blur(4.5px) brightness(0.75)',
},
}));
const ThumbnailMetadataWrapper = styled('div')(({ theme }) => ({ const ThumbnailMetadataWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@@ -58,6 +81,7 @@ const ThumbnailMetadataWrapper = styled('div')(({ theme }) => ({
})); }));
const MetadataContainer = styled('div')(({ theme }) => ({ const MetadataContainer = styled('div')(({ theme }) => ({
zIndex: 1,
marginLeft: theme.spacing(1), marginLeft: theme.spacing(1),
})); }));
@@ -261,6 +285,10 @@ export const MangaDetails = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const {
settings: { mangaThumbnailBackdrop },
} = useMetadataServerSettings();
useEffect(() => { useEffect(() => {
if (!manga.source) { if (!manga.source) {
makeToast(translate('source.error.label.source_not_found'), 'error'); makeToast(translate('source.error.label.source_not_found'), 'error');
@@ -281,7 +309,7 @@ export const MangaDetails = ({
return ( return (
<> <>
<DetailsWrapper> <DetailsWrapper>
<TopContentWrapper> <TopContentWrapper url={Mangas.getThumbnailUrl(manga)} mangaThumbnailBackdrop={mangaThumbnailBackdrop}>
<ThumbnailMetadataWrapper> <ThumbnailMetadataWrapper>
<Thumbnail manga={manga} /> <Thumbnail manga={manga} />
<MetadataContainer> <MetadataContainer>

View File

@@ -60,6 +60,7 @@ const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
// themes // themes
'customThemes', 'customThemes',
'mangaThumbnailBackdrop',
]; ];
/** /**

View File

@@ -54,6 +54,7 @@ export const getDefaultSettings = (): MetadataServerSettings => ({
// themes // themes
customThemes: {}, customThemes: {},
mangaThumbnailBackdrop: true,
}); });
export const convertSettingsToMetadata = ( export const convertSettingsToMetadata = (

View File

@@ -25,12 +25,19 @@ import { I18nResourceCode, i18nResources } from '@/i18n';
import { langCodeToName } from '@/util/language.tsx'; import { langCodeToName } from '@/util/language.tsx';
import { getTheme } from '@/lib/ui/AppThemes.ts'; import { getTheme } from '@/lib/ui/AppThemes.ts';
import { ThemeList } from '@/screens/settings/appearance/theme/ThemeList.tsx'; import { ThemeList } from '@/screens/settings/appearance/theme/ThemeList.tsx';
import {
createUpdateMetadataServerSettings,
useMetadataServerSettings,
} from '@/lib/metadata/metadataServerSettings.ts';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { MetadataThemeSettings } from '@/typings.ts';
import { makeToast } from '@/components/util/Toast.tsx';
export const Appearance = () => { export const Appearance = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { themeMode, setThemeMode, pureBlackMode, setPureBlackMode, appTheme } = useContext(ThemeModeContext); const { themeMode, setThemeMode, pureBlackMode, setPureBlackMode, appTheme } = useContext(ThemeModeContext);
const isDarkMode =
getTheme(appTheme).muiTheme.palette?.mode === 'dark' || MediaQuery.getThemeMode() === ThemeMode.DARK;
const { setTitle, setAction } = useContext(NavBarContext); const { setTitle, setAction } = useContext(NavBarContext);
useEffect(() => { useEffect(() => {
@@ -43,9 +50,34 @@ export const Appearance = () => {
}; };
}, [t]); }, [t]);
const {
settings,
request: { loading, error, refetch },
} = useMetadataServerSettings();
const updateMetadataSetting = createUpdateMetadataServerSettings<keyof MetadataThemeSettings>(() =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error'),
);
const isDarkMode =
getTheme(appTheme).muiTheme.palette?.mode === 'dark' || MediaQuery.getThemeMode() === ThemeMode.DARK;
const DEFAULT_ITEM_WIDTH = 300; const DEFAULT_ITEM_WIDTH = 300;
const [itemWidth, setItemWidth] = useLocalStorage<number>('ItemWidth', DEFAULT_ITEM_WIDTH); const [itemWidth, setItemWidth] = useLocalStorage<number>('ItemWidth', DEFAULT_ITEM_WIDTH);
if (loading) {
return <LoadingPlaceholder />;
}
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
messageExtra={error.message}
retry={() => refetch().catch(defaultPromiseErrorHandler('Appearance::refetch'))}
/>
);
}
return ( return (
<List <List
subheader={ subheader={
@@ -121,6 +153,18 @@ export const Appearance = () => {
showSlider showSlider
handleUpdate={setItemWidth} handleUpdate={setItemWidth}
/> />
<ListItem>
<ListItemText
primary={t('settings.appearance.manga_thumbnail_backdrop.title')}
secondary={t('settings.appearance.manga_thumbnail_backdrop.description')}
/>
<Switch
edge="end"
checked={settings.mangaThumbnailBackdrop}
onChange={(e) => updateMetadataSetting('mangaThumbnailBackdrop', e.target.checked)}
/>
</ListItem>
</List> </List>
</List> </List>
); );

View File

@@ -159,6 +159,7 @@ export type MetadataUpdateSettings = {
export type MetadataThemeSettings = { export type MetadataThemeSettings = {
customThemes: Record<string, AppTheme>; customThemes: Record<string, AppTheme>;
mangaThumbnailBackdrop: boolean;
}; };
export type MetadataServerSettings = MetadataDownloadSettings & export type MetadataServerSettings = MetadataDownloadSettings &