Items are actually stored and retrieved from the DB 🎉

This commit is contained in:
Gabriel 2025-07-03 20:57:37 -04:00
parent 2534cd2730
commit 03084ac15f
10 changed files with 210 additions and 55 deletions

View file

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [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"]} reqwest = { version = "0.12", features= ["blocking"]}
rss = "2.0" rss = "2.0"
web-sys = "0.3.70" web-sys = "0.3.70"

44
docs/planned_features.md Normal file
View file

@ -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

6
docs/readme.md Normal file
View file

@ -0,0 +1,6 @@
# Documentation
This is where guides and details about RSSCAR will be.
-[Planned Features](planned_features.md)

View file

@ -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. In short: **RSSCAR is to be the RSS reader built for the modern independent web!**
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.
## 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: This Project is FAR from complete ⏳
- Feed management ---
- Beautiful UI *Please be patient!*
- Multimedia support 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: ### Posts:
- [Trying out iced](https://gabe.rocks/tech/trying-out-iced/) - [Trying out iced](https://gabe.rocks/tech/trying-out-iced/)

View file

@ -22,6 +22,9 @@ fn get_db_path() -> PathBuf {
get_data_directory().join(DB_LOCATION) 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' ( const FEEDS_TABLE_CREATE: &str = "CREATE TABLE IF NOT EXISTS 'feeds' (
'feedID' INTEGER NOT NULL, 'feedID' INTEGER NOT NULL,
'title' TEXT 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 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() { pub fn initialize() {
let path = get_db_path(); let path = get_db_path();
println!("Database at {} initialized", path.as_os_str().display()); 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_TABLE_CREATE, []).unwrap();
conn.execute(FEEDS_INDEX_CREATE, []).unwrap(); conn.execute(FEEDS_INDEX_CREATE, []).unwrap();
conn.execute(ITEMS_TABLE_CREATE, []).unwrap(); conn.execute(ITEMS_TABLE_CREATE, []).unwrap();
@ -62,6 +83,7 @@ pub fn initialize() {
println!("Database Initialized.") println!("Database Initialized.")
} }
pub fn add_feed(url: &str) { pub fn add_feed(url: &str) {
let conn = Connection::open(get_db_path()).unwrap(); let conn = Connection::open(get_db_path()).unwrap();
let feed = load_rss(url).unwrap(); let feed = load_rss(url).unwrap();
@ -73,6 +95,7 @@ pub fn add_feed(url: &str) {
) )
.unwrap(); .unwrap();
conn.close().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); 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<String>,
pub description: Option<String>,
pub content: Option<String>
//date missing! needed for ordering!!!
}
pub fn get_feed_items(id: usize) -> Vec<FeedItem>{
let conn = get_db();
let mut stmt = conn.prepare("select itemID,title,url,icon,description,content from items").unwrap();
let items:Result<Vec<FeedItem>> = 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 struct Feed {
pub feed_id: u8, pub feed_id: usize,
pub title: String, pub title: String,
pub description: Option<String>, pub description: Option<String>,
pub icon: Option<String>, pub icon: Option<String>,
@ -131,8 +184,8 @@ fn time_string_conversion(str: String) -> Option<DateTime<Utc>>{
} }
pub fn get_feeds() -> Vec<Feed> { pub fn get_feeds() -> Vec<Feed> {
let conn = Connection::open(get_db_path()).unwrap(); let conn = get_db();
let mut stmt = conn.prepare("select feedID,title,description,icon,url,subscribed,last_updated from feeds").unwrap(); 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<Vec<Feed>> = stmt let rows: Result<Vec<Feed>> = stmt
.query_map([], |row| { .query_map([], |row| {
Ok(Feed { Ok(Feed {
@ -147,10 +200,9 @@ pub fn get_feeds() -> Vec<Feed> {
}).unwrap().collect(); }).unwrap().collect();
match rows { match rows {
Ok(r) => { Ok(r) => {
println!("Feed found\n{}",r[0].title);
r r
} }
Err(e) => {panic!()} Err(e) => {panic!("No idea what causes this")}
} }
} }
struct ReturnedFeedURLs { struct ReturnedFeedURLs {

View file

@ -7,7 +7,7 @@ pub fn get_data_directory() -> std::path::PathBuf {
match fs::create_dir(dirs.data_dir()){ match fs::create_dir(dirs.data_dir()){
Ok(_) => {} Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} 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() dirs.data_dir().to_owned()
} }

View file

@ -1,10 +1,9 @@
mod net; mod net;
use db::initialize;
use net::load_rss;
mod ui; mod ui;
mod html; mod html;
mod db; mod db;
mod files; mod files;
mod widgets;
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
db::initialize(); db::initialize();

View file

@ -1,5 +1,4 @@
use core::panic; use core::panic;
use std::io::{Bytes, Read};
use iced::widget::image::Handle; use iced::widget::image::Handle;
use rss::{Channel}; use rss::{Channel};
use reqwest::{self, blocking::Client, header::USER_AGENT}; use reqwest::{self, blocking::Client, header::USER_AGENT};
@ -54,7 +53,7 @@ pub fn download_image(url: &str) -> Option<iced::widget::image::Handle>{
let img: Handle = Handle::from_bytes(r.bytes().unwrap()); let img: Handle = Handle::from_bytes(r.bytes().unwrap());
Some(img) Some(img)
} }
Err(e) => { Err(_) => {
println!("Failed to download image."); println!("Failed to download image.");
None None
} }

View file

@ -1,5 +1,8 @@
use crate::db::add_feed;
use super::db; use super::db;
use db::Feed; use super::widgets;
use iced::widget::scrollable;
use iced::{ use iced::{
widget::{button, column, container, text}, widget::{button, column, container, text},
Element, Element,
@ -13,27 +16,47 @@ pub fn user_interface() -> iced::Result {
enum Page { enum Page {
home, home,
feed_view, feed_view,
all_items,
item_view, item_view,
testing, testing,
} }
struct State { struct State {
page: Page, page: Page,
current_feed: usize
} }
impl Default for State { impl Default for State {
fn default() -> Self { fn default() -> Self {
State { page: Page::home } State {
page: Page::home,
current_feed: 0
}
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { pub enum Message {
changePage(Page), ChangePage(Page),
LoadFeed(usize),
AddFeed(String),
ResetDB
} }
fn update(state: &mut State, mes: Message) { fn update(state: &mut State, mes: Message) {
match mes { 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> { fn view(state: &State) -> Element<'_, Message> {
match state.page { match state.page {
Page::home => home(&state), Page::home => home(&state),
Page::feed_view => feeds(&state), Page::feed_view => feed_layout(&state),
Page::item_view => item(&state), Page::all_items => item_list(&state),
Page::item_view => item_view(&state),
Page::testing => testing(&state), Page::testing => testing(&state),
} }
} }
fn home(state: &State) -> Element<'_, Message> { fn home(state: &State) -> Element<'_, Message> {
container(column!( container(column!(
list_feeds(), scrollable(widgets::list_feeds()).width(iced::Fill).height(iced::Fill),
button("Go to test!").on_press(Message::changePage(Page::testing)) button("Go to test!").on_press(Message::ChangePage(Page::testing))
)) ))
.padding(15) .padding(15)
.height(Fill) .height(Fill)
@ -60,37 +84,33 @@ fn home(state: &State) -> Element<'_, Message> {
.into() .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> { fn feed_layout(state: &State) -> Element<'_, Message> {
container(list_feeds().padding(15)) 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) .height(Fill)
.width(Fill) .width(Fill)
.into() .into()
} }
fn item(state: &State) -> Element<'_, Message> { fn item_view(state: &State) -> Element<'_, Message> {
todo!() todo!()
} }
fn item_list(state: &State) -> Element<'_,Message>{
todo!()
}
fn testing(state: &State) -> Element<'_, Message> { fn testing(state: &State) -> Element<'_, Message> {
column!( column!(
iced::widget::text("Ayy lmao"), text("Dev Panel"),
button("go back!").on_press(Message::changePage(Page::home)) 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) .spacing(5)
.into() .into()

35
src/widgets.rs Normal file
View file

@ -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::FeedItem> = 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)
}