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

Commit bce4832

Browse files
committed
feat: add scheduling
1 parent cc2d6dd commit bce4832

26 files changed

+368
-538
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,6 @@ $RECYCLE.BIN/
379379
# and uncomment the following lines
380380
# .pnp.*
381381

382-
# End of https://www.toptal.com/developers/gitignore/api/linux,windows,macos,node,visualstudiocode,intellij+all,nextjs,yarn
382+
# End of https://www.toptal.com/developers/gitignore/api/linux,windows,macos,node,visualstudiocode,intellij+all,nextjs,yarn
383+
384+
mangas/

prisma/migrations/20221006210146_init/migration.sql renamed to prisma/migrations/20221014002316_init/migration.sql

+2-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ CREATE TABLE "Manga" (
1111
"cover" TEXT NOT NULL,
1212
"interval" TEXT NOT NULL,
1313
"source" TEXT NOT NULL,
14-
"query" TEXT NOT NULL,
15-
"libraryId" INTEGER,
16-
CONSTRAINT "Manga_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library" ("id") ON DELETE SET NULL ON UPDATE CASCADE
14+
"libraryId" INTEGER NOT NULL,
15+
CONSTRAINT "Manga_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
1716
);

prisma/schema.prisma

+2-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ model Manga {
2323
cover String
2424
interval String
2525
source String
26-
query String
27-
Library Library? @relation(fields: [libraryId], references: [id])
28-
libraryId Int?
26+
Library Library @relation(fields: [libraryId], references: [id])
27+
libraryId Int
2928
}

src/components/addLibrary.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ function Form({ onClose }: { onClose: () => void }) {
2929
<form
3030
onSubmit={form.onSubmit(async (values) => {
3131
setVisible((v) => !v);
32+
let library = null;
3233
try {
33-
await libraryMutation.mutateAsync({
34+
library = await libraryMutation.mutateAsync({
3435
path: values.library.path,
3536
});
3637
} catch (err) {
@@ -61,7 +62,7 @@ function Form({ onClose }: { onClose: () => void }) {
6162
title: 'Library',
6263
message: (
6364
<Text>
64-
Library is created at <Code color="blue">{values.library.path}</Code>
65+
Library is created at <Code color="blue">{library.path}</Code>
6566
</Text>
6667
),
6768
});

src/components/addManga/form.tsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const useStyles = createStyles((theme) => ({
2727
const schema = z.object({
2828
source: z.string().min(1, { message: 'You must select a source' }),
2929
query: z.string().min(1, { message: 'Cannot be empty' }),
30-
mangaOrder: z.number().gte(0, { message: 'Please select a manga' }),
3130
mangaTitle: z.string().min(1, { message: 'Please select a manga' }),
3231
interval: z.string().min(1, { message: 'Please select an interval' }),
3332
});
@@ -46,7 +45,6 @@ export function AddMangaForm({ onClose }: { onClose: () => void }) {
4645
initialValues: {
4746
source: '',
4847
query: '',
49-
mangaOrder: -1,
5048
mangaTitle: '',
5149
interval: '',
5250
},
@@ -62,8 +60,7 @@ export function AddMangaForm({ onClose }: { onClose: () => void }) {
6260
}
6361
if (active === 1) {
6462
form.validateField('mangaTitle');
65-
form.validateField('mangaOrder');
66-
if (!form.isValid('mangaOrder') || !form.isValid('mangaTitle')) {
63+
if (!form.isValid('mangaTitle')) {
6764
return;
6865
}
6966
}
@@ -89,7 +86,6 @@ export function AddMangaForm({ onClose }: { onClose: () => void }) {
8986
}
9087
if (active === 2) {
9188
form.setFieldValue('query', '');
92-
form.setFieldValue('mangaOrder', -1);
9389
form.setFieldValue('mangaTitle', '');
9490
form.setFieldValue('interval', '');
9591
}
@@ -101,11 +97,9 @@ export function AddMangaForm({ onClose }: { onClose: () => void }) {
10197

10298
const onSubmit = form.onSubmit(async (values) => {
10399
setVisible((v) => !v);
104-
const { mangaOrder, mangaTitle, query, source, interval } = values;
100+
const { mangaTitle, source, interval } = values;
105101
try {
106102
await mutation.mutateAsync({
107-
keyword: query,
108-
order: mangaOrder,
109103
title: mangaTitle,
110104
interval,
111105
source,

src/components/addManga/mangaSearchResult.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ ImageCheckbox.defaultProps = {
9898
type IMangaSearchResult = {
9999
status: string;
100100
title: string;
101-
order: number;
102101
cover: string;
103102
};
104103

src/components/addManga/steps/downloadStep.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Box, LoadingOverlay, Select, Stack, TextInput } from '@mantine/core';
22
import { UseFormReturnType } from '@mantine/form';
33
import { IconFolderPlus } from '@tabler/icons';
4+
import { sanitizer } from '../../../utils/sanitize';
45
import { trpc } from '../../../utils/trpc';
56
import type { FormType } from '../form';
67

@@ -17,12 +18,7 @@ export function DownloadStep({ form }: { form: UseFormReturnType<FormType> }) {
1718
return <LoadingOverlay visible />;
1819
}
1920

20-
const sanitizeMangaName = form.values.mangaTitle
21-
.replaceAll(/[\\/<>:;"'|?!*{}#%&^+,~\s]/g, '_')
22-
.replaceAll(/__+/g, '_')
23-
.replaceAll(/^[_\-.]+|[_\-.]+$/g, '_');
24-
25-
const downloadPath = `${libraryPath}/${sanitizeMangaName}`;
21+
const downloadPath = `${libraryPath}/${sanitizer(form.values.mangaTitle)}`;
2622

2723
return (
2824
<Box>

src/components/addManga/steps/reviewStep.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ const useStyles = createStyles((_theme) => ({
1313
export function ReviewStep({ form }: { form: UseFormReturnType<FormType> }) {
1414
const query = trpc.manga.detail.useQuery(
1515
{
16-
keyword: form.values.query,
1716
source: form.values.source,
18-
order: form.values.mangaOrder,
17+
title: form.values.mangaTitle,
1918
},
2019
{
2120
staleTime: Infinity,
22-
enabled: !!form.values.query && !!form.values.source && !!form.values.mangaTitle && form.values.mangaOrder > -1,
21+
enabled: !!form.values.source && !!form.values.mangaTitle,
2322
},
2423
);
2524

src/components/addManga/steps/searchStep.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export function SearchStep({ form }: { form: UseFormReturnType<FormType> }) {
1919
if (!form.isValid('query')) {
2020
return;
2121
}
22-
form.setFieldValue('mangaOrder', -1);
2322
form.setFieldValue('mangaTitle', '');
2423
setLoading(true);
2524
const result = await ctx.manga.search.fetch({
@@ -59,10 +58,8 @@ export function SearchStep({ form }: { form: UseFormReturnType<FormType> }) {
5958
items={searchResult}
6059
onSelect={(selected) => {
6160
if (selected) {
62-
form.setFieldValue('mangaOrder', selected.order);
6361
form.setFieldValue('mangaTitle', selected.title);
6462
} else {
65-
form.setFieldValue('mangaOrder', -1);
6663
form.setFieldValue('mangaTitle', '');
6764
}
6865
}}

src/components/mangaCard.tsx

+43-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { Badge, Button, createStyles, Paper, Title } from '@mantine/core';
2-
import { IconExternalLink } from '@tabler/icons';
1+
import { ActionIcon, Badge, Button, Code, createStyles, Paper, Text, Title } from '@mantine/core';
2+
import { openConfirmModal } from '@mantine/modals';
3+
import { IconExternalLink, IconX } from '@tabler/icons';
34

4-
const useStyles = createStyles((theme) => ({
5+
const useStyles = createStyles((theme, _params, getRef) => ({
56
card: {
7+
position: 'relative',
68
height: 350,
79
width: 210,
810
display: 'flex',
@@ -18,8 +20,17 @@ const useStyles = createStyles((theme) => ({
1820
transform: 'scale(1.01)',
1921
boxShadow: theme.shadows.md,
2022
},
23+
[`&:hover .${getRef('removeButton')}`]: {
24+
display: 'flex',
25+
},
26+
},
27+
removeButton: {
28+
ref: getRef('removeButton'),
29+
position: 'absolute',
30+
right: -5,
31+
top: -5,
32+
display: 'none',
2133
},
22-
2334
title: {
2435
fontFamily: `${theme.fontFamily}`,
2536
fontWeight: 900,
@@ -37,10 +48,34 @@ interface ArticleCardImageProps {
3748
image: string;
3849
title: string;
3950
category?: string;
51+
onRemove: () => void;
4052
}
4153

42-
export function MangaCard({ image, title, category }: ArticleCardImageProps) {
54+
const createRemoveModal = (title: string, onRemove: () => void) => {
55+
const openDeleteModal = () =>
56+
openConfirmModal({
57+
title: `Delete ${title}?`,
58+
centered: true,
59+
children: (
60+
<Text size="sm">
61+
Are you sure you want to delete
62+
<Code className="text-base font-bold" color="red">
63+
{title}
64+
</Code>
65+
? This action is destructive and all downloaded files will be removed
66+
</Text>
67+
),
68+
labels: { confirm: 'Delete', cancel: 'Cancel' },
69+
confirmProps: { color: 'red' },
70+
onConfirm: onRemove,
71+
});
72+
73+
return openDeleteModal;
74+
};
75+
76+
export function MangaCard({ image, title, category, onRemove }: ArticleCardImageProps) {
4377
const { classes } = useStyles();
78+
const removeModal = createRemoveModal(title, onRemove);
4479

4580
return (
4681
<Paper
@@ -50,6 +85,9 @@ export function MangaCard({ image, title, category }: ArticleCardImageProps) {
5085
sx={{ backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.5)), url(${image})` }}
5186
className={classes.card}
5287
>
88+
<ActionIcon color="red" variant="filled" radius="xl" className={classes.removeButton} onClick={removeModal}>
89+
<IconX size={16} />
90+
</ActionIcon>
5391
<div>
5492
{category && (
5593
<Badge color="teal" variant="filled" className={classes.category} size="md">

src/pages/index.tsx

+42-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Grid, LoadingOverlay } from '@mantine/core';
1+
import { Code, Grid, LoadingOverlay, Text } from '@mantine/core';
2+
import { showNotification } from '@mantine/notifications';
3+
import { IconCheck, IconX } from '@tabler/icons';
24
import { AddManga } from '../components/addManga';
35

46
import { EmptyPrompt } from '../components/emptyPrompt';
@@ -7,6 +9,7 @@ import { trpc } from '../utils/trpc';
79

810
export default function IndexPage() {
911
const libraryQuery = trpc.library.query.useQuery();
12+
const mangaDelete = trpc.manga.remove.useMutation();
1013

1114
const libraryId = libraryQuery.data?.id;
1215

@@ -34,6 +37,38 @@ export default function IndexPage() {
3437
);
3538
}
3639

40+
const handleDelete = async (id: number, title: string) => {
41+
try {
42+
await mangaDelete.mutateAsync({
43+
id,
44+
});
45+
showNotification({
46+
icon: <IconCheck size={18} />,
47+
color: 'teal',
48+
autoClose: true,
49+
title: 'Manga',
50+
message: (
51+
<Text>
52+
<Code color="blue">{title}</Code> is removed from library
53+
</Text>
54+
),
55+
});
56+
} catch (err) {
57+
showNotification({
58+
icon: <IconX size={18} />,
59+
color: 'red',
60+
autoClose: true,
61+
title: 'Manga',
62+
message: (
63+
<Text>
64+
<Code color="red">{`${err}`}</Code>
65+
</Text>
66+
),
67+
});
68+
}
69+
mangaQuery.refetch();
70+
};
71+
3772
return (
3873
<Grid justify="flex-start">
3974
<Grid.Col span="content">
@@ -47,7 +82,12 @@ export default function IndexPage() {
4782
mangaQuery.data.map((manga) => {
4883
return (
4984
<Grid.Col span="content" key={manga.id}>
50-
<MangaCard category={manga.interval} title={manga.title} image={manga.cover} />
85+
<MangaCard
86+
category={manga.interval}
87+
title={manga.title}
88+
image={manga.cover}
89+
onRemove={() => handleDelete(manga.id, manga.title)}
90+
/>
5191
</Grid.Col>
5292
);
5393
})}

src/server/downloader/bullboard.ts

-12
This file was deleted.

0 commit comments

Comments
 (0)