Skip to content

Commit 6a3c2fa

Browse files
committed
Add support for adding new article URLs from the reader
1 parent f7cf21d commit 6a3c2fa

5 files changed

Lines changed: 273 additions & 13 deletions

File tree

crates/core/src/articles/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ pub const ARTICLES_DIR: &str = ".articles";
2525
#[derive(Serialize, Deserialize)]
2626
pub struct ArticleIndex {
2727
pub articles: BTreeMap<String, Article>,
28+
#[serde(skip_serializing_if = "Vec::is_empty")]
29+
#[serde(default)]
30+
pub queued: Vec<String>,
2831
}
2932

3033
impl Default for ArticleIndex {
3134
fn default() -> Self {
3235
ArticleIndex {
3336
articles: BTreeMap::new(),
37+
queued: Vec::new(),
3438
}
3539
}
3640
}
@@ -131,3 +135,8 @@ pub fn load(auth: settings::ArticleAuth) -> Box<dyn Service> {
131135
_ => Box::new(dummy::Dummy::new()),
132136
}
133137
}
138+
139+
pub fn queue_link(service: &mut Box<dyn Service>, link: String) {
140+
service.index().lock().unwrap().queued.push(link);
141+
service.save_index();
142+
}

crates/core/src/articles/wallabag.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,36 @@ fn update(hub: &Hub, auth: ArticleAuth, index: Arc<Mutex<ArticleIndex>>) -> Resu
526526
hub.send(Event::ArticlesAuth(Ok(auth.clone()))).ok();
527527
}
528528

529+
// Submit new URLs.
530+
let queued = index.lock().unwrap().queued.clone();
531+
if !queued.is_empty() {
532+
// Send the list of URLs via a GET parameter, because for some reason
533+
// the Wallabag server only accepts those (and not a form in the POST
534+
// request).
535+
// See: https://github.com/wallabag/wallabag/issues/8353
536+
match agent
537+
.post(format!("{url}api/entries/lists"))
538+
.query("urls", serde_json::to_string(&queued).unwrap())
539+
.header("Authorization", "Bearer ".to_owned() + &auth.access_token)
540+
.send_empty()
541+
{
542+
Ok(_) => {
543+
let index = &mut index.lock().unwrap();
544+
if index.queued.len() > queued.len() {
545+
// New URLs were added in the meantime, so keep those.
546+
// (Unlikely, but possible).
547+
index.queued = index.queued.split_off(queued.len());
548+
} else {
549+
// No new URLs were added, so we can clear the queue.
550+
index.queued.clear();
551+
}
552+
}
553+
Err(err) => {
554+
return Err(Error::other(format!("submitting article failed: {err}")));
555+
}
556+
};
557+
}
558+
529559
// Sync local changes.
530560
let mut changes: BTreeMap<String, Vec<(&str, &str)>> = BTreeMap::new();
531561
let mut deleted: BTreeSet<String> = BTreeSet::new();

crates/core/src/view/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ pub enum Event {
357357
Authenticate,
358358
ArticlesAuth(Result<ArticleAuth, String>),
359359
ArticleUpdateProgress(ArticleUpdateProgress),
360+
QueueLink(String),
361+
AddArticleLink(String),
360362
CheckBattery,
361363
SetWifi(bool),
362364
MightSuspend,
@@ -416,6 +418,7 @@ pub enum ViewId {
416418
ArticleInputServer,
417419
ArticleInputUsername,
418420
ArticleInputPassword,
421+
ExternalLink,
419422
PageMenu,
420423
PresetMenu,
421424
MarginCropperMenu,
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use crate::color::{BLACK, WHITE};
2+
use crate::context::Context;
3+
use crate::device::CURRENT_DEVICE;
4+
use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
5+
use crate::framebuffer::Framebuffer;
6+
use crate::geom::{BorderSpec, CornerSpec, Rectangle};
7+
use crate::gesture::GestureEvent;
8+
use crate::unit::scale_by_dpi;
9+
use crate::view::button::Button;
10+
use crate::view::icon::Icon;
11+
use crate::view::label::Label;
12+
use crate::view::{Align, Bus, Event, Hub, Id, RenderQueue, View, ViewId, ID_FEEDER};
13+
use crate::view::{BORDER_RADIUS_MEDIUM, SMALL_BAR_HEIGHT, THICKNESS_LARGE};
14+
15+
const LABEL_QUEUE: &str = "Queue";
16+
const LABEL_ADD_ARTICLE: &str = "Add article";
17+
18+
pub struct ExternalLink {
19+
id: Id,
20+
rect: Rectangle,
21+
children: Vec<Box<dyn View>>,
22+
}
23+
24+
impl ExternalLink {
25+
pub fn new(context: &mut Context, link: String) -> ExternalLink {
26+
let id = ID_FEEDER.next();
27+
let fonts = &mut context.fonts;
28+
let mut children = Vec::new();
29+
let dpi = CURRENT_DEVICE.dpi;
30+
let (width, height) = context.display.dims;
31+
let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
32+
let thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as i32;
33+
let border_radius = scale_by_dpi(BORDER_RADIUS_MEDIUM, dpi) as i32;
34+
35+
let (x_height, padding) = {
36+
let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
37+
(font.x_heights.0 as i32, font.em() as i32)
38+
};
39+
40+
let window_width = width as i32 - 2 * padding;
41+
let window_height = small_height * 4 + 2 * padding;
42+
43+
let dx = (width as i32 - window_width) / 2;
44+
let dy = (height as i32 - window_height) / 4;
45+
46+
let rect = rect![dx, dy, dx + window_width, dy + window_height];
47+
48+
let close_icon = Icon::new(
49+
"close",
50+
rect![
51+
rect.max.x - small_height,
52+
rect.min.y + thickness,
53+
rect.max.x - thickness,
54+
rect.min.y + small_height
55+
],
56+
Event::Close(ViewId::ExternalLink),
57+
)
58+
.corners(Some(CornerSpec::Detailed {
59+
north_west: 0,
60+
north_east: border_radius - thickness,
61+
south_east: 0,
62+
south_west: 0,
63+
}));
64+
children.push(Box::new(close_icon) as Box<dyn View>);
65+
66+
let label = Label::new(
67+
rect![
68+
rect.min.x + small_height,
69+
rect.min.y + thickness,
70+
rect.max.x - small_height,
71+
rect.min.y + small_height
72+
],
73+
"External link".to_string(),
74+
Align::Center,
75+
);
76+
children.push(Box::new(label) as Box<dyn View>);
77+
78+
// TODO: wrap the URL if needed.
79+
let link_label = Label::new(
80+
rect![
81+
rect.min.x + small_height,
82+
rect.min.y + thickness + small_height,
83+
rect.max.x - small_height,
84+
rect.min.y + 2 * small_height
85+
],
86+
link.clone(),
87+
Align::Left(0),
88+
);
89+
children.push(Box::new(link_label) as Box<dyn View>);
90+
91+
let max_button_label_width = {
92+
let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
93+
[LABEL_QUEUE, LABEL_ADD_ARTICLE]
94+
.iter()
95+
.map(|t| font.plan(t, None, None).width)
96+
.max()
97+
.unwrap() as i32
98+
};
99+
100+
let button_y = rect.min.y + small_height * 3;
101+
let button_height = 4 * x_height;
102+
103+
let button_queue = Button::new(
104+
rect![
105+
rect.min.x + 3 * padding,
106+
button_y + small_height - button_height,
107+
rect.min.x + 5 * padding + max_button_label_width,
108+
button_y + small_height
109+
],
110+
Event::QueueLink(link.clone()),
111+
LABEL_QUEUE.to_string(),
112+
)
113+
.disabled(context.settings.external_urls_queue.is_none());
114+
children.push(Box::new(button_queue) as Box<dyn View>);
115+
116+
let button_add_article = Button::new(
117+
rect![
118+
rect.max.x - 5 * padding - max_button_label_width,
119+
button_y + small_height - button_height,
120+
rect.max.x - 3 * padding,
121+
button_y + small_height
122+
],
123+
Event::AddArticleLink(link),
124+
LABEL_ADD_ARTICLE.to_string(),
125+
)
126+
.disabled(context.settings.article_auth.api == "");
127+
children.push(Box::new(button_add_article) as Box<dyn View>);
128+
129+
ExternalLink { id, rect, children }
130+
}
131+
}
132+
133+
impl View for ExternalLink {
134+
fn handle_event(
135+
&mut self,
136+
evt: &Event,
137+
_hub: &Hub,
138+
bus: &mut Bus,
139+
_rq: &mut RenderQueue,
140+
_context: &mut Context,
141+
) -> bool {
142+
match *evt {
143+
Event::Gesture(GestureEvent::Tap(center)) if !self.rect.includes(center) => {
144+
bus.push_back(Event::Close(ViewId::ExternalLink));
145+
true
146+
}
147+
Event::Gesture(..) => true,
148+
_ => false,
149+
}
150+
}
151+
152+
fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {
153+
let dpi = CURRENT_DEVICE.dpi;
154+
155+
let border_radius = scale_by_dpi(BORDER_RADIUS_MEDIUM, dpi) as i32;
156+
let border_thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as u16;
157+
158+
fb.draw_rounded_rectangle_with_border(
159+
&self.rect,
160+
&CornerSpec::Uniform(border_radius),
161+
&BorderSpec {
162+
thickness: border_thickness,
163+
color: BLACK,
164+
},
165+
&WHITE,
166+
);
167+
}
168+
169+
fn is_background(&self) -> bool {
170+
true
171+
}
172+
173+
fn rect(&self) -> &Rectangle {
174+
&self.rect
175+
}
176+
177+
fn rect_mut(&mut self) -> &mut Rectangle {
178+
&mut self.rect
179+
}
180+
181+
fn children(&self) -> &Vec<Box<dyn View>> {
182+
&self.children
183+
}
184+
185+
fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
186+
&mut self.children
187+
}
188+
189+
fn id(&self) -> Id {
190+
self.id
191+
}
192+
}

crates/core/src/view/reader/mod.rs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod results_bar;
44
mod margin_cropper;
55
mod chapter_label;
66
mod results_label;
7+
mod external_link;
78

89
use std::thread;
910
use std::sync::{Arc, Mutex};
@@ -19,8 +20,10 @@ use regex::Regex;
1920
use septem::prelude::*;
2021
use septem::{Roman, Digit};
2122
use rand_core::RngCore;
23+
use crate::articles;
2224
use crate::input::{DeviceEvent, FingerStatus, ButtonCode, ButtonStatus};
2325
use crate::framebuffer::{Framebuffer, UpdateMode, Pixmap};
26+
use crate::view::reader::external_link::ExternalLink;
2427
use crate::view::{View, Event, AppCmd, Hub, Bus, RenderQueue, RenderData};
2528
use crate::view::{ViewId, Id, ID_FEEDER, EntryKind, EntryId, SliderId};
2629
use crate::view::{SMALL_BAR_HEIGHT, BIG_BAR_HEIGHT, THICKNESS_MEDIUM};
@@ -2173,6 +2176,13 @@ impl Reader {
21732176
}
21742177
}
21752178

2179+
fn close_external_link_popup(&mut self, rq: &mut RenderQueue) {
2180+
if let Some(index) = locate::<ExternalLink>(self) {
2181+
self.children.remove(index);
2182+
rq.add(RenderData::expose(self.rect, UpdateMode::Gui));
2183+
}
2184+
}
2185+
21762186
fn set_font_size(&mut self, font_size: f32, hub: &Hub, rq: &mut RenderQueue, context: &mut Context) {
21772187
if Arc::strong_count(&self.doc) > 1 {
21782188
return;
@@ -3139,19 +3149,9 @@ impl View for Reader {
31393149
hub.send(Event::GoTo(location)).ok();
31403150
} else {
31413151
if link.text.starts_with("https:") || link.text.starts_with("http:") {
3142-
if let Some(path) = context.settings.external_urls_queue.as_ref() {
3143-
if let Ok(mut file) = OpenOptions::new().create(true)
3144-
.append(true)
3145-
.open(path) {
3146-
if let Err(e) = writeln!(file, "{}", link.text) {
3147-
eprintln!("Couldn't write to {}: {:#}.", path.display(), e);
3148-
} else {
3149-
let message = format!("Queued {}.", link.text);
3150-
let notif = Notification::new(message, hub, rq, context);
3151-
self.children.push(Box::new(notif) as Box<dyn View>);
3152-
}
3153-
}
3154-
}
3152+
let view = ExternalLink::new(context, link.text);
3153+
rq.add(RenderData::new(view.id(), *view.rect(), UpdateMode::Gui));
3154+
self.children.push(Box::new(view) as Box<dyn View>);
31553155
} else {
31563156
eprintln!("Can't resolve URI: {}.", link.text);
31573157
}
@@ -3593,6 +3593,10 @@ impl View for Reader {
35933593
self.toggle_keyboard(false, None, hub, rq, context);
35943594
false
35953595
},
3596+
Event::Close(ViewId::ExternalLink) => {
3597+
self.close_external_link_popup(rq);
3598+
true
3599+
}
35963600
Event::Show(ViewId::TableOfContents) => {
35973601
{
35983602
self.toggle_bars(Some(false), hub, rq, context);
@@ -3926,6 +3930,28 @@ impl View for Reader {
39263930
}
39273931
true
39283932
},
3933+
Event::QueueLink(ref link) => {
3934+
self.close_external_link_popup(rq);
3935+
if let Some(path) = context.settings.external_urls_queue.as_ref() {
3936+
if let Ok(mut file) = OpenOptions::new().create(true)
3937+
.append(true)
3938+
.open(path) {
3939+
if let Err(e) = writeln!(file, "{}", link) {
3940+
eprintln!("Couldn't write to {}: {:#}.", path.display(), e);
3941+
} else {
3942+
let message = format!("Queued {}.", link);
3943+
let notif = Notification::new(message, hub, rq, context);
3944+
self.children.push(Box::new(notif) as Box<dyn View>);
3945+
}
3946+
}
3947+
}
3948+
true
3949+
},
3950+
Event::AddArticleLink(ref link) => {
3951+
self.close_external_link_popup(rq);
3952+
articles::queue_link(&mut context.article_service, link.clone());
3953+
true
3954+
}
39293955
_ => false,
39303956
}
39313957
}

0 commit comments

Comments
 (0)