Migrate to lingui
Switch to "lingui" for better DX. Tried to persist existing languages as much as possible. Removed "vite-plugin-node-polyfills" because it's incompatible with "lingui"
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
@@ -14,6 +13,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { useUpdateChecker } from '@/features/app-updates/hooks/useUpdateChecker.tsx';
|
||||
import { VersionUpdateInfoDialog } from '@/features/app-updates/components/VersionUpdateInfoDialog.tsx';
|
||||
@@ -24,7 +24,7 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
const disabledUpdateCheck = () => Promise.resolve();
|
||||
|
||||
export const ServerUpdateChecker = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [serverVersion, setServerVersion] = useLocalStorage<string>('serverVersion');
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -98,11 +98,8 @@ export const ServerUpdateChecker = () => {
|
||||
|
||||
return (
|
||||
<VersionUpdateInfoDialog
|
||||
info={t('global.update.label.info', {
|
||||
channel: selectedServerChannelInfo.channel,
|
||||
version: selectedServerChannelInfo.tag,
|
||||
})}
|
||||
actionTitle={t('global.button.download')}
|
||||
info={t`Server version ${selectedServerChannelInfo.tag} (${selectedServerChannelInfo.channel}) available for download`}
|
||||
actionTitle={t`Download`}
|
||||
actionUrl={selectedServerChannelInfo.url}
|
||||
updateCheckerProps={['server', checkForUpdate, selectedServerChannelInfo?.tag]}
|
||||
/>
|
||||
@@ -115,20 +112,14 @@ export const ServerUpdateChecker = () => {
|
||||
|
||||
return (
|
||||
<Dialog open={open}>
|
||||
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
|
||||
<DialogTitle>{t`Updated version`}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('global.update.label.update_success', {
|
||||
name: t('settings.server.title.server'),
|
||||
version,
|
||||
channel: aboutServer?.buildType,
|
||||
})}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t`Server was updated to version ${version} (${aboutServer?.buildType})`}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{changelogUrl && (
|
||||
<Button href={changelogUrl} target="_blank" rel="noreferrer">
|
||||
{t('global.button.changelog')}
|
||||
{t`Changelog`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -138,7 +129,7 @@ export const ServerUpdateChecker = () => {
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t('global.button.ok')}
|
||||
{t`Ok`}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -13,8 +13,8 @@ import Typography from '@mui/material/Typography';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import { t as translate } from 'i18next';
|
||||
import DownloadingIcon from '@mui/icons-material/Downloading';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { UpdateState } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export type BaseVersionInfoProps = {
|
||||
@@ -69,27 +69,27 @@ const getUpdateCheckButtonText = (
|
||||
) => {
|
||||
const isUpdating = updateState === UpdateState.Downloading;
|
||||
if (isUpdating) {
|
||||
return translate('global.update.label.updating', { progress });
|
||||
return t`${progress}% | Updating…`;
|
||||
}
|
||||
|
||||
const didUpdateFail = updateState === UpdateState.Error;
|
||||
if (didUpdateFail) {
|
||||
return translate('global.update.label.update_failure');
|
||||
return t`Update failed`;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return translate('global.update.label.checking');
|
||||
return t`Checking for update`;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return translate('global.update.label.check_failure');
|
||||
return t`Could not check for update`;
|
||||
}
|
||||
|
||||
if (isUpdateAvailable) {
|
||||
return translate('global.update.label.available');
|
||||
return t`Update available`;
|
||||
}
|
||||
|
||||
return translate('global.update.label.up_to_date');
|
||||
return t`This is the latest version`;
|
||||
};
|
||||
|
||||
export const VersionInfo = ({
|
||||
|
||||
@@ -15,8 +15,8 @@ import DialogContentText from '@mui/material/DialogContentText';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useUpdateChecker } from '@/features/app-updates/hooks/useUpdateChecker.tsx';
|
||||
|
||||
interface BaseProps {
|
||||
@@ -48,7 +48,7 @@ export const VersionUpdateInfoDialog = ({
|
||||
changelogUrl,
|
||||
disabled,
|
||||
}: VersionUpdateInfoDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const updateChecker = useUpdateChecker(...updateCheckerProps);
|
||||
|
||||
@@ -58,7 +58,7 @@ export const VersionUpdateInfoDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogTitle>{t('global.update.label.available')}</DialogTitle>
|
||||
<DialogTitle>{t`Update available`}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{info}</DialogContentText>
|
||||
</DialogContent>
|
||||
@@ -72,7 +72,7 @@ export const VersionUpdateInfoDialog = ({
|
||||
>
|
||||
{changelogUrl && (
|
||||
<Button href={changelogUrl} target="_blank" rel="noreferrer">
|
||||
{t('global.button.changelog')}
|
||||
{t`Changelog`}
|
||||
</Button>
|
||||
)}
|
||||
<Stack direction="row">
|
||||
@@ -80,7 +80,7 @@ export const VersionUpdateInfoDialog = ({
|
||||
{(popupState) => (
|
||||
<>
|
||||
<Button disabled={disabled} {...bindTrigger(popupState)}>
|
||||
{t('global.label.close')}
|
||||
{t`Close`}
|
||||
</Button>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
@@ -89,7 +89,7 @@ export const VersionUpdateInfoDialog = ({
|
||||
popupState.close();
|
||||
}}
|
||||
>
|
||||
{t('global.button.remind_later')}
|
||||
{t`Remind later`}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
@@ -97,7 +97,7 @@ export const VersionUpdateInfoDialog = ({
|
||||
popupState.close();
|
||||
}}
|
||||
>
|
||||
{t('global.button.ignore')}
|
||||
{t`Ignore`}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
@@ -14,6 +13,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { UpdateState, WebUiChannel, WebUiUpdateStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { useLocalStorage, useSessionStorage } from '@/base/hooks/useStorage.tsx';
|
||||
@@ -36,7 +36,7 @@ if (BrowserUtil.isActualPageLoad()) {
|
||||
}
|
||||
|
||||
export const WebUIUpdateChecker = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [webUIVersion, setWebUIVersion] = useLocalStorage<string>('webUIVersion');
|
||||
const [initialLoadTimestamp] = useSessionStorage<number>(INITIAL_LOAD_TIMESTAMP_KEY, Date.now());
|
||||
@@ -93,7 +93,7 @@ export const WebUIUpdateChecker = () => {
|
||||
useEffect(() => {
|
||||
const isError = webUIUpdateState === UpdateState.Error;
|
||||
if (isError) {
|
||||
makeToast(t('settings.about.webui.label.update_failure'), 'error');
|
||||
makeToast(t`Could not update WebUI`, 'error');
|
||||
}
|
||||
|
||||
const updateFinished = webUIUpdateState === UpdateState.Finished;
|
||||
@@ -139,24 +139,15 @@ export const WebUIUpdateChecker = () => {
|
||||
|
||||
return (
|
||||
<VersionUpdateInfoDialog
|
||||
info={t('settings.about.webui.label.info', {
|
||||
version: webUIUpdateData?.checkForWebUIUpdate.tag,
|
||||
channel: webUIUpdateData?.checkForWebUIUpdate.channel,
|
||||
})}
|
||||
info={t`WebUI version ${webUIUpdateData?.checkForWebUIUpdate.tag} (${webUIUpdateData?.checkForWebUIUpdate.channel}) available for download`}
|
||||
changelogUrl={changelogUrl}
|
||||
disabled={isUpdateInProgress}
|
||||
onAction={() =>
|
||||
requestManager
|
||||
.updateWebUI()
|
||||
.response.catch((e) =>
|
||||
makeToast(t('settings.about.webui.label.update_failure'), 'error', getErrorMessage(e)),
|
||||
)
|
||||
}
|
||||
actionTitle={
|
||||
isUpdateInProgress
|
||||
? t('global.update.label.updating', { progress: updateStatus.progress })
|
||||
: t('extension.action.label.update')
|
||||
.response.catch((e) => makeToast(t`Could not update WebUI`, 'error', getErrorMessage(e)))
|
||||
}
|
||||
actionTitle={isUpdateInProgress ? t`${updateStatus.progress}% | Updating…` : t`Update`}
|
||||
updateCheckerProps={[
|
||||
'webUI',
|
||||
isAutoUpdateEnabled ? disabledUpdateCheck : checkForUpdate,
|
||||
@@ -172,19 +163,13 @@ export const WebUIUpdateChecker = () => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={shouldForceRefresh ? () => setOpen(false) : noOp}>
|
||||
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
|
||||
<DialogTitle>{t`Updated version`}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('global.update.label.update_success', {
|
||||
name: t('settings.webui.title.webui'),
|
||||
version: newVersion,
|
||||
channel: aboutWebUI?.channel,
|
||||
})}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{t`{name} was updated to version ${newVersion} (${aboutWebUI?.channel})`}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button href={changelogUrl} target="_blank" rel="noreferrer">
|
||||
{t('global.button.changelog')}
|
||||
{t`Changelog`}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -197,7 +182,7 @@ export const WebUIUpdateChecker = () => {
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t(shouldForceRefresh ? 'global.button.refresh' : 'global.button.ok')}
|
||||
{shouldForceRefresh ? t`Refresh` : t`Ok`}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Stack from '@mui/material/Stack';
|
||||
@@ -14,6 +13,7 @@ import Button from '@mui/material/Button';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
@@ -27,7 +27,7 @@ import { ServerAddressSetting } from '@/features/settings/components/ServerAddre
|
||||
|
||||
export const LoginPage = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { setOverride } = useNavBarContext();
|
||||
const navigate = useNavigate();
|
||||
const isAuthenticated = AuthManager.useIsAuthenticated();
|
||||
@@ -48,7 +48,7 @@ export const LoginPage = () => {
|
||||
navigate(redirect ?? AppRoutes.root.path);
|
||||
}
|
||||
} catch (e) {
|
||||
makeToast(t('tracking.action.login.label.failure', { name: 'Suwayomi' }), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not log in to Suwayomi`, 'error', getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ export const LoginPage = () => {
|
||||
margin="dense"
|
||||
id="username"
|
||||
name="username"
|
||||
label={t('global.label.username')}
|
||||
label={t`Username`}
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
@@ -127,7 +127,7 @@ export const LoginPage = () => {
|
||||
/>
|
||||
</Stack>
|
||||
<Button disabled={isLoading || (!username && !password)} variant="contained" onClick={doLogin}>
|
||||
{t('global.button.log_in')}
|
||||
{t`Log in`}
|
||||
</Button>
|
||||
<Stack sx={{ position: 'absolute', left: 0, bottom: 0 }}>
|
||||
<ServerAddressSetting />
|
||||
|
||||
@@ -6,22 +6,23 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { BackupFlag, BackupFlagGroup } from '@/features/backup/Backup.types.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, TranslationKey> = {
|
||||
includeManga: 'settings.backup.flag.manga',
|
||||
includeCategories: 'settings.backup.flag.categories',
|
||||
includeChapters: 'settings.backup.flag.chapters',
|
||||
includeClientData: 'settings.backup.flag.client_data',
|
||||
includeHistory: 'settings.backup.flag.history',
|
||||
includeServerSettings: 'settings.backup.flag.server_settings',
|
||||
includeTracking: 'settings.backup.flag.tracking',
|
||||
export const BACKUP_FLAGS_TO_TRANSLATION: Record<BackupFlag, MessageDescriptor> = {
|
||||
includeManga: msg`Library entries`,
|
||||
includeCategories: msg`Categories`,
|
||||
includeChapters: msg`Chapters`,
|
||||
includeClientData: msg`Client data`,
|
||||
includeHistory: msg`History`,
|
||||
includeServerSettings: msg`Server settings`,
|
||||
includeTracking: msg`Tracking`,
|
||||
};
|
||||
|
||||
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, TranslationKey> = {
|
||||
[BackupFlagGroup.LIBRARY]: 'settings.backup.flag.group.library',
|
||||
[BackupFlagGroup.SETTINGS]: 'settings.backup.flag.group.settings',
|
||||
export const BACKUP_FLAG_GROUP_TO_TRANSLATION: Record<BackupFlagGroup, MessageDescriptor> = {
|
||||
[BackupFlagGroup.LIBRARY]: msg`Library`,
|
||||
[BackupFlagGroup.SETTINGS]: msg`Settings`,
|
||||
};
|
||||
|
||||
export const BACKUP_FLAGS = Object.keys(BACKUP_FLAGS_TO_TRANSLATION) as readonly BackupFlag[];
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { t as translate } from 'i18next';
|
||||
import { plural, t } from '@lingui/core/macro';
|
||||
import { AutoBackupFlagInclusionState, BackupFlag, BackupFlagInclusionState } from '@/features/backup/Backup.types.ts';
|
||||
import { BACKUP_FLAGS_TO_TRANSLATION } from '@/features/backup/Backup.constants.ts';
|
||||
|
||||
@@ -32,11 +32,11 @@ export const convertToBackupFlags = (flags: AutoBackupFlagInclusionState): Backu
|
||||
|
||||
const getIncludeExcludeText = (count: number, allCount: number, specificText: string): string => {
|
||||
if (count === 0) {
|
||||
return translate('global.label.none');
|
||||
return t`None`;
|
||||
}
|
||||
|
||||
if (count === allCount) {
|
||||
return translate('extension.language.all');
|
||||
return t`All`;
|
||||
}
|
||||
|
||||
return specificText;
|
||||
@@ -49,9 +49,9 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
|
||||
const flagsByState = Object.groupBy(Object.entries(flags), ([, value]) => value.toString());
|
||||
|
||||
const includedFlagsString =
|
||||
flagsByState.true?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
flagsByState.true?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
const excludedFlagsString =
|
||||
flagsByState.false?.map(([key]) => translate(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
flagsByState.false?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
|
||||
return {
|
||||
false: getIncludeExcludeText(flagsByState.false?.length ?? 0, totalFlags, excludedFlagsString),
|
||||
@@ -61,8 +61,11 @@ export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState):
|
||||
|
||||
export const getBackupCleanupDisplayValue = (ttl: number): string => {
|
||||
if (ttl === 0) {
|
||||
return translate('global.label.never');
|
||||
return t`Never`;
|
||||
}
|
||||
|
||||
return translate('settings.backup.automated.cleanup.label.value', { days: ttl, count: ttl });
|
||||
return plural(ttl, {
|
||||
one: `Delete backups that are older than # day`,
|
||||
other: `Delete backups that are older than # days`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,12 +11,12 @@ import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { AwaitableComponentProps } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import {
|
||||
BACKUP_FLAG_GROUP_TO_TRANSLATION,
|
||||
@@ -34,7 +34,7 @@ export const BackupFlagInclusionDialog = ({
|
||||
title,
|
||||
flags,
|
||||
}: AwaitableComponentProps<BackupFlagInclusionState> & { title: string; flags?: BackupFlagInclusionState }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [includeStateByFlag, setIncludeStateByFlag] = useState(
|
||||
Object.fromEntries(BACKUP_FLAGS.map((flag) => [flag, flags?.[flag] ?? true])) as BackupFlagInclusionState,
|
||||
@@ -67,10 +67,10 @@ export const BackupFlagInclusionDialog = ({
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={onDismiss} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button onClick={() => onSubmit(includeStateByFlag)} color="primary">
|
||||
{t('global.button.ok')}
|
||||
{t`Ok`}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import List from '@mui/material/List';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
@@ -17,6 +16,7 @@ import ListItem from '@mui/material/ListItem';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { AwaitableComponentProps } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { BrowseTab } from '@/features/browse/Browse.types.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import { ValidateBackupResult } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -28,16 +28,16 @@ export const BackupValidationDialog = ({
|
||||
isVisible,
|
||||
onExitComplete,
|
||||
}: AwaitableComponentProps & { validationResult: ValidateBackupResult }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Dialog open={isVisible} onTransitionExited={onExitComplete} onClose={onDismiss}>
|
||||
<DialogTitle>{t('settings.backup.action.validate.dialog.title')}</DialogTitle>
|
||||
<DialogTitle>{t`Backup validation`}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{!!validationResult?.missingSources.length && (
|
||||
<List
|
||||
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
|
||||
subheader={t('settings.backup.action.validate.dialog.content.label.missing_sources')}
|
||||
subheader={t`The following sources are not installed:`}
|
||||
>
|
||||
{validationResult?.missingSources.map(({ id, name }) => (
|
||||
<ListItem sx={{ display: 'list-item' }} key={id}>
|
||||
@@ -49,7 +49,7 @@ export const BackupValidationDialog = ({
|
||||
{!!validationResult?.missingTrackers.length && (
|
||||
<List
|
||||
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
|
||||
subheader={t('settings.backup.action.validate.dialog.content.label.missing_trackers')}
|
||||
subheader={t`The following trackers are not logged in:`}
|
||||
>
|
||||
{validationResult?.missingTrackers.map(({ name }) => (
|
||||
<ListItem sx={{ display: 'list-item' }} key={name}>
|
||||
@@ -75,7 +75,7 @@ export const BackupValidationDialog = ({
|
||||
autoFocus={!!validationResult?.missingSources.length}
|
||||
variant={validationResult?.missingSources.length ? 'contained' : 'text'}
|
||||
>
|
||||
{t('extension.action.label.install')}
|
||||
{t`Install`}
|
||||
</Button>
|
||||
)}
|
||||
{!!validationResult?.missingTrackers.length && (
|
||||
@@ -86,11 +86,11 @@ export const BackupValidationDialog = ({
|
||||
autoFocus={!!validationResult?.missingTrackers.length}
|
||||
variant={validationResult?.missingTrackers.length ? 'contained' : 'text'}
|
||||
>
|
||||
{t('global.button.log_in')}
|
||||
{t`Log in`}
|
||||
</Button>
|
||||
)}
|
||||
<Stack direction="row">
|
||||
<Button onClick={onDismiss}>{t('global.button.cancel')}</Button>
|
||||
<Button onClick={onDismiss}>{t`Cancel`}</Button>
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
autoFocus={
|
||||
@@ -102,7 +102,7 @@ export const BackupValidationDialog = ({
|
||||
: 'text'
|
||||
}
|
||||
>
|
||||
{t('global.button.restore')}
|
||||
{t`Restore`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -10,12 +10,13 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { fromEvent } from 'file-selector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import { useEventListener, useMergedRef, useWindowEvent } from '@mantine/hooks';
|
||||
import { AwaitableComponent } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -41,9 +42,9 @@ import { BackupSettingsType } from '@/features/backup/Backup.types.ts';
|
||||
let backupRestoreId: string | undefined;
|
||||
|
||||
export function Backup() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('settings.backup.title'));
|
||||
useAppTitle(t`Backup`);
|
||||
|
||||
const {
|
||||
data: settingsData,
|
||||
@@ -76,7 +77,7 @@ export function Backup() {
|
||||
value: BackupSettingsType[Setting],
|
||||
) => {
|
||||
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,7 +85,7 @@ export function Backup() {
|
||||
settings: Record<Setting, BackupSettingsType[Setting]>,
|
||||
) => {
|
||||
mutateSettings({ variables: { input: { settings } } }).catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,11 +100,11 @@ export function Backup() {
|
||||
const isRestoreFinished = isSuccess || isFailure;
|
||||
if (isRestoreFinished) {
|
||||
if (isSuccess) {
|
||||
makeToast(t('settings.backup.action.restore.label.success'), 'success');
|
||||
makeToast(t`Backup restored.`, 'success');
|
||||
}
|
||||
|
||||
if (isFailure) {
|
||||
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error');
|
||||
makeToast(t`Could not restore backup`, 'error');
|
||||
}
|
||||
|
||||
requestManager.reset();
|
||||
@@ -121,21 +122,17 @@ export function Backup() {
|
||||
|
||||
const createBackup = async () => {
|
||||
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
|
||||
title: t('settings.backup.action.create.label.title'),
|
||||
title: t`Create backup`,
|
||||
});
|
||||
|
||||
makeToast(t('settings.backup.action.create.label.in_progress'), 'info');
|
||||
makeToast(t`Creating backup…`, 'info');
|
||||
|
||||
try {
|
||||
const backupFileResponse = await requestManager.createBackupFile({ flags }).response;
|
||||
|
||||
const backupFileUrl = backupFileResponse.data?.createBackup.url;
|
||||
if (!backupFileUrl) {
|
||||
makeToast(
|
||||
t('settings.backup.action.create.error.failure'),
|
||||
'error',
|
||||
getErrorMessage(backupFileResponse.errors),
|
||||
);
|
||||
makeToast(t`Could not create backup`, 'error', getErrorMessage(backupFileResponse.errors));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,7 +143,7 @@ export function Backup() {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (e) {
|
||||
makeToast(t('settings.backup.action.create.error.failure'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not create backup`, 'error', getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,7 +169,7 @@ export function Backup() {
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
makeToast(t('settings.backup.action.validate.error.label.failure'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not validate backup`, 'error', getErrorMessage(e));
|
||||
} finally {
|
||||
resetBackupState();
|
||||
}
|
||||
@@ -182,17 +179,17 @@ export function Backup() {
|
||||
|
||||
const restoreBackup = async (backup: File) => {
|
||||
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
|
||||
title: t('settings.backup.action.restore.label.title'),
|
||||
title: t`Restore Backup`,
|
||||
});
|
||||
|
||||
try {
|
||||
makeToast(t('settings.backup.action.restore.label.in_progress'), 'info');
|
||||
makeToast(t`Restoring backup…`, 'info');
|
||||
|
||||
const response = await requestManager.restoreBackupFile({ backup, flags }).response;
|
||||
backupRestoreId = response.data?.restoreBackup.id;
|
||||
setTriggerReRender(Date.now());
|
||||
} catch (e) {
|
||||
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not restore backup`, 'error', getErrorMessage(e));
|
||||
} finally {
|
||||
resetBackupState();
|
||||
}
|
||||
@@ -200,13 +197,13 @@ export function Backup() {
|
||||
|
||||
const submitBackup = async (file: File) => {
|
||||
if (file.name.toLowerCase().endsWith('json')) {
|
||||
makeToast(t('settings.backup.action.restore.error.label.legacy_backup_unsupported'), 'error');
|
||||
makeToast(t`legacy backups are not supported!`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const isValidFilename = file.name.toLowerCase().match(/proto\.gz$|tachibk$/g);
|
||||
if (!isValidFilename) {
|
||||
makeToast(t('global.error.label.invalid_file_type'), 'error');
|
||||
makeToast(t`Invalid filetype`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,7 +235,7 @@ export function Backup() {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('Backup::refetch'))}
|
||||
/>
|
||||
@@ -247,19 +244,20 @@ export function Backup() {
|
||||
|
||||
const backupSettings = settingsData!.settings;
|
||||
|
||||
const autoBackupFlagsInfo = getAutoBackupFlagsInfo(backupSettings);
|
||||
const includedCategoriesText = autoBackupFlagsInfo.true;
|
||||
const excludedCategoriesText = autoBackupFlagsInfo.false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<List sx={{ padding: 0 }}>
|
||||
<ListItemButton onClick={createBackup}>
|
||||
<ListItemText
|
||||
primary={t('settings.backup.action.create.label.title')}
|
||||
secondary={t('settings.backup.action.create.label.description')}
|
||||
/>
|
||||
<ListItemText primary={t`Create backup`} secondary={t`Back up library as a Tachiyomi backup`} />
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={() => inputRef.current?.click()} disabled={!!backupRestoreId}>
|
||||
<ListItemText
|
||||
primary={t('settings.backup.action.restore.label.title')}
|
||||
secondary={t('settings.backup.action.restore.label.description')}
|
||||
primary={t`Restore Backup`}
|
||||
secondary={t`You can also drag and drop the backup file here to restore it`}
|
||||
/>
|
||||
{backupRestoreId ? (
|
||||
<ListItemIcon>
|
||||
@@ -275,19 +273,17 @@ export function Backup() {
|
||||
}
|
||||
>
|
||||
<TextSetting
|
||||
settingName={t('settings.backup.automated.location.label.title')}
|
||||
dialogDescription={t('settings.backup.automated.location.label.description')}
|
||||
settingName={t`Backup location`}
|
||||
dialogDescription={t`The path to the directory on the server where automated backups should get saved in`}
|
||||
value={backupSettings.backupPath}
|
||||
settingDescription={
|
||||
backupSettings.backupPath.length ? backupSettings.backupPath : t('global.label.default')
|
||||
}
|
||||
settingDescription={backupSettings.backupPath.length ? backupSettings.backupPath : t`Default`}
|
||||
handleChange={(path) => updateSetting('backupPath', path)}
|
||||
/>
|
||||
<ListItemButton
|
||||
onClick={async () => {
|
||||
try {
|
||||
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
|
||||
title: t('settings.backup.automated.flags.title'),
|
||||
title: t`Backup data`,
|
||||
flags: convertToBackupFlags(backupSettings),
|
||||
});
|
||||
|
||||
@@ -298,54 +294,46 @@ export function Backup() {
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.backup.automated.flags.title')}
|
||||
primary={t`Backup data`}
|
||||
secondary={
|
||||
<>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.include', {
|
||||
includedCategoriesText: getAutoBackupFlagsInfo(backupSettings).true,
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.exclude', {
|
||||
excludedCategoriesText: getAutoBackupFlagsInfo(backupSettings).false,
|
||||
})}
|
||||
</span>
|
||||
<span>{t`Include: ${includedCategoriesText}`}</span>
|
||||
<span>{t`Exclude: ${excludedCategoriesText}`}</span>
|
||||
</>
|
||||
}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<TimeSetting
|
||||
settingName={t('settings.backup.automated.label.time')}
|
||||
settingName={t`Backup time`}
|
||||
value={backupSettings.backupTime}
|
||||
defaultValue="00:00"
|
||||
handleChange={(time: string) => updateSetting('backupTime', time)}
|
||||
/>
|
||||
<NumberSetting
|
||||
settingTitle={t('settings.backup.automated.label.interval')}
|
||||
settingValue={t('global.date.value.label.day', {
|
||||
days: backupSettings.backupInterval,
|
||||
count: backupSettings.backupInterval,
|
||||
settingTitle={t`Backup interval`}
|
||||
settingValue={plural(backupSettings.backupInterval, {
|
||||
one: '# day',
|
||||
other: '# days',
|
||||
})}
|
||||
value={backupSettings.backupInterval}
|
||||
defaultValue={1}
|
||||
minValue={1}
|
||||
maxValue={31}
|
||||
stepSize={1}
|
||||
valueUnit={t('global.date.label.day_one')}
|
||||
valueUnit={t`Day`}
|
||||
showSlider
|
||||
handleUpdate={(interval: number) => updateSetting('backupInterval', interval)}
|
||||
/>
|
||||
<NumberSetting
|
||||
settingTitle={t('settings.backup.automated.cleanup.label.title')}
|
||||
settingTitle={t`Backup cleanup`}
|
||||
settingValue={getBackupCleanupDisplayValue(backupSettings.backupTTL)}
|
||||
value={backupSettings.backupTTL}
|
||||
defaultValue={14}
|
||||
minValue={0}
|
||||
maxValue={1000}
|
||||
stepSize={1}
|
||||
valueUnit={t('global.date.label.day_one')}
|
||||
valueUnit={t`Day`}
|
||||
showSlider
|
||||
handleUpdate={(ttl: number) => updateSetting('backupTTL', ttl)}
|
||||
/>
|
||||
|
||||
@@ -14,9 +14,9 @@ import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useWindowEvent } from '@mantine/hooks';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
ExtensionState,
|
||||
TExtension,
|
||||
} from '@/features/extension/Extensions.types.ts';
|
||||
import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts';
|
||||
import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP } from '@/features/extension/Extensions.constants.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import {
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
import { MetadataBrowseSettings } from '@/features/browse/Browse.types.ts';
|
||||
import { useAppAction } from '@/features/navigation-bar/hooks/useAppAction.ts';
|
||||
import { SearchParam } from '@/base/Base.types.ts';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const LANGUAGE = 0;
|
||||
const EXTENSIONS = 1;
|
||||
@@ -73,7 +74,7 @@ const GroupHeader = ({
|
||||
setUpdatingExtensionIds: (ids: TExtension['pkgName'][]) => void;
|
||||
handleExtensionUpdate: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<StyledGroupHeader
|
||||
@@ -96,8 +97,10 @@ const GroupHeader = ({
|
||||
.response.then(() => handleExtensionUpdate())
|
||||
.catch((e) =>
|
||||
makeToast(
|
||||
t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[ExtensionAction.UPDATE], {
|
||||
count: groupExtensionIds.length,
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({
|
||||
...EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP[ExtensionAction.UPDATE],
|
||||
values: { count: groupExtensionIds.length },
|
||||
}),
|
||||
'error',
|
||||
getErrorMessage(e),
|
||||
@@ -106,7 +109,7 @@ const GroupHeader = ({
|
||||
.finally(() => setUpdatingExtensionIds([]));
|
||||
}}
|
||||
>
|
||||
{t('extension.action.label.update_all')}
|
||||
{t`Update all`}
|
||||
</Button>
|
||||
)}
|
||||
</StyledGroupHeader>
|
||||
@@ -114,7 +117,7 @@ const GroupHeader = ({
|
||||
};
|
||||
|
||||
export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const {
|
||||
data: serverSettingsData,
|
||||
@@ -130,7 +133,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
} = useMetadataServerSettings();
|
||||
const updateMetadataServerSettings = createUpdateMetadataServerSettings<
|
||||
keyof Pick<MetadataBrowseSettings, 'extensionLanguages'>
|
||||
>((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
|
||||
>((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
|
||||
|
||||
const [query] = useQueryParam(SearchParam.QUERY, StringParam);
|
||||
|
||||
@@ -170,18 +173,18 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
|
||||
const submitExternalExtension = (file: File) => {
|
||||
if (!file.name.toLowerCase().endsWith('apk')) {
|
||||
makeToast(t('global.error.label.invalid_file_type'), 'error');
|
||||
makeToast(t`Invalid filetype`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
makeToast(t('extension.label.installing_file'), 'info');
|
||||
makeToast(t`Installing extension file…`, 'info');
|
||||
requestManager
|
||||
.installExternalExtension(file)
|
||||
.response.then(() => {
|
||||
handleExtensionUpdate();
|
||||
makeToast(t('extension.label.installed_successfully'), 'success');
|
||||
makeToast(t`Extension installed`, 'success');
|
||||
})
|
||||
.catch((e) => makeToast(t('extension.label.installation_failed'), 'error', getErrorMessage(e)));
|
||||
.catch((e) => makeToast(t`Could not install the extension`, 'error', getErrorMessage(e)));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -191,7 +194,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
useAppAction(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
<CustomTooltip title={t('extension.action.label.install_external')}>
|
||||
<CustomTooltip title={t`Install external extension`}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
const input = document.createElement('input');
|
||||
@@ -241,7 +244,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => {
|
||||
if (serverSettingsError) {
|
||||
@@ -267,9 +270,9 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
paddingTop: '20px',
|
||||
}}
|
||||
>
|
||||
<Typography>{t('extension.label.add_repository_info')}</Typography>
|
||||
<Typography>{t`You have to add a extension repository to be able to install extensions`}</Typography>
|
||||
<Button component={Link} variant="contained" to={AppRoutes.settings.childRoutes.browse.path}>
|
||||
{t('settings.title')}
|
||||
{t`Settings`}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -10,10 +10,10 @@ import { useEffect, useState } from 'react';
|
||||
import Card from '@mui/material/Card';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import {
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import {
|
||||
EXTENSION_ACTION_TO_NEXT_ACTION_MAP,
|
||||
EXTENSION_ACTION_TO_STATE_MAP,
|
||||
INSTALLED_STATE_TO_TRANSLATION_KEY_MAP,
|
||||
INSTALLED_STATE_TO_TRANSLATION_MAP,
|
||||
} from '@/features/extension/Extensions.constants.ts';
|
||||
import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
@@ -45,7 +45,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export function ExtensionCard(props: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const {
|
||||
extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw, repo },
|
||||
@@ -134,7 +134,7 @@ export function ExtensionCard(props: IProps) {
|
||||
{showSourceRepo && <Typography variant="caption">{repo}</Typography>}
|
||||
</Stack>
|
||||
{isInstalled && (
|
||||
<CustomTooltip title={t('settings.title')}>
|
||||
<CustomTooltip title={t`Settings`}>
|
||||
<IconButton color="inherit" {...MUIUtil.preventRippleProp()}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
@@ -151,7 +151,7 @@ export function ExtensionCard(props: IProps) {
|
||||
handleButtonClick();
|
||||
}}
|
||||
>
|
||||
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
|
||||
{t(INSTALLED_STATE_TO_TRANSLATION_MAP[installedState])}
|
||||
</Button>
|
||||
</ListCardContent>
|
||||
</OptionalCardActionAreaLink>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Sources } from '@/features/browse/sources/Sources.tsx';
|
||||
import { Extensions } from '@/features/browse/extensions/Extensions.tsx';
|
||||
import { TabPanel } from '@/base/components/tabs/TabPanel.tsx';
|
||||
@@ -23,8 +23,8 @@ import { GROUPED_VIRTUOSO_Z_INDEX } from '@/lib/virtuoso/Virtuoso.constants.ts';
|
||||
import { SearchParam } from '@/base/Base.types.ts';
|
||||
|
||||
export function Browse() {
|
||||
const { t } = useTranslation();
|
||||
useAppTitle(t('global.label.browse'));
|
||||
const { t } = useLingui();
|
||||
useAppTitle(t`Browse`);
|
||||
|
||||
const tabsMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const [tabsMenuHeight, setTabsMenuHeight] = useState(0);
|
||||
@@ -49,9 +49,9 @@ export function Browse() {
|
||||
value={tabName}
|
||||
onChange={(_, newTab) => setTabSearchParam(newTab, 'replaceIn')}
|
||||
>
|
||||
<Tab value={BrowseTab.SOURCES} sx={{ textTransform: 'none' }} label={t('source.title_other')} />
|
||||
<Tab value={BrowseTab.EXTENSIONS} sx={{ textTransform: 'none' }} label={t('extension.title_other')} />
|
||||
<Tab value={BrowseTab.MIGRATE} sx={{ textTransform: 'none' }} label={t('migrate.title')} />
|
||||
<Tab value={BrowseTab.SOURCES} sx={{ textTransform: 'none' }} label={t`Source`} />
|
||||
<Tab value={BrowseTab.EXTENSIONS} sx={{ textTransform: 'none' }} label={t`Extension`} />
|
||||
<Tab value={BrowseTab.MIGRATE} sx={{ textTransform: 'none' }} label={t`Migrate`} />
|
||||
</TabsMenu>
|
||||
<TabPanel index={BrowseTab.SOURCE_DEPRECATED} currentIndex={tabName}>
|
||||
<Sources tabsMenuHeight={tabsMenuHeight} />
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
|
||||
import { MutableListSetting } from '@/base/components/settings/MutableListSetting.tsx';
|
||||
@@ -31,9 +32,9 @@ import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
|
||||
type ExtensionsSettings = Pick<GqlServerSettings, 'maxSourcesInParallel' | 'localSourcePath' | 'extensionRepos'>;
|
||||
|
||||
export const BrowseSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('global.label.browse'));
|
||||
useAppTitle(t`Browse`);
|
||||
|
||||
const { data, loading, error, refetch } = requestManager.useGetServerSettings({
|
||||
notifyOnNetworkStatusChange: true,
|
||||
@@ -45,7 +46,7 @@ export const BrowseSettings = () => {
|
||||
value: ExtensionsSettings[Setting],
|
||||
) => {
|
||||
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,7 +54,7 @@ export const BrowseSettings = () => {
|
||||
settings: { hideLibraryEntries, showNsfw },
|
||||
} = useMetadataServerSettings();
|
||||
const updateMetadataServerSettings = createUpdateMetadataServerSettings<keyof MetadataBrowseSettings>((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
@@ -63,7 +64,7 @@ export const BrowseSettings = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('BrowseSettings::refetch'))}
|
||||
/>
|
||||
@@ -75,7 +76,7 @@ export const BrowseSettings = () => {
|
||||
return (
|
||||
<List sx={{ pt: 0 }}>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('settings.label.hide_library_entries')} />
|
||||
<ListItemText primary={t`Hide entries already in library`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={hideLibraryEntries}
|
||||
@@ -83,10 +84,7 @@ export const BrowseSettings = () => {
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t('settings.label.show_nsfw')}
|
||||
secondary={t('settings.label.show_nsfw_description')}
|
||||
/>
|
||||
<ListItemText primary={t`Show NSFW`} secondary={t`Hide NSFW extensions and sources`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={showNsfw}
|
||||
@@ -94,12 +92,12 @@ export const BrowseSettings = () => {
|
||||
/>
|
||||
</ListItem>
|
||||
<NumberSetting
|
||||
settingTitle={t('settings.server.requests.sources.parallel.label.title')}
|
||||
settingValue={t('settings.server.requests.sources.parallel.label.value', {
|
||||
value: serverSettings.maxSourcesInParallel,
|
||||
count: serverSettings.maxSourcesInParallel,
|
||||
settingTitle={t`Parallel source requests`}
|
||||
settingValue={plural(serverSettings.maxSourcesInParallel, {
|
||||
one: '# Source',
|
||||
other: '# Sources',
|
||||
})}
|
||||
valueUnit={t('source.title_one')}
|
||||
valueUnit={t`Source`}
|
||||
value={serverSettings.maxSourcesInParallel}
|
||||
defaultValue={6}
|
||||
minValue={1}
|
||||
@@ -109,10 +107,10 @@ export const BrowseSettings = () => {
|
||||
handleUpdate={(parallelSources) => updateSetting('maxSourcesInParallel', parallelSources)}
|
||||
/>
|
||||
<MutableListSetting
|
||||
settingName={t('extension.settings.repositories.custom.label.title')}
|
||||
description={t('extension.settings.repositories.custom.label.description')}
|
||||
settingName={t`Extension repositories`}
|
||||
description={t`Add repositories from which extensions can be installed`}
|
||||
dialogDisclaimer={
|
||||
<Trans i18nKey="extension.settings.repositories.custom.label.disclaimer">
|
||||
<Trans>
|
||||
<strong>Suwayomi does not provide any support for 3rd party repositories or extensions!</strong>
|
||||
<br />
|
||||
Use with caution as there could be malicious actors making those repositories.
|
||||
@@ -125,22 +123,20 @@ export const BrowseSettings = () => {
|
||||
requestManager.clearExtensionCache();
|
||||
}}
|
||||
valueInfos={serverSettings.extensionRepos.map((extensionRepo) => [extensionRepo])}
|
||||
addItemButtonTitle={t('extension.settings.repositories.custom.dialog.action.button.add')}
|
||||
addItemButtonTitle={t`Add repository`}
|
||||
placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo"
|
||||
validateItem={(repo) =>
|
||||
!!repo.match(
|
||||
/https:\/\/(www\.|raw\.)?(github|githubusercontent)\.com\/([^/]+)\/([^/]+)((\/tree|\/blob)?\/([^/\n]*))?(\/([^/\n]*\.json)?)?/g,
|
||||
)
|
||||
}
|
||||
invalidItemError={t('extension.settings.repositories.custom.error.label.invalid_url')}
|
||||
invalidItemError={t`Invalid repository url`}
|
||||
/>
|
||||
<TextSetting
|
||||
settingName={t('settings.server.local_source.path.label.title')}
|
||||
dialogDescription={t('settings.server.local_source.path.label.description')}
|
||||
settingName={t`Local source location`}
|
||||
dialogDescription={t`The path to the directory on the server where local source files are saved in`}
|
||||
value={serverSettings.localSourcePath}
|
||||
settingDescription={
|
||||
serverSettings.localSourcePath.length ? serverSettings.localSourcePath : t('global.label.default')
|
||||
}
|
||||
settingDescription={serverSettings.localSourcePath.length ? serverSettings.localSourcePath : t`Default`}
|
||||
handleChange={(path) => updateSetting('localSourcePath', path)}
|
||||
/>
|
||||
</List>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { useCallback, useMemo } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { DefaultLanguage } from '@/base/utils/Languages.ts';
|
||||
@@ -32,7 +32,7 @@ import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupIt
|
||||
import { SourceLanguageSelect } from '@/features/source/components/SourceLanguageSelect.tsx';
|
||||
|
||||
export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { languages: shownLangs, setLanguages: setShownLangs } = SourceService.useLanguages();
|
||||
const {
|
||||
@@ -97,7 +97,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
|
||||
useAppAction(
|
||||
<>
|
||||
<CustomTooltip title={t('search.title.global_search')}>
|
||||
<CustomTooltip title={t`Global Search`}>
|
||||
<IconButton onClick={() => navigate(AppRoutes.sources.childRoutes.searchAll.path())} color="inherit">
|
||||
<TravelExploreIcon />
|
||||
</IconButton>
|
||||
@@ -117,7 +117,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('Sources::refetch'))}
|
||||
/>
|
||||
@@ -125,7 +125,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||
}
|
||||
|
||||
if (sources?.length === 0) {
|
||||
return <EmptyViewAbsoluteCentered message={t('source.error.label.no_sources_found')} />;
|
||||
return <EmptyViewAbsoluteCentered message={t`No sources found. Install Some extensions first.`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,12 +11,12 @@ import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { SourceContentType } from '@/features/source/browse/screens/SourceMangas.tsx';
|
||||
import { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -38,7 +38,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const SourceCard: React.FC<IProps> = (props: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { source, showSourceRepo, showLanguage } = props;
|
||||
const {
|
||||
@@ -53,10 +53,10 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
const { isPinned } = useGetSourceMetadata(source);
|
||||
|
||||
const sourceName = Sources.isLocalSource(source) ? t('source.local_source.title') : name;
|
||||
const sourceName = Sources.isLocalSource(source) ? t`Local source` : name;
|
||||
|
||||
const updateSetting = createUpdateSourceMetadata(source, (e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -105,10 +105,10 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
|
||||
to={AppRoutes.sources.childRoutes.browse.path(id)}
|
||||
state={{ contentType: SourceContentType.LATEST, clearCache: true }}
|
||||
>
|
||||
{t('global.button.latest')}
|
||||
{t`Latest`}
|
||||
</Button>
|
||||
)}
|
||||
<CustomTooltip title={t(isPinned ? 'source.pin.remove' : 'source.pin.add')}>
|
||||
<CustomTooltip title={isPinned ? t`Unpin source` : t`Pin source`}>
|
||||
<IconButton
|
||||
{...MUIUtil.preventRippleProp()}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
@@ -16,7 +15,8 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { t as translate } from 'i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { t as translate } from '@lingui/core/macro';
|
||||
import { ThreeStateCheckboxInput } from '@/base/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { IncludeOrExclude } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -68,11 +68,11 @@ const getCategoryUpdateInfo = (
|
||||
const noSpecificallyIncludedCategories = areIncluded && !categories.length && unsetCategories;
|
||||
const includesAllCategories = categories.length === allCategories;
|
||||
if (noSpecificallyIncludedCategories || includesAllCategories) {
|
||||
return translate('extension.language.all');
|
||||
return translate`All`;
|
||||
}
|
||||
|
||||
if (!categories.length) {
|
||||
return translate('global.label.none');
|
||||
return translate`None`;
|
||||
}
|
||||
|
||||
return categories.map((category) => category.name).join(', ');
|
||||
@@ -91,7 +91,7 @@ export const CategoriesInclusionSetting = ({
|
||||
includeField,
|
||||
dialogText,
|
||||
}: CategoriesInclusionSettingProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [dialogCategories, setDialogCategories] = useState(categories);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
@@ -141,7 +141,7 @@ export const CategoriesInclusionSetting = ({
|
||||
// TODO - update cache immediately
|
||||
// mutate(categoriesEndpoint, [...dialogCategories], { revalidate: false });
|
||||
} catch (e) {
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e));
|
||||
// mutate(categoriesEndpoint, [...categories]);
|
||||
}
|
||||
};
|
||||
@@ -155,27 +155,18 @@ export const CategoriesInclusionSetting = ({
|
||||
<>
|
||||
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
||||
<ListItemText
|
||||
primary={t('category.title.category_other')}
|
||||
primary={t`Category`}
|
||||
secondary={
|
||||
<>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.include', {
|
||||
includedCategoriesText,
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t('category.settings.inclusion.label.exclude', {
|
||||
excludedCategoriesText,
|
||||
})}
|
||||
</span>
|
||||
<span>{t`Include: ${includedCategoriesText}`}</span>
|
||||
<span>{t`Exclude: ${excludedCategoriesText}`}</span>
|
||||
</>
|
||||
}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
||||
<DialogTitle>{t('category.title.category_other')}</DialogTitle>
|
||||
<DialogTitle>{t`Category`}</DialogTitle>
|
||||
<DialogContent>
|
||||
{dialogText && <DialogContentText sx={{ paddingBottom: '10px' }}>{dialogText}</DialogContentText>}
|
||||
<CheckboxContainer>
|
||||
@@ -207,10 +198,10 @@ export const CategoriesInclusionSetting = ({
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeDialog} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button onClick={updateCategories} color="primary">
|
||||
{t('global.button.ok')}
|
||||
{t`Ok`}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -13,10 +13,10 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { AwaitableComponentProps } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts';
|
||||
@@ -90,7 +90,7 @@ const getCategoryCheckedState = (
|
||||
};
|
||||
|
||||
export function CategorySelect(props: CategorySelectProps) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const {
|
||||
onDismiss,
|
||||
@@ -161,7 +161,7 @@ export function CategorySelect(props: CategorySelectProps) {
|
||||
|
||||
if (doNotShowAddToLibraryDialogAgain) {
|
||||
updateMetadataServerSettings('showAddToLibraryCategorySelectDialog', false).catch((e) =>
|
||||
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,10 +196,10 @@ export function CategorySelect(props: CategorySelectProps) {
|
||||
onTransitionExited={onExitComplete}
|
||||
onClose={handleCancel}
|
||||
>
|
||||
<DialogTitle>{t('category.title.set_categories')}</DialogTitle>
|
||||
<DialogTitle>{t`Set categories`}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
{allCategories.length === 0 && <span>{t('category.error.no_categories_found.label.info')}</span>}
|
||||
{allCategories.length === 0 && <span>{t`You don't have any categories yet.`}</span>}
|
||||
{allCategories.map((category) => (
|
||||
<ThreeStateCheckboxInput
|
||||
checked={getCategoryCheckedState(
|
||||
@@ -232,7 +232,7 @@ export function CategorySelect(props: CategorySelectProps) {
|
||||
<CheckboxInput
|
||||
sx={{ margin: 0 }}
|
||||
size="small"
|
||||
label={t('global.button.dont_show_dialog_again')}
|
||||
label={t`Don't show this dialog again`}
|
||||
onChange={(e) => setDoNotShowAddToLibraryDialogAgain(e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
@@ -249,15 +249,15 @@ export function CategorySelect(props: CategorySelectProps) {
|
||||
to={AppRoutes.settings.childRoutes.categories.path}
|
||||
onClick={onDismiss}
|
||||
>
|
||||
{t(allCategories.length ? 'global.button.edit' : 'global.button.create')}
|
||||
{allCategories.length ? t`Edit` : t`Create`}
|
||||
</Button>
|
||||
<Stack direction="row">
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
{!!allCategories.length && (
|
||||
<Button onClick={handleOk} color="primary">
|
||||
{t('global.button.ok')}
|
||||
{t`Ok`}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -10,11 +10,11 @@ import IconButton from '@mui/material/IconButton';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { CategoryType } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -27,7 +27,7 @@ export const CategorySettingsCard = ({
|
||||
category: Pick<CategoryType, 'id' | 'name'>;
|
||||
onEdit: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const deleteCategory = () => {
|
||||
requestManager.deleteCategory(category.id);
|
||||
@@ -42,12 +42,12 @@ export const CategorySettingsCard = ({
|
||||
{category.name}
|
||||
</Typography>
|
||||
<Stack sx={{ flexDirection: 'row' }}>
|
||||
<CustomTooltip title={t('global.button.edit')}>
|
||||
<CustomTooltip title={t`Edit`}>
|
||||
<IconButton component={Box} onClick={onEdit}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('chapter.action.download.delete.label.action')}>
|
||||
<CustomTooltip title={t`Delete`}>
|
||||
<IconButton component={Box} onClick={deleteCategory}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -15,7 +15,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { CategoryDefaultInfo, CategoryIdInfo, CategoryNameInfo } from '@/features/category/Category.types.ts';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
@@ -31,7 +31,7 @@ export const CreateOrEditCategoryDialog = ({
|
||||
}) => {
|
||||
const isEditMode = !!category;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [dialogName, setDialogName] = useState(category?.name);
|
||||
const [dialogDefault, setDialogDefault] = useState(!!category?.default);
|
||||
@@ -47,47 +47,43 @@ export const CreateOrEditCategoryDialog = ({
|
||||
if (isEditMode) {
|
||||
requestManager
|
||||
.updateCategory(category.id, { name: dialogName, default: dialogDefault })
|
||||
.response.catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
);
|
||||
.response.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
requestManager
|
||||
.createCategory({ name: dialogName, default: dialogDefault })
|
||||
.response.catch((e) => makeToast(t('category.error.label.create_failure'), 'error', getErrorMessage(e)));
|
||||
.response.catch((e) => makeToast(t`Could not create category`, 'error', getErrorMessage(e)));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose}>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{isEditMode ? t('category.dialog.title.edit_category_one') : t('category.dialog.title.new_category')}
|
||||
</DialogTitle>
|
||||
<DialogTitle id="form-dialog-title">{isEditMode ? t`Edit category` : t`New category`}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
label={t('category.label.category_name')}
|
||||
label={t`Category Name`}
|
||||
type="text"
|
||||
fullWidth
|
||||
value={dialogName}
|
||||
onChange={(e) => setDialogName(e.target.value.trim())}
|
||||
error={isInvalidName}
|
||||
helperText={isInvalidName ? t`global.error.label.invalid_input` : undefined}
|
||||
helperText={isInvalidName ? t`Invalid input` : undefined}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={dialogDefault} onChange={(e) => setDialogDefault(e.target.checked)} />}
|
||||
label={t('category.label.use_as_default_category')}
|
||||
label={t`Default category when adding new manga to the library`}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button onClick={handleDialogSubmit} color="primary" disabled={!canSubmit}>
|
||||
{t('global.button.submit')}
|
||||
{t`Submit`}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import { ComponentProps, useMemo, useState } from 'react';
|
||||
import Fab from '@mui/material/Fab';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
@@ -31,10 +31,10 @@ import { CREATE_NEW_CATEGORY_ID } from '@/features/category/Category.constants.t
|
||||
import { CreateOrEditCategoryDialog } from '@/features/category/components/CreateOrEditCategoryDialog.tsx';
|
||||
|
||||
export function CategorySettings() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const dndSensors = DndKitUtil.useSensorsForDevice();
|
||||
|
||||
useAppTitle(t('category.dialog.title.edit_category_other'));
|
||||
useAppTitle(t`Edit categories`);
|
||||
|
||||
const { data, loading, error, refetch } = requestManager.useGetCategories<
|
||||
GetCategoriesSettingsQuery,
|
||||
@@ -96,7 +96,7 @@ export function CategorySettings() {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('category.error.label.request_failure')}
|
||||
message={t`Could not load categories`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('CategorySettings::refetch'))}
|
||||
/>
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { ChapterAction, ChapterListOptions, ChapterSortMode } from '@/features/chapter/Chapter.types.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
export const FALLBACK_CHAPTER = { id: -1, name: '', realUrl: '', isDownloaded: false, isBookmarked: false };
|
||||
|
||||
@@ -21,11 +22,11 @@ export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = {
|
||||
excludedScanlators: [],
|
||||
};
|
||||
|
||||
export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY: Record<ChapterSortMode, TranslationKey> = {
|
||||
source: 'global.sort.label.by_source',
|
||||
chapterNumber: 'global.sort.label.by_chapter_number',
|
||||
uploadedAt: 'global.sort.label.by_upload_date',
|
||||
fetchedAt: 'global.sort.label.by_fetch_date',
|
||||
export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION: Record<ChapterSortMode, MessageDescriptor> = {
|
||||
source: msg`By source`,
|
||||
chapterNumber: msg`By chapter number`,
|
||||
uploadedAt: msg`By upload date`,
|
||||
fetchedAt: msg`By date fetched`,
|
||||
};
|
||||
|
||||
export const CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED: Record<
|
||||
@@ -43,65 +44,65 @@ export const CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED: Record<
|
||||
export const CHAPTER_ACTION_TO_TRANSLATION: {
|
||||
[key in ChapterAction]: {
|
||||
action: {
|
||||
single: TranslationKey;
|
||||
selected: TranslationKey;
|
||||
single: MessageDescriptor;
|
||||
selected: MessageDescriptor;
|
||||
};
|
||||
confirmation?: TranslationKey;
|
||||
success: TranslationKey;
|
||||
error: TranslationKey;
|
||||
confirmation?: MessageDescriptor;
|
||||
success: MessageDescriptor;
|
||||
error: MessageDescriptor;
|
||||
};
|
||||
} = {
|
||||
download: {
|
||||
action: {
|
||||
single: 'chapter.action.download.add.label.action',
|
||||
selected: 'chapter.action.download.add.button.selected',
|
||||
single: msg`Download`,
|
||||
selected: msg`Download selected`,
|
||||
},
|
||||
confirmation: 'chapter.action.download.add.label.confirmation',
|
||||
success: 'chapter.action.download.add.label.success',
|
||||
error: 'chapter.action.download.add.label.error',
|
||||
confirmation: msg`{count, plural, one {You are about to download one chapter} other {You are about to download # chapters.\nSuwayomi is not a mass downloader and too many downloads can get you banned from sources and/or cause performance issues.}}`,
|
||||
success: msg`{count, plural, one {Download added} other {# downloads added}}`,
|
||||
error: msg`{count, plural, one {Could not add the download} other {Could not add downloads}}`,
|
||||
},
|
||||
delete: {
|
||||
action: {
|
||||
single: 'chapter.action.download.delete.label.action',
|
||||
selected: 'chapter.action.download.delete.button.selected',
|
||||
single: msg`Delete`,
|
||||
selected: msg`Delete selected`,
|
||||
},
|
||||
confirmation: 'chapter.action.download.delete.label.confirmation',
|
||||
success: 'chapter.action.download.delete.label.success',
|
||||
error: 'chapter.action.download.delete.label.error',
|
||||
confirmation: msg`{count, plural, one {You are about to delete one download} other {You are about to delete # downloads}}`,
|
||||
success: msg`{count, plural, one {Chapter deleted} other {# chapters deleted}}`,
|
||||
error: msg`{count, plural, one {Could not delete the chapter} other {Could not delete chapters}}`,
|
||||
},
|
||||
bookmark: {
|
||||
action: {
|
||||
single: 'chapter.action.bookmark.add.label.action',
|
||||
selected: 'chapter.action.bookmark.add.button.selected',
|
||||
single: msg`Add bookmark`,
|
||||
selected: msg`Bookmark selected`,
|
||||
},
|
||||
success: 'chapter.action.bookmark.add.label.success',
|
||||
error: 'chapter.action.bookmark.add.label.error',
|
||||
success: msg`{count, plural, one {Chapter bookmarked} other {# chapters bookmarked}}`,
|
||||
error: msg`{count, plural, one {Could not bookmark the chapter} other {Could not bookmark chapters}}`,
|
||||
},
|
||||
unbookmark: {
|
||||
action: {
|
||||
single: 'chapter.action.bookmark.remove.label.action',
|
||||
selected: 'chapter.action.bookmark.remove.button.selected',
|
||||
single: msg`Remove bookmark`,
|
||||
selected: msg`Remove bookmarks from selected`,
|
||||
},
|
||||
confirmation: 'chapter.action.bookmark.remove.label.confirmation',
|
||||
success: 'chapter.action.bookmark.remove.label.success',
|
||||
error: 'chapter.action.bookmark.remove.label.error',
|
||||
confirmation: msg`{count, plural, one {You are about to remove one bookmark} other {You are about to remove # bookmarks}}`,
|
||||
success: msg`{count, plural, one {Chapter bookmark removed} other {# chapter bookmarks removed}}`,
|
||||
error: msg`{count, plural, one {Could not remove the bookmark} other {Could not remove the bookmarks}}`,
|
||||
},
|
||||
mark_as_read: {
|
||||
action: {
|
||||
single: 'chapter.action.mark_as_read.add.label.action.current',
|
||||
selected: 'chapter.action.mark_as_read.add.button.selected',
|
||||
single: msg`Mark as read`,
|
||||
selected: msg`Mark selected as read`,
|
||||
},
|
||||
confirmation: 'chapter.action.mark_as_read.add.label.confirmation',
|
||||
success: 'chapter.action.mark_as_read.add.label.success',
|
||||
error: 'chapter.action.mark_as_read.add.label.error',
|
||||
confirmation: msg`{count, plural, one {You are about to mark one chapter as read} other {You are about to mark # chapters as read}}`,
|
||||
success: msg`{count, plural, one {Chapter marked as read} other {# chapters marked as read}}`,
|
||||
error: msg`{count, plural, one {Could not mark the chapter as read} other {Could not mark chapters as read}}`,
|
||||
},
|
||||
mark_as_unread: {
|
||||
action: {
|
||||
single: 'chapter.action.mark_as_read.remove.label.action',
|
||||
selected: 'chapter.action.mark_as_read.remove.button.selected',
|
||||
single: msg`Mark as unread`,
|
||||
selected: msg`Mark selected as unread`,
|
||||
},
|
||||
confirmation: 'chapter.action.mark_as_read.remove.label.confirmation',
|
||||
success: 'chapter.action.mark_as_read.remove.label.success',
|
||||
error: 'chapter.action.mark_as_read.remove.label.error',
|
||||
confirmation: msg`{count, plural, one {You are about to mark one chapter as unread} other {You are about to mark # chapters as unread}}`,
|
||||
success: msg`{count, plural, one {Chapter marked as unread} other {# chapters marked as unread}}`,
|
||||
error: msg`{count, plural, one {Could not mark the chapter as unread} other {Could not mark chapters as unread}}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import { bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined';
|
||||
import DisabledByDefaultRounded from '@mui/icons-material/DisabledByDefaultRounded';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CheckboxListSetting } from '@/base/components/settings/CheckboxListSetting.tsx';
|
||||
import { updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
@@ -23,7 +23,7 @@ export const ChapterExcludeSanlatorsFilter = ({
|
||||
scanlators: string[];
|
||||
excludedScanlators: string[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const popupState = usePopupState({ variant: 'dialog', popupId: 'chapter-list-options-scanlator-filter-dialog' });
|
||||
|
||||
if (!scanlators.length) {
|
||||
@@ -34,13 +34,13 @@ export const ChapterExcludeSanlatorsFilter = ({
|
||||
<>
|
||||
<CheckboxInput
|
||||
{...bindTrigger(popupState)}
|
||||
label={t('global.label.scanlator')}
|
||||
label={t`Scanlator`}
|
||||
icon={<PeopleAltOutlinedIcon />}
|
||||
checkedIcon={<PeopleAltOutlinedIcon color="warning" />}
|
||||
checked={!!excludedScanlators.length}
|
||||
/>
|
||||
<CheckboxListSetting
|
||||
title={t('chapter.option.exclude_scanlators')}
|
||||
title={t`Exclude scanlators`}
|
||||
open={popupState.isOpen}
|
||||
onClose={(selectedScanlators) => {
|
||||
if (selectedScanlators) {
|
||||
|
||||
@@ -11,7 +11,8 @@ import Stack from '@mui/material/Stack';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ComponentProps, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ResumeFab } from '@/features/manga/components/ResumeFAB.tsx';
|
||||
import {
|
||||
@@ -82,7 +83,7 @@ const ChapterListFAB = ({
|
||||
}) => {
|
||||
if (selectedChapters.length) {
|
||||
return (
|
||||
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
|
||||
<SelectionFAB title={plural(selectedChapters.length, { one: '# chapter', other: '# chapters' })}>
|
||||
{(handleClose) => (
|
||||
<ChapterActionMenuItems
|
||||
selectedChapters={selectedChapters}
|
||||
@@ -110,7 +111,7 @@ export const ChapterList = ({
|
||||
manga: Pick<MangaScreenFieldsFragment, 'id' | 'firstUnreadChapter' | 'chapters' | 'unreadCount' | 'downloadCount'>;
|
||||
isRefreshing: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { appBarHeight } = useNavBarContext();
|
||||
|
||||
const isMobileWidth = MediaQuery.useIsBelowWidth('md');
|
||||
@@ -126,7 +127,7 @@ export const ChapterList = ({
|
||||
|
||||
const options = useChapterListOptions(manga);
|
||||
const updateOption = updateChapterListOptions(manga, (e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
const {
|
||||
data: chaptersData,
|
||||
@@ -173,7 +174,7 @@ export const ChapterList = ({
|
||||
return (
|
||||
<Stack sx={{ justifyContent: 'center', position: 'relative', flexGrow: 1 }}>
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('ChapterList::refetch'))}
|
||||
/>
|
||||
@@ -193,13 +194,17 @@ export const ChapterList = ({
|
||||
>
|
||||
<Stack>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t('chapter.value', { count: visibleChapters.length })}
|
||||
{plural(visibleChapters.length, {
|
||||
one: '# chapter',
|
||||
other: '# chapters',
|
||||
})}
|
||||
</Typography>
|
||||
{!!missingChapterCount && (
|
||||
<Typography variant="body2" color="warning">
|
||||
{`${t('chapter.missing', {
|
||||
count: missingChapterCount,
|
||||
})}`}
|
||||
{plural(missingChapterCount, {
|
||||
one: 'Missing # chapter',
|
||||
other: 'Missing # chapters',
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -225,10 +230,8 @@ export const ChapterList = ({
|
||||
</Stack>
|
||||
</ChapterListHeader>
|
||||
|
||||
{noChaptersFound && <EmptyViewAbsoluteCentered message={t('chapter.error.label.no_chapter_found')} />}
|
||||
{noChaptersMatchingFilter && (
|
||||
<EmptyViewAbsoluteCentered message={t('chapter.error.label.no_matches')} />
|
||||
)}
|
||||
{noChaptersFound && <EmptyViewAbsoluteCentered message={t`No chapters found`} />}
|
||||
{noChaptersMatchingFilter && <EmptyViewAbsoluteCentered message={t`No chapters matching filter`} />}
|
||||
|
||||
<StyledVirtuoso
|
||||
persistKey={`manga-${manga.id}-chapter-list`}
|
||||
|
||||
@@ -6,18 +6,19 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { RadioInput } from '@/base/components/inputs/RadioInput.tsx';
|
||||
import { SortRadioInput } from '@/base/components/inputs/SortRadioInput.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/base/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { OptionsTabs } from '@/base/components/modals/OptionsTabs.tsx';
|
||||
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY } from '@/features/chapter/Chapter.constants.ts';
|
||||
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
|
||||
import { ChapterListOptions } from '@/features/chapter/Chapter.types.ts';
|
||||
import { updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
|
||||
import { ChapterExcludeSanlatorsFilter } from '@/features/chapter/components/ChapterExcludeSanlatorsFilter.tsx';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
@@ -28,10 +29,10 @@ interface IProps {
|
||||
excludedScanlators: string[];
|
||||
}
|
||||
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
sort: 'global.label.sort',
|
||||
display: 'global.label.display',
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = {
|
||||
filter: msg`Filter`,
|
||||
sort: msg`Sort`,
|
||||
display: msg`Display`,
|
||||
};
|
||||
|
||||
export const ChapterOptions: React.FC<IProps> = ({
|
||||
@@ -42,7 +43,7 @@ export const ChapterOptions: React.FC<IProps> = ({
|
||||
scanlators,
|
||||
excludedScanlators,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||
@@ -56,17 +57,17 @@ export const ChapterOptions: React.FC<IProps> = ({
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.unread')}
|
||||
label={t`Unread`}
|
||||
checked={options.unread}
|
||||
onChange={(c) => updateOption('unread', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.downloaded')}
|
||||
label={t`Downloaded`}
|
||||
checked={options.downloaded}
|
||||
onChange={(c) => updateOption('downloaded', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.bookmarked')}
|
||||
label={t`Bookmarked`}
|
||||
checked={options.bookmarked}
|
||||
onChange={(c) => updateOption('bookmarked', c)}
|
||||
/>
|
||||
@@ -79,7 +80,7 @@ export const ChapterOptions: React.FC<IProps> = ({
|
||||
);
|
||||
}
|
||||
if (key === 'sort') {
|
||||
return Object.entries(CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY).map(([mode, label]) => (
|
||||
return Object.entries(CHAPTER_SORT_OPTIONS_TO_TRANSLATION).map(([mode, label]) => (
|
||||
<SortRadioInput
|
||||
key={mode}
|
||||
label={t(label)}
|
||||
@@ -87,10 +88,7 @@ export const ChapterOptions: React.FC<IProps> = ({
|
||||
sortDescending={options.reverse}
|
||||
onClick={() =>
|
||||
mode !== options.sortBy
|
||||
? updateOption(
|
||||
'sortBy',
|
||||
mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY,
|
||||
)
|
||||
? updateOption('sortBy', mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION)
|
||||
: updateOption('reverse', !options.reverse)
|
||||
}
|
||||
/>
|
||||
@@ -102,8 +100,8 @@ export const ChapterOptions: React.FC<IProps> = ({
|
||||
onChange={() => updateOption('showChapterNumber', !options.showChapterNumber)}
|
||||
value={options.showChapterNumber}
|
||||
>
|
||||
<RadioInput label={t('chapter.option.display.label.source_title')} value={false} />
|
||||
<RadioInput label={t('chapter.option.display.label.chapter_number')} value />
|
||||
<RadioInput label={t`Source title`} value={false} />
|
||||
<RadioInput label={t`Chapter number`} value />
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
import FilterList from '@mui/icons-material/FilterList';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import DoneAllIcon from '@mui/icons-material/DoneAll';
|
||||
import { useMemo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { ChapterOptions } from '@/features/chapter/components/ChapterOptions.tsx';
|
||||
import { isFilterActive, updateChapterListOptions } from '@/features/chapter/utils/ChapterList.util.tsx';
|
||||
@@ -45,7 +45,7 @@ export const ChaptersToolbarMenu = ({
|
||||
scanlators,
|
||||
excludeScanlators,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const isFiltered = isFilterActive(options);
|
||||
@@ -55,7 +55,7 @@ export const ChaptersToolbarMenu = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomTooltip title={t('chapter.action.mark_as_read.add.label.action.all')} disabled={areAllChaptersRead}>
|
||||
<CustomTooltip title={t`Mark all as read`} disabled={areAllChaptersRead}>
|
||||
<IconButton
|
||||
disabled={areAllChaptersRead}
|
||||
onClick={() => Chapters.markAsRead(Chapters.getNonRead(chapters), true, mangaId)}
|
||||
@@ -67,7 +67,7 @@ export const ChaptersToolbarMenu = ({
|
||||
<PopupState variant="popover" popupId="chapterlist-download-button">
|
||||
{(popupState) => (
|
||||
<>
|
||||
<CustomTooltip title={t('global.button.download')} disabled={areAllChaptersRead}>
|
||||
<CustomTooltip title={t`Download`} disabled={areAllChaptersRead}>
|
||||
<IconButton
|
||||
disabled={areAllChaptersDownloaded}
|
||||
{...bindTrigger(popupState)}
|
||||
@@ -84,7 +84,7 @@ export const ChaptersToolbarMenu = ({
|
||||
</>
|
||||
)}
|
||||
</PopupState>
|
||||
<CustomTooltip title={t('chapter.action.filter_and_sort.label')}>
|
||||
<CustomTooltip title={t`Filter and sort`}>
|
||||
<IconButton onClick={() => setOpen(true)} color="inherit">
|
||||
<FilterList color={isFiltered ? 'warning' : undefined} />
|
||||
</IconButton>
|
||||
|
||||
@@ -17,40 +17,43 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
|
||||
export const MissingChaptersInfoSeparator = ({ missingChaptersGap }: { missingChaptersGap: number }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
export const MissingChaptersInfoSeparator = ({
|
||||
missingChaptersGap: missingChapterCount,
|
||||
}: {
|
||||
missingChaptersGap: number;
|
||||
}) => (
|
||||
<Stack
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 2,
|
||||
pt: 3.5,
|
||||
pb: 2.5,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 2,
|
||||
pt: 3.5,
|
||||
pb: 2.5,
|
||||
flexGrow: 1,
|
||||
border: '1px solid',
|
||||
borderColor: (theme) => theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
border: '1px solid',
|
||||
borderColor: (theme) => theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
<Typography sx={{ px: 2 }} variant="body2" color="textSecondary">
|
||||
{t('chapter.missing', { count: missingChaptersGap })}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
border: '1px solid',
|
||||
borderColor: (theme) => theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<Typography sx={{ px: 2 }} variant="body2" color="textSecondary">
|
||||
{plural(missingChapterCount, {
|
||||
one: 'Missing # chapter',
|
||||
other: 'Missing # chapters',
|
||||
})}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
border: '1px solid',
|
||||
borderColor: (theme) => theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -11,11 +11,11 @@ import Delete from '@mui/icons-material/Delete';
|
||||
import Download from '@mui/icons-material/Download';
|
||||
import RemoveDone from '@mui/icons-material/RemoveDone';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
|
||||
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
||||
import DoneAll from '@mui/icons-material/DoneAll';
|
||||
import { ComponentProps, useMemo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { MenuItem } from '@/base/components/menu/MenuItem.tsx';
|
||||
@@ -74,7 +74,7 @@ export const ChapterActionMenuItems = ({
|
||||
onClose,
|
||||
selectable = true,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const isSingleMode = !!chapter;
|
||||
const { isDownloaded, isRead, isBookmarked } = chapter ?? {};
|
||||
@@ -173,7 +173,7 @@ export const ChapterActionMenuItems = ({
|
||||
return (
|
||||
<>
|
||||
{isSingleMode && selectable && (
|
||||
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
|
||||
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t`Select`} />
|
||||
)}
|
||||
{isSingleMode && (
|
||||
<>
|
||||
@@ -184,7 +184,7 @@ export const ChapterActionMenuItems = ({
|
||||
window.open(chapter!.realUrl!, '_blank', 'noopener,noreferrer');
|
||||
onClose();
|
||||
}}
|
||||
title={t('global.button.open_browser')}
|
||||
title={t`Open in browser`}
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={IconWebView}
|
||||
@@ -197,7 +197,7 @@ export const ChapterActionMenuItems = ({
|
||||
);
|
||||
onClose();
|
||||
}}
|
||||
title={t('global.button.open_webview')}
|
||||
title={t`Open in WebView`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -255,7 +255,7 @@ export const ChapterActionMenuItems = ({
|
||||
<MenuItem
|
||||
onClick={() => performAction('mark_prev_as_read', [])}
|
||||
Icon={DoneAll}
|
||||
title={t('chapter.action.mark_as_read.add.label.action.previous')}
|
||||
title={t`Mark previous as read`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import gql from 'graphql-tag';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
@@ -28,26 +29,46 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const DOWNLOAD_OPTIONS: {
|
||||
title: TranslationKey;
|
||||
title: MessageDescriptor;
|
||||
getCount: (downloadAheadLimit: number) => number | undefined;
|
||||
onlyUnread?: boolean;
|
||||
isDownloadAhead?: boolean;
|
||||
}[] = [
|
||||
{ title: 'chapter.action.download.add.label.next', getCount: () => 1 },
|
||||
{ title: 'chapter.action.download.add.label.next', getCount: () => 5 },
|
||||
{ title: 'chapter.action.download.add.label.next', getCount: () => 10 },
|
||||
{ title: 'chapter.action.download.add.label.next', getCount: () => 25 },
|
||||
{
|
||||
title: 'chapter.action.download.add.label.ahead',
|
||||
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
|
||||
getCount: () => 1,
|
||||
},
|
||||
{
|
||||
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
|
||||
getCount: () => 5,
|
||||
},
|
||||
{
|
||||
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
|
||||
getCount: () => 10,
|
||||
},
|
||||
{
|
||||
title: msg`{count, plural, one {Next chapter} other {Next # chapters}}`,
|
||||
getCount: () => 25,
|
||||
},
|
||||
{
|
||||
title: msg`Download ahead ({count})`,
|
||||
getCount: (downloadAheadLimit) => downloadAheadLimit,
|
||||
onlyUnread: true,
|
||||
isDownloadAhead: true,
|
||||
},
|
||||
{ title: 'chapter.action.download.add.label.unread', getCount: () => undefined, onlyUnread: true },
|
||||
{ title: 'chapter.action.download.add.label.all', getCount: () => undefined, onlyUnread: false },
|
||||
{
|
||||
title: msg`Unread`,
|
||||
getCount: () => undefined,
|
||||
onlyUnread: true,
|
||||
},
|
||||
{
|
||||
title: msg`All`,
|
||||
getCount: () => undefined,
|
||||
onlyUnread: false,
|
||||
},
|
||||
];
|
||||
|
||||
const handleDownload = async (
|
||||
@@ -131,8 +152,6 @@ export const ChaptersDownloadActionMenuItems = ({
|
||||
mangaIds: MangaType['id'][];
|
||||
closeMenu: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
settings: { downloadAheadLimit },
|
||||
} = useMetadataServerSettings();
|
||||
@@ -140,8 +159,10 @@ export const ChaptersDownloadActionMenuItems = ({
|
||||
const handleSelect = (size?: number, onlyUnread: boolean = true, downloadAhead: boolean = false) => {
|
||||
handleDownload(mangaIds, onlyUnread, size, downloadAhead).catch((e) =>
|
||||
makeToast(
|
||||
t(CHAPTER_ACTION_TO_TRANSLATION.download.error, {
|
||||
count: size,
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({
|
||||
...CHAPTER_ACTION_TO_TRANSLATION.download.error,
|
||||
values: { count: size },
|
||||
}),
|
||||
'error',
|
||||
getErrorMessage(e),
|
||||
@@ -155,10 +176,16 @@ export const ChaptersDownloadActionMenuItems = ({
|
||||
<>
|
||||
{DOWNLOAD_OPTIONS.map(({ title, getCount, onlyUnread, isDownloadAhead }) => (
|
||||
<MenuItem
|
||||
key={t(title, { count: getCount(downloadAheadLimit) })}
|
||||
key={
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...title, values: { count: getCount(downloadAheadLimit) } })
|
||||
}
|
||||
onClick={() => handleSelect(getCount(downloadAheadLimit), onlyUnread, isDownloadAhead)}
|
||||
>
|
||||
{t(title, { count: getCount(downloadAheadLimit) })}
|
||||
{
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...title, values: { count: getCount(downloadAheadLimit) } })
|
||||
}
|
||||
</MenuItem>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
@@ -24,15 +24,13 @@ export const ChapterDownloadButton = ({
|
||||
chapterId: ChapterIdInfo['id'];
|
||||
isDownloaded: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const download = Chapters.useDownloadStatusFromCache(chapterId);
|
||||
|
||||
const downloadChapter = () => {
|
||||
requestManager
|
||||
.addChapterToDownloadQueue(chapterId)
|
||||
.response.catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
);
|
||||
.response.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
|
||||
};
|
||||
|
||||
if (download == null && isDownloaded) {
|
||||
@@ -40,7 +38,7 @@ export const ChapterDownloadButton = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t('chapter.action.download.add.label.action')}>
|
||||
<CustomTooltip title={t`Download`}>
|
||||
<IconButton
|
||||
{...MUIUtil.preventRippleProp()}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Refresh from '@mui/icons-material/Refresh';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
@@ -19,14 +19,14 @@ import { MUIUtil } from '@/lib/mui/MUI.util.ts';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterIdInfo['id'] }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const download = Chapters.useDownloadStatusFromCache(chapterId);
|
||||
|
||||
const handleRetry = async () => {
|
||||
try {
|
||||
await requestManager.addChapterToDownloadQueue(chapterId).response;
|
||||
} catch (e) {
|
||||
makeToast(t('download.queue.error.label.failed_to_retry'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not retry failed download.`, 'error', getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const ChapterDownloadRetryButton = ({ chapterId }: { chapterId: ChapterId
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t('global.button.retry')}>
|
||||
<CustomTooltip title={t`Retry`}>
|
||||
<IconButton
|
||||
{...MUIUtil.preventRippleProp()}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -16,9 +16,9 @@ import IconButton from '@mui/material/IconButton';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React, { memo, MouseEvent, TouchEvent, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import { useLongPress } from 'use-long-press';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { getDateString } from '@/base/utils/DateHelper.ts';
|
||||
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
|
||||
@@ -61,7 +61,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const ChapterCard = memo((props: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const preventMobileContextMenu = MediaQuery.usePreventMobileContextMenu();
|
||||
|
||||
@@ -134,13 +134,11 @@ export const ChapterCard = memo((props: IProps) => {
|
||||
>
|
||||
<ListCardContent>
|
||||
<ChapterCardMetadata
|
||||
title={
|
||||
showChapterNumber
|
||||
? `${t('chapter.title_one')} ${chapter.chapterNumber}`
|
||||
: chapter.name
|
||||
}
|
||||
title={showChapterNumber ? `${t`Chapter`} ${chapter.chapterNumber}` : chapter.name}
|
||||
secondaryText={chapter.scanlator}
|
||||
ternaryText={`${getDateString(Number(chapter.uploadDate ?? 0), true)}${isDownloaded ? ` • ${t('chapter.status.label.downloaded')}` : ''}`}
|
||||
ternaryText={`${getDateString(Number(chapter.uploadDate ?? 0), true)}${
|
||||
isDownloaded ? ` • ${t`Downloaded`}` : ''
|
||||
}`}
|
||||
infoIcons={
|
||||
chapter.isBookmarked && (
|
||||
<BookmarkIcon
|
||||
@@ -180,7 +178,7 @@ export const ChapterCard = memo((props: IProps) => {
|
||||
|
||||
<Stack sx={{ minHeight: '48px' }}>
|
||||
{selected === null ? (
|
||||
<CustomTooltip title={t('global.button.options')}>
|
||||
<CustomTooltip title={t`Options`}>
|
||||
<IconButton
|
||||
ref={menuButtonRef}
|
||||
{...MUIUtil.preventRippleProp(bindTrigger(popupState), {
|
||||
@@ -198,9 +196,7 @@ export const ChapterCard = memo((props: IProps) => {
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
) : (
|
||||
<CustomTooltip
|
||||
title={t(selected ? 'global.button.deselect' : 'global.button.select')}
|
||||
>
|
||||
<CustomTooltip title={selected ? t`Deselect` : t`Select`}>
|
||||
<Checkbox checked={selected} />
|
||||
</CustomTooltip>
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { t as translate } from 'i18next';
|
||||
import { DocumentNode, MaybeMasked, Unmasked, useFragment } from '@apollo/client';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
DownloadTypeFieldsFragment,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
import { CHAPTER_LIST_FIELDS } from '@/lib/graphql/chapter/ChapterFragments.ts';
|
||||
|
||||
import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||
|
||||
import { ReaderOpenChapterLocationState, ReaderResumeMode } from '@/features/reader/Reader.types.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import { assertIsDefined } from '@/base/Asserts.ts';
|
||||
import { DirectionOffset } from '@/base/Base.types.ts';
|
||||
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export class Chapters {
|
||||
static getIds(chapters: { id: number }[]): number[] {
|
||||
@@ -276,11 +277,12 @@ export class Chapters {
|
||||
|
||||
try {
|
||||
await Confirmation.show({
|
||||
title: translate('global.label.are_you_sure'),
|
||||
message: translate(confirmationMessage, { count: itemCount }),
|
||||
title: t`Are you sure?`,
|
||||
/* lingui-extract-ignore */
|
||||
message: i18n.t({ ...confirmationMessage, values: { count: itemCount } }),
|
||||
actions: {
|
||||
confirm: {
|
||||
title: translate('global.button.ok'),
|
||||
title: t`Ok`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -290,10 +292,15 @@ export class Chapters {
|
||||
}
|
||||
|
||||
await fnToExecute();
|
||||
makeToast(translate(CHAPTER_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success');
|
||||
makeToast(
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...CHAPTER_ACTION_TO_TRANSLATION[action].success, values: { count: itemCount } }),
|
||||
'success',
|
||||
);
|
||||
} catch (e) {
|
||||
makeToast(
|
||||
translate(CHAPTER_ACTION_TO_TRANSLATION[action].error, { count: itemCount }),
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...CHAPTER_ACTION_TO_TRANSLATION[action].error, values: { count: itemCount } }),
|
||||
'error',
|
||||
getErrorMessage(e),
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
updateMetadataServerSettings,
|
||||
useMetadataServerSettings,
|
||||
@@ -28,9 +28,9 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
|
||||
|
||||
export const DeviceSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('settings.device.title.device'));
|
||||
useAppTitle(t`Device`);
|
||||
|
||||
const {
|
||||
metadata,
|
||||
@@ -55,7 +55,7 @@ export const DeviceSetting = () => {
|
||||
}
|
||||
|
||||
updateMetadataServerSettings(setting, value).catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ export const DeviceSetting = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('DeviceSetting::refetch'))}
|
||||
/>
|
||||
@@ -76,32 +76,30 @@ export const DeviceSetting = () => {
|
||||
return (
|
||||
<List sx={{ pt: 0 }}>
|
||||
<MutableListSetting
|
||||
settingName={t('settings.device.devices.label.title')}
|
||||
description={t('settings.device.devices.label.description')}
|
||||
settingName={t`Devices`}
|
||||
description={t`Manage your existing devices.\nUI specific settings that are stored on the server are per device.\nThis makes it possible to have e.g. different settings on a desktop and a smartphone`}
|
||||
handleChange={(deviceList) => {
|
||||
updateMetadataSetting('devices', [
|
||||
...new Set(
|
||||
[DEFAULT_DEVICE, ...deviceList].filter((device) => device !== t('global.label.default')),
|
||||
),
|
||||
...new Set([DEFAULT_DEVICE, ...deviceList].filter((device) => device !== t`Default`)),
|
||||
]);
|
||||
}}
|
||||
valueInfos={devices.map((device) => [
|
||||
device === DEFAULT_DEVICE ? t('global.label.default') : device,
|
||||
device === DEFAULT_DEVICE ? t`Default` : device,
|
||||
{ mutable: false, deletable: device !== DEFAULT_DEVICE },
|
||||
])}
|
||||
addItemButtonTitle={t('global.button.create')}
|
||||
addItemButtonTitle={t`Create`}
|
||||
validateItem={(device) => device.length <= 16 && !!device.match(/^[a-zA-Z0-9\-_]+$/g)}
|
||||
placeholder={t('settings.device.label.placeholder')}
|
||||
placeholder={t`Smartphone_Name-1 | length: 16, chars: letters, numbers, -, _`}
|
||||
/>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t('settings.device.active_device.label.title')}
|
||||
secondary={t('settings.device.active_device.label.description')}
|
||||
primary={t`Active device`}
|
||||
secondary={t`Select a device to use its server stored UI settings`}
|
||||
/>
|
||||
<Select value={activeDevice} onChange={({ target: { value: device } }) => setActiveDevice(device)}>
|
||||
{devices.map((device) => (
|
||||
<MenuItem key={device} value={device}>
|
||||
{device === DEFAULT_DEVICE ? t('global.label.default') : device}
|
||||
{device === DEFAULT_DEVICE ? t`Default` : device}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
SelectSetting,
|
||||
SelectSettingValue,
|
||||
@@ -14,33 +15,30 @@ import {
|
||||
} from '@/base/components/settings/SelectSetting.tsx';
|
||||
|
||||
const CHAPTERS_TO_DELETE = [0, 1, 2, 3, 4, 5] as const;
|
||||
const CHAPTERS_TO_DELETE_TO_TRANSLATION_KEY: {
|
||||
const CHAPTERS_TO_DELETE_TO_TRANSLATION: {
|
||||
[flavor in (typeof CHAPTERS_TO_DELETE)[number]]: SelectSettingValueDisplayInfo;
|
||||
} = {
|
||||
0: {
|
||||
text: 'global.label.disabled',
|
||||
text: msg`Disabled`,
|
||||
},
|
||||
1: {
|
||||
text: 'download.settings.delete_chapters.while_reading.option.label.first',
|
||||
text: msg`Last read chapter`,
|
||||
},
|
||||
2: {
|
||||
text: 'download.settings.delete_chapters.while_reading.option.label.second',
|
||||
text: msg`Second to last read chapter`,
|
||||
},
|
||||
3: {
|
||||
text: 'download.settings.delete_chapters.while_reading.option.label.third',
|
||||
text: msg`Third to last read chapter`,
|
||||
},
|
||||
4: {
|
||||
text: 'download.settings.delete_chapters.while_reading.option.label.fourth',
|
||||
text: msg`Fourth to last read chapter`,
|
||||
},
|
||||
5: {
|
||||
text: 'download.settings.delete_chapters.while_reading.option.label.fifth',
|
||||
text: msg`Fifth to last read chapter`,
|
||||
},
|
||||
};
|
||||
const CHAPTERS_TO_DELETE_SELECT_VALUES: SelectSettingValue<(typeof CHAPTERS_TO_DELETE)[number]>[] =
|
||||
CHAPTERS_TO_DELETE.map((chapterToDelete) => [
|
||||
chapterToDelete,
|
||||
CHAPTERS_TO_DELETE_TO_TRANSLATION_KEY[chapterToDelete],
|
||||
]);
|
||||
CHAPTERS_TO_DELETE.map((chapterToDelete) => [chapterToDelete, CHAPTERS_TO_DELETE_TO_TRANSLATION[chapterToDelete]]);
|
||||
|
||||
const getNormalizedChapterToDelete = (chapterToDelete: number | boolean) => {
|
||||
const isMigrationVersion0 = typeof chapterToDelete === 'boolean';
|
||||
@@ -58,13 +56,13 @@ export const DeleteChaptersWhileReadingSetting = ({
|
||||
chapterToDelete: number;
|
||||
handleChange: (chapterToDelete: number) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const normalizedChapterToDelete = getNormalizedChapterToDelete(chapterToDelete);
|
||||
|
||||
return (
|
||||
<SelectSetting
|
||||
settingName={t('download.settings.delete_chapters.while_reading.label.title')}
|
||||
settingName={t`Delete finished chapters while reading`}
|
||||
value={normalizedChapterToDelete}
|
||||
values={CHAPTERS_TO_DELETE_SELECT_VALUES}
|
||||
handleChange={handleChange}
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
|
||||
import { getPersistedServerSetting, usePersistedValue } from '@/base/hooks/usePersistedValue.tsx';
|
||||
import { updateMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||
@@ -25,7 +26,7 @@ export const DownloadAheadSetting = ({
|
||||
}: {
|
||||
downloadAheadLimit: MetadataServerSettings['downloadAheadLimit'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const shouldDownloadAhead = !!downloadAheadLimit;
|
||||
const [currentDownloadAheadLimit, persistDownloadAheadLimit] = usePersistedValue(
|
||||
@@ -38,7 +39,7 @@ export const DownloadAheadSetting = ({
|
||||
const updateSetting = (value: MetadataDownloadSettings['downloadAheadLimit']) => {
|
||||
persistDownloadAheadLimit(value === 0 ? currentDownloadAheadLimit : value);
|
||||
updateMetadataServerSettings('downloadAheadLimit', value).catch((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,14 +51,14 @@ export const DownloadAheadSetting = ({
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('download.settings.download_ahead.label.while_reading')} />
|
||||
<ListItemText primary={t`Auto download while reading`} />
|
||||
<Switch edge="end" checked={shouldDownloadAhead} onChange={(e) => setDoAutoUpdates(e.target.checked)} />
|
||||
</ListItem>
|
||||
<NumberSetting
|
||||
settingTitle={t('download.settings.download_ahead.label.unread_chapters_to_download')}
|
||||
settingValue={t('download.settings.download_ahead.label.value', {
|
||||
chapters: currentDownloadAheadLimit,
|
||||
count: currentDownloadAheadLimit,
|
||||
settingTitle={t`Number of unread chapters to download`}
|
||||
settingValue={plural(currentDownloadAheadLimit, {
|
||||
one: '# Chapter',
|
||||
other: '# Chapters',
|
||||
})}
|
||||
value={currentDownloadAheadLimit}
|
||||
minValue={DOWNLOAD_AHEAD.min}
|
||||
@@ -65,9 +66,9 @@ export const DownloadAheadSetting = ({
|
||||
defaultValue={DOWNLOAD_AHEAD.default}
|
||||
stepSize={DOWNLOAD_AHEAD.step}
|
||||
showSlider
|
||||
dialogDescription={t('download.settings.download_ahead.label.description')}
|
||||
dialogDisclaimer={t('download.settings.download_ahead.label.disclaimer')}
|
||||
valueUnit={t('chapter.title_one')}
|
||||
dialogDescription={t`How many chapters should get downloaded while reading.`}
|
||||
dialogDisclaimer={t`Only works if the current chapter plus the next chapter are already downloaded.`}
|
||||
valueUnit={t`Chapter`}
|
||||
handleUpdate={updateSetting}
|
||||
disabled={!shouldDownloadAhead}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,7 @@ import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { ChapterDownloadRetryButton } from '@/features/chapter/components/buttons/ChapterDownloadRetryButton.tsx';
|
||||
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
|
||||
@@ -32,7 +32,7 @@ import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
||||
|
||||
export const DownloadQueueChapterCard = memo(
|
||||
({ item, status }: { item: ChapterDownloadStatus; status: DownloaderState }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const preventMobileContextMenu = MediaQuery.usePreventMobileContextMenu();
|
||||
|
||||
const handleDelete = useCallback(
|
||||
@@ -53,7 +53,7 @@ export const DownloadQueueChapterCard = memo(
|
||||
requestManager.deleteDownloadedChapter(chapter.id).response,
|
||||
]);
|
||||
} catch (e) {
|
||||
makeToast(t('download.queue.error.label.failed_to_remove'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not remove the download from the queue.`, 'error', getErrorMessage(e));
|
||||
}
|
||||
|
||||
if (!isRunning) {
|
||||
@@ -83,7 +83,7 @@ export const DownloadQueueChapterCard = memo(
|
||||
<ChapterCardMetadata title={item.manga.title} secondaryText={item.chapter.name} />
|
||||
<DownloadStateIndicator chapterId={item.chapter.id} />
|
||||
<ChapterDownloadRetryButton chapterId={item.chapter.id} />
|
||||
<CustomTooltip title={t('chapter.action.download.delete.label.action')}>
|
||||
<CustomTooltip title={t`Delete`}>
|
||||
<IconButton
|
||||
{...MUIUtil.preventRippleProp()}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -11,11 +11,11 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { useWindowEvent } from '@mantine/hooks';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
@@ -34,9 +34,9 @@ import { ChapterDownloadStatus } from '@/features/chapter/Chapter.types.ts';
|
||||
import { VirtuosoPersisted } from '@/lib/virtuoso/Component/VirtuosoPersisted.tsx';
|
||||
|
||||
export const DownloadQueue: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('download.title.queue'));
|
||||
useAppTitle(t`Download queue`);
|
||||
|
||||
const [reorderDownload, { reset: revertReorder }] = requestManager.useReorderChapterInDownloadQueue();
|
||||
|
||||
@@ -60,7 +60,7 @@ export const DownloadQueue: React.FC = () => {
|
||||
try {
|
||||
await requestManager.clearDownloads().response;
|
||||
} catch (e) {
|
||||
makeToast(t('download.queue.error.label.failed_delete_all'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not remove all downloads from the queue`, 'error', getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,16 +99,13 @@ export const DownloadQueue: React.FC = () => {
|
||||
|
||||
useAppAction(
|
||||
<>
|
||||
<CustomTooltip title={t('download.queue.label.delete_all')}>
|
||||
<CustomTooltip title={t`Delete all`}>
|
||||
<IconButton onClick={clearQueue} color="inherit">
|
||||
<DeleteSweepIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
|
||||
<CustomTooltip
|
||||
title={t(status === DownloaderState.Started ? 'global.button.stop' : 'global.button.start')}
|
||||
disabled={isQueueEmpty}
|
||||
>
|
||||
<CustomTooltip title={status === DownloaderState.Started ? t`Stop` : t`Start`} disabled={isQueueEmpty}>
|
||||
<IconButton onClick={toggleQueueStatus} disabled={isQueueEmpty} color="inherit">
|
||||
{status === DownloaderState.Stopped ? <PlayArrowIcon /> : <PauseIcon />}
|
||||
</IconButton>
|
||||
@@ -135,7 +132,7 @@ export const DownloadQueue: React.FC = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('DownloadQueue::refetch'))}
|
||||
/>
|
||||
@@ -143,7 +140,7 @@ export const DownloadQueue: React.FC = () => {
|
||||
}
|
||||
|
||||
if (isQueueEmpty) {
|
||||
return <EmptyViewAbsoluteCentered message={t('download.queue.label.no_downloads')} />;
|
||||
return <EmptyViewAbsoluteCentered message={t`No downloads`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { DownloadAheadSetting } from '@/features/downloads/components/DownloadAheadSetting.tsx';
|
||||
@@ -47,9 +48,9 @@ type DownloadSettingsType = Pick<
|
||||
>;
|
||||
|
||||
export const DownloadSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('download.title.download'));
|
||||
useAppTitle(t`Downloads`);
|
||||
|
||||
const categories = requestManager.useGetCategories<GetCategoriesSettingsQuery, GetCategoriesSettingsQueryVariables>(
|
||||
GET_CATEGORIES_SETTINGS,
|
||||
@@ -71,7 +72,7 @@ export const DownloadSettings = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => {
|
||||
if (serverSettings.error) {
|
||||
@@ -101,28 +102,28 @@ export const DownloadSettings = () => {
|
||||
value: DownloadSettingsType[Setting],
|
||||
): Promise<any> => {
|
||||
const mutation = mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
|
||||
mutation.catch((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
|
||||
mutation.catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
||||
const updateMetadataSetting = createUpdateMetadataServerSettings<keyof MetadataDownloadSettings>((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
return (
|
||||
<List sx={{ pt: 0 }}>
|
||||
<TextSetting
|
||||
settingName={t('download.settings.download_path.label.title')}
|
||||
dialogDescription={t('download.settings.download_path.label.description')}
|
||||
settingName={t`Download location`}
|
||||
dialogDescription={t`The path to the directory on the server where downloaded files should get saved in`}
|
||||
value={downloadSettings?.downloadsPath}
|
||||
settingDescription={
|
||||
downloadSettings?.downloadsPath.length ? downloadSettings.downloadsPath : t('global.label.default')
|
||||
downloadSettings?.downloadsPath.length ? downloadSettings.downloadsPath : t`Default`
|
||||
}
|
||||
handleChange={(path) => updateSetting('downloadsPath', path)}
|
||||
/>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('download.settings.file_type.label.cbz')} />
|
||||
<ListItemText primary={t`Save as CBZ archive`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={!!downloadSettings?.downloadAsCbz}
|
||||
@@ -130,17 +131,17 @@ export const DownloadSettings = () => {
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItemLink to={AppRoutes.settings.childRoutes.images.childRoutes.processingDownloads.path}>
|
||||
<ListItemText primary={t('download.settings.conversion.title')} />
|
||||
<ListItemText primary={t`Image download processing`} />
|
||||
</ListItemLink>
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader component="div" id="download-settings-auto-delete-downloads">
|
||||
{t('download.settings.delete_chapters.title')}
|
||||
{t`Delete chapters`}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('download.settings.delete_chapters.label.manually_marked_as_read')} />
|
||||
<ListItemText primary={t`Delete chapter after manually marking it as read`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={metadataSettings.deleteChaptersManuallyMarkedRead}
|
||||
@@ -154,7 +155,7 @@ export const DownloadSettings = () => {
|
||||
}
|
||||
/>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('download.settings.delete_chapters.label.allow_deletion_of_bookmarked')} />
|
||||
<ListItemText primary={t`Allow deleting bookmarked chapters`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={metadataSettings.deleteChaptersWithBookmark}
|
||||
@@ -165,12 +166,12 @@ export const DownloadSettings = () => {
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader component="div" id="download-settings-auto-download">
|
||||
{t('download.settings.auto_download.title')}
|
||||
{t`Auto-download`}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('download.settings.auto_download.label.new_chapters')} />
|
||||
<ListItemText primary={t`Download new chapters`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={!!downloadSettings?.autoDownloadNewChapters}
|
||||
@@ -179,28 +180,28 @@ export const DownloadSettings = () => {
|
||||
</ListItem>
|
||||
<NumberSetting
|
||||
disabled={!downloadSettings?.autoDownloadNewChapters}
|
||||
settingTitle={t('download.settings.auto_download.download_limit.label.title')}
|
||||
dialogDescription={t('download.settings.auto_download.download_limit.label.description')}
|
||||
settingTitle={t`Chapter download limit`}
|
||||
dialogDescription={t`Limit the amount of new chapters that are going to get downloaded.`}
|
||||
value={downloadSettings?.autoDownloadNewChaptersLimit ?? 0}
|
||||
settingValue={
|
||||
!downloadSettings.autoDownloadNewChaptersLimit
|
||||
? t('global.label.none')
|
||||
: t('download.settings.download_ahead.label.value', {
|
||||
chapters: downloadSettings.autoDownloadNewChaptersLimit,
|
||||
count: downloadSettings.autoDownloadNewChaptersLimit,
|
||||
? t`None`
|
||||
: plural(downloadSettings.autoDownloadNewChaptersLimit, {
|
||||
one: '# Chapter',
|
||||
other: '# Chapters',
|
||||
})
|
||||
}
|
||||
defaultValue={0}
|
||||
minValue={0}
|
||||
maxValue={20}
|
||||
showSlider
|
||||
valueUnit={t('chapter.title_one')}
|
||||
valueUnit={t`Chapter`}
|
||||
handleUpdate={(autoDownloadNewChaptersLimit) =>
|
||||
updateSetting('autoDownloadNewChaptersLimit', autoDownloadNewChaptersLimit)
|
||||
}
|
||||
/>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('download.settings.auto_download.label.ignore_with_unread_chapters')} />
|
||||
<ListItemText primary={t`Ignore automatic chapter downloads for entries with unread chapters`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={!!downloadSettings?.excludeEntryWithUnreadChapters}
|
||||
@@ -209,7 +210,7 @@ export const DownloadSettings = () => {
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('download.settings.auto_download.label.ignore_re_uploads')} />
|
||||
<ListItemText primary={t`Ignore re-uploaded chapters`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={!!downloadSettings?.autoDownloadIgnoreReUploads}
|
||||
@@ -220,13 +221,13 @@ export const DownloadSettings = () => {
|
||||
<CategoriesInclusionSetting
|
||||
categories={categories.data!.categories.nodes}
|
||||
includeField="includeInDownload"
|
||||
dialogText={t('download.settings.auto_download.categories.label.include_in_download')}
|
||||
dialogText={t`Entries in excluded categories will not be downloaded even if they are also in included categories`}
|
||||
/>
|
||||
</List>
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader component="div" id="download-settings-download-ahead">
|
||||
{t('download.settings.download_ahead.title')}
|
||||
{t`Download ahead`}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
ExtensionAction,
|
||||
ExtensionGroupState,
|
||||
@@ -14,7 +16,6 @@ import {
|
||||
InstalledStates,
|
||||
} from '@/features/extension/Extensions.types.ts';
|
||||
import { DefaultLanguage } from '@/base/utils/Languages.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
export const EXTENSION_ACTION_TO_STATE_MAP: { [action in ExtensionAction]: ExtensionState } = {
|
||||
[ExtensionAction.UPDATE]: ExtensionState.UPDATING,
|
||||
@@ -28,31 +29,33 @@ export const EXTENSION_ACTION_TO_NEXT_ACTION_MAP: { [action in ExtensionAction]:
|
||||
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
|
||||
} as const;
|
||||
|
||||
export const INSTALLED_STATE_TO_TRANSLATION_KEY_MAP: { [installedState in InstalledStates]: TranslationKey } = {
|
||||
[InstalledState.UNINSTALL]: 'extension.action.label.uninstall',
|
||||
[InstalledState.INSTALL]: 'extension.action.label.install',
|
||||
[InstalledState.UPDATE]: 'extension.action.label.update',
|
||||
[InstalledState.OBSOLETE]: 'extension.state.label.obsolete',
|
||||
[InstalledState.UPDATING]: 'extension.state.label.updating',
|
||||
[InstalledState.UNINSTALLING]: 'extension.state.label.uninstalling',
|
||||
[InstalledState.INSTALLING]: 'extension.state.label.installing',
|
||||
export const INSTALLED_STATE_TO_TRANSLATION_MAP: { [installedState in InstalledStates]: MessageDescriptor } = {
|
||||
[InstalledState.UNINSTALL]: msg`Uninstall`,
|
||||
[InstalledState.INSTALL]: msg`Install`,
|
||||
[InstalledState.UPDATE]: msg`Update`,
|
||||
[InstalledState.OBSOLETE]: msg`Obsolete`,
|
||||
[InstalledState.UPDATING]: msg`Updating`,
|
||||
[InstalledState.UNINSTALLING]: msg`Uninstalling`,
|
||||
[InstalledState.INSTALLING]: msg`Installing`,
|
||||
} as const;
|
||||
|
||||
export const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP: {
|
||||
[action in ExtensionAction]: TranslationKey;
|
||||
export const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP: {
|
||||
[action in ExtensionAction]: MessageDescriptor;
|
||||
} = {
|
||||
[ExtensionAction.UPDATE]: 'extension.label.update_failed',
|
||||
[ExtensionAction.INSTALL]: 'extension.label.installation_failed',
|
||||
[ExtensionAction.UNINSTALL]: 'extension.label.uninstallation_failed',
|
||||
[ExtensionAction.UPDATE]: msg`{count, plural, one {Could not update the extension} other {Could not update the extensions}}`,
|
||||
[ExtensionAction.INSTALL]: msg`{count, plural, one {Could not install the extension} other {Could not install the extensions}}`,
|
||||
[ExtensionAction.UNINSTALL]: msg`{count, plural, one {Could not uninstall the extension} other {Could not uninstall the extensions}}`,
|
||||
};
|
||||
|
||||
export const extensionLanguageToTranslationKey: { [state in ExtensionGroupState | DefaultLanguage]: TranslationKey } = {
|
||||
[ExtensionGroupState.INSTALLED]: 'extension.state.label.installed',
|
||||
[ExtensionGroupState.UPDATE_PENDING]: 'extension.state.label.update_pending',
|
||||
[ExtensionGroupState.OBSOLETE]: 'extension.state.label.obsolete',
|
||||
[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',
|
||||
export const extensionLanguageToTranslation: {
|
||||
[state in ExtensionGroupState | DefaultLanguage]: MessageDescriptor;
|
||||
} = {
|
||||
[ExtensionGroupState.INSTALLED]: msg`Installed`,
|
||||
[ExtensionGroupState.UPDATE_PENDING]: msg`Update pending`,
|
||||
[ExtensionGroupState.OBSOLETE]: msg`Obsolete`,
|
||||
[DefaultLanguage.ALL]: msg`All`,
|
||||
[DefaultLanguage.OTHER]: msg`Other`,
|
||||
[DefaultLanguage.LOCAL_SOURCE]: msg`Other`,
|
||||
[DefaultLanguage.PINNED]: msg`Pinned`,
|
||||
[DefaultLanguage.LAST_USED_SOURCE]: msg`Last used`,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
ExtensionAction,
|
||||
ExtensionGroupState,
|
||||
@@ -23,14 +22,15 @@ import {
|
||||
toComparableLanguages,
|
||||
} from '@/base/utils/Languages.ts';
|
||||
import {
|
||||
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP,
|
||||
extensionLanguageToTranslationKey,
|
||||
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP,
|
||||
extensionLanguageToTranslation,
|
||||
} from '@/features/extension/Extensions.constants.ts';
|
||||
import { enhancedCleanup } from '@/base/utils/Strings.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { toUniqueISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export const getInstalledState = (
|
||||
isInstalled: boolean,
|
||||
@@ -65,7 +65,7 @@ export const isExtensionStateOrLanguage = (languageCode: string): boolean =>
|
||||
|
||||
export const translateExtensionLanguage = (languageCode: string): string =>
|
||||
isExtensionStateOrLanguage(languageCode)
|
||||
? t(extensionLanguageToTranslationKey[languageCode as ExtensionGroupState | DefaultLanguage])
|
||||
? i18n._(extensionLanguageToTranslation[languageCode as ExtensionGroupState | DefaultLanguage])
|
||||
: languageCodeToName(languageCode);
|
||||
|
||||
export function groupExtensionsByLanguage(extensions: TExtension[]): GroupedExtensionsResult {
|
||||
@@ -165,7 +165,8 @@ export const updateExtension = async (
|
||||
}
|
||||
} catch (e) {
|
||||
makeToast(
|
||||
t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[action], { count: 1 }),
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...EXTENSION_ACTION_TO_FAILURE_TRANSLATION_MAP[action], values: { count: 1 } }),
|
||||
'error',
|
||||
getErrorMessage(e),
|
||||
);
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { INSTALLED_STATE_TO_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { INSTALLED_STATE_TO_TRANSLATION_MAP } from '@/features/extension/Extensions.constants.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts';
|
||||
import { useBackButton } from '@/base/hooks/useBackButton.ts';
|
||||
@@ -17,7 +17,7 @@ import { ExtensionAction, InstalledState, TExtension } from '@/features/extensio
|
||||
|
||||
export const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TExtension) => {
|
||||
const handleBack = useBackButton();
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const installedState = getInstalledState(isInstalled, isObsolete, hasUpdate);
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TE
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
|
||||
{t(INSTALLED_STATE_TO_TRANSLATION_MAP[installedState])}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -7,25 +7,19 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { TExtension } from '@/features/extension/Extensions.types.ts';
|
||||
import { ExtensionMetadata } from '@/features/extension/info/components/ExtensionMetadata.tsx';
|
||||
import { languageCodeToName } from '@/base/utils/Languages.ts';
|
||||
|
||||
export const Meta = ({ versionName, lang, isNsfw }: TExtension) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<ExtensionMetadata title={t('global.label.version')} value={versionName} />
|
||||
<ExtensionMetadata title={t('global.language.label.language')} value={languageCodeToName(lang)} />
|
||||
{isNsfw && (
|
||||
<ExtensionMetadata
|
||||
title={t('extension.label.age_rating')}
|
||||
value="18+"
|
||||
valueProps={{ color: 'error' }}
|
||||
/>
|
||||
)}
|
||||
<ExtensionMetadata title={t`Version`} value={versionName} />
|
||||
<ExtensionMetadata title={t`Language`} value={languageCodeToName(lang)} />
|
||||
{isNsfw && <ExtensionMetadata title={t`Age rating`} value="18+" valueProps={{ color: 'error' }} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
*/
|
||||
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Card from '@mui/material/Card';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import CardActionArea from '@mui/material/CardActionArea';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { MUIUtil } from '@/lib/mui/MUI.util.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
@@ -29,11 +29,11 @@ import { SourceConfigurableInfo, SourceIdInfo, SourceLanguageInfo } from '@/feat
|
||||
export const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo) => {
|
||||
const { id, isConfigurable } = source;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { isEnabled } = useGetSourceMetadata(source);
|
||||
|
||||
const updateSetting = createUpdateSourceMetadata(source, (e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -45,7 +45,7 @@ export const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceCon
|
||||
{translateExtensionLanguage(Sources.getLanguage(source))}
|
||||
</Typography>
|
||||
{isConfigurable && (
|
||||
<CustomTooltip title={t('settings.title')}>
|
||||
<CustomTooltip title={t`Settings`}>
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={AppRoutes.sources.childRoutes.configure.path(id)}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMemo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
@@ -26,10 +26,10 @@ import { ActionButton } from '@/features/extension/info/components/ActionButton.
|
||||
import { SourceCard } from '@/features/extension/info/components/SourceCard.tsx';
|
||||
|
||||
export const ExtensionInfo = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { pkgName } = useParams<{ pkgName: string }>();
|
||||
|
||||
useAppTitle(t('source.extension_info.title'));
|
||||
useAppTitle(t`Extension info`);
|
||||
|
||||
const extensionResponse = requestManager.useGetExtension(pkgName);
|
||||
const sourcesResponse = requestManager.useGetSourceList();
|
||||
@@ -55,7 +55,7 @@ export const ExtensionInfo = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(extensionResponse.error)}
|
||||
retry={() => {
|
||||
if (extensionResponse.error) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import Typography from '@mui/material/Typography';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Button from '@mui/material/Button';
|
||||
@@ -23,6 +22,7 @@ import { useElementSize } from '@mantine/hooks';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import { d } from 'koration';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
|
||||
import { useDebounce } from '@/base/hooks/useDebounce.ts';
|
||||
@@ -128,7 +128,7 @@ const SourceSearchPreview = React.memo(
|
||||
emptyQuery: boolean;
|
||||
} & Pick<MangaCardProps, 'mode'> &
|
||||
Pick<MetadataBrowseSettings, 'shouldShowOnlySourcesWithResults'>) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { id, name, lang } = source;
|
||||
|
||||
@@ -163,9 +163,9 @@ const SourceSearchPreview = React.memo(
|
||||
|
||||
let errorMessage: string | undefined;
|
||||
if (error) {
|
||||
errorMessage = t('search.error.label.source_search_failed');
|
||||
errorMessage = t`Could not search source`;
|
||||
} else if (noMangasFound) {
|
||||
errorMessage = t('manga.error.label.no_mangas_found');
|
||||
errorMessage = t`No manga found`;
|
||||
}
|
||||
|
||||
if ((!isLoading && !searchString) || emptyQuery) {
|
||||
@@ -188,7 +188,7 @@ const SourceSearchPreview = React.memo(
|
||||
<Typography variant="h5">{name}</Typography>
|
||||
<Typography variant="caption">{translateExtensionLanguage(lang)}</Typography>
|
||||
</Box>
|
||||
<CustomTooltip title={t('global.button.show_more')}>
|
||||
<CustomTooltip title={t`Show more`}>
|
||||
<IconButton {...MUIUtil.preventRippleProp()}>
|
||||
<ArrowForwardIcon />
|
||||
</IconButton>
|
||||
@@ -232,7 +232,7 @@ const SourceSearchPreview = React.memo(
|
||||
);
|
||||
|
||||
export const SearchAll: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
const { pathname, state } = useLocation<{ mangaTitle?: string; shouldShowOnlyPinnedSources?: boolean }>();
|
||||
const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize();
|
||||
@@ -288,11 +288,11 @@ export const SearchAll: React.FC = () => {
|
||||
);
|
||||
|
||||
const updateMetadataSettings = createUpdateMetadataServerSettings<'shouldShowOnlySourcesWithResults'>((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
useAppTitleAndAction(
|
||||
t(isMigrateMode ? 'migrate.search.title' : 'search.title.global_search', { title: state?.mangaTitle }),
|
||||
isMigrateMode ? t`Migrate "${state?.mangaTitle}"` : t`Global Search`,
|
||||
<>
|
||||
<AppbarSearch isClosable={false} />
|
||||
<SourceLanguageSelect
|
||||
@@ -312,7 +312,7 @@ export const SearchAll: React.FC = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('SearchAll::refetch'))}
|
||||
/>
|
||||
@@ -351,7 +351,7 @@ export const SearchAll: React.FC = () => {
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('global.label.pinned')}
|
||||
{t`Pinned`}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<DoneAllIcon />}
|
||||
@@ -369,7 +369,7 @@ export const SearchAll: React.FC = () => {
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('extension.language.all')}
|
||||
{t`All`}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Button
|
||||
@@ -379,7 +379,7 @@ export const SearchAll: React.FC = () => {
|
||||
updateMetadataSettings('shouldShowOnlySourcesWithResults', !shouldShowOnlySourcesWithResults)
|
||||
}
|
||||
>
|
||||
{t('search.filter.has_results')}
|
||||
{t`Has results`}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Box sx={{ pt: `${filterHeaderHeight}px` }}>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
@@ -23,9 +23,9 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
|
||||
|
||||
export const History: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('history.title'));
|
||||
useAppTitle(t`History`);
|
||||
|
||||
const {
|
||||
data: chapterHistoryData,
|
||||
@@ -66,7 +66,7 @@ export const History: React.FC = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('History::refetch'))}
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ export const History: React.FC = () => {
|
||||
}
|
||||
|
||||
if (!isLoading && readEntries.length === 0) {
|
||||
return <EmptyViewAbsoluteCentered message={t('history.error.label.no_history_available')} />;
|
||||
return <EmptyViewAbsoluteCentered message={t`You have not read any series yet.`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
createUpdateMetadataServerSettings,
|
||||
useMetadataServerSettings,
|
||||
@@ -24,16 +24,16 @@ import { MetadataHistorySettings } from '@/features/history/History.types.ts';
|
||||
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
|
||||
|
||||
export const HistorySettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('history.title'));
|
||||
useAppTitle(t`History`);
|
||||
|
||||
const {
|
||||
settings: { hideHistory },
|
||||
request: { loading, error, refetch },
|
||||
} = useMetadataServerSettings();
|
||||
const updateMetadataServerSettings = createUpdateMetadataServerSettings<keyof MetadataHistorySettings>((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
@@ -43,7 +43,7 @@ export const HistorySettings = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('HistorySettings::refetch'))}
|
||||
/>
|
||||
@@ -53,7 +53,7 @@ export const HistorySettings = () => {
|
||||
return (
|
||||
<List sx={{ pt: 0 }}>
|
||||
<ListItem>
|
||||
<ListItemText primary={t('history.settings.hide')} />
|
||||
<ListItemText primary={t`Hide history`} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={hideHistory}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IMangaGridProps, MangaGrid } from '@/features/manga/components/MangaGrid.tsx';
|
||||
import { GridLayout } from '@/base/Base.types.ts';
|
||||
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||
@@ -27,7 +27,7 @@ export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({
|
||||
messageExtra,
|
||||
...gridProps
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const {
|
||||
settings: { gridLayout },
|
||||
@@ -46,7 +46,7 @@ export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({
|
||||
{...gridProps}
|
||||
hasNextPage={false}
|
||||
loadMore={loadMoreNoop}
|
||||
message={showFilteredOutMessage ? t('library.error.label.no_matches') : message}
|
||||
message={showFilteredOutMessage ? t`No manga matches this filter` : message}
|
||||
messageExtra={showFilteredOutMessage ? undefined : messageExtra}
|
||||
gridLayout={gridLayout}
|
||||
/>
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import FormLabel from '@mui/material/FormLabel';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { RadioInput } from '@/base/components/inputs/RadioInput.tsx';
|
||||
import { SortRadioInput } from '@/base/components/inputs/SortRadioInput.tsx';
|
||||
@@ -28,23 +30,23 @@ import {
|
||||
import { LibrarySortMode } from '@/features/library/Library.types.ts';
|
||||
import { CategoryMetadataInfo } from '@/features/category/Category.types.ts';
|
||||
import { MANGA_STATUS_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
|
||||
import { GridLayout, TranslationKey } from '@/base/Base.types.ts';
|
||||
import { GridLayout } from '@/base/Base.types';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
sort: 'global.label.sort',
|
||||
display: 'global.label.display',
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: MessageDescriptor } = {
|
||||
filter: msg`Filter`,
|
||||
sort: msg`Sort`,
|
||||
display: msg`Display`,
|
||||
};
|
||||
|
||||
const SORT_OPTIONS: [LibrarySortMode, TranslationKey][] = [
|
||||
['unreadChapters', 'library.option.sort.label.by_unread_chapters'],
|
||||
['totalChapters', 'library.option.sort.label.by_total_chapters'],
|
||||
['alphabetically', 'library.option.sort.label.alphabetically'],
|
||||
['dateAdded', 'library.option.sort.label.by_date_added'],
|
||||
['lastRead', 'library.option.sort.label.by_last_read'],
|
||||
['latestFetchedChapter', 'library.option.sort.label.by_latest_fetched_chapter'],
|
||||
['latestUploadedChapter', 'library.option.sort.label.by_latest_uploaded_chapter'],
|
||||
const SORT_OPTIONS: [LibrarySortMode, MessageDescriptor][] = [
|
||||
['unreadChapters', msg`Unread chapters`],
|
||||
['totalChapters', msg`Total chapters`],
|
||||
['alphabetically', msg`A-Z`],
|
||||
['dateAdded', msg`Recently added`],
|
||||
['lastRead', msg`Recently read`],
|
||||
['latestFetchedChapter', msg`Latest fetched chapter`],
|
||||
['latestUploadedChapter', msg`Latest uploaded chapter`],
|
||||
];
|
||||
|
||||
export const LibraryOptionsPanel = ({
|
||||
@@ -56,21 +58,21 @@ export const LibraryOptionsPanel = ({
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
|
||||
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
|
||||
|
||||
const categoryLibraryOptions = useGetCategoryMetadata(category);
|
||||
const updateCategoryLibraryOptions = createUpdateCategoryMetadata(category, (e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
const {
|
||||
settings: { showTabSize, showContinueReadingButton, showDownloadBadge, showUnreadBadge, gridLayout },
|
||||
} = useMetadataServerSettings();
|
||||
const setSettingValue = createUpdateMetadataServerSettings((e) =>
|
||||
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -84,31 +86,31 @@ export const LibraryOptionsPanel = ({
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.unread')}
|
||||
label={t`Unread`}
|
||||
checked={categoryLibraryOptions.hasUnreadChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasUnreadChapters', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.started')}
|
||||
label={t`Started`}
|
||||
checked={categoryLibraryOptions.hasReadChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasReadChapters', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.downloaded')}
|
||||
label={t`Downloaded`}
|
||||
checked={categoryLibraryOptions.hasDownloadedChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasDownloadedChapters', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.bookmarked')}
|
||||
label={t`Bookmarked`}
|
||||
checked={categoryLibraryOptions.hasBookmarkedChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasBookmarkedChapters', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.duplicate_chapters')}
|
||||
label={t`Duplicate chapters`}
|
||||
checked={categoryLibraryOptions.hasDuplicateChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasDuplicateChapters', c)}
|
||||
/>
|
||||
<FormLabel sx={{ mt: 2 }}>{t('manga.label.status')}</FormLabel>
|
||||
<FormLabel sx={{ mt: 2 }}>{t`Status`}</FormLabel>
|
||||
{Object.values(MangaStatus).map((status) => (
|
||||
<ThreeStateCheckboxInput
|
||||
key={status}
|
||||
@@ -122,7 +124,7 @@ export const LibraryOptionsPanel = ({
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<FormLabel sx={{ mt: 2 }}>{t('global.filter.label.tracked')}</FormLabel>
|
||||
<FormLabel sx={{ mt: 2 }}>{t`Tracked`}</FormLabel>
|
||||
{loggedInTrackers.map((tracker) => (
|
||||
<ThreeStateCheckboxInput
|
||||
key={tracker.id}
|
||||
@@ -157,50 +159,47 @@ export const LibraryOptionsPanel = ({
|
||||
if (key === 'display') {
|
||||
return (
|
||||
<>
|
||||
<FormLabel>{t('global.grid_layout.title')}</FormLabel>
|
||||
<FormLabel>{t`Display mode`}</FormLabel>
|
||||
<RadioGroup
|
||||
onChange={(e) => updateMetadataServerSettings('gridLayout', Number(e.target.value))}
|
||||
value={gridLayout}
|
||||
>
|
||||
<RadioInput
|
||||
label={t('global.grid_layout.label.compact_grid')}
|
||||
label={t`Compact grid`}
|
||||
value={GridLayout.Compact}
|
||||
checked={gridLayout == null || gridLayout === GridLayout.Compact}
|
||||
/>
|
||||
<RadioInput
|
||||
label={t('global.grid_layout.label.comfortable_grid')}
|
||||
label={t`Comfortable grid`}
|
||||
value={GridLayout.Comfortable}
|
||||
checked={gridLayout === GridLayout.Comfortable}
|
||||
/>
|
||||
<RadioInput
|
||||
label={t('global.grid_layout.label.list')}
|
||||
label={t`List`}
|
||||
value={GridLayout.List}
|
||||
checked={gridLayout === GridLayout.List}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.badge.title')}</FormLabel>
|
||||
<FormLabel sx={{ mt: 2 }}>{t`Badges`}</FormLabel>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.badge.label.unread_badges')}
|
||||
label={t`Unread badges`}
|
||||
checked={showUnreadBadge}
|
||||
onChange={() => updateMetadataServerSettings('showUnreadBadge', !showUnreadBadge)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.badge.label.download_badges')}
|
||||
label={t`Download badges`}
|
||||
checked={showDownloadBadge}
|
||||
onChange={() => updateMetadataServerSettings('showDownloadBadge', !showDownloadBadge)}
|
||||
/>
|
||||
|
||||
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.tab.title')}</FormLabel>
|
||||
<FormLabel sx={{ mt: 2 }}>{t`Tabs`}</FormLabel>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.tab.label.show_number_of_items')}
|
||||
label={t`Show number of items`}
|
||||
checked={showTabSize}
|
||||
onChange={() => setSettingValue('showTabSize', !showTabSize)}
|
||||
/>
|
||||
|
||||
<FormLabel sx={{ mt: 2 }}>{t('global.label.other')}</FormLabel>
|
||||
<FormLabel sx={{ mt: 2 }}>{t`Other`}</FormLabel>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.other.label.show_continue_reading_button')}
|
||||
label={t`Show continue reading button`}
|
||||
checked={showContinueReadingButton}
|
||||
onChange={() =>
|
||||
updateMetadataServerSettings(
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import FilterList from '@mui/icons-material/FilterList';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { ComponentProps, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { LibraryOptionsPanel } from '@/features/library/components/LibraryOptionsPanel.tsx';
|
||||
import { getCategoryMetadata } from '@/features/category/services/CategoryMetadata.ts';
|
||||
@@ -19,7 +19,7 @@ export const LibraryToolbarMenu = ({
|
||||
}: {
|
||||
category: ComponentProps<typeof LibraryOptionsPanel>['category'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const options = getCategoryMetadata(category);
|
||||
@@ -34,7 +34,7 @@ export const LibraryToolbarMenu = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomTooltip title={t('settings.title')}>
|
||||
<CustomTooltip title={t`Settings`}>
|
||||
<IconButton onClick={() => setOpen(!open)} color={active ? 'warning' : 'inherit'}>
|
||||
<FilterList />
|
||||
</IconButton>
|
||||
|
||||
@@ -11,10 +11,11 @@ import Tab from '@mui/material/Tab';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueryParam, NumberParam, StringParam } from 'use-query-params';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
@@ -59,7 +60,7 @@ const TitleSizeTag = ({ sx, ...props }: ChipProps) => (
|
||||
);
|
||||
|
||||
export function Library() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
@@ -155,7 +156,7 @@ export function Library() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectionFAB selectedItemsCount={selectedItemIds.length} title="manga.title">
|
||||
<SelectionFAB title={plural(selectedItemIds.length, { one: '# manga', other: '# manga' })}>
|
||||
{(handleClose, setHideMenu) => (
|
||||
<MangaActionMenuItems
|
||||
selectedMangas={selectedMangas}
|
||||
@@ -181,7 +182,7 @@ export function Library() {
|
||||
to={AppRoutes.sources.childRoutes.searchAll.path(query)}
|
||||
sx={{ textTransform: 'none', width: '100%' }}
|
||||
>
|
||||
{t('library.action.label.search_globally', { query })}
|
||||
{t`Search for "${query}" globally`}
|
||||
</Button>
|
||||
</Box>
|
||||
),
|
||||
@@ -190,7 +191,7 @@ export function Library() {
|
||||
|
||||
useAppTitle(
|
||||
<TitleWithSizeTag>
|
||||
{t('library.title')}
|
||||
{t`Library`}
|
||||
{showTabSize && (
|
||||
<TitleSizeTag
|
||||
sx={{ ...theme.applyStyles('light', { backgroundColor: 'background.paper' }) }}
|
||||
@@ -198,7 +199,7 @@ export function Library() {
|
||||
/>
|
||||
)}
|
||||
</TitleWithSizeTag>,
|
||||
t('library.title'),
|
||||
t`Library`,
|
||||
[t, showTabSize, librarySize],
|
||||
);
|
||||
useAppAction(
|
||||
@@ -240,7 +241,7 @@ export function Library() {
|
||||
if (tabsError != null || librarySizeResponse.error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={tabsError?.message ?? librarySizeResponse.error?.message}
|
||||
retry={() => {
|
||||
if (tabsError) {
|
||||
@@ -260,7 +261,7 @@ export function Library() {
|
||||
}
|
||||
|
||||
if (tabs.length === 0) {
|
||||
return <EmptyViewAbsoluteCentered message={t('library.error.label.empty')} />;
|
||||
return <EmptyViewAbsoluteCentered message={t`Your library is empty`} />;
|
||||
}
|
||||
|
||||
if (tabs.length === 1) {
|
||||
@@ -271,7 +272,7 @@ export function Library() {
|
||||
// the key needs to include filters and query to force a re-render of the virtuoso grid to prevent https://github.com/petyosi/react-virtuoso/issues/1242
|
||||
key={filterKey}
|
||||
mangas={mangas}
|
||||
message={mangaError ? t('manga.error.label.request_failure') : t('library.error.label.empty')}
|
||||
message={mangaError ? t`Could not load manga` : t`Your library is empty`}
|
||||
messageExtra={mangaError?.message}
|
||||
isLoading={mangaLoading}
|
||||
selectedMangaIds={selectedItemIds}
|
||||
@@ -310,9 +311,7 @@ export function Library() {
|
||||
// the key needs to include filters and query to force a re-render of the virtuoso grid to prevent https://github.com/petyosi/react-virtuoso/issues/1242
|
||||
key={filterKey}
|
||||
mangas={mangas}
|
||||
message={
|
||||
mangaError ? t('manga.error.label.request_failure') : t('category.error.label.empty')
|
||||
}
|
||||
message={mangaError ? t`Could not load manga` : t`The category is empty`}
|
||||
messageExtra={mangaError?.message}
|
||||
isLoading={mangaLoading}
|
||||
selectedMangaIds={selectedItemIds}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
@@ -15,6 +14,7 @@ import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { useLocalStorage } from '@/base/hooks/useStorage.tsx';
|
||||
import { GridLayouts } from '@/base/components/GridLayouts.tsx';
|
||||
@@ -37,7 +37,7 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts';
|
||||
|
||||
export const LibraryDuplicates = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [gridLayout, setGridLayout] = useLocalStorage('libraryDuplicatesGridLayout', GridLayout.List);
|
||||
const [checkAlternativeTitles, setCheckAlternativeTitles] = useLocalStorage(
|
||||
@@ -46,7 +46,7 @@ export const LibraryDuplicates = () => {
|
||||
);
|
||||
|
||||
useAppTitleAndAction(
|
||||
t('library.settings.advanced.duplicates.label.title'),
|
||||
t`Duplicated entries`,
|
||||
<>
|
||||
<GridLayouts gridLayout={gridLayout} onChange={setGridLayout} />
|
||||
<PopupState variant="popover" popupId="library-dupliactes-settings">
|
||||
@@ -58,7 +58,7 @@ export const LibraryDuplicates = () => {
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem>
|
||||
<CheckboxInput
|
||||
label={t('library.settings.advanced.duplicates.settings.label.check_description')}
|
||||
label={t`Check description`}
|
||||
checked={checkAlternativeTitles}
|
||||
onChange={(_, checked) => setCheckAlternativeTitles(checked)}
|
||||
/>
|
||||
@@ -130,7 +130,7 @@ export const LibraryDuplicates = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('LibraryDuplicates::refetch'))}
|
||||
/>
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import { t as translate } from 'i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural, t as translate } from '@lingui/core/macro';
|
||||
import { GlobalUpdateSettings } from '@/features/settings/components/globalUpdate/GlobalUpdateSettings.tsx';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import {
|
||||
@@ -56,16 +56,16 @@ const removeNonLibraryMangasFromCategories = async (): Promise<void> => {
|
||||
clearCategories: true,
|
||||
}).response;
|
||||
}
|
||||
makeToast(translate('library.settings.advanced.database.cleanup.label.success'), 'success');
|
||||
makeToast(translate`Removed non library manga from categories`, 'success');
|
||||
} catch (e) {
|
||||
makeToast(translate('library.settings.advanced.database.cleanup.label.error'), 'error', getErrorMessage(e));
|
||||
makeToast(translate`Could not remove non library manga from categories`, 'error', getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
export function LibrarySettings() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
useAppTitle(t('library.title'));
|
||||
useAppTitle(t`Library`);
|
||||
|
||||
const categories = requestManager.useGetCategories<GetCategoriesSettingsQuery, GetCategoriesSettingsQueryVariables>(
|
||||
GET_CATEGORIES_SETTINGS,
|
||||
@@ -78,7 +78,7 @@ export function LibrarySettings() {
|
||||
} = useMetadataServerSettings();
|
||||
|
||||
const setSettingValue = createUpdateMetadataServerSettings<keyof MetadataLibrarySettings>((e) =>
|
||||
makeToast(t('search.error.label.failed_to_save_settings'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Could not save the default search settings to the server`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
|
||||
// -1 for the DEFAULT category
|
||||
@@ -93,7 +93,7 @@ export function LibrarySettings() {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => {
|
||||
if (serverSettings.error) {
|
||||
@@ -121,20 +121,23 @@ export function LibrarySettings() {
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader component="div" id="library-category-settings">
|
||||
{t('category.title.category_other')}
|
||||
{t`Categories`}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItemLink to={AppRoutes.settings.childRoutes.categories.path}>
|
||||
<ListItemText
|
||||
primary={t('category.dialog.title.edit_category_other')}
|
||||
secondary={t('category.value', { count: categoryCount })}
|
||||
primary={t`Edit categories`}
|
||||
secondary={plural(categoryCount, {
|
||||
one: '# category',
|
||||
other: '# categories',
|
||||
})}
|
||||
/>
|
||||
</ListItemLink>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t('library.settings.general.add_to_library.category_selection.label.title')}
|
||||
secondary={t('library.settings.general.add_to_library.category_selection.label.description')}
|
||||
primary={t`Category selection dialog`}
|
||||
secondary={t`Show the category selection dialog when adding a manga to the library`}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -144,10 +147,8 @@ export function LibrarySettings() {
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t('library.settings.general.remove_from_library.remove_from_categories.label.title')}
|
||||
secondary={t(
|
||||
'library.settings.general.remove_from_library.remove_from_categories.label.description',
|
||||
)}
|
||||
primary={t`Forget manga categories`}
|
||||
secondary={t`Remove manga from categories when removing them from the library`}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -159,14 +160,14 @@ export function LibrarySettings() {
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader component="div" id="library-general-settings">
|
||||
{t('global.label.general')}
|
||||
{t`General`}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t('library.settings.general.search.ignore_filters.label.title')}
|
||||
secondary={t('library.settings.general.search.ignore_filters.label.description')}
|
||||
primary={t`Ignore filters when searching`}
|
||||
secondary={t`Search results will include manga that do not match the current filters`}
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
@@ -182,20 +183,20 @@ export function LibrarySettings() {
|
||||
<List
|
||||
subheader={
|
||||
<ListSubheader component="div" id="library-advanced">
|
||||
{t('global.label.advanced')}
|
||||
{t`Advanced`}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItemButton onClick={() => removeNonLibraryMangasFromCategories()}>
|
||||
<ListItemText
|
||||
primary={t('library.settings.advanced.database.cleanup.label.title')}
|
||||
secondary={t('library.settings.advanced.database.cleanup.label.description')}
|
||||
primary={t`Cleanup database`}
|
||||
secondary={t`Remove non library manga from categories`}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<ListItemLink to={AppRoutes.settings.childRoutes.library.childRoutes.duplicates.path}>
|
||||
<ListItemText
|
||||
primary={t('library.settings.advanced.duplicates.label.title')}
|
||||
secondary={t('library.settings.advanced.duplicates.label.description')}
|
||||
primary={t`Duplicated entries`}
|
||||
secondary={t`Show all duplicated entries in your library`}
|
||||
/>
|
||||
</ListItemLink>
|
||||
</List>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { MangaStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { MangaAction, MangaIdInfo, MangaType } from '@/features/manga/Manga.types.ts';
|
||||
import {
|
||||
@@ -13,7 +15,6 @@ import {
|
||||
CHAPTER_ACTION_TO_TRANSLATION,
|
||||
} from '@/features/chapter/Chapter.constants.ts';
|
||||
import { GqlMetaHolder } from '@/features/metadata/Metadata.types.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
export const FALLBACK_MANGA: MangaIdInfo & GqlMetaHolder = { id: -1 };
|
||||
|
||||
@@ -21,14 +22,14 @@ export const GLOBAL_READER_SETTINGS_MANGA: MangaIdInfo = { id: -2 };
|
||||
|
||||
export const MANGA_COVER_ASPECT_RATIO = '1 / 1.5';
|
||||
|
||||
export const MANGA_STATUS_TO_TRANSLATION: Record<MangaStatus, TranslationKey> = {
|
||||
[MangaStatus.Cancelled]: 'manga.status.cancelled',
|
||||
[MangaStatus.Completed]: 'manga.status.completed',
|
||||
[MangaStatus.Licensed]: 'manga.status.licensed',
|
||||
[MangaStatus.Ongoing]: 'manga.status.ongoing',
|
||||
[MangaStatus.OnHiatus]: 'manga.status.hiatus',
|
||||
[MangaStatus.PublishingFinished]: 'manga.status.publishing_finished',
|
||||
[MangaStatus.Unknown]: 'manga.status.unknown',
|
||||
export const MANGA_STATUS_TO_TRANSLATION: Record<MangaStatus, MessageDescriptor> = {
|
||||
[MangaStatus.Cancelled]: msg`Cancelled`,
|
||||
[MangaStatus.Completed]: msg`Completed`,
|
||||
[MangaStatus.Licensed]: msg`Licensed`,
|
||||
[MangaStatus.Ongoing]: msg`Ongoing`,
|
||||
[MangaStatus.OnHiatus]: msg`Hiatus`,
|
||||
[MangaStatus.PublishingFinished]: msg`Publishing finished`,
|
||||
[MangaStatus.Unknown]: msg`Unknown`,
|
||||
};
|
||||
|
||||
export const MANGA_ACTION_TO_CONFIRMATION_REQUIRED: Record<
|
||||
@@ -45,48 +46,48 @@ export const MANGA_ACTION_TO_CONFIRMATION_REQUIRED: Record<
|
||||
export const MANGA_ACTION_TO_TRANSLATION: {
|
||||
[key in MangaAction]: {
|
||||
action: {
|
||||
single: TranslationKey;
|
||||
selected: TranslationKey;
|
||||
single: MessageDescriptor;
|
||||
selected: MessageDescriptor;
|
||||
};
|
||||
confirmation?: TranslationKey;
|
||||
success: TranslationKey;
|
||||
error: TranslationKey;
|
||||
confirmation?: MessageDescriptor;
|
||||
success: MessageDescriptor;
|
||||
error: MessageDescriptor;
|
||||
};
|
||||
} = {
|
||||
...CHAPTER_ACTION_TO_TRANSLATION,
|
||||
remove_from_library: {
|
||||
action: {
|
||||
single: 'manga.action.library.remove.label.action',
|
||||
selected: 'manga.action.library.remove.button.selected',
|
||||
single: msg`Remove from the library`,
|
||||
selected: msg`Remove selected from the library`,
|
||||
},
|
||||
confirmation: 'manga.action.library.remove.label.confirmation',
|
||||
success: 'manga.action.library.remove.label.success',
|
||||
error: 'manga.action.library.remove.label.error',
|
||||
confirmation: msg`{count, plural, one {You are about to remove one entry from your library} other {You are about to remove # entries from your library}}`,
|
||||
success: msg`{count, plural, one {Removed manga from the library} other {Removed # manga from the library}}`,
|
||||
error: msg`{count, plural, one {Could not remove manga from the library} other {Could not remove manga from the library}}`,
|
||||
},
|
||||
change_categories: {
|
||||
action: {
|
||||
single: 'manga.action.category.label.action',
|
||||
selected: 'manga.action.category.button.selected',
|
||||
single: msg`Change categories`,
|
||||
selected: msg`Change categories of selected`,
|
||||
},
|
||||
confirmation: 'manga.action.category.label.confirmation',
|
||||
success: 'manga.action.category.label.success',
|
||||
error: 'manga.action.category.label.error',
|
||||
confirmation: msg`{count, plural, one {You are about to change the category of one entry} other {You are about to change the category of # entries}}`,
|
||||
success: msg`{count, plural, one {Changed categories of manga} other {Changed categories of # manga}}`,
|
||||
error: msg`{count, plural, one {Could not change the categories of the manga} other {Could not change the categories of the manga}}`,
|
||||
},
|
||||
migrate: {
|
||||
action: {
|
||||
single: 'global.button.migrate',
|
||||
selected: 'global.button.migrate', // not supported
|
||||
single: msg`Migrate`,
|
||||
selected: msg`Migrate`, // not supported
|
||||
},
|
||||
success: 'manga.action.migrate.label.success',
|
||||
error: 'manga.action.migrate.label.error',
|
||||
success: msg`Successfully migrated manga`,
|
||||
error: msg`Could not migrate manga`,
|
||||
},
|
||||
track: {
|
||||
action: {
|
||||
single: 'manga.action.track.add.label.action',
|
||||
selected: 'manga.action.track.add.label.action', // not supported
|
||||
single: msg`Track`,
|
||||
selected: msg`Track`, // not supported
|
||||
},
|
||||
success: 'manga.action.track.add.label.success',
|
||||
error: 'manga.action.track.add.label.error',
|
||||
success: msg`Tracked manga`,
|
||||
error: msg`Could not track manga`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -151,10 +152,10 @@ export const SOURCES_BY_MANGA_TYPE: Record<MangaType, string[]> = {
|
||||
],
|
||||
};
|
||||
|
||||
export const MANGA_TAGS_BY_MANGA_TYPE: Record<MangaType, TranslationKey[]> = {
|
||||
[MangaType.MANGA]: ['manga.type.manga'],
|
||||
[MangaType.COMIC]: ['manga.type.comic'],
|
||||
[MangaType.WEBTOON]: ['manga.type.webtoon', 'manga.type.long_strip'],
|
||||
[MangaType.MANHWA]: ['manga.type.manhwa', 'manga.type.long_strip'],
|
||||
[MangaType.MANHUA]: ['manga.type.manhua', 'manga.type.long_strip'],
|
||||
export const MANGA_TAGS_BY_MANGA_TYPE: Record<MangaType, MessageDescriptor[]> = {
|
||||
[MangaType.MANGA]: [msg`Manga`],
|
||||
[MangaType.COMIC]: [msg`Comic`],
|
||||
[MangaType.WEBTOON]: [msg`Webtoon`, msg`Long strip`],
|
||||
[MangaType.MANHWA]: [msg`Manhwa`, msg`Long strip`],
|
||||
[MangaType.MANHUA]: [msg`Manhua`, msg`Long strip`],
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip';
|
||||
import {
|
||||
ChapterNameInfo,
|
||||
@@ -28,7 +28,7 @@ export const ContinueReadingTooltip = ({
|
||||
ChapterScanlatorInfo & {
|
||||
children: ReactElement;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const isFirstChapter = sourceOrder === 1;
|
||||
|
||||
@@ -41,11 +41,11 @@ export const ContinueReadingTooltip = ({
|
||||
},
|
||||
},
|
||||
}}
|
||||
title={t(isFirstChapter ? 'chapter.action.read.start' : 'chapter.action.read.resume', {
|
||||
chapterNumber,
|
||||
title: name,
|
||||
scanlator,
|
||||
})}
|
||||
title={
|
||||
isFirstChapter
|
||||
? t`Start reading\n#${chapterNumber} — ${name}\n${scanlator}`
|
||||
: t`Continue reading\n#${chapterNumber} — ${name}\n${scanlator}`
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</CustomTooltip>
|
||||
|
||||
@@ -11,7 +11,6 @@ import Delete from '@mui/icons-material/Delete';
|
||||
import Download from '@mui/icons-material/Download';
|
||||
import RemoveDone from '@mui/icons-material/RemoveDone';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||
import Label from '@mui/icons-material/Label';
|
||||
import { useMemo, useState } from 'react';
|
||||
@@ -20,6 +19,7 @@ import { Link } from 'react-router-dom';
|
||||
import SyncIcon from '@mui/icons-material/Sync';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { AwaitableComponent } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
import { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
|
||||
import { MenuItem } from '@/base/components/menu/MenuItem.tsx';
|
||||
@@ -60,7 +60,7 @@ export const MangaActionMenuItems = ({
|
||||
onClose,
|
||||
setHideMenu,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [isTrackDialogOpen, setIsTrackDialogOpen] = useState(false);
|
||||
|
||||
@@ -108,7 +108,7 @@ export const MangaActionMenuItems = ({
|
||||
return (
|
||||
<>
|
||||
{!!handleSelection && isSingleMode && (
|
||||
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
|
||||
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t`Select`} />
|
||||
)}
|
||||
{shouldShowMenuItem(!isFullyDownloaded) && (
|
||||
<NestedMenuItem
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { MangaCardMode } from '@/features/manga/Manga.types.ts';
|
||||
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
||||
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||
@@ -42,7 +42,7 @@ export const MangaBadges = ({
|
||||
downloadCount?: number;
|
||||
mode: MangaCardMode;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
@@ -69,7 +69,7 @@ export const MangaBadges = ({
|
||||
}}
|
||||
color={isInLibrary ? 'error' : 'primary'}
|
||||
>
|
||||
{t(isInLibrary ? 'manga.action.library.remove.label.action' : 'manga.button.add_to_library')}
|
||||
{isInLibrary ? t`Remove from the library` : t`Add To Library`}
|
||||
</Button>
|
||||
)}
|
||||
{inLibraryIndicator && isInLibrary && (
|
||||
@@ -77,7 +77,7 @@ export const MangaBadges = ({
|
||||
className="source-manga-library-state-indicator"
|
||||
sx={{ backgroundColor: 'primary.dark', color: 'primary.contrastText', p: 0.3 }}
|
||||
>
|
||||
{t('manga.button.in_library')}
|
||||
{t`In Library`}
|
||||
</Typography>
|
||||
)}
|
||||
{((showUnreadBadge && mode === 'default') || mode === 'duplicate') && (unread ?? 0) > 0 && (
|
||||
|
||||
@@ -10,7 +10,7 @@ import React, { ForwardedRef, Ref, useCallback, useLayoutEffect, useMemo, useRef
|
||||
import Grid, { GridTypeMap } from '@mui/material/Grid';
|
||||
import Box, { BoxProps } from '@mui/material/Box';
|
||||
import { GridItemProps } from 'react-virtuoso';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { MangaCard } from '@/features/manga/components/cards/MangaCard.tsx';
|
||||
@@ -209,7 +209,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = ({
|
||||
retry,
|
||||
gridWrapperProps,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { navBarWidth } = useNavBarContext();
|
||||
const {
|
||||
@@ -313,7 +313,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = ({
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
noFaces={noFaces}
|
||||
message={message ?? t('manga.error.label.no_mangas_found')}
|
||||
message={message ?? t`No manga found`}
|
||||
messageExtra={messageExtra}
|
||||
retry={retry}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BaseSyntheticEvent, ChangeEvent, useMemo, ForwardedRef } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
@@ -14,6 +13,7 @@ import IconButton from '@mui/material/IconButton';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { PopupState } from 'material-ui-popup-state/hooks';
|
||||
import { bindTrigger } from 'material-ui-popup-state';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -34,7 +34,7 @@ export const MangaOptionButton = ({
|
||||
popupState: PopupState;
|
||||
ref?: ForwardedRef<HTMLButtonElement | null>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]);
|
||||
|
||||
@@ -59,7 +59,7 @@ export const MangaOptionButton = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t(selected ? 'global.button.deselect' : 'global.button.select')}>
|
||||
<CustomTooltip title={selected ? t`Deselect` : t`Select`}>
|
||||
<Checkbox {...MUIUtil.preventRippleProp()} checked={selected} onChange={handleSelectionChange} />
|
||||
</CustomTooltip>
|
||||
);
|
||||
@@ -67,7 +67,7 @@ export const MangaOptionButton = ({
|
||||
|
||||
if (asCheckbox) {
|
||||
return (
|
||||
<CustomTooltip title={t('global.button.options')}>
|
||||
<CustomTooltip title={t`Options`}>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
|
||||
@@ -80,7 +80,7 @@ export const MangaOptionButton = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t('global.button.options')}>
|
||||
<CustomTooltip title={t`Options`}>
|
||||
<Button
|
||||
ref={ref}
|
||||
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
|
||||
|
||||
@@ -15,13 +15,13 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import SyncAltIcon from '@mui/icons-material/SyncAlt';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { AwaitableComponent } from 'awaitable-component';
|
||||
import ColorLensIcon from '@mui/icons-material/ColorLens';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
@@ -40,7 +40,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const theme = useTheme();
|
||||
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
@@ -78,7 +78,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<>
|
||||
{isLargeScreen && (
|
||||
<>
|
||||
<CustomTooltip title={t('manga.label.reload_from_source')} disabled={refreshing}>
|
||||
<CustomTooltip title={t`Reload data from source`} disabled={refreshing}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onRefresh();
|
||||
@@ -90,7 +90,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
{settings.mangaDynamicColorSchemes && (
|
||||
<CustomTooltip title={t('settings.appearance.manga_dynamic_color_schemes.save')}>
|
||||
<CustomTooltip title={t`Save dynamic color theme`}>
|
||||
<IconButton onClick={saveDynamicColorTheme} color="inherit">
|
||||
<ColorLensIcon />
|
||||
</IconButton>
|
||||
@@ -98,7 +98,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
)}
|
||||
{manga.inLibrary && (
|
||||
<>
|
||||
<CustomTooltip title={t('global.button.migrate')}>
|
||||
<CustomTooltip title={t`Migrate`}>
|
||||
<Link
|
||||
to={AppRoutes.migrate.childRoutes.search.path(
|
||||
manga.sourceId,
|
||||
@@ -113,7 +113,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
</IconButton>
|
||||
</Link>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('manga.label.edit_categories')}>
|
||||
<CustomTooltip title={t`Edit manga categories`}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
openCategorySelection();
|
||||
@@ -158,14 +158,14 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<ListItemIcon>
|
||||
<Refresh fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('manga.label.reload_from_source')}</ListItemText>
|
||||
<ListItemText>{t`Reload data from source`}</ListItemText>
|
||||
</MenuItem>
|
||||
{settings.mangaDynamicColorSchemes && (
|
||||
<MenuItem onClick={saveDynamicColorTheme}>
|
||||
<ListItemIcon>
|
||||
<ColorLensIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settings.appearance.manga_dynamic_color_schemes.save')}</ListItemText>
|
||||
<ListItemText>{t`Save dynamic color theme`}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{manga.inLibrary && [
|
||||
@@ -179,7 +179,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<ListItemIcon>
|
||||
<SyncAltIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('migrate.title')}</ListItemText>
|
||||
<ListItemText>{t`Migrate`}</ListItemText>
|
||||
</MenuItem>,
|
||||
<MenuItem
|
||||
key="categories"
|
||||
@@ -191,7 +191,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<ListItemIcon>
|
||||
<Label fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('manga.label.edit_categories')}</ListItemText>
|
||||
<ListItemText>{t`Edit manga categories`}</ListItemText>
|
||||
</MenuItem>,
|
||||
]}
|
||||
</Menu>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import PlayArrow from '@mui/icons-material/PlayArrow';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { StyledFab } from '@/base/components/buttons/StyledFab.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ export function ResumeFab({
|
||||
ChapterNameInfo &
|
||||
ChapterScanlatorInfo;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { sourceOrder, name, chapterNumber, scanlator } = chapter;
|
||||
const isFirstChapter = sourceOrder === 1;
|
||||
@@ -51,7 +51,7 @@ export function ResumeFab({
|
||||
state={Chapters.getReaderOpenChapterLocationState(chapter)}
|
||||
>
|
||||
<PlayArrow />
|
||||
{isFirstChapter ? t('global.button.start') : t('global.button.resume')}
|
||||
{isFirstChapter ? t`Start` : t`Resume`}
|
||||
</StyledFab>
|
||||
</ContinueReadingTooltip>
|
||||
);
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import SyncIcon from '@mui/icons-material/Sync';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PopupState, { bindDialog, bindTrigger } from 'material-ui-popup-state';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { TrackManga } from '@/features/tracker/components/TrackManga.tsx';
|
||||
@@ -23,7 +24,7 @@ import { MangaTrackRecordInfo } from '@/features/manga/Manga.types.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
|
||||
export const TrackMangaButton = ({ manga }: { manga: MangaTrackRecordInfo & Pick<MangaType, 'title'> }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
|
||||
@@ -35,7 +36,7 @@ export const TrackMangaButton = ({ manga }: { manga: MangaTrackRecordInfo & Pick
|
||||
|
||||
const handleClick = (openPopup: () => void) => {
|
||||
if (trackerList.error) {
|
||||
makeToast(t('tracking.error.label.could_not_load_track_info'), 'error', trackerList.error?.toString());
|
||||
makeToast(t`Could not load track info`, 'error', trackerList.error?.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,8 +61,11 @@ export const TrackMangaButton = ({ manga }: { manga: MangaTrackRecordInfo & Pick
|
||||
>
|
||||
{trackersInUse.length ? <CheckIcon /> : <SyncIcon />}
|
||||
{trackersInUse.length
|
||||
? t('manga.button.track.active', { count: trackersInUse.length })
|
||||
: t('manga.button.track.start')}
|
||||
? plural(trackersInUse.length, {
|
||||
one: '# Tracker',
|
||||
other: '# Tracker',
|
||||
})
|
||||
: t`Tracking`}
|
||||
</CustomButton>
|
||||
{popupState.isOpen && (
|
||||
<Dialog {...bindDialog(popupState)} maxWidth="md" fullWidth scroll="paper">
|
||||
|
||||
@@ -10,14 +10,14 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { ComponentProps, ReactNode, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { t as translate } from 'i18next';
|
||||
import Link from '@mui/material/Link';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { t as translate } from '@lingui/core/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
@@ -136,11 +136,11 @@ const MangaButtonsContainer = styled('div')(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const OpenSourceButton = ({ url }: { url?: string | null }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<CustomTooltip title={t('global.button.open_browser')} disabled={!url}>
|
||||
<CustomTooltip title={t`Open in browser`} disabled={!url}>
|
||||
<CustomButtonIcon
|
||||
size="medium"
|
||||
disabled={!url}
|
||||
@@ -153,7 +153,7 @@ const OpenSourceButton = ({ url }: { url?: string | null }) => {
|
||||
<IconBrowser />
|
||||
</CustomButtonIcon>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('global.button.open_webview')} disabled={!url}>
|
||||
<CustomTooltip title={t`Open in WebView`} disabled={!url}>
|
||||
<CustomButtonIcon
|
||||
size="medium"
|
||||
disabled={!url}
|
||||
@@ -172,11 +172,11 @@ const OpenSourceButton = ({ url }: { url?: string | null }) => {
|
||||
|
||||
function getSourceName(source?: Pick<SourceType, 'id' | 'displayName'> | null): string {
|
||||
if (!source) {
|
||||
return translate('global.label.unknown');
|
||||
return translate`Unknown`;
|
||||
}
|
||||
|
||||
if (Sources.isLocalSource(source)) {
|
||||
return translate('source.local_source.title');
|
||||
return translate`Local source`;
|
||||
}
|
||||
|
||||
return source.displayName ?? source.id;
|
||||
@@ -215,7 +215,7 @@ export const MangaDetails = ({
|
||||
};
|
||||
mode: MangaLocationState['mode'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const {
|
||||
settings: { mangaThumbnailBackdrop, mangaDynamicColorSchemes },
|
||||
@@ -223,16 +223,16 @@ export const MangaDetails = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!manga.source) {
|
||||
makeToast(translate('source.error.label.source_not_found'), 'error');
|
||||
makeToast(t`Could not find source. Check your installed extensions.`, 'error');
|
||||
}
|
||||
}, [manga.source]);
|
||||
}, [manga.source, t]);
|
||||
|
||||
const { updateLibraryState } = useManageMangaLibraryState(manga);
|
||||
|
||||
const copyTitle = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(manga.title);
|
||||
makeToast(t('global.label.copied_clipboard'), 'info');
|
||||
makeToast(t`Copied to clipboard`, 'info');
|
||||
} catch (e) {
|
||||
defaultPromiseErrorHandler('MangaDetails::copyTitleLongPress')(e);
|
||||
}
|
||||
@@ -250,7 +250,7 @@ export const MangaDetails = ({
|
||||
{manga.title}
|
||||
</Typography>
|
||||
</SearchLink>
|
||||
<CustomTooltip title={t('global.button.copy')}>
|
||||
<CustomTooltip title={t`Copy`}>
|
||||
<IconButton onClick={copyTitle} color="inherit">
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -258,21 +258,18 @@ export const MangaDetails = ({
|
||||
</Stack>
|
||||
{manga.author && (
|
||||
<Metadata
|
||||
title={t('manga.label.author')}
|
||||
title={t`Author`}
|
||||
value={valuesToJoinedSearchLinks(Mangas.getAuthors(manga), manga.source?.id, mode)}
|
||||
/>
|
||||
)}
|
||||
{manga.artist && (
|
||||
<Metadata
|
||||
title={t('manga.label.artist')}
|
||||
title={t`Artist`}
|
||||
value={valuesToJoinedSearchLinks(Mangas.getArtists(manga), manga.source?.id, mode)}
|
||||
/>
|
||||
)}
|
||||
<Metadata
|
||||
title={t('manga.label.status')}
|
||||
value={t(MANGA_STATUS_TO_TRANSLATION[manga.status])}
|
||||
/>
|
||||
<Metadata title={t('source.title_one')} value={getSourceName(manga.source)} />
|
||||
<Metadata title={t`Status`} value={t(MANGA_STATUS_TO_TRANSLATION[manga.status])} />
|
||||
<Metadata title={t`Source`} value={getSourceName(manga.source)} />
|
||||
</MetadataContainer>
|
||||
</ThumbnailMetadataWrapper>
|
||||
<MangaButtonsContainer>
|
||||
@@ -282,7 +279,7 @@ export const MangaDetails = ({
|
||||
variant={manga.inLibrary ? 'contained' : 'outlined'}
|
||||
>
|
||||
{manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
|
||||
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
|
||||
{manga.inLibrary ? t`In Library` : t`Add To Library`}
|
||||
</CustomButton>
|
||||
<TrackMangaButton manga={manga} />
|
||||
<OpenSourceButton url={manga.realUrl} />
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import gql from 'graphql-tag';
|
||||
import { AwaitableComponent } from 'awaitable-component';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||
@@ -27,7 +27,7 @@ export const useManageMangaLibraryState = (
|
||||
manga: Pick<MangaType, 'id' | 'title'> & Partial<Pick<MangaType, 'inLibrary'>>,
|
||||
confirmRemoval: boolean = false,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [isInLibrary, setIsInLibrary] = useState(!!manga.inLibrary);
|
||||
|
||||
@@ -42,10 +42,10 @@ export const useManageMangaLibraryState = (
|
||||
updateManga: { inLibrary: true },
|
||||
updateMangaCategories: { addToCategories, removeFromCategories },
|
||||
})
|
||||
.response.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
|
||||
.response.then(() => makeToast(t`Added manga to library!`, 'success'))
|
||||
.then(() => setIsInLibrary(true))
|
||||
.catch((e) => {
|
||||
makeToast(t('library.error.label.add_to_library'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not add manga to library!`, 'error', getErrorMessage(e));
|
||||
});
|
||||
},
|
||||
[manga.id],
|
||||
@@ -55,10 +55,12 @@ export const useManageMangaLibraryState = (
|
||||
if (confirmRemoval) {
|
||||
await Confirmation.show(
|
||||
{
|
||||
title: t('global.label.are_you_sure'),
|
||||
message: t('manga.action.library.remove.dialog.label.message', { title: manga.title }),
|
||||
title: t`Are you sure?`,
|
||||
message: t`You are about to remove "${manga.title}" from your library`,
|
||||
actions: {
|
||||
confirm: { title: t('global.button.remove') },
|
||||
confirm: {
|
||||
title: t`Remove`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ id: `manga-library-state-remove-${manga.id}` },
|
||||
@@ -83,7 +85,7 @@ export const useManageMangaLibraryState = (
|
||||
showAddToLibraryCategorySelectDialog = (await getMetadataServerSettings())
|
||||
.showAddToLibraryCategorySelectDialog;
|
||||
} catch (e) {
|
||||
makeToast(t('global.error.label.failed_to_load_data'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Unable to load data`, 'error', getErrorMessage(e));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ export const useManageMangaLibraryState = (
|
||||
GetCategoriesBaseQueryVariables
|
||||
>(GET_CATEGORIES_BASE).response;
|
||||
} catch (e) {
|
||||
makeToast(t('category.error.label.request_failure'), 'error', getErrorMessage(e));
|
||||
makeToast(t`Could not load categories`, 'error', getErrorMessage(e));
|
||||
return;
|
||||
}
|
||||
const userCreatedCategories = Categories.getUserCreated(categories.data.categories.nodes);
|
||||
@@ -109,15 +111,20 @@ export const useManageMangaLibraryState = (
|
||||
try {
|
||||
duplicatedLibraryMangas = await Mangas.getDuplicateLibraryMangas(manga.title).response;
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorMessage(e);
|
||||
await Confirmation.show(
|
||||
{
|
||||
title: t('global.error.label.failed_to_load_data'),
|
||||
message: t('manga.action.library.add.dialog.duplicate.label.failure', {
|
||||
error: getErrorMessage(e),
|
||||
}),
|
||||
title: t`Unable to load data`,
|
||||
message: t`Could not check for duplicated manga in your library.\n\nError: ${errorMessage}`,
|
||||
actions: {
|
||||
extra: { show: true, title: t('global.button.retry'), contain: true },
|
||||
confirm: { title: t('global.button.add') },
|
||||
extra: {
|
||||
show: true,
|
||||
title: t`Retry`,
|
||||
contain: true,
|
||||
},
|
||||
confirm: {
|
||||
title: t`Add`,
|
||||
},
|
||||
},
|
||||
onExtra: () =>
|
||||
update().catch(
|
||||
@@ -132,16 +139,18 @@ export const useManageMangaLibraryState = (
|
||||
if (doDuplicatesExist) {
|
||||
await Confirmation.show(
|
||||
{
|
||||
title: t('global.label.are_you_sure'),
|
||||
message: t('manga.action.library.add.dialog.duplicate.label.info'),
|
||||
title: t`Are you sure?`,
|
||||
message: t`You have an entry in your library with the same name.`,
|
||||
actions: {
|
||||
extra: {
|
||||
show: true,
|
||||
title: t('migrate.dialog.action.button.show_entry'),
|
||||
title: t`Show entry`,
|
||||
contain: true,
|
||||
link: AppRoutes.manga.path(duplicatedLibraryMangas!.data.mangas.nodes[0].id),
|
||||
},
|
||||
confirm: { title: t('global.button.add') },
|
||||
confirm: {
|
||||
title: t`Add`,
|
||||
},
|
||||
},
|
||||
onExtra: () => {},
|
||||
},
|
||||
|
||||
@@ -12,9 +12,9 @@ import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Box from '@mui/material/Box';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { isNetworkRequestInFlight } from '@apollo/client/core/networkStatus';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ChapterList } from '@/features/chapter/components/ChapterList.tsx';
|
||||
@@ -30,7 +30,7 @@ import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitl
|
||||
import { MangaLocationState } from '@/features/manga/Manga.types.ts';
|
||||
|
||||
export const Manga: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { mode } = useLocation<MangaLocationState>().state ?? {};
|
||||
|
||||
@@ -61,7 +61,7 @@ export const Manga: React.FC = () => {
|
||||
}, [manga]);
|
||||
|
||||
useAppTitleAndAction(
|
||||
manga?.title ?? t('manga.title_one'),
|
||||
manga?.title ?? t`Manga`,
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
@@ -72,7 +72,7 @@ export const Manga: React.FC = () => {
|
||||
<CustomTooltip
|
||||
title={
|
||||
<>
|
||||
{t('manga.error.label.request_failure')}
|
||||
{t`Could not load manga`}
|
||||
<br />
|
||||
{getErrorMessage(error)}
|
||||
</>
|
||||
@@ -94,12 +94,7 @@ export const Manga: React.FC = () => {
|
||||
);
|
||||
|
||||
if (error && !manga) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('manga.error.label.request_failure')}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
/>
|
||||
);
|
||||
return <EmptyViewAbsoluteCentered message={t`Could not load manga`} messageExtra={getErrorMessage(error)} />;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import i18next, { t as translate } from 'i18next';
|
||||
import { DocumentNode, Unmasked } from '@apollo/client/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { i18n } from '@/i18n';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import {
|
||||
ChapterConditionInput,
|
||||
@@ -557,11 +558,12 @@ export class Mangas {
|
||||
|
||||
try {
|
||||
await Confirmation.show({
|
||||
title: translate('global.label.are_you_sure'),
|
||||
message: translate(confirmationMessage, { count: itemCount }),
|
||||
title: t`Are you sure?`,
|
||||
/* lingui-extract-ignore */
|
||||
message: i18n.t({ ...confirmationMessage, values: { count: itemCount } }),
|
||||
actions: {
|
||||
confirm: {
|
||||
title: translate('global.button.ok'),
|
||||
title: t`Ok`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -571,10 +573,15 @@ export class Mangas {
|
||||
}
|
||||
|
||||
await fnToExecute();
|
||||
makeToast(translate(MANGA_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success');
|
||||
makeToast(
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...MANGA_ACTION_TO_TRANSLATION[action].success, values: { count: itemCount } }),
|
||||
'success',
|
||||
);
|
||||
} catch (e) {
|
||||
makeToast(
|
||||
translate(MANGA_ACTION_TO_TRANSLATION[action].error, { count: itemCount }),
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...MANGA_ACTION_TO_TRANSLATION[action].error, values: { count: itemCount } }),
|
||||
'error',
|
||||
getErrorMessage(e),
|
||||
);
|
||||
@@ -650,9 +657,14 @@ export class Mangas {
|
||||
const translateMangaTagsByMangaTypeEntries = Object.entries(MANGA_TAGS_BY_MANGA_TYPE).map(
|
||||
([mangaType, tags]) => [
|
||||
mangaType,
|
||||
['en', i18next.language, manga.source?.lang]
|
||||
['en', i18n.locale, manga.source?.lang]
|
||||
.filter((lng) => !!lng)
|
||||
.flatMap((language) => tags.flatMap((tag) => translate(tag, { lng: language }))),
|
||||
.flatMap((language) =>
|
||||
tags.flatMap((tag) =>
|
||||
/* lingui-extract-ignore */
|
||||
i18n.t({ ...tag, values: { lng: language } }),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
const translatedMangaTagsByMangaType = Object.fromEntries(translateMangaTagsByMangaTypeEntries) as Record<
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { SortBy, SortOrder } from '@/features/migration/Migration.types.ts';
|
||||
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
export const sortByToTranslationKey: Record<SortBy, TranslationKey> = {
|
||||
[SortBy.SOURCE_NAME]: 'migrate.sort.by_source_name',
|
||||
[SortBy.MANGA_COUNT]: 'migrate.sort.by_manga_count',
|
||||
export const sortByToTranslation: Record<SortBy, MessageDescriptor> = {
|
||||
[SortBy.SOURCE_NAME]: msg`By source name`,
|
||||
[SortBy.MANGA_COUNT]: msg`By manga count`,
|
||||
};
|
||||
|
||||
export const sortOrderToTranslationKey: Record<SortBy, TranslationKey> = {
|
||||
[SortOrder.ASC]: 'global.sort.label.asc',
|
||||
[SortOrder.DESC]: 'global.sort.label.desc',
|
||||
export const sortOrderToTranslation: Record<SortBy, MessageDescriptor> = {
|
||||
[SortOrder.ASC]: msg`Ascending`,
|
||||
[SortOrder.DESC]: msg`Descending`,
|
||||
};
|
||||
|
||||
export const DEFAULT_SORT_SETTINGS = {
|
||||
|
||||
@@ -11,11 +11,11 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
@@ -29,7 +29,7 @@ import { MigrateMode } from '@/features/manga/Manga.types.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
|
||||
export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrateTo: number; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -51,7 +51,7 @@ export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrat
|
||||
throw new Error(`MigrateDialog::migrate: unexpected mangaId "${mangaId}"`);
|
||||
}
|
||||
|
||||
makeToast(t('migrate.label.info'), 'info');
|
||||
makeToast(t`Migrating manga…`, 'info');
|
||||
|
||||
setIsMigrationInProcess(true);
|
||||
|
||||
@@ -72,30 +72,30 @@ export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrat
|
||||
|
||||
return (
|
||||
<Dialog open fullWidth onClose={onClose}>
|
||||
<DialogTitle>{t('migrate.dialog.title')}</DialogTitle>
|
||||
<DialogTitle>{t`Select data to include`}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
<CheckboxInput
|
||||
disabled={isMigrationInProcess}
|
||||
label={t('chapter.title_one')}
|
||||
label={t`Chapter`}
|
||||
checked={migrateChapters}
|
||||
onChange={(_, checked) => setMigrationFlag('migrateChapters', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
disabled={isMigrationInProcess}
|
||||
label={t('category.title.category_one')}
|
||||
label={t`Category`}
|
||||
checked={migrateCategories}
|
||||
onChange={(_, checked) => setMigrationFlag('migrateCategories', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
disabled={isMigrationInProcess}
|
||||
label={t('tracking.title')}
|
||||
label={t`Tracking`}
|
||||
checked={migrateTracking}
|
||||
onChange={(_, checked) => setMigrationFlag('migrateTracking', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
disabled={isMigrationInProcess}
|
||||
label={t('migrate.dialog.label.delete_downloaded')}
|
||||
label={t`Delete downloaded`}
|
||||
checked={deleteChapters}
|
||||
onChange={(_, checked) => setMigrationFlag('deleteChapters', checked)}
|
||||
/>
|
||||
@@ -114,17 +114,17 @@ export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrat
|
||||
component={Link}
|
||||
to={AppRoutes.manga.path(mangaIdToMigrateTo)}
|
||||
>
|
||||
{t('migrate.dialog.action.button.show_entry')}
|
||||
{t`Show entry`}
|
||||
</Button>
|
||||
<Stack direction="row">
|
||||
<Button disabled={isMigrationInProcess} onClick={onClose}>
|
||||
{t('global.button.cancel')}
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button disabled={isMigrationInProcess} onClick={() => migrate('copy')}>
|
||||
{t('global.button.copy')}
|
||||
{t`Copy`}
|
||||
</Button>
|
||||
<Button disabled={isMigrationInProcess} onClick={() => migrate('migrate')}>
|
||||
{t('global.button.migrate')}
|
||||
{t`Migrate`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -12,7 +12,7 @@ import CardActionArea from '@mui/material/CardActionArea';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
|
||||
@@ -26,10 +26,10 @@ export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas'][
|
||||
|
||||
// TODO - cleanup source/extension components
|
||||
export const MigrationCard = ({ id, name, lang, iconUrl, mangaCount }: TMigratableSource) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const isLocalSource = Number(id) === 0;
|
||||
const sourceName = isLocalSource ? t('source.local_source.title') : name;
|
||||
const sourceName = isLocalSource ? t`Local source` : name;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { TMigratableSource } from '@/features/migration/components/MigrationCard.tsx';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
@@ -25,7 +25,7 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts';
|
||||
|
||||
export const Migrate = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { sourceId: paramSourceId } = useParams<{ sourceId: string }>();
|
||||
|
||||
@@ -68,7 +68,7 @@ export const Migrate = () => {
|
||||
});
|
||||
|
||||
useAppTitleAndAction(
|
||||
name ?? sourceId ?? t('migrate.title'),
|
||||
name ?? sourceId ?? t`Migrate`,
|
||||
<GridLayouts gridLayout={gridLayout} onChange={setGridLayout} />,
|
||||
[gridLayout],
|
||||
);
|
||||
@@ -96,7 +96,7 @@ export const Migrate = () => {
|
||||
const error = (hasErrorSource ? sourceError : mangasError)!;
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => {
|
||||
if (hasErrorSource) {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import List from '@mui/material/List';
|
||||
import Stack from '@mui/material/Stack';
|
||||
@@ -15,6 +14,7 @@ import SortByAlphaIcon from '@mui/icons-material/SortByAlpha';
|
||||
import TagIcon from '@mui/icons-material/Tag';
|
||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
@@ -23,7 +23,7 @@ import { MigrationCard, TMigratableSource } from '@/features/migration/component
|
||||
import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { SortBy, SortOrder, SortSettings, TMigratableSourcesResult } from '@/features/migration/Migration.types.ts';
|
||||
import { sortByToTranslationKey, sortOrderToTranslationKey } from '@/features/migration/Migration.constants.ts';
|
||||
import { sortByToTranslation, sortOrderToTranslation } from '@/features/migration/Migration.constants.ts';
|
||||
import {
|
||||
createUpdateMetadataServerSettings,
|
||||
useMetadataServerSettings,
|
||||
@@ -75,14 +75,14 @@ const getMigratableSources = (
|
||||
};
|
||||
|
||||
export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { appBarHeight } = useNavBarContext();
|
||||
|
||||
const {
|
||||
settings: { migrateSortSettings },
|
||||
} = useMetadataServerSettings();
|
||||
const updateMetadataServerSettings = createUpdateMetadataServerSettings<'migrateSortSettings'>((e) =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
|
||||
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
const { sortBy, sortOrder } = migrateSortSettings;
|
||||
|
||||
@@ -101,7 +101,7 @@ export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
message={t`Unable to load data`}
|
||||
messageExtra={getErrorMessage(error)}
|
||||
retry={() => refetch().catch(defaultPromiseErrorHandler('Migration::refetch'))}
|
||||
/>
|
||||
@@ -123,7 +123,7 @@ export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<CustomTooltip title={t(sortByToTranslationKey[sortBy])}>
|
||||
<CustomTooltip title={t(sortByToTranslation[sortBy])}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={() =>
|
||||
@@ -133,7 +133,7 @@ export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
|
||||
{sortBy ? <TagIcon /> : <SortByAlphaIcon />}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t(sortOrderToTranslationKey[sortOrder])}>
|
||||
<CustomTooltip title={t(sortOrderToTranslation[sortOrder])}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={() =>
|
||||
|
||||
@@ -19,7 +19,8 @@ import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined';
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { msg, plural } from '@lingui/core/macro';
|
||||
import { NavbarItem, NavBarItemMoreGroup } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
@@ -30,7 +31,7 @@ type RestrictedNavBarItem<Show extends NavbarItem['show']> = Omit<NavbarItem, 's
|
||||
const NAVIGATION_BAR_BASE_ITEMS = [
|
||||
{
|
||||
path: AppRoutes.library.path() as RestrictedNavBarItem<'both'>['path'],
|
||||
title: 'library.title',
|
||||
title: msg`Library`,
|
||||
SelectedIconComponent: CollectionsBookmarkIcon,
|
||||
IconComponent: CollectionsOutlinedBookmarkIcon,
|
||||
show: 'both',
|
||||
@@ -38,7 +39,7 @@ const NAVIGATION_BAR_BASE_ITEMS = [
|
||||
},
|
||||
{
|
||||
path: AppRoutes.updates.path,
|
||||
title: 'updates.title',
|
||||
title: msg`Updates`,
|
||||
SelectedIconComponent: NewReleasesIcon,
|
||||
IconComponent: NewReleasesOutlinedIcon,
|
||||
show: 'both',
|
||||
@@ -46,7 +47,7 @@ const NAVIGATION_BAR_BASE_ITEMS = [
|
||||
},
|
||||
{
|
||||
path: AppRoutes.history.path,
|
||||
title: 'history.title',
|
||||
title: msg`History`,
|
||||
SelectedIconComponent: HistoryIcon,
|
||||
IconComponent: HistoryOutlinedIcon,
|
||||
show: 'both',
|
||||
@@ -54,13 +55,12 @@ const NAVIGATION_BAR_BASE_ITEMS = [
|
||||
},
|
||||
{
|
||||
path: AppRoutes.browse.path() as RestrictedNavBarItem<'both'>['path'],
|
||||
title: 'global.label.browse',
|
||||
title: msg`Browse`,
|
||||
SelectedIconComponent: ExploreIcon,
|
||||
IconComponent: ExploreOutlinedIcon,
|
||||
show: 'both',
|
||||
moreGroup: NavBarItemMoreGroup.GENERAL,
|
||||
useBadge: () => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = requestManager.useGetExtensionList({ fetchPolicy: 'cache-only' });
|
||||
|
||||
const extensions = data?.extensions.nodes ?? [];
|
||||
@@ -75,7 +75,10 @@ const NAVIGATION_BAR_BASE_ITEMS = [
|
||||
|
||||
return {
|
||||
count: availableUpdates,
|
||||
title: t('extension.label.available_updates', { count: availableUpdates }),
|
||||
title: plural(availableUpdates, {
|
||||
one: '# update available',
|
||||
other: '# updates available',
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -84,14 +87,14 @@ const NAVIGATION_BAR_BASE_ITEMS = [
|
||||
const NAVIGATION_BAR_DESKTOP_ITEMS = [
|
||||
{
|
||||
path: AppRoutes.downloads.path,
|
||||
title: 'download.title.download',
|
||||
moreTitle: 'download.title.queue',
|
||||
title: msg`Downloads`,
|
||||
moreTitle: msg`Download queue`,
|
||||
SelectedIconComponent: GetAppIcon,
|
||||
IconComponent: GetAppOutlinedIcon,
|
||||
show: 'desktop',
|
||||
moreGroup: NavBarItemMoreGroup.HIDDEN_ITEM,
|
||||
useBadge: () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { data } = requestManager.useGetDownloadStatus();
|
||||
const downloadStatus = data?.downloadStatus;
|
||||
|
||||
@@ -107,13 +110,13 @@ const NAVIGATION_BAR_DESKTOP_ITEMS = [
|
||||
|
||||
return {
|
||||
count,
|
||||
title: t(isPaused ? 'download.queue.info.paused' : 'download.queue.info.remaining', { count }),
|
||||
title: isPaused ? t`Paused — ${count} remaining` : t`${count} remaining`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: AppRoutes.settings.path,
|
||||
title: 'settings.title',
|
||||
title: msg`Settings`,
|
||||
SelectedIconComponent: SettingsIcon,
|
||||
IconComponent: SettingsIcon,
|
||||
show: 'desktop',
|
||||
@@ -121,7 +124,7 @@ const NAVIGATION_BAR_DESKTOP_ITEMS = [
|
||||
},
|
||||
{
|
||||
path: AppRoutes.about.path,
|
||||
title: 'settings.about.title',
|
||||
title: msg`About`,
|
||||
SelectedIconComponent: InfoIcon,
|
||||
IconComponent: InfoIcon,
|
||||
show: 'desktop',
|
||||
@@ -132,7 +135,7 @@ const NAVIGATION_BAR_DESKTOP_ITEMS = [
|
||||
export const NAVIGATION_BAR_MOBILE_ITEMS = [
|
||||
{
|
||||
path: AppRoutes.more.path,
|
||||
title: 'global.label.more',
|
||||
title: msg`More`,
|
||||
SelectedIconComponent: MoreHorizIcon,
|
||||
IconComponent: MoreHorizIcon,
|
||||
show: 'mobile',
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||
import { SvgIconTypeMap } from '@mui/material/SvgIcon';
|
||||
import { ReactNode } from 'react';
|
||||
import { StaticAppRoute } from '@/base/AppRoute.constants.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
export interface INavbarOverride {
|
||||
status: boolean;
|
||||
@@ -25,8 +25,8 @@ export enum NavBarItemMoreGroup {
|
||||
|
||||
export interface NavbarItem {
|
||||
path: StaticAppRoute;
|
||||
title: TranslationKey;
|
||||
moreTitle?: TranslationKey;
|
||||
title: MessageDescriptor;
|
||||
moreTitle?: MessageDescriptor;
|
||||
SelectedIconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
||||
IconComponent: OverridableComponent<SvgIconTypeMap<{}, 'svg'>>;
|
||||
show: 'mobile' | 'desktop' | 'both';
|
||||
|
||||
@@ -10,7 +10,6 @@ import Drawer from '@mui/material/Drawer';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||
@@ -22,6 +21,7 @@ import Box from '@mui/material/Box';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
|
||||
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
@@ -40,7 +40,7 @@ const DrawerHeader = styled('div')(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const NavigationBarItem = ({ path, title, IconComponent, SelectedIconComponent, useBadge }: NavbarItem) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const location = useLocation();
|
||||
const { isCollapsed } = useNavBarContext();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
|
||||
import BottomNavigation from '@mui/material/BottomNavigation';
|
||||
import BottomNavigationAction from '@mui/material/BottomNavigationAction';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { CSSProperties, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
|
||||
import { NavbarItem } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { StaticAppRoute } from '@/base/AppRoute.constants.ts';
|
||||
|
||||
export const MobileBottomBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const { setBottomBarHeight } = useNavBarContext();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
|
||||
@@ -18,18 +19,21 @@ export const ReaderSettingAutoScroll = ({
|
||||
}: Pick<IReaderSettings, 'autoScroll'> & {
|
||||
setAutoScroll: (updatedAutoScroll: IReaderSettings['autoScroll'], commit: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.auto_scroll.smooth')}
|
||||
label={t`Smooth auto scrolling`}
|
||||
checked={autoScroll.smooth}
|
||||
onChange={(_, checked) => setAutoScroll({ ...autoScroll, smooth: checked }, true)}
|
||||
/>
|
||||
<SliderInput
|
||||
label={t('reader.settings.auto_scroll.speed')}
|
||||
value={t('global.time.seconds.value', { count: autoScroll.value })}
|
||||
label={t`Auto scroll speed`}
|
||||
value={plural(autoScroll.value, {
|
||||
one: '# second',
|
||||
other: '# seconds',
|
||||
})}
|
||||
onDefault={() =>
|
||||
setAutoScroll({ ...autoScroll, value: DEFAULT_READER_SETTINGS.autoScroll.value }, true)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import PauseCircleFilledIcon from '@mui/icons-material/PauseCircleFilled';
|
||||
import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled';
|
||||
@@ -15,6 +14,8 @@ import TextField from '@mui/material/TextField';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import { useRef } from 'react';
|
||||
import { d } from 'koration';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { AUTO_SCROLL_SPEED } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
||||
@@ -26,7 +27,7 @@ export const ReaderNavBarDesktopAutoScroll = ({
|
||||
}: Pick<IReaderSettings, 'autoScroll'> & {
|
||||
setAutoScroll: (newAutoScroll: IReaderSettings['autoScroll'], commit: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { isActive, toggleActive } = useReaderAutoScrollStore((state) => ({
|
||||
isActive: state.autoScroll.isActive,
|
||||
toggleActive: state.autoScroll.toggleActive,
|
||||
@@ -44,7 +45,7 @@ export const ReaderNavBarDesktopAutoScroll = ({
|
||||
variant="contained"
|
||||
startIcon={isActive ? <PauseCircleFilledIcon /> : <PlayCircleFilledIcon />}
|
||||
>
|
||||
{t('reader.settings.auto_scroll.title')}
|
||||
{t`Auto scroll`}
|
||||
</Button>
|
||||
<TextField
|
||||
value={autoScroll.value}
|
||||
@@ -73,7 +74,10 @@ export const ReaderNavBarDesktopAutoScroll = ({
|
||||
inputProps: AUTO_SCROLL_SPEED,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t('global.time.seconds.second', { count: autoScroll.value })}
|
||||
{plural(autoScroll.value, {
|
||||
one: 'Second',
|
||||
other: 'Seconds',
|
||||
})}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
|
||||
|
||||
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderSettingBrightness = ({
|
||||
@@ -24,18 +23,18 @@ export const ReaderSettingBrightness = ({
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.brightness')}
|
||||
label={t`Custom brightness`}
|
||||
checked={brightness.enabled}
|
||||
onChange={(_, checked) => updateSetting('brightness', { ...brightness, enabled: checked }, true)}
|
||||
/>
|
||||
{brightness.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.brightness')}
|
||||
label={t`Custom brightness`}
|
||||
value={brightness.value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
|
||||
@@ -23,18 +23,18 @@ export const ReaderSettingContrast = ({
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.contrast')}
|
||||
label={t`Custom contrast`}
|
||||
checked={contrast.enabled}
|
||||
onChange={(_, checked) => updateSetting('contrast', { ...contrast, enabled: checked }, true)}
|
||||
/>
|
||||
{contrast.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.contrast')}
|
||||
label={t`Custom contrast`}
|
||||
value={contrast.value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
@@ -17,15 +17,11 @@ export const ReaderSettingGrayscale = ({
|
||||
}: Pick<IReaderSettings['customFilter'], 'grayscale'> & {
|
||||
updateSetting: (grayscale: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.grayscale')}
|
||||
checked={grayscale}
|
||||
onChange={(_, checked) => updateSetting(checked)}
|
||||
/>
|
||||
<CheckboxInput label={t`Grayscale`} checked={grayscale} onChange={(_, checked) => updateSetting(checked)} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
|
||||
@@ -23,18 +23,18 @@ export const ReaderSettingHue = ({
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.hue')}
|
||||
label={t`Custom hue`}
|
||||
checked={hue.enabled}
|
||||
onChange={(_, checked) => updateSetting('hue', { ...hue, enabled: checked }, true)}
|
||||
/>
|
||||
{hue.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.hue')}
|
||||
label={t`Custom hue`}
|
||||
value={hue.value}
|
||||
onDefault={() =>
|
||||
updateSetting('hue', { ...hue, value: DEFAULT_READER_SETTINGS.customFilter.hue.value }, true)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
@@ -17,15 +17,11 @@ export const ReaderSettingInvert = ({
|
||||
}: Pick<IReaderSettings['customFilter'], 'invert'> & {
|
||||
updateSetting: (invert: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.invert')}
|
||||
checked={invert}
|
||||
onChange={(_, checked) => updateSetting(checked)}
|
||||
/>
|
||||
<CheckboxInput label={t`Invert`} checked={invert} onChange={(_, checked) => updateSetting(checked)} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,28 +6,29 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
|
||||
|
||||
import {
|
||||
CUSTOM_FILTER,
|
||||
DEFAULT_READER_SETTINGS,
|
||||
READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READER_BLEND_MODE_VALUES,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
|
||||
import { ButtonSelectInput } from '@/base/components/inputs/ButtonSelectInput.tsx';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
type RGBAType = Exclude<keyof IReaderSettings['customFilter']['rgba']['value'], 'blendMode'>;
|
||||
|
||||
const RGBA_TYPE_TO_TRANSLATION_KEY: Record<RGBAType, TranslationKey> = {
|
||||
red: 'reader.settings.custom_filter.rgba.red',
|
||||
green: 'reader.settings.custom_filter.rgba.green',
|
||||
blue: 'reader.settings.custom_filter.rgba.blue',
|
||||
alpha: 'reader.settings.custom_filter.rgba.alpha',
|
||||
const RGBA_TYPE_TO_TRANSLATION: Record<RGBAType, MessageDescriptor> = {
|
||||
red: msg`Red`,
|
||||
green: msg`Green`,
|
||||
blue: msg`Blue`,
|
||||
alpha: msg`Alpha`,
|
||||
};
|
||||
|
||||
export const ReaderSettingRGBA = ({
|
||||
@@ -40,12 +41,12 @@ export const ReaderSettingRGBA = ({
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.rgba.title')}
|
||||
label={t`Custom color filters`}
|
||||
checked={rgba.enabled}
|
||||
onChange={(_, checked) => updateSetting('rgba', { ...rgba, enabled: checked }, true)}
|
||||
/>
|
||||
@@ -59,7 +60,7 @@ export const ReaderSettingRGBA = ({
|
||||
return (
|
||||
<SliderInput
|
||||
key={key}
|
||||
label={t(RGBA_TYPE_TO_TRANSLATION_KEY[key as RGBAType])}
|
||||
label={t(RGBA_TYPE_TO_TRANSLATION[key as RGBAType])}
|
||||
value={value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
@@ -101,7 +102,7 @@ export const ReaderSettingRGBA = ({
|
||||
);
|
||||
})}
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.custom_filter.rgba.blend_mode.title')}
|
||||
label={t`Color filter blend mode`}
|
||||
value={rgba.value.blendMode}
|
||||
values={READER_BLEND_MODE_VALUES}
|
||||
setValue={(value) =>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/base/components/inputs/SliderInput.tsx';
|
||||
@@ -23,18 +23,18 @@ export const ReaderSettingSaturate = ({
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.saturate')}
|
||||
label={t`Custom saturate`}
|
||||
checked={saturate.enabled}
|
||||
onChange={(_, checked) => updateSetting('saturate', { ...saturate, enabled: checked }, true)}
|
||||
/>
|
||||
{saturate.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.saturate')}
|
||||
label={t`Custom saturate`}
|
||||
value={saturate.value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
@@ -17,15 +17,11 @@ export const ReaderSettingSepia = ({
|
||||
}: Pick<IReaderSettings['customFilter'], 'sepia'> & {
|
||||
updateSetting: (sepia: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.sepia')}
|
||||
checked={sepia}
|
||||
onChange={(_, checked) => updateSetting(checked)}
|
||||
/>
|
||||
<CheckboxInput label={t`Sepia`} checked={sepia} onChange={(_, checked) => updateSetting(checked)} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { d } from 'koration';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/features/reader/Reader.types.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { READING_MODE_VALUE_TO_DISPLAY_DATA } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
const HIDE_PREVIEW_TIMEOUT = d(5).seconds.inWholeMilliseconds;
|
||||
|
||||
@@ -28,7 +28,7 @@ export const useReaderShowSettingPreviewOnChange = (
|
||||
shouldShowTapZoneLayoutPreview: IReaderSettings['shouldShowTapZoneLayoutPreview'],
|
||||
setShowPreview: TReaderTapZoneContext['setShowPreview'],
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
// show setting previews on change or when open reader
|
||||
const previousReadingMode = useRef<IReaderSettingsWithDefaultFlag['readingMode']>(undefined);
|
||||
@@ -44,7 +44,7 @@ export const useReaderShowSettingPreviewOnChange = (
|
||||
const didReadingModeChange = JSON.stringify(readingMode) !== JSON.stringify(previousReadingMode.current);
|
||||
const showReadingModePreview = shouldShowReadingModePreview && didReadingModeChange;
|
||||
if (showReadingModePreview) {
|
||||
makeToast(t(READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].title as TranslationKey), {
|
||||
makeToast(t(READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].title as MessageDescriptor), {
|
||||
autoHideDuration: HIDE_PREVIEW_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { ReaderSettingsTypeProps } from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderSettingHotkey } from '@/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx';
|
||||
import { READER_HOTKEYS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { ResetButton } from '@/base/components/buttons/ResetButton.tsx';
|
||||
|
||||
export const ReaderHotkeysSettings = ({ settings, updateSetting, onDefault }: ReaderSettingsTypeProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<Stack sx={{ alignItems: 'end' }}>
|
||||
<Typography variant="caption">{t('hotkeys.info.delete')}</Typography>
|
||||
<Typography variant="caption">{t`Click key to remove binding`}</Typography>
|
||||
</Stack>
|
||||
{READER_HOTKEYS.map((hotkey) => (
|
||||
<ReaderSettingHotkey
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { Kbd } from '@/base/components/texts/Kbd.tsx';
|
||||
|
||||
export const Hotkey = ({ keys, removeKey }: { keys: string[]; removeKey?: (key: string) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
|
||||
{keys.map((key, index) => (
|
||||
<Fragment key={key}>
|
||||
<CustomTooltip title={t('global.button.delete')} hidden={!removeKey}>
|
||||
<CustomTooltip title={t`Delete`} hidden={!removeKey}>
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -6,37 +6,38 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { ReaderHotkey } from '@/features/reader/Reader.types.ts';
|
||||
import { DEFAULT_READER_SETTINGS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { RecordHotkey } from '@/features/reader/hotkeys/settings/components/RecordHotkey.tsx';
|
||||
import { Hotkey } from '@/features/reader/hotkeys/settings/components/Hotkey.tsx';
|
||||
import { ResetButton } from '@/base/components/buttons/ResetButton.tsx';
|
||||
import { TranslationKey } from '@/base/Base.types.ts';
|
||||
|
||||
const READER_HOTKEY_TO_TITLE: Record<ReaderHotkey, TranslationKey> = {
|
||||
[ReaderHotkey.PREVIOUS_PAGE]: 'reader.settings.hotkey.previous_page',
|
||||
[ReaderHotkey.NEXT_PAGE]: 'reader.settings.hotkey.next_page',
|
||||
[ReaderHotkey.SCROLL_BACKWARD]: 'reader.settings.hotkey.scroll_backward',
|
||||
[ReaderHotkey.SCROLL_FORWARD]: 'reader.settings.hotkey.scroll_forward',
|
||||
[ReaderHotkey.PREVIOUS_CHAPTER]: 'reader.settings.hotkey.previous_chapter',
|
||||
[ReaderHotkey.NEXT_CHAPTER]: 'reader.settings.hotkey.next_chapter',
|
||||
[ReaderHotkey.TOGGLE_MENU]: 'reader.settings.hotkey.menu',
|
||||
[ReaderHotkey.CYCLE_SCALE_TYPE]: 'reader.settings.hotkey.scale_type',
|
||||
[ReaderHotkey.STRETCH_IMAGE]: 'reader.settings.hotkey.stretch_image',
|
||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: 'reader.settings.hotkey.offset_spread_pages',
|
||||
[ReaderHotkey.CYCLE_READING_MODE]: 'reader.settings.hotkey.reading_mode',
|
||||
[ReaderHotkey.CYCLE_READING_DIRECTION]: 'reader.settings.hotkey.reading_direction',
|
||||
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: 'reader.settings.hotkey.auto_scroll',
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE]: 'reader.settings.hotkey.auto_scroll_speed_increase',
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: 'reader.settings.hotkey.auto_scroll_speed_decrease',
|
||||
[ReaderHotkey.EXIT_READER]: 'reader.button.exit',
|
||||
const READER_HOTKEY_TO_TITLE: Record<ReaderHotkey, MessageDescriptor> = {
|
||||
[ReaderHotkey.PREVIOUS_PAGE]: msg`Previous page`,
|
||||
[ReaderHotkey.NEXT_PAGE]: msg`Next page`,
|
||||
[ReaderHotkey.SCROLL_BACKWARD]: msg`Scroll backward`,
|
||||
[ReaderHotkey.SCROLL_FORWARD]: msg`Scroll forward`,
|
||||
[ReaderHotkey.PREVIOUS_CHAPTER]: msg`Previous chapter`,
|
||||
[ReaderHotkey.NEXT_CHAPTER]: msg`Next chapter`,
|
||||
[ReaderHotkey.TOGGLE_MENU]: msg`Toggle menu`,
|
||||
[ReaderHotkey.CYCLE_SCALE_TYPE]: msg`Cycle image scale type`,
|
||||
[ReaderHotkey.STRETCH_IMAGE]: msg`Toggle stretch image`,
|
||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: msg`Toggle offset spread pages`,
|
||||
[ReaderHotkey.CYCLE_READING_MODE]: msg`Cycle reading mode`,
|
||||
[ReaderHotkey.CYCLE_READING_DIRECTION]: msg`Cycle reading direction`,
|
||||
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: msg`Toggle auto scroll`,
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE]: msg`Increase auto scroll speed`,
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: msg`Decrease auto scroll speed`,
|
||||
[ReaderHotkey.EXIT_READER]: msg`Exit reader`,
|
||||
};
|
||||
|
||||
export const ReaderSettingHotkey = ({
|
||||
@@ -50,7 +51,7 @@ export const ReaderSettingHotkey = ({
|
||||
existingKeys: string[];
|
||||
updateSetting: (keys: string[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const popupState = usePopupState({ popupId: 'reader-setting-record-hotkey', variant: 'dialog' });
|
||||
|
||||
return (
|
||||
@@ -61,7 +62,7 @@ export const ReaderSettingHotkey = ({
|
||||
keys={keys}
|
||||
removeKey={(keyToRemove) => updateSetting(keys.filter((key) => key !== keyToRemove))}
|
||||
/>
|
||||
<CustomTooltip title={t('global.button.add')}>
|
||||
<CustomTooltip title={t`Add`}>
|
||||
<IconButton {...bindTrigger(popupState)} color="inherit">
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useRecordHotkeys } from 'react-hotkeys-hook';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
@@ -16,6 +15,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { Hotkey } from '@/features/reader/hotkeys/settings/components/Hotkey.tsx';
|
||||
|
||||
export const RecordHotkey = ({
|
||||
@@ -27,7 +27,7 @@ export const RecordHotkey = ({
|
||||
onCreate: (keys: string[]) => void;
|
||||
existingKeys: string[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [recordedKeys, { start, stop, resetKeys }] = useRecordHotkeys();
|
||||
const keys = [[...recordedKeys].join('+')];
|
||||
@@ -45,23 +45,15 @@ export const RecordHotkey = ({
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose} fullWidth>
|
||||
<DialogTitle>{t('hotkeys.create.dialog.title')}</DialogTitle>
|
||||
<DialogTitle>{t`Record keybind`}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
|
||||
<Trans
|
||||
i18nKey="hotkeys.create.dialog.label"
|
||||
components={{
|
||||
Keys: recordedKeys.size ? (
|
||||
<Hotkey keys={keys} />
|
||||
) : (
|
||||
<Typography>{t('hotkeys.create.dialog.placeholder')}</Typography>
|
||||
),
|
||||
}}
|
||||
>
|
||||
Recorded keys:
|
||||
<Trans>
|
||||
Recorded hotkeys:{' '}
|
||||
{recordedKeys.size ? <Hotkey keys={keys} /> : <Typography>{t`Press keys`}</Typography>}
|
||||
</Trans>
|
||||
</Stack>
|
||||
{isExistingKey && <Typography color="error">{t('hotkeys.create.error.exists')}</Typography>}
|
||||
{isExistingKey && <Typography color="error">{t`Hotkey already exists`}</Typography>}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Stack
|
||||
@@ -71,9 +63,9 @@ export const RecordHotkey = ({
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Button onClick={resetKeys}>{t('global.button.reset')}</Button>
|
||||
<Button onClick={resetKeys}>{t`Reset`}</Button>
|
||||
<Stack direction="row">
|
||||
<Button onClick={onClose}>{t('global.button.cancel')}</Button>
|
||||
<Button onClick={onClose}>{t`Cancel`}</Button>
|
||||
<Button
|
||||
disabled={isExistingKey}
|
||||
onClick={() => {
|
||||
@@ -81,7 +73,7 @@ export const RecordHotkey = ({
|
||||
onCreate(keys);
|
||||
}}
|
||||
>
|
||||
{t('global.button.create')}
|
||||
{t`Create`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -11,13 +11,13 @@ import Stack from '@mui/material/Stack';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { bindMenu, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import Link from '@mui/material/Link';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { memo, Ref } 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';
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
|
||||
|
||||
const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & { ref?: Ref<HTMLDivElement> }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' });
|
||||
const currentChapter = useReaderChaptersStore((state) => state.chapters.currentChapter);
|
||||
|
||||
@@ -102,7 +102,7 @@ const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & {
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('global.button.open_browser')}
|
||||
{t`Open in browser`}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
@@ -111,16 +111,16 @@ const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & {
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('global.button.open_webview')}
|
||||
{t`Open in WebView`}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!realUrl}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(title);
|
||||
makeToast(t('global.label.copied_clipboard'), 'info');
|
||||
makeToast(t`Copied to clipboard`, 'info');
|
||||
}}
|
||||
>
|
||||
{t('global.label.share')}
|
||||
{t`Share`}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo } from 'react';
|
||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { ChapterAction, TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
||||
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
|
||||
|
||||
const BaseReaderBookmarkButton = ({ id, isBookmarked }: Pick<TChapterReader, 'id' | 'isBookmarked'>) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const bookmarkAction: Extract<ChapterAction, 'unbookmark' | 'bookmark'> = isBookmarked ? 'unbookmark' : 'bookmark';
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import { memo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
|
||||
const BaseReaderExitButton = ({ exit }: { exit: ReturnType<typeof ReaderService.useExit> }) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const getOptionForDirection = useGetOptionForDirection();
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t('reader.button.exit')}>
|
||||
<CustomTooltip title={t`Exit reader`}>
|
||||
<IconButton sx={{ marginRight: 2 }} onClick={exit} color="inherit">
|
||||
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
|
||||
</IconButton>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||
import { memo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
@@ -27,13 +27,11 @@ export const ReaderLibraryButton = memo(() => {
|
||||
|
||||
const { inLibrary } = manga ?? ACTION_FALLBACK_MANGA;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { updateLibraryState } = useManageMangaLibraryState(manga ?? ACTION_FALLBACK_MANGA, true);
|
||||
|
||||
return (
|
||||
<CustomTooltip
|
||||
title={inLibrary ? t('manga.action.library.remove.label.action') : t('manga.button.add_to_library')}
|
||||
>
|
||||
<CustomTooltip title={inLibrary ? t`Remove from the library` : t`Add To Library`}>
|
||||
<IconButton onClick={updateLibraryState} color="inherit">
|
||||
{inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
|
||||
</IconButton>
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { ReaderNavBarDesktopProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { ReaderNavContainer } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavContainer.tsx';
|
||||
@@ -58,7 +58,7 @@ const BaseReaderNavBarDesktop = ({
|
||||
openSettings,
|
||||
setReaderNavBarWidth,
|
||||
}: ReaderNavBarDesktopProps & Pick<NavbarContextType, 'setReaderNavBarWidth'>) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const manga = useReaderStore((state) => state.manga);
|
||||
const {
|
||||
chapters,
|
||||
@@ -112,7 +112,7 @@ const BaseReaderNavBarDesktop = ({
|
||||
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<ReaderExitButton />
|
||||
<CustomTooltip title={t('reader.settings.label.static_navigation')}>
|
||||
<CustomTooltip title={t`Static navigation`}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setReaderNavBarWidth(0);
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { DownloadStateIndicator } from '@/base/components/downloads/DownloadStateIndicator.tsx';
|
||||
@@ -26,7 +26,7 @@ import { useReaderChaptersStore, useReaderPagesStore } from '@/features/reader/s
|
||||
import { ChapterDownloadInfo, ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const DownloadButton = ({ id = -1, isDownloaded }: ChapterIdInfo & ChapterDownloadInfo) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const downloadStatus = Chapters.useDownloadStatusFromCache(id);
|
||||
|
||||
@@ -67,7 +67,7 @@ export const ReaderNavBarDesktopActions = memo(() => {
|
||||
realUrl: state.chapters.currentChapter?.realUrl ?? FALLBACK_CHAPTER.realUrl,
|
||||
}));
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { pageLoadStates, setPageLoadStates, setRetryFailedPagesKeyPrefix } = useReaderPagesStore((state) => ({
|
||||
pageLoadStates: state.pages.pageLoadStates,
|
||||
setPageLoadStates: state.pages.setPageLoadStates,
|
||||
@@ -85,7 +85,7 @@ export const ReaderNavBarDesktopActions = memo(() => {
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'center', gap: 1 }}>
|
||||
<ReaderLibraryButton />
|
||||
<ReaderBookmarkButton id={id} isBookmarked={isBookmarked} />
|
||||
<CustomTooltip title={t('reader.button.retry_load_pages')} disabled={!haveSomePagesFailedToLoad}>
|
||||
<CustomTooltip title={t`Retry errored pages`} disabled={!haveSomePagesFailedToLoad}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setPageLoadStates((statePageLoadStates) =>
|
||||
@@ -104,12 +104,12 @@ export const ReaderNavBarDesktopActions = memo(() => {
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<DownloadButton id={id} isDownloaded={isDownloaded} />
|
||||
<CustomTooltip title={t('global.button.open_browser')} disabled={!realUrl}>
|
||||
<CustomTooltip title={t`Open in browser`} disabled={!realUrl}>
|
||||
<IconButton disabled={!realUrl} href={realUrl ?? ''} rel="noreferrer" target="_blank" color="inherit">
|
||||
<IconBrowser />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('global.button.open_webview')} disabled={!realUrl}>
|
||||
<CustomTooltip title={t`Open in WebView`} disabled={!realUrl}>
|
||||
<IconButton
|
||||
disabled={!realUrl}
|
||||
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { memo, useLayoutEffect } from 'react';
|
||||
@@ -16,6 +15,7 @@ import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Select } from '@/base/components/inputs/Select.tsx';
|
||||
import { ReaderChapterList } from '@/features/reader/overlay/navigation/components/ReaderChapterList.tsx';
|
||||
import { ReaderNavBarDesktopNextPreviousButton } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopNextPreviousButton.tsx';
|
||||
@@ -41,7 +41,7 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
|
||||
} & Pick<ReaderStateChapters, 'chapters' | 'previousChapter' | 'nextChapter'> & {
|
||||
readerThemeDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
|
||||
const popupState = usePopupState({ variant: 'popover', popupId: 'reader-nav-bar-desktop-chapter-list' });
|
||||
|
||||
@@ -53,27 +53,21 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="previous"
|
||||
title={t(
|
||||
getOptionForDirection(
|
||||
'reader.button.previous_chapter',
|
||||
'reader.button.next_chapter',
|
||||
readerThemeDirection,
|
||||
),
|
||||
)}
|
||||
title={getOptionForDirection(t`Previous chapter`, t`Next chapter`, readerThemeDirection)}
|
||||
onClick={() => {
|
||||
ReaderControls.openChapter(getOptionForDirection('previous', 'next', readerThemeDirection));
|
||||
}}
|
||||
disabled={getOptionForDirection(!previousChapter, !nextChapter, readerThemeDirection)}
|
||||
/>
|
||||
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
|
||||
<InputLabel id="reader-nav-bar-desktop-chapter-select">{t('chapter.title_one')}</InputLabel>
|
||||
<InputLabel id="reader-nav-bar-desktop-chapter-select">{t`Chapter`}</InputLabel>
|
||||
<Select
|
||||
{...bindTrigger(popupState)}
|
||||
open={popupState.isOpen}
|
||||
value={currentChapterId ?? 0}
|
||||
// hide actual select menu
|
||||
MenuProps={{ sx: { visibility: 'hidden' } }}
|
||||
label={t('chapter.title_one')}
|
||||
label={t`Chapter`}
|
||||
labelId="reader-nav-bar-desktop-chapter-select"
|
||||
>
|
||||
{/* hacky way to use the select component with a custom menu, the only possible value that is needed is the current chapter */}
|
||||
@@ -85,13 +79,7 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
component={Link}
|
||||
type="next"
|
||||
title={t(
|
||||
getOptionForDirection(
|
||||
'reader.button.next_chapter',
|
||||
'reader.button.previous_chapter',
|
||||
readerThemeDirection,
|
||||
),
|
||||
)}
|
||||
title={getOptionForDirection(t`Next chapter`, t`Previous chapter`, readerThemeDirection)}
|
||||
onClick={() => {
|
||||
ReaderControls.openChapter(getOptionForDirection('next', 'previous', readerThemeDirection));
|
||||
}}
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { memo, useMemo } from 'react';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Select } from '@/base/components/inputs/Select.tsx';
|
||||
import { getNextIndexFromPage, getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
@@ -21,7 +21,7 @@ import { useReaderPagesStore, useReaderSettingsStore } from '@/features/reader/s
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
|
||||
const BaseReaderNavBarDesktopPageNavigation = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const getOptionForDirection = useGetOptionForDirection();
|
||||
const { currentPageIndex, pages } = useReaderPagesStore((state) => ({
|
||||
currentPageIndex: state.pages.currentPageIndex,
|
||||
@@ -36,7 +36,7 @@ const BaseReaderNavBarDesktopPageNavigation = () => {
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="previous"
|
||||
title={t(getOptionForDirection('reader.button.previous_page', 'reader.button.next_page', direction))}
|
||||
title={getOptionForDirection(t`Previous page`, t`Next page`, direction)}
|
||||
disabled={getOptionForDirection(
|
||||
!currentPage.primary.index,
|
||||
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
|
||||
@@ -45,10 +45,10 @@ const BaseReaderNavBarDesktopPageNavigation = () => {
|
||||
onClick={() => ReaderControls.openPage('previous', undefined, false)}
|
||||
/>
|
||||
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
|
||||
<InputLabel id="reader-nav-bar-desktop-page-select">{t('reader.page_info.label.page')}</InputLabel>
|
||||
<InputLabel id="reader-nav-bar-desktop-page-select">{t`Page`}</InputLabel>
|
||||
<Select
|
||||
labelId="reader-nav-bar-desktop-page-select"
|
||||
label={t('reader.page_info.label.page')}
|
||||
label={t`Page`}
|
||||
value={getNextIndexFromPage(currentPage)}
|
||||
onChange={(e) => ReaderControls.openPage(e.target.value as number, undefined, false)}
|
||||
>
|
||||
@@ -61,7 +61,7 @@ const BaseReaderNavBarDesktopPageNavigation = () => {
|
||||
</FormControl>
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="next"
|
||||
title={t(getOptionForDirection('reader.button.next_page', 'reader.button.previous_page', direction))}
|
||||
title={getOptionForDirection(t`Next page`, t`Previous page`, direction)}
|
||||
disabled={getOptionForDirection(
|
||||
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
|
||||
!currentPage.primary.index,
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { memo } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { ReaderNavBarDesktopPageScale } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopPageScale.tsx';
|
||||
import { ReaderNavBarDesktopReadingMode } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopReadingMode.tsx';
|
||||
import { ReaderNavBarDesktopOffsetDoubleSpread } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopOffsetDoubleSpread.tsx';
|
||||
@@ -21,7 +21,7 @@ import { ReaderNavBarDesktopAutoScroll } from '@/features/reader/auto-scroll/set
|
||||
import { useReaderSettingsStore } from '@/features/reader/stores/ReaderStore.ts';
|
||||
|
||||
const BaseReaderNavBarDesktopQuickSettings = ({ openSettings }: Pick<ReaderNavBarDesktopProps, 'openSettings'>) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useLingui();
|
||||
const { readingMode, shouldOffsetDoubleSpreads, pageScaleMode, shouldStretchPage, readingDirection, autoScroll } =
|
||||
useReaderSettingsStore((state) => ({
|
||||
readingMode: state.settings.readingMode,
|
||||
@@ -71,7 +71,7 @@ const BaseReaderNavBarDesktopQuickSettings = ({ openSettings }: Pick<ReaderNavBa
|
||||
variant="contained"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
{t('settings.title')}
|
||||
{t`Settings`}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user