186 lines
6 KiB
TypeScript
186 lines
6 KiB
TypeScript
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<FeedItem[]>([]);
|
|
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;
|
|
}
|