Skip to content

Commit c1eb8bc

Browse files
preferences and thumbnail image embeds
1 parent b5e88f9 commit c1eb8bc

10 files changed

Lines changed: 360 additions & 44 deletions

File tree

sass/includes/basic.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ figcaption {
9696
}
9797
}
9898

99+
form {
100+
label {
101+
display: block;
102+
margin: 0 0 10px;
103+
}
104+
}
105+
99106
footer {
100107
max-width: $page-width;
101108
margin: 1em auto;

src/api/common.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@ pub struct Article {
2020
pub description: String,
2121
pub content_elements: Option<Box<[serde_json::Value]>>,
2222
pub authors: Option<Box<[Topic]>>,
23+
pub thumbnail: Option<Image>,
2324
pub published_time: String,
2425
}
2526

27+
#[derive(Deserialize)]
28+
pub struct Image {
29+
pub caption: Option<String>,
30+
pub width: u16,
31+
pub height: u16,
32+
pub resizer_url: String,
33+
}
34+
2635
#[derive(Deserialize)]
2736
pub struct ApiResponse<T> {
2837
#[serde(rename = "statusCode")]

src/main.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod api;
22
mod client;
33
mod render;
44
mod routes;
5+
mod settings;
56

67
use std::collections::HashMap;
78

@@ -12,7 +13,9 @@ use routes::{
1213
article::render_article,
1314
internet_news::render_legacy_article,
1415
markets::render_market,
16+
proxy::image_proxy,
1517
search::{render_search, render_section, render_topic},
18+
settings::handle_settings,
1619
};
1720

1821
const CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/main.css"));
@@ -24,7 +27,7 @@ macro_rules! document {
2427
html lang="en" {
2528
head {
2629
title { ($title) }
27-
link rel="stylesheet" href="/main.css?v=0";
30+
link rel="stylesheet" href="/main.css?v=1";
2831
meta name="viewport" content="width=device-width, initial-scale=1";
2932
$( ($head) )?
3033
}
@@ -35,13 +38,16 @@ macro_rules! document {
3538
" - "
3639
a href="/search" { "Search" }
3740
" - "
41+
a href="/settings" { "Settings" }
42+
" - "
3843
a href="/about" { "About" } } }
3944
}
4045
}
4146
}
4247
};
4348
}
4449
pub(crate) use document;
50+
use settings::Settings;
4551

4652
pub struct Section {
4753
id: String,
@@ -133,12 +139,15 @@ fn main() {
133139
.as_deref()
134140
.unwrap_or_default()
135141
.iter()
136-
.map(|s| SectionChild { id: s.id.clone(), name: s.name.clone() })
142+
.map(|s| SectionChild {
143+
id: s.id.clone(),
144+
name: s.name.clone(),
145+
})
137146
.collect();
138147
for child in section.children.unwrap_or_default() {
139148
queue.push(child);
140149
}
141-
let node= Section {
150+
let node = Section {
142151
id: section.id.clone(),
143152
name: section.name.clone(),
144153
children,
@@ -151,6 +160,8 @@ fn main() {
151160
println!("Listening on http://{}", list_address);
152161
rouille::start_server(list_address, move |request| {
153162
let path = request.url();
163+
let settings = Settings::from_request(request);
164+
154165
let response = match path.as_str() {
155166
"/" | "/home" | "/world/" => {
156167
let offset = request
@@ -159,10 +170,11 @@ fn main() {
159170
let section = sections_by_id
160171
.get("/world/")
161172
.unwrap_or_else(|| panic!("Section 'world' not found"));
162-
173+
163174
render_section(&client, section, offset, 8)
164175
}
165176
"/about" => render_about(),
177+
"/settings" => return handle_settings(request, &settings),
166178
"/search" | "/search/" => render_search(&client, request),
167179
"/main.css" => {
168180
return rouille::Response {
@@ -213,23 +225,26 @@ fn main() {
213225
render_market(&client, path)
214226
} else if let Some(path) = path.strip_prefix("/markets/companies/") {
215227
render_market(&client, path)
228+
} else if let Some(path) = request.raw_url().strip_prefix("/proxy/") {
229+
return image_proxy(&client, request, path);
216230
} else {
217-
render_article(&client, &path)
231+
render_article(&client, &path, &settings)
218232
}
219233
}
220234
};
221235

222236
match response {
223237
Ok(body) => rouille::Response::html(body),
224-
Err(err) => render_api_error(&err, &path),
238+
Err(err) => render_api_error(&err, &path, &settings),
225239
}
226240
});
227241
}
228242

229-
fn render_api_error(err: &ApiError, path: &str) -> rouille::Response {
243+
fn render_api_error(err: &ApiError, path: &str, settings: &Settings) -> rouille::Response {
230244
let (status, title) = match err {
231-
ApiError::Empty |
232-
ApiError::External(404, _) => (404, "404 - Content not found".to_string()),
245+
ApiError::Empty | ApiError::External(404, _) => {
246+
(404, "404 - Content not found".to_string())
247+
}
233248
ApiError::Redirect(_, _) => (200, format!("Redirect found")),
234249
ApiError::External(code, _) => (*code, format!("{code} - External error")),
235250
ApiError::Internal(message) => (500, format!("500 - Internal server error {message}")),
@@ -241,21 +256,23 @@ fn render_api_error(err: &ApiError, path: &str) -> rouille::Response {
241256
let location = strip_prefix(location);
242257
(
243258
maud::html! {
244-
meta http-equiv="refresh" content=(format!("10; url={}", location));
259+
meta http-equiv="refresh" content=(format!("{}; url={}", settings.redirect_timer, location));
245260
link rel="canonical" href=(location);
246261
},
247262
maud::html! {
248-
p { "Redirecting to " (location) " in 10 seconds. Or click " a href=(location) { "here" } " to follow the link directly." }
249-
}
250-
)},
263+
p { "Redirecting to " (location) " in " (settings.redirect_timer) " seconds. Or click " a href=(location) { "here" } " to follow the link directly." }
264+
},
265+
)
266+
}
251267
ApiError::External(_, message) => (
252268
maud::html!(),
253269
maud::html! {
254270
details {
255271
summary { "Server response" }
256272
p { (message) }
257273
}
258-
}),
274+
},
275+
),
259276
ApiError::Internal(_) => (maud::html!(), maud::html!()),
260277
};
261278

src/render/images.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use crate::{api::common::Image, routes::proxy, settings::Settings};
2+
3+
const RESIZE_STEPS: [u16; 6] = [480, 640, 720, 960, 1080, 1200];
4+
5+
pub fn render_image(thumbnail: &Image, settings: &Settings) -> maud::Markup {
6+
let resizer_url = &thumbnail.resizer_url;
7+
8+
let url = if settings.proxy_images {
9+
if let Some(base_path) = proxy::strip_prefix(&resizer_url) {
10+
format!("/proxy/{base_path}")
11+
} else {
12+
return maud::html! {
13+
p {
14+
i { "Proxy requested but no supported link found!" }
15+
}
16+
};
17+
}
18+
} else {
19+
resizer_url.to_string()
20+
};
21+
let mut srcset = String::new();
22+
for width in RESIZE_STEPS {
23+
if width > thumbnail.width {
24+
break;
25+
}
26+
srcset.push_str(&format!("{url}&width={width}&quality=80 {width}w,"));
27+
}
28+
29+
maud::html! {
30+
figure {
31+
img src=(url)
32+
srcset=(srcset)
33+
width=(thumbnail.width) height=(thumbnail.height)
34+
alt="";
35+
@if let Some(caption) = &thumbnail.caption {
36+
figcaption { i { (caption) } }
37+
}
38+
}
39+
}
40+
}

src/render/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod byline;
2+
pub mod images;
23
pub mod legacy_article_byline;

src/routes/article.rs

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use crate::{
2-
api::{article::fetch_article_by_url, error::ApiResult},
2+
api::{article::fetch_article_by_url, common::Image, error::ApiResult},
33
client::Client,
4-
render::byline,
4+
render::{byline, images::render_image},
5+
settings::Settings,
56
};
67
use chrono::{DateTime, Utc};
78
use maud::{html, PreEscaped};
89

9-
pub fn render_article(client: &Client, path: &str) -> ApiResult<String> {
10+
pub fn render_article(client: &Client, path: &str, settings: &Settings) -> ApiResult<String> {
1011
let article = fetch_article_by_url(client, path)?;
1112

1213
let published_time = article
@@ -29,16 +30,25 @@ pub fn render_article(client: &Client, path: &str) -> ApiResult<String> {
2930
}
3031
@if Some("live-blog") == article.subtype.as_deref() {
3132
p {
32-
"You seem to have accidentally clicked on AI sloppa. There is nothing of value here."
33+
i {
34+
"You seem to have accidentally clicked on AI sloppa. There is nothing of value here."
35+
}
3336
}
3437
p {
35-
"Neuters is currently not planning to support live blogs.
36-
If you want to see the original \"content\", disable any redirector extension and click on this link: "
37-
@let url = format!("https://www.reuters.com{}", path);
38-
a href=(url) { "Original" }
38+
i {
39+
"Neuters is currently not planning to support live blogs.
40+
If you want to see the original \"content\", disable any redirector extension and click on this link: "
41+
@let url = format!("https://www.reuters.com{}", path);
42+
a href=(url) { "Original" }
43+
}
3944
}
4045
} @else {
41-
(render_items(&article.content_elements.unwrap_or_default()))
46+
@if settings.embed_images {
47+
@if let Some(thumbnail) = &article.thumbnail {
48+
(render_image(thumbnail, settings))
49+
}
50+
}
51+
(render_items(&article.content_elements.unwrap_or_default(), settings))
4252
}
4353
),
4454
html! {
@@ -52,7 +62,7 @@ pub fn render_article(client: &Client, path: &str) -> ApiResult<String> {
5262
Ok(doc.into_string())
5363
}
5464

55-
fn render_items(items: &[serde_json::Value]) -> maud::Markup {
65+
fn render_items(items: &[serde_json::Value], settings: &Settings) -> maud::Markup {
5666
html! {
5767
@for content in items {
5868
@match content["type"].as_str() {
@@ -75,24 +85,50 @@ fn render_items(items: &[serde_json::Value]) -> maud::Markup {
7585
}
7686
}
7787
Some("image") => {
78-
@if let Some(image) = content["url"].as_str() {
79-
@let alt = content["alt"].as_str();
80-
@let (width, height) = (content["width"].as_u64(), content["height"].as_u64());
81-
img src=(image) alt=[alt] width=[width] height=[height];
88+
@if settings.embed_images {
89+
@if let Some(image) = content["url"].as_str() {
90+
@let alt = content["alt"].as_str();
91+
@let (width, height) = (content["width"].as_u64(), content["height"].as_u64());
92+
img src=(image) alt=[alt] width=[width] height=[height];
93+
}
94+
} @else {
95+
p {
96+
i {
97+
"Embedding images is disabled. Navigate to the original resource or change the settings to enable it."
98+
}
99+
}
100+
@if let Some(image) = content["url"].as_str() {
101+
p {
102+
a href=(image) { "Image" }
103+
}
104+
}
82105
}
83106
}
84107
Some("graphic") => {
85-
@match content["graphic_type"].as_str() {
86-
Some("image") => {
87-
@if let (Some(image), Some(description)) = (content["url"].as_str(), content["description"].as_str()) {
88-
figure {
89-
img src=(image) alt=(description);
90-
figcaption { (description) }
108+
@if settings.embed_images {
109+
@match content["graphic_type"].as_str() {
110+
Some("image") => {
111+
@if let (Some(image), Some(description)) = (content["url"].as_str(), content["description"].as_str()) {
112+
figure {
113+
img src=(image) alt=(description);
114+
figcaption { (description) }
115+
}
91116
}
92117
}
118+
Some(unknown) => { p { "Unknown graphic type: " (unknown) } }
119+
None => { p { "Missing graphic type" } }
120+
}
121+
} @else {
122+
p {
123+
i {
124+
"Embedding images is disabled. Navigate to the original resource or change the settings to enable it."
125+
}
126+
}
127+
@if let Some(image) = content["url"].as_str() {
128+
p {
129+
a href=(image) { "Image" }
130+
}
93131
}
94-
Some(unknown) => { p { "Unknown graphic type: " (unknown) } }
95-
None => { p { "Missing graphic type" } }
96132
}
97133
}
98134
Some("table") => {
@@ -120,17 +156,25 @@ fn render_items(items: &[serde_json::Value]) -> maud::Markup {
120156
}
121157
Some("list") => {
122158
@if let Some(items) = content["items"].as_array() {
123-
(render_items(items))
159+
(render_items(items, settings))
124160
}
125161
}
126162
Some("social_media") => {
127-
@if let Some(markup) = content["html"].as_str() {
128-
@let embed = if let Some(index) = markup.find("\n<script") {
129-
&markup[..index]
130-
} else {
131-
markup
132-
};
133-
(maud::PreEscaped(embed))
163+
@if settings.embed_embeds {
164+
@if let Some(markup) = content["html"].as_str() {
165+
@let embed = if let Some(index) = markup.find("\n<script") {
166+
&markup[..index]
167+
} else {
168+
markup
169+
};
170+
(maud::PreEscaped(embed))
171+
}
172+
} @else {
173+
p {
174+
i {
175+
"Embedding social media is disabled. Navigate to the original resource or change the settings to enable it."
176+
}
177+
}
134178
}
135179
}
136180
Some(unknown) => { p { "Unknown type: " (unknown) } }

src/routes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ pub mod about;
22
pub mod article;
33
pub mod internet_news;
44
pub mod markets;
5+
pub mod proxy;
56
pub mod search;
7+
pub mod settings;

0 commit comments

Comments
 (0)