169 lines
4.6 KiB
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>
|