import { Gtk } from "ags/gtk4"; import { createState } from "ags"; import GLib from "gi://GLib"; import Gio from "gi://Gio"; import { watchFile } from "../../lib/fileMonitor"; type FeedItem = { title: string; link: string; date: string; summary: string; author: string; feed_title: string; tag: string; }; const CACHE_FILE = GLib.build_filenamev([ GLib.get_user_cache_dir(), "ags", "rss-feeds.json", ]); export default function RSSList() { const [items, setItems] = createState([]); let listContainer: Gtk.Box | null = null; function readItemsFromCache(): FeedItem[] { try { const file = Gio.File.new_for_path(CACHE_FILE); const [ok, contents] = file.load_contents(null); if (!ok) return []; const data = JSON.parse(new TextDecoder("utf-8").decode(contents)); if (!Array.isArray(data)) return []; return data.map((item: any) => ({ title: item.title || "(untitled)", link: item.link || "", date: item.date ? formatDate(item.date) : "", summary: item.summary || "", author: item.author || "", feed_title: item.feed_title || "", tag: item.tag || "", })); } catch (err) { console.error("Failed to read RSS cache", err); return []; } } function formatDate(iso: string): string { try { const d = new Date(iso); const now = new Date(); const diff = now.getTime() - d.getTime(); const hours = Math.floor(diff / (1000 * 60 * 60)); if (hours < 1) return "just now"; if (hours < 24) return `${hours}h ago`; if (hours < 48) return "yesterday"; return d.toLocaleDateString(); } catch { return iso; } } function rebuildList() { if (!listContainer) return; let child = listContainer.get_first_child(); while (child) { const next = child.get_next_sibling(); listContainer.remove(child); child = next; } const currentItems = items(); if (currentItems.length === 0) { const emptyBox = new Gtk.Box({ halign: Gtk.Align.CENTER, cssClasses: ["status-box"], }); emptyBox.append(new Gtk.Label({ label: "📰", cssClasses: ["empty-icon"] })); emptyBox.append(new Gtk.Label({ label: "No feed items", cssClasses: ["status-text"] })); listContainer.append(emptyBox); } else { for (const item of currentItems) { const entry = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, cssClasses: ["rss-item"], }); // Title const titleLabel = new Gtk.Label({ label: item.title, halign: Gtk.Align.START, hexpand: false, cssClasses: ["rss-title"], wrap: true, wrapMode: 2, }); entry.append(titleLabel); // Meta: tag badge, feed title, date const meta = new Gtk.Box({ spacing: 8, cssClasses: ["rss-meta"] }); if (item.tag) { const tagBadge = new Gtk.Label({ label: item.tag, cssClasses: ["rss-tag", `rss-tag-${item.tag}`], }); meta.append(tagBadge); } meta.append(new Gtk.Label({ label: item.feed_title, cssClasses: ["rss-feed-title"], })); meta.append(new Gtk.Label({ label: "•", cssClasses: ["rss-separator"], })); meta.append(new Gtk.Label({ label: item.date, cssClasses: ["rss-date"], })); entry.append(meta); // Summary (truncated) if (item.summary) { const maxLen = 160; const text = item.summary.length <= maxLen ? item.summary : item.summary.slice(0, maxLen).trimEnd() + "…"; const summaryLabel = new Gtk.Label({ label: text, halign: Gtk.Align.START, cssClasses: ["rss-summary"], wrap: true, wrapMode: 2, }); entry.append(summaryLabel); } // Make clickable if link exists if (item.link) { entry.add_css_class("rss-item-link"); const gesture = new Gtk.GestureClick(); gesture.connect("pressed", () => { GLib.spawn_command_line_async(`xdg-open '${item.link.replace(/'/g, "'\''")}'`); }); entry.add_controller(gesture); } listContainer.append(entry); } } } function updateDisplay() { const parsed = readItemsFromCache(); setItems(parsed); rebuildList(); } const root = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, cssClasses: ["rss-list-container"], widthRequest: 300, }); const scrolled = new Gtk.ScrolledWindow({ vexpand: true, cssClasses: ["rss-scroll", "grid-card"], }); listContainer = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 12, hexpand: true, cssClasses: ["rss-list"], }); scrolled.set_child(listContainer); root.append(scrolled); updateDisplay(); watchFile(CACHE_FILE, updateDisplay, root); return root; }