From 799bf18af7f2c546c8333ee2eb1a8e391ce89fd2 Mon Sep 17 00:00:00 2001
From: mindfreq <144544047+mindfreq@users.noreply.github.com>
Date: Sun, 3 May 2026 12:18:02 +0200
Subject: [PATCH] feat: add feed commands
---
src-tauri/exam.atom | 98 ---------------------------
src-tauri/feeds.json | 3 +-
src-tauri/src/commands.rs | 2 +-
src-tauri/src/commands/feed.rs | 56 ++++++++++++++++
src-tauri/src/commands/feeds.rs | 28 --------
src-tauri/src/config.rs | 114 +++++++++++++++++---------------
src-tauri/src/lib.rs | 19 +++---
src-tauri/src/parser.rs | 112 +++++++++++++++++++++++++------
8 files changed, 220 insertions(+), 212 deletions(-)
delete mode 100644 src-tauri/exam.atom
create mode 100644 src-tauri/src/commands/feed.rs
delete mode 100644 src-tauri/src/commands/feeds.rs
diff --git a/src-tauri/exam.atom b/src-tauri/exam.atom
deleted file mode 100644
index ce7ac27..0000000
--- a/src-tauri/exam.atom
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
- Example Tech Blog
-
-
- urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6
- 2024-03-15T14:30:00Z
-
-
- Latest posts about tech, programming, and web development
-
- Jane Developer
- jane@example.com
- https://example.com/jane
-
- © 2024 Example Tech Blog. All rights reserved.
- Example CMS
- https://example.com/favicon.ico
- https://example.com/logo.png
-
-
-
- Getting Started with Rust
-
- urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
- 2024-03-15T14:30:00Z
- 2024-03-14T09:00:00Z
-
-
- Jane Developer
- jane@example.com
-
-
-
-
-
-
- <h1>Getting Started with Rust</h1>
- <p>Rust is a systems programming language that runs blazingly fast...</p>
- <pre><code>fn main() {
- println!("Hello, world!");
-}
-</code></pre>
-
-
- Learn the basics of Rust programming language in this beginner-friendly tutorial.
- © 2024 Jane Developer
-
-
-
-
- CSS Grid Layout Mastery
-
-
-
- urn:uuid:d8f3e7a2-9b4c-4f8e-9a3c-1e4f7b9d2c5a
- 2024-03-10T11:15:00Z
- 2024-03-08T16:20:00Z
-
-
- John Smith
- https://example.com/john-smith
-
-
-
- Maria Garcia
- maria@example.com
-
-
-
- <div xmlns="http://www.w3.org/1999/xhtml">
- <h1>CSS Grid Layout Mastery</h1>
- <p>CSS Grid is a powerful layout system...</p>
- </div>
-
-
-
-
-
- New Version Released: v2.0.0
-
- tag:example.com,2024-03-01:/blog/version-2-release
- 2024-03-01T08:00:00Z
- 2024-03-01T08:00:00Z
-
- Release Team
-
-
- Version 2.0.0 is now available! Includes performance improvements and bug fixes.
-
-
-
-
\ No newline at end of file
diff --git a/src-tauri/feeds.json b/src-tauri/feeds.json
index 0637a08..32960f8 100644
--- a/src-tauri/feeds.json
+++ b/src-tauri/feeds.json
@@ -1 +1,2 @@
-[]
\ No newline at end of file
+[
+]
\ No newline at end of file
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index abf705f..7065d2b 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -1 +1 @@
-pub mod feeds;
+pub mod feed;
diff --git a/src-tauri/src/commands/feed.rs b/src-tauri/src/commands/feed.rs
new file mode 100644
index 0000000..aac6f7c
--- /dev/null
+++ b/src-tauri/src/commands/feed.rs
@@ -0,0 +1,56 @@
+use crate::client::CLIENT;
+use crate::config::feed_config::Feed;
+use crate::parser::{extract_feed_info, FeedEntry, FeedItem};
+use crate::Error;
+
+// ======================= Feed Config Command =======================
+#[tauri::command]
+pub async fn add_feed(url: &str) -> Result {
+ let html = CLIENT.get(url).send().await?.text().await?;
+
+ let feeds = extract_feed_info(&html, url);
+
+ match feeds.first() {
+ Some(feed) => {
+ let title = feed.title.as_deref().unwrap_or("Untitled Feed");
+ let icon = feed.favicon.as_deref().unwrap_or("favicon.ico");
+ Feed::add(title, &feed.url, &feed.feed_url, icon)
+ }
+ None => {
+ return Err(Error::MissingField(
+ "No RSS/Atom/JSON feed found at this URL".into(),
+ ));
+ }
+ }
+}
+
+#[tauri::command]
+pub async fn get_feeds() -> Result, Error> {
+ let feeds = Feed::get_all()?;
+ Ok(feeds)
+}
+
+#[tauri::command]
+pub async fn remove_feed(website_url: &str) -> Result {
+ Ok(Feed::remove(website_url)?)
+}
+// =======================================================
+
+pub async fn get_feed_item(website_url: &str) -> Result, Error> {
+
+ if let Ok(feed) = Feed::get(&website_url) {
+ let xml = CLIENT.get(&feed.feed_url).send().await?.text().await?;
+ FeedItem::from_feed(&xml)
+ } else {
+ return Err(Error::MissingField("no feed found in the settings.".into()));
+ }
+}
+
+pub async fn get_entry(website_url: &str, target_url: &str) -> Result {
+ if let Ok(feed) = Feed::get(&website_url) {
+ let xml = CLIENT.get(&feed.feed_url).send().await?.text().await?;
+ FeedEntry::from_feed(&xml, &target_url)
+ } else {
+ return Err(Error::MissingField("no feed found in the settings.".into()));
+ }
+}
diff --git a/src-tauri/src/commands/feeds.rs b/src-tauri/src/commands/feeds.rs
deleted file mode 100644
index f90ced6..0000000
--- a/src-tauri/src/commands/feeds.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use crate::Error;
-
-use crate::client::CLIENT;
-use crate::parser::{FeedItemDetail, FeedItem};
-
-pub async fn get_summaries() -> Result, Error> {
- let xml = CLIENT
- .get("https://blog.rust-lang.org/feed")
- .send()
- .await?
- .text()
- .await?;
- let parser_xml = FeedItem::from_feed(&xml);
- println!("{:?}", parser_xml);
- parser_xml
-}
-
-pub async fn get_entry(taget_url: &str) -> Result {
- let xml = CLIENT
- .get("https://blog.rust-lang.org/feed")
- .send()
- .await?
- .text()
- .await?;
- let parser_xml = FeedItemDetail::from_feed(&xml, taget_url);
- println!("{:?}", parser_xml);
- parser_xml
-}
diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs
index 5e081f3..001ea2e 100644
--- a/src-tauri/src/config.rs
+++ b/src-tauri/src/config.rs
@@ -2,62 +2,72 @@ use crate::Error;
use serde::{Deserialize, Serialize};
use std::fs;
-#[derive(Serialize, Deserialize, Debug)]
-struct Feed {
- id: String,
- title: String,
- url: String,
- icon: String,
-}
+pub mod feed_config {
+ use super::*;
-impl Feed {
- fn get(url: &str) -> Result {
- let feeds = Self::get_content()?;
- feeds
- .into_iter()
- .find(|feed| feed.url == url)
- .ok_or_else(|| Error::MissingField(format!("Feed with url '{}' not found", url)))
- }
- fn get_all() -> Result, Error> {
- Self::get_content()
+ #[derive(Serialize, Deserialize, Debug)]
+ pub struct Feed {
+ pub id: String,
+ pub title: String,
+ pub url: String,
+ pub feed_url: String,
+ pub icon: String,
}
- fn add(title: String, url: String, icon: String) -> Result {
- let mut feeds = Self::get_content()?;
- let new_feed = Self {
- id: uuid::Uuid::new_v4().to_string(),
- title,
- url,
- icon,
- };
- feeds.push(new_feed);
- let json = serde_json::to_string_pretty(&feeds)?;
- fs::write("feeds.json", json)?;
-
- Ok(feeds.pop().unwrap())
- }
-
- fn remove(url: &str) -> Result {
- let mut feeds = Self::get_content()?;
- let index = feeds.iter()
- .position(|feed| feed.url == url)
- .ok_or_else(|| Error::MissingField(format!("Feed with url '{}' not found", url)))?;
-
- let removed = feeds.remove(index);
- let json = serde_json::to_string_pretty(&feeds)?;
- fs::write("feeds.json", json)?;
-
- Ok(removed)
-
- }
-
- fn get_content() -> Result, Error> {
- let path = std::path::Path::new("feeds.json");
- if !path.exists() {
- fs::write(path, "[]")?;
+ impl Feed {
+ pub fn get(url: &str) -> Result {
+ let feeds = Self::get_content()?;
+ feeds
+ .into_iter()
+ .find(|feed| feed.url == url)
+ .ok_or_else(|| Error::MissingField(format!("Feed with url '{}' not found", url)))
+ }
+ pub fn get_all() -> Result, Error> {
+ Self::get_content()
}
- let data = fs::read_to_string(&path)?;
- Ok(serde_json::from_str(&data)?)
+ pub fn add(title: &str, url: &str, feed_url: &str, icon: &str) -> Result {
+ let mut feeds = Self::get_content()?;
+
+ // Prevent Duplicate
+ feeds.retain(|feed| feed.url != url);
+
+ let new_feed = Feed {
+ id: uuid::Uuid::new_v4().to_string(),
+ title: title.to_string(),
+ url: url.to_string(),
+ feed_url: feed_url.to_string(),
+ icon: icon.to_string(),
+ };
+ feeds.push(new_feed);
+ let json = serde_json::to_string_pretty(&feeds)?;
+ fs::write("feeds.json", json)?;
+
+ Ok(feeds.pop().unwrap())
+ }
+
+ pub fn remove(url: &str) -> Result {
+ let mut feeds = Self::get_content()?;
+ let index = feeds
+ .iter()
+ .position(|feed| feed.url == url)
+ .ok_or_else(|| Error::MissingField(format!("Feed with url '{}' not found", url)))?;
+
+ let removed = feeds.remove(index);
+ let json = serde_json::to_string_pretty(&feeds)?;
+ fs::write("feeds.json", json)?;
+
+ Ok(removed)
+ }
+
+ fn get_content() -> Result, Error> {
+ let path = std::path::Path::new("feeds.json");
+ if !path.exists() {
+ fs::write(path, "[]")?;
+ }
+
+ let data = fs::read_to_string(&path)?;
+ Ok(serde_json::from_str(&data)?)
+ }
}
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index c3096ac..79b4bbc 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,27 +1,24 @@
-use feed_rs::parser::ParseFeedError;
-
pub mod client;
pub mod commands;
pub mod config;
pub mod parser;
-use client::CLIENT;
+use commands::feed::{add_feed, get_entry, get_feeds, get_feed_item};
pub async fn test_thing() -> Result<(), Error> {
-
-}
+ match get_entry("https://blog.rust-lang.org/", "https://blog.rust-lang.org/2026/03/20/rust-challenges/").await {
+ Ok(feed) => println!("{:?}", feed),
+ Err(e) => eprintln!("add_feed failed: {}", e),
+ }
-// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
-#[tauri::command]
-fn greet(name: &str) -> String {
- format!("Hello, {}! You've been greeted from Rust!", name)
+ Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
- .invoke_handler(tauri::generate_handler![greet])
+ .invoke_handler(tauri::generate_handler![add_feed])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
@@ -33,7 +30,7 @@ pub enum Error {
Io(#[from] std::io::Error),
#[error("Failed to parse feed: {0}")]
- ParseFeed(#[from] ParseFeedError),
+ ParseFeed(#[from] feed_rs::parser::ParseFeedError),
#[error("Missing required field: {0}")]
MissingField(String),
diff --git a/src-tauri/src/parser.rs b/src-tauri/src/parser.rs
index 065748e..1494be4 100644
--- a/src-tauri/src/parser.rs
+++ b/src-tauri/src/parser.rs
@@ -4,7 +4,6 @@ use scraper::{Html, Selector};
use crate::Error;
-
#[derive(Debug)]
pub struct FeedItem {
pub title: String,
@@ -42,9 +41,8 @@ impl FeedItem {
}
}
-
#[derive(Debug)]
-pub struct FeedItemDetail {
+pub struct FeedEntry {
pub id: String,
pub title: String,
pub url: String,
@@ -55,7 +53,7 @@ pub struct FeedItemDetail {
pub authors: Vec,
}
-impl FeedItemDetail {
+impl FeedEntry {
pub fn from_feed(xml: &str, target_url: &str) -> Result {
let feed = parser::parse(xml.as_bytes())?;
@@ -95,26 +93,98 @@ impl FeedItemDetail {
}
}
- Err(Error::MissingField(
- format!("no entry found for url: {}", target_url)
- ))
+ Err(Error::MissingField(format!(
+ "no entry found for url: {}",
+ target_url
+ )))
}
}
+#[derive(Debug, Clone)]
+pub struct FeedInfo {
+ pub url: String,
+ pub feed_url: String,
+ pub title: Option,
+ pub favicon: Option,
+}
-
-pub fn extract_feed_urls(html: &str) -> Vec {
+pub fn extract_feed_info(html: &str, base_url: &str) -> Vec {
let document = Html::parse_document(html);
-
- let selector = Selector::parse(
- r#"link[rel="alternate"][type="application/rss+xml"],
- link[rel="alternate"][type="application/atom+xml"],
- link[rel="alternate"][type="application/feed+json"]"#
- ).unwrap();
- document
- .select(&selector)
- .filter_map(|el| el.value().attr("href"))
- .map(|href| href.to_string())
- .collect()
-}
\ No newline at end of file
+ let rss_selector =
+ Selector::parse(r#"link[rel="alternate"][type="application/rss+xml"]"#).unwrap();
+ let atom_selector =
+ Selector::parse(r#"link[rel="alternate"][type="application/atom+xml"]"#).unwrap();
+ let json_selector =
+ Selector::parse(r#"link[rel="alternate"][type="application/feed+json"]"#).unwrap();
+
+ let favicon = extract_favicon(&document, base_url);
+
+ let mut feeds: Vec = Vec::new();
+
+ for selector in [&rss_selector, &atom_selector, &json_selector] {
+ for el in document.select(selector) {
+ if let Some(href) = el.value().attr("href") {
+ let title = el.value().attr("title").map(String::from);
+
+ feeds.push(FeedInfo {
+ url: base_url.to_string(),
+ feed_url: resolve_url(base_url, href),
+ title,
+ favicon: favicon.clone(),
+ });
+ }
+ }
+ }
+
+ feeds
+}
+
+fn extract_favicon(document: &Html, base_url: &str) -> Option {
+ // ترتيب الأولوية: Apple Touch Icon -> PNG Icon -> Standard Icon -> favicon.ico
+ let favicon_selectors = [
+ r#"link[rel="apple-touch-icon"]"#,
+ r#"link[rel="apple-touch-icon-precomposed"]"#,
+ r#"link[rel="icon"][type="image/png"]"#,
+ r#"link[rel="icon"][sizes="32x32"]"#,
+ r#"link[rel="icon"][sizes="any"]"#,
+ r#"link[rel="icon"]"#,
+ r#"link[rel="shortcut icon"]"#,
+ ];
+
+ for selector_str in &favicon_selectors {
+ if let Ok(sel) = Selector::parse(selector_str) {
+ if let Some(el) = document.select(&sel).next() {
+ if let Some(href) = el.value().attr("href") {
+ return Some(resolve_url(base_url, href));
+ }
+ }
+ }
+ }
+
+ Some(format!("{}/favicon.ico", base_url.trim_end_matches('/')))
+}
+
+fn resolve_url(base: &str, href: &str) -> String {
+ if href.starts_with("http://") || href.starts_with("https://") {
+ href.to_string()
+ } else if href.starts_with("//") {
+ format!("https:{}", href)
+ } else if href.starts_with('/') {
+ // رابط مطلق نسبي
+ let base = if base.ends_with('/') {
+ &base[..base.len() - 1]
+ } else {
+ base
+ };
+ format!("{}{}", base, href)
+ } else {
+ // رابط نسبي
+ let base = if base.ends_with('/') {
+ base.to_string()
+ } else {
+ format!("{}/", base)
+ };
+ format!("{}{}", base, href)
+ }
+}