diff --git a/Cargo.toml b/Cargo.toml index 9cd2430..404e3b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -iced = {git = "https://github.com/iced-rs/iced", branch = "master", features = ["advanced","image","markdown","svg",]} +iced = {git = "https://github.com/iced-rs/iced", branch = "master", features = ["image","markdown","svg",]} reqwest = { version = "0.12", features= ["blocking"]} rss = "2.0" web-sys = "0.3.70" @@ -14,11 +14,12 @@ html2md = "0.2" regex = "1" directories = "6.0.0" chrono = "0.4.41" +rss_content = { git = "https://code.gabe.rocks/gabriel/rss_content", version = "0.1.1" } [profile.dev] debug=true incremental = true -codegen-units = 256 +codegen-units = 16 [profile.release] opt-level = "z" diff --git a/docs/planned_features.md b/docs/planned-features.md similarity index 52% rename from docs/planned_features.md rename to docs/planned-features.md index 9f6675d..ec69d45 100644 --- a/docs/planned_features.md +++ b/docs/planned-features.md @@ -7,21 +7,24 @@ I have ambitious plans for the future, there are many features At minimum I need to build a functionally complete RSS reader which will include: - Feed management -- Beautiful UI +- Beautiful UI (gonna be hard!) - Multimedia support - Export stories to Markdown ### Performance +- Needs to make use of threads with UI feedback on processing (need to use tasks & `iced::stack`) - Loads of caching - Respect 304 (not modified) ## Pro-Decentralization -Many import/export options (OPML + full database) - +### Import/Export +- Many import/export options (OPML + full database) +- Import items from FreshRSS (stories) +- ### Discovery Service -RSSCAR crawls links from the content to discover new feeds. This adds a crucial discovery feature that can help people actually browse the independent web without worrying about censorship. +RSSCAR will crawl links from the content to discover new feeds. This adds a crucial discovery feature that can help people actually browse the independent web without worrying about censorship. ### Darknet Support @@ -33,12 +36,18 @@ Where possible the podcast namespace should be supported (transcriptions, chapte ## Information Management + ### Full-text search Basically mandatory +- requires [triggers](https://youtu.be/eXMA_2dEMO0) to stay up to date... +- Can help a great deal with performing as a useful search engine + ### Categories Manage feeds and put them in logical groups +## Auto archive? +- Add stories to wayback machine ### Markdown Export -Export stories for Logseq/Obsidian \ No newline at end of file +Export stories for Logseq/Obsidian (how will you get keywords/tags?) \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index 568ce57..e8df9d1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -93,11 +93,11 @@ pub fn initialize() { pub fn add_feed(url: &str) { let conn = Connection::open(get_db_path()).unwrap(); let feed = load_rss(url).unwrap(); - let new_feed = feed.clone(); + let new_feed = feed.clone(); let time = Utc::now().to_rfc2822(); conn.execute( "insert into feeds(title,url,description,last_updated) values(?1,?2,?3,?4)", - [feed.title, url.to_owned(), feed.description, time], + [feed.title, url.to_string(), feed.description, time], ) .unwrap(); let mut stmt = conn.prepare("select feedID from feeds where url=?1").unwrap(); @@ -110,15 +110,15 @@ pub fn add_feed(url: &str) { pub fn store_items(feed: rss::Channel,feed_id: usize) { let conn = Connection::open(get_db_path()).unwrap(); - feed.items.iter().for_each(|i: &rss::Item| { + feed.items.into_iter().for_each(|i: rss::Item| { conn.execute( "insert into items(url,title,description,content,feedID) values(?1,?2,?3,?4,?5)", [ - i.link.clone(), - i.title.clone(), - i.description.clone(), - i.content.clone(), + i.link, + i.title, + i.description, + i.content, Some(feed_id.to_string()) ], ) @@ -158,10 +158,10 @@ pub struct FeedItem { } -pub fn get_feed_items(id: usize) -> Vec{ +pub fn get_feed_items(feed_id: usize) -> Vec{ let conn = get_db(); let mut stmt = conn.prepare("select itemID,title,url,icon,description,content from items where feedID = ?1").unwrap(); - let items:Result> = stmt.query_map([id], |row| { + let items:Result> = stmt.query_map([feed_id], |row| { Ok(FeedItem{ item_id: row.get(0).unwrap(), title: row.get(1).unwrap(), @@ -173,10 +173,27 @@ pub fn get_feed_items(id: usize) -> Vec{ }).unwrap().collect(); match items { Ok(i) => {i}, - Err(_) => {panic!("No items for this feed\nFeedID:{}",id)} + Err(_) => {panic!("No items for this feed\nFeedID:{}",feed_id)} } } +pub fn get_item(item_id: usize) -> FeedItem { + let conn = get_db(); + let mut stmt = conn.prepare("select itemID,title,url,icon,description,content from items where itemID = ?1").unwrap(); + let item:FeedItem = stmt.query_one([item_id],|row| { + Ok( + FeedItem{ + item_id: row.get(0).unwrap(), + title: row.get(1).unwrap(), + url: row.get(2).unwrap(), + icon: row.get(3).unwrap(), + description: row.get(4).unwrap(), + content: row.get(5).unwrap(), + }) + }).unwrap(); + item +} + pub struct Feed { pub feed_id: usize, pub title: String, diff --git a/src/ui.rs b/src/ui.rs index 8858faa..64c874d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,14 +1,18 @@ +use crate::widgets::content_view; + use super::db; use super::widgets; use iced::widget::row; use iced::widget::scrollable; use iced::widget::text_input; +use iced::widget::markdown::Url; use iced::Task; use iced::{ widget::{button, column, container, text}, Element, Length::Fill, }; + pub fn user_interface() -> iced::Result { iced::run(update, view) } @@ -25,6 +29,7 @@ pub enum Page { struct State { page: Page, current_feed: usize, + current_item: usize, feed_input: String } impl Default for State { @@ -32,6 +37,7 @@ impl Default for State { State { page: Page::Home, current_feed: 0, + current_item: 0, feed_input: String::from("") } } @@ -42,7 +48,9 @@ pub enum Message { ChangePage(Page), LoadFeed(usize), AddFeed(String), + LoadItem(usize), FieldUpdated(AppField,String), + LinkClicked(Url), Done(String), ResetDB } @@ -72,10 +80,19 @@ fn update(state: &mut State, mes: Message) -> Task{ Message::AddFeed(f) => { state.feed_input = "".to_string(); Task::perform( - add_feed_background(f), + add_feed_background(f.to_string()), Message::Done ) }, + Message::LinkClicked(l) => { + println!("Link clicked: {}",l); + Task::none() + } + Message::LoadItem(id) => { + state.current_item = id; + state.page = Page::ItemView; + Task::none() + } Message::Done(_) => {Task::none()}, Message::FieldUpdated(field, value) => { match field { @@ -121,7 +138,7 @@ fn home(_state: &State) -> Element<'_, Message> { fn feed_layout(state: &State) -> Element<'_, Message> { container( column!( - button(text("Go Home")).on_press(Message::ChangePage(Page::Home)), + button(text("Home")).on_press(Message::ChangePage(Page::Home)), scrollable(widgets::list_items(state.current_feed)).width(iced::Fill).height(iced::Fill), ) ) @@ -131,7 +148,19 @@ fn feed_layout(state: &State) -> Element<'_, Message> { } fn item_view(state: &State) -> Element<'_, Message> { - todo!() + let item = db::get_item(state.current_item); + container( + column!( + row!( + button(text("Home")).on_press(Message::ChangePage(Page::Home)), + button(text("Feed")).on_press(Message::ChangePage(Page::FeedView)) + ).spacing(10), + content_view(item), + ) + ) + .height(Fill) + .width(Fill) + .into() } fn item_list(state: &State) -> Element<'_,Message>{ diff --git a/src/widgets.rs b/src/widgets.rs index 31bcebf..5703be7 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,6 +1,14 @@ +use crate::db::FeedItem; + use super::db; use super::ui; +use iced::widget::markdown; +use iced::widget::scrollable; +use iced::widget::Container; +use iced::Settings; +use rss_content::Content; use ui::Message; +use rss_content; use iced::{ widget::{button, column, container, text}, Element, @@ -27,11 +35,53 @@ pub fn list_items(feed_id: usize) -> iced::widget::Column<'static,Message> { column( items.iter() .map(|i| { - text(i.title.clone()) + button(text(i.title.clone())).on_press(Message::LoadItem(i.item_id)) }) .map(Element::from), ) .align_x(iced::Alignment::Start) .spacing(5) .padding(15) +} + +pub fn content_area(cnt: String) -> iced::widget::Container<'static,Message>{ + let content = rss_content::parse_content(&cnt); + container( + column( + content.into_iter().map(|c: Content|{ + match c { + Content::Markdown(md) => { + text(md) + }, + Content::Image(_) => {text("Image goes here")}, + Content::Audio(_) => {text("Audio widget here")}, + Content::Video(_) => {text("video player here")}, + _ => {text("")} + } + }).map(Element::from) + ) + ) + +} + +pub fn content_view(item: FeedItem) -> iced::widget::Scrollable<'static, Message> { + + scrollable( + column!( + text(item.title).size(34), + match item.description { + Some(d) => { + content_area(d) + }, + None => {container(text("No description found"))} + }, + match item.content { + Some(c) => { + content_area(c) + } + None => {container(text("No content found"))} + } + + ) + ) } \ No newline at end of file