From 39f08c7218bd4a4af603c26853f3ffcd760eba88 Mon Sep 17 00:00:00 2001 From: mace Date: Sun, 7 Jun 2026 20:18:31 +0200 Subject: [PATCH] added favicon, improve layout --- src/reader/sync.rs | 78 ++++++++++++++++++++++++++++++++++++++-- vue/index.html | 3 +- vue/public/favicon.ico | Bin 4286 -> 15086 bytes vue/public/favicon.svg | 6 ++++ vue/src/assets/main.css | 6 ++-- 5 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 vue/public/favicon.svg diff --git a/src/reader/sync.rs b/src/reader/sync.rs index 8b860b5..1ac0908 100644 --- a/src/reader/sync.rs +++ b/src/reader/sync.rs @@ -38,6 +38,28 @@ fn image_src_is_resolvable(element: &scraper::ElementRef) -> bool { } } +fn escape_html_attr(value: &str) -> String { + value + .replace('&', "&") + .replace('"', """) + .replace('<', "<") + .replace('>', ">") +} + +// Some feeds (e.g. Deutsche Welle) don't embed an in the item content at +// all — they carry the article image as an RSS instead. Build an +// tag from it so those feeds get a preview image too. +fn enclosure_image_html(item: &Item) -> Option { + let enclosure = item.enclosure()?; + if !enclosure.mime_type().to_lowercase().starts_with("image/") { + return None; + } + Some(format!( + r#""#, + escape_html_attr(enclosure.url()) + )) +} + fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) { let item_title = item.title.clone().unwrap(); log::info!("Create feed item: {}", item_title); @@ -48,9 +70,17 @@ fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) { let mut content = "".to_string(); let selector_img = Selector::parse("img").unwrap(); - if let Some(image) = frag.select(&selector_img).find(image_src_is_resolvable) { - content.push_str(&image.html()); - content.push_str("
"); + match frag.select(&selector_img).find(image_src_is_resolvable) { + Some(image) => { + content.push_str(&image.html()); + content.push_str("
"); + } + None => { + if let Some(image_html) = enclosure_image_html(&item) { + content.push_str(&image_html); + content.push_str("
"); + } + } } for node in frag.tree.nodes() { @@ -176,6 +206,48 @@ mod tests { assert!(html.select(&selector).find(image_src_is_resolvable).is_none()); } + #[test] + fn enclosure_image_html_builds_img_for_image_enclosures() { + let mut item = Item::default(); + item.set_enclosure(rss::Enclosure { + url: "https://static.dw.com/image/73880499_302.jpg".to_string(), + length: "2000".to_string(), + mime_type: "image/jpeg".to_string(), + }); + + assert_eq!( + Some(r#""#.to_string()), + enclosure_image_html(&item) + ); + } + + #[test] + fn enclosure_image_html_ignores_non_image_enclosures() { + let mut item = Item::default(); + item.set_enclosure(rss::Enclosure { + url: "https://example.test/episode.mp3".to_string(), + length: "2000".to_string(), + mime_type: "audio/mpeg".to_string(), + }); + + assert_eq!(None, enclosure_image_html(&item)); + } + + #[test] + fn enclosure_image_html_escapes_url_attribute() { + let mut item = Item::default(); + item.set_enclosure(rss::Enclosure { + url: "https://example.test/img.jpg?a=1&b=\"x\"".to_string(), + length: "2000".to_string(), + mime_type: "image/jpeg".to_string(), + }); + + assert_eq!( + Some(r#""#.to_string()), + enclosure_image_html(&item) + ); + } + #[actix_web::test] async fn create_feed_item_does_not_duplicate_existing_items() { let mut connection = establish_connection(); diff --git a/vue/index.html b/vue/index.html index 891c171..d033333 100644 --- a/vue/index.html +++ b/vue/index.html @@ -3,7 +3,8 @@ - + + RSS-Reader diff --git a/vue/public/favicon.ico b/vue/public/favicon.ico index df36fcfb72584e00488330b560ebcf34a41c64c2..0878ef517a2c89cb729adb3864c571ef4b30ea6c 100644 GIT binary patch literal 15086 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 literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S diff --git a/vue/public/favicon.svg b/vue/public/favicon.svg new file mode 100644 index 0000000..d617e7b --- /dev/null +++ b/vue/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/vue/src/assets/main.css b/vue/src/assets/main.css index c423616..3f77094 100644 --- a/vue/src/assets/main.css +++ b/vue/src/assets/main.css @@ -95,7 +95,7 @@ a, .feed-content { font-family: Georgia, 'Times New Roman', Times, serif; font-size: clamp(1rem, 3.5vw, 1.25rem); - padding: 1em; + padding: 0 1em 1em; overflow-wrap: break-word; } @@ -105,11 +105,11 @@ a, } .feed-content p { - padding: 1em; + padding: 0.5em 0; } .feed-content h3 { - padding: 1em; + padding: 0.5em 0; font-size: clamp(1rem, 3vw, 1.3rem); font-weight: bold; }