Skip to content

Commit de6c2ff

Browse files
committed
feat: implement OPML import functionality with feed selection modal
- Added the DiscoverImport component to handle OPML file uploads and parsing. - Introduced OpmlSelectionModal for users to select feeds to import, including quota management and search functionality. - Enhanced error handling and user feedback during the import process. - Updated translations for new features and improved user experience. Signed-off-by: Innei <[email protected]>
1 parent 7b3ec11 commit de6c2ff

File tree

11 files changed

+601
-132
lines changed

11 files changed

+601
-132
lines changed

apps/desktop/layer/renderer/src/modules/discover/DiscoverImport.tsx

Lines changed: 32 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
AccordionTrigger,
66
} from "@follow/components/ui/accordion/index.js"
77
import { Button } from "@follow/components/ui/button/index.js"
8-
import { Card, CardContent, CardHeader } from "@follow/components/ui/card/index.jsx"
98
import {
109
Form,
1110
FormControl,
@@ -14,27 +13,34 @@ import {
1413
FormMessage,
1514
} from "@follow/components/ui/form/index.jsx"
1615
import { Input } from "@follow/components/ui/input/index.js"
17-
import { cn } from "@follow/utils/utils"
16+
import type { BizRespose } from "@follow/models"
1817
import { zodResolver } from "@hookform/resolvers/zod"
1918
import { useMutation } from "@tanstack/react-query"
20-
import { Fragment } from "react/jsx-runtime"
19+
import { Fragment } from "react"
2120
import { useForm } from "react-hook-form"
2221
import { Trans, useTranslation } from "react-i18next"
2322
import { z } from "zod"
2423

2524
import { DropZone } from "~/components/ui/drop-zone"
2625
import { Media } from "~/components/ui/media"
26+
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
2727
import { apiFetch } from "~/lib/api-fetch"
2828
import { toastFetchError } from "~/lib/error-parser"
29-
import { Queries } from "~/queries"
3029

31-
import { FollowSummary } from "../feed/feed-summary"
30+
import { OpmlSelectionModal } from "./OpmlSelectionModal"
31+
import type { ParsedOpmlData } from "./types"
3232

33-
type FeedResponseList = {
34-
id: string
35-
url: string
36-
title: string | null
37-
}[]
33+
const parseOpmlFile = async (file: File): Promise<ParsedOpmlData> => {
34+
const formData = new FormData()
35+
formData.append("file", file)
36+
37+
const data = await apiFetch<BizRespose<ParsedOpmlData>>("/subscriptions/parse-opml", {
38+
method: "POST",
39+
body: formData,
40+
})
41+
42+
return data.data
43+
}
3844

3945
const formSchema = z.object({
4046
file: z
@@ -47,64 +53,32 @@ const formSchema = z.object({
4753
}),
4854
})
4955

50-
const NumberDisplay = ({ value }) => <span className="font-bold text-zinc-800">{value ?? 0}</span>
51-
52-
const list: {
53-
key: string
54-
title: I18nKeys
55-
className: string
56-
}[] = [
57-
{
58-
key: "parsedErrorItems",
59-
title: "discover.import.parsedErrorItems",
60-
className: "text-red-500",
61-
},
62-
{
63-
key: "successfulItems",
64-
title: "discover.import.successfulItems",
65-
className: "text-green-500",
66-
},
67-
{
68-
key: "conflictItems",
69-
title: "discover.import.conflictItems",
70-
className: "text-yellow-500",
71-
},
72-
]
73-
7456
export function DiscoverImport() {
7557
const form = useForm<z.infer<typeof formSchema>>({
7658
resolver: zodResolver(formSchema),
7759
})
7860

79-
const mutation = useMutation({
80-
mutationFn: async (file: File) => {
81-
const formData = new FormData()
82-
formData.append("file", file)
83-
// FIXME: if post data is form data, hono hc not support this.
61+
const { present } = useModalStack()
8462

85-
const { data } = await apiFetch<{
86-
data: {
87-
successfulItems: FeedResponseList
88-
conflictItems: FeedResponseList
89-
parsedErrorItems: FeedResponseList
90-
}
91-
}>("/subscriptions/import", {
92-
method: "POST",
93-
body: formData,
94-
})
95-
96-
return data
97-
},
98-
onSuccess: () => {
99-
Queries.subscription.all().invalidateRoot()
100-
},
63+
const parseOpmlMutation = useMutation({
64+
mutationFn: parseOpmlFile,
10165
async onError(err) {
10266
toastFetchError(err)
10367
},
10468
})
10569

10670
function onSubmit(values: z.infer<typeof formSchema>) {
107-
mutation.mutate(values.file)
71+
parseOpmlMutation.mutate(values.file, {
72+
onSuccess: (parsedData) => {
73+
present({
74+
title: t("discover.import.preview_opml_content"),
75+
content: () => <OpmlSelectionModal file={values.file} parsedData={parsedData} />,
76+
clickOutsideToDismiss: false,
77+
modalClassName: "max-w-2xl w-full h-[80vh]",
78+
modalContentClassName: "flex flex-col h-full",
79+
})
80+
},
81+
})
10882
}
10983

11084
const { t } = useTranslation()
@@ -180,7 +154,7 @@ export function DiscoverImport() {
180154
</AccordionItem>
181155
<AccordionItem value="other" className="border-b-0">
182156
<AccordionTrigger className="justify-normal gap-2 hover:no-underline">
183-
<i className="i-mgc-rss-cute-fi -ml-[0.14rem] size-6 text-orange-500" />
157+
<i className="i-mgc-rss-cute-fi ml-[-0.14rem] size-6 text-orange-500" />
184158
{t("discover.import.opml_step1_other")}
185159
</AccordionTrigger>
186160
<AccordionContent className="flex flex-col gap-1">
@@ -230,47 +204,13 @@ export function DiscoverImport() {
230204
<Button
231205
type="submit"
232206
disabled={!form.formState.dirtyFields.file}
233-
isLoading={mutation.isPending}
207+
isLoading={parseOpmlMutation.isPending}
234208
>
235209
{t("words.import")}
236210
</Button>
237211
</div>
238212
</form>
239213
</Form>
240-
{mutation.isSuccess && (
241-
<div className="mt-8 w-full max-w-lg">
242-
<Card>
243-
<CardHeader className="block text-zinc-500">
244-
<Trans
245-
ns="app"
246-
i18nKey="discover.import.result"
247-
components={{
248-
SuccessfulNum: <NumberDisplay value={mutation.data?.successfulItems.length} />,
249-
ConflictNum: <NumberDisplay value={mutation.data?.conflictItems.length} />,
250-
ErrorNum: <NumberDisplay value={mutation.data?.parsedErrorItems.length} />,
251-
}}
252-
/>
253-
</CardHeader>
254-
<CardContent className="space-y-6">
255-
{list.map((item) => (
256-
<div key={item.key}>
257-
<div className={cn("mb-4 text-lg font-medium", item.className)}>
258-
{t(item.title)}
259-
</div>
260-
<div className="space-y-4">
261-
{!mutation.data?.[item.key].length && (
262-
<div className="text-zinc-500">{t("discover.import.noItems")}</div>
263-
)}
264-
{mutation.data?.[item.key].map((feed) => (
265-
<FollowSummary className="max-w-[462px]" key={feed.id} feed={feed} />
266-
))}
267-
</div>
268-
</div>
269-
))}
270-
</CardContent>
271-
</Card>
272-
</div>
273-
)}
274214
</div>
275215
)
276216
}

0 commit comments

Comments
 (0)