window: prefetch images and queue offline read/unread actions

After a successful article refresh, all images referenced in article
content are downloaded in the background so articles can be read
offline. The prefetch only runs when the cache-images setting is
enabled and the connection is not metered.

Read/unread state changes that fail to reach the server (e.g. when
offline) are now persisted to a local queue in
~/.cache/net.jeena.FeedTheMonkey/pending_sync.json. The queue is
flushed at the start of the next successful fetch.
This commit is contained in:
Jeena 2026-03-21 02:45:45 +00:00
parent 3f759bce2e
commit 9bed643023
4 changed files with 123 additions and 5 deletions

65
src/pending_actions.rs Normal file
View file

@ -0,0 +1,65 @@
use std::path::PathBuf;
use crate::api::Api;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Action {
Read,
Unread,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PendingAction {
pub action: Action,
pub id: String,
}
fn path() -> PathBuf {
glib::user_cache_dir()
.join("net.jeena.FeedTheMonkey")
.join("pending_sync.json")
}
fn load() -> Vec<PendingAction> {
let Ok(data) = std::fs::read_to_string(path()) else { return Vec::new() };
serde_json::from_str(&data).unwrap_or_default()
}
fn save(actions: &[PendingAction]) {
let dir = path();
std::fs::create_dir_all(dir.parent().unwrap()).ok();
if let Ok(s) = serde_json::to_string(actions) {
std::fs::write(path(), s).ok();
}
}
/// Queue an action for an article. If the same article already has a pending
/// action, it is replaced with the new one (last writer wins).
pub fn add(action: Action, id: &str) {
let mut actions = load();
actions.retain(|a| a.id != id);
actions.push(PendingAction { action, id: id.to_string() });
save(&actions);
}
/// Send all queued actions to the server. Successfully synced actions are
/// removed; failed ones remain in the queue for the next attempt.
pub async fn flush(api: &Api, write_token: &str) {
let actions = load();
if actions.is_empty() {
return;
}
let mut remaining = Vec::new();
for pending in actions {
let result = match pending.action {
Action::Read => api.mark_read(write_token, &pending.id).await,
Action::Unread => api.mark_unread(write_token, &pending.id).await,
};
if result.is_err() {
remaining.push(pending);
}
}
save(&remaining);
}