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"
[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"

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.
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/)

View file

@ -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<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 feed_id: u8,
pub feed_id: usize,
pub title: String,
pub description: 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> {
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<Vec<Feed>> = stmt
.query_map([], |row| {
Ok(Feed {
@ -147,10 +200,9 @@ pub fn get_feeds() -> Vec<Feed> {
}).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 {

View file

@ -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()
}

View file

@ -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();

View file

@ -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<iced::widget::image::Handle>{
let img: Handle = Handle::from_bytes(r.bytes().unwrap());
Some(img)
}
Err(e) => {
Err(_) => {
println!("Failed to download image.");
None
}

View file

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

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