Require new debug information in github bug issue

This commit is contained in:
schroda
2026-07-22 17:37:46 +02:00
parent d4fab13e0a
commit fcc6df37ca
11 changed files with 553 additions and 94 deletions

View File

@@ -36,27 +36,6 @@ body:
validations: validations:
required: true required: true
- type: input
id: suwayomi-server-version
attributes:
label: Suwayomi-Server version
description: You can find your Suwayomi-Server version in **More → About**.
placeholder: |
Example: "v1.1.1 | v1.1.1-r1701"
validations:
required: true
- type: dropdown
id: database
attributes:
label: Used database
description: H2 is the default database
options:
- H2
- PostgreSQL
validations:
required: true
- type: input - type: input
id: server-os id: server-os
attributes: attributes:
@@ -87,33 +66,22 @@ body:
validations: validations:
required: true required: true
- type: input - type: textarea
id: suwayomi-webui-version id: debug-information
attributes: attributes:
label: Suwayomi-WebUI version label: Debug information
description: You can find your Suwayomi-WebUI version in **More → About**. description: |
placeholder: | Provide the debug information from the About page.
Example: "r1689 STABLE | r2401 PREVIEW" _Note:_ You can paste the copied debug information into the placeholder below.
validations: value: |
required: true <details>
<summary>Click here to see debug information</summary>
- type: input <!-- Please leave one blank line below for enabling the code block rendering. -->
id: client-browser
attributes: ```
label: Used web browser Replace this text with the copied debug information
description: The browser which is used to open Suwayomi-WebUI ```
placeholder: | </details>
Example: "Chrome 134.0.6998.118 (64-Bit) | FireFox 136.0.2 (64-Bit) | Electron v35.0.2"
validations:
required: true
- type: input
id: client-os
attributes:
label: Client operating system
description: The system on which the Suwayomi-WebUI is running on
placeholder: |
Example: "Windows 11 Pro 24H2 | Ubuntu 24.04.2 LTS"
validations: validations:
required: true required: true

View File

@@ -16,3 +16,5 @@ export const enhancedCleanup = (str: string): string =>
export const reverseString = (str: string, separator: string = ''): string => export const reverseString = (str: string, separator: string = ''): string =>
str.split(separator).reverse().join(separator); str.split(separator).reverse().join(separator);
export const indent = (str: string, level: number, char: string): string => char.repeat(level) + str;

View File

@@ -8,7 +8,7 @@
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts'; import { copyToClipboard, getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
@@ -127,9 +127,8 @@ const ExtensionStoreCard = ({
)} )}
<CustomTooltip title={t`Copy index url`}> <CustomTooltip title={t`Copy index url`}>
<IconButton <IconButton
onClick={async () => { onClick={() => {
await navigator.clipboard.writeText(indexUrl); copyToClipboard(indexUrl);
makeToast(t`Copied to clipboard`, 'info');
}} }}
color="inherit" color="inherit"
> >

View File

@@ -27,7 +27,6 @@ import { FlexWrapButton } from '@/base/components/buttons/FlexWrapButton.tsx';
import { TrackMangaButton } from '@/features/manga/components/TrackMangaButton.tsx'; import { TrackMangaButton } from '@/features/manga/components/TrackMangaButton.tsx';
import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx'; import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx';
import { Metadata as BaseMetadata } from '@/base/components/texts/Metadata.tsx'; import { Metadata as BaseMetadata } from '@/base/components/texts/Metadata.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import type { MangaType, SourceType } from '@/lib/graphql/generated/graphql-base.types.ts'; import type { MangaType, SourceType } from '@/lib/graphql/generated/graphql-base.types.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { MANGA_STATUS_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts'; import { MANGA_STATUS_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
@@ -56,6 +55,7 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { IconBrowser } from '@/assets/icons/IconBrowser.tsx'; import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
import { IconWebView } from '@/assets/icons/IconWebView.tsx'; import { IconWebView } from '@/assets/icons/IconWebView.tsx';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
const DetailsWrapper = styled('div')(({ theme }) => ({ const DetailsWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@@ -240,15 +240,6 @@ export const MangaDetails = ({
const { updateLibraryState } = useManageMangaLibraryState(manga); const { updateLibraryState } = useManageMangaLibraryState(manga);
const copyTitle = async () => {
try {
await navigator.clipboard.writeText(manga.title);
makeToast(t`Copied to clipboard`, 'info');
} catch (e) {
defaultPromiseErrorHandler('MangaDetails::copyTitleLongPress')(e);
}
};
return ( return (
<DetailsWrapper> <DetailsWrapper>
<TopContentWrapper url={Mangas.getThumbnailUrl(manga)} mangaThumbnailBackdrop={mangaThumbnailBackdrop}> <TopContentWrapper url={Mangas.getThumbnailUrl(manga)} mangaThumbnailBackdrop={mangaThumbnailBackdrop}>
@@ -263,7 +254,7 @@ export const MangaDetails = ({
</SearchLink> </SearchLink>
{!!navigator.clipboard && ( {!!navigator.clipboard && (
<CustomTooltip title={t`Copy`}> <CustomTooltip title={t`Copy`}>
<IconButton onClick={copyTitle} color="inherit"> <IconButton onClick={() => copyToClipboard(manga.title)} color="inherit">
<ContentCopyIcon fontSize="small" /> <ContentCopyIcon fontSize="small" />
</IconButton> </IconButton>
</CustomTooltip> </CustomTooltip>

View File

@@ -12,6 +12,8 @@ import { getActiveDevice } from '@/features/device/services/Device.ts';
export const extractOriginalKey = (key: string) => key.split('_').slice(-1)[0]; export const extractOriginalKey = (key: string) => key.split('_').slice(-1)[0];
export const getMetadataKeyRaw = (key: string, prefixes: string[]): string => `${prefixes.join('_')}_${key}`;
/** /**
* Returns the key with the provided prefixes. * Returns the key with the provided prefixes.
* *
@@ -34,7 +36,7 @@ export const getMetadataKey = (key: string, prefixes: string[] = [], appPrefix:
(prefix) => prefix.toLowerCase() !== 'default', (prefix) => prefix.toLowerCase() !== 'default',
); );
return `${finalPrefix.join('_')}_${key}`; return getMetadataKeyRaw(key, finalPrefix);
}; };
export const doesAppMetadataKeyExistIn = ( export const doesAppMetadataKeyExistIn = (
@@ -46,3 +48,19 @@ export const doesAppMetadataKeyExistIn = (
export const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string): boolean => export const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string): boolean =>
Object.prototype.hasOwnProperty.call(meta ?? {}, key); Object.prototype.hasOwnProperty.call(meta ?? {}, key);
export const getAppMetadataFrom = (
meta: Metadata,
prefixes: string[] = [],
appPrefix: string = APP_METADATA_KEY_PREFIX,
): Metadata => {
const appMetadata: Metadata = {};
Object.entries(meta).forEach(([key, value]) => {
if (key.startsWith([appPrefix, ...prefixes].join('_'))) {
appMetadata[key] = value;
}
});
return appMetadata;
};

View File

@@ -20,7 +20,7 @@ import type {
MetadataHolderType, MetadataHolderType,
MetadataKeyValuePair, MetadataKeyValuePair,
} from '@/features/metadata/Metadata.types.ts'; } from '@/features/metadata/Metadata.types.ts';
import { extractOriginalKey, getMetadataKey } from '@/features/metadata/Metadata.utils.ts'; import { extractOriginalKey, getAppMetadataFrom, getMetadataKey } from '@/features/metadata/Metadata.utils.ts';
import type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; import type { MangaIdInfo } from '@/features/manga/Manga.types.ts';
import type { CategoryIdInfo } from '@/features/category/Category.types.ts'; import type { CategoryIdInfo } from '@/features/category/Category.types.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
@@ -37,22 +37,6 @@ const getAppKeyPrefixForMigration = (migrationId: number): string => {
return appKeyPrefix?.appKeyPrefix?.newPrefix ?? APP_METADATA_KEY_PREFIX; return appKeyPrefix?.appKeyPrefix?.newPrefix ?? APP_METADATA_KEY_PREFIX;
}; };
const getAppMetadataFrom = (
meta: Metadata,
prefixes: string[] = [],
appPrefix: string = APP_METADATA_KEY_PREFIX,
): Metadata => {
const appMetadata: Metadata = {};
Object.entries(meta).forEach(([key, value]) => {
if (key.startsWith([appPrefix, ...prefixes].join('_'))) {
appMetadata[key] = value;
}
});
return appMetadata;
};
const applyAppKeyPrefixMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => { const applyAppKeyPrefixMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => {
const migratedMetadata: Metadata = { ...meta }; const migratedMetadata: Metadata = { ...meta };

View File

@@ -20,7 +20,6 @@ import { memo } from 'react';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import type { MobileHeaderProps } from '@/features/reader/overlay/ReaderOverlay.types.ts'; import type { MobileHeaderProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts';
@@ -35,6 +34,7 @@ import {
useReaderScrollbarStore, useReaderScrollbarStore,
useReaderStore, useReaderStore,
} from '@/features/reader/stores/ReaderStore.ts'; } from '@/features/reader/stores/ReaderStore.ts';
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' }; const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
@@ -113,13 +113,7 @@ const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & {
> >
{t`Open in WebView`} {t`Open in WebView`}
</MenuItem> </MenuItem>
<MenuItem <MenuItem disabled={!realUrl} onClick={() => copyToClipboard(title)}>
disabled={!realUrl}
onClick={async () => {
await navigator.clipboard.writeText(title);
makeToast(t`Copied to clipboard`, 'info');
}}
>
{t`Share`} {t`Share`}
</MenuItem> </MenuItem>
</Menu> </Menu>

View File

@@ -0,0 +1,448 @@
/*
* 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 { useLingui } from '@lingui/react/macro';
import { Collapsable } from '@/base/components/Collapsable.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import Stack from '@mui/material/Stack';
import { Metadata } from '@/base/components/texts/Metadata.tsx';
import { STABLE_EMPTY_ARRAY, STABLE_EMPTY_OBJECT } from '@/base/Base.constants.ts';
import type { ServerSettings } from '@/features/settings/Settings.types.ts';
import { epochToDate } from '@/base/utils/DateHelper.ts';
import dayjs from 'dayjs';
import { getReaderSettings, useDefaultReaderSettings } from '@/features/reader/settings/ReaderSettingsMetadata.ts';
import { getActiveDevice } from '@/features/device/services/Device.ts';
import {
READING_MODE_VALUE_TO_DISPLAY_DATA,
READING_MODE_VALUES,
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import type { IReaderSettings, ReadingMode } from '@/features/reader/Reader.types.ts';
import { Sources } from '@/features/source/services/Sources.ts';
import omitBy from 'lodash/fp/omitBy';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { indent } from '@/base/utils/Strings.ts';
import Button from '@mui/material/Button';
import { assertIsDefined } from '@/base/Asserts.ts';
import { copyToClipboard, getRenderedText } from '@/lib/HelperFunctions.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { EmptyView } from '@/base/components/feedback/EmptyView.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { useLocalStorage } from '@/base/hooks/useStorage.tsx';
import { MIGRATION_LOCAL_STORAGE_KEY } from '@/features/migration/Migration.constants.ts';
import type { MigrationState } from '@/features/migration/Migration.types.ts';
const PRIVACY_UNSAFE_SERVER_SETTINGS: (keyof ServerSettings)[] = [
'socksProxyUsername',
'socksProxyPassword',
'authUsername',
'authPassword',
'downloadConversions',
'serveConversions',
'databaseUrl',
'databaseUsername',
'databasePassword',
'syncYomiApiKey',
'jwtAudience',
];
const getBrowserDebugInfo = async (serverAddress: string) => {
const nav = navigator;
const serviceWorkerRegistration =
'serviceWorker' in navigator
? await navigator.serviceWorker.getRegistration().catch(() => undefined)
: undefined;
return {
page: {
protocol: window.location.protocol,
localhost: window.location.hostname.includes('localhost'),
matchesServerAddress: window.location.origin === serverAddress,
},
connection:
'connection' in nav
? {
effectiveType: (nav.connection as any).effectiveType,
downlink: (nav.connection as any).downlink,
rtt: (nav.connection as any).rtt,
saveData: (nav.connection as any).saveData,
type: (nav.connection as any).type,
}
: 'Unknown',
security: {
secureContext: window.isSecureContext,
https: window.location.protocol === 'https:',
localhost: window.location.hostname.includes('localhost'),
},
protocols: {
http: true,
https: window.location.protocol === 'https:',
websocket: typeof WebSocket !== 'undefined',
webRTC: typeof RTCPeerConnection !== 'undefined',
},
browser: {
userAgent: nav.userAgent,
language: nav.language,
languages: nav.languages,
platform: 'userAgentData' in nav ? (nav.userAgentData as any)?.platform : nav.platform,
vendor: nav.vendor,
online: nav.onLine,
},
device: {
hardwareConcurrency: nav.hardwareConcurrency ?? 'Unknown',
deviceMemory: 'deviceMemory' in nav ? `${nav.deviceMemory} GB` : 'Unknown',
maxTouchPoints: nav.maxTouchPoints,
touchSupported: 'ontouchstart' in window || nav.maxTouchPoints > 0,
},
screen: {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
colorDepth: screen.colorDepth,
pixelDepth: screen.pixelDepth,
pixelRatio: window.devicePixelRatio,
orientation: screen.orientation?.type ?? 'Unknown',
},
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
locale: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
locale: Intl.DateTimeFormat().resolvedOptions().locale,
},
storage: {
localStorage: (() => {
try {
localStorage.setItem('__test', '1');
localStorage.removeItem('__test');
return true;
} catch {
return false;
}
})(),
sessionStorage: (() => {
try {
sessionStorage.setItem('__test', '1');
sessionStorage.removeItem('__test');
return true;
} catch {
return false;
}
})(),
},
features: {
serviceWorker: 'serviceWorker' in navigator,
webGL: (() => {
try {
const canvas = document.createElement('canvas');
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
} catch {
return false;
}
})(),
},
pwa: {
installed:
window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone === true,
displayMode: {
browser: window.matchMedia('(display-mode: browser)').matches,
standalone: window.matchMedia('(display-mode: standalone)').matches,
minimalUi: window.matchMedia('(display-mode: minimal-ui)').matches,
fullscreen: window.matchMedia('(display-mode: fullscreen)').matches,
windowControlsOverlay: window.matchMedia('(display-mode: window-controls-overlay)').matches,
},
iosStandalone: (navigator as any).standalone ?? false,
serviceWorker: {
supported: 'serviceWorker' in navigator,
controller: !!navigator.serviceWorker?.controller,
registered: !!serviceWorkerRegistration,
active: !!serviceWorkerRegistration?.active,
waiting: !!serviceWorkerRegistration?.waiting,
installing: !!serviceWorkerRegistration?.installing,
},
beforeInstallPromptSupported: 'onbeforeinstallprompt' in window,
manifest: {
linked: !!document.querySelector('link[rel="manifest"]'),
},
},
};
};
const mapObjectToMetadata = (obj: object, tabs: number = 0) => (
<Stack>
{Object.entries(obj).map(([key, value], index, array) =>
!!value && typeof value === 'object' && !Array.isArray(value) ? (
<Fragment key={key}>
{index >= 1 && <br />}
<TypographyMaxLines component="span">{indent(key, tabs, ' ')}</TypographyMaxLines>
{mapObjectToMetadata(value, tabs + 2)}
{array[index + 1] && typeof array[index + 1]?.[1] !== 'object' && <br />}
</Fragment>
) : (
<Metadata
key={key}
title={indent(key, tabs, ' ')}
value={indent(JSON.stringify(value), 1, ' ')}
titleProps={{ component: 'span' }}
valueProps={{ component: 'span' }}
stackProps={{ sx: { display: 'inline-block' } }}
/>
),
)}
</Stack>
);
export const DebugInformation = () => {
const { t } = useLingui();
const [baseUrl] = requestManager.useBaseUrl();
const aboutRequest = requestManager.useGetAbout();
const extensionStoresRequest = requestManager.useGetExtensionStores();
const extensionsRequest = requestManager.useGetExtensionList({ variables: { condition: { isInstalled: true } } });
const sourcesRequest = requestManager.useGetSourceList();
const serverSettingsRequest = requestManager.useGetServerSettings();
const clientSettings = useMetadataServerSettings();
const defaultReaderSettings = useDefaultReaderSettings();
const [migrationState] = useLocalStorage<{ state: MigrationState }>(MIGRATION_LOCAL_STORAGE_KEY);
const contentTextRef = useRef<HTMLDivElement>(null);
const [readerSettingsByReadingMode, setReaderSettingsByReadingMode] =
useState<Record<ReadingMode, IReaderSettings>>(STABLE_EMPTY_OBJECT);
const [browserDebugInfo, setBrowserDebugInfo] =
useState<Awaited<ReturnType<typeof getBrowserDebugInfo>>>(STABLE_EMPTY_OBJECT);
const aboutServer = aboutRequest.data?.aboutServer;
const aboutWebUI = aboutRequest.data?.aboutWebUI;
const extensionStoresCount = extensionStoresRequest.data?.extensionStores.totalCount ?? 0;
const extensions = extensionsRequest.data?.extensions.nodes ?? STABLE_EMPTY_ARRAY;
const sources = sourcesRequest.data?.sources.nodes ?? STABLE_EMPTY_ARRAY;
const areFromMultipleStores = useMemo(() => Sources.areFromMultipleStores(sources), [sources]);
const enabledSourcesCount = useMemo(() => Sources.filter(sources, { enabled: true }).length, [sources]);
const nsfwSourcesCount = useMemo(() => Sources.filter(sources, { isNsfw: true }).length, [sources]);
const pinnedSourcesCount = useMemo(() => Sources.filter(sources, { pinned: true }).length, [sources]);
const activeDevice = getActiveDevice();
const debugInfo = useMemo(
() => ({
About: {
Server: {
Version: aboutServer?.version,
Channel: aboutServer?.buildType,
'Build time': aboutServer?.buildTime
? epochToDate(Number(aboutServer.buildTime)).toISOString()
: '-',
},
WebUI: {
Version: aboutWebUI?.tag,
Channel: aboutWebUI?.channel,
'Update timestamp': aboutWebUI?.updateTimestamp
? dayjs(Number(aboutWebUI.updateTimestamp)).toISOString()
: '-',
},
},
Settings: {
Server: omitBy(
(_value, key) => PRIVACY_UNSAFE_SERVER_SETTINGS.includes(key as keyof ServerSettings),
serverSettingsRequest.data?.settings ?? STABLE_EMPTY_OBJECT,
),
WebUI: {
'Active Device': activeDevice,
...clientSettings.settings,
Reader: {
Default: defaultReaderSettings.settings,
...Object.fromEntries(
READING_MODE_VALUES.map((readingMode) => [
READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode].title.message ?? readingMode,
omitBy(
(value, key) =>
JSON.stringify(defaultReaderSettings.settings[key as keyof IReaderSettings]) ===
JSON.stringify(value),
readerSettingsByReadingMode[readingMode],
),
]),
),
},
},
},
'Extensions/Sources': {
'Extension stores': extensionStoresCount,
'Extensions installed': extensions.length,
'Sources from different stores': areFromMultipleStores,
'Sources enabled': enabledSourcesCount,
'Sources NSFW': nsfwSourcesCount,
'Sources pinned': pinnedSourcesCount,
'Show NSFW': clientSettings.settings.showNsfw,
'Browse languages': clientSettings.settings.browseLanguages,
},
'Migration state': migrationState?.state,
Client: browserDebugInfo,
}),
[
aboutServer,
aboutWebUI,
serverSettingsRequest.data?.settings,
activeDevice,
clientSettings.settings,
defaultReaderSettings.settings,
readerSettingsByReadingMode,
extensionStoresCount,
extensions.length,
areFromMultipleStores,
enabledSourcesCount,
nsfwSourcesCount,
pinnedSourcesCount,
browserDebugInfo,
migrationState,
],
);
useEffect(() => {
const settingsByReadingModeEntries = READING_MODE_VALUES.map(
(readingMode) =>
[
readingMode,
getReaderSettings(
'global',
{ meta: defaultReaderSettings.metadata },
defaultReaderSettings.settings,
undefined,
readingMode,
),
] satisfies [ReadingMode, IReaderSettings],
);
const settingsByReadingMode = Object.fromEntries(settingsByReadingModeEntries) as Record<
ReadingMode,
IReaderSettings
>;
setReaderSettingsByReadingMode(settingsByReadingMode);
}, [defaultReaderSettings.metadata, defaultReaderSettings.settings]);
useEffect(() => {
getBrowserDebugInfo(baseUrl)
.then(setBrowserDebugInfo)
.catch(defaultPromiseErrorHandler('DebugInformation::getBrowserDebugInfo'));
}, [baseUrl]);
const isLoading =
aboutRequest.loading ||
extensionStoresRequest.loading ||
extensionsRequest.loading ||
sourcesRequest.loading ||
serverSettingsRequest.loading ||
clientSettings.loading ||
defaultReaderSettings.loading;
if (isLoading) {
return <LoadingPlaceholder />;
}
const hasError =
aboutRequest.error ||
extensionStoresRequest.error ||
extensionsRequest.error ||
sourcesRequest.error ||
serverSettingsRequest.error ||
clientSettings.request.error ||
defaultReaderSettings.request.error;
if (hasError) {
return (
<EmptyView
message={t`Could not load data`}
retry={() => {
if (aboutRequest.error) {
aboutRequest.refetch().catch(defaultPromiseErrorHandler('DebugInformation::aboutRequest'));
}
if (extensionStoresRequest.error) {
extensionStoresRequest
.refetch()
.catch(defaultPromiseErrorHandler('DebugInformation::extensionStoresRequest'));
}
if (extensionsRequest.error) {
extensionsRequest
.refetch()
.catch(defaultPromiseErrorHandler('DebugInformation::extensionsRequest'));
}
if (sourcesRequest.error) {
sourcesRequest.refetch().catch(defaultPromiseErrorHandler('DebugInformation::sourcesRequest'));
}
if (serverSettingsRequest.error) {
serverSettingsRequest
.refetch()
.catch(defaultPromiseErrorHandler('DebugInformation::serverSettingsRequest'));
}
if (clientSettings.request.error) {
clientSettings.request
.refetch()
.catch(defaultPromiseErrorHandler('DebugInformation::clientSettings'));
}
if (defaultReaderSettings.request.error) {
defaultReaderSettings.request
.refetch()
.catch(defaultPromiseErrorHandler('DebugInformation::defaultReaderSettings'));
}
}}
/>
);
}
return (
<Collapsable
slots={{
headerWrapper: { sx: { alignItems: 'center' } },
collapse: { slotProps: { wrapperInner: { sx: { whiteSpace: 'pre' } } }, unmountOnExit: false },
}}
header={
<Stack sx={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>
{t`Show`}
{'clipboard' in navigator && (
<Button
variant="text"
size="small"
onClick={async (e) => {
e.stopPropagation();
assertIsDefined(contentTextRef.current?.innerText);
copyToClipboard(getRenderedText(contentTextRef.current, 'white-space: pre;'));
}}
>{t`Copy`}</Button>
)}
</Stack>
}
collapse={<Stack ref={contentTextRef}>{mapObjectToMetadata(debugInfo)}</Stack>}
/>
);
};

View File

@@ -22,6 +22,8 @@ import { VersionInfo } from '@/features/app-updates/components/VersionInfo.tsx';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { epochToDate } from '@/base/utils/DateHelper.ts'; import { epochToDate } from '@/base/utils/DateHelper.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts'; import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { DebugInformation } from '@/features/settings/components/DebugInformation.tsx';
import Stack from '@mui/material/Stack';
export function About() { export function About() {
const { t } = useLingui(); const { t } = useLingui();
@@ -160,6 +162,17 @@ export function About() {
<ListItemText primary={t`Discord`} secondary={aboutServer.discord} /> <ListItemText primary={t`Discord`} secondary={aboutServer.discord} />
</ListItemLink> </ListItemLink>
</List> </List>
<List
subheader={
<ListSubheader component="div" id="about-webui-info">
{t`Debug information`}
</ListSubheader>
}
>
<Stack sx={{ px: 2, py: 1 }}>
<DebugInformation />
</Stack>
</List>
</List> </List>
); );
} }

View File

@@ -993,14 +993,9 @@ msgstr ""
msgid "Convert images to different formats" msgid "Convert images to different formats"
msgstr "Convert images to different formats" msgstr "Convert images to different formats"
#: src/features/extension/store/screens/ExtensionStores.tsx
#: src/features/manga/components/details/MangaDetails.tsx
#: src/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx
msgid "Copied to clipboard"
msgstr "Copied to clipboard"
#: src/features/manga/components/details/MangaDetails.tsx #: src/features/manga/components/details/MangaDetails.tsx
#: src/features/migration/components/MigrationOptionsDialog.tsx #: src/features/migration/components/MigrationOptionsDialog.tsx
#: src/features/settings/components/DebugInformation.tsx
msgid "Copy" msgid "Copy"
msgstr "Copy" msgstr "Copy"
@@ -1072,6 +1067,10 @@ msgstr "Could not install the extension"
msgid "Could not load categories" msgid "Could not load categories"
msgstr "Could not load categories" msgstr "Could not load categories"
#: src/features/settings/components/DebugInformation.tsx
msgid "Could not load data"
msgstr "Could not load data"
#: src/features/settings/screens/Appearance.tsx #: src/features/settings/screens/Appearance.tsx
msgid "Could not load language" msgid "Could not load language"
msgstr "Could not load language" msgstr "Could not load language"
@@ -1314,6 +1313,10 @@ msgstr "Database url"
msgid "Day" msgid "Day"
msgstr "Day" msgstr "Day"
#: src/features/settings/screens/About.tsx
msgid "Debug information"
msgstr "Debug information"
#: src/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx #: src/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx
msgid "Decrease auto scroll speed" msgid "Decrease auto scroll speed"
msgstr "Decrease auto scroll speed" msgstr "Decrease auto scroll speed"
@@ -3361,6 +3364,10 @@ msgstr "Settings"
msgid "Share" msgid "Share"
msgstr "Share" msgstr "Share"
#: src/features/settings/components/DebugInformation.tsx
msgid "Show"
msgstr "Show"
#: src/features/library/screens/LibrarySettings.tsx #: src/features/library/screens/LibrarySettings.tsx
msgid "Show all duplicated entries in your library" msgid "Show all duplicated entries in your library"
msgstr "Show all duplicated entries in your library" msgstr "Show all duplicated entries in your library"

View File

@@ -8,6 +8,7 @@
import { CombinedGraphQLErrors } from '@apollo/client'; import { CombinedGraphQLErrors } from '@apollo/client';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { makeToast } from '@/base/utils/Toast.ts';
export const jsonSaveParse = <T = any>(...args: Parameters<typeof JSON.parse>): T | null => { export const jsonSaveParse = <T = any>(...args: Parameters<typeof JSON.parse>): T | null => {
try { try {
@@ -95,3 +96,37 @@ export const maybeExecuteWithDelay = (
action(); action();
return undefined; return undefined;
}; };
export const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
makeToast('Copied to clipboard', 'info');
} catch (e) {
makeToast('Could not copy to clipboard', 'error', getErrorMessage(e));
}
};
export const getRenderedText = (el: HTMLElement, cssText: string) => {
const clone = el.cloneNode(true) as HTMLElement;
const rect = el.getBoundingClientRect();
clone.style.cssText = `
position: absolute;
left: -99999px;
top: 0;
visibility: visible;
display: block;
height: auto;
max-height: none;
overflow: visible;
width: ${rect.width}px;
${cssText}
`;
document.body.appendChild(clone);
const text = clone.innerText;
clone.remove();
return text;
};