commit 3a499fce1230681181588ca5e0b91f39303ba612 Author: Gabriel Date: Tue Sep 3 22:53:57 2024 -0400 repo import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b5bf85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b773cd7 --- /dev/null +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c51f6f4 --- /dev/null +++ b/readme.md @@ -0,0 +1,9 @@ +# RSS Or BUST! + +## Building the perfect rss GUI + +Motivation: learn rust + iced. + + + + diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..2e25b66 --- /dev/null +++ b/src/html.rs @@ -0,0 +1,3 @@ +pub fn process_html(content: String) -> String { + content +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f013d1d --- /dev/null +++ b/src/main.rs @@ -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, + 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() + } + +} + + diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 0000000..af1f8bd --- /dev/null +++ b/src/net.rs @@ -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{ + 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{ + 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 + } + } +} \ No newline at end of file diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..c550dcf --- /dev/null +++ b/src/ui.rs @@ -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) -> Container { + 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 { + let title = feed.title(); + let desc = feed.description(); + let fig: Image = + 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 { + 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 { + 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 { + 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, + 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 + } + } +} diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 0000000..60b9a22 --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,52 @@ +use advanced::{layout, renderer::{Quad, Style}, Layout, Widget}; +use advanced::widget::Tree; +use iced::*; + +pub struct ItemPreview; + +impl Widget for ItemPreview +where + Renderer: iced::advanced::Renderer, +{ + fn size(&self) -> Size { + 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 for Element<'a, Message, Theme, Renderer> +where + Renderer: iced::advanced::Renderer, + { + fn from(widget: ItemPreview) -> Self{ + Self::new(widget) + } + } \ No newline at end of file