Compare commits
2 commits
ba0dde7ec8
...
0615cd665f
| Author | SHA1 | Date | |
|---|---|---|---|
| 0615cd665f | |||
| c23c1584ed |
9 changed files with 20227 additions and 26 deletions
|
|
@ -11,12 +11,13 @@ rusqlite = {version=">=0.34",features=['bundled']}
|
|||
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" }
|
||||
url = "2.5.4"
|
||||
opml = "1.1.6"
|
||||
sha1 = "0.10.6"
|
||||
bytes = "1.11.1"
|
||||
ego-tree = "0.10.0"
|
||||
#rfd = "0.15.4" (for importing files)
|
||||
|
||||
[profile.dev]
|
||||
debug=true
|
||||
incremental = true
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ pub fn get_feed_id_by_url(url: &str) -> Option<i64> {
|
|||
}
|
||||
}
|
||||
pub fn add_feed(url: &str) -> Option<i64> {
|
||||
let mut feed: Channel;
|
||||
let feed: Channel;
|
||||
match load_rss(url) {
|
||||
Some(f) => {
|
||||
feed = f;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ pub mod ui;
|
|||
pub mod db;
|
||||
pub mod files;
|
||||
pub mod widgets;
|
||||
mod rss_content;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
db::initialize();
|
||||
|
|
|
|||
28
src/net.rs
28
src/net.rs
|
|
@ -89,28 +89,20 @@ fn get_client(network: Network) -> Result<Client, Error> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_content(url: &str) -> Option<String> {
|
||||
fn fetch(url: &str) -> Option<reqwest::blocking::Response> {
|
||||
let client = get_client(url_network(url)).unwrap();
|
||||
let res = client.get(url).header(USER_AGENT, "RSS Reader").send();
|
||||
match res {
|
||||
Ok(resp) => match resp.text() {
|
||||
Ok(body) => return Some(body),
|
||||
Err(_) => return None,
|
||||
},
|
||||
Err(_) => return None,
|
||||
}
|
||||
client.get(url)
|
||||
.header(USER_AGENT, "RSS Reader")
|
||||
.send()
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn get_content(url: &str) -> Option<String> {
|
||||
fetch(url)?.text().ok()
|
||||
}
|
||||
|
||||
pub fn get_bytes(url: &str) -> Option<bytes::Bytes> {
|
||||
let client = get_client(url_network(url)).unwrap();
|
||||
let res = client.get(url).header(USER_AGENT, "RSS Reader").send();
|
||||
match res {
|
||||
Ok(resp) => match resp.bytes() {
|
||||
Ok(body) => return Some(body),
|
||||
Err(_) => return None,
|
||||
},
|
||||
Err(_) => return None,
|
||||
}
|
||||
fetch(url)?.bytes().ok()
|
||||
}
|
||||
|
||||
pub fn retrieve_opml(url: &str) -> Vec<Url> {
|
||||
|
|
|
|||
350
src/rss_content/mod.rs
Normal file
350
src/rss_content/mod.rs
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
use scraper::{ElementRef, Html, Node};
|
||||
use iced::widget::markdown;
|
||||
|
||||
/*
|
||||
The goal here is to flatten the DOM as much as possible.
|
||||
paragraphs with fancy formatting are turned into markdown, same with
|
||||
*/
|
||||
|
||||
//Supported content
|
||||
#[derive(Debug,Clone)]
|
||||
pub enum Content {
|
||||
Markdown(String),
|
||||
MarkdownParsed(Vec<markdown::Item>),
|
||||
Image(String),
|
||||
Audio(String),
|
||||
Video(String),
|
||||
Ignore
|
||||
}
|
||||
|
||||
pub fn parse_content(c: &str) -> Vec<Content>{
|
||||
process_content(&itemize_content(c)).into_iter().map(|i| {
|
||||
match i {
|
||||
Content::Markdown(s) => {
|
||||
Content::MarkdownParsed(markdown::parse(&s).collect())
|
||||
//this is super lazy but it works....
|
||||
}
|
||||
_ => {i}
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn markdownify_child(item: &Item) -> String {
|
||||
let mut result = "".to_owned();
|
||||
match markdown_content(&item) {
|
||||
Content::Markdown(s) => {
|
||||
result = result + &s;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
result
|
||||
|
||||
}
|
||||
|
||||
fn process_children(children: &Vec<Item>) -> String {
|
||||
let mut result = "".to_owned();
|
||||
for c in children{
|
||||
result = result + &markdownify_child(c);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
fn has_image(children: &Vec<Item>) -> bool {
|
||||
for c in children {
|
||||
match c {
|
||||
Item::Image(_) => {
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn has_video(children: &Vec<Item>) -> bool{
|
||||
for c in children {
|
||||
match c {
|
||||
Item::Video(_) => {
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn markdown_content(item: &Item) -> Content {
|
||||
let mut markdown = String::new();
|
||||
match item {
|
||||
Item::Title(n,children) => {
|
||||
markdown = markdown + &"#".repeat(*n) + " " +&process_children(children);
|
||||
},
|
||||
Item::BoldedText(children) => {
|
||||
markdown = format!("**{}**",process_children(children));
|
||||
},
|
||||
Item::EmphasisText(children) => {
|
||||
markdown = format!("*{}*",process_children(children));
|
||||
}
|
||||
Item::Text(s) => {
|
||||
markdown = markdown + s;
|
||||
},
|
||||
Item::Link(href, children) => {
|
||||
markdown = markdown + &format!("[{}]({})",process_children(children),href);
|
||||
}
|
||||
Item::Paragraph(children) => {
|
||||
markdown = markdown + &process_children(children);
|
||||
}
|
||||
Item::UnorderedList(children) => {
|
||||
markdown = markdown + &process_children(children);
|
||||
}
|
||||
Item::OrderedList(children) => {
|
||||
markdown = markdown + &process_children(children);
|
||||
}
|
||||
Item::ListItem(children) => {
|
||||
markdown = "\n- ".to_owned() + &process_children(children);
|
||||
}
|
||||
Item::Code(children) => {
|
||||
markdown = markdown + &format!("```{}```",&process_children(children));
|
||||
}
|
||||
Item::Blockquote(children) => {
|
||||
markdown = markdown + "> " + &process_children(children);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Content::Markdown(markdown)
|
||||
}
|
||||
|
||||
fn get_media_source(children: &Vec<Item>) -> Option<String> {
|
||||
for c in children {
|
||||
match c {
|
||||
Item::Source(src) => {
|
||||
return Some(src.to_owned());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn media_content(item: &Item) -> Content{
|
||||
match item {
|
||||
Item::Link(_,children) => {
|
||||
for c in children {
|
||||
match c {
|
||||
Item::Video(_) => {return media_content(c);}
|
||||
Item::Audio(_) => {return media_content(c);}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Content::Ignore
|
||||
}
|
||||
Item::Audio(children) => {
|
||||
match get_media_source(children) {
|
||||
Some(s) => {
|
||||
return Content::Audio(s);
|
||||
}
|
||||
None => {
|
||||
return Content::Markdown("<Audio Element with no source>".to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
Item::Video(children) => {
|
||||
match get_media_source(children) {
|
||||
Some(source) => {
|
||||
return Content::Video(source)
|
||||
}
|
||||
None => {
|
||||
return Content::Markdown("<Video element with no source>".to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
Content::Markdown(format!("Incorrectly assigned element:{:#?}",item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn process_content(items: &Vec<Item>) -> Vec<Content> {
|
||||
let mut result: Vec<Content> = Vec::new();
|
||||
//println!("Converting {} items into Content",items.len());
|
||||
for i in items {
|
||||
match i {
|
||||
Item::Title(_,_) => {
|
||||
result.push(markdown_content(i));
|
||||
}
|
||||
Item::Paragraph(_) => {
|
||||
result.push(markdown_content(i));
|
||||
},
|
||||
Item::Link(_,children) => {
|
||||
if has_video(children){
|
||||
result.push(media_content(i));
|
||||
}
|
||||
else if has_image(children){
|
||||
result.push(media_content(i));
|
||||
|
||||
}
|
||||
else {
|
||||
result.push(markdown_content(i))
|
||||
}
|
||||
}
|
||||
Item::UnorderedList(_) => {
|
||||
result.push(markdown_content(i));
|
||||
}
|
||||
Item::OrderedList(_) => {
|
||||
result.push(markdown_content(i));
|
||||
}
|
||||
Item::ListItem(_) => {
|
||||
result.push(markdown_content(i));
|
||||
}
|
||||
Item::Image(src) => {
|
||||
result.push(Content::Image(src.to_owned()));
|
||||
}
|
||||
Item::Video(_) => {
|
||||
result.push(media_content(i));
|
||||
}
|
||||
Item::Audio(_) => {
|
||||
result.push(media_content(i));
|
||||
}
|
||||
Item::Container(children) => {
|
||||
//should actually plan to contain things
|
||||
for c in process_content(children){
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
Item::Code(_) => {
|
||||
result.push(markdown_content(i));
|
||||
}
|
||||
Item::Blockquote(_) => {
|
||||
result.push(markdown_content(i));
|
||||
}
|
||||
_ => {
|
||||
result.push(Content::Ignore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
enum Item {
|
||||
Ignore,
|
||||
Title(usize,Vec<Item>),
|
||||
Text(String),
|
||||
//text, links, formatting are all markdown
|
||||
//arguably, for better control it will be best to turn markdown into its own set of items
|
||||
Image(String),
|
||||
Video(Vec<Item>),
|
||||
Audio(Vec<Item>),
|
||||
Source(String),
|
||||
Blockquote(Vec<Item>),
|
||||
Code(Vec<Item>),
|
||||
BoldedText(Vec<Item>),
|
||||
EmphasisText(Vec<Item>),
|
||||
UnorderedList(Vec<Item>),
|
||||
OrderedList(Vec<Item>),
|
||||
ListItem(Vec<Item>),
|
||||
Paragraph(Vec<Item>),
|
||||
Container(Vec<Item>),
|
||||
Link(String,Vec<Item>),
|
||||
Table(Vec<Item>)
|
||||
}
|
||||
|
||||
|
||||
fn itemize_content(content: &str) -> Vec<Item> {
|
||||
let frag = Html::parse_fragment(content);
|
||||
frag.root_element().children().map(|e|{
|
||||
parse_items(e)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn get_children(el: &ElementRef) -> Vec<Item>{
|
||||
el.children().map(|c|{parse_items(c)}).collect()
|
||||
}
|
||||
|
||||
fn parse_items(n: ego_tree::NodeRef<'_,Node>) -> Item{
|
||||
if n.value().is_text(){
|
||||
return Item::Text((&n.value().as_text().unwrap()).to_string())
|
||||
}
|
||||
if n.value().is_element(){
|
||||
let el = ElementRef::wrap(n).unwrap();
|
||||
let tag_name = el.value().name();
|
||||
match tag_name {
|
||||
"h1" => {return Item::Title(1, get_children(&el))},
|
||||
"h2" => {return Item::Title(2, get_children(&el))},
|
||||
"h3" => {return Item::Title(3, get_children(&el))},
|
||||
"h4" => {return Item::Title(4, get_children(&el))},
|
||||
"h5" => {return Item::Title(5, get_children(&el))},
|
||||
"h6" => {return Item::Title(6, get_children(&el))},
|
||||
"strong" => {return Item::BoldedText(get_children(&el))},
|
||||
"em" => {return Item::EmphasisText(get_children(&el))},
|
||||
"br" => {return Item::Text("\n".to_owned())},
|
||||
"hr" => {return Item::Text("---".to_owned())}
|
||||
"p" => {
|
||||
return Item::Paragraph(get_children(&el))
|
||||
},
|
||||
"a" => {
|
||||
let href = match el.attr("href") {
|
||||
Some(link) => {link}
|
||||
None => {""}
|
||||
};
|
||||
return Item::Link(href.to_owned(),get_children(&el))
|
||||
|
||||
|
||||
}
|
||||
"img" => {
|
||||
match el.attr("src") {
|
||||
Some(src) => {
|
||||
return Item::Image(src.to_owned())
|
||||
},
|
||||
None => {return Item::Ignore}
|
||||
}
|
||||
}
|
||||
"source" => {
|
||||
match el.attr("src") {
|
||||
Some(src) => {
|
||||
return Item::Source(src.to_owned())
|
||||
},
|
||||
None => {return Item::Ignore}
|
||||
}
|
||||
}
|
||||
"video" => {
|
||||
return Item::Video(get_children(&el))
|
||||
}
|
||||
"audio" => {
|
||||
return Item::Audio(get_children(&el))
|
||||
}
|
||||
"ol" => {
|
||||
return Item::OrderedList(get_children(&el))
|
||||
}
|
||||
"ul" => {
|
||||
return Item::UnorderedList(get_children(&el))
|
||||
}
|
||||
"li" => {
|
||||
return Item::ListItem(get_children(&el))
|
||||
}
|
||||
"div" => {
|
||||
return Item::Container(get_children(&el))
|
||||
}
|
||||
"code" => {
|
||||
return Item::Code(get_children(&el))
|
||||
}
|
||||
"blockquote" => {
|
||||
return Item::Blockquote(get_children(&el))
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
Item::Ignore
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
19792
src/rss_content/tests/example_data.rs
Normal file
19792
src/rss_content/tests/example_data.rs
Normal file
File diff suppressed because it is too large
Load diff
67
src/rss_content/tests/mod.rs
Normal file
67
src/rss_content/tests/mod.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
mod example_data;
|
||||
use super::*;
|
||||
use example_data::*;
|
||||
|
||||
fn get_feed(u: &str) -> rss::Channel {
|
||||
rss::Channel::read_from(u.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragments() {
|
||||
for f in FRAGMENTS {
|
||||
println!("Processing Fragment:{}",f);
|
||||
let items = itemize_content(f);
|
||||
let content = process_content(&items);
|
||||
println!("Content:\n{:#?}",content);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_test() {
|
||||
let example_text = Item::Text("Example.com".to_owned());
|
||||
let example_link = Item::Link("https://example.com".to_owned(), [example_text].to_vec());
|
||||
let result = process_content(&[example_link].to_vec());
|
||||
println!("Items to content parse result:\n{:#?}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_display() {
|
||||
let feed = get_feed(example_data::GABE_ROCKS);
|
||||
let content: &Vec<Content> = &process_content(&itemize_content(
|
||||
feed.items.get(4).unwrap().content().unwrap(),
|
||||
));
|
||||
println!("Content: {:#?}", content)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn itemize_feeds() {
|
||||
let _ = FEEDS.map(|u| {
|
||||
let feed = get_feed(u);
|
||||
let results: Vec<_> = feed
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
itemize_content(&item.content.unwrap());
|
||||
})
|
||||
.collect();
|
||||
//let results: Vec<_> = itemize_content(u);
|
||||
println!(
|
||||
"Evaluated feed\nScanned {} items without errors",
|
||||
results.len()
|
||||
)
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn markdownify_feeds() {
|
||||
let _ = FEEDS.map(|u| {
|
||||
let feed = get_feed(u);
|
||||
let results: Vec<_> = feed
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
process_content(&itemize_content(&item.content.unwrap()));
|
||||
})
|
||||
.collect();
|
||||
println!("Processed {} items without errors", results.len())
|
||||
});
|
||||
}
|
||||
|
|
@ -15,9 +15,8 @@ use iced::{
|
|||
Element,
|
||||
Length::Fill,
|
||||
};
|
||||
use rss_content::parse_content;
|
||||
use rss_content::Content;
|
||||
use url::Url;
|
||||
use crate::rss_content::parse_content;
|
||||
use crate::rss_content::Content;
|
||||
const ICON: &[u8] = include_bytes!("../assets/icon_placeholder.png");
|
||||
|
||||
pub fn user_interface() -> iced::Result {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ use iced::{
|
|||
widget::{button, column, container, text},
|
||||
Element,
|
||||
};
|
||||
use rss_content;
|
||||
use rss_content::Content;
|
||||
use crate::rss_content::Content;
|
||||
use ui::Message;
|
||||
|
||||
pub fn list_feeds() -> iced::widget::Column<'static, Message> {
|
||||
|
|
@ -91,7 +90,7 @@ pub fn media_view(state: &'_ ui::State) -> Element<'_, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn navbar(state: &ui::State) -> Element<Message> {
|
||||
pub fn navbar(state: &ui::State) -> Element<'_, Message> {
|
||||
match state.page {
|
||||
Page::Home => row::Row::new()
|
||||
.push(button("Feeds").on_press(Message::ChangePage(Page::Feeds)))
|
||||
|
|
|
|||
Loading…
Reference in a new issue