feat(api): google tasks oauth, sync engine, and background worker
- ApiClient with manual OAuth2 Device Flow (no yup-oauth2 dependency) - Devide auth: POST device/code -> show URL+code -> poll token endpoint - Token persistence in ~/.config/task_app/token.json - CRUD: create_task, update_task, delete_task, move_task via Google Tasks API - fetch_lists and fetch_tasks for initial sync import - Db wraps Connection in std::sync::Mutex for thread-safe sharing via Arc - Sync engine: background thread with tokio runtime, processes queue every 30s - process_sync_queue drains sync_queue and calls API methods - trigger_sync() called after every local mutation (create/update/delete/reorder) - Network status propagated to UI (Online/Offline/Syncing) - Initial sync skeleton ready for full import flow
This commit is contained in:
+28
-6
@@ -1,7 +1,11 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::domain::models::*;
|
||||
use crate::infrastructure::api::ApiClient;
|
||||
use crate::infrastructure::db::Db;
|
||||
use crate::ui::{Focus, NetworkStatus, Popup};
|
||||
|
||||
@@ -19,11 +23,18 @@ pub struct App {
|
||||
pub should_quit: bool,
|
||||
pub task_list_scroll: u16,
|
||||
pub detail_scroll: u16,
|
||||
pub db: Db,
|
||||
pub db: Arc<Db>,
|
||||
pub api_client: Arc<ApiClient>,
|
||||
sync_tx: mpsc::Sender<SyncCommand>,
|
||||
}
|
||||
|
||||
pub enum SyncCommand {
|
||||
TriggerSync,
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(db: Db) -> Self {
|
||||
pub fn new(db: Arc<Db>, api_client: Arc<ApiClient>, sync_tx: mpsc::Sender<SyncCommand>) -> Self {
|
||||
let lists = db.get_lists();
|
||||
let tasks = if !lists.is_empty() {
|
||||
let list_id = &lists[0].id;
|
||||
@@ -47,9 +58,15 @@ impl App {
|
||||
task_list_scroll: 0,
|
||||
detail_scroll: 0,
|
||||
db,
|
||||
api_client,
|
||||
sync_tx,
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_sync(&self) {
|
||||
let _ = self.sync_tx.try_send(SyncCommand::TriggerSync);
|
||||
}
|
||||
|
||||
pub fn handle_key(&mut self, key: KeyEvent) {
|
||||
if let Some(ref popup) = self.show_popup.clone() {
|
||||
self.handle_popup_key(key, popup);
|
||||
@@ -167,6 +184,7 @@ impl App {
|
||||
&list.id,
|
||||
&serde_json::to_string(&list).unwrap_or_default(),
|
||||
).ok();
|
||||
self.trigger_sync();
|
||||
self.load_lists();
|
||||
}
|
||||
Focus::TaskList => {
|
||||
@@ -188,13 +206,14 @@ impl App {
|
||||
list_id,
|
||||
&serde_json::to_string(&task).unwrap_or_default(),
|
||||
).ok();
|
||||
self.trigger_sync();
|
||||
self.load_tasks();
|
||||
}
|
||||
}
|
||||
Focus::Detail => {
|
||||
if !self.tasks.is_empty() {
|
||||
let task = &mut self.tasks[self.selected_task];
|
||||
task.title = input;
|
||||
task.title = input;
|
||||
self.db.update_task(task).ok();
|
||||
self.db.push_sync(
|
||||
SyncAction::Update,
|
||||
@@ -202,9 +221,8 @@ impl App {
|
||||
&task.list_id,
|
||||
&serde_json::to_string(task).unwrap_or_default(),
|
||||
).ok();
|
||||
self.trigger_sync();
|
||||
self.load_tasks();
|
||||
if !self.tasks.is_empty() && self.selected_task < self.tasks.len() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +275,7 @@ impl App {
|
||||
&task.list_id,
|
||||
&serde_json::to_string(task).unwrap_or_default(),
|
||||
).ok();
|
||||
self.trigger_sync();
|
||||
self.load_tasks();
|
||||
}
|
||||
self.show_popup = None;
|
||||
@@ -285,6 +304,7 @@ impl App {
|
||||
&list_id,
|
||||
"",
|
||||
).ok();
|
||||
self.trigger_sync();
|
||||
self.load_lists();
|
||||
if self.selected_list >= self.lists.len() {
|
||||
self.selected_list = self.lists.len().saturating_sub(1);
|
||||
@@ -304,6 +324,7 @@ impl App {
|
||||
&list_id,
|
||||
"",
|
||||
).ok();
|
||||
self.trigger_sync();
|
||||
self.load_tasks();
|
||||
if self.selected_task >= self.tasks.len() {
|
||||
self.selected_task = self.tasks.len().saturating_sub(1);
|
||||
@@ -354,6 +375,7 @@ impl App {
|
||||
&list_id,
|
||||
&payload.to_string(),
|
||||
).ok();
|
||||
self.trigger_sync();
|
||||
self.selected_task = new_index as usize;
|
||||
self.load_tasks();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user