|
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