Add WebView support

This commit is contained in:
schroda
2025-07-19 19:09:22 +02:00
parent 0d23d8077f
commit bac77599f5
8 changed files with 113 additions and 30 deletions

View File

@@ -104,7 +104,6 @@
} }
}, },
"label": { "label": {
"open_on_source": "Open on source",
"select": "Select" "select": "Select"
}, },
"mark_as_read": { "mark_as_read": {
@@ -349,7 +348,8 @@
"migrate": "Migrate", "migrate": "Migrate",
"ok": "Ok", "ok": "Ok",
"open": "Open", "open": "Open",
"open_site": "Open Site", "open_browser": "Open in browser",
"open_webview": "Open in WebView",
"options": "Options", "options": "Options",
"popular": "Popular", "popular": "Popular",
"refresh": "Refresh", "refresh": "Refresh",

View File

@@ -0,0 +1,11 @@
/*
* 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 LaunchIcon from '@mui/icons-material/Launch';
export const IconBrowser = LaunchIcon;

View File

@@ -0,0 +1,11 @@
/*
* 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 OpenInBrowserIcon from '@mui/icons-material/OpenInBrowser';
export const IconWebView = OpenInBrowserIcon;

View File

@@ -466,6 +466,10 @@ export class RequestManager {
return `${this.getBaseUrl()}${apiVersion}${endpoint}`; return `${this.getBaseUrl()}${apiVersion}${endpoint}`;
} }
public getWebviewUrl(url: string): string {
return `${this.getValidUrlFor('webview')}#${url}`;
}
public clearBrowseCacheFor(sourceId: string) { public clearBrowseCacheFor(sourceId: string) {
const cacheKeys = this.cache.getMatchingKeys( const cacheKeys = this.cache.getMatchingKeys(
new RegExp(`${CACHE_INITIAL_PAGES_FETCHING_KEY}|${CACHE_PAGES_KEY}|${CACHE_RESULTS_KEY}.*${sourceId}`), new RegExp(`${CACHE_INITIAL_PAGES_FETCHING_KEY}|${CACHE_PAGES_KEY}|${CACHE_RESULTS_KEY}.*${sourceId}`),

View File

@@ -16,7 +16,6 @@ import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
import BookmarkAdd from '@mui/icons-material/BookmarkAdd'; import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
import DoneAll from '@mui/icons-material/DoneAll'; import DoneAll from '@mui/icons-material/DoneAll';
import { ComponentProps, useMemo } from 'react'; import { ComponentProps, useMemo } from 'react';
import LaunchIcon from '@mui/icons-material/Launch';
import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts'; import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
import { Chapters } from '@/modules/chapter/services/Chapters.ts'; import { Chapters } from '@/modules/chapter/services/Chapters.ts';
import { MenuItem } from '@/modules/core/components/menu/MenuItem.tsx'; import { MenuItem } from '@/modules/core/components/menu/MenuItem.tsx';
@@ -41,6 +40,8 @@ import {
ChapterReadInfo, ChapterReadInfo,
ChapterRealUrlInfo, ChapterRealUrlInfo,
} from '@/modules/chapter/Chapter.types.ts'; } from '@/modules/chapter/Chapter.types.ts';
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
type BaseProps = { onClose: () => void; selectable?: boolean }; type BaseProps = { onClose: () => void; selectable?: boolean };
@@ -173,15 +174,30 @@ export const ChapterActionMenuItems = ({
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} /> <MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
)} )}
{isSingleMode && ( {isSingleMode && (
<MenuItem <>
Icon={LaunchIcon} <MenuItem
disabled={!chapter!.realUrl} Icon={IconBrowser}
onClick={() => { disabled={!chapter!.realUrl}
window.open(chapter!.realUrl!, '_blank', 'noopener,noreferrer'); onClick={() => {
onClose(); window.open(chapter!.realUrl!, '_blank', 'noopener,noreferrer');
}} onClose();
title={t('chapter.action.label.open_on_source')} }}
/> title={t('global.button.open_browser')}
/>
<MenuItem
Icon={IconWebView}
disabled={!chapter!.realUrl}
onClick={() => {
window.open(
requestManager.getWebviewUrl(chapter!.realUrl!),
'_blank',
'noopener,noreferrer',
);
onClose();
}}
title={t('global.button.open_webview')}
/>
</>
)} )}
{shouldShowMenuItem(canBeDownloaded) && ( {shouldShowMenuItem(canBeDownloaded) && (
<MenuItem <MenuItem

View File

@@ -14,10 +14,10 @@ import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next'; import { t as translate } from 'i18next';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import LaunchIcon from '@mui/icons-material/Launch';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ButtonGroup from '@mui/material/ButtonGroup';
import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
import { makeToast } from '@/modules/core/utils/Toast.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts';
import { Mangas } from '@/modules/manga/services/Mangas.ts'; import { Mangas } from '@/modules/manga/services/Mangas.ts';
@@ -51,6 +51,9 @@ import { SourceIdInfo } from '@/modules/source/Source.types.ts';
import { Thumbnail } from '@/modules/manga/components/details/Thumbnail.tsx'; import { Thumbnail } from '@/modules/manga/components/details/Thumbnail.tsx';
import { DescriptionGenre } from '@/modules/manga/components/details/DescriptionGenre.tsx'; import { DescriptionGenre } from '@/modules/manga/components/details/DescriptionGenre.tsx';
import { SearchLink } from '@/modules/manga/components/details/SearchLink.tsx'; import { SearchLink } from '@/modules/manga/components/details/SearchLink.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
const DetailsWrapper = styled('div')(({ theme }) => ({ const DetailsWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@@ -136,19 +139,34 @@ const OpenSourceButton = ({ url }: { url?: string | null }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<CustomTooltip title={t('global.button.open_site')} disabled={!url}> <ButtonGroup>
<CustomButtonIcon <CustomTooltip title={t('global.button.open_browser')} disabled={!url}>
size="medium" <CustomButtonIcon
disabled={!url} size="medium"
component={Link} disabled={!url}
href={url ?? undefined} component={Link}
target="_blank" href={url ?? undefined}
rel="noreferrer" target="_blank"
variant="outlined" rel="noreferrer"
> variant="outlined"
<LaunchIcon /> >
</CustomButtonIcon> <IconBrowser />
</CustomTooltip> </CustomButtonIcon>
</CustomTooltip>
<CustomTooltip title={t('global.button.open_webview')} disabled={!url}>
<CustomButtonIcon
size="medium"
disabled={!url}
component={Link}
href={url ? requestManager.getWebviewUrl(url) : undefined}
target="_blank"
rel="noreferrer"
variant="outlined"
>
<IconWebView />
</CustomButtonIcon>
</CustomTooltip>
</ButtonGroup>
); );
}; };

View File

@@ -38,6 +38,7 @@ import { ReaderBookmarkButton } from '@/modules/reader/components/overlay/naviga
import { FALLBACK_CHAPTER } from '@/modules/chapter/Chapter.constants.ts'; import { FALLBACK_CHAPTER } from '@/modules/chapter/Chapter.constants.ts';
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts'; import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
import { ReaderExitButton } from '@/modules/reader/components/overlay/navigation/ReaderExitButton.tsx'; import { ReaderExitButton } from '@/modules/reader/components/overlay/navigation/ReaderExitButton.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' }; const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
@@ -107,7 +108,16 @@ const BaseReaderOverlayHeaderMobile = forwardRef<
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
{t('chapter.action.label.open_on_source')} {t('global.button.open_browser')}
</MenuItem>
<MenuItem
component={Link}
disabled={!realUrl}
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}
rel="noreferrer"
target="_blank"
>
{t('global.button.open_webview')}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
disabled={!realUrl} disabled={!realUrl}

View File

@@ -9,7 +9,6 @@
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import ReplayIcon from '@mui/icons-material/Replay'; import ReplayIcon from '@mui/icons-material/Replay';
import { memo, useMemo, useRef } from 'react'; import { memo, useMemo, useRef } from 'react';
@@ -25,6 +24,9 @@ import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/Rea
import { ReaderLibraryButton } from '@/modules/reader/components/overlay/navigation/ReaderLibraryButton.tsx'; import { ReaderLibraryButton } from '@/modules/reader/components/overlay/navigation/ReaderLibraryButton.tsx';
import { ReaderBookmarkButton } from '@/modules/reader/components/overlay/navigation/ReaderBookmarkButton.tsx'; import { ReaderBookmarkButton } from '@/modules/reader/components/overlay/navigation/ReaderBookmarkButton.tsx';
import { CHAPTER_ACTION_TO_TRANSLATION, FALLBACK_CHAPTER } from '@/modules/chapter/Chapter.constants.ts'; import { CHAPTER_ACTION_TO_TRANSLATION, FALLBACK_CHAPTER } from '@/modules/chapter/Chapter.constants.ts';
import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
const DownloadButton = ({ currentChapter }: Required<Pick<ReaderStateChapters, 'currentChapter'>>) => { const DownloadButton = ({ currentChapter }: Required<Pick<ReaderStateChapters, 'currentChapter'>>) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -100,7 +102,7 @@ const BaseReaderNavBarDesktopActions = memo(
</IconButton> </IconButton>
</CustomTooltip> </CustomTooltip>
<DownloadButton currentChapter={currentChapter} /> <DownloadButton currentChapter={currentChapter} />
<CustomTooltip title={t('chapter.action.label.open_on_source')} disabled={!realUrl}> <CustomTooltip title={t('global.button.open_browser')} disabled={!realUrl}>
<IconButton <IconButton
disabled={!realUrl} disabled={!realUrl}
href={realUrl ?? ''} href={realUrl ?? ''}
@@ -108,7 +110,18 @@ const BaseReaderNavBarDesktopActions = memo(
target="_blank" target="_blank"
color="inherit" color="inherit"
> >
<OpenInNewIcon /> <IconBrowser />
</IconButton>
</CustomTooltip>
<CustomTooltip title={t('global.button.open_webview')} disabled={!realUrl}>
<IconButton
disabled={!realUrl}
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}
rel="noreferrer"
target="_blank"
color="inherit"
>
<IconWebView />
</IconButton> </IconButton>
</CustomTooltip> </CustomTooltip>
</Stack> </Stack>