very minor improvements. I know the AI generated icon is cringe, but it's a placeholder for now. (Doesn't even work on Wayland anyways)

Successfully migrated to iced::application for more fine-grained control over settings.
Added "all-items" view that breaks things slightly.
This commit is contained in:
Gabriel 2025-12-04 15:20:12 -05:00
parent d877e9e127
commit e94905c9e6
8 changed files with 92 additions and 48 deletions

View file

@ -7,17 +7,13 @@ edition = "2021"
iced = { git = "https://github.com/iced-rs/iced", branch = "master", features = ["image", "markdown", "svg"] } iced = { git = "https://github.com/iced-rs/iced", branch = "master", features = ["image", "markdown", "svg"] }
reqwest = { version = "0.12", features= ["blocking","socks"]} reqwest = { version = "0.12", features= ["blocking","socks"]}
rss = "2.0" rss = "2.0"
web-sys = "0.3.70"
mdka = "1.2.10" #Not Sure if this is useful...
rusqlite = {version=">=0.34",features=['bundled']} rusqlite = {version=">=0.34",features=['bundled']}
html2md = "0.2" scraper = "0.23.1"
regex = "1"
directories = "6.0.0" directories = "6.0.0"
chrono = "0.4.41" chrono = "0.4.41"
rss_content = { git = "https://code.gabe.rocks/gabriel/rss_content", version = "0.1.1" } rss_content = { git = "https://code.gabe.rocks/gabriel/rss_content", version = "0.1.1" }
bytes = "1.10.1"
scraper = "0.23.1"
url = "2.5.4" url = "2.5.4"
#rfd = "0.15.4" (for importing files)
[profile.dev] [profile.dev]
debug=true debug=true

BIN
assets/icon_placeholder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View file

@ -21,7 +21,7 @@ At minimum I need to build a functionally complete RSS reader which will include
### Darknet Support ### Darknet Support
Censorship resistance matters! Censorship resistance matters!
Tor & I2P sites should be first-class citizens and handled accordingly. This is [currently supported](https://code.gabe.rocks/gabriel/rss-tool/commit/240cb19589bc9d42fbae4331562e0f24413c108c) via the system daemons, but in the future will use mechanisms like [arti-client](https://crates.io/crates/arti-client) Tor & I2P sites should be first-class citizens and handled accordingly. This is [currently supported](https://code.gabe.rocks/gabriel/rss-tool/commit/240cb19589bc9d42fbae4331562e0f24413c108c) via the system-wide daemons, but in the future will use mechanisms like [arti-client](https://crates.io/crates/arti-client)
### Information Management ### Information Management

View file

@ -2,6 +2,7 @@ use super::files::*;
use super::net::*; use super::net::*;
use chrono::DateTime; use chrono::DateTime;
use chrono::Utc; use chrono::Utc;
use rusqlite::Row;
//Maybe use a different time? //Maybe use a different time?
use rusqlite::{Connection, Result}; use rusqlite::{Connection, Result};
use std::path::PathBuf; use std::path::PathBuf;
@ -205,6 +206,7 @@ pub fn store_items(feed: rss::Channel, feed_id: usize) {
}; };
}); });
conn.close().unwrap(); conn.close().unwrap();
println!("Finished storing items")
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -219,21 +221,40 @@ pub struct FeedItem {
pub media: Option<String>, //date missing! needed for ordering!!! pub media: Option<String>, //date missing! needed for ordering!!!
} }
fn row2feed_item(row: &Row) -> FeedItem{
FeedItem {
item_id: row.get(0).unwrap(),
title: row.get(1).unwrap_or("NO TITLE!".to_owned()),
url: row.get(2).unwrap(),
date: row.get(3).unwrap(),
media: row.get(4).unwrap(),
description: row.get(5).unwrap(),
content: row.get(6).unwrap(),
icon: None,
}
}
pub fn get_all_items() -> Vec<FeedItem> {
let conn = get_db();
let mut stmt = conn.prepare("select itemID,title,url,date,media,description,content from items").unwrap();
let items: Result<Vec<FeedItem>> = stmt.query_map([],|row| {
Ok(row2feed_item(row))
}).unwrap().collect();
match items {
Ok(i) => i,
Err(_) => {
panic!("No Items found!")
}
}
}
pub fn get_feed_items(feed_id: usize) -> Vec<FeedItem> { pub fn get_feed_items(feed_id: usize) -> Vec<FeedItem> {
let conn = get_db(); let conn = get_db();
let mut stmt = conn.prepare("select itemID,title,url,date,media,description,content from items where feedID = ?1 order by date(date)").unwrap(); let mut stmt = conn.prepare("select itemID,title,url,date,media,description,content from items where feedID = ?1 order by date(date)").unwrap();
let items: Result<Vec<FeedItem>> = stmt let items: Result<Vec<FeedItem>> = stmt
.query_map([feed_id], |row| { .query_map([feed_id], |row| {
Ok(FeedItem { Ok(row2feed_item(row))
item_id: row.get(0).unwrap(),
title: row.get(1).unwrap(),
url: row.get(2).unwrap(),
date: row.get(3).unwrap(),
media: row.get(4).unwrap(),
description: row.get(5).unwrap(),
content: row.get(6).unwrap(),
icon: None,
})
}) })
.unwrap() .unwrap()
.collect(); .collect();

View file

@ -1,23 +0,0 @@
use html2md;
use regex::Regex;
/*
Content needs to be processed in chunks.
Separate out content into blocks that can be used by the UI framework
p -> text
a -> link (does this exist?)
img - > image widget
and so on...
*/
/* this works, but...
- images are omitted
- Blockquotes don't display
*/
pub fn process(content: String) -> String {
let re = Regex::new(r"(?s)<iframe[^>]*>.*?</iframe>").unwrap();
let cleaned = re.replace_all(&content,"");
html2md::parse_html(&cleaned)
}

View file

@ -1,7 +1,6 @@
#[allow(unused)] #[allow(unused)]
pub mod net; pub mod net;
pub mod ui; pub mod ui;
pub mod html;
pub mod db; pub mod db;
pub mod files; pub mod files;
pub mod widgets; pub mod widgets;

View file

@ -1,10 +1,10 @@
use crate::db::FeedItem; use crate::db::FeedItem;
use crate::widgets::content_view; use crate::widgets::content_view;
use crate::widgets::media_view; use crate::widgets::media_view;
use crate::widgets::navbar;
use super::db; use super::db;
use super::widgets; use super::widgets;
use iced::widget::markdown::Url;
use iced::widget::row; use iced::widget::row;
use iced::widget::scrollable; use iced::widget::scrollable;
use iced::widget::text_input; use iced::widget::text_input;
@ -16,9 +16,18 @@ use iced::{
}; };
use rss_content::parse_content; use rss_content::parse_content;
use rss_content::Content; use rss_content::Content;
const ICON: &[u8] = include_bytes!("../assets/icon_placeholder.png");
pub fn user_interface() -> iced::Result { pub fn user_interface() -> iced::Result {
iced::run(update, view) //iced::run(update, view)
let icon = iced::window::icon::from_file_data(ICON, None).ok();
let app = iced::application(State::default,update,view)
.title("RSSCar")
.theme(iced::Theme::Dark)
.window(iced::window::Settings{
icon, ..Default::default()
});
app.run()
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -27,6 +36,7 @@ pub enum Page {
FeedView, FeedView,
AllItems, AllItems,
ItemView, ItemView,
CategoryView,
Testing, Testing,
} }
@ -59,7 +69,7 @@ pub enum Message {
RemoveFeed(usize), RemoveFeed(usize),
LoadItem(usize), LoadItem(usize),
FieldUpdated(AppField, String), FieldUpdated(AppField, String),
LinkClicked(Url), LinkClicked(String),
Done(String), Done(String),
ResetDB, ResetDB,
} }
@ -144,12 +154,14 @@ fn view(state: &State) -> Element<'_, Message> {
Page::FeedView => feed_layout(&state), Page::FeedView => feed_layout(&state),
Page::AllItems => item_list(&state), Page::AllItems => item_list(&state),
Page::ItemView => item_view(&state), Page::ItemView => item_view(&state),
Page::CategoryView => category_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!(
widgets::navbar(state),
scrollable(widgets::list_feeds()) scrollable(widgets::list_feeds())
.width(iced::Fill) .width(iced::Fill)
.height(iced::Fill), .height(iced::Fill),
@ -193,15 +205,35 @@ fn item_view(state: &State) -> Element<'_, Message> {
.into() .into()
} }
fn item_list(_: &State) -> Element<'_, Message> { fn item_list(state: &State) -> Element<'_, Message> {
todo!() container(
column!(
navbar(state),
scrollable(
column(
db::get_all_items().iter().map(|i|{
widgets::list_item(i.item_id, i.title.clone(), i.description.clone().unwrap_or("".to_owned()))
}).map(iced::Element::from)
).align_x(iced::Alignment::Center)).width(Fill).spacing(5)
),
).width(Fill).height(Fill).into()
} }
fn category_view(state: &State) -> Element<'_,Message> {
column!(
navbar(state)
).spacing(5).into()
}
fn testing(state: &State) -> Element<'_, Message> { fn testing(state: &State) -> Element<'_, Message> {
column!( column!(
text("Dev Panel"), text("Dev Panel"),
button("Add gabe.rocks").on_press(Message::AddFeed(String::from("https://gabe.rocks/rss"))), button("Add gabe.rocks").on_press(Message::AddFeed(String::from("https://gabe.rocks/rss"))),
button("Add LSN").on_press(Message::AddFeed(String::from( button("Add LSN").on_press(Message::AddFeed(String::from(
"https://libresolutions.network/rss" "https://libresolutions.network/archive/index.xml"
))), ))),
row!( row!(
text_input("Add a feed", &state.feed_input) text_input("Add a feed", &state.feed_input)

View file

@ -1,7 +1,9 @@
use crate::net; use crate::net;
use crate::ui::Page;
use super::db; use super::db;
use super::ui; use super::ui;
use iced::widget::Text;
use iced::widget::markdown; use iced::widget::markdown;
use iced::widget::row; use iced::widget::row;
use iced::widget::scrollable; use iced::widget::scrollable;
@ -98,6 +100,23 @@ pub fn media_view(state: &'_ ui::State) -> Element<'_, Message> {
} }
} }
pub fn navbar (state: &'_ ui::State) -> Element<'_,Message>{
row!(
button("Home").on_press(Message::ChangePage(Page::Home)),
button("All Items").on_press(Message::ChangePage(Page::AllItems)),
button("Categories").on_press(Message::ChangePage(Page::CategoryView))
).spacing(5).padding(5).width(iced::Fill).into()
}
pub fn list_item(id: usize,title: String, description: String) -> Column<'static,Message>{
Column::new()
.push(button(Text::new(title)).on_press(Message::LoadItem(id)))
.push(text(description))
.align_x(iced::Alignment::Center)
.spacing(5)
}
pub fn content_view(state: &'_ ui::State) -> iced::widget::Scrollable<'_, Message> { pub fn content_view(state: &'_ ui::State) -> iced::widget::Scrollable<'_, Message> {
let item = state.current_item.clone().unwrap(); let item = state.current_item.clone().unwrap();