Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit cba8a74

Browse files
committed
feat: add refresh metadata button to cards
1 parent f75c878 commit cba8a74

File tree

6 files changed

+287
-105
lines changed

6 files changed

+287
-105
lines changed

src/components/mangaCard.tsx

+52-104
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
1-
import {
2-
ActionIcon,
3-
Alert,
4-
Badge,
5-
Box,
6-
Button,
7-
Checkbox,
8-
Code,
9-
createStyles,
10-
Paper,
11-
Skeleton,
12-
Text,
13-
Title,
14-
} from '@mantine/core';
15-
import { useModals } from '@mantine/modals';
1+
import { ActionIcon, Badge, createStyles, Paper, Skeleton, Title, Tooltip } from '@mantine/core';
162
import { Prisma } from '@prisma/client';
17-
import { IconEdit, IconX } from '@tabler/icons';
3+
import { IconEdit, IconRefresh, IconX } from '@tabler/icons';
184
import { contrastColor } from 'contrast-color';
19-
import { useState } from 'react';
205
import stc from 'string-to-color';
6+
import { useRefreshModal } from './refreshMetadata';
7+
import { useRemoveModal } from './removeManga';
218
import { useUpdateModal } from './updateManga';
229

2310
const useStyles = createStyles((theme, _params, getRef) => ({
@@ -49,6 +36,9 @@ const useStyles = createStyles((theme, _params, getRef) => ({
4936
[`&:hover .${getRef('editButton')}`]: {
5037
display: 'flex',
5138
},
39+
[`&:hover .${getRef('refreshButton')}`]: {
40+
display: 'flex',
41+
},
5242
},
5343
removeButton: {
5444
ref: getRef('removeButton'),
@@ -57,6 +47,18 @@ const useStyles = createStyles((theme, _params, getRef) => ({
5747
top: -5,
5848
display: 'none',
5949
},
50+
refreshButton: {
51+
ref: getRef('refreshButton'),
52+
backgroundColor: theme.white,
53+
color: theme.colors.blue[6],
54+
position: 'absolute',
55+
right: 10,
56+
bottom: 50,
57+
display: 'none',
58+
'&:hover': {
59+
backgroundColor: theme.colors.gray[0],
60+
},
61+
},
6062
editButton: {
6163
ref: getRef('editButton'),
6264
backgroundColor: theme.white,
@@ -93,92 +95,21 @@ interface MangaCardProps {
9395
manga: MangaWithLibraryAndMetadata;
9496
onRemove: (shouldRemoveFiles: boolean) => void;
9597
onUpdate: () => void;
98+
onRefresh: () => void;
9699
onClick: () => void;
97100
}
98101

99-
function RemoveModalContent({
100-
title,
101-
onRemove,
102-
onClose,
103-
}: {
104-
title: string;
105-
onRemove: (shouldRemoveFiles: boolean) => void;
106-
onClose: () => void;
107-
}) {
108-
const [shouldRemoveFiles, setShouldRemoveFiles] = useState(false);
109-
return (
110-
<>
111-
<Text mb={4} size="sm">
112-
Are you sure you want to remove
113-
<Code className="text-base font-bold" color="red">
114-
{title}
115-
</Code>
116-
?
117-
</Text>
118-
<Alert
119-
icon={
120-
<Checkbox
121-
checked={shouldRemoveFiles}
122-
color="red"
123-
onChange={(event) => setShouldRemoveFiles(event.currentTarget.checked)}
124-
/>
125-
}
126-
title="Remove files?"
127-
color="red"
128-
>
129-
This action is destructive and all downloaded files will be removed
130-
</Alert>
131-
<Box
132-
sx={(theme) => ({
133-
display: 'flex',
134-
gap: theme.spacing.xs,
135-
justifyContent: 'end',
136-
marginTop: theme.spacing.md,
137-
})}
138-
>
139-
<Button variant="default" color="dark" onClick={onClose}>
140-
Cancel
141-
</Button>
142-
<Button
143-
variant="filled"
144-
color="red"
145-
onClick={() => {
146-
onRemove(shouldRemoveFiles);
147-
onClose();
148-
}}
149-
>
150-
Remove
151-
</Button>
152-
</Box>
153-
</>
154-
);
155-
}
156-
157-
const useRemoveModal = (title: string, onRemove: (shouldRemoveFiles: boolean) => void) => {
158-
const modals = useModals();
159-
160-
const openRemoveModal = () => {
161-
const id = modals.openModal({
162-
title: `Remove ${title}?`,
163-
centered: true,
164-
children: <RemoveModalContent title={title} onRemove={onRemove} onClose={() => modals.closeModal(id)} />,
165-
});
166-
};
167-
168-
return openRemoveModal;
169-
};
170-
171102
export function SkeletonMangaCard() {
172103
const { classes } = useStyles();
173104

174105
return <Skeleton radius="md" className={classes.skeletonCard} />;
175106
}
176107

177-
export function MangaCard({ manga, onRemove, onUpdate, onClick }: MangaCardProps) {
108+
export function MangaCard({ manga, onRemove, onUpdate, onRefresh, onClick }: MangaCardProps) {
178109
const { classes } = useStyles();
179110
const removeModal = useRemoveModal(manga.title, onRemove);
111+
const refreshModal = useRefreshModal(manga.title, onRefresh);
180112
const updateModal = useUpdateModal(manga, onUpdate);
181-
182113
return (
183114
<Paper
184115
onClick={onClick}
@@ -202,19 +133,36 @@ export function MangaCard({ manga, onRemove, onUpdate, onClick }: MangaCardProps
202133
>
203134
<IconX size={16} />
204135
</ActionIcon>
205-
<ActionIcon
206-
color="blue"
207-
variant="light"
208-
size="lg"
209-
radius="xl"
210-
className={classes.editButton}
211-
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
212-
e.stopPropagation();
213-
updateModal();
214-
}}
215-
>
216-
<IconEdit size={18} />
217-
</ActionIcon>
136+
<Tooltip withinPortal withArrow label="Refresh Metadata" position="left">
137+
<ActionIcon
138+
color="teal"
139+
variant="filled"
140+
size="lg"
141+
radius="xl"
142+
className={classes.refreshButton}
143+
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
144+
e.stopPropagation();
145+
refreshModal();
146+
}}
147+
>
148+
<IconRefresh size={18} />
149+
</ActionIcon>
150+
</Tooltip>
151+
<Tooltip withinPortal withArrow label="Edit" position="left">
152+
<ActionIcon
153+
color="blue"
154+
variant="light"
155+
size="lg"
156+
radius="xl"
157+
className={classes.editButton}
158+
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
159+
e.stopPropagation();
160+
updateModal();
161+
}}
162+
>
163+
<IconEdit size={18} />
164+
</ActionIcon>
165+
</Tooltip>
218166
<div>
219167
<Badge
220168
sx={{ backgroundColor: stc(manga.source), color: contrastColor({ bgColor: stc(manga.source) }) }}

src/components/refreshMetadata.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Box, Button, Text } from '@mantine/core';
2+
import { useModals } from '@mantine/modals';
3+
4+
function RefreshMetadataModalContent({ onRefresh, onClose }: { onRefresh: () => void; onClose: () => void }) {
5+
return (
6+
<>
7+
<Text mb={4} size="sm">
8+
This will update all downloaded chapters with the latest metadata from AniList
9+
</Text>
10+
<Box
11+
sx={(theme) => ({
12+
display: 'flex',
13+
gap: theme.spacing.xs,
14+
justifyContent: 'end',
15+
marginTop: theme.spacing.md,
16+
})}
17+
>
18+
<Button variant="default" color="dark" onClick={onClose}>
19+
Cancel
20+
</Button>
21+
<Button
22+
variant="filled"
23+
color="teal"
24+
onClick={() => {
25+
onRefresh();
26+
onClose();
27+
}}
28+
>
29+
Refresh
30+
</Button>
31+
</Box>
32+
</>
33+
);
34+
}
35+
36+
export const useRefreshModal = (title: string, onRefresh: () => void) => {
37+
const modals = useModals();
38+
39+
const openRemoveModal = () => {
40+
const id = modals.openModal({
41+
title: `Refresh Metadata for ${title}?`,
42+
centered: true,
43+
children: <RefreshMetadataModalContent onRefresh={onRefresh} onClose={() => modals.closeModal(id)} />,
44+
});
45+
};
46+
47+
return openRemoveModal;
48+
};

src/components/removeManga.tsx

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Alert, Box, Button, Checkbox, Code, Text } from '@mantine/core';
2+
import { useModals } from '@mantine/modals';
3+
import { useState } from 'react';
4+
5+
function RemoveModalContent({
6+
title,
7+
onRemove,
8+
onClose,
9+
}: {
10+
title: string;
11+
onRemove: (shouldRemoveFiles: boolean) => void;
12+
onClose: () => void;
13+
}) {
14+
const [shouldRemoveFiles, setShouldRemoveFiles] = useState(false);
15+
return (
16+
<>
17+
<Text mb={4} size="sm">
18+
Are you sure you want to remove
19+
<Code className="text-base font-bold" color="red">
20+
{title}
21+
</Code>
22+
?
23+
</Text>
24+
<Alert
25+
icon={
26+
<Checkbox
27+
checked={shouldRemoveFiles}
28+
color="red"
29+
onChange={(event) => setShouldRemoveFiles(event.currentTarget.checked)}
30+
/>
31+
}
32+
title="Remove files?"
33+
color="red"
34+
>
35+
This action is destructive and all downloaded files will be removed
36+
</Alert>
37+
<Box
38+
sx={(theme) => ({
39+
display: 'flex',
40+
gap: theme.spacing.xs,
41+
justifyContent: 'end',
42+
marginTop: theme.spacing.md,
43+
})}
44+
>
45+
<Button variant="default" color="dark" onClick={onClose}>
46+
Cancel
47+
</Button>
48+
<Button
49+
variant="filled"
50+
color="red"
51+
onClick={() => {
52+
onRemove(shouldRemoveFiles);
53+
onClose();
54+
}}
55+
>
56+
Remove
57+
</Button>
58+
</Box>
59+
</>
60+
);
61+
}
62+
63+
export const useRemoveModal = (title: string, onRemove: (shouldRemoveFiles: boolean) => void) => {
64+
const modals = useModals();
65+
66+
const openRemoveModal = () => {
67+
const id = modals.openModal({
68+
title: `Remove ${title}?`,
69+
centered: true,
70+
children: <RemoveModalContent title={title} onRemove={onRemove} onClose={() => modals.closeModal(id)} />,
71+
});
72+
};
73+
74+
return openRemoveModal;
75+
};

src/pages/index.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { trpc } from '../utils/trpc';
1111
export default function IndexPage() {
1212
const libraryQuery = trpc.library.query.useQuery();
1313
const mangaRemove = trpc.manga.remove.useMutation();
14+
const mangaRefresh = trpc.manga.refreshMetaData.useMutation();
1415
const router = useRouter();
1516

1617
const mangaQuery = trpc.manga.query.useQuery();
@@ -79,6 +80,38 @@ export default function IndexPage() {
7980
mangaQuery.refetch();
8081
};
8182

83+
const handleRefresh = async (id: number, title: string) => {
84+
try {
85+
await mangaRefresh.mutateAsync({
86+
id,
87+
});
88+
showNotification({
89+
icon: <IconCheck size={18} />,
90+
color: 'teal',
91+
autoClose: true,
92+
title: 'Manga',
93+
message: (
94+
<Text>
95+
<Code color="blue">{title}</Code> chapters are queued for the metadata update
96+
</Text>
97+
),
98+
});
99+
} catch (err) {
100+
showNotification({
101+
icon: <IconX size={18} />,
102+
color: 'red',
103+
autoClose: true,
104+
title: 'Manga',
105+
message: (
106+
<Text>
107+
<Code color="red">{`${err}`}</Code>
108+
</Text>
109+
),
110+
});
111+
}
112+
mangaQuery.refetch();
113+
};
114+
82115
return (
83116
<ScrollArea sx={{ height: 'calc(100vh - 88px)' }}>
84117
<Grid m={12} justify="flex-start">
@@ -91,6 +124,7 @@ export default function IndexPage() {
91124
<Grid.Col span="content" key={manga.id}>
92125
<MangaCard
93126
manga={manga}
127+
onRefresh={() => handleRefresh(manga.id, manga.title)}
94128
onUpdate={() => mangaQuery.refetch()}
95129
onRemove={(shouldRemoveFiles: boolean) => handleRemove(manga.id, manga.title, shouldRemoveFiles)}
96130
onClick={() => router.push(`/manga/${manga.id}`)}

0 commit comments

Comments
 (0)