Compare commits
3 Commits
83762720a1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cfad78ef8 | |||
| 6fd5b82941 | |||
| 66b833b7ac |
+16
@@ -1,2 +1,18 @@
|
|||||||
|
# Rust
|
||||||
target/
|
target/
|
||||||
|
**/*.rs.bk
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Database (runtime)
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# task_app
|
||||||
|
|
||||||
|
Aplicação TUI de gestão de tarefas com sincronização Google Tasks.
|
||||||
|
|
||||||
|
## Funcionalidades
|
||||||
|
|
||||||
|
- Listas e tarefas do Google Tasks num terminal
|
||||||
|
- Offline-first: dados guardados localmente em SQLite
|
||||||
|
- Sincronização bidirecional automática em background
|
||||||
|
- Calendário com eventos do Google Calendar
|
||||||
|
- Reordenação de tarefas com persistência
|
||||||
|
- Operações CRUD em listas e tarefas
|
||||||
|
- Seleção múltipla e ações em lote
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **UI:** ratatui + crossterm
|
||||||
|
- **Async:** tokio
|
||||||
|
- **DB:** rusqlite (SQLite)
|
||||||
|
- **Auth:** yup-oauth2 (OAuth 2.0)
|
||||||
|
- **API:** reqwest (Google Tasks + Calendar)
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Rust (edition 2021)
|
||||||
|
- Conta Google com Tasks ativado
|
||||||
|
- `client_secret.json` da Google Cloud Console
|
||||||
|
|
||||||
|
## Configuração
|
||||||
|
|
||||||
|
Coloca o `client_secret.json` num dos seguintes locais:
|
||||||
|
|
||||||
|
- `$GOOGLE_CLIENT_SECRET_FILE`
|
||||||
|
- `~/.config/task_app/client_secret.json`
|
||||||
|
- `./client_secret.json` (diretório atual)
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atalhos
|
||||||
|
|
||||||
|
| Tecla | Ação |
|
||||||
|
|---|---|
|
||||||
|
| `Tab` | Navegar entre painéis |
|
||||||
|
| `↑/↓` | Navegar dentro do painel |
|
||||||
|
| `Ctrl+←/→` | Mudar de lista |
|
||||||
|
| `Alt+↑/↓` | Reordenar tarefa |
|
||||||
|
| `n` | Nova tarefa / lista |
|
||||||
|
| `e` | Editar tarefa |
|
||||||
|
| `d` | Apagar (Enter confirma) |
|
||||||
|
| `Enter` | Completar/descompletar tarefa |
|
||||||
|
| `t` | Definir data (d=today, t=tomorrow, w=week, m=month) |
|
||||||
|
| `Ctrl+r` | Sincronização forçada |
|
||||||
|
| `q` | Sair |
|
||||||
+133
-48
@@ -224,6 +224,10 @@ impl App {
|
|||||||
}
|
}
|
||||||
let task = &mut self.tasks[self.selected_task];
|
let task = &mut self.tasks[self.selected_task];
|
||||||
task.due = Some(due);
|
task.due = Some(due);
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] TASK UPDATE: title=\"{}\" id={} due={}",
|
||||||
|
task.title, task.id, due
|
||||||
|
));
|
||||||
self.db.update_task(task).ok();
|
self.db.update_task(task).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Update,
|
SyncAction::Update,
|
||||||
@@ -441,7 +445,25 @@ impl App {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('d') | KeyCode::Char('D') => {
|
KeyCode::Char('d') | KeyCode::Char('D') => {
|
||||||
if !self.needs_auth {
|
if !self.needs_auth {
|
||||||
self.show_popup = Some(Popup::ConfirmDelete);
|
let context = match self.focus {
|
||||||
|
Focus::Tabs => {
|
||||||
|
if self.selected_list < self.lists.len() {
|
||||||
|
format!("Delete list: \"{}\"?", self.lists[self.selected_list].title)
|
||||||
|
} else {
|
||||||
|
"Delete this list?".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if !self.tasks.is_empty() && self.selected_task < self.tasks.len() {
|
||||||
|
let title = &self.tasks[self.selected_task].title;
|
||||||
|
let preview: String = title.chars().take(40).collect();
|
||||||
|
format!("Delete task: \"{}\"?", preview)
|
||||||
|
} else {
|
||||||
|
"Delete this task?".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.show_popup = Some(Popup::ConfirmDelete { context });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('e') | KeyCode::Char('E') => {
|
KeyCode::Char('e') | KeyCode::Char('E') => {
|
||||||
@@ -470,10 +492,15 @@ impl App {
|
|||||||
self.show_popup = Some(Popup::BulkAction);
|
self.show_popup = Some(Popup::BulkAction);
|
||||||
} else {
|
} else {
|
||||||
let task = &mut self.tasks[self.selected_task];
|
let task = &mut self.tasks[self.selected_task];
|
||||||
|
let old_status = task.status.clone();
|
||||||
task.status = match task.status {
|
task.status = match task.status {
|
||||||
TaskStatus::Completed => TaskStatus::NeedsAction,
|
TaskStatus::Completed => TaskStatus::NeedsAction,
|
||||||
TaskStatus::NeedsAction => TaskStatus::Completed,
|
TaskStatus::NeedsAction => TaskStatus::Completed,
|
||||||
};
|
};
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] TASK UPDATE: title=\"{}\" id={} status={:?}->{:?}",
|
||||||
|
task.title, task.id, old_status, task.status
|
||||||
|
));
|
||||||
self.db.update_task(task).ok();
|
self.db.update_task(task).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Update,
|
SyncAction::Update,
|
||||||
@@ -543,6 +570,10 @@ impl App {
|
|||||||
id: uuid_v4(),
|
id: uuid_v4(),
|
||||||
title: input,
|
title: input,
|
||||||
};
|
};
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] LIST CREATE: title=\"{}\" id={}",
|
||||||
|
list.title, list.id
|
||||||
|
));
|
||||||
self.db.insert_list(&list).ok();
|
self.db.insert_list(&list).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::CreateList,
|
SyncAction::CreateList,
|
||||||
@@ -655,6 +686,10 @@ impl App {
|
|||||||
if let Some(d) = due {
|
if let Some(d) = due {
|
||||||
task.due = Some(d);
|
task.due = Some(d);
|
||||||
}
|
}
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] TASK UPDATE: title=\"{}\" id={} has_notes={} has_due={}",
|
||||||
|
task.title, task.id, task.notes.is_some(), task.due.is_some()
|
||||||
|
));
|
||||||
self.db.update_task(task).ok();
|
self.db.update_task(task).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Update,
|
SyncAction::Update,
|
||||||
@@ -678,6 +713,10 @@ impl App {
|
|||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
};
|
};
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] TASK CREATE: title=\"{}\" id={} list={}",
|
||||||
|
task.title, task.id, list_id
|
||||||
|
));
|
||||||
self.db.insert_task(&task).ok();
|
self.db.insert_task(&task).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Create,
|
SyncAction::Create,
|
||||||
@@ -783,6 +822,10 @@ impl App {
|
|||||||
if !self.tasks.is_empty() {
|
if !self.tasks.is_empty() {
|
||||||
let task = &mut self.tasks[self.selected_task];
|
let task = &mut self.tasks[self.selected_task];
|
||||||
task.due = Some(self.draft_date);
|
task.due = Some(self.draft_date);
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] TASK UPDATE: title=\"{}\" id={} due={}",
|
||||||
|
task.title, task.id, self.draft_date
|
||||||
|
));
|
||||||
self.db.update_task(task).ok();
|
self.db.update_task(task).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Update,
|
SyncAction::Update,
|
||||||
@@ -803,7 +846,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Popup::ConfirmDelete => match key.code {
|
Popup::ConfirmDelete { context: _ } => match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
self.show_popup = None;
|
self.show_popup = None;
|
||||||
}
|
}
|
||||||
@@ -812,6 +855,7 @@ impl App {
|
|||||||
Focus::Tabs => {
|
Focus::Tabs => {
|
||||||
if self.selected_list < self.lists.len() {
|
if self.selected_list < self.lists.len() {
|
||||||
let list_id = self.lists[self.selected_list].id.clone();
|
let list_id = self.lists[self.selected_list].id.clone();
|
||||||
|
let title = self.lists[self.selected_list].title.clone();
|
||||||
self.db.delete_list(&list_id).ok();
|
self.db.delete_list(&list_id).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::DeleteList,
|
SyncAction::DeleteList,
|
||||||
@@ -819,6 +863,10 @@ impl App {
|
|||||||
&list_id,
|
&list_id,
|
||||||
"",
|
"",
|
||||||
).ok();
|
).ok();
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] LIST DELETE: title=\"{}\" id={}",
|
||||||
|
title, list_id
|
||||||
|
));
|
||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_lists();
|
self.load_lists();
|
||||||
if self.selected_list >= self.lists.len() {
|
if self.selected_list >= self.lists.len() {
|
||||||
@@ -832,13 +880,20 @@ impl App {
|
|||||||
let task = &self.tasks[self.selected_task];
|
let task = &self.tasks[self.selected_task];
|
||||||
let task_id = task.id.clone();
|
let task_id = task.id.clone();
|
||||||
let list_id = task.list_id.clone();
|
let list_id = task.list_id.clone();
|
||||||
|
let title = task.title.clone();
|
||||||
self.db.delete_task(&task_id).ok();
|
self.db.delete_task(&task_id).ok();
|
||||||
|
if !list_id.contains('-') {
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Delete,
|
SyncAction::Delete,
|
||||||
&task_id,
|
&task_id,
|
||||||
&list_id,
|
&list_id,
|
||||||
"",
|
"",
|
||||||
).ok();
|
).ok();
|
||||||
|
}
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] TASK DELETE: title=\"{}\" id={} list={}",
|
||||||
|
title, task_id, list_id
|
||||||
|
));
|
||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
if self.selected_task >= self.tasks.len() {
|
if self.selected_task >= self.tasks.len() {
|
||||||
@@ -921,6 +976,10 @@ impl App {
|
|||||||
&serde_json::to_string(task).unwrap_or_default(),
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
).ok();
|
).ok();
|
||||||
}
|
}
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] BULK COMPLETED: {} tasks",
|
||||||
|
indices.len()
|
||||||
|
));
|
||||||
self.clear_selection();
|
self.clear_selection();
|
||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
@@ -941,6 +1000,10 @@ impl App {
|
|||||||
&serde_json::to_string(task).unwrap_or_default(),
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
).ok();
|
).ok();
|
||||||
}
|
}
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] BULK DUE: {} tasks -> today",
|
||||||
|
indices.len()
|
||||||
|
));
|
||||||
self.clear_selection();
|
self.clear_selection();
|
||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
@@ -951,6 +1014,15 @@ impl App {
|
|||||||
id: uuid_v4(),
|
id: uuid_v4(),
|
||||||
title: list_name.to_string(),
|
title: list_name.to_string(),
|
||||||
};
|
};
|
||||||
|
crate::log_msg(&format!("[task_app] bulk_move_to_new_list: list={} title={} selected={} tasks_len={}",
|
||||||
|
list.id, list_name, self.selected_tasks.len(), self.tasks.len()));
|
||||||
|
for &i in self.selected_tasks.iter() {
|
||||||
|
if i < self.tasks.len() {
|
||||||
|
crate::log_msg(&format!("[task_app] selected[{}]: task={} list_id={} has_hyphen={}",
|
||||||
|
i, self.tasks[i].id, self.tasks[i].list_id, self.tasks[i].list_id.contains('-')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.db.insert_list(&list).ok();
|
self.db.insert_list(&list).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::CreateList,
|
SyncAction::CreateList,
|
||||||
@@ -963,43 +1035,37 @@ impl App {
|
|||||||
for &i in &indices {
|
for &i in &indices {
|
||||||
if i >= self.tasks.len() { continue; }
|
if i >= self.tasks.len() { continue; }
|
||||||
let original = &self.tasks[i];
|
let original = &self.tasks[i];
|
||||||
let new_task = Task {
|
|
||||||
id: uuid_v4(),
|
|
||||||
list_id: list.id.clone(),
|
|
||||||
title: original.title.clone(),
|
|
||||||
notes: original.notes.clone(),
|
|
||||||
status: original.status.clone(),
|
|
||||||
due: original.due,
|
|
||||||
position: 0,
|
|
||||||
created_at: None,
|
|
||||||
updated_at: None,
|
|
||||||
};
|
|
||||||
self.db.insert_task(&new_task).ok();
|
|
||||||
self.db.push_sync(
|
|
||||||
SyncAction::Create,
|
|
||||||
&new_task.id,
|
|
||||||
&list.id,
|
|
||||||
&serde_json::to_string(&new_task).unwrap_or_default(),
|
|
||||||
).ok();
|
|
||||||
|
|
||||||
// Delete original
|
// Always update local DB immediately
|
||||||
self.db.delete_task(&original.id).ok();
|
self.db.update_task_list_id(&original.id, &list.id).ok();
|
||||||
|
|
||||||
|
if !original.list_id.contains('-') {
|
||||||
|
// Source list has server ID — also push Move sync
|
||||||
|
let payload = serde_json::to_string(&MovePayload {
|
||||||
|
destination_list_id: list.id.clone(),
|
||||||
|
}).unwrap_or_default();
|
||||||
|
crate::log_msg(&format!("[task_app] pushing Move: task={} source={} dest={}",
|
||||||
|
original.id, original.list_id, list.id));
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Delete,
|
SyncAction::Move,
|
||||||
&original.id,
|
&original.id,
|
||||||
&original.list_id,
|
&original.list_id,
|
||||||
"",
|
&payload,
|
||||||
).ok();
|
).ok();
|
||||||
|
} else {
|
||||||
|
crate::log_msg(&format!("[task_app] skipping Move (source has hyphen): task={} list_id={}",
|
||||||
|
original.id, original.list_id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clear_selection();
|
self.clear_selection();
|
||||||
self.trigger_sync();
|
|
||||||
self.load_lists();
|
self.load_lists();
|
||||||
// Switch to the new list
|
// Switch to the new list
|
||||||
if let Some(pos) = self.lists.iter().position(|l| l.id == list.id) {
|
if let Some(pos) = self.lists.iter().position(|l| l.id == list.id) {
|
||||||
self.selected_list = pos;
|
self.selected_list = pos;
|
||||||
}
|
}
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
|
self.trigger_full_sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bulk_mark_uncomplete(&mut self) {
|
fn bulk_mark_uncomplete(&mut self) {
|
||||||
@@ -1016,6 +1082,10 @@ impl App {
|
|||||||
&serde_json::to_string(task).unwrap_or_default(),
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
).ok();
|
).ok();
|
||||||
}
|
}
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] BULK UNCOMPLETE: {} tasks",
|
||||||
|
indices.len()
|
||||||
|
));
|
||||||
self.clear_selection();
|
self.clear_selection();
|
||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
@@ -1038,6 +1108,10 @@ impl App {
|
|||||||
&serde_json::to_string(task).unwrap_or_default(),
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
).ok();
|
).ok();
|
||||||
}
|
}
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] BULK DUE: {} tasks -> tomorrow",
|
||||||
|
indices.len()
|
||||||
|
));
|
||||||
self.clear_selection();
|
self.clear_selection();
|
||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
@@ -1060,49 +1134,56 @@ impl App {
|
|||||||
&serde_json::to_string(task).unwrap_or_default(),
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
).ok();
|
).ok();
|
||||||
}
|
}
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] BULK DUE: {} tasks -> next week",
|
||||||
|
indices.len()
|
||||||
|
));
|
||||||
self.clear_selection();
|
self.clear_selection();
|
||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bulk_move_to_existing_list(&mut self, target_list_id: &str) {
|
fn bulk_move_to_existing_list(&mut self, target_list_id: &str) {
|
||||||
|
crate::log_msg(&format!("[task_app] bulk_move_to_existing_list: target={} selected={} tasks_len={}",
|
||||||
|
target_list_id, self.selected_tasks.len(), self.tasks.len()));
|
||||||
|
for &i in self.selected_tasks.iter() {
|
||||||
|
if i < self.tasks.len() {
|
||||||
|
crate::log_msg(&format!("[task_app] selected[{}]: task={} list_id={} has_hyphen={}",
|
||||||
|
i, self.tasks[i].id, self.tasks[i].list_id, self.tasks[i].list_id.contains('-')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let indices: Vec<usize> = self.selected_tasks.iter().copied().collect();
|
let indices: Vec<usize> = self.selected_tasks.iter().copied().collect();
|
||||||
for &i in &indices {
|
for &i in &indices {
|
||||||
if i >= self.tasks.len() { continue; }
|
if i >= self.tasks.len() { continue; }
|
||||||
let original = &self.tasks[i];
|
let original = &self.tasks[i];
|
||||||
let new_task = Task {
|
|
||||||
id: uuid_v4(),
|
|
||||||
list_id: target_list_id.to_string(),
|
|
||||||
title: original.title.clone(),
|
|
||||||
notes: original.notes.clone(),
|
|
||||||
status: original.status.clone(),
|
|
||||||
due: original.due,
|
|
||||||
position: 0,
|
|
||||||
created_at: None,
|
|
||||||
updated_at: None,
|
|
||||||
};
|
|
||||||
self.db.insert_task(&new_task).ok();
|
|
||||||
self.db.push_sync(
|
|
||||||
SyncAction::Create,
|
|
||||||
&new_task.id,
|
|
||||||
target_list_id,
|
|
||||||
&serde_json::to_string(&new_task).unwrap_or_default(),
|
|
||||||
).ok();
|
|
||||||
|
|
||||||
self.db.delete_task(&original.id).ok();
|
// Always update local DB immediately
|
||||||
|
self.db.update_task_list_id(&original.id, target_list_id).ok();
|
||||||
|
|
||||||
|
if !original.list_id.contains('-') {
|
||||||
|
let payload = serde_json::to_string(&MovePayload {
|
||||||
|
destination_list_id: target_list_id.to_string(),
|
||||||
|
}).unwrap_or_default();
|
||||||
|
crate::log_msg(&format!("[task_app] pushing Move: task={} source={} dest={}",
|
||||||
|
original.id, original.list_id, target_list_id));
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Delete,
|
SyncAction::Move,
|
||||||
&original.id,
|
&original.id,
|
||||||
&original.list_id,
|
&original.list_id,
|
||||||
"",
|
&payload,
|
||||||
).ok();
|
).ok();
|
||||||
|
} else {
|
||||||
|
crate::log_msg(&format!("[task_app] skipping Move (source has hyphen): task={} list_id={}",
|
||||||
|
original.id, original.list_id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.clear_selection();
|
self.clear_selection();
|
||||||
self.trigger_sync();
|
|
||||||
if let Some(pos) = self.lists.iter().position(|l| l.id == target_list_id) {
|
if let Some(pos) = self.lists.iter().position(|l| l.id == target_list_id) {
|
||||||
self.selected_list = pos;
|
self.selected_list = pos;
|
||||||
}
|
}
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
|
self.trigger_full_sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_bulk_action(&mut self, action_idx: usize) {
|
fn execute_bulk_action(&mut self, action_idx: usize) {
|
||||||
@@ -1119,8 +1200,8 @@ impl App {
|
|||||||
self.show_popup = Some(Popup::Input);
|
self.show_popup = Some(Popup::Input);
|
||||||
}
|
}
|
||||||
6 => {
|
6 => {
|
||||||
|
self.load_lists();
|
||||||
self.popup_list_indices = self.lists.iter()
|
self.popup_list_indices = self.lists.iter()
|
||||||
.filter(|l| !l.id.contains('-'))
|
|
||||||
.map(|l| (l.title.clone(), l.id.clone()))
|
.map(|l| (l.title.clone(), l.id.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
self.popup_list_selected = 0;
|
self.popup_list_selected = 0;
|
||||||
@@ -1155,6 +1236,10 @@ impl App {
|
|||||||
let new_pos = self.tasks[new_index as usize].position;
|
let new_pos = self.tasks[new_index as usize].position;
|
||||||
|
|
||||||
if self.db.reorder_task(&task_id, new_pos).is_ok() {
|
if self.db.reorder_task(&task_id, new_pos).is_ok() {
|
||||||
|
crate::log_msg(&format!(
|
||||||
|
"[task_app] TASK REORDER: id={} list={} new_pos={}",
|
||||||
|
task_id, list_id, new_pos
|
||||||
|
));
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"new_position": new_pos
|
"new_position": new_pos
|
||||||
|
|||||||
@@ -32,10 +32,16 @@ pub enum SyncAction {
|
|||||||
Update,
|
Update,
|
||||||
Delete,
|
Delete,
|
||||||
Reorder,
|
Reorder,
|
||||||
|
Move,
|
||||||
CreateList,
|
CreateList,
|
||||||
DeleteList,
|
DeleteList,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MovePayload {
|
||||||
|
pub destination_list_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncQueueItem {
|
pub struct SyncQueueItem {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ impl ApiClient {
|
|||||||
req = req.query(&[("previous", p)]);
|
req = req.query(&[("previous", p)]);
|
||||||
}
|
}
|
||||||
if let Some(s) = sibling {
|
if let Some(s) = sibling {
|
||||||
req = req.query(&[("destinationTaskList", s)]);
|
req = req.query(&[("destinationTasklist", s)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = req
|
let resp = req
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ impl Db {
|
|||||||
payload TEXT NOT NULL,
|
payload TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
retries INTEGER NOT NULL DEFAULT 0
|
retries INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS list_id_map (
|
||||||
|
old_id TEXT PRIMARY KEY,
|
||||||
|
new_id TEXT NOT NULL
|
||||||
);",
|
);",
|
||||||
)?;
|
)?;
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
@@ -206,6 +211,15 @@ impl Db {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_task_list_id(&self, task_id: &str, new_list_id: &str) -> SqlResult<()> {
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE tasks SET list_id = ?1, updated_at = ?2 WHERE id = ?3",
|
||||||
|
params![new_list_id, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), task_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reorder_task(&self, task_id: &str, new_position: i64) -> SqlResult<()> {
|
pub fn reorder_task(&self, task_id: &str, new_position: i64) -> SqlResult<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let (old_position, list_id): (i64, String) = conn.query_row(
|
let (old_position, list_id): (i64, String) = conn.query_row(
|
||||||
@@ -281,6 +295,7 @@ impl Db {
|
|||||||
SyncAction::Update => "Update",
|
SyncAction::Update => "Update",
|
||||||
SyncAction::Delete => "Delete",
|
SyncAction::Delete => "Delete",
|
||||||
SyncAction::Reorder => "Reorder",
|
SyncAction::Reorder => "Reorder",
|
||||||
|
SyncAction::Move => "Move",
|
||||||
SyncAction::CreateList => "CreateList",
|
SyncAction::CreateList => "CreateList",
|
||||||
SyncAction::DeleteList => "DeleteList",
|
SyncAction::DeleteList => "DeleteList",
|
||||||
};
|
};
|
||||||
@@ -328,10 +343,19 @@ impl Db {
|
|||||||
|
|
||||||
pub fn update_list_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
|
pub fn update_list_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
|
let title: Option<String> = conn.query_row(
|
||||||
|
"SELECT title FROM task_lists WHERE id = ?1",
|
||||||
|
params![old_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
).ok();
|
||||||
|
conn.execute("DELETE FROM task_lists WHERE id = ?1", params![old_id])?;
|
||||||
|
conn.execute("DELETE FROM task_lists WHERE id = ?1", params![new_id])?;
|
||||||
|
if let Some(title) = title {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE task_lists SET id = ?1 WHERE id = ?2",
|
"INSERT INTO task_lists (id, title) VALUES (?1, ?2)",
|
||||||
params![new_id, old_id],
|
params![new_id, title],
|
||||||
)?;
|
)?;
|
||||||
|
}
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE tasks SET list_id = ?1 WHERE list_id = ?2",
|
"UPDATE tasks SET list_id = ?1 WHERE list_id = ?2",
|
||||||
params![new_id, old_id],
|
params![new_id, old_id],
|
||||||
@@ -340,9 +364,63 @@ impl Db {
|
|||||||
"UPDATE sync_queue SET list_id = ?1 WHERE list_id = ?2",
|
"UPDATE sync_queue SET list_id = ?1 WHERE list_id = ?2",
|
||||||
params![new_id, old_id],
|
params![new_id, old_id],
|
||||||
)?;
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO list_id_map (old_id, new_id) VALUES (?1, ?2)",
|
||||||
|
params![old_id, new_id],
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_list_id(&self, list_id: &str) -> String {
|
||||||
|
if !list_id.contains('-') {
|
||||||
|
return list_id.to_string();
|
||||||
|
}
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
|
// Check if this list_id has been mapped to a server ID
|
||||||
|
let result: Option<String> = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT new_id FROM list_id_map WHERE old_id = ?1",
|
||||||
|
params![list_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
// Also check if task_lists has this id directly (already a server ID from old schema)
|
||||||
|
let result = result.or_else(|| {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT id FROM task_lists WHERE id = ?1 AND id NOT LIKE '%-%'",
|
||||||
|
params![list_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
// Also check if task_lists has this list with a different id (same title)
|
||||||
|
let result = result.or_else(|| {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT id FROM task_lists WHERE title = (SELECT title FROM task_lists WHERE id = ?1) AND id NOT LIKE '%-%'",
|
||||||
|
params![list_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
if result.is_none() && list_id.contains('-') {
|
||||||
|
let log_path = {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||||
|
let mut p = std::path::PathBuf::from(home);
|
||||||
|
p.push(".local/share/task_app/sync.log");
|
||||||
|
p
|
||||||
|
};
|
||||||
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(log_path)
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = writeln!(f, "[task_app] resolve_list_id: {} NOT FOUND in list_id_map", list_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.unwrap_or_else(|| list_id.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn update_sync_list_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
|
pub fn update_sync_list_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
@@ -366,6 +444,7 @@ impl Db {
|
|||||||
"\"Update\"" | "Update" => SyncAction::Update,
|
"\"Update\"" | "Update" => SyncAction::Update,
|
||||||
"\"Delete\"" | "Delete" => SyncAction::Delete,
|
"\"Delete\"" | "Delete" => SyncAction::Delete,
|
||||||
"\"Reorder\"" | "Reorder" => SyncAction::Reorder,
|
"\"Reorder\"" | "Reorder" => SyncAction::Reorder,
|
||||||
|
"\"Move\"" | "Move" => SyncAction::Move,
|
||||||
"\"CreateList\"" | "CreateList" => SyncAction::CreateList,
|
"\"CreateList\"" | "CreateList" => SyncAction::CreateList,
|
||||||
"\"DeleteList\"" | "DeleteList" => SyncAction::DeleteList,
|
"\"DeleteList\"" | "DeleteList" => SyncAction::DeleteList,
|
||||||
_ => SyncAction::Update,
|
_ => SyncAction::Update,
|
||||||
|
|||||||
+102
-17
@@ -20,6 +20,28 @@ use crate::infrastructure::api::ApiClient;
|
|||||||
use crate::infrastructure::db::Db;
|
use crate::infrastructure::db::Db;
|
||||||
use crate::ui::{draw, AppView, NetworkStatus};
|
use crate::ui::{draw, AppView, NetworkStatus};
|
||||||
|
|
||||||
|
fn log_file_path() -> PathBuf {
|
||||||
|
let data_dir = dirs_data_dir();
|
||||||
|
std::fs::create_dir_all(&data_dir).ok();
|
||||||
|
data_dir.join("sync.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_msg(msg: &str) {
|
||||||
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(log_file_path())
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = writeln!(f, "{}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dirs_data_dir() -> PathBuf {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||||
|
PathBuf::from(home).join(".local/share/task_app")
|
||||||
|
}
|
||||||
|
|
||||||
fn find_secret_file() -> Option<PathBuf> {
|
fn find_secret_file() -> Option<PathBuf> {
|
||||||
if let Ok(path) = std::env::var("GOOGLE_CLIENT_SECRET_FILE") {
|
if let Ok(path) = std::env::var("GOOGLE_CLIENT_SECRET_FILE") {
|
||||||
let p = PathBuf::from(&path);
|
let p = PathBuf::from(&path);
|
||||||
@@ -216,11 +238,21 @@ async fn run_initial_sync(
|
|||||||
Ok(lists) => {
|
Ok(lists) => {
|
||||||
total_lists = lists.len();
|
total_lists = lists.len();
|
||||||
for list in &lists {
|
for list in &lists {
|
||||||
|
log_msg(&format!(
|
||||||
|
"[task_app] LIST SYNC: title=\"{}\" id={}",
|
||||||
|
list.title, list.id
|
||||||
|
));
|
||||||
db.insert_list(list).ok();
|
db.insert_list(list).ok();
|
||||||
}
|
}
|
||||||
for list in &lists {
|
for list in &lists {
|
||||||
if let Ok(tasks) = api.fetch_tasks(&list.id).await {
|
if let Ok(tasks) = api.fetch_tasks(&list.id).await {
|
||||||
total_tasks += tasks.len();
|
total_tasks += tasks.len();
|
||||||
|
log_msg(&format!(
|
||||||
|
"[task_app] TASK SYNC: {} tasks in list=\"{}\" id={}",
|
||||||
|
tasks.len(),
|
||||||
|
list.title,
|
||||||
|
list.id
|
||||||
|
));
|
||||||
db.replace_all_tasks(&list.id, &tasks).ok();
|
db.replace_all_tasks(&list.id, &tasks).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,6 +295,12 @@ async fn push_sync(
|
|||||||
|
|
||||||
let mut all_ok = true;
|
let mut all_ok = true;
|
||||||
|
|
||||||
|
log_msg(&format!("[task_app] push_sync: {} items in queue", items.len()));
|
||||||
|
for (idx, item) in items.iter().enumerate() {
|
||||||
|
log_msg(&format!("[task_app] item[{}]: action={:?} task={} list={} payload_len={}",
|
||||||
|
idx, item.action, item.task_id, item.list_id, item.payload.len()));
|
||||||
|
}
|
||||||
|
|
||||||
// First pass: CreateList items (so list IDs are updated before task operations)
|
// First pass: CreateList items (so list IDs are updated before task operations)
|
||||||
for i in 0..items.len() {
|
for i in 0..items.len() {
|
||||||
if items[i].action != SyncAction::CreateList {
|
if items[i].action != SyncAction::CreateList {
|
||||||
@@ -274,19 +312,22 @@ async fn push_sync(
|
|||||||
});
|
});
|
||||||
match api.create_list(&list.title).await {
|
match api.create_list(&list.title).await {
|
||||||
Ok(server_list) => {
|
Ok(server_list) => {
|
||||||
|
log_msg(&format!("[task_app] CreateList success: local={} -> server={}",
|
||||||
|
items[i].task_id, server_list.id));
|
||||||
if server_list.id != items[i].task_id {
|
if server_list.id != items[i].task_id {
|
||||||
let _ = db.update_list_id(&items[i].task_id, &server_list.id);
|
let _ = db.update_list_id(&items[i].task_id, &server_list.id);
|
||||||
// Update list_id in remaining items of this batch
|
|
||||||
for j in (i + 1)..items.len() {
|
for j in (i + 1)..items.len() {
|
||||||
if items[j].list_id == items[i].task_id {
|
if items[j].list_id == items[i].task_id {
|
||||||
|
log_msg(&format!("[task_app] -> updating batch item[{}] list_id: {} -> {}",
|
||||||
|
j, items[j].list_id, server_list.id));
|
||||||
items[j].list_id = server_list.id.clone();
|
items[j].list_id = server_list.id.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("[task_app] Sync failed (retry {}/{}): action=CreateList list={} error={}",
|
log_msg(&format!("[task_app] Sync failed (retry {}/{}): action=CreateList list={} error={}",
|
||||||
items[i].retries, MAX_SYNC_RETRIES, items[i].task_id, err);
|
items[i].retries, MAX_SYNC_RETRIES, items[i].task_id, err));
|
||||||
if items[i].retries < MAX_SYNC_RETRIES {
|
if items[i].retries < MAX_SYNC_RETRIES {
|
||||||
let _ = db.push_sync_with_retry(
|
let _ = db.push_sync_with_retry(
|
||||||
SyncAction::CreateList,
|
SyncAction::CreateList,
|
||||||
@@ -306,11 +347,15 @@ async fn push_sync(
|
|||||||
if item.action == SyncAction::CreateList {
|
if item.action == SyncAction::CreateList {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let api_list_id = db.resolve_list_id(&item.list_id);
|
||||||
|
if api_list_id != item.list_id {
|
||||||
|
log_msg(&format!("[task_app] -> resolved list_id for API call: {} -> {}", item.list_id, api_list_id));
|
||||||
|
}
|
||||||
let result = match item.action {
|
let result = match item.action {
|
||||||
SyncAction::Create => {
|
SyncAction::Create => {
|
||||||
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
||||||
id: item.task_id.clone(),
|
id: item.task_id.clone(),
|
||||||
list_id: item.list_id.clone(),
|
list_id: api_list_id.clone(),
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
notes: None,
|
notes: None,
|
||||||
status: TaskStatus::NeedsAction,
|
status: TaskStatus::NeedsAction,
|
||||||
@@ -319,7 +364,7 @@ async fn push_sync(
|
|||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
});
|
});
|
||||||
api.create_task(&item.list_id, &task).await.map(|server_task| {
|
api.create_task(&api_list_id, &task).await.map(|server_task| {
|
||||||
if server_task.id != item.task_id {
|
if server_task.id != item.task_id {
|
||||||
let _ = db.update_task_id(&item.task_id, &server_task.id);
|
let _ = db.update_task_id(&item.task_id, &server_task.id);
|
||||||
let _ = db.update_sync_task_id(&item.task_id, &server_task.id);
|
let _ = db.update_sync_task_id(&item.task_id, &server_task.id);
|
||||||
@@ -329,7 +374,7 @@ async fn push_sync(
|
|||||||
SyncAction::Update => {
|
SyncAction::Update => {
|
||||||
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
||||||
id: item.task_id.clone(),
|
id: item.task_id.clone(),
|
||||||
list_id: item.list_id.clone(),
|
list_id: api_list_id.clone(),
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
notes: None,
|
notes: None,
|
||||||
status: TaskStatus::NeedsAction,
|
status: TaskStatus::NeedsAction,
|
||||||
@@ -338,13 +383,22 @@ async fn push_sync(
|
|||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
});
|
});
|
||||||
api.update_task(&item.list_id, &task).await
|
api.update_task(&api_list_id, &task).await
|
||||||
}
|
}
|
||||||
SyncAction::Delete => {
|
SyncAction::Delete => {
|
||||||
api.delete_task(&item.list_id, &item.task_id).await
|
api.delete_task(&api_list_id, &item.task_id).await
|
||||||
}
|
}
|
||||||
SyncAction::Reorder => {
|
SyncAction::Reorder => {
|
||||||
api.move_task(&item.list_id, &item.task_id, None, None).await
|
api.move_task(&api_list_id, &item.task_id, None, None).await
|
||||||
|
}
|
||||||
|
SyncAction::Move => {
|
||||||
|
let move_payload: MovePayload = serde_json::from_str(&item.payload).unwrap_or_else(|_| MovePayload {
|
||||||
|
destination_list_id: String::new(),
|
||||||
|
});
|
||||||
|
let resolved_dest = db.resolve_list_id(&move_payload.destination_list_id);
|
||||||
|
log_msg(&format!("[task_app] Move: task={} source={} dest_raw={} dest_resolved={}",
|
||||||
|
item.task_id, api_list_id, move_payload.destination_list_id, resolved_dest));
|
||||||
|
api.move_task(&api_list_id, &item.task_id, None, Some(&resolved_dest)).await
|
||||||
}
|
}
|
||||||
SyncAction::DeleteList => {
|
SyncAction::DeleteList => {
|
||||||
api.delete_list(&item.task_id).await
|
api.delete_list(&item.task_id).await
|
||||||
@@ -353,19 +407,36 @@ async fn push_sync(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
eprintln!("[task_app] Sync failed (retry {}/{}): action={:?} task={} error={}",
|
log_msg(&format!("[task_app] Sync failed (retry {}/{}): action={:?} task={} list_id={} error={}",
|
||||||
item.retries, MAX_SYNC_RETRIES, item.action, item.task_id, err);
|
item.retries, MAX_SYNC_RETRIES, item.action, item.task_id, item.list_id, err));
|
||||||
if item.retries < MAX_SYNC_RETRIES {
|
let resolved_list_id = db.resolve_list_id(&item.list_id);
|
||||||
|
if resolved_list_id != item.list_id {
|
||||||
|
log_msg(&format!("[task_app] -> resolved list_id: {} -> {}", item.list_id, resolved_list_id));
|
||||||
|
}
|
||||||
|
if resolved_list_id.contains('-') {
|
||||||
|
log_msg("[task_app] -> list_id is local UUID (list not synced), dropping item");
|
||||||
|
} else if item.retries < MAX_SYNC_RETRIES {
|
||||||
|
let re_payload = if item.action == SyncAction::Move {
|
||||||
|
let move_payload: MovePayload = serde_json::from_str(&item.payload).unwrap_or_else(|_| MovePayload {
|
||||||
|
destination_list_id: String::new(),
|
||||||
|
});
|
||||||
|
let resolved_dest = db.resolve_list_id(&move_payload.destination_list_id);
|
||||||
|
serde_json::to_string(&MovePayload {
|
||||||
|
destination_list_id: resolved_dest,
|
||||||
|
}).unwrap_or_else(|_| item.payload.clone())
|
||||||
|
} else {
|
||||||
|
item.payload.clone()
|
||||||
|
};
|
||||||
let _ = db.push_sync_with_retry(
|
let _ = db.push_sync_with_retry(
|
||||||
item.action.clone(),
|
item.action.clone(),
|
||||||
&item.task_id,
|
&item.task_id,
|
||||||
&item.list_id,
|
&resolved_list_id,
|
||||||
&item.payload,
|
&re_payload,
|
||||||
item.retries + 1,
|
item.retries + 1,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("[task_app] Dropping sync item after {} failed attempts: action={:?} task={}",
|
log_msg(&format!("[task_app] Dropping sync item after {} failed attempts: action={:?} task={}",
|
||||||
MAX_SYNC_RETRIES, item.action, item.task_id);
|
MAX_SYNC_RETRIES, item.action, item.task_id));
|
||||||
}
|
}
|
||||||
all_ok = false;
|
all_ok = false;
|
||||||
}
|
}
|
||||||
@@ -421,6 +492,10 @@ async fn pull_sync(
|
|||||||
Ok(lists) => {
|
Ok(lists) => {
|
||||||
total_lists = lists.len();
|
total_lists = lists.len();
|
||||||
for list in &lists {
|
for list in &lists {
|
||||||
|
log_msg(&format!(
|
||||||
|
"[task_app] LIST SYNC: title=\"{}\" id={}",
|
||||||
|
list.title, list.id
|
||||||
|
));
|
||||||
db.insert_list(list).ok();
|
db.insert_list(list).ok();
|
||||||
}
|
}
|
||||||
for list in &lists {
|
for list in &lists {
|
||||||
@@ -432,10 +507,20 @@ async fn pull_sync(
|
|||||||
if let Ok(tasks) = result {
|
if let Ok(tasks) = result {
|
||||||
total_tasks += tasks.len();
|
total_tasks += tasks.len();
|
||||||
if use_incremental {
|
if use_incremental {
|
||||||
|
log_msg(&format!(
|
||||||
|
"[task_app] TASK SYNC (incremental): {} tasks in list=\"{}\"",
|
||||||
|
tasks.len(),
|
||||||
|
list.title
|
||||||
|
));
|
||||||
for task in &tasks {
|
for task in &tasks {
|
||||||
db.insert_task(task).ok();
|
db.insert_task(task).ok();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log_msg(&format!(
|
||||||
|
"[task_app] TASK SYNC (full): {} tasks in list=\"{}\"",
|
||||||
|
tasks.len(),
|
||||||
|
list.title
|
||||||
|
));
|
||||||
db.replace_all_tasks(&list.id, &tasks).ok();
|
db.replace_all_tasks(&list.id, &tasks).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,7 +552,7 @@ async fn refresh_calendar(
|
|||||||
*guard = events;
|
*guard = events;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[task_app] Calendar fetch failed: {}", e);
|
log_msg(&format!("[task_app] Calendar fetch failed: {}", e));
|
||||||
*network_status.lock().await = NetworkStatus::Offline;
|
*network_status.lock().await = NetworkStatus::Offline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -477,8 +477,8 @@ pub fn render_date_picker(
|
|||||||
frame.render_widget(paragraph, popup_area);
|
frame.render_widget(paragraph, popup_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_confirm_popup(frame: &mut Frame, area: Rect) {
|
pub fn render_confirm_popup(frame: &mut Frame, area: Rect, context: &str) {
|
||||||
let popup_area = centered_rect(50, 5, area);
|
let popup_area = centered_rect(60, 6, area);
|
||||||
frame.render_widget(Clear, popup_area);
|
frame.render_widget(Clear, popup_area);
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
@@ -490,7 +490,7 @@ pub fn render_confirm_popup(frame: &mut Frame, area: Rect) {
|
|||||||
let text = Text::from(vec![
|
let text = Text::from(vec![
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
" Delete this item? ",
|
format!(" {} ", context),
|
||||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||||
)),
|
)),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ pub enum Popup {
|
|||||||
Input,
|
Input,
|
||||||
EditTask { field: usize },
|
EditTask { field: usize },
|
||||||
DatePicker,
|
DatePicker,
|
||||||
ConfirmDelete,
|
ConfirmDelete { context: String },
|
||||||
BulkAction,
|
BulkAction,
|
||||||
PickList,
|
PickList,
|
||||||
DeviceAuth { url: String, code: String },
|
DeviceAuth { url: String, code: String },
|
||||||
@@ -129,7 +129,7 @@ pub fn draw(frame: &mut Frame, view: AppView) {
|
|||||||
view.notes_scroll, *field,
|
view.notes_scroll, *field,
|
||||||
),
|
),
|
||||||
Popup::DatePicker => render_date_picker(frame, area, view.draft_date),
|
Popup::DatePicker => render_date_picker(frame, area, view.draft_date),
|
||||||
Popup::ConfirmDelete => render_confirm_popup(frame, area),
|
Popup::ConfirmDelete { context } => render_confirm_popup(frame, area, context),
|
||||||
Popup::BulkAction => render_bulk_action_popup(frame, area, view.selected_tasks.len(), view.bulk_action_selected),
|
Popup::BulkAction => render_bulk_action_popup(frame, area, view.selected_tasks.len(), view.bulk_action_selected),
|
||||||
Popup::PickList => render_pick_list_popup(frame, area, view.popup_list_indices, view.popup_list_selected),
|
Popup::PickList => render_pick_list_popup(frame, area, view.popup_list_indices, view.popup_list_selected),
|
||||||
Popup::DeviceAuth { url, code } => render_device_auth_popup(frame, area, url, code, view.auth_error),
|
Popup::DeviceAuth { url, code } => render_device_auth_popup(frame, area, url, code, view.auth_error),
|
||||||
|
|||||||
Reference in New Issue
Block a user