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