diff --git a/Cargo.toml b/Cargo.toml index ee8a700..9cd2430 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","tokio"]} +iced = {git = "https://github.com/iced-rs/iced", branch = "master", features = ["advanced","image","markdown","svg",]} reqwest = { version = "0.12", features= ["blocking"]} rss = "2.0" web-sys = "0.3.70" diff --git a/docs/planned_features.md b/docs/planned_features.md new file mode 100644 index 0000000..9f6675d --- /dev/null +++ b/docs/planned_features.md @@ -0,0 +1,44 @@ + +# Future Plans + +I have ambitious plans for the future, there are many features + +## Building a great RSS Desktop Client + +At minimum I need to build a functionally complete RSS reader which will include: +- Feed management +- Beautiful UI +- Multimedia support +- Export stories to Markdown + +### Performance +- Loads of caching +- Respect 304 (not modified) + +## Pro-Decentralization + +Many import/export options (OPML + full database) + +### 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. + + +### Darknet Support +Censorship resistance matters! Tor & I2P sites should be first-class citizens and handled accordingly. + +### Podcasting 2.0 +Where possible the podcast namespace should be supported (transcriptions, chapters, etc) + + +## Information Management + +### Full-text search +Basically mandatory + +### Categories +Manage feeds and put them in logical groups + + +### Markdown Export +Export stories for Logseq/Obsidian \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..0885e04 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,6 @@ +# Documentation + +This is where guides and details about RSSCAR will be. + + +-[Planned Features](planned_features.md) \ No newline at end of file diff --git a/readme.md b/readme.md index 9b6045c..21799e8 100644 --- a/readme.md +++ b/readme.md @@ -1,21 +1,21 @@ -# Really Sweet Stuff +RSSCAR +====== +### *Really Simple Syndication Crawler-Aided Reader* -*Really Sweet Stuff* is my temporary codename for the ambitious new project. Learning [Rust](https://www.rust-lang.org/) & [iced](https://iced.rs) by building a great RSS desktop client. +RSSCAR is a new kind of RSS reader. [RSS is fantastic](https://gabe.rocks/tech/rss-love) but a huge problem is that it lacks *discovery*. With relatively simple client-side crawling, it is trivial to find additional publications related to your own interests. -It's got a long way to go, but I'm optimistic. -For now it uses hardcoded URLs while I work on the UI. -I don't consider this even usable yet, so please consider looking elsewhere until I have more features built. +In short: **RSSCAR is to be the RSS reader built for the modern independent web!** -## Building a great RSS Desktop Client +This project began as my own way to learn [Rust](https://www.rust-lang.org/) & [iced](https://iced.rs) by building a great RSS desktop client. -At minimum I need to build a functionally complete RSS reader which will include: -- Feed management -- Beautiful UI -- Multimedia support +This Project is FAR from complete ⏳ +--- +*Please be patient!* +It's got a long way to go, but progress is ongoing. -But! I have much more ambitious goals for this project once I start building momentum. -The best way to stay up to date on this would be to follow my personal RSS feed at [gabe.rocks/rss](https://gabe.rocks/rss) +Currently The best way to stay up to date on this would be to follow my [tech only feed](https://gabe.rocks/tech/index.xml) + +### Posts: +- [Trying out iced](https://gabe.rocks/tech/trying-out-iced/) -## Posts: -- [Trying out iced](https://gabe.rocks/tech/trying-out-iced/) \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index acca8d3..114025e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -22,6 +22,9 @@ fn get_db_path() -> PathBuf { get_data_directory().join(DB_LOCATION) } +fn get_db() -> Connection{ + Connection::open(get_db_path()).unwrap() +} const FEEDS_TABLE_CREATE: &str = "CREATE TABLE IF NOT EXISTS 'feeds' ( 'feedID' INTEGER NOT NULL, 'title' TEXT NOT NULL, @@ -50,10 +53,28 @@ const ITEMS_TABLE_CREATE: &str = "CREATE TABLE IF NOT EXISTS 'items' ( );"; const ITEMS_INDEX_CREATE: &str = "CREATE INDEX IF NOT EXISTS 'items_idx' on 'items'('itemID' ASC);"; +const DB_RESET: &str = " + drop table feeds; + drop table items; +"; + +pub fn reset(){ + println!("⚠️WARNING⚠️\nResetting Database"); + let conn = get_db(); + match conn.execute_batch(DB_RESET) { + Ok(_) => {println!("Database successfully wiped.")} + Err(e) => {panic!("Error erasing database.\nError: {0}",e)} + } + conn.close().unwrap(); + initialize(); + +} + + pub fn initialize() { let path = get_db_path(); println!("Database at {} initialized", path.as_os_str().display()); - let conn = Connection::open(path).unwrap(); + let conn = get_db(); conn.execute(FEEDS_TABLE_CREATE, []).unwrap(); conn.execute(FEEDS_INDEX_CREATE, []).unwrap(); conn.execute(ITEMS_TABLE_CREATE, []).unwrap(); @@ -62,6 +83,7 @@ pub fn initialize() { println!("Database Initialized.") } + pub fn add_feed(url: &str) { let conn = Connection::open(get_db_path()).unwrap(); let feed = load_rss(url).unwrap(); @@ -73,6 +95,7 @@ pub fn add_feed(url: &str) { ) .unwrap(); conn.close().unwrap(); + //need to get the feed_id from the DB and then make sure items are mapped to feed store_items(new_feed); } @@ -113,8 +136,38 @@ pub fn return_item() -> String { } } +pub struct FeedItem { + pub item_id: usize, + pub title: String, + pub url: String, + pub icon: Option, + pub description: Option, + pub content: Option + //date missing! needed for ordering!!! + +} + +pub fn get_feed_items(id: usize) -> Vec{ + let conn = get_db(); + let mut stmt = conn.prepare("select itemID,title,url,icon,description,content from items").unwrap(); + let items:Result> = stmt.query_map([], |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().collect(); + match items { + Ok(i) => {i}, + Err(_) => {panic!("No items for this feed\nFeedID:{}",id)} + } +} + pub struct Feed { - pub feed_id: u8, + pub feed_id: usize, pub title: String, pub description: Option, pub icon: Option, @@ -131,8 +184,8 @@ fn time_string_conversion(str: String) -> Option>{ } pub fn get_feeds() -> Vec { - let conn = Connection::open(get_db_path()).unwrap(); - let mut stmt = conn.prepare("select feedID,title,description,icon,url,subscribed,last_updated from feeds").unwrap(); + let conn = get_db(); + let mut stmt = conn.prepare("select feedID,title,description,icon,url,subscribed,last_updated from feeds order by last_updated desc").unwrap(); let rows: Result> = stmt .query_map([], |row| { Ok(Feed { @@ -147,10 +200,9 @@ pub fn get_feeds() -> Vec { }).unwrap().collect(); match rows { Ok(r) => { - println!("Feed found\n{}",r[0].title); r } - Err(e) => {panic!()} + Err(e) => {panic!("No idea what causes this")} } } struct ReturnedFeedURLs { diff --git a/src/files.rs b/src/files.rs index ff69196..2861eff 100644 --- a/src/files.rs +++ b/src/files.rs @@ -7,7 +7,7 @@ pub fn get_data_directory() -> std::path::PathBuf { match fs::create_dir(dirs.data_dir()){ Ok(_) => {} Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} - Err(e) => {println!("Unable to create data directory")} + Err(_) => {println!("Unable to create data directory")} }; dirs.data_dir().to_owned() } diff --git a/src/main.rs b/src/main.rs index 5e34dee..353f8b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,9 @@ mod net; -use db::initialize; -use net::load_rss; mod ui; mod html; mod db; mod files; +mod widgets; pub fn main() -> iced::Result { db::initialize(); diff --git a/src/net.rs b/src/net.rs index fa20a1f..41cb2e0 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,5 +1,4 @@ use core::panic; -use std::io::{Bytes, Read}; use iced::widget::image::Handle; use rss::{Channel}; use reqwest::{self, blocking::Client, header::USER_AGENT}; @@ -54,7 +53,7 @@ pub fn download_image(url: &str) -> Option{ let img: Handle = Handle::from_bytes(r.bytes().unwrap()); Some(img) } - Err(e) => { + Err(_) => { println!("Failed to download image."); None } diff --git a/src/ui.rs b/src/ui.rs index f6f88b4..d0217b4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,8 @@ +use crate::db::add_feed; + use super::db; -use db::Feed; +use super::widgets; +use iced::widget::scrollable; use iced::{ widget::{button, column, container, text}, Element, @@ -13,27 +16,47 @@ pub fn user_interface() -> iced::Result { enum Page { home, feed_view, + all_items, item_view, testing, } struct State { page: Page, + current_feed: usize } impl Default for State { fn default() -> Self { - State { page: Page::home } + State { + page: Page::home, + current_feed: 0 + } } } #[derive(Debug, Clone)] -enum Message { - changePage(Page), +pub enum Message { + ChangePage(Page), + LoadFeed(usize), + AddFeed(String), + ResetDB } fn update(state: &mut State, mes: Message) { match mes { - Message::changePage(p) => state.page=p + Message::ChangePage(p) => state.page=p, + Message::LoadFeed(feed_id) => { + state.current_feed = feed_id; + state.page=Page::feed_view; + + }, + Message::AddFeed(f) => { + db::add_feed(&f); + }, + Message::ResetDB => { + db::reset(); + } + } @@ -43,16 +66,17 @@ fn update(state: &mut State, mes: Message) { fn view(state: &State) -> Element<'_, Message> { match state.page { Page::home => home(&state), - Page::feed_view => feeds(&state), - Page::item_view => item(&state), + Page::feed_view => feed_layout(&state), + Page::all_items => item_list(&state), + Page::item_view => item_view(&state), Page::testing => testing(&state), } } fn home(state: &State) -> Element<'_, Message> { container(column!( - list_feeds(), - button("Go to test!").on_press(Message::changePage(Page::testing)) + scrollable(widgets::list_feeds()).width(iced::Fill).height(iced::Fill), + button("Go to test!").on_press(Message::ChangePage(Page::testing)) )) .padding(15) .height(Fill) @@ -60,37 +84,33 @@ fn home(state: &State) -> Element<'_, Message> { .into() } -fn list_feeds() -> iced::widget::Column<'static, Message> { - let feeds = db::get_feeds(); - column( - feeds - .iter() - .map(|f| { - let title = f.title.clone(); - let index = f.feedID; - text(title) - }) - .map(Element::from), - ) - .align_x(iced::Alignment::Start) - .spacing(5) -} -fn feeds(state: &State) -> Element<'_, Message> { - container(list_feeds().padding(15)) +fn feed_layout(state: &State) -> Element<'_, Message> { + container( + column!( + button(text("Go Home")).on_press(Message::ChangePage(Page::home)), + scrollable(widgets::list_items(state.current_feed)).width(iced::Fill).height(iced::Fill), + ) + ) .height(Fill) .width(Fill) .into() } -fn item(state: &State) -> Element<'_, Message> { +fn item_view(state: &State) -> Element<'_, Message> { todo!() } +fn item_list(state: &State) -> Element<'_,Message>{ + todo!() +} fn testing(state: &State) -> Element<'_, Message> { column!( - iced::widget::text("Ayy lmao"), - button("go back!").on_press(Message::changePage(Page::home)) + text("Dev Panel"), + button("Add gabe.rocks").on_press(Message::AddFeed(String::from("https://gabe.rocks/rss"))), + button("Add LSN").on_press(Message::AddFeed(String::from("https://libresolutions.network/rss"))), + button("Wipe DB").on_press(Message::ResetDB), + button("go back!").on_press(Message::ChangePage(Page::home)) ) .spacing(5) .into() diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 0000000..467f9e8 --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,35 @@ +use super::db; +use super::ui; +use ui::Message; +use iced::{ + widget::{button, column, container, text}, + Element, + Length::Fill, +}; + +pub fn list_feeds() -> iced::widget::Column<'static, Message> { + let feeds = db::get_feeds(); + column( + feeds + .iter() + .map(|f| { + button(text(f.title.clone())).on_press(Message::LoadFeed(f.feed_id)) + }) + .map(Element::from), + ) + .align_x(iced::Alignment::Start) + .spacing(5) +} + +pub fn list_items(feed_id: usize) -> iced::widget::Column<'static,Message> { + let items: Vec = db::get_feed_items(feed_id); + column( + items.iter() + .map(|i| { + text(i.title.clone()) + }) + .map(Element::from), + ) + .align_x(iced::Alignment::Start) + .spacing(5) +} \ No newline at end of file