Items are actually stored and retrieved from the DB 🎉
This commit is contained in:
parent
2534cd2730
commit
03084ac15f
10 changed files with 210 additions and 55 deletions
|
@ -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
44
docs/planned_features.md
Normal 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
6
docs/readme.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Documentation
|
||||
|
||||
This is where guides and details about RSSCAR will be.
|
||||
|
||||
|
||||
-[Planned Features](planned_features.md)
|
26
readme.md
26
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:
|
||||
### Posts:
|
||||
- [Trying out iced](https://gabe.rocks/tech/trying-out-iced/)
|
||||
|
||||
|
|
64
src/db.rs
64
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<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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
78
src/ui.rs
78
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()
|
||||
|
|
35
src/widgets.rs
Normal file
35
src/widgets.rs
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue