Add sort options to migration list
This commit is contained in:
@@ -423,10 +423,12 @@
|
|||||||
},
|
},
|
||||||
"sort": {
|
"sort": {
|
||||||
"label": {
|
"label": {
|
||||||
|
"asc": "Ascending",
|
||||||
"by_chapter_number": "By chapter number",
|
"by_chapter_number": "By chapter number",
|
||||||
"by_fetch_date": "By date fetched",
|
"by_fetch_date": "By date fetched",
|
||||||
"by_source": "By source",
|
"by_source": "By source",
|
||||||
"by_upload_date": "By upload date"
|
"by_upload_date": "By upload date",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
@@ -735,6 +737,10 @@
|
|||||||
"search": {
|
"search": {
|
||||||
"title": "$t(migrate.title) \"{{title}}\""
|
"title": "$t(migrate.title) \"{{title}}\""
|
||||||
},
|
},
|
||||||
|
"sort": {
|
||||||
|
"by_manga_count": "By manga count",
|
||||||
|
"by_source_name": "By source name"
|
||||||
|
},
|
||||||
"title": "Migrate"
|
"title": "Migrate"
|
||||||
},
|
},
|
||||||
"reader": {
|
"reader": {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
|||||||
'migrateCategories',
|
'migrateCategories',
|
||||||
'migrateTracking',
|
'migrateTracking',
|
||||||
'deleteChapters',
|
'deleteChapters',
|
||||||
|
'migrateSortSettings',
|
||||||
|
|
||||||
// browse
|
// browse
|
||||||
'hideLibraryEntries',
|
'hideLibraryEntries',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
AllowedMetadataValueTypes,
|
AllowedMetadataValueTypes,
|
||||||
AppMetadataKeys,
|
AppMetadataKeys,
|
||||||
Metadata,
|
Metadata,
|
||||||
|
MetadataMigrationSettings,
|
||||||
MetadataServerSettingKeys,
|
MetadataServerSettingKeys,
|
||||||
MetadataServerSettings,
|
MetadataServerSettings,
|
||||||
MetadataThemeSettings,
|
MetadataThemeSettings,
|
||||||
@@ -19,6 +20,7 @@ import { convertFromGqlMeta, getMetadataFrom, requestUpdateServerMetadata } from
|
|||||||
import { jsonSaveParse } from '@/util/HelperFunctions.ts';
|
import { jsonSaveParse } from '@/util/HelperFunctions.ts';
|
||||||
import { DEFAULT_DEVICE } from '@/util/device.ts';
|
import { DEFAULT_DEVICE } from '@/util/device.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
|
import { DEFAULT_SORT_SETTINGS } from '@/screens/Migration.constants.ts';
|
||||||
|
|
||||||
export const getDefaultSettings = (): MetadataServerSettings => ({
|
export const getDefaultSettings = (): MetadataServerSettings => ({
|
||||||
// downloads
|
// downloads
|
||||||
@@ -41,6 +43,7 @@ export const getDefaultSettings = (): MetadataServerSettings => ({
|
|||||||
migrateCategories: true,
|
migrateCategories: true,
|
||||||
migrateTracking: true,
|
migrateTracking: true,
|
||||||
deleteChapters: true,
|
deleteChapters: true,
|
||||||
|
migrateSortSettings: DEFAULT_SORT_SETTINGS,
|
||||||
|
|
||||||
// browse
|
// browse
|
||||||
hideLibraryEntries: false,
|
hideLibraryEntries: false,
|
||||||
@@ -64,6 +67,7 @@ export const convertSettingsToMetadata = (
|
|||||||
...settings,
|
...settings,
|
||||||
devices: JSON.stringify(settings.devices),
|
devices: JSON.stringify(settings.devices),
|
||||||
customThemes: JSON.stringify(settings.customThemes),
|
customThemes: JSON.stringify(settings.customThemes),
|
||||||
|
migrateSortSettings: JSON.stringify(settings.migrateSortSettings),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const convertMetadataToSettings = (
|
export const convertMetadataToSettings = (
|
||||||
@@ -76,6 +80,10 @@ export const convertMetadataToSettings = (
|
|||||||
customThemes:
|
customThemes:
|
||||||
jsonSaveParse<MetadataThemeSettings['customThemes']>((metadata.customThemes as string) ?? '') ??
|
jsonSaveParse<MetadataThemeSettings['customThemes']>((metadata.customThemes as string) ?? '') ??
|
||||||
getDefaultSettings().customThemes,
|
getDefaultSettings().customThemes,
|
||||||
|
migrateSortSettings:
|
||||||
|
jsonSaveParse<MetadataMigrationSettings['migrateSortSettings']>(
|
||||||
|
(metadata.migrateSortSettings as string) ?? '',
|
||||||
|
) ?? getDefaultSettings().migrateSortSettings,
|
||||||
}) satisfies MetadataServerSettings;
|
}) satisfies MetadataServerSettings;
|
||||||
|
|
||||||
const getMetadataServerSettingsWithDefaultFallback = (
|
const getMetadataServerSettingsWithDefaultFallback = (
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function Browse() {
|
|||||||
<Extensions tabsMenuHeight={tabsMenuHeight} />
|
<Extensions tabsMenuHeight={tabsMenuHeight} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel index={2} currentIndex={tabNum}>
|
<TabPanel index={2} currentIndex={tabNum}>
|
||||||
<Migration />
|
<Migration tabsMenuHeight={tabsMenuHeight} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabsWrapper>
|
</TabsWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
25
src/screens/Migration.constants.ts
Normal file
25
src/screens/Migration.constants.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SortBy, SortOrder } from '@/screens/Migration.types.ts';
|
||||||
|
import { TranslationKey } from '@/typings.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 sortOrderToTranslationKey: Record<SortBy, TranslationKey> = {
|
||||||
|
[SortOrder.ASC]: 'global.sort.label.asc',
|
||||||
|
[SortOrder.DESC]: 'global.sort.label.desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_SORT_SETTINGS = {
|
||||||
|
sortBy: SortBy.SOURCE_NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
};
|
||||||
@@ -9,45 +9,89 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
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 Tooltip from '@mui/material/Tooltip';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
||||||
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
||||||
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
|
||||||
import { MigrationCard, TMigratableSource } from '@/components/MigrationCard.tsx';
|
import { MigrationCard, TMigratableSource } from '@/components/MigrationCard.tsx';
|
||||||
import { StyledGroupItemWrapper } from '@/components/virtuoso/StyledGroupItemWrapper.tsx';
|
import { StyledGroupItemWrapper } from '@/components/virtuoso/StyledGroupItemWrapper.tsx';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
|
import { SortBy, SortOrder, SortSettings, TMigratableSourcesResult } from '@/screens/Migration.types.ts';
|
||||||
|
import { sortByToTranslationKey, sortOrderToTranslationKey } from '@/screens/Migration.constants';
|
||||||
|
import {
|
||||||
|
createUpdateMetadataServerSettings,
|
||||||
|
useMetadataServerSettings,
|
||||||
|
} from '@/lib/metadata/metadataServerSettings.ts';
|
||||||
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||||
|
|
||||||
type TMigratableSourcesResult = GetMigratableSourcesQuery['mangas']['nodes'];
|
const getMigratableSources = (
|
||||||
type TMigratableSources = Record<string, TMigratableSource>;
|
mangas: TMigratableSourcesResult | undefined,
|
||||||
|
{ sortBy, sortOrder }: SortSettings,
|
||||||
const getMigratableSources = (mangas?: TMigratableSourcesResult): TMigratableSources => {
|
): TMigratableSource[] => {
|
||||||
if (!mangas) {
|
if (!mangas) {
|
||||||
return {};
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueSources: TMigratableSources = {};
|
const sourceBySourceId: Record<string, TMigratableSource> = {};
|
||||||
|
|
||||||
mangas.forEach(({ sourceId, source }) => {
|
mangas.forEach(({ sourceId, source }) => {
|
||||||
const uniqueSource = uniqueSources[sourceId] ?? {
|
const uniqueSource = sourceBySourceId[sourceId] ?? {
|
||||||
...{ id: sourceId, name: sourceId, lang: 'unknown', iconUrl: null, mangaCount: 0, ...source },
|
...{ id: sourceId, name: sourceId, lang: 'unknown', iconUrl: null, mangaCount: 0, ...source },
|
||||||
};
|
};
|
||||||
|
|
||||||
uniqueSources[sourceId] = {
|
sourceBySourceId[sourceId] = {
|
||||||
...uniqueSource,
|
...uniqueSource,
|
||||||
mangaCount: uniqueSource.mangaCount + 1,
|
mangaCount: uniqueSource.mangaCount + 1,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return uniqueSources;
|
const sourcesSortedBy = Object.values(sourceBySourceId).toSorted((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case SortBy.SOURCE_NAME:
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
case SortBy.MANGA_COUNT:
|
||||||
|
return a.mangaCount - b.mangaCount;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected "sortBy" "${sortBy}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (sortOrder) {
|
||||||
|
case SortOrder.ASC:
|
||||||
|
return sourcesSortedBy;
|
||||||
|
case SortOrder.DESC:
|
||||||
|
return sourcesSortedBy.toReversed();
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected "sortOrder" "${sortOrder}"`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Migration = () => {
|
export const Migration = ({ tabsMenuHeight }: { tabsMenuHeight: number }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { appBarHeight } = useNavBarContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
settings: { migrateSortSettings },
|
||||||
|
} = useMetadataServerSettings();
|
||||||
|
const updateMetadataServerSettings = createUpdateMetadataServerSettings<'migrateSortSettings'>(() =>
|
||||||
|
makeToast(t('global.error.label.failed_to_save_changes'), 'error'),
|
||||||
|
);
|
||||||
|
const { sortBy, sortOrder } = migrateSortSettings;
|
||||||
|
|
||||||
const { data, loading, error, refetch } = requestManager.useGetMigratableSources({
|
const { data, loading, error, refetch } = requestManager.useGetMigratableSources({
|
||||||
notifyOnNetworkStatusChange: true,
|
notifyOnNetworkStatusChange: true,
|
||||||
});
|
});
|
||||||
const migratableSources = useMemo(() => getMigratableSources(data?.mangas.nodes), [data?.mangas.nodes]);
|
const migratableSources = useMemo(
|
||||||
|
() => getMigratableSources(data?.mangas.nodes, migrateSortSettings),
|
||||||
|
[data?.mangas.nodes, migrateSortSettings],
|
||||||
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingPlaceholder />;
|
return <LoadingPlaceholder />;
|
||||||
@@ -64,12 +108,53 @@ export const Migration = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<>
|
||||||
{Object.values(migratableSources).map((migratableSource) => (
|
<Stack
|
||||||
<StyledGroupItemWrapper key={migratableSource.id}>
|
sx={{
|
||||||
<MigrationCard {...migratableSource} />
|
position: 'sticky',
|
||||||
</StyledGroupItemWrapper>
|
top: `${appBarHeight + tabsMenuHeight}px`,
|
||||||
))}
|
flexDirection: 'row',
|
||||||
</List>
|
justifyContent: 'end',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1,
|
||||||
|
backgroundColor: 'background.default',
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title={t(sortByToTranslationKey[sortBy])}>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
color="inherit"
|
||||||
|
onClick={() =>
|
||||||
|
updateMetadataServerSettings('migrateSortSettings', { sortBy: (sortBy + 1) % 2, sortOrder })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sortBy ? <TagIcon /> : <SortByAlphaIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={t(sortOrderToTranslationKey[sortOrder])}>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
color="inherit"
|
||||||
|
onClick={() =>
|
||||||
|
updateMetadataServerSettings('migrateSortSettings', {
|
||||||
|
sortBy,
|
||||||
|
sortOrder: (sortOrder + 1) % 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sortOrder ? <ArrowDownwardIcon /> : <ArrowUpwardIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
<List sx={{ p: 0 }}>
|
||||||
|
{migratableSources.map((migratableSource) => (
|
||||||
|
<StyledGroupItemWrapper key={migratableSource.id}>
|
||||||
|
<MigrationCard {...migratableSource} />
|
||||||
|
</StyledGroupItemWrapper>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
26
src/screens/Migration.types.ts
Normal file
26
src/screens/Migration.types.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
|
||||||
|
export enum SortBy {
|
||||||
|
SOURCE_NAME,
|
||||||
|
MANGA_COUNT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortOrder {
|
||||||
|
ASC,
|
||||||
|
DESC,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortSettings {
|
||||||
|
sortBy: SortBy;
|
||||||
|
sortOrder: SortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TMigratableSourcesResult = GetMigratableSourcesQuery['mangas']['nodes'];
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
TrackerType,
|
TrackerType,
|
||||||
} from '@/lib/graphql/generated/graphql.ts';
|
} from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { AppTheme } from '@/lib/ui/AppThemes.ts';
|
import { AppTheme } from '@/lib/ui/AppThemes.ts';
|
||||||
|
import { SortSettings } from '@/screens/Migration.types.ts';
|
||||||
|
|
||||||
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
|
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ export type MetadataMigrationSettings = {
|
|||||||
migrateCategories: boolean;
|
migrateCategories: boolean;
|
||||||
migrateTracking: boolean;
|
migrateTracking: boolean;
|
||||||
deleteChapters: boolean;
|
deleteChapters: boolean;
|
||||||
|
migrateSortSettings: SortSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MetadataBrowseSettings = {
|
export type MetadataBrowseSettings = {
|
||||||
|
|||||||
Reference in New Issue
Block a user