diff --git a/src/components/addManga/steps/reviewStep.tsx b/src/components/addManga/steps/reviewStep.tsx index 57cde94..47fe606 100644 --- a/src/components/addManga/steps/reviewStep.tsx +++ b/src/components/addManga/steps/reviewStep.tsx @@ -1,5 +1,22 @@ -import { Badge, createStyles, Divider, Grid, Group, Image, LoadingOverlay, Text, Title, Tooltip } from '@mantine/core'; +import { + ActionIcon, + Badge, + createStyles, + Divider, + Grid, + Group, + Image, + LoadingOverlay, + Popover, + Text, + TextInput, + Title, + Tooltip, +} from '@mantine/core'; import { UseFormReturnType } from '@mantine/form'; +import { getHotkeyHandler } from '@mantine/hooks'; +import { IconCheck, IconEdit, IconGitMerge } from '@tabler/icons'; +import { useState } from 'react'; import { trpc } from '../../../utils/trpc'; import type { FormType } from '../form'; @@ -11,6 +28,9 @@ const useStyles = createStyles((_theme) => ({ })); export function ReviewStep({ form }: { form: UseFormReturnType }) { + const [anilistId, setAnilistId] = useState(); + const [opened, setOpened] = useState(false); + const bindMutation = trpc.manga.bind.useMutation(); const query = trpc.manga.detail.useQuery( { source: form.values.source, @@ -26,9 +46,23 @@ export function ReviewStep({ form }: { form: UseFormReturnType }) { const manga = query.data; + const handleBind = async () => { + setOpened(false); + if (!anilistId || !manga?.Name) { + return; + } + await bindMutation.mutateAsync({ + anilistId, + title: manga.Name, + }); + + query.refetch(); + setAnilistId(''); + }; + return ( <> - + {manga && ( @@ -55,7 +89,67 @@ export function ReviewStep({ form }: { form: UseFormReturnType }) { /> - {manga.Name}} /> + + {manga.Name} + + + + setOpened((o) => !o)} + > + + + + + ({ + background: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white, + })} + > + setAnilistId(event.currentTarget.value)} + onKeyDown={getHotkeyHandler([['Enter', handleBind]])} + icon={} + rightSection={ + + + + } + rightSectionWidth={42} + label={ + + Please enter a new AniList id for {manga.Name} + + } + placeholder="AniList Id" + /> + + + + } + /> {manga.Metadata.Synonyms && ( {manga.Metadata.Synonyms.map((synonym) => ( diff --git a/src/server/trpc/router/manga.ts b/src/server/trpc/router/manga.ts index 65bebe0..102d4ce 100644 --- a/src/server/trpc/router/manga.ts +++ b/src/server/trpc/router/manga.ts @@ -4,7 +4,14 @@ import { z } from 'zod'; import { isCronValid, sanitizer } from '../../../utils'; import { checkChaptersQueue, removeJob, schedule } from '../../queue/checkChapters'; import { downloadQueue } from '../../queue/download'; -import { getAvailableSources, getMangaDetail, Manga, removeManga, search } from '../../utils/mangal'; +import { + bindTitleToAnilistId, + getAvailableSources, + getMangaDetail, + Manga, + removeManga, + search, +} from '../../utils/mangal'; import { t } from '../trpc'; export const mangaRouter = t.router({ @@ -14,6 +21,17 @@ export const mangaRouter = t.router({ sources: t.procedure.query(async () => { return getAvailableSources(); }), + bind: t.procedure + .input( + z.object({ + title: z.string().trim().min(1), + anilistId: z.string().trim().min(1), + }), + ) + .mutation(async ({ input }) => { + const { title, anilistId } = input; + await bindTitleToAnilistId(title, anilistId); + }), detail: t.procedure .input( z.object({ diff --git a/src/server/utils/mangal.ts b/src/server/utils/mangal.ts index 21aa6c8..d106fda 100644 --- a/src/server/utils/mangal.ts +++ b/src/server/utils/mangal.ts @@ -63,6 +63,15 @@ export const getAvailableSources = async () => { return []; }; +export const bindTitleToAnilistId = async (title: string, anilistId: string) => { + try { + const { command } = await execa('mangal', ['inline', 'anilist', 'set', '--name', title, '--id', anilistId]); + logger.info(`Bind manga to anilist id with following command: ${command}`); + } catch (err) { + logger.error(`Failed to bind manga to anilist id. err: ${err}`); + } +}; + export const getMangaPath = (libraryPath: string, title: string) => path.resolve(libraryPath, sanitizer(title)); export const search = async (source: string, keyword: string): Promise => { @@ -71,7 +80,7 @@ export const search = async (source: string, keyword: string): Promise logger.info(`Search manga with following command: ${command}`); return JSON.parse(stdout); } catch (err) { - logger.error(`Failed to get available sources: err: ${err}`); + logger.error(`Failed to search manga. err: ${err}`); } return {