Rename folder "modules" to "features"
This commit is contained in:
15
src/features/core/utils/ApplyStyles.ts
Normal file
15
src/features/core/utils/ApplyStyles.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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 { Theme } from '@mui/material/styles';
|
||||
|
||||
// use CSSObject instead of SxProps<Theme> because this completely fucks over typescript by causing an out of memory error during compilation
|
||||
type CSSObject = ReturnType<Theme['applyStyles']>;
|
||||
|
||||
const emptyStyle = {};
|
||||
export const applyStyles = (isActive: boolean, styling: CSSObject): CSSObject => (isActive ? styling : emptyStyle);
|
||||
50
src/features/core/utils/AwaitableDialog.tsx
Normal file
50
src/features/core/utils/AwaitableDialog.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 { createRoot } from 'react-dom/client';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { ConfirmDialog } from '@/features/core/components/modals/ConfirmDialog.tsx';
|
||||
import { ControlledPromise } from '@/lib/ControlledPromise.ts';
|
||||
import { getCurrentTheme } from '@/features/theme/services/ThemeCreator.ts';
|
||||
|
||||
export const awaitConfirmation = async (
|
||||
dialogProps: Omit<React.ComponentProps<typeof ConfirmDialog>, 'onCancel' | 'onConfirm'>,
|
||||
) => {
|
||||
const dialogContainer = document.createElement('div');
|
||||
document.body.appendChild(dialogContainer);
|
||||
|
||||
const root = createRoot(dialogContainer);
|
||||
|
||||
const confirmationPromise = new ControlledPromise();
|
||||
const handleConfirmation = (accepted: boolean) => {
|
||||
if (accepted) {
|
||||
confirmationPromise.resolve();
|
||||
} else {
|
||||
confirmationPromise.reject(new Error('Confirmation declined'));
|
||||
}
|
||||
|
||||
root.unmount();
|
||||
document.body.removeChild(dialogContainer);
|
||||
};
|
||||
|
||||
root.render(
|
||||
<ThemeProvider theme={getCurrentTheme()}>
|
||||
<ConfirmDialog
|
||||
{...dialogProps}
|
||||
onExtra={() => {
|
||||
handleConfirmation(false);
|
||||
dialogProps.onExtra?.();
|
||||
}}
|
||||
onCancel={() => handleConfirmation(false)}
|
||||
onConfirm={() => handleConfirmation(true)}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
return confirmationPromise.promise;
|
||||
};
|
||||
144
src/features/core/utils/Languages.ts
Normal file
144
src/features/core/utils/Languages.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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 { t } from 'i18next';
|
||||
import { ISOLanguage, IsoLanguages } from '@/features/core/IsoLanguages.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
export enum DefaultLanguage {
|
||||
ALL = 'all',
|
||||
OTHER = 'other',
|
||||
LOCAL_SOURCE = 'localsourcelang',
|
||||
PINNED = 'pinned',
|
||||
LAST_USED_SOURCE = 'last_used_source',
|
||||
}
|
||||
|
||||
const DEFAULT_LANGUAGE_TO_TRANSLATION: Record<DefaultLanguage, TranslationKey> = {
|
||||
[DefaultLanguage.ALL]: 'extension.language.all',
|
||||
[DefaultLanguage.OTHER]: 'extension.language.other',
|
||||
[DefaultLanguage.LOCAL_SOURCE]: 'extension.language.other',
|
||||
[DefaultLanguage.PINNED]: 'global.label.pinned',
|
||||
[DefaultLanguage.LAST_USED_SOURCE]: 'global.label.last_used',
|
||||
};
|
||||
|
||||
type LanguageObject = ISOLanguage & { orgCode: string; isoCode: string };
|
||||
|
||||
function getISOLanguage(code: string): LanguageObject | null {
|
||||
if (IsoLanguages[code]) {
|
||||
return {
|
||||
...IsoLanguages[code],
|
||||
orgCode: code,
|
||||
isoCode: code,
|
||||
};
|
||||
}
|
||||
|
||||
if (IsoLanguages[code.toLocaleLowerCase()]) {
|
||||
return {
|
||||
...IsoLanguages[code.toLocaleLowerCase()],
|
||||
orgCode: code,
|
||||
isoCode: code.toLocaleLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
const whereToCut = code.indexOf('-') !== -1 ? code.indexOf('-') : code.length;
|
||||
const processedCode = code.toLocaleLowerCase().substring(0, whereToCut);
|
||||
if (IsoLanguages[processedCode]) {
|
||||
return {
|
||||
...IsoLanguages[processedCode],
|
||||
orgCode: code,
|
||||
isoCode: processedCode,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getLanguage(code: string): LanguageObject {
|
||||
const isoLanguage = getISOLanguage(code);
|
||||
|
||||
if (isoLanguage) {
|
||||
return isoLanguage;
|
||||
}
|
||||
|
||||
return {
|
||||
orgCode: code,
|
||||
isoCode: code,
|
||||
name: t('global.language.label.language_with_code', { code }),
|
||||
nativeName: t('global.language.label.language_with_code', { code }),
|
||||
};
|
||||
}
|
||||
|
||||
export function languageCodeToName(code: string): string {
|
||||
const isCustomLanguage = Object.keys(DEFAULT_LANGUAGE_TO_TRANSLATION).includes(code);
|
||||
if (isCustomLanguage) {
|
||||
return t(DEFAULT_LANGUAGE_TO_TRANSLATION[code as DefaultLanguage]);
|
||||
}
|
||||
|
||||
return getLanguage(code).nativeName;
|
||||
}
|
||||
|
||||
export const toUniqueLanguageCodes = (codes: string[]): string[] => {
|
||||
const languages = codes.map((code) => getLanguage(code));
|
||||
const languagesByIsoCode = Object.groupBy(languages, (language) => language.isoCode);
|
||||
|
||||
return Object.entries(languagesByIsoCode)
|
||||
.filter(([, languagesOfIsoCode]) => !!languagesOfIsoCode?.length)
|
||||
.map(([, languagesOfIsoCode]) => languagesOfIsoCode![0].orgCode);
|
||||
};
|
||||
|
||||
export const toComparableLanguage = (code: string): string => getLanguage(code).isoCode;
|
||||
|
||||
export const toComparableLanguages = (codes: string[]): string[] => codes.map(toComparableLanguage);
|
||||
|
||||
function defaultNativeLang(): readonly string[] {
|
||||
const preferredLanguages = toUniqueLanguageCodes([...navigator.languages]);
|
||||
|
||||
if (!preferredLanguages.length) {
|
||||
return ['en'];
|
||||
}
|
||||
|
||||
return preferredLanguages;
|
||||
}
|
||||
|
||||
export function getDefaultLanguages(): string[] {
|
||||
return [...defaultNativeLang(), DefaultLanguage.ALL];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort languages by their native name.
|
||||
* Custom languages are optionally treated specially:
|
||||
* - All: first
|
||||
* - Other: last
|
||||
*/
|
||||
export const languageSortComparator = (a: string, b: string, specialCustomLanguagesHandling?: boolean) => {
|
||||
const isALanguageAll = a === DefaultLanguage.ALL;
|
||||
const isALanguageOther = a === DefaultLanguage.OTHER || a === DefaultLanguage.LOCAL_SOURCE;
|
||||
|
||||
const isBLanguageAll = b === DefaultLanguage.ALL;
|
||||
const isBLanguageOther = b === DefaultLanguage.OTHER || b === DefaultLanguage.LOCAL_SOURCE;
|
||||
|
||||
if (specialCustomLanguagesHandling) {
|
||||
if (isALanguageAll || isBLanguageOther) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (isALanguageOther || isBLanguageAll) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return languageCodeToName(a).localeCompare(languageCodeToName(b));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort languages by their native name.
|
||||
* Custom languages are treated specially:
|
||||
* - All: first
|
||||
* - Other: last
|
||||
*/
|
||||
export const languageSpecialSortComparator = (a: string, b: string) => languageSortComparator(a, b, true);
|
||||
11
src/features/core/utils/LazyLoad.tsx
Normal file
11
src/features/core/utils/LazyLoad.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
|
||||
|
||||
export const lazyLoadFallback = { fallback: <LoadingPlaceholder /> };
|
||||
132
src/features/core/utils/MediaQuery.tsx
Normal file
132
src/features/core/utils/MediaQuery.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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 useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { Breakpoint, SxProps, Theme } from '@mui/material/styles';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getCurrentTheme } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ThemeMode } from '@/features/theme/contexts/AppThemeContext.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
export class MediaQuery {
|
||||
static readonly MOBILE_WIDTH: Breakpoint | number = 'sm';
|
||||
|
||||
static readonly TABLET_WIDTH: Breakpoint | number = 1025;
|
||||
|
||||
static isTouchDevice(): boolean {
|
||||
return window.matchMedia('not (pointer: fine)').matches;
|
||||
}
|
||||
|
||||
static useIsTouchDevice(): boolean {
|
||||
return useMediaQuery('not (pointer: fine)');
|
||||
}
|
||||
|
||||
static useIsBelowWidth(breakpoint: Breakpoint | number): boolean {
|
||||
return useMediaQuery(getCurrentTheme().breakpoints.down(breakpoint));
|
||||
}
|
||||
|
||||
static useIsMobileWidth(): boolean {
|
||||
return this.useIsBelowWidth(this.MOBILE_WIDTH);
|
||||
}
|
||||
|
||||
static useIsTabletWidth(): boolean {
|
||||
return this.useIsBelowWidth(this.TABLET_WIDTH);
|
||||
}
|
||||
|
||||
private static getScrollbarSize(type: 'height' | 'width'): number {
|
||||
const outer = document.createElement('div');
|
||||
outer.style.position = 'absolute';
|
||||
outer.style.top = '-9999px';
|
||||
outer.style.visibility = 'hidden';
|
||||
outer.style.overflow = 'scroll';
|
||||
document.body.appendChild(outer);
|
||||
|
||||
const inner = document.createElement('div');
|
||||
inner.style.width = '100%';
|
||||
inner.style.height = '100%';
|
||||
outer.appendChild(inner);
|
||||
|
||||
const width = outer.offsetWidth - inner.offsetWidth;
|
||||
const height = outer.offsetHeight - inner.offsetHeight;
|
||||
|
||||
document.body.removeChild(outer);
|
||||
|
||||
return type === 'height' ? height : width;
|
||||
}
|
||||
|
||||
static useGetScrollbarSize(
|
||||
type: 'height' | 'width',
|
||||
element: HTMLElement | null = document.documentElement,
|
||||
): number {
|
||||
const [scrollbarSize, setScrollbarSize] = useState(0);
|
||||
|
||||
useResizeObserver(
|
||||
element,
|
||||
useCallback(() => {
|
||||
const hasYScrollbar = !!(element!.scrollHeight - element!.clientHeight);
|
||||
const hasXScrollbar = !!(element!.scrollWidth - element!.clientWidth);
|
||||
|
||||
const hasScrollbar = (type === 'height' && hasYScrollbar) || (type === 'width' && hasXScrollbar);
|
||||
if (hasScrollbar) {
|
||||
setScrollbarSize(this.getScrollbarSize(type));
|
||||
return;
|
||||
}
|
||||
|
||||
setScrollbarSize(0);
|
||||
}, [element]),
|
||||
);
|
||||
|
||||
return scrollbarSize;
|
||||
}
|
||||
|
||||
static getSystemThemeMode(): Exclude<ThemeMode, 'system'> {
|
||||
const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
return prefersDarkMode ? ThemeMode.DARK : ThemeMode.LIGHT;
|
||||
}
|
||||
|
||||
static getThemeMode(themeMode: ThemeMode): Exclude<ThemeMode, 'system'> {
|
||||
const isSystemMode = themeMode === ThemeMode.SYSTEM;
|
||||
if (isSystemMode) {
|
||||
return this.getSystemThemeMode();
|
||||
}
|
||||
|
||||
return themeMode;
|
||||
}
|
||||
|
||||
static listenToSystemThemeChange(onChange: (themeMode: Exclude<ThemeMode, 'system'>) => void): () => void {
|
||||
const handleSystemThemeModeChange = (e: MediaQueryListEvent) => {
|
||||
onChange(e.matches ? ThemeMode.DARK : ThemeMode.LIGHT);
|
||||
};
|
||||
|
||||
const matchSystemThemeMode = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
matchSystemThemeMode.addEventListener('change', handleSystemThemeModeChange);
|
||||
|
||||
return () => matchSystemThemeMode.removeEventListener('change', handleSystemThemeModeChange);
|
||||
}
|
||||
|
||||
static usePreventMobileContextMenu() {
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
return useCallback(
|
||||
(e: React.MouseEvent<any, MouseEvent>) => {
|
||||
if (isTouchDevice) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[isTouchDevice],
|
||||
);
|
||||
}
|
||||
|
||||
static preventMobileContextMenuSx(): SxProps<Theme> {
|
||||
return {
|
||||
userSelect: 'none',
|
||||
'-webkit-touch-callout': 'none',
|
||||
};
|
||||
}
|
||||
}
|
||||
14
src/features/core/utils/ShouldForwardProp.ts
Normal file
14
src/features/core/utils/ShouldForwardProp.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
export const shouldForwardProp =
|
||||
<TCustomProps extends Record<string, unknown>>(customProps: TupleUnion<keyof TCustomProps>) =>
|
||||
(prop: string): boolean =>
|
||||
// @ts-ignore - TS2589: Type instantiation is excessively deep and possibly infinite.
|
||||
// this function should never be used without a strict type, thus, this error can be ignored
|
||||
!customProps.includes(prop);
|
||||
28
src/features/core/utils/Toast.ts
Normal file
28
src/features/core/utils/Toast.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { enqueueSnackbar, OptionsObject, SnackbarKey } from 'notistack';
|
||||
|
||||
export function makeToast(message: string, severity?: OptionsObject['variant'], description?: string): SnackbarKey;
|
||||
export function makeToast(message: string, options?: OptionsObject, description?: string): SnackbarKey;
|
||||
export function makeToast(
|
||||
message: string,
|
||||
options: OptionsObject['variant'] | OptionsObject = 'default',
|
||||
description?: string,
|
||||
): SnackbarKey {
|
||||
const variant = typeof options === 'string' ? options : undefined;
|
||||
const snackbarOptions = typeof options === 'object' ? options : {};
|
||||
|
||||
return enqueueSnackbar(message, {
|
||||
variant,
|
||||
...snackbarOptions,
|
||||
// @ts-ignore - TS2353, "notistack" is outdated and the provided way to define custom props is not working, however,
|
||||
// everything in the options object gets passed to the custom snackbar component
|
||||
description,
|
||||
});
|
||||
}
|
||||
23
src/features/core/utils/ValueRotationButton.utils.ts
Normal file
23
src/features/core/utils/ValueRotationButton.utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
export const getNextRotationValue = <Value>(
|
||||
indexOfValue: number,
|
||||
values: Value[],
|
||||
isDefaultable?: boolean,
|
||||
): Value | undefined => {
|
||||
const nextValueIndex = (indexOfValue + 1) % values.length;
|
||||
const wasLastValue = nextValueIndex === 0;
|
||||
|
||||
const isDefaultNextValue = !!isDefaultable && wasLastValue;
|
||||
if (isDefaultNextValue) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return values[(indexOfValue + 1) % values.length];
|
||||
};
|
||||
Reference in New Issue
Block a user