Skip to content

Commit 412b896

Browse files
committed
Add support for adding new article URLs from the reader
1 parent df10c0f commit 412b896

File tree

5 files changed

+303
-14
lines changed

5 files changed

+303
-14
lines changed

crates/core/src/articles/mod.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod wallabag;
44
use chrono::FixedOffset;
55
use fxhash::FxHashSet;
66
use serde::{Deserialize, Serialize};
7+
use std::fs::OpenOptions;
8+
use std::io::prelude::*;
79
use std::{
810
collections::{BTreeMap, BTreeSet},
911
fs::{self, File},
@@ -176,3 +178,45 @@ fn save_index(index: &ArticleIndex) -> io::Result<()> {
176178
ARTICLES_DIR.to_owned() + "/index.json",
177179
)
178180
}
181+
182+
static QUEUE_MUTEX: Mutex<u32> = Mutex::new(0);
183+
184+
pub fn queue_link(link: String) {
185+
let lock = QUEUE_MUTEX.lock().unwrap();
186+
let path = format!("{ARTICLES_DIR}/queued.txt");
187+
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&path) {
188+
if let Err(e) = writeln!(file, "{}", link) {
189+
eprintln!("Couldn't write to {}: {:#}.", path, e);
190+
}
191+
}
192+
std::mem::drop(lock);
193+
}
194+
195+
pub fn read_queued() -> Vec<String> {
196+
// Lock the queue to avoid race conditions between adding a link and reading
197+
// the links.
198+
let lock = QUEUE_MUTEX.lock().unwrap();
199+
200+
// Read all the data in the file.
201+
let path = format!("{ARTICLES_DIR}/queued.txt");
202+
let mut file = match File::open(&path) {
203+
Ok(file) => file,
204+
Err(_) => return Vec::new(),
205+
};
206+
let mut data = String::new();
207+
if let Err(_) = file.read_to_string(&mut data) {
208+
return Vec::new();
209+
}
210+
211+
// Remove the file.
212+
fs::remove_file(path).ok();
213+
214+
// Make sure the lock stays locked until here.
215+
std::mem::drop(lock);
216+
217+
// Split each line in the file.
218+
data.split("\n")
219+
.filter(|s| !s.is_empty())
220+
.map(|s| s.to_string())
221+
.collect()
222+
}

crates/core/src/articles/wallabag.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ use ureq::Agent;
2121
use url::Url;
2222

2323
use crate::{
24-
articles::{save_index, Article, ArticleIndex, Changes, Service, ARTICLES_DIR},
24+
articles::{
25+
queue_link, read_queued, save_index, Article, ArticleIndex, Changes, Service, ARTICLES_DIR,
26+
},
2527
settings::ArticleAuth,
2628
view::{ArticleUpdateProgress, Event, Hub},
2729
};
@@ -503,6 +505,28 @@ fn update(hub: &Hub, auth: ArticleAuth, index: Arc<Mutex<ArticleIndex>>) -> Resu
503505
hub.send(Event::ArticlesAuth(Ok(auth.clone()))).ok();
504506
}
505507

508+
// Submit new URLs.
509+
let queued = read_queued();
510+
if !queued.is_empty() {
511+
// Send the list of URLs via a GET parameter, because for some reason
512+
// the Wallabag server only accepts those (and not a form in the POST
513+
// request).
514+
// See: https://github.com/wallabag/wallabag/issues/8353
515+
if let Err(err) = agent
516+
.post(format!("{url}api/entries/lists"))
517+
.query("urls", serde_json::to_string(&queued).unwrap())
518+
.header("Authorization", "Bearer ".to_owned() + &auth.access_token)
519+
.send_empty()
520+
{
521+
// Add the links back (this is inefficient, but it should work).
522+
for link in queued {
523+
queue_link(link);
524+
}
525+
526+
return Err(Error::other(format!("submitting article failed: {err}")));
527+
};
528+
}
529+
506530
// Sync local changes.
507531
let mut changes: BTreeMap<String, Vec<(&str, &str)>> = BTreeMap::new();
508532
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+
}

0 commit comments

Comments
 (0)