Require new debug information in github bug issue
This commit is contained in:
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user