Files
webnews/src/components/AddFeedModal.svelte

169 lines
4.6 KiB
Svelte

<script lang="ts">
import { db } from '$lib/db';
import { fetchFeed } from '$lib/rss';
import { parseOPML } from '$lib/opml';
import { newsStore } from '$lib/store.svelte';
import { X, Loader2, Upload } from 'lucide-svelte';
import { toast } from '$lib/toast.svelte';
let { onOpenChange } = $props();
let feedUrl = $state('');
let categoryId = $state(newsStore.categories[0]?.id || 'uncategorized');
let loading = $state(false);
let error = $state('');
async function handleSubmit() {
if (!feedUrl) return;
loading = true;
error = '';
try {
const { feed, articles } = await fetchFeed(feedUrl);
await db.saveFeed({
id: feedUrl,
title: feed.title || feedUrl,
siteUrl: feed.siteUrl || '',
description: feed.description || '',
categoryId: categoryId,
order: newsStore.feeds.length,
lastFetched: Date.now(),
fetchInterval: 30,
enabled: true,
consecutiveErrors: 0,
});
await db.saveArticles(articles);
await newsStore.refresh();
onOpenChange(false);
} catch (e: any) {
error = e.message || 'Failed to add feed';
} finally {
loading = false;
}
}
async function handleImport(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
loading = true;
error = '';
try {
const text = await file.text();
const { feeds, categories } = parseOPML(text);
if (categories.length > 0) {
await db.saveCategories(categories as any);
}
if (feeds.length > 0) {
await db.saveFeeds(feeds as any);
}
toast.success(`Imported ${feeds.length} feeds`);
await newsStore.init();
onOpenChange(false);
} catch (err) {
console.error('Import failed:', err);
error = 'Failed to import OPML';
} finally {
loading = false;
target.value = '';
}
}
</script>
<div class="fixed inset-0 z-[100] flex items-center justify-center p-4">
<button
class="absolute inset-0 bg-black/60 backdrop-blur-sm w-full h-full border-none cursor-default"
onclick={() => onOpenChange(false)}
aria-label="Close modal"
></button>
<div
class="bg-bg-primary border border-border-color rounded-2xl shadow-2xl w-full max-w-md relative overflow-hidden z-10"
>
<div class="p-6 border-b border-border-color flex justify-between items-center">
<h2 class="text-xl font-bold">Add RSS Feed</h2>
<button
class="text-text-secondary hover:text-text-primary"
onclick={() => onOpenChange(false)}
>
<X size={24} />
</button>
</div>
<form
class="p-6 space-y-4"
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<div class="space-y-2">
<label for="url" class="text-sm font-medium text-text-secondary">Feed URL</label>
<input
id="url"
type="url"
bind:value={feedUrl}
placeholder="https://example.com/rss.xml"
class="w-full bg-bg-secondary border border-border-color rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-accent-blue/20 transition-all"
required
/>
</div>
<div class="space-y-2">
<label for="category" class="text-sm font-medium text-text-secondary">Category</label>
<select
id="category"
bind:value={categoryId}
class="w-full bg-bg-secondary border border-border-color rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-accent-blue/20 transition-all text-sm"
>
{#each newsStore.categories as cat}
<option value={cat.id}>{cat.name}</option>
{/each}
</select>
</div>
{#if error}
<p class="text-red-500 text-sm">{error}</p>
{/if}
<button
type="submit"
class="w-full btn-primary flex items-center justify-center gap-2 py-3"
disabled={loading}
>
{#if loading}
<Loader2 size={20} class="animate-spin" />
Adding...
{:else}
Add Feed
{/if}
</button>
<div class="relative py-2">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-border-color"></div>
</div>
<div class="relative flex justify-center text-xs uppercase">
<span class="bg-bg-primary px-2 text-text-secondary">Or</span>
</div>
</div>
<label
class="w-full flex items-center justify-center gap-2 py-3 bg-bg-secondary border border-border-color rounded-xl text-sm font-semibold text-text-primary hover:bg-bg-primary transition-all cursor-pointer {loading
? 'opacity-50 pointer-events-none'
: ''}"
>
{#if loading}
<Loader2 size={18} class="animate-spin" />
Importing...
{:else}
<Upload size={18} />
Import OPML File
{/if}
<input type="file" accept=".opml,.xml" class="hidden" onchange={handleImport} />
</label>
</form>
</div>
</div>