repo import
This commit is contained in:
commit
3a499fce12
8 changed files with 390 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target
|
||||||
|
cargo.lock
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "rss-tool"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = {git = "https://github.com/iced-rs/iced", branch = "master", features = ["advanced","image","markdown"]}
|
||||||
|
reqwest = { version = "0.12", features= ["blocking"]}
|
||||||
|
rss = "2.0"
|
||||||
|
web-sys = "0.3.70"
|
||||||
|
mdka = "1.2.10"
|
9
readme.md
Normal file
9
readme.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# RSS Or BUST!
|
||||||
|
|
||||||
|
## Building the perfect rss GUI
|
||||||
|
|
||||||
|
Motivation: learn rust + iced.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
3
src/html.rs
Normal file
3
src/html.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub fn process_html(content: String) -> String {
|
||||||
|
content
|
||||||
|
}
|
105
src/main.rs
Normal file
105
src/main.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
mod net;
|
||||||
|
use iced::{application, widget::{self, button, column, row, text}, Settings, Theme};
|
||||||
|
use net::load_rss;
|
||||||
|
use rss::Channel;
|
||||||
|
mod ui;
|
||||||
|
mod html;
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
iced::application("Really Sweet Stuff",State::update,State::view)
|
||||||
|
.theme(theme)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
|
struct State {
|
||||||
|
scene: Scene,
|
||||||
|
channels: Vec<Channel>,
|
||||||
|
current_channel: usize,
|
||||||
|
current_item: usize,
|
||||||
|
item_open: bool,
|
||||||
|
play_media: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme(state: &State) -> Theme {
|
||||||
|
iced::Theme::Nord
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Scene {
|
||||||
|
Feeds,
|
||||||
|
Items,
|
||||||
|
ItemView,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Scene {
|
||||||
|
fn default() -> Self {
|
||||||
|
Scene::Feeds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
SetScene(Scene),
|
||||||
|
SetChannel(usize),
|
||||||
|
SetItem(usize),
|
||||||
|
OpenItem,
|
||||||
|
ToggleMedia,
|
||||||
|
AddFeed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
let main = net::load_rss("https://libresolutions.network/rss").unwrap();
|
||||||
|
let small = net::load_rss("https://libresolutions.network/about/index.xml").unwrap();
|
||||||
|
let test = net::load_rss("http://localhost:1313/about/index.xml").unwrap();
|
||||||
|
let gabefeed = net::load_rss("https://gabe.rocks/rss").unwrap();
|
||||||
|
let channels = vec![test];
|
||||||
|
Self {
|
||||||
|
scene: Scene::ItemView,
|
||||||
|
channels,
|
||||||
|
current_channel: 0,
|
||||||
|
current_item: 0,
|
||||||
|
item_open: true,
|
||||||
|
play_media: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
|
||||||
|
fn update(&mut self, mes: Message) {
|
||||||
|
match mes {
|
||||||
|
Message::SetScene (scene) => self.scene = scene,
|
||||||
|
Message::SetChannel(c) => {
|
||||||
|
self.current_channel = c;
|
||||||
|
self.scene = Scene::Items;
|
||||||
|
},
|
||||||
|
Message::SetItem(i) => {
|
||||||
|
self.current_item = i;
|
||||||
|
self.scene = Scene::ItemView;
|
||||||
|
},
|
||||||
|
Message::OpenItem => self.item_open = !self.item_open,
|
||||||
|
Message::ToggleMedia => self.play_media = !self.play_media,
|
||||||
|
Message::AddFeed(feed) => {self.channels.push(load_rss(&feed).unwrap())}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> iced::Element<'_, Message> {
|
||||||
|
match self.scene {
|
||||||
|
Scene::Feeds => {
|
||||||
|
ui::channel_view(&self.channels)
|
||||||
|
},
|
||||||
|
Scene::Items => {
|
||||||
|
ui::item_list_view(&self.channels[self.current_channel])
|
||||||
|
},
|
||||||
|
Scene::ItemView => {
|
||||||
|
let item = &self.channels[self.current_channel].items()[self.current_item];
|
||||||
|
ui::item_view(item)
|
||||||
|
}
|
||||||
|
}.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
41
src/net.rs
Normal file
41
src/net.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use core::panic;
|
||||||
|
use std::io::{Bytes, Read};
|
||||||
|
use iced::widget::image::Handle;
|
||||||
|
use rss::{Channel};
|
||||||
|
use reqwest::{self, blocking::Client, header::USER_AGENT};
|
||||||
|
|
||||||
|
pub fn load_rss(url: &str) -> Option<Channel>{
|
||||||
|
let client = Client::new();
|
||||||
|
let res = client.get(url)
|
||||||
|
.header(USER_AGENT,"RSS Reader")
|
||||||
|
.send();
|
||||||
|
match res {
|
||||||
|
Ok(resp) => {
|
||||||
|
match resp.bytes() {
|
||||||
|
Ok(body) => {
|
||||||
|
match Channel::read_from(&*body) {
|
||||||
|
Ok(channel) => {Some(channel)}
|
||||||
|
Err(e) => {panic!("Error parsing feed:\n{}",e);}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
Err(e) => { panic!("Empty response")}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {panic!("Error loading feed.")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_image(url: &str) -> Option<iced::widget::image::Handle>{
|
||||||
|
match reqwest::blocking::get(url) {
|
||||||
|
Ok(r) => {
|
||||||
|
let img: Handle = Handle::from_bytes(r.bytes().unwrap());
|
||||||
|
Some(img)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to download image.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
167
src/ui.rs
Normal file
167
src/ui.rs
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use crate::net::download_image;
|
||||||
|
|
||||||
|
use super::net;
|
||||||
|
use iced::{
|
||||||
|
self,
|
||||||
|
alignment::Vertical,
|
||||||
|
theme::Palette,
|
||||||
|
widget::{
|
||||||
|
button, column, container,
|
||||||
|
image::{self, Handle},
|
||||||
|
row, scrollable, text, Button, Column, Container, Image, Row,
|
||||||
|
},
|
||||||
|
Color, Element,
|
||||||
|
Length::Fill,
|
||||||
|
};
|
||||||
|
use rss::{self, Channel, Item};
|
||||||
|
|
||||||
|
use super::Message;
|
||||||
|
use super::Scene;
|
||||||
|
|
||||||
|
pub fn channel_view(feeds: &Vec<Channel>) -> Container<Message> {
|
||||||
|
container(
|
||||||
|
column(
|
||||||
|
feeds
|
||||||
|
.iter()
|
||||||
|
.map(|c: &Channel| {
|
||||||
|
let title = c.title();
|
||||||
|
let index = feeds.iter().position(|i| i.title() == title).unwrap();
|
||||||
|
channel_preview(c, index)
|
||||||
|
})
|
||||||
|
.map(Element::from),
|
||||||
|
)
|
||||||
|
.align_x(iced::Alignment::Start)
|
||||||
|
.spacing(5)
|
||||||
|
.padding(15),
|
||||||
|
)
|
||||||
|
.height(Fill)
|
||||||
|
.width(Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channel_preview(feed: &rss::Channel, index: usize) -> Button<Message> {
|
||||||
|
let title = feed.title();
|
||||||
|
let desc = feed.description();
|
||||||
|
let fig: Image<Handle> =
|
||||||
|
iced::widget::Image::new(download_image(feed.image().unwrap().url()).unwrap());
|
||||||
|
//image needs to be downloaded...
|
||||||
|
fancy_button(fig, title, desc).on_press(Message::SetChannel(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item_list_view(feed: &Channel) -> Container<Message> {
|
||||||
|
println!("Loading items..\n");
|
||||||
|
let rw = row![button("Feeds").on_press(Message::SetScene(Scene::Feeds))]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Alignment::Start);
|
||||||
|
let item_list = column(
|
||||||
|
feed.items
|
||||||
|
.iter()
|
||||||
|
.map(|i: &Item| {
|
||||||
|
let title = i.title();
|
||||||
|
let index = feed.items.iter().position(|n| n.title() == title).unwrap();
|
||||||
|
item_preview(i, index).width(Fill)
|
||||||
|
})
|
||||||
|
.map(Element::from),
|
||||||
|
)
|
||||||
|
.width(Fill)
|
||||||
|
.align_x(iced::Alignment::Start)
|
||||||
|
.width(Fill)
|
||||||
|
.spacing(5);
|
||||||
|
let scroll = scrollable(item_list).width(iced::Length::Fill).height(Fill);
|
||||||
|
|
||||||
|
container(column![rw, scroll])
|
||||||
|
.align_x(iced::alignment::Horizontal::Center)
|
||||||
|
.align_y(iced::alignment::Vertical::Center)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.padding(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item_preview(item: &rss::Item, index: usize) -> Button<Message> {
|
||||||
|
let title = match item.title() {
|
||||||
|
Some(t) => t,
|
||||||
|
None => "Missing title",
|
||||||
|
};
|
||||||
|
let date = match item.pub_date() {
|
||||||
|
Some(d) => d,
|
||||||
|
None => "Missing Date",
|
||||||
|
};
|
||||||
|
let desc = item.description().unwrap();
|
||||||
|
let fig = iced::widget::image(match get_item_image(item) {
|
||||||
|
Some(img) => net::download_image(img).unwrap(),
|
||||||
|
None => Handle::from("rss.png"),
|
||||||
|
});
|
||||||
|
fancy_button(fig, title, desc).on_press(Message::SetItem((index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item_view(item: &rss::Item) -> Container<Message> {
|
||||||
|
let title = item.title().unwrap();
|
||||||
|
let desc: &str = item.description().unwrap();
|
||||||
|
let date = match item.pub_date() {
|
||||||
|
Some(dt) => dt,
|
||||||
|
None => "",
|
||||||
|
};
|
||||||
|
let content = item.content.clone().unwrap();
|
||||||
|
let desc = item.description().unwrap();
|
||||||
|
let rw = row![
|
||||||
|
button("Feeds").on_press(Message::SetScene(Scene::Feeds)),
|
||||||
|
button("Items").on_press(Message::SetScene(Scene::Items))
|
||||||
|
]
|
||||||
|
.spacing(15)
|
||||||
|
.padding(5)
|
||||||
|
.align_y(iced::Alignment::Start);
|
||||||
|
let list = column![
|
||||||
|
text(title).size(50),
|
||||||
|
text(date).size(25),
|
||||||
|
text(desc).size(35),
|
||||||
|
iced::widget::scrollable(text(super::html::process_html(content)).size(25)),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_x(iced::Alignment::Start);
|
||||||
|
container(column![rw, list].width(Fill))
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fancy_button<'a>(
|
||||||
|
icon: iced::widget::Image<Handle>,
|
||||||
|
title: &'a str,
|
||||||
|
description: &'a str,
|
||||||
|
) -> Button<'a, Message> {
|
||||||
|
let c = container(
|
||||||
|
row![
|
||||||
|
icon.width(120),
|
||||||
|
column![text(title).size(40), text(description).size(25),]
|
||||||
|
.align_x(iced::Alignment::Start)
|
||||||
|
.spacing(5)
|
||||||
|
]
|
||||||
|
.spacing(5)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
)
|
||||||
|
.align_x(iced::alignment::Horizontal::Center);
|
||||||
|
button(c)
|
||||||
|
.padding(5)
|
||||||
|
.width(Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_item_image(item: &rss::Item) -> Option<&str> {
|
||||||
|
// Only bother with itunes:image
|
||||||
|
print!("{} \n", item.title().unwrap());
|
||||||
|
match item.itunes_ext() {
|
||||||
|
Some(e) => match e.image() {
|
||||||
|
Some(img) => {
|
||||||
|
println!("Image found: {}", img);
|
||||||
|
Some(img)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("Itunes extension found, but image was not..");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!("found no extensions");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
src/widgets.rs
Normal file
52
src/widgets.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use advanced::{layout, renderer::{Quad, Style}, Layout, Widget};
|
||||||
|
use advanced::widget::Tree;
|
||||||
|
use iced::*;
|
||||||
|
|
||||||
|
pub struct ItemPreview;
|
||||||
|
|
||||||
|
impl<Message, Renderer> Widget<Message, Theme, Renderer> for ItemPreview
|
||||||
|
where
|
||||||
|
Renderer: iced::advanced::Renderer,
|
||||||
|
{
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size {
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn layout(&self, _tree: &mut Tree, _renderer: &Renderer, _limits: &layout::Limits) -> layout::Node {
|
||||||
|
layout::Node::new([100, 100].into())
|
||||||
|
}
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_state: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
_style: &Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor: mouse::Cursor,
|
||||||
|
_viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
renderer.fill_quad(
|
||||||
|
Quad {
|
||||||
|
bounds: layout.bounds(),
|
||||||
|
border: Border {
|
||||||
|
color: Color::from_rgb(0.6, 0.8, 1.0),
|
||||||
|
width: 1.0,
|
||||||
|
radius: 10.0.into(),
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
},
|
||||||
|
Color::from_rgb(0.0, 0.2, 0.4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a,Message,Renderer> From<ItemPreview> for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced::advanced::Renderer,
|
||||||
|
{
|
||||||
|
fn from(widget: ItemPreview) -> Self{
|
||||||
|
Self::new(widget)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue