Require new debug information in github bug issue
This commit is contained in:
60
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
60
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -36,27 +36,6 @@ body:
|
||||
validations:
|
||||
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
|
||||
id: server-os
|
||||
attributes:
|
||||
@@ -87,33 +66,22 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: suwayomi-webui-version
|
||||
- type: textarea
|
||||
id: debug-information
|
||||
attributes:
|
||||
label: Suwayomi-WebUI version
|
||||
description: You can find your Suwayomi-WebUI version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "r1689 STABLE | r2401 PREVIEW"
|
||||
validations:
|
||||
required: true
|
||||
label: Debug information
|
||||
description: |
|
||||
Provide the debug information from the About page.
|
||||
_Note:_ You can paste the copied debug information into the placeholder below.
|
||||
value: |
|
||||
<details>
|
||||
<summary>Click here to see debug information</summary>
|
||||
<!-- Please leave one blank line below for enabling the code block rendering. -->
|
||||
|
||||
- type: input
|
||||
id: client-browser
|
||||
attributes:
|
||||
label: Used web browser
|
||||
description: The browser which is used to open Suwayomi-WebUI
|
||||
placeholder: |
|
||||
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"
|
||||
```
|
||||
Replace this text with the copied debug information
|
||||
```
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -16,3 +16,5 @@ export const enhancedCleanup = (str: string): string =>
|
||||
|
||||
export const reverseString = (str: string, separator: string = ''): string =>
|
||||
str.split(separator).reverse().join(separator);
|
||||
|
||||
export const indent = (str: string, level: number, char: string): string => char.repeat(level) + str;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
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 { useLingui } from '@lingui/react/macro';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
@@ -127,9 +127,8 @@ const ExtensionStoreCard = ({
|
||||
)}
|
||||
<CustomTooltip title={t`Copy index url`}>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(indexUrl);
|
||||
makeToast(t`Copied to clipboard`, 'info');
|
||||
onClick={() => {
|
||||
copyToClipboard(indexUrl);
|
||||
}}
|
||||
color="inherit"
|
||||
>
|
||||
|
||||
@@ -27,7 +27,6 @@ import { FlexWrapButton } from '@/base/components/buttons/FlexWrapButton.tsx';
|
||||
import { TrackMangaButton } from '@/features/manga/components/TrackMangaButton.tsx';
|
||||
import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.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 { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.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 { IconWebView } from '@/assets/icons/IconWebView.tsx';
|
||||
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
||||
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
const DetailsWrapper = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@@ -240,15 +240,6 @@ export const MangaDetails = ({
|
||||
|
||||
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 (
|
||||
<DetailsWrapper>
|
||||
<TopContentWrapper url={Mangas.getThumbnailUrl(manga)} mangaThumbnailBackdrop={mangaThumbnailBackdrop}>
|
||||
@@ -263,7 +254,7 @@ export const MangaDetails = ({
|
||||
</SearchLink>
|
||||
{!!navigator.clipboard && (
|
||||
<CustomTooltip title={t`Copy`}>
|
||||
<IconButton onClick={copyTitle} color="inherit">
|
||||
<IconButton onClick={() => copyToClipboard(manga.title)} color="inherit">
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
|
||||
@@ -12,6 +12,8 @@ import { getActiveDevice } from '@/features/device/services/Device.ts';
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -34,7 +36,7 @@ export const getMetadataKey = (key: string, prefixes: string[] = [], appPrefix:
|
||||
(prefix) => prefix.toLowerCase() !== 'default',
|
||||
);
|
||||
|
||||
return `${finalPrefix.join('_')}_${key}`;
|
||||
return getMetadataKeyRaw(key, finalPrefix);
|
||||
};
|
||||
|
||||
export const doesAppMetadataKeyExistIn = (
|
||||
@@ -46,3 +48,19 @@ export const doesAppMetadataKeyExistIn = (
|
||||
|
||||
export const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string): boolean =>
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
MetadataHolderType,
|
||||
MetadataKeyValuePair,
|
||||
} 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 { CategoryIdInfo } from '@/features/category/Category.types.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
@@ -37,22 +37,6 @@ const getAppKeyPrefixForMigration = (migrationId: number): string => {
|
||||
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 migratedMetadata: Metadata = { ...meta };
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { memo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.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 { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
useReaderScrollbarStore,
|
||||
useReaderStore,
|
||||
} from '@/features/reader/stores/ReaderStore.ts';
|
||||
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
|
||||
|
||||
@@ -113,13 +113,7 @@ const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & {
|
||||
>
|
||||
{t`Open in WebView`}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!realUrl}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(title);
|
||||
makeToast(t`Copied to clipboard`, 'info');
|
||||
}}
|
||||
>
|
||||
<MenuItem disabled={!realUrl} onClick={() => copyToClipboard(title)}>
|
||||
{t`Share`}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
448
src/features/settings/components/DebugInformation.tsx
Normal file
448
src/features/settings/components/DebugInformation.tsx
Normal 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>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -22,6 +22,8 @@ import { VersionInfo } from '@/features/app-updates/components/VersionInfo.tsx';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { epochToDate } from '@/base/utils/DateHelper.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() {
|
||||
const { t } = useLingui();
|
||||
@@ -160,6 +162,17 @@ export function About() {
|
||||
<ListItemText primary={t`Discord`} secondary={aboutServer.discord} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader component="div" id="about-webui-info">
|
||||
{t`Debug information`}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<Stack sx={{ px: 2, py: 1 }}>
|
||||
<DebugInformation />
|
||||
</Stack>
|
||||
</List>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -993,14 +993,9 @@ msgstr ""
|
||||
msgid "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/migration/components/MigrationOptionsDialog.tsx
|
||||
#: src/features/settings/components/DebugInformation.tsx
|
||||
msgid "Copy"
|
||||
msgstr "Copy"
|
||||
|
||||
@@ -1072,6 +1067,10 @@ msgstr "Could not install the extension"
|
||||
msgid "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
|
||||
msgid "Could not load language"
|
||||
msgstr "Could not load language"
|
||||
@@ -1314,6 +1313,10 @@ msgstr "Database url"
|
||||
msgid "Day"
|
||||
msgstr "Day"
|
||||
|
||||
#: src/features/settings/screens/About.tsx
|
||||
msgid "Debug information"
|
||||
msgstr "Debug information"
|
||||
|
||||
#: src/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx
|
||||
msgid "Decrease auto scroll speed"
|
||||
msgstr "Decrease auto scroll speed"
|
||||
@@ -3361,6 +3364,10 @@ msgstr "Settings"
|
||||
msgid "Share"
|
||||
msgstr "Share"
|
||||
|
||||
#: src/features/settings/components/DebugInformation.tsx
|
||||
msgid "Show"
|
||||
msgstr "Show"
|
||||
|
||||
#: src/features/library/screens/LibrarySettings.tsx
|
||||
msgid "Show all duplicated entries in your library"
|
||||
msgstr "Show all duplicated entries in your library"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { CombinedGraphQLErrors } from '@apollo/client';
|
||||
import type { ReactNode } from 'react';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
|
||||
export const jsonSaveParse = <T = any>(...args: Parameters<typeof JSON.parse>): T | null => {
|
||||
try {
|
||||
@@ -95,3 +96,37 @@ export const maybeExecuteWithDelay = (
|
||||
action();
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user