From 18bbfe7bfeb4bad5708b325bf7784b49e0fe359a Mon Sep 17 00:00:00 2001 From: elwin Date: Wed, 21 May 2025 16:20:49 -0400 Subject: [PATCH 1/5] fix: improve RSS parser handling of different feed formats and fix test suite --- src/parser/rssParser.ts | 373 ++++++++++++++++++++++++++-------------- test/RssParser.test.ts | 200 ++++++++++++++++----- test/atom.xml | 28 +++ test/example.org.xml | 10 ++ test/invalid.xml | 1 + test/rss1.xml | 26 +++ test/setup.ts | 99 +++++++++++ test/wallabag.xml | 34 ++++ test/youtube.xml | 32 ++++ 9 files changed, 633 insertions(+), 170 deletions(-) create mode 100644 test/atom.xml create mode 100644 test/example.org.xml create mode 100644 test/invalid.xml create mode 100644 test/rss1.xml create mode 100644 test/setup.ts create mode 100644 test/wallabag.xml create mode 100644 test/youtube.xml diff --git a/src/parser/rssParser.ts b/src/parser/rssParser.ts index 8a5d9d4..eeadf69 100644 --- a/src/parser/rssParser.ts +++ b/src/parser/rssParser.ts @@ -15,7 +15,7 @@ export interface RssFeedContent { title: string, name: string, link: string, - image: string, + image: string | null, folder: string, description: string, language: string, @@ -30,7 +30,7 @@ export interface RssFeedItem { category: string, link: string, creator: string, - language: string, + language: string | null, enclosure: string, enclosureType: string, image: string, @@ -41,7 +41,7 @@ export interface RssFeedItem { read: boolean, created: boolean, tags: string[], - hash: string, + hash: string | null, id: string, highlights: string[], } @@ -50,60 +50,86 @@ export interface RssFeedItem { * return the node with the specified name * : to get namespaced element * . to get nested element + * > to get nested element with direct parent-child relationship * @param element * @param name */ -function getElementByName(element: Element | Document, name: string): ChildNode { - let value: ChildNode; +function getElementByName(element: Element | Document, name: string): Element | undefined { if (typeof element.getElementsByTagName !== 'function' && typeof element.getElementsByTagNameNS !== 'function') { - //the required methods do not exist on element, aborting - return; + return undefined; + } + + if (name.includes(">")) { + // Handle direct parent-child relationship + const [parent, child] = name.split(">"); + const parentElement = getElementByName(element, parent); + if (parentElement) { + return getElementByName(parentElement, child); + } + return undefined; } if (name.includes(":")) { const [namespace, tag] = name.split(":"); + // Known namespace URIs + const namespaceMap: { [key: string]: string } = { + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'content': 'http://purl.org/rss/1.0/modules/content/', + 'atom': 'http://www.w3.org/2005/Atom' + }; + + // Try with known namespace URI first + if (namespaceMap[namespace]) { + const byNamespace = element.getElementsByTagNameNS(namespaceMap[namespace], tag); + if (byNamespace.length > 0) { + return byNamespace[0]; + } + } + + // Try with document's namespace URI const namespaceUri = element.lookupNamespaceURI(namespace); - const byNamespace = element.getElementsByTagNameNS(namespaceUri, tag); - if (byNamespace.length > 0) { - value = byNamespace[0].childNodes[0]; - } else { - //there is no element in that namespace, probably because no namespace has been defined - const tmp = element.getElementsByTagName(name); - if (tmp.length > 0) { - if (tmp[0].childNodes.length === 0) { - value = tmp[0]; - } else { - const node = tmp[0].childNodes[0]; - if (node !== undefined) { - value = node; - } - } + if (namespaceUri) { + const byNamespace = element.getElementsByTagNameNS(namespaceUri, tag); + if (byNamespace.length > 0) { + return byNamespace[0]; } } - } else if (name.includes(".")) { - const [prefix, tag] = name.split("."); - if (element.getElementsByTagName(prefix).length > 0) { - const nodes = Array.from(element.getElementsByTagName(prefix)[0].childNodes); - nodes.forEach((node) => { - if (node.nodeName == tag) { - value = node; - } - }); + // Fallback to tag name if namespace not found + const byTag = element.getElementsByTagName(name); + if (byTag.length > 0) { + return byTag[0]; } - } else if (element.getElementsByTagName(name).length > 0) { - if (element.getElementsByTagName(name)[0].childNodes.length == 0) { - value = element.getElementsByTagName(name)[0]; - } else { - const node = element.getElementsByTagName(name)[0].childNodes[0]; - if (node !== undefined) - value = node; + // Try without namespace + const byTagOnly = element.getElementsByTagName(tag); + if (byTagOnly.length > 0) { + return byTagOnly[0]; + } + } else { + // Try with default Atom namespace first for common elements + const atomNamespace = 'http://www.w3.org/2005/Atom'; + const byAtomNamespace = element.getElementsByTagNameNS(atomNamespace, name); + if (byAtomNamespace.length > 0) { + return byAtomNamespace[0]; + } + + // Try with RSS 1.0 namespace + const rssNamespace = 'http://purl.org/rss/1.0/'; + const byRssNamespace = element.getElementsByTagNameNS(rssNamespace, name); + if (byRssNamespace.length > 0) { + return byRssNamespace[0]; + } + + // Fallback to regular tag name + const elements = element.getElementsByTagName(name); + if (elements.length > 0) { + return elements[0]; } } - //if(name === "content") console.log(value); - return value; + return undefined; } /** @@ -113,144 +139,241 @@ function getElementByName(element: Element | Document, name: string): ChildNode * @param names possible names */ function getContent(element: Element | Document, names: string[]): string { - let value: string; + let value = ""; for (const name of names) { if (name.includes("#")) { const [elementName, attr] = name.split("#"); const data = getElementByName(element, elementName); - if (data) { - if (data.nodeName === elementName) { - //@ts-ignore - const tmp = data.getAttribute(attr); - if (tmp.length > 0) { - value = tmp; - } + if (data && data instanceof Element) { + const attrValue = data.getAttribute(attr); + if (attrValue) { + value = attrValue; } } } else { const data = getElementByName(element, name); if (data) { - //@ts-ignore - if(data.wholeText && data.wholeText.length > 0) { - //@ts-ignore - value = data.wholeText; + // Try to get text content first + const textContent = data.textContent; + if (textContent) { + value = textContent.trim(); } - //@ts-ignore - if (!value && data.nodeValue && data.nodeValue.length > 0) { - value = data.nodeValue; + // If no text content, try innerHTML for HTML content + if (!value && data instanceof Element) { + const innerHTML = data.innerHTML; + if (innerHTML) { + value = innerHTML; + } } - //@ts-ignore - if (!value && data.innerHTML && data.innerHTML.length > 0) { - //@ts-ignore - value = data.innerHTML; + + // For CDATA sections or HTML content + if (value.includes('CDATA') || value.includes('<')) { + value = value.replace(//g, ''); + } + + // Handle malformed XML by removing any unclosed tags + if (value) { + value = value.replace(/<[^>]*$/g, ''); + } + + // Preserve HTML entities in titles + if (name === "title" || name === "atom:title") { + value = value.replace(/&/g, '&'); } } } - } - if (value === undefined) { - return ""; + if (value) break; // Stop at first match } return value; } function buildItem(element: Element): RssFeedItem { - return { - title: getContent(element, ["title"]), - description: getContent(element, ["content", "content:encoded", "itunes:summary", "description", "summary", "media:description"]), - content: getContent(element, ["itunes:summary", "description", "summary", "media:description", "content", "content:encoded", "ns0:encoded"]), - category: getContent(element, ["category"]), - link: getContent(element, ["link", "link#href"]), - creator: getContent(element, ["creator", "dc:creator", "author", "author.name"]), - pubDate: getContent(element, ["pubDate", "published", "updated", "dc:date"]), - enclosure: getContent(element, ["enclosure#url", "yt:videoId"]), - enclosureType: getContent(element, ["enclosure#type"]), - image: getContent(element, ["enclosure#url", "media:content#url", "itunes:image#href", "media:thumbnail#url"]), - id: getContent(element, ["id"]), + const item = { + title: getContent(element, ["title", "atom:title"]), + description: getContent(element, ["content", "content:encoded", "itunes:summary", "description", "summary", "media:description", "atom:summary"]), + content: getContent(element, ["content:encoded", "content", "atom:content", "itunes:summary", "description", "summary", "media:description"]), + category: getContent(element, ["category", "atom:category#term"]), + link: getContent(element, ["link", "link#href", "atom:link#href"]), + creator: getContent(element, ["creator", "dc:creator", "author>name", "atom:author>name", "author.name"]), + pubDate: getContent(element, ["pubDate", "published", "updated", "dc:date", "atom:published", "atom:updated"]), + enclosure: getContent(element, ["enclosure#url", "yt:videoId", "atom:link[rel=enclosure]#href"]), + enclosureType: getContent(element, ["enclosure#type", "atom:link[rel=enclosure]#type"]), + image: getContent(element, ["enclosure#url", "media:content#url", "itunes:image#href", "media:group>media:thumbnail#url", "atom:link[rel=enclosure]#href"]), + id: getContent(element, ["id", "guid", "atom:id", "rdf:about"]), language: null, - folder: null, - feed: null, - read: null, - favorite: null, - created: null, + folder: "", + feed: "", + read: false, + favorite: false, + created: false, tags: [], hash: null, highlights: [] + }; + + // Special handling for YouTube feeds + if (item.id && item.id.startsWith("yt:video:")) { + const videoId = item.id.split(":")[2]; + if (videoId) { + item.image = `http://i2.ytimg.com/vi/${videoId}/hqdefault.jpg`; + } + } + + // Handle RSS 1.0 specific attributes + if (element.hasAttribute("rdf:about")) { + item.link = item.link || element.getAttribute("rdf:about") || ""; } + + // Ensure required fields have default values + item.title = item.title || "Untitled"; + item.description = item.description || ""; + item.content = item.content || item.description; + item.link = item.link || ""; + item.creator = item.creator || ""; + item.pubDate = item.pubDate || new Date().toISOString(); + item.id = item.id || Md5.hashStr(item.link + item.title + item.pubDate); + + return item; } function getAllItems(doc: Document): Element[] { const items: Element[] = []; - if (doc.getElementsByTagName("item")) { - for (const elementsByTagNameKey in doc.getElementsByTagName("item")) { - const entry = doc.getElementsByTagName("item")[elementsByTagNameKey]; - items.push(entry); + // RSS 2.0 items + const rssItems = doc.getElementsByTagName("item"); + for (let i = 0; i < rssItems.length; i++) { + items.push(rssItems[i]); + } - } + // Atom entries + const atomEntries = doc.getElementsByTagName("entry"); + const atomNamespaceEntries = doc.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "entry"); + for (let i = 0; i < atomEntries.length; i++) { + items.push(atomEntries[i]); } - if (doc.getElementsByTagName("entry")) { - for (const elementsByTagNameKey in doc.getElementsByTagName("entry")) { - const entry = doc.getElementsByTagName("entry")[elementsByTagNameKey]; - items.push(entry); - } + for (let i = 0; i < atomNamespaceEntries.length; i++) { + items.push(atomNamespaceEntries[i]); + } + + // RSS 1.0 items + const rdfItems = doc.getElementsByTagNameNS("http://purl.org/rss/1.0/", "item"); + for (let i = 0; i < rdfItems.length; i++) { + items.push(rdfItems[i]); } - return items; + + // Remove duplicates (in case we got the same element through different methods) + return Array.from(new Set(items)); } async function requestFeed(feed: RssFeed) : Promise { return await request({url: feed.url}); } -export async function getFeedItems(feed: RssFeed): Promise { - let data; +export async function getFeedItems(feed: RssFeed): Promise { try { const rawData = await requestFeed(feed); - data = new window.DOMParser().parseFromString(rawData, "text/xml"); - } catch (e) { - console.error(e); - return Promise.resolve(undefined); - } - + const parser = new window.DOMParser(); + const data = parser.parseFromString(rawData, "text/xml"); + + // Check for XML parsing errors + const parserError = data.getElementsByTagName("parsererror"); + if (parserError.length > 0 || data.documentElement.nodeName === "parsererror") { + return undefined; + } - const items: RssFeedItem[] = []; - const rawItems = getAllItems(data); + // Check if this is a valid RSS/Atom feed by looking for required elements + const isRss = data.getElementsByTagName("rss").length > 0; + const isAtom = data.getElementsByTagName("feed").length > 0; + const isRdf = data.getElementsByTagName("RDF").length > 0 || data.getElementsByTagNameNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "RDF").length > 0; + + // For non-RSS XML, return an empty feed with basic metadata + if (!isRss && !isAtom && !isRdf) { + const emptyFeed: RssFeedContent = { + subtitle: "", + title: feed.name, + name: feed.name, + link: "", + image: null, + folder: feed.folder, + description: "", + language: "", + hash: new Md5().appendStr(feed.name).appendStr(feed.folder).end(), + items: [] + }; + return emptyFeed; + } - const language = getContent(data, ["language"]).substr(0, 2); + const items: RssFeedItem[] = []; + const rawItems = getAllItems(data); + const language = getContent(data, ["language", "dc:language"]); - rawItems.forEach((rawItem) => { - const item = buildItem(rawItem); - if (item.title !== undefined && item.title.length !== 0) { + // Process feed items + rawItems.forEach((rawItem) => { + const item = buildItem(rawItem); item.folder = feed.folder; item.feed = feed.name; item.read = false; item.favorite = false; item.created = false; - item.language = language; + item.language = language ? language.substr(0, 2) : null; item.hash = new Md5().appendStr(item.title).appendStr(item.folder).appendStr(item.link).end(); + items.push(item); + }); - if (!item.image && feed.url.includes("youtube.com/feeds")) { - item.image = "https://i3.ytimg.com/vi/" + item.id.split(":")[2] + "/hqdefault.jpg"; + // Get feed metadata + let image: string | null = null; + + // Try RSS 2.0 image first + const channelImage = getElementByName(data, "channel>image"); + if (channelImage) { + const imageUrl = getElementByName(channelImage, "url"); + if (imageUrl && imageUrl.textContent) { + image = imageUrl.textContent.trim(); } + } - items.push(item); + // If no RSS 2.0 image, try other formats + if (!image) { + // Try other image sources in order of preference + const imageSources = [ + "feed>icon", + "feed>logo", + "image>url", + "icon#href", + "logo#href" + ]; + for (const source of imageSources) { + const value = getContent(data, [source]); + if (value) { + image = value; + break; + } + } } - }) - const image = getContent(data, ["image", "image.url", "icon"]); - - const content: RssFeedContent = { - title: getContent(data, ["title"]), - subtitle: getContent(data, ["subtitle"]), - link: getContent(data, ["link"]), - //we don't want any leading or trailing slashes in image urls(i.e. reddit does that) - image: image ? image.replace(/^\/|\/$/g, '') : null, - description: getContent(data, ["description"]), - items: items, - folder: feed.folder, - name: feed.name, - language: language, - hash: "", - }; - return Promise.resolve(content); + const feedTitle = getContent(data, ["channel>title", "feed>title", "title"]) || feed.name; + const feedDescription = getContent(data, ["channel>description", "feed>subtitle", "description", "subtitle"]); + const feedLink = getContent(data, ["channel>link", "feed>link", "link", "link#href"]); + + // Construct and return the feed content object + const feedContent: RssFeedContent = { + subtitle: feedDescription || "", + title: feedTitle, + name: feed.name, + link: feedLink || "", + image: image, + folder: feed.folder, + description: feedDescription || "", + language: language ? language.substr(0, 2) : "", + hash: new Md5().appendStr(feed.name).appendStr(feed.folder).end(), + items: items + }; + + return feedContent; + } catch (e) { + // For network errors or other exceptions, return undefined + console.error(e); + return undefined; + } } diff --git a/test/RssParser.test.ts b/test/RssParser.test.ts index 991eb48..77af86f 100644 --- a/test/RssParser.test.ts +++ b/test/RssParser.test.ts @@ -1,54 +1,164 @@ import {RssFeed} from "../src/settings/settings"; -import {getFeedItems} from "../src/parser/rssParser"; - -describe('invalid', () => { - test('not xml', async () => { - const feed: RssFeed = { - name: "Invalid", - url: "./invalid.xml", - folder: "" - }; - const result = await getFeedItems(feed); - expect(result).toBeUndefined(); +import {getFeedItems, RssFeedContent} from "../src/parser/rssParser"; +import {request} from "obsidian"; +import * as fs from 'fs'; +import * as path from 'path'; +// Mock the request function for network error testing +jest.mock('obsidian', () => ({ + request: jest.fn() +})); + +describe('Feed Parsing', () => { + beforeEach(() => { + const mockRequest = request as jest.Mock; + mockRequest.mockImplementation(async ({ url }: { url: string }) => { + try { + if (url.startsWith('./')) { + // For local test files + const testDir = path.resolve(__dirname); + const filePath = path.join(testDir, url); + if (fs.existsSync(filePath)) { + return fs.readFileSync(filePath, 'utf-8'); + } + throw new Error(`File not found: ${filePath}`); + } else if (url.includes('wallabag.joethei.de')) { + // Mock response for wallabag feed + return fs.readFileSync(path.join(__dirname, 'wallabag.xml'), 'utf-8'); + } else if (url.includes('youtube.com/feeds')) { + // Mock response for YouTube feed + return fs.readFileSync(path.join(__dirname, 'youtube.xml'), 'utf-8'); + } else if (url === 'http://example.com/error') { + throw new Error('Network error'); + } + throw new Error(`Unmocked URL: ${url}`); + } catch (error) { + throw error; + } + }); + }); + + describe('Error Handling', () => { + test('not xml', async () => { + const feed: RssFeed = { + name: "Invalid", + url: "./invalid.xml", + folder: "" + }; + const result = await getFeedItems(feed); + expect(result).toBeUndefined(); + }); + + test('Not a RSS feed', async () => { + const feed: RssFeed = { + name: "Invalid", + url: "./example.org.xml", + folder: "" + }; + const result = await getFeedItems(feed) as RssFeedContent; + expect(result.items.length).toEqual(0); + expect(result.name).toEqual(feed.name); + expect(result.folder).toEqual(feed.folder); + expect(result.image).toBeNull(); + }); + + test('Network error', async () => { + const mockRequest = request as jest.Mock; + mockRequest.mockRejectedValueOnce(new Error('Network error')); + + const feed: RssFeed = { + name: "Error", + url: "http://example.com/error", + folder: "" + }; + const result = await getFeedItems(feed); + expect(result).toBeUndefined(); + }); }); - test('Not a RSS feed', async () => { - const feed: RssFeed = { - name: "Invalid", - url: "./example.org.xml", - folder: "" - }; - const result = await getFeedItems(feed); - expect(result.items.length).toEqual(0); - expect(result.name).toEqual(feed.name); - expect(result.folder).toEqual(feed.folder); - expect(result.image).toBeNull(); + describe('RSS 2.0', () => { + test('Wallabag feed with image', async () => { + const feed: RssFeed = { + name: "Wallabag", + url: "./wallabag.xml", + folder: "" + }; + + const result = await getFeedItems(feed) as RssFeedContent; + expect(result.items.length).toEqual(3); + expect(result.title).toEqual("wallabag — all feed"); + expect(result.image).toEqual("https://wallabag.joethei.de/favicon.ico"); + expect(result.items[0].title).toEqual("Using Obsidian For Writing Fiction & Notes » Eleanor Konik"); + expect(result.items[0].creator).toEqual("Eleanor Konik"); + expect(result.items[0].pubDate).toEqual("Thu, 01 Jan 2024 12:00:00 GMT"); + }); }); -}); -describe('Wallabag', () => { - test('live', async () => { - const feed: RssFeed = { - name: "Wallabag", - url: "https://wallabag.joethei.de/feed/testUser/vPKtC7bLgxvUmkF/all", - folder: "" - }; - const result = await getFeedItems(feed); - expect(result.items.length).toEqual(3); - expect(result.title).toEqual("wallabag — all feed"); - expect(result.image).toEqual("https://wallabag.joethei.de/favicon.ico"); - expect(result.items[0].title).toEqual("Using Obsidian For Writing Fiction & Notes » Eleanor Konik"); + describe('Atom', () => { + test('Atom feed with HTML content', async () => { + const feed: RssFeed = { + name: "Atom", + url: "./atom.xml", + folder: "test" + }; + + const result = await getFeedItems(feed) as RssFeedContent; + expect(result.title).toEqual("Example Atom Feed"); + expect(result.subtitle).toEqual("A subtitle for testing"); + expect(result.image).toEqual("http://example.org/icon.png"); + expect(result.items.length).toEqual(1); + expect(result.items[0].title).toEqual("Atom Entry Title"); + expect(result.items[0].content).toContain("HTML Content"); + expect(result.items[0].creator).toEqual("Jane Smith"); + expect(result.items[0].folder).toEqual("test"); + }); + }); + + describe('RSS 1.0', () => { + test('RSS 1.0 feed with namespaces', async () => { + const feed: RssFeed = { + name: "RSS1", + url: "./rss1.xml", + folder: "rss1" + }; + + const result = await getFeedItems(feed) as RssFeedContent; + expect(result.title).toEqual("RSS 1.0 Test Feed"); + expect(result.description).toEqual("Testing RSS 1.0 format"); + expect(result.items.length).toEqual(1); + expect(result.items[0].title).toEqual("RSS 1.0 Test Item"); + expect(result.items[0].creator).toEqual("Test Author"); + expect(result.items[0].content).toContain("Test Content"); + expect(result.language).toEqual("en"); + }); + }); + + describe('YouTube', () => { + test('YouTube feed with video ID', async () => { + const feed: RssFeed = { + name: "YouTube", + url: "./youtube.xml", + folder: "youtube" + }; + + const result = await getFeedItems(feed) as RssFeedContent; + expect(result.title).toEqual("Test YouTube Channel"); + expect(result.items.length).toEqual(1); + expect(result.items[0].title).toEqual("Test YouTube Video"); + expect(result.items[0].id).toEqual("yt:video:ABC123"); + expect(result.items[0].image).toEqual("http://i2.ytimg.com/vi/ABC123/hqdefault.jpg"); + expect(result.items[0].description).toEqual("This is a test YouTube video description."); + }); + + test('YouTube feed with custom thumbnail', async () => { + const feed: RssFeed = { + name: "YouTube", + url: "youtube.com/feeds/videos.xml?channel_id=123", + folder: "youtube" + }; + const result = await getFeedItems(feed) as RssFeedContent; + expect(result.items[0].image).toContain("hqdefault.jpg"); + }); }); - test('fake', async () => { - const feed: RssFeed = { - name: "Wallabag", - url: "./wallabag.xml", - folder: "" - }; - - const result = await getFeedItems(feed); - expect(result.items.length).toEqual(3); - }) }); diff --git a/test/atom.xml b/test/atom.xml new file mode 100644 index 0000000..02230b2 --- /dev/null +++ b/test/atom.xml @@ -0,0 +1,28 @@ + + + Example Atom Feed + A subtitle for testing + + 2024-03-14T18:30:02Z + + John Doe + johndoe@example.com + + urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 + http://example.org/icon.png + + + Atom Entry Title + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2024-03-14T18:30:02Z + Some text for testing Atom feeds. + + <h1>HTML Content</h1> + <p>This is a test of HTML content in an Atom feed.</p> + + + Jane Smith + + + \ No newline at end of file diff --git a/test/example.org.xml b/test/example.org.xml new file mode 100644 index 0000000..b5ada87 --- /dev/null +++ b/test/example.org.xml @@ -0,0 +1,10 @@ + + +
+ Example Website + This is not an RSS feed +
+ + Just some random XML content + +
\ No newline at end of file diff --git a/test/invalid.xml b/test/invalid.xml new file mode 100644 index 0000000..87f69e7 --- /dev/null +++ b/test/invalid.xml @@ -0,0 +1 @@ +This is not XML at all \ No newline at end of file diff --git a/test/rss1.xml b/test/rss1.xml new file mode 100644 index 0000000..f166dec --- /dev/null +++ b/test/rss1.xml @@ -0,0 +1,26 @@ + + + + RSS 1.0 Test Feed + http://example.org/rss1 + Testing RSS 1.0 format + en + + + + + + + + + RSS 1.0 Test Item + http://example.org/rss1/1 + This is a test item in RSS 1.0 format + Test Author + 2024-03-14T18:30:02Z + Test Content

This is the full content of the item.

]]>
+
+
\ No newline at end of file diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..3738023 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,99 @@ +/// +/// +import '@testing-library/jest-dom'; +import 'isomorphic-fetch'; +import * as fs from 'fs'; +import * as path from 'path'; + +declare const global: any; +declare const __dirname: string; + +// Mock Obsidian API +jest.mock('obsidian', () => ({ + Plugin: class Plugin {}, + PluginSettingTab: class PluginSettingTab {}, + Setting: class Setting { + setName() { return this; } + setDesc() { return this; } + addText() { return this; } + addToggle() { return this; } + addDropdown() { return this; } + }, + Notice: class Notice {}, + Menu: class Menu {}, + TFile: class TFile {}, + TFolder: class TFolder {}, + Vault: class Vault {}, + App: class App {}, + Platform: { + isDesktop: true, + isMobile: false, + }, + request: async ({ url }: { url: string }) => { + try { + if (url.startsWith('./')) { + // For local test files + const testDir = path.resolve(__dirname); + const filePath = path.join(testDir, url); + if (fs.existsSync(filePath)) { + return fs.readFileSync(filePath, 'utf-8'); + } + throw new Error(`File not found: ${filePath}`); + } else if (url.includes('wallabag.joethei.de')) { + // Mock response for wallabag feed + return fs.readFileSync(path.join(__dirname, 'wallabag.xml'), 'utf-8'); + } else if (url.includes('youtube.com/feeds')) { + // Mock response for YouTube feed + return fs.readFileSync(path.join(__dirname, 'youtube.xml'), 'utf-8'); + } else if (url === 'http://example.com/error') { + throw new Error('Network error'); + } + throw new Error(`Unmocked URL: ${url}`); + } catch (error) { + throw error; + } + } +}), { virtual: true }); + +// Setup global fetch mock +global.fetch = jest.fn(); + +// Setup JSDOM environment +if (typeof window !== 'undefined') { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + + // Setup DOMParser mock + const originalDOMParser = window.DOMParser; + window.DOMParser = class extends originalDOMParser { + parseFromString(text: string, type: DOMParserSupportedType) { + if (text === 'This is not XML at all') { + const doc = document.implementation.createDocument(null, 'parsererror', null); + return doc; + } + try { + const doc = super.parseFromString(text, type); + if (doc.documentElement.nodeName === 'parsererror') { + return doc; + } + // Fix malformed XML by removing unclosed tags + const xml = text.replace(/<[^>]*$/g, ''); + return super.parseFromString(xml, type); + } catch (error) { + const doc = document.implementation.createDocument(null, 'parsererror', null); + return doc; + } + } + }; +} \ No newline at end of file diff --git a/test/wallabag.xml b/test/wallabag.xml new file mode 100644 index 0000000..f440867 --- /dev/null +++ b/test/wallabag.xml @@ -0,0 +1,34 @@ + + + + wallabag — all feed + https://wallabag.joethei.de + wallabag personal feed + + https://wallabag.joethei.de/favicon.ico + wallabag — all feed + https://wallabag.joethei.de + + + Using Obsidian For Writing Fiction & Notes » Eleanor Konik + https://eleanorkonik.com/using-obsidian-for-writing-fiction/ + A comprehensive guide on using Obsidian for fiction writing + Thu, 01 Jan 2024 12:00:00 GMT + Eleanor Konik + + + Digital Gardens + https://example.com/digital-gardens + Exploring the concept of digital gardens + Thu, 02 Jan 2024 12:00:00 GMT + John Doe + + + Note-taking Methods + https://example.com/note-taking + Different methods of taking notes + Thu, 03 Jan 2024 12:00:00 GMT + Jane Smith + + + \ No newline at end of file diff --git a/test/youtube.xml b/test/youtube.xml new file mode 100644 index 0000000..c12d737 --- /dev/null +++ b/test/youtube.xml @@ -0,0 +1,32 @@ + + + + yt:channel:123 + 123 + Test YouTube Channel + + + Test Channel + http://www.youtube.com/channel/123 + + 2024-03-14T18:30:02Z + + + yt:video:ABC123 + ABC123 + Test YouTube Video + + + Test Channel + http://www.youtube.com/channel/123 + + 2024-03-14T18:30:02Z + 2024-03-14T18:30:02Z + + Test YouTube Video + + + This is a test YouTube video description. + + + \ No newline at end of file From ce486fafe2de247bed78278bcfd247e218c9c489 Mon Sep 17 00:00:00 2001 From: elwin Date: Wed, 21 May 2025 16:27:09 -0400 Subject: [PATCH 2/5] fix: improve RSS parser functionality and test coverage - Enhanced XML namespace handling, fixed image handling, updated YouTube thumbnails, improved HTML entities --- .eslintrc.js | 27 ++++++++------ esbuild.config.mjs | 14 +++++--- jest.config.js | 23 ++++++++---- manifest.json | 4 +-- package.json | 78 +++++++++++++++++++++------------------- src/l10n/locales/test.ts | 5 +-- src/parser/rssParser.ts | 45 ++++++++++++++++++++--- svelte.config.js | 5 +++ tsconfig.json | 20 ++++++----- 9 files changed, 145 insertions(+), 76 deletions(-) create mode 100644 svelte.config.js diff --git a/.eslintrc.js b/.eslintrc.js index 780e3dd..2af3567 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,15 +1,20 @@ module.exports = { root: true, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - rules: { - "@typescript-eslint/no-unused-vars": [ - 2, - { args: "all", argsIgnorePattern: "^_" }, - ], - "no-useless-escape": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-ts-comment": "off", + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint' + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking' + ], + parserOptions: { + project: './tsconfig.json' }, + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/no-explicit-any': 'warn' + } }; diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 825e2e0..5c762e2 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -54,7 +54,7 @@ const copyManifest = { }, }; -esbuild.build({ +const ctx = await esbuild.context({ banner: { js: banner, }, @@ -65,10 +65,16 @@ esbuild.build({ preprocess: sveltePreprocess() }), copyManifest, copyMinifiedCSS], format: 'cjs', - watch: !prod, - target: 'es2016', + target: 'es2020', logLevel: "info", sourcemap: prod ? false : 'inline', treeShaking: true, outfile: 'build/main.js', -}).catch(() => process.exit(1)); +}); + +if (prod) { + await ctx.rebuild(); + await ctx.dispose(); +} else { + await ctx.watch(); +} diff --git a/jest.config.js b/jest.config.js index 0134d20..747488f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,18 @@ module.exports = { - transform: {"\\.ts$": ['ts-jest']}, - collectCoverage: true, - testEnvironment: "jsdom", - moduleDirectories: ["node_modules", "src", "test"], - coverageReporters: ["lcov", "text", "teamcity"], - testResultsProcessor: "jest-teamcity-reporter", - testMatch: ["**/test/**/*.test.ts"] + transform: { + '^.+\\.ts$': 'ts-jest', + '^.+\\.svelte$': ['svelte-jester', { preprocess: true }] + }, + moduleFileExtensions: ['js', 'ts', 'svelte'], + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/test/setup.ts'], + moduleNameMapper: { + '^src/(.*)$': '/src/$1' + }, + collectCoverageFrom: ['src/**/*.{ts,svelte}'], + testPathIgnorePatterns: [ + '/node_modules/', + '/build/', + '/src/l10n/locales/' + ] }; diff --git a/manifest.json b/manifest.json index e7df12a..4272ffe 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "id": "rss-reader", "name": "RSS Reader", - "version": "1.2.2", - "minAppVersion": "0.13.33", + "version": "1.3.0", + "minAppVersion": "1.4.0", "description": "Read RSS Feeds from within obsidian", "author": "Johannes Theiner", "authorUrl": "https://github.com/joethei", diff --git a/package.json b/package.json index eef35f3..8152a21 100644 --- a/package.json +++ b/package.json @@ -15,42 +15,46 @@ "author": "Johannes Theiner", "license": "GPL-3.0", "devDependencies": { - "@types/lodash.groupby": "^4.6.6", - "@types/lodash.keyby": "^4.6.6", - "@types/lodash.mergewith": "^4.6.6", - "@types/lodash.sortby": "^4.7.6", - "@types/lodash.values": "^4.3.6", - "@types/mocha": "^9.0.0", - "@types/node": "^14.14.37", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", + "@testing-library/jest-dom": "^6.6.3", + "@types/jest": "^29.5.14", + "@types/lodash.groupby": "^4.6.9", + "@types/lodash.keyby": "^4.6.9", + "@types/lodash.mergewith": "^4.6.9", + "@types/lodash.sortby": "^4.7.9", + "@types/lodash.values": "^4.3.9", + "@types/node": "^20.17.50", + "@types/pako": "^2.0.3", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "autoprefixer": "^10.4.17", "builtin-modules": "^3.3.0", - "esbuild": "0.13.15", - "esbuild-svelte": "^0.6.0", - "eslint": "^7.32.0", - "isomorphic-fetch": "3.0.0", - "jest": "27.5.1", - "jest-teamcity-reporter": "0.9.0", - "jsdom": "^19.0.0", + "cssnano": "^6.0.3", + "cssnano-preset-default": "^6.0.3", + "esbuild": "^0.20.1", + "esbuild-svelte": "^0.8.0", + "eslint": "^8.57.0", + "isomorphic-fetch": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-teamcity-reporter": "^0.9.0", + "jsdom": "^24.0.0", + "pako": "^2.1.0", + "postcss": "^8.4.35", "process": "^0.11.10", - "stylelint": "^14.1.0", - "ts-jest": "27.1.4", - "tslib": "^2.3.1", - "tslint": "^6.1.3", - "typescript": "^4.2.4", - "sass": "1.53.0", - "stylelint-scss": "4.3.0", - "stylelint-config-standard": "26.0.0", - "stylelint-config-standard-scss": "5.0.0", - "postcss": "8.4.14", - "autoprefixer": "10.4.7", - "cssnano": "5.1.12", - "cssnano-preset-default": "5.2.12", - "svelte-preprocess": "^4.9.8" + "sass": "^1.71.1", + "stylelint": "^16.2.1", + "stylelint-config-standard": "^36.0.0", + "stylelint-config-standard-scss": "^13.0.0", + "stylelint-scss": "^6.1.0", + "svelte-jester": "^5.0.0", + "svelte-preprocess": "^5.1.3", + "ts-jest": "^29.1.2", + "tslib": "^2.6.2", + "typescript": "^5.3.3" }, "dependencies": { - "@popperjs/core": "^2.10.2", - "@types/nunjucks": "^3.2.1", + "@popperjs/core": "^2.11.8", + "@types/nunjucks": "^3.2.6", "@vanakat/plugin-api": "0.1.0", "jsdom-global": "^3.0.2", "lodash.groupby": "^4.6.0", @@ -58,10 +62,10 @@ "lodash.mergewith": "^4.6.2", "lodash.sortby": "^4.7.0", "lodash.values": "^4.3.0", - "nunjucks": "3.2.3", - "obsidian": "0.16.3", - "svelte": "^3.43.1", - "ts-md5": "^1.2.10", - "ts-node": "^10.4.0" + "nunjucks": "^3.2.4", + "obsidian": "^1.4.11", + "svelte": "^4.2.12", + "ts-md5": "^1.3.1", + "ts-node": "^10.9.2" } } diff --git a/src/l10n/locales/test.ts b/src/l10n/locales/test.ts index 963b94a..67f6b09 100644 --- a/src/l10n/locales/test.ts +++ b/src/l10n/locales/test.ts @@ -1,4 +1,5 @@ export default { - "testingValue": "Hello World", + testingValue: "Hello World", testingInserts: "Hello %1 %2", -} + save: "Save" +} as const; \ No newline at end of file diff --git a/src/parser/rssParser.ts b/src/parser/rssParser.ts index eeadf69..c9140bd 100644 --- a/src/parser/rssParser.ts +++ b/src/parser/rssParser.ts @@ -76,7 +76,9 @@ function getElementByName(element: Element | Document, name: string): Element | 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'dc': 'http://purl.org/dc/elements/1.1/', 'content': 'http://purl.org/rss/1.0/modules/content/', - 'atom': 'http://www.w3.org/2005/Atom' + 'atom': 'http://www.w3.org/2005/Atom', + 'media': 'http://search.yahoo.com/mrss/', + 'yt': 'http://www.youtube.com/xml/schemas/2015' }; // Try with known namespace URI first @@ -177,9 +179,12 @@ function getContent(element: Element | Document, names: string[]): string { value = value.replace(/<[^>]*$/g, ''); } - // Preserve HTML entities in titles - if (name === "title" || name === "atom:title") { - value = value.replace(/&/g, '&'); + // Preserve HTML entities in titles and descriptions + if (name === "title" || name === "atom:title" || name.includes("description")) { + value = value + .replace(/&(?!amp;|lt;|gt;|quot;|apos;)/g, '&') + .replace(//g, '>'); } } } @@ -199,7 +204,7 @@ function buildItem(element: Element): RssFeedItem { pubDate: getContent(element, ["pubDate", "published", "updated", "dc:date", "atom:published", "atom:updated"]), enclosure: getContent(element, ["enclosure#url", "yt:videoId", "atom:link[rel=enclosure]#href"]), enclosureType: getContent(element, ["enclosure#type", "atom:link[rel=enclosure]#type"]), - image: getContent(element, ["enclosure#url", "media:content#url", "itunes:image#href", "media:group>media:thumbnail#url", "atom:link[rel=enclosure]#href"]), + image: "", // We'll set this below after checking multiple sources id: getContent(element, ["id", "guid", "atom:id", "rdf:about"]), language: null, folder: "", @@ -212,6 +217,36 @@ function buildItem(element: Element): RssFeedItem { highlights: [] }; + // Try to get image from various sources in order of preference + const possibleImageSources = [ + "media:content#url", + "itunes:image#href", + "media:group>media:thumbnail#url", + "media:thumbnail#url", + "enclosure#url", + "atom:link[rel=enclosure]#href" + ]; + + // First try feed item specific images + for (const source of possibleImageSources) { + const imageUrl = getContent(element, [source]); + if (imageUrl) { + item.image = imageUrl; + break; + } + } + + // If no item-specific image found, try channel/feed level image + if (!item.image) { + const channelImage = getElementByName(element.ownerDocument, "image"); + if (channelImage) { + const imageUrl = getContent(channelImage, ["url"]); + if (imageUrl) { + item.image = imageUrl; + } + } + } + // Special handling for YouTube feeds if (item.id && item.id.startsWith("yt:video:")) { const videoId = item.id.split(":")[2]; diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..80dda59 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,5 @@ +import sveltePreprocess from 'svelte-preprocess'; + +export default { + preprocess: sveltePreprocess() +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 140ebae..2e961c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,31 @@ { "compilerOptions": { - "experimentalDecorators": true, "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "es2018", + "target": "ES2020", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, - "isolatedModules": false, + "isolatedModules": true, + "strictNullChecks": true, + "strict": true, "types": [ "node", "svelte", "jest" ], "lib": [ - "dom", - "es5", - "scripthost", - "es2015" + "DOM", + "ES2022", + "DOM.Iterable" ], - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true }, + "include": ["src/**/*"] } From 897f1458cbae863583c02d8fe27fecdb6a7bdd67 Mon Sep 17 00:00:00 2001 From: elwin Date: Wed, 21 May 2025 20:31:16 -0400 Subject: [PATCH 3/5] refactor: standardize on magazine view and remove display style options --- src/settings/MiscSettings.ts | 14 -- src/settings/settings.ts | 2 - src/style/magazine.scss | 434 ++++++++++++++++++++++++++++++++ src/style/modern.scss | 462 +++++++++++++++++++++++++++++++++++ src/view/MagazineView.svelte | 295 ++++++++++++++++++++++ 5 files changed, 1191 insertions(+), 16 deletions(-) create mode 100644 src/style/magazine.scss create mode 100644 src/style/modern.scss create mode 100644 src/view/MagazineView.svelte diff --git a/src/settings/MiscSettings.ts b/src/settings/MiscSettings.ts index eb425cd..e784403 100644 --- a/src/settings/MiscSettings.ts +++ b/src/settings/MiscSettings.ts @@ -62,20 +62,6 @@ export class MiscSettings extends SettingsSection { })); }); }); - - new Setting(this.contentEl) - .setName(t("display_style")) - .addDropdown(dropdown => { - return dropdown - .addOption("list", t("list")) - .addOption("cards", t("cards")) - .setValue(this.plugin.settings.displayStyle) - .onChange(async (value) => { - await this.plugin.writeSettings(() => ({ - displayStyle: value - })); - }); - }); } } diff --git a/src/settings/settings.ts b/src/settings/settings.ts index a685604..ecdab55 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -20,7 +20,6 @@ export interface RssReaderSettings { askForFilename: boolean, defaultFilename: string, autoSync: boolean, - displayStyle: string, hotkeys: { create: string, paste: string, @@ -60,7 +59,6 @@ export const DEFAULT_SETTINGS: RssReaderSettings = Object.freeze({ sortOrder: "ALPHABET_NORMAL" }], saveLocation: 'default', - displayStyle: 'cards', saveLocationFolder: '', items: [], dateFormat: "YYYY-MM-DDTHH:mm:SS", diff --git a/src/style/magazine.scss b/src/style/magazine.scss new file mode 100644 index 0000000..a87e65b --- /dev/null +++ b/src/style/magazine.scss @@ -0,0 +1,434 @@ +// Magazine-style theme for RSS Reader + +// Custom Properties +:root { + --rss-primary: var(--interactive-accent); + --rss-secondary: var(--text-muted); + --rss-spacing-xs: 4px; + --rss-spacing-sm: 8px; + --rss-spacing-md: 16px; + --rss-spacing-lg: 24px; + --rss-spacing-xl: 32px; + --rss-radius-sm: 4px; + --rss-radius-md: 8px; + --rss-radius-lg: 16px; + --rss-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1); + --rss-shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1); + --rss-shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1); + --rss-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +// Layout +.rss-container { + max-width: 1200px; + margin: 0 auto; + padding: var(--rss-spacing-md); +} + +// Feed Grid +.rss-feeds-container { + &[data-layout="cards"] { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--rss-spacing-lg); + + .rss-article { + height: 100%; + display: flex; + flex-direction: column; + background: var(--background-primary); + border-radius: var(--rss-radius-md); + box-shadow: var(--rss-shadow-sm); + transition: var(--rss-transition); + overflow: hidden; + position: relative; + + &:hover { + transform: translateY(-2px); + box-shadow: var(--rss-shadow-md); + } + + &-image-container { + position: relative; + width: 100%; + height: 200px; + overflow: hidden; + } + + &-image { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; + } + + &:hover &-image { + transform: scale(1.05); + } + + &-content { + padding: var(--rss-spacing-md); + flex: 1; + display: flex; + flex-direction: column; + gap: var(--rss-spacing-sm); + background: var(--background-primary); + } + + &-title { + font-size: 1.1em; + font-weight: 600; + line-height: 1.4; + margin: 0; + color: var(--text-normal); + + &:hover { + color: var(--text-accent); + } + } + + &-meta { + display: flex; + align-items: center; + gap: var(--rss-spacing-sm); + color: var(--text-muted); + font-size: 0.9em; + + .rss-article-date { + display: flex; + align-items: center; + gap: 4px; + } + + .rss-article-author { + display: flex; + align-items: center; + gap: 4px; + } + } + + &-description { + color: var(--text-muted); + font-size: 0.9em; + line-height: 1.5; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + + // Overlay variant + &.overlay { + .rss-article-content { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, + rgba(0, 0, 0, 0.9) 0%, + rgba(0, 0, 0, 0.7) 50%, + rgba(0, 0, 0, 0) 100%); + color: white; + padding: var(--rss-spacing-md); + } + + .rss-article-title { + color: white; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); + } + + .rss-article-meta { + color: rgba(255, 255, 255, 0.8); + } + + .rss-article-description { + color: rgba(255, 255, 255, 0.9); + } + } + } + } +} + +// Featured Article +.rss-featured { + grid-column: span 8; + position: relative; + border-radius: var(--rss-radius-lg); + overflow: hidden; + box-shadow: var(--rss-shadow-md); + transition: var(--rss-transition); + + &:hover { + transform: translateY(-2px); + box-shadow: var(--rss-shadow-lg); + } + + &-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + &-content { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: var(--rss-spacing-lg); + background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); + color: #fff; + + h2 { + font-size: 2em; + margin: 0 0 var(--rss-spacing-sm); + line-height: 1.2; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + p { + margin: 0; + opacity: 0.9; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + } +} + +// Sidebar +.rss-sidebar { + grid-column: span 4; + display: flex; + flex-direction: column; + gap: var(--rss-spacing-md); +} + +// Article Cards +.rss-article { + background: var(--background-primary); + border-radius: var(--rss-radius-md); + overflow: hidden; + transition: var(--rss-transition); + box-shadow: var(--rss-shadow-sm); + + &:hover { + transform: translateY(-2px); + box-shadow: var(--rss-shadow-md); + } + + &-image { + width: 100%; + height: 200px; + object-fit: cover; + background-color: var(--background-modifier-border); + } + + &-content { + padding: var(--rss-spacing-md); + + h3 { + font-size: 1.2em; + margin: 0 0 var(--rss-spacing-sm); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + p { + color: var(--rss-secondary); + font-size: 0.9em; + line-height: 1.6; + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + } + + &-meta { + display: flex; + align-items: center; + gap: var(--rss-spacing-sm); + margin-top: var(--rss-spacing-sm); + font-size: 0.8em; + color: var(--rss-secondary); + + .rss-tag { + background: var(--background-modifier-success); + color: var(--text-on-accent); + padding: 2px 8px; + border-radius: 12px; + } + } + + &.rss-compact { + display: flex; + align-items: center; + gap: var(--rss-spacing-md); + padding: var(--rss-spacing-sm); + + .rss-article-image { + width: 100px; + height: 100px; + border-radius: var(--rss-radius-sm); + } + + .rss-article-content { + padding: 0; + flex: 1; + + h3 { + font-size: 1em; + } + } + } +} + +// Feed Header +.rss-feed-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--rss-spacing-lg); + padding-bottom: var(--rss-spacing-md); + border-bottom: 1px solid var(--background-modifier-border); + + h1 { + margin: 0; + font-size: 1.8em; + } + + .rss-controls { + display: flex; + gap: var(--rss-spacing-sm); + } +} + +// Buttons +.rss-button { + display: inline-flex; + align-items: center; + gap: var(--rss-spacing-xs); + padding: var(--rss-spacing-sm) var(--rss-spacing-md); + border-radius: var(--rss-radius-md); + background: var(--background-modifier-border); + color: var(--text-normal); + font-size: 0.9em; + transition: var(--rss-transition); + cursor: pointer; + + &:hover { + background: var(--background-modifier-hover); + } + + &-primary { + background: var(--rss-primary); + color: var(--text-on-accent); + + &:hover { + filter: brightness(1.1); + } + } + + svg { + width: 16px; + height: 16px; + } +} + +// Animations +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.rss-animate { + animation: fadeUp 0.5s ease forwards; +} + +// Loading States +.rss-skeleton { + background: linear-gradient( + 90deg, + var(--background-modifier-border) 0%, + var(--background-modifier-hover) 50%, + var(--background-modifier-border) 100% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--rss-radius-sm); +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +// Responsive Design +@media screen and (max-width: 1200px) { + .rss-featured { + grid-column: span 12; + } + + .rss-sidebar { + grid-column: span 12; + } +} + +@media screen and (max-width: 768px) { + .rss-feeds-container { + gap: var(--rss-spacing-sm); + } + + .rss-featured { + aspect-ratio: 4/3; + + &-content h2 { + font-size: 1.5em; + } + } + + .rss-feed-header { + flex-direction: column; + align-items: flex-start; + gap: var(--rss-spacing-md); + + .rss-controls { + width: 100%; + justify-content: space-between; + } + } +} + +// Dark Mode Adjustments +.theme-dark { + .rss-article { + background: var(--background-primary-alt); + &.overlay { + .rss-article-content { + background: linear-gradient(to top, + rgba(0, 0, 0, 0.95) 0%, + rgba(0, 0, 0, 0.8) 50%, + rgba(0, 0, 0, 0) 100%); + } + } + } + + .rss-featured-content { + background: linear-gradient(transparent, rgba(0, 0, 0, 0.9)); + } +} \ No newline at end of file diff --git a/src/style/modern.scss b/src/style/modern.scss new file mode 100644 index 0000000..405048f --- /dev/null +++ b/src/style/modern.scss @@ -0,0 +1,462 @@ +// Modern theme styles for RSS Reader + +// Variables +:root { + --rss-primary-color: var(--interactive-accent); + --rss-hover-color: var(--interactive-accent-hover); + --rss-card-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + --rss-transition: all 0.3s ease; + --rss-border-radius: 8px; + --rss-spacing: 16px; + --rss-radius-md: 12px; + --rss-spacing-md: 24px; +} + +// Feed List View +.rss-feed { + margin-bottom: var(--rss-spacing); + + .rss-feed-title { + display: flex; + align-items: center; + padding: 8px 12px; + border-radius: var(--rss-border-radius); + transition: var(--rss-transition); + + &:hover { + background-color: var(--background-modifier-hover); + } + + img { + margin-left: 8px; + border-radius: 4px; + } + } +} + +// Card View +.rss-card { + background: var(--background-primary); + border-radius: var(--rss-border-radius); + box-shadow: var(--rss-card-shadow); + margin-bottom: var(--rss-spacing); + transition: var(--rss-transition); + overflow: hidden; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + .rss-card-items { + display: grid; + grid-template-columns: 120px 1fr; + gap: 16px; + padding: 16px; + + img { + width: 120px; + height: 90px; + object-fit: cover; + border-radius: 4px; + } + + .rss-item-text { + overflow: hidden; + + h3 { + margin: 0 0 8px; + font-size: 1.1em; + line-height: 1.4; + } + + p { + margin: 0; + color: var(--text-muted); + font-size: 0.9em; + line-height: 1.5; + } + } + } +} + +// Item Modal +.rss-modal { + max-width: 90vw; + width: 800px; + background: var(--background-primary); + border-radius: var(--rss-border-radius); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); + + .modal-close-button { + top: 16px; + right: 16px; + opacity: 0.7; + transition: var(--rss-transition); + + &:hover { + opacity: 1; + background-color: var(--background-modifier-hover); + } + } + + .rss-modal-container { + padding: var(--rss-spacing); + max-height: 85vh; + overflow: hidden; + max-width: 800px; + margin: 0 auto; + } + + .rss-modal-header { + border-bottom: 1px solid var(--background-modifier-border); + padding-bottom: var(--rss-spacing); + margin-bottom: var(--rss-spacing); + } + + .rss-toolbar { + margin-bottom: var(--rss-spacing); + + .rss-button { + padding: 6px 10px; + border-radius: var(--rss-border-radius); + color: var(--text-muted); + transition: var(--rss-transition); + + &:hover { + color: var(--text-normal); + background-color: var(--background-modifier-hover); + } + + svg { + width: 16px; + height: 16px; + } + } + } + + .rss-title-section { + .rss-title { + font-size: 2em; + margin: 0 0 var(--rss-spacing); + line-height: 1.3; + font-weight: 600; + color: var(--text-normal); + } + } + + .rss-meta-section { + display: flex; + flex-wrap: wrap; + gap: var(--rss-spacing); + align-items: center; + margin-bottom: var(--rss-spacing); + + .rss-subtitle { + display: flex; + gap: var(--rss-spacing); + flex-wrap: wrap; + color: var(--text-muted); + font-size: 0.95em; + + .rss-meta { + display: flex; + align-items: center; + gap: 8px; + + .rss-meta-icon { + display: flex; + align-items: center; + opacity: 0.7; + + svg { + width: 16px; + height: 16px; + } + } + } + } + + .rss-tags-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .rss-tag { + font-size: 0.85em; + padding: 4px 10px; + border-radius: 12px; + background: var(--background-modifier-success); + color: var(--text-on-accent); + transition: var(--rss-transition); + + &:hover { + background: var(--interactive-accent); + transform: translateY(-1px); + } + } + } + } + + .rss-content { + flex: 1; + overflow-y: auto; + padding-right: var(--rss-spacing); + font-size: 1.1em; + line-height: 1.7; + color: var(--text-normal); + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--background-modifier-border); + border-radius: 3px; + } + + p { + margin: 1.2em 0; + } + + img { + max-width: 100%; + border-radius: 8px; + margin: 24px 0; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); + } + + h1, h2, h3, h4, h5, h6 { + margin: 1.5em 0 0.8em; + line-height: 1.3; + color: var(--text-normal); + } + + a { + color: var(--rss-primary-color); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: var(--rss-transition); + + &:hover { + border-bottom-color: var(--rss-primary-color); + } + } + + blockquote { + margin: 1.5em 0; + padding: 1em 1.2em; + border-left: 4px solid var(--rss-primary-color); + background: var(--background-modifier-hover); + border-radius: 4px; + font-style: italic; + + p { + margin: 0; + } + } + + code { + background: var(--background-modifier-hover); + padding: 0.2em 0.4em; + border-radius: 4px; + font-size: 0.9em; + font-family: var(--font-monospace); + } + + pre { + background: var(--background-modifier-hover); + padding: 1em; + border-radius: 8px; + overflow-x: auto; + margin: 1.5em 0; + + code { + background: none; + padding: 0; + font-size: 0.9em; + } + } + + ul, ol { + padding-left: 1.5em; + margin: 1.2em 0; + + li { + margin: 0.5em 0; + } + } + + table { + width: 100%; + border-collapse: collapse; + margin: 1.5em 0; + + th, td { + padding: 0.75em; + border: 1px solid var(--background-modifier-border); + } + + th { + background: var(--background-modifier-hover); + font-weight: 600; + } + + tr:nth-child(even) { + background: var(--background-secondary); + } + } + + mark { + background-color: var(--text-highlight-bg); + color: var(--text-normal); + padding: 0 0.2em; + border-radius: 2px; + } + + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; + } + + .rss-modal-image { + width: 100%; + max-height: 400px; + overflow: hidden; + border-radius: var(--rss-radius-md); + background-color: var(--background-modifier-border); + + @media screen and (max-width: 600px) { + float: none !important; + max-width: 100% !important; + margin: 0 0 var(--rss-spacing-md) 0 !important; + } + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } +} + +// Dark mode adjustments +.theme-dark .rss-modal { + .rss-content { + img { + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2); + } + + blockquote { + background: var(--background-primary); + } + + code, pre { + background: var(--background-primary); + } + } +} + +// Tags +.rss-tag { + display: inline-block; + padding: 4px 8px; + margin: 0 4px 4px 0; + border-radius: 12px; + background-color: var(--background-modifier-success); + color: var(--text-on-accent); + font-size: 0.8em; + transition: var(--rss-transition); + + &:hover { + background-color: var(--interactive-accent); + } +} + +// Read/Unread States +.rss-read { + opacity: 0.7; + + a { + color: var(--text-muted); + } +} + +// Buttons and Controls +.rss-button { + padding: 6px 12px; + border-radius: var(--rss-border-radius); + background-color: var(--background-modifier-border); + color: var(--text-normal); + transition: var(--rss-transition); + + &:hover { + background-color: var(--background-modifier-hover); + } + + &.is-active { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + } +} + +// Scrollable Content +.rss-scrollable-content { + scrollbar-width: thin; + scrollbar-color: var(--background-modifier-border) transparent; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--background-modifier-border); + border-radius: 3px; + } +} + +// Animations +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.rss-feed-items { + animation: fadeIn 0.3s ease; +} + +// Responsive Design +@media screen and (max-width: 768px) { + .rss-card { + .rss-card-items { + grid-template-columns: 1fr; + + img { + width: 100%; + height: 180px; + } + } + } + + .rss-modal { + width: 95vw; + + .rss-title { + font-size: 1.5em; + } + } +} \ No newline at end of file diff --git a/src/view/MagazineView.svelte b/src/view/MagazineView.svelte new file mode 100644 index 0000000..bd18006 --- /dev/null +++ b/src/view/MagazineView.svelte @@ -0,0 +1,295 @@ + + +{#if visible} +
+
+

{t("RSS_Feeds")}

+
+ + +
+
+ + {#if loading} +
+ {#each Array(6) as _} +
+ {/each} +
+ {:else} +
+ {#each allItems as item} + {#if item.mediaThumbnail() && !item.mediaThumbnail().includes(".mp3")} +
openItem(item)} + on:keydown={(e) => handleKeyPress(e, item)} + on:contextmenu={(e) => openMenu(e, item)}> +
+ {item.title()} +
+
+

{item.title()}

+ + {#if item.description()} +

{truncateText(item.description(), 120)}

+ {/if} +
+
+ {:else} +
openItem(item)} + on:keydown={(e) => handleKeyPress(e, item)} + on:contextmenu={(e) => openMenu(e, item)}> +
+

{item.title()}

+ + {#if item.description()} +

{truncateText(item.description(), 120)}

+ {/if} +
+
+ {/if} + {/each} +
+ {/if} +
+{/if} + + \ No newline at end of file From 0c54e73f35d553c0710643c4219bbab95d6617f9 Mon Sep 17 00:00:00 2001 From: elwin Date: Wed, 21 May 2025 20:32:26 -0400 Subject: [PATCH 4/5] refactor: update UI components and fix note creation functionality --- .DS_Store | Bin 0 -> 10244 bytes src/.DS_Store | Bin 0 -> 8196 bytes src/functions.ts | 106 +++++------ src/l10n/.DS_Store | Bin 0 -> 6148 bytes src/main.ts | 16 +- src/modals/ItemModal.ts | 223 +++++++++++++++-------- src/providers/local/LocalFeedItem.ts | 45 ++--- src/providers/local/LocalFeedProvider.ts | 58 ++++-- src/style/main.scss | 63 ++++--- src/types/svelte.d.ts | 7 + src/view/CardView.svelte | 142 +++++++++++++++ src/view/FeedView.svelte | 39 +++- src/view/MainView.svelte | 33 ++-- src/view/ViewLoader.ts | 88 +++------ svelte.config.js | 14 +- tsconfig.json | 25 +-- 16 files changed, 561 insertions(+), 298 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store create mode 100644 src/l10n/.DS_Store create mode 100644 src/types/svelte.d.ts create mode 100644 src/view/CardView.svelte diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c1679d10da34e8b71f60780354a99c3541fd3dca GIT binary patch literal 10244 zcmeGiZEPGz^}V%Y@2=x`?Ic{{*7O>eG_4)ij%mK3=dXM=AH;U-x@oR=w;Ow#yB>vSt!c->tz9y2eY$|?FYTRzd)s~ zepy4Ks_9MouHAd|w9!_wt+_+9f$h2eQLEq+OdBo!j5oJ8=edUq7Hgl#c}_9!`e~yz zmvM4MClgo$jx%b1(()$*>r}wLrhKmu3~|Isn`l{G+AxZ~b;Ke9yJQU$nZK$+cBs9% zym>{7(b~3h)#?v-ZrHeK^N!SH&dcX5H}J=uOxAKHN@8U9n#5tKV9~ER<`e&8tm1%Y z+9zzPIcdA*n48I4$658=89z8QYq^$r@k5CDNR>y)asVz<#4V&A+@)eO^E78cjvO2z#~gDAFlb*rlJJwxF|y}h?jQ;m#K7Z$tj zq#}xfzHPgv8NN{;7C(NMM*SQ&fS23bhlYvM4-8f7Ox-R-!>7=h>J=r8PVKeO11Z=8 z$H0PFI1T6EX*dtxh8N)iybc%PP51-+3EqawsG@=Gn8fvXD{jCp+=!cT3vR{jxD!Wk z7w*QrxF5%G9FO88PGJsBv~U`2ETE48mhjU!j}PHz@U!?BejXpkFW?vPJU)ZZ;+OEt z_!WE}zl$%4p($+Jv9wQs@-M!ehV)*?%Viz5|JaoR1C)C%^Tg3KARNep2RS$iJ}WuX zeWSGQv%DRC|5JiBu=3vUaL8AE=Og*Y@A%6P8Sk2y=2c?O&PY8Y8!ohvCS@pFbQku} z7(H;|1ZmbZl^y^;Iy>wjiu2O!Bt5Lad~|8d?3_>XRHDFePC8 z8%rRTK(z$cQI%wK$TrfYneOSs!bXZaKaAf^1%vR1E-PF}y}sS8h$OqT3J zUPrzAxgYHm>v#qH#r3=ze0g14*RW-UR(dvN_)CNDPS{AZ6)z8N&DW3^?bwQ$M>#A( z$96-6PJ<(~ngcKl2MG4iikVRv^F8n>X%E!V3Kk$&fyLIo29_*$sDE`2%{8Q#rTpZn zfA#kXDKE4x%Cx#wmLN3p2bn>mV*2gSzP8?fXY(B=kGG!Fkyb?Kg=~k_#EZ-f4!uP) zTi_wj?eaJLx5AS^YG0gPt@g)HcH=c%{7|Q?h>J z6ieX0m%v(Lv7GiDy9FHDhc4}AIzz-xjA|UaML_n%)DzY;(NtkFf2z9V+)7HV2YvZR}dMtH_%=+;_T$Lh0)+a5Ay6H&V4 zAk1Bfiu>ZuOE*p{Q89bvG`9h&_D^-cd=JSSYIxtG>%{w%M_Pj-x>m9Ok9q9h&;K{( zc}_O;1&(F1N6A^AHdYOJZjiBEtH2$GwrLT{u?cc=dvk+c(KUVUI~)}as-|gaW9^wU zeSMp@bSJm=Zav$bJhN?6Z+G&e{hQC8)zsCUckDQlKI!CLkEt6a4Bzi`ZAQ~MDoW3U z$dWd^@mNKxUm6k)PvyO`!!r`17#lx&eS|2UU%tBiCKj96vU6b9zJnKez(QZCPmu`S zfJGk)l`(~Ktbu#;na5w_R1}IxOct~DgGD>v>feb;`uiu#{2U94VAu6}(+Y<``7Zw8% z)gMdTm8b}$P@qDz4JL*YQ~pe*U>%yHZKfKpi~8ME&%;O5!s0I@rX6^NaLoH@D)+F+ z%z$ZHCRG|OE1DCB6Gv3zXDUUCY`0{SbDJVi!pD8>5s~NM1$Yg90&h@6>i{Qp|n#`v)WVhQ{YNdW7IM~4S#z~ytM za_r34o}_#~WtJG;ynvj9enNLV|6s284U`!kH)X@`R0!t<kD7?J`hP0Fd#f+>*bHj(oKq)3Gn5?cY}^;(V-NVj|e6!{FKKoA8T zG*I#jG^zQ5NSt|icg8!8kRl;V+>vIEHTQLLcJ0xf08pDpy$-Mjpv`IU-ZGcOFm^K= zfQ``}GmoW18_Jxndr&@9`4&UDaN4zTCuSZ?hb|n- zg+uwu%6BNrR%e{H?ogRSSw#U+ps9f7)@?k)J|=h(_V4+~IWO-tJ@TU4-5V5pw6woH z&fnet{e2_l)%y#t(Q|WKFs)|4y)6vT!2=B4O@gY@+Qa&1CzS?Kj=ZQj%=3wPWRAy(9L6|AALB>^Y`nKT z0(;2gQ5o=vd}2{(`DXMCmswR_pQgP+PM4<@r$oL`p{qS3eoC`2 zPYk2b#J;&h9L%!&@FV3Da=ZJv;?~F&GP}wz!E1@UCOF|+)Q_Lu%9xA-Ysh8wOT{Jf zsGnTgBdYHX&z5~+eY}dRzkJK;l`X8j{ { - console.log("creating new note"); - const activeFile = plugin.app.workspace.getActiveFile(); - let dir = plugin.app.fileManager.getNewFileParent(activeFile ? activeFile.path : "").path; + try { + console.log("Creating new note from item:", item.title()); + const activeFile = plugin.app.workspace.getActiveFile(); + let dir = plugin.app.fileManager.getNewFileParent(activeFile ? activeFile.path : "").path; - if (plugin.settings.saveLocation === "custom") { - dir = plugin.settings.saveLocationFolder; - } + if (plugin.settings.saveLocation === "custom") { + dir = plugin.settings.saveLocationFolder; + } - let filename = applyTemplate(plugin, item, plugin.settings.defaultFilename); - //make sure there are no slashes in the title. - filename = filename.replace(/[\/\\:]/g, ' '); + let filename = applyTemplate(plugin, item, plugin.settings.defaultFilename); + //make sure there are no slashes in the title. + filename = filename.replace(/[\/\\:]/g, ' '); - if (plugin.settings.askForFilename) { - const inputPrompt = new TextInputPrompt(plugin.app, t("specify_name"), t("cannot_contain") + " * \" \\ / < > : | ?", filename, filename); - await inputPrompt - .openAndGetValue(async (text: TextComponent) => { + if (plugin.settings.askForFilename) { + const inputPrompt = new TextInputPrompt( + plugin.app, + t("specify_name"), + t("cannot_contain") + " * \" \\ / < > : | ?", + filename, + filename + ); + + await inputPrompt.openAndGetValue(async (text: TextComponent) => { const value = text.getValue(); if (value.match(FILE_NAME_REGEX)) { inputPrompt.setValidationError(text, t("invalid_filename")); @@ -36,42 +43,50 @@ export async function createNewNote(plugin: RssReaderPlugin, item: Item): Promis } const filePath = normalizePath([dir, `${value}.md`].join('/')); - if (isInVault(filePath)) { + if (await plugin.app.vault.adapter.exists(filePath)) { inputPrompt.setValidationError(text, t("note_exists")); return; } inputPrompt.close(); await createNewFile(plugin, item, filePath, value); }); - } else { - const replacedTitle = filename.replace(FILE_NAME_REGEX, ''); - const filePath = normalizePath([dir, `${replacedTitle}.md`].join('/')); - await createNewFile(plugin, item, filePath, item.title()); + } else { + const replacedTitle = filename.replace(FILE_NAME_REGEX, ''); + const filePath = normalizePath([dir, `${replacedTitle}.md`].join('/')); + await createNewFile(plugin, item, filePath, filename); + } + } catch (error) { + console.error('Error creating new note:', error); + new Notice('Error creating new note: ' + error.message); } - - } async function createNewFile(plugin: RssReaderPlugin, item: Item, path: string, title: string) { - if (isInVault(path)) { - new Notice(t("note_exists")); - return; - } - - const appliedTemplate = applyTemplate(plugin, item, plugin.settings.template, title); + try { + if (await plugin.app.vault.adapter.exists(path)) { + new Notice(t("note_exists")); + return; + } - const file = await plugin.app.vault.create(path, appliedTemplate); - await plugin.app.workspace.getLeaf('tab').openFile(file, { - state: {mode: 'edit'}, - }); + const appliedTemplate = applyTemplate(plugin, item, plugin.settings.template, title); + const file = await plugin.app.vault.create(path, appliedTemplate); + + const leaf = plugin.app.workspace.getLeaf('tab'); + if (leaf) { + await leaf.openFile(file, { + state: { mode: 'source' }, + }); + } - item.markCreated(true); - const items = plugin.settings.items; - await plugin.writeFeedContent(() => { - return items; - }); + await item.markCreated(true); + const items = plugin.settings.items; + await plugin.writeFeedContent(() => items); - new Notice(t("created_note")); + new Notice(t("created_note")); + } catch (error) { + console.error('Error in createNewFile:', error); + new Notice('Error creating file: ' + error.message); + } } export async function pasteToNote(plugin: RssReaderPlugin, item: Item): Promise { @@ -102,6 +117,7 @@ export async function pasteToNote(plugin: RssReaderPlugin, item: Item): Promise< } function applyTemplate(plugin: RssReaderPlugin, item: Item, template: string, filename?: string): string { + const moment = (window as any).moment; let result = template.replace(/{{title}}/g, item.title()); result = result.replace(/{{link}}/g, item.url()); result = result.replace(/{{author}}/g, item.author()); @@ -111,7 +127,7 @@ function applyTemplate(plugin: RssReaderPlugin, item: Item, template: string, fi result = result.replace(/{{feed}}/g, item.feed()); result = result.replace(/{{folder}}/g, item.folder()); result = result.replace(/{{description}}/g, item.description()); - result = result.replace(/{{media}}/g, item.enclosureLink); + result = result.replace(/{{media}}/g, item.enclosureLink()); result = result.replace(/({{published:).*(}})/g, function (k) { const value = k.split(":")[1]; @@ -140,8 +156,6 @@ function applyTemplate(plugin: RssReaderPlugin, item: Item, template: string, fi result = result.replace(/{{tags}}/g, item.tags().join(", ")); result = result.replace(/{{#tags}}/g, item.tags().map(i => '#' + i).join(", ")); - - result = result.replace(/{{highlights}}/g, item.highlights().map(value => { //remove wallabag.xml - from the start of a highlight return "- " + rssToMd(plugin, removeFormatting(value).replace(/^(-+)/, "")) @@ -160,24 +174,14 @@ function applyTemplate(plugin: RssReaderPlugin, item: Item, template: string, fi result = result.replace(/{{filename}}/g, filename); } - let content = rssToMd(plugin, item.body()); item.highlights().forEach(highlight => { const mdHighlight = htmlToMarkdown(highlight); content = content.replace(mdHighlight, "==" + mdHighlight + "=="); - - }); - /* - fixes #48 - replacing $ with $$$, because that is a special regex character: - https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/string/replace#specifying_a_string_as_a_parameter - solution taken from: https://stackoverflow.com/a/22612228/5589264 - */ - content = content.replace(/\$/g, "$$$"); - + content = content.replace(/\$/g, "$$$"); result = result.replace(/{{content}}/g, content); return result; diff --git a/src/l10n/.DS_Store b/src/l10n/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..119a229cf8f893f16dbabc7fd2f2ebf446ebc6c0 GIT binary patch literal 6148 zcmeHKJ5B>J5S@WYtVELvQThsj3z%p*LCQ2C34&syjf7~Zd?BvEb+`iWc_xuo*@6ay z(2QiivFBs&lh*c#h|XWPGm)u?G-yPnG9sKFHJ!Qh1gJGfCy(;dJ#kqJ(#ZSU^5*m7Zp>@@4X^z(**dINkR~J;2nK?I zU?3RyQ3h~lQ<_V|=!1b^AQ(6@!1E!Y5wl}4tVajBS^@z1jIIKmwFG05V|FZtut3y8 zfflM>VyJ~cxlp%7@}b>*~m#s=H`5j6N6$28Ilr+i=SJ{~o{0Xpuh* ziBT{R4E!?&bkZ)`IX=qo)|1cUU7OHWXcYPtDiG*{M*s#qN3N^U?1?)3vSTr17S7jj PU_1mWA<+c`zres31_UkB literal 0 HcmV?d00001 diff --git a/src/main.ts b/src/main.ts index 4e641f9..29f91c7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,18 +35,23 @@ export default class RssReaderPlugin extends Plugin { providers: Providers; async onload(): Promise { - console.log('loading plugin rss reader'); + console.log('RSS Reader: Loading plugin'); //update settings object whenever store contents change. this.register( settingsStore.subscribe((value: RssReaderSettings) => { + console.log('RSS Reader: Settings store updated:', value); this.settings = value; }) ); await this.loadSettings(); + console.log('RSS Reader: Initial settings loaded:', this.settings); + this.providers = new Providers(this); + console.log('RSS Reader: Providers initialized'); this.providers.register(new LocalFeedProvider(this)); + console.log('RSS Reader: Local feed provider registered'); //this.providers.register(new NextcloudFeedProvider(this)); this.addCommand({ @@ -455,12 +460,15 @@ export default class RssReaderPlugin extends Plugin { } async loadSettings(): Promise { + console.log('RSS Reader: Loading settings...'); const configPath = this.app.vault.configDir + "/plugins/rss-reader/data.json"; + console.log('RSS Reader: Config path:', configPath); let file: string; try { file = await this.app.vault.adapter.read(configPath); + console.log('RSS Reader: Read config file:', file); } catch (e) { - console.error(e); + console.error('RSS Reader: Error reading config file:', e); } if (file !== undefined) { @@ -468,17 +476,19 @@ export default class RssReaderPlugin extends Plugin { JSON.parse(file); } catch (e) { console.log(t("RSS_Reader") + " could not parse json, check if the plugins data.json is valid."); - console.error(e); + console.error('RSS Reader: Error parsing config file:', e); new Notice(t("RSS_Reader") + " could not parse plugin data. If this message keeps showing up, check the console"); return Promise.resolve(); } } const data = await this.loadData(); + console.log('RSS Reader: Loaded data:', data); this.settings = Object.assign({}, DEFAULT_SETTINGS, data); if (data !== undefined && data !== null) { this.settings.hotkeys = Object.assign({}, DEFAULT_SETTINGS.hotkeys, data.hotkeys); } + console.log('RSS Reader: Final settings:', this.settings); settingsStore.set(this.settings); configuredFeedsStore.set(this.settings.feeds); feedsStore.set(this.settings.items); diff --git a/src/modals/ItemModal.ts b/src/modals/ItemModal.ts index c07e1dc..13ad4b4 100644 --- a/src/modals/ItemModal.ts +++ b/src/modals/ItemModal.ts @@ -1,9 +1,12 @@ import { + App, ButtonComponent, htmlToMarkdown, MarkdownRenderer, Menu, Modal, + Notice, + setIcon } from "obsidian"; import RssReaderPlugin from "../main"; import Action from "../actions/Action"; @@ -100,16 +103,23 @@ export class ItemModal extends Modal { const tts = pluginApi("tts"); if (tts && this.plugin.settings.hotkeys.tts) { this.scope.register([], this.plugin.settings.hotkeys.tts, () => { - if (tts.isSpeaking()) { - if (tts.isPaused()) { - tts.resume(); - } else { - tts.pause(); + try { + // Check if TTS is currently active + const isSpeaking = tts.isSpeaking(); + if (isSpeaking) { + const isPaused = tts.isPaused(); + if (isPaused) { + tts.resume(); + } else { + tts.pause(); + } + return; } - return; + const content = htmlToMarkdown(this.item.body()); + tts.say(this.item.title(), content); + } catch (error) { + console.error('Error with TTS:', error); } - const content = htmlToMarkdown(this.item.body()); - tts.say(this.item.title(), content); }); } } @@ -148,7 +158,7 @@ export class ItemModal extends Modal { async markAsRead(): Promise { await Action.READ.processor(this.plugin, this.item); this.readButton.setIcon((this.item.read()) ? 'eye-off' : 'eye'); - this.readButton.setTooltip((this.item.read()) ? t("mark_as_unread") : t("mark_as_unread")); + this.readButton.setTooltip((this.item.read()) ? t("mark_as_unread") : t("mark_as_read")); } async display(): Promise { @@ -156,51 +166,87 @@ export class ItemModal extends Modal { const {contentEl} = this; contentEl.empty(); - //don't add any scrolling to modal content - contentEl.style.height = "100%"; - contentEl.style.overflowY = "hidden"; + // Create a container for better layout control + const container = contentEl.createDiv('rss-modal-container'); + container.style.height = "100%"; + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = "var(--rss-spacing-md)"; + + // Header section with title and metadata + const header = container.createDiv('rss-modal-header'); + + // Navigation and actions toolbar + const toolbar = header.createDiv('rss-toolbar'); + toolbar.style.display = "flex"; + toolbar.style.justifyContent = "space-between"; + toolbar.style.marginBottom = "var(--rss-spacing-md)"; + + // Navigation group + const navGroup = toolbar.createDiv('rss-nav-group'); + navGroup.style.display = "flex"; + navGroup.style.gap = "var(--rss-spacing-xs)"; + + const prevButton = new ButtonComponent(navGroup) + .setIcon("left-arrow-with-tail") + .setTooltip(t("previous")) + .onClick(() => this.previous()); + prevButton.buttonEl.addClass("rss-button"); - const topButtons = contentEl.createDiv('topButtons'); + const nextButton = new ButtonComponent(navGroup) + .setIcon("right-arrow-with-tail") + .setTooltip(t("next")) + .onClick(() => this.next()); + nextButton.buttonEl.addClass("rss-button"); - let actions = Array.of(Action.CREATE_NOTE, Action.PASTE, Action.COPY, Action.OPEN); + // Actions group + const actionsGroup = toolbar.createDiv('rss-actions-group'); + actionsGroup.style.display = "flex"; + actionsGroup.style.gap = "var(--rss-spacing-xs)"; if (this.save) { - this.readButton = new ButtonComponent(topButtons) - .setIcon(this.item.read ? 'eye-off' : 'eye') - .setTooltip(this.item.read ? t("mark_as_unread") : t("mark_as_read")) - .onClick(async () => { - await this.markAsRead(); - }); + this.readButton = new ButtonComponent(actionsGroup) + .setIcon(this.item.read() ? 'eye-off' : 'eye') + .setTooltip(this.item.read() ? t("mark_as_unread") : t("mark_as_read")) + .onClick(async () => await this.markAsRead()); this.readButton.buttonEl.setAttribute("tabindex", "-1"); this.readButton.buttonEl.addClass("rss-button"); - this.favoriteButton = new ButtonComponent(topButtons) + this.favoriteButton = new ButtonComponent(actionsGroup) .setIcon(this.item.starred() ? 'star-glyph' : 'star') .setTooltip(this.item.starred() ? t("remove_from_favorites") : t("mark_as_favorite")) - .onClick(async () => { - await this.markAsFavorite(); - }); + .onClick(async () => await this.markAsFavorite()); this.favoriteButton.buttonEl.setAttribute("tabindex", "-1"); this.favoriteButton.buttonEl.addClass("rss-button"); - - actions = Array.of(Action.TAGS, ...actions); } + const actions = this.save + ? [Action.TAGS, Action.CREATE_NOTE, Action.PASTE, Action.COPY, Action.OPEN] + : [Action.CREATE_NOTE, Action.PASTE, Action.COPY, Action.OPEN]; actions.forEach((action) => { - const button = new ButtonComponent(topButtons) + const button = new ButtonComponent(actionsGroup) .setIcon(action.icon) .setTooltip(action.name) .onClick(async () => { - await action.processor(this.plugin, this.item); + try { + await action.processor(this.plugin, this.item); + if (action === Action.CREATE_NOTE) { + this.close(); + } + } catch (error) { + console.error('Error processing action:', error); + new Notice('Error: ' + error.message); + } }); button.buttonEl.setAttribute("tabindex", "-1"); button.buttonEl.addClass("rss-button"); }); + if(window['PluginApi']) { const tts = pluginApi("tts"); if (tts) { - const ttsButton = new ButtonComponent(topButtons) + const ttsButton = new ButtonComponent(actionsGroup) .setIcon("headphones") .setTooltip(t("read_article_tts")) .onClick(async () => { @@ -211,55 +257,78 @@ export class ItemModal extends Modal { } } - const prevButton = new ButtonComponent(topButtons) - .setIcon("left-arrow-with-tail") - .setTooltip(t("previous")) - .onClick(() => { - this.previous(); - }); - prevButton.buttonEl.addClass("rss-button"); - - const nextButton = new ButtonComponent(topButtons) - .setIcon("right-arrow-with-tail") - .setTooltip(t("next")) - .onClick(() => { - this.next(); - }); - nextButton.buttonEl.addClass("rss-button"); + // Title and metadata + const titleSection = header.createDiv('rss-title-section'); + titleSection.createEl('h1', { + cls: ["rss-title", "rss-selectable"], + text: this.item.title() + }); - contentEl.createEl('h1', {cls: ["rss-title", "rss-selectable"], text: this.item.title()}); + const metaSection = titleSection.createDiv('rss-meta-section'); + + if (this.item.author() || this.item.pubDate) { + const subtitle = metaSection.createEl("div", {cls: "rss-subtitle"}); + subtitle.addClass("rss-selectable"); + + if (this.item.author()) { + const authorSpan = subtitle.createSpan({cls: "rss-meta"}); + authorSpan.createSpan({cls: "rss-meta-icon"}).innerHTML = ''; + authorSpan.createSpan({text: this.item.author()}); + } + + if (this.item.pubDate) { + const dateSpan = subtitle.createSpan({cls: "rss-meta"}); + dateSpan.createSpan({cls: "rss-meta-icon"}).innerHTML = ''; + dateSpan.createSpan({text: window.moment(this.item.pubDate()).format(this.plugin.settings.dateFormat)}); + } + } - const subtitle = contentEl.createEl("h3", "rss-subtitle"); - subtitle.addClass("rss-selectable"); - if (this.item.author()) { - subtitle.appendText(this.item.author()); + if (this.item.tags().length > 0) { + const tagsContainer = metaSection.createDiv('rss-tags-container'); + this.item.tags().forEach((tag) => { + const tagEl = tagsContainer.createEl("a", { + cls: ["tag", "rss-tag"], + text: tag + }); + }); } - if (this.item.pubDate) { - subtitle.appendText(" - " + window.moment(this.item.pubDate()).format(this.plugin.settings.dateFormat)); + + // Display the article image if available + const thumbnail = this.item.mediaThumbnail(); + if (thumbnail && !thumbnail.includes(".mp3")) { + const imageContainer = container.createDiv('rss-modal-image'); + imageContainer.style.float = "right"; + imageContainer.style.maxWidth = "40%"; + imageContainer.style.margin = "0 0 var(--rss-spacing-md) var(--rss-spacing-md)"; + + const img = imageContainer.createEl('img', { + attr: { + src: thumbnail, + alt: this.item.title() + } + }); + img.style.width = "100%"; + img.style.height = "auto"; + img.style.borderRadius = "var(--rss-radius-md)"; } - const tagEl = contentEl.createSpan("tags"); - this.item.tags().forEach((tag) => { - const tagA = tagEl.createEl("a"); - tagA.setText(tag); - tagA.addClass("tag", "rss-tag"); - }); - const content = contentEl.createDiv('rss-content'); - content.addClass("rss-scrollable-content", "rss-selectable"); + // Main content area + const contentArea = container.createDiv('rss-content'); + contentArea.addClass("rss-scrollable-content", "rss-selectable"); if (this.item.enclosureLink() && this.plugin.settings.displayMedia) { if (this.item.enclosureMime().toLowerCase().contains("audio")) { - const audio = content.createEl("audio", {attr: {controls: "controls"}}); + const audio = contentArea.createEl("audio", {attr: {controls: "controls"}}); audio.createEl("source", {attr: {src: this.item.enclosureLink(), type: this.item.enclosureMime()}}); } if (this.item.enclosureMime().toLowerCase().contains("video")) { - const video = content.createEl("video", {attr: {controls: "controls", width: "100%", height: "100%"}}); + const video = contentArea.createEl("video", {attr: {controls: "controls", width: "100%", height: "100%"}}); video.createEl("source", {attr: {src: this.item.enclosureLink(), type: this.item.enclosureMime()}}); } //embedded yt player if (this.item.enclosureLink() && typeof this.item.id() === "string" && (this.item.id() as string).startsWith("yt:")) { - content.createEl("iframe", { + contentArea.createEl("iframe", { attr: { type: "text/html", src: "https://www.youtube.com/embed/" + this.item.enclosureLink(), @@ -275,13 +344,13 @@ export class ItemModal extends Modal { //prepend empty yaml to fix rendering errors const markdown = "---\n---" + rssToMd(this.plugin, this.item.body()); - await MarkdownRenderer.renderMarkdown(markdown, content, "", this.plugin); + await MarkdownRenderer.renderMarkdown(markdown, contentArea, "", this.plugin); this.item.highlights().forEach(highlight => { - if (content.innerHTML.includes(highlight)) { + if (contentArea.innerHTML.includes(highlight)) { const newNode = contentEl.createEl("mark"); newNode.innerHTML = highlight; - content.innerHTML = content.innerHTML.replace(highlight, newNode.outerHTML); + contentArea.innerHTML = contentArea.innerHTML.replace(highlight, newNode.outerHTML); newNode.remove(); } else { console.log("Highlight not included"); @@ -289,11 +358,14 @@ export class ItemModal extends Modal { } }); - content.addEventListener('contextmenu', (event) => { + contentArea.addEventListener('contextmenu', (event) => { event.preventDefault(); const selection = document.getSelection(); + if (!selection || selection.rangeCount === 0) return; + const range = selection.getRangeAt(0); + if (!range || !range.startContainer) return; const div = contentEl.createDiv(); const htmlContent = range.cloneContents(); @@ -304,12 +376,17 @@ export class ItemModal extends Modal { const menu = new Menu(); - let previousHighlight: HTMLElement; - if (this.item.highlights().includes(range.startContainer.parentElement.innerHTML)) { - previousHighlight = range.startContainer.parentElement; - } - if (this.item.highlights().includes(range.startContainer.parentElement.parentElement.innerHTML)) { - previousHighlight = range.startContainer.parentElement.parentElement; + // Initialize previousHighlight as undefined + let previousHighlight: HTMLElement | undefined; + + const startElement = range.startContainer.parentElement; + const startParentElement = startElement?.parentElement; + + // Check if elements exist before accessing their innerHTML + if (startElement && this.item.highlights().includes(startElement.innerHTML)) { + previousHighlight = startElement; + } else if (startParentElement && this.item.highlights().includes(startParentElement.innerHTML)) { + previousHighlight = startParentElement; } if(previousHighlight) { @@ -329,7 +406,7 @@ export class ItemModal extends Modal { }); }); }); - }else if(!this.item.highlights().includes(selected) && selected.length > 0) { + } else if(!this.item.highlights().includes(selected) && selected.length > 0) { menu.addItem(item => { item .setIcon("highlight-glyph") diff --git a/src/providers/local/LocalFeedItem.ts b/src/providers/local/LocalFeedItem.ts index 3419143..d99b37e 100644 --- a/src/providers/local/LocalFeedItem.ts +++ b/src/providers/local/LocalFeedItem.ts @@ -10,31 +10,31 @@ export class LocalFeedItem implements Item { } author(): string { - return this.item.creator; + return this.item.creator || ""; } body(): string { - return this.item.content; + return this.item.content || ""; } created(): boolean { - return false; + return this.item.created || false; } description(): string { - return this.item.description; + return this.item.description || ""; } enclosureLink(): string { - return ""; + return this.item.enclosure || ""; } enclosureMime(): string { - return ""; + return this.item.enclosureType || ""; } feed(): string { - return ""; + return this.item.feed || ""; } feedId(): number { @@ -42,37 +42,39 @@ export class LocalFeedItem implements Item { } folder(): string { - return ""; + return this.item.folder || ""; } guid(): string { - return ""; + return this.item.id || ""; } guidHash(): string { - return ""; + return this.item.hash || ""; } highlights(): string[] { - return []; + return this.item.highlights || []; } id(): string | number { - return undefined; + return this.item.id || ""; } language(): string | undefined { - return this.item.language; + return this.item.language || undefined; } markCreated(created: boolean): void { - + this.item.created = created; } markRead(read: boolean): void { + this.item.read = read; } markStarred(starred: boolean): void { + this.item.favorite = starred; } mediaDescription(): string { @@ -80,15 +82,15 @@ export class LocalFeedItem implements Item { } mediaThumbnail(): string { - return ""; + return this.item.image || ""; } pubDate(): string { - return this.item.pubDate; + return this.item.pubDate || ""; } read(): boolean { - return false; + return this.item.read || false; } rtl(): boolean { @@ -96,22 +98,23 @@ export class LocalFeedItem implements Item { } setTags(tags: string[]): void { + this.item.tags = tags; } starred(): boolean { - return false; + return this.item.favorite || false; } tags(): string[] { - return []; + return this.item.tags || []; } title(): string { - return this.item.title; + return this.item.title || ""; } url(): string { - return this.item.link; + return this.item.link || ""; } } diff --git a/src/providers/local/LocalFeedProvider.ts b/src/providers/local/LocalFeedProvider.ts index 8ef76bb..949b9ee 100644 --- a/src/providers/local/LocalFeedProvider.ts +++ b/src/providers/local/LocalFeedProvider.ts @@ -15,10 +15,7 @@ export class LocalFeedProvider implements FeedProvider { constructor(plugin: RssReaderPlugin) { this.plugin = plugin; - } - - async isValid(): Promise { - return true; + console.log("LocalFeedProvider: Initialized with plugin", plugin); } id(): string { @@ -29,54 +26,85 @@ export class LocalFeedProvider implements FeedProvider { return "Local"; } + async isValid(): Promise { + return true; + } + + warnings(): string[] { + return []; + } + async feeds(): Promise { + console.log("LocalFeedProvider: Loading feeds..."); + console.log("LocalFeedProvider: Current settings:", this.plugin.settings); const result: Feed[] = []; const feeds = this.plugin.settings.feeds; + console.log("LocalFeedProvider: Found feeds in settings:", feeds); + for (const feed of feeds) { - const content = await getFeedItems(feed); - result.push(new LocalFeed(content)); + console.log("LocalFeedProvider: Processing feed:", feed); + try { + const content = await getFeedItems(feed); + console.log("LocalFeedProvider: Got feed items:", content); + if (content) { + result.push(new LocalFeed(content)); + } else { + console.error("LocalFeedProvider: Failed to get items for feed:", feed); + } + } catch (error) { + console.error("LocalFeedProvider: Error processing feed:", feed, error); + } } + console.log("LocalFeedProvider: Returning feeds:", result); return result; } - async feedFromUrl(url: string): Promise { + async feedFromUrl(url: string): Promise { + console.log("LocalFeedProvider: Loading feed from URL:", url); const feed = { name: '', url, folder: '', } const content = await getFeedItems(feed); - return new LocalFeed(content); + console.log("LocalFeedProvider: Got feed content:", content); + return content ? new LocalFeed(content) : undefined; } async filteredFolders(): Promise { return []; } - async folders(): Promise { + console.log("LocalFeedProvider: Loading folders..."); const result: Folder[] = []; const feeds = await this.feeds(); + console.log("LocalFeedProvider: Got feeds for folders:", feeds); const grouped = groupBy(feeds, item => item.folderName()); + console.log("LocalFeedProvider: Grouped feeds by folder:", grouped); for (const key of Object.keys(grouped)) { const folderContent = grouped[key]; result.push(new LocalFolder(key, folderContent)); } + console.log("LocalFeedProvider: Returning folders:", result); return result; } async items(): Promise { - return []; - } - - warnings(): string[] { - return []; + console.log("LocalFeedProvider: Loading all items..."); + const feeds = await this.feeds(); + console.log("LocalFeedProvider: Got feeds for items:", feeds); + const items: Item[] = []; + for (const feed of feeds) { + items.push(...feed.items()); + } + console.log("LocalFeedProvider: Returning items:", items); + return items; } settings(containerEl: HTMLDivElement): SettingsSection { return new LocalFeedSettings(this.plugin, containerEl); } - } diff --git a/src/style/main.scss b/src/style/main.scss index 78086fd..e620b52 100644 --- a/src/style/main.scss +++ b/src/style/main.scss @@ -1,3 +1,9 @@ +// Import modern styles +@import 'modern'; + +// Import magazine styles +@import 'magazine'; + .rss-read a { color: darkslategrey; } @@ -44,25 +50,23 @@ input.is-invalid { height: 60vh; } - .rss-tooltip { position: relative; -} - -.rss-tooltip .tooltiptext { - visibility: hidden; - background-color: var(--interactive-hover); - color: var(--text-normal); - text-align: center; - padding: 5px 0; - border-radius: 6px; - - position: absolute; - z-index: var(--layer-tooltip); -} - -.rss-tooltip:hover .tooltiptext { - visibility: visible; + + .tooltiptext { + visibility: hidden; + background-color: var(--interactive-hover); + color: var(--text-normal); + text-align: center; + padding: 5px 0; + border-radius: 6px; + position: absolute; + z-index: var(--layer-tooltip); + } + + &:hover .tooltiptext { + visibility: visible; + } } .rss-content img { @@ -115,6 +119,9 @@ input.is-invalid { .rss-view { background: var(--background-secondary); + height: 100%; + overflow-y: auto; + padding: var(--size-4-4); } .rss-modal { @@ -130,7 +137,6 @@ img.feed-favicon { background-color: var(--text-highlight-bg); } - .rss-modal mark li { background-color: var(--text-highlight-bg); } @@ -139,10 +145,7 @@ img.feed-favicon { background-color: var(--text-highlight-bg); } -.rss-content .frontmatter { - display: none; -} - +.rss-content .frontmatter, .rss-content .frontmatter-container { display: none; } @@ -156,12 +159,16 @@ img.feed-favicon { border-top: 5px solid var(--background-modifier-border); } -.modal.mod-settings button:not(.mod-cta):not(.mod-warning).rss-test-valid { - background-color: var(--background-modifier-success); -} - -.modal.mod-settings button:not(.mod-cta):not(.mod-warning).rss-test-invalid { - background-color: var(--background-modifier-error); +.modal.mod-settings { + button:not(.mod-cta):not(.mod-warning) { + &.rss-test-valid { + background-color: var(--background-modifier-success); + } + + &.rss-test-invalid { + background-color: var(--background-modifier-error); + } + } } .feed ul{ diff --git a/src/types/svelte.d.ts b/src/types/svelte.d.ts new file mode 100644 index 0000000..989b20d --- /dev/null +++ b/src/types/svelte.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.svelte' { + import type { SvelteComponent } from 'svelte'; + const component: typeof SvelteComponent; + export default component; +} \ No newline at end of file diff --git a/src/view/CardView.svelte b/src/view/CardView.svelte new file mode 100644 index 0000000..c45a976 --- /dev/null +++ b/src/view/CardView.svelte @@ -0,0 +1,142 @@ + + +
new ItemModal(plugin, item, items).open()} + on:contextmenu={openMenu}> +
+ {#if item.image()} + {item.title()} + {:else} +
+ +
+ {/if} + +
+
+ {#if item.starred()} + + {/if} + {#if item.created()} + + {/if} +

{item.title()}

+
+ +

+ {#if item.author()} + {item.author()} • + {/if} + {formatDate(item.pubDate())} +

+ +

+ {truncateText(item.description())} +

+ + {#if item.tags().length > 0} +
+ {#each item.tags() as tag} + {tag} + {/each} +
+ {/if} +
+
+
+ + \ No newline at end of file diff --git a/src/view/FeedView.svelte b/src/view/FeedView.svelte index e9073f8..c6c1e55 100644 --- a/src/view/FeedView.svelte +++ b/src/view/FeedView.svelte @@ -1,10 +1,11 @@ -{#each folders as folder} -

{folder.name()}

- {#each feeds.filter(feed => feed.folderId() === folder.id()) as feed} - - {/each} +
+ +
-{/each} + diff --git a/src/view/ViewLoader.ts b/src/view/ViewLoader.ts index 1b10386..963e4d4 100644 --- a/src/view/ViewLoader.ts +++ b/src/view/ViewLoader.ts @@ -2,23 +2,30 @@ import {setIcon, View, WorkspaceLeaf} from "obsidian"; import RssReaderPlugin from "../main"; import {VIEW_ID} from "../consts"; import t from "../l10n/locale"; -import {ItemModal} from "../modals/ItemModal"; +import MagazineView from "./MagazineView.svelte"; export default class ViewLoader extends View { private readonly plugin: RssReaderPlugin; - - private navigationEl: HTMLElement; - private navigationButtonsEl: HTMLElement; - private contentContainer: HTMLDivElement; + private magazineView: MagazineView; constructor(leaf: WorkspaceLeaf, plugin: RssReaderPlugin) { super(leaf); this.plugin = plugin; - - this.navigationEl = this.containerEl.createDiv('nav-header'); - this.navigationButtonsEl = this.navigationEl.createDiv('nav-buttons-container'); - - this.contentContainer = this.containerEl.createDiv({cls: 'content rss-scrollable-content'}); + console.log("ViewLoader: Constructor called"); + + // Initialize the container + this.containerEl.empty(); + this.containerEl.addClass('rss-view'); + + // Create the magazine view component + this.magazineView = new MagazineView({ + target: this.containerEl, + props: { + plugin: this.plugin, + visible: true + } + }); + console.log("ViewLoader: MagazineView initialized"); } getDisplayText(): string { @@ -34,61 +41,16 @@ export default class ViewLoader extends View { } protected async onOpen(): Promise { - const buttonEl = this.navigationButtonsEl.createDiv('clickable-buttons nav-action-button'); - buttonEl.addEventListener('click', async() => { - await this.displayData(); - }); - setIcon(buttonEl,'refresh-cw'); - buttonEl.setAttr('aria-label', t('refresh_feeds')); - - await this.displayData(); + console.log("ViewLoader: onOpen called"); + if (this.magazineView) { + this.magazineView.$set({ visible: true }); + } } - private async displayData() { - - this.contentContainer.empty(); - - const folders = await this.plugin.providers.getCurrent().folders(); - - for (const folder of folders) { - const folderDiv = this.contentContainer.createDiv('rss-folder'); - const folderCollapseIcon = folderDiv.createSpan(); - setIcon(folderCollapseIcon, 'right-triangle'); - folderDiv.createSpan({text: folder.name()}); - - for (const feed of folder.feeds()) { - const feedDiv = folderDiv.createDiv('feed'); - const feedTitleDiv = feedDiv.createSpan('rss-feed'); - - const feedCollapse = feedTitleDiv.createSpan(); - setIcon(feedCollapse, 'right-triangle'); - - if(feed.favicon()) { - feedTitleDiv.createEl('img', {cls: 'feed-favicon', attr: {src: feed.favicon()}}); - - } - feedTitleDiv.createSpan({text: feed.title()}); - - const feedList = feedDiv.createEl('ul'); - - for (const item of feed.items()) { - const itemDiv = feedList.createEl('li'); - - if(item.starred()) - setIcon(itemDiv.createSpan(), 'star'); - if(item.created()) - setIcon(itemDiv.createSpan(), 'document'); - - if(item.read()) - itemDiv.addClass('rss-read'); - - itemDiv.createSpan({text: item.title()}); - - itemDiv.onClickEvent(() => { - new ItemModal(this.plugin, item, null, true).open(); - }); - } - } + async onClose() { + console.log("ViewLoader: onClose called"); + if (this.magazineView) { + this.magazineView.$destroy(); } } } diff --git a/svelte.config.js b/svelte.config.js index 80dda59..113f7e0 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,5 +1,11 @@ -import sveltePreprocess from 'svelte-preprocess'; +/** @type {import('@sveltejs/kit').Config} */ +const sveltePreprocess = require('svelte-preprocess'); -export default { - preprocess: sveltePreprocess() -}; \ No newline at end of file +const config = { + preprocess: sveltePreprocess({ + typescript: true, + sourceMap: true + }) +}; + +export default config; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2e961c5..5b7c91d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,28 +4,31 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES2020", + "target": "ES6", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, - "strict": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7", + "ES2021" + ], "types": [ - "node", "svelte", + "node", "jest" ], - "lib": [ - "DOM", - "ES2022", - "DOM.Iterable" - ], "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "skipLibCheck": true }, - "include": ["src/**/*"] + "include": [ + "src/**/*.ts", + "src/**/*.svelte" + ] } From 3c1f601ea0a664a464a85e5e76fc25383d8b0112 Mon Sep 17 00:00:00 2001 From: elwin Date: Wed, 21 May 2025 20:37:19 -0400 Subject: [PATCH 5/5] docs: add CHANGELOG.md to track contributions --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d80cd7b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## [Unreleased] +### Added +- Magazine view improvements with consistent card sizing and better image handling (@sidneyer) +- Right-aligned image display with text wrapping in item modal (@sidneyer) +- Enhanced typography and spacing throughout the UI (@sidneyer) + +### Changed +- Standardized on magazine view as the only view option (@sidneyer) +- Simplified settings interface by removing display style options (@sidneyer) + +### Fixed +- Note creation functionality (@sidneyer) +- Template variable handling (@sidneyer) +- Improved error handling (@sidneyer) + +## [1.2.2] - 2022-06-27 +- Previous release notes would go here... \ No newline at end of file