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) + } +}