|
1 | 1 | <script lang="ts"> |
2 | 2 | import type { groupFeeds } from './+page'; |
3 | 3 | import * as Sheet from '$lib/components/ui/sheet'; |
4 | | - import * as Select from '$lib/components/ui/select'; |
5 | 4 | import * as Tabs from '$lib/components/ui/tabs'; |
6 | 5 | import { Button } from '$lib/components/ui/button'; |
7 | 6 | import { Label } from '$lib/components/ui/label'; |
8 | 7 | import { createFeed } from '$lib/api/feed'; |
9 | 8 | import { toast } from 'svelte-sonner'; |
10 | 9 | import { invalidateAll } from '$app/navigation'; |
11 | 10 | import { dump, parse } from '$lib/opml'; |
| 11 | + import { FolderIcon } from 'lucide-svelte'; |
| 12 | + import { createGroup } from '$lib/api/group'; |
12 | 13 |
|
13 | 14 | export let groups: groupFeeds[]; |
14 | 15 | export let open: boolean; |
15 | 16 |
|
16 | 17 | let uploadedOpmls: FileList; |
17 | 18 | $: parseOPML(uploadedOpmls); |
18 | | - let opmlGroup = { id: groups[0].id, name: groups[0].name }; |
19 | | - let parsedOpmlFeeds: { name: string; link: string }[] = []; |
| 19 | + let parsedGroupFeeds: { name: string; feeds: { name: string; link: string }[] }[] = []; |
| 20 | + let importing = false; |
| 21 | +
|
20 | 22 | $: { |
21 | 23 | if (!open) { |
22 | | - parsedOpmlFeeds = []; |
| 24 | + parsedGroupFeeds = []; |
23 | 25 | } |
24 | 26 | } |
25 | 27 |
|
26 | 28 | function parseOPML(opmls: FileList) { |
27 | 29 | if (!opmls) return; |
| 30 | +
|
28 | 31 | const reader = new FileReader(); |
29 | 32 | reader.onload = (f) => { |
30 | 33 | const content = f.target?.result?.toString(); |
31 | 34 | if (!content) { |
32 | 35 | toast.error('Failed to load file content'); |
33 | 36 | return; |
34 | 37 | } |
35 | | - parsedOpmlFeeds = parse(content); |
36 | | - console.log(parsedOpmlFeeds); |
| 38 | + parsedGroupFeeds = parse(content).filter((v) => v.feeds.length > 0); |
| 39 | + console.log(parsedGroupFeeds); |
37 | 40 | }; |
38 | 41 | reader.readAsText(opmls[0]); |
39 | 42 | } |
40 | 43 |
|
41 | 44 | async function handleImportFeeds() { |
42 | | - try { |
43 | | - await createFeed({ group_id: opmlGroup.id, feeds: parsedOpmlFeeds }); |
44 | | - toast.success('Feeds have been imported. Refreshing is running in the background'); |
45 | | - } catch (e) { |
46 | | - toast.error((e as Error).message); |
| 45 | + importing = true; |
| 46 | + let success = 0; |
| 47 | + const existingGroups = groups.map((v) => { |
| 48 | + return { id: v.id, name: v.name }; |
| 49 | + }); |
| 50 | + for (const g of parsedGroupFeeds) { |
| 51 | + try { |
| 52 | + let groupID = existingGroups.find((v) => v.name === g.name)?.id; |
| 53 | + if (groupID === undefined) { |
| 54 | + groupID = (await createGroup(g.name)).id; |
| 55 | + toast.success(`Created group ${g.name}`); |
| 56 | + } |
| 57 | + await createFeed({ group_id: groupID, feeds: g.feeds }); |
| 58 | + toast.success(`Imported into group ${g.name}`); |
| 59 | + success++; |
| 60 | + } catch (e) { |
| 61 | + toast.error(`Failed to import group ${g.name}, error: ${(e as Error).message}`); |
| 62 | + break; |
| 63 | + } |
| 64 | + } |
| 65 | + if (success === parsedGroupFeeds.length) { |
| 66 | + toast.success('All feeds have been imported. Refreshing is running in the background'); |
47 | 67 | } |
| 68 | + importing = false; |
48 | 69 | invalidateAll(); |
49 | 70 | } |
50 | 71 |
|
|
69 | 90 | </script> |
70 | 91 |
|
71 | 92 | <Sheet.Root bind:open> |
72 | | - <Sheet.Content class="w-full md:w-auto"> |
| 93 | + <Sheet.Content class="w-full md:max-w-[700px] overflow-y-auto"> |
73 | 94 | <Sheet.Header> |
74 | 95 | <Sheet.Title>Import or Export Feeds</Sheet.Title> |
75 | 96 | <Sheet.Description> |
|
87 | 108 | </Tabs.List> |
88 | 109 | <Tabs.Content value="import"> |
89 | 110 | <form class="space-y-2" on:submit|preventDefault={handleImportFeeds}> |
90 | | - <div> |
91 | | - <Label for="group">Group</Label> |
92 | | - <Select.Root |
93 | | - disabled={groups.length < 2} |
94 | | - items={groups.map((v) => { |
95 | | - return { value: v.id, label: v.name }; |
96 | | - })} |
97 | | - onSelectedChange={(v) => v && (opmlGroup.id = v.value)} |
98 | | - > |
99 | | - <Select.Trigger> |
100 | | - <Select.Value placeholder={opmlGroup.name} /> |
101 | | - </Select.Trigger> |
102 | | - <Select.Content> |
103 | | - {#each groups as g} |
104 | | - <Select.Item value={g.id}>{g.name}</Select.Item> |
105 | | - {/each} |
106 | | - </Select.Content> |
107 | | - </Select.Root> |
108 | | - </div> |
109 | 111 | <div> |
110 | 112 | <Label for="feed_file">File</Label> |
111 | 113 | <input |
|
117 | 119 | class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" |
118 | 120 | /> |
119 | 121 | </div> |
120 | | - {#if parsedOpmlFeeds.length > 0} |
| 122 | + {#if parsedGroupFeeds.length > 0} |
121 | 123 | <div> |
122 | | - <p class="text-sm text-muted-foreground">Parsed out {parsedOpmlFeeds.length} feeds</p> |
| 124 | + <p class="text-sm text-green-700">Parsed successfully.</p> |
123 | 125 | <div |
124 | | - class="max-h-[200px] overflow-scroll p-2 rounded-md border bg-muted text-muted-foreground text-nowrap" |
| 126 | + class="p-2 rounded-md border bg-muted/40 text-muted-foreground text-nowrap overflow-x-auto" |
125 | 127 | > |
126 | | - <ul> |
127 | | - {#each parsedOpmlFeeds as feed, index} |
128 | | - <li>{index + 1}. <b>{feed.name}</b> {feed.link}</li> |
129 | | - {/each} |
130 | | - </ul> |
| 128 | + {#each parsedGroupFeeds as group} |
| 129 | + <div class="flex flex-row items-center gap-1"> |
| 130 | + <FolderIcon size={14} />{group.name} |
| 131 | + </div> |
| 132 | + <ul class="list-inside list-decimal ml-[2ch] [&:not(:last-child)]:mb-2"> |
| 133 | + {#each group.feeds as feed} |
| 134 | + <li>{feed.name}, {feed.link}</li> |
| 135 | + {/each} |
| 136 | + </ul> |
| 137 | + {/each} |
131 | 138 | </div> |
132 | 139 | </div> |
133 | 140 | {/if} |
134 | | - <Button type="submit">Import</Button> |
| 141 | + <div class="text-sm text-secondary-foreground"> |
| 142 | + <p>Note:</p> |
| 143 | + <p> |
| 144 | + 1. Feeds will be imported into the corresponding group, which will be created |
| 145 | + automatically if it does not exist. |
| 146 | + </p> |
| 147 | + <p>2. The existing feed with the same link will be override.</p> |
| 148 | + </div> |
| 149 | + <Button type="submit" disabled={importing}>Import</Button> |
135 | 150 | </form> |
136 | 151 | </Tabs.Content> |
137 | 152 | <Tabs.Content value="export"> |
|
0 commit comments