many improvements, unordered lists work now
This commit is contained in:
parent
d428b253d6
commit
58bc894d39
5 changed files with 127 additions and 21 deletions
|
@ -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","svg",]}
|
iced = {git = "https://github.com/iced-rs/iced", branch = "master", features = ["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"
|
||||||
|
@ -14,11 +14,12 @@ html2md = "0.2"
|
||||||
regex = "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" }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug=true
|
debug=true
|
||||||
incremental = true
|
incremental = true
|
||||||
codegen-units = 256
|
codegen-units = 16
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|
|
@ -7,21 +7,24 @@ I have ambitious plans for the future, there are many features
|
||||||
|
|
||||||
At minimum I need to build a functionally complete RSS reader which will include:
|
At minimum I need to build a functionally complete RSS reader which will include:
|
||||||
- Feed management
|
- Feed management
|
||||||
- Beautiful UI
|
- Beautiful UI (gonna be hard!)
|
||||||
- Multimedia support
|
- Multimedia support
|
||||||
- Export stories to Markdown
|
- Export stories to Markdown
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
- Needs to make use of threads with UI feedback on processing (need to use tasks & `iced::stack`)
|
||||||
- Loads of caching
|
- Loads of caching
|
||||||
- Respect 304 (not modified)
|
- Respect 304 (not modified)
|
||||||
|
|
||||||
## Pro-Decentralization
|
## Pro-Decentralization
|
||||||
|
|
||||||
Many import/export options (OPML + full database)
|
### Import/Export
|
||||||
|
- Many import/export options (OPML + full database)
|
||||||
|
- Import items from FreshRSS (stories)
|
||||||
|
-
|
||||||
### Discovery Service
|
### 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.
|
RSSCAR will crawl 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
|
### Darknet Support
|
||||||
|
@ -33,12 +36,18 @@ Where possible the podcast namespace should be supported (transcriptions, chapte
|
||||||
|
|
||||||
## Information Management
|
## Information Management
|
||||||
|
|
||||||
|
|
||||||
### Full-text search
|
### Full-text search
|
||||||
Basically mandatory
|
Basically mandatory
|
||||||
|
- requires [triggers](https://youtu.be/eXMA_2dEMO0) to stay up to date...
|
||||||
|
- Can help a great deal with performing as a useful search engine
|
||||||
|
|
||||||
|
|
||||||
### Categories
|
### Categories
|
||||||
Manage feeds and put them in logical groups
|
Manage feeds and put them in logical groups
|
||||||
|
|
||||||
|
## Auto archive?
|
||||||
|
- Add stories to wayback machine
|
||||||
|
|
||||||
### Markdown Export
|
### Markdown Export
|
||||||
Export stories for Logseq/Obsidian
|
Export stories for Logseq/Obsidian (how will you get keywords/tags?)
|
37
src/db.rs
37
src/db.rs
|
@ -93,11 +93,11 @@ pub fn initialize() {
|
||||||
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();
|
||||||
let new_feed = feed.clone();
|
let new_feed = feed.clone();
|
||||||
let time = Utc::now().to_rfc2822();
|
let time = Utc::now().to_rfc2822();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"insert into feeds(title,url,description,last_updated) values(?1,?2,?3,?4)",
|
"insert into feeds(title,url,description,last_updated) values(?1,?2,?3,?4)",
|
||||||
[feed.title, url.to_owned(), feed.description, time],
|
[feed.title, url.to_string(), feed.description, time],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut stmt = conn.prepare("select feedID from feeds where url=?1").unwrap();
|
let mut stmt = conn.prepare("select feedID from feeds where url=?1").unwrap();
|
||||||
|
@ -110,15 +110,15 @@ pub fn add_feed(url: &str) {
|
||||||
|
|
||||||
pub fn store_items(feed: rss::Channel,feed_id: usize) {
|
pub fn store_items(feed: rss::Channel,feed_id: usize) {
|
||||||
let conn = Connection::open(get_db_path()).unwrap();
|
let conn = Connection::open(get_db_path()).unwrap();
|
||||||
feed.items.iter().for_each(|i: &rss::Item| {
|
feed.items.into_iter().for_each(|i: rss::Item| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"insert into items(url,title,description,content,feedID)
|
"insert into items(url,title,description,content,feedID)
|
||||||
values(?1,?2,?3,?4,?5)",
|
values(?1,?2,?3,?4,?5)",
|
||||||
[
|
[
|
||||||
i.link.clone(),
|
i.link,
|
||||||
i.title.clone(),
|
i.title,
|
||||||
i.description.clone(),
|
i.description,
|
||||||
i.content.clone(),
|
i.content,
|
||||||
Some(feed_id.to_string())
|
Some(feed_id.to_string())
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -158,10 +158,10 @@ pub struct FeedItem {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_feed_items(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,icon,description,content from items where feedID = ?1").unwrap();
|
let mut stmt = conn.prepare("select itemID,title,url,icon,description,content from items where feedID = ?1").unwrap();
|
||||||
let items:Result<Vec<FeedItem>> = stmt.query_map([id], |row| {
|
let items:Result<Vec<FeedItem>> = stmt.query_map([feed_id], |row| {
|
||||||
Ok(FeedItem{
|
Ok(FeedItem{
|
||||||
item_id: row.get(0).unwrap(),
|
item_id: row.get(0).unwrap(),
|
||||||
title: row.get(1).unwrap(),
|
title: row.get(1).unwrap(),
|
||||||
|
@ -173,10 +173,27 @@ pub fn get_feed_items(id: usize) -> Vec<FeedItem>{
|
||||||
}).unwrap().collect();
|
}).unwrap().collect();
|
||||||
match items {
|
match items {
|
||||||
Ok(i) => {i},
|
Ok(i) => {i},
|
||||||
Err(_) => {panic!("No items for this feed\nFeedID:{}",id)}
|
Err(_) => {panic!("No items for this feed\nFeedID:{}",feed_id)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_item(item_id: usize) -> FeedItem {
|
||||||
|
let conn = get_db();
|
||||||
|
let mut stmt = conn.prepare("select itemID,title,url,icon,description,content from items where itemID = ?1").unwrap();
|
||||||
|
let item:FeedItem = stmt.query_one([item_id],|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();
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Feed {
|
pub struct Feed {
|
||||||
pub feed_id: usize,
|
pub feed_id: usize,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
35
src/ui.rs
35
src/ui.rs
|
@ -1,14 +1,18 @@
|
||||||
|
use crate::widgets::content_view;
|
||||||
|
|
||||||
use super::db;
|
use super::db;
|
||||||
use super::widgets;
|
use super::widgets;
|
||||||
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;
|
||||||
|
use iced::widget::markdown::Url;
|
||||||
use iced::Task;
|
use iced::Task;
|
||||||
use iced::{
|
use iced::{
|
||||||
widget::{button, column, container, text},
|
widget::{button, column, container, text},
|
||||||
Element,
|
Element,
|
||||||
Length::Fill,
|
Length::Fill,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn user_interface() -> iced::Result {
|
pub fn user_interface() -> iced::Result {
|
||||||
iced::run(update, view)
|
iced::run(update, view)
|
||||||
}
|
}
|
||||||
|
@ -25,6 +29,7 @@ pub enum Page {
|
||||||
struct State {
|
struct State {
|
||||||
page: Page,
|
page: Page,
|
||||||
current_feed: usize,
|
current_feed: usize,
|
||||||
|
current_item: usize,
|
||||||
feed_input: String
|
feed_input: String
|
||||||
}
|
}
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
|
@ -32,6 +37,7 @@ impl Default for State {
|
||||||
State {
|
State {
|
||||||
page: Page::Home,
|
page: Page::Home,
|
||||||
current_feed: 0,
|
current_feed: 0,
|
||||||
|
current_item: 0,
|
||||||
feed_input: String::from("")
|
feed_input: String::from("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +48,9 @@ pub enum Message {
|
||||||
ChangePage(Page),
|
ChangePage(Page),
|
||||||
LoadFeed(usize),
|
LoadFeed(usize),
|
||||||
AddFeed(String),
|
AddFeed(String),
|
||||||
|
LoadItem(usize),
|
||||||
FieldUpdated(AppField,String),
|
FieldUpdated(AppField,String),
|
||||||
|
LinkClicked(Url),
|
||||||
Done(String),
|
Done(String),
|
||||||
ResetDB
|
ResetDB
|
||||||
}
|
}
|
||||||
|
@ -72,10 +80,19 @@ fn update(state: &mut State, mes: Message) -> Task<Message>{
|
||||||
Message::AddFeed(f) => {
|
Message::AddFeed(f) => {
|
||||||
state.feed_input = "".to_string();
|
state.feed_input = "".to_string();
|
||||||
Task::perform(
|
Task::perform(
|
||||||
add_feed_background(f),
|
add_feed_background(f.to_string()),
|
||||||
Message::Done
|
Message::Done
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
Message::LinkClicked(l) => {
|
||||||
|
println!("Link clicked: {}",l);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::LoadItem(id) => {
|
||||||
|
state.current_item = id;
|
||||||
|
state.page = Page::ItemView;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::Done(_) => {Task::none()},
|
Message::Done(_) => {Task::none()},
|
||||||
Message::FieldUpdated(field, value) => {
|
Message::FieldUpdated(field, value) => {
|
||||||
match field {
|
match field {
|
||||||
|
@ -121,7 +138,7 @@ fn home(_state: &State) -> Element<'_, Message> {
|
||||||
fn feed_layout(state: &State) -> Element<'_, Message> {
|
fn feed_layout(state: &State) -> Element<'_, Message> {
|
||||||
container(
|
container(
|
||||||
column!(
|
column!(
|
||||||
button(text("Go Home")).on_press(Message::ChangePage(Page::Home)),
|
button(text("Home")).on_press(Message::ChangePage(Page::Home)),
|
||||||
scrollable(widgets::list_items(state.current_feed)).width(iced::Fill).height(iced::Fill),
|
scrollable(widgets::list_items(state.current_feed)).width(iced::Fill).height(iced::Fill),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -131,7 +148,19 @@ fn feed_layout(state: &State) -> Element<'_, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn item_view(state: &State) -> Element<'_, Message> {
|
fn item_view(state: &State) -> Element<'_, Message> {
|
||||||
todo!()
|
let item = db::get_item(state.current_item);
|
||||||
|
container(
|
||||||
|
column!(
|
||||||
|
row!(
|
||||||
|
button(text("Home")).on_press(Message::ChangePage(Page::Home)),
|
||||||
|
button(text("Feed")).on_press(Message::ChangePage(Page::FeedView))
|
||||||
|
).spacing(10),
|
||||||
|
content_view(item),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.height(Fill)
|
||||||
|
.width(Fill)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn item_list(state: &State) -> Element<'_,Message>{
|
fn item_list(state: &State) -> Element<'_,Message>{
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
use crate::db::FeedItem;
|
||||||
|
|
||||||
use super::db;
|
use super::db;
|
||||||
use super::ui;
|
use super::ui;
|
||||||
|
use iced::widget::markdown;
|
||||||
|
use iced::widget::scrollable;
|
||||||
|
use iced::widget::Container;
|
||||||
|
use iced::Settings;
|
||||||
|
use rss_content::Content;
|
||||||
use ui::Message;
|
use ui::Message;
|
||||||
|
use rss_content;
|
||||||
use iced::{
|
use iced::{
|
||||||
widget::{button, column, container, text},
|
widget::{button, column, container, text},
|
||||||
Element,
|
Element,
|
||||||
|
@ -27,11 +35,53 @@ pub fn list_items(feed_id: usize) -> iced::widget::Column<'static,Message> {
|
||||||
column(
|
column(
|
||||||
items.iter()
|
items.iter()
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
text(i.title.clone())
|
button(text(i.title.clone())).on_press(Message::LoadItem(i.item_id))
|
||||||
})
|
})
|
||||||
.map(Element::from),
|
.map(Element::from),
|
||||||
)
|
)
|
||||||
.align_x(iced::Alignment::Start)
|
.align_x(iced::Alignment::Start)
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
.padding(15)
|
.padding(15)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content_area(cnt: String) -> iced::widget::Container<'static,Message>{
|
||||||
|
let content = rss_content::parse_content(&cnt);
|
||||||
|
container(
|
||||||
|
column(
|
||||||
|
content.into_iter().map(|c: Content|{
|
||||||
|
match c {
|
||||||
|
Content::Markdown(md) => {
|
||||||
|
text(md)
|
||||||
|
},
|
||||||
|
Content::Image(_) => {text("Image goes here")},
|
||||||
|
Content::Audio(_) => {text("Audio widget here")},
|
||||||
|
Content::Video(_) => {text("video player here")},
|
||||||
|
_ => {text("")}
|
||||||
|
}
|
||||||
|
}).map(Element::from)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content_view(item: FeedItem) -> iced::widget::Scrollable<'static, Message> {
|
||||||
|
|
||||||
|
scrollable(
|
||||||
|
column!(
|
||||||
|
text(item.title).size(34),
|
||||||
|
match item.description {
|
||||||
|
Some(d) => {
|
||||||
|
content_area(d)
|
||||||
|
},
|
||||||
|
None => {container(text("No description found"))}
|
||||||
|
},
|
||||||
|
match item.content {
|
||||||
|
Some(c) => {
|
||||||
|
content_area(c)
|
||||||
|
}
|
||||||
|
None => {container(text("No content found"))}
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
Loading…
Reference in a new issue