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"] }
reqwest = { version = "0.12", features= ["blocking","socks"]}
rss = "2.0"
web-sys = "0.3.70"
mdka = "1.2.10" #Not Sure if this is useful...
rusqlite = {version=">=0.34",features=['bundled']}
html2md = "0.2"
regex = "1"
scraper = "0.23.1"
directories = "6.0.0"
chrono = "0.4.41"
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"
#rfd = "0.15.4" (for importing files)
[profile.dev]
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
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

View file

@ -2,6 +2,7 @@ use super::files::*;
use super::net::*;
use chrono::DateTime;
use chrono::Utc;
use rusqlite::Row;
//Maybe use a different time?
use rusqlite::{Connection, Result};
use std::path::PathBuf;
@ -205,6 +206,7 @@ pub fn store_items(feed: rss::Channel, feed_id: usize) {
};
});
conn.close().unwrap();
println!("Finished storing items")
}
#[derive(Debug, Clone)]
@ -219,21 +221,40 @@ pub struct FeedItem {
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> {
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 items: Result<Vec<FeedItem>> = stmt
.query_map([feed_id], |row| {
Ok(FeedItem {
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,
})
Ok(row2feed_item(row))
})
.unwrap()
.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)]
pub mod net;
pub mod ui;
pub mod html;
pub mod db;
pub mod files;
pub mod widgets;

View file

@ -1,10 +1,10 @@
use crate::db::FeedItem;
use crate::widgets::content_view;
use crate::widgets::media_view;
use crate::widgets::navbar;
use super::db;
use super::widgets;
use iced::widget::markdown::Url;
use iced::widget::row;
use iced::widget::scrollable;
use iced::widget::text_input;
@ -16,9 +16,18 @@ use iced::{
};
use rss_content::parse_content;
use rss_content::Content;
const ICON: &[u8] = include_bytes!("../assets/icon_placeholder.png");
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)]
@ -27,6 +36,7 @@ pub enum Page {
FeedView,
AllItems,
ItemView,
CategoryView,
Testing,
}
@ -59,7 +69,7 @@ pub enum Message {
RemoveFeed(usize),
LoadItem(usize),
FieldUpdated(AppField, String),
LinkClicked(Url),
LinkClicked(String),
Done(String),
ResetDB,
}
@ -144,12 +154,14 @@ fn view(state: &State) -> Element<'_, Message> {
Page::FeedView => feed_layout(&state),
Page::AllItems => item_list(&state),
Page::ItemView => item_view(&state),
Page::CategoryView => category_view(&state),
Page::Testing => testing(&state),
}
}
fn home(_state: &State) -> Element<'_, Message> {
fn home(state: &State) -> Element<'_, Message> {
container(column!(
widgets::navbar(state),
scrollable(widgets::list_feeds())
.width(iced::Fill)
.height(iced::Fill),
@ -193,15 +205,35 @@ fn item_view(state: &State) -> Element<'_, Message> {
.into()
}
fn item_list(_: &State) -> Element<'_, Message> {
todo!()
fn item_list(state: &State) -> Element<'_, Message> {
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> {
column!(
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"
"https://libresolutions.network/archive/index.xml"
))),
row!(
text_input("Add a feed", &state.feed_input)

View file

@ -1,7 +1,9 @@
use crate::net;
use crate::ui::Page;
use super::db;
use super::ui;
use iced::widget::Text;
use iced::widget::markdown;
use iced::widget::row;
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> {
let item = state.current_item.clone().unwrap();