repo import

This commit is contained in:
Gabriel 2024-09-03 22:53:57 -04:00
commit 3a499fce12
8 changed files with 390 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
cargo.lock

11
Cargo.toml Normal file
View 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
View file

@ -0,0 +1,9 @@
# RSS Or BUST!
## Building the perfect rss GUI
Motivation: learn rust + iced.

3
src/html.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn process_html(content: String) -> String {
content
}

105
src/main.rs Normal file
View 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
View 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
View 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
View 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)
}
}