chore: checkpoint before removing POPUP_BG
This commit is contained in:
+247
-31
@@ -2,6 +2,7 @@ use std::sync::mpsc as std_mpsc;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use chrono::NaiveTime;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ pub struct App {
|
|||||||
pub network_status: NetworkStatus,
|
pub network_status: NetworkStatus,
|
||||||
pub popup_input: String,
|
pub popup_input: String,
|
||||||
pub popup_cursor: usize,
|
pub popup_cursor: usize,
|
||||||
|
pub popup_secondary: String,
|
||||||
|
pub popup_secondary_cursor: usize,
|
||||||
|
pub edit_field: usize,
|
||||||
pub draft_date: NaiveDateTime,
|
pub draft_date: NaiveDateTime,
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
pub task_list_scroll: u16,
|
pub task_list_scroll: u16,
|
||||||
@@ -40,6 +44,8 @@ pub struct App {
|
|||||||
pub auth_error: Option<String>,
|
pub auth_error: Option<String>,
|
||||||
pub sync_stats: SyncStats,
|
pub sync_stats: SyncStats,
|
||||||
last_sync_version: u64,
|
last_sync_version: u64,
|
||||||
|
editing_task_id: Option<String>,
|
||||||
|
pending_date_key: bool,
|
||||||
auth_tx: std_mpsc::Sender<AuthEvent>,
|
auth_tx: std_mpsc::Sender<AuthEvent>,
|
||||||
auth_rx: std_mpsc::Receiver<AuthEvent>,
|
auth_rx: std_mpsc::Receiver<AuthEvent>,
|
||||||
sync_tx: mpsc::Sender<SyncCommand>,
|
sync_tx: mpsc::Sender<SyncCommand>,
|
||||||
@@ -90,6 +96,9 @@ impl App {
|
|||||||
network_status: NetworkStatus::Online,
|
network_status: NetworkStatus::Online,
|
||||||
popup_input: String::new(),
|
popup_input: String::new(),
|
||||||
popup_cursor: 0,
|
popup_cursor: 0,
|
||||||
|
popup_secondary: String::new(),
|
||||||
|
popup_secondary_cursor: 0,
|
||||||
|
edit_field: 0,
|
||||||
draft_date: chrono::Local::now().naive_local(),
|
draft_date: chrono::Local::now().naive_local(),
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
task_list_scroll: 0,
|
task_list_scroll: 0,
|
||||||
@@ -100,6 +109,8 @@ impl App {
|
|||||||
auth_error: None,
|
auth_error: None,
|
||||||
sync_stats: SyncStats::default(),
|
sync_stats: SyncStats::default(),
|
||||||
last_sync_version: 0,
|
last_sync_version: 0,
|
||||||
|
editing_task_id: None,
|
||||||
|
pending_date_key: false,
|
||||||
auth_tx,
|
auth_tx,
|
||||||
auth_rx,
|
auth_rx,
|
||||||
sync_tx,
|
sync_tx,
|
||||||
@@ -181,12 +192,68 @@ impl App {
|
|||||||
let _ = self.sync_tx.try_send(SyncCommand::FullSync);
|
let _ = self.sync_tx.try_send(SyncCommand::FullSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_task_due(&mut self, due: chrono::NaiveDateTime) {
|
||||||
|
if self.tasks.is_empty() || self.selected_task >= self.tasks.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let task = &mut self.tasks[self.selected_task];
|
||||||
|
task.due = Some(due);
|
||||||
|
self.db.update_task(task).ok();
|
||||||
|
self.db.push_sync(
|
||||||
|
SyncAction::Update,
|
||||||
|
&task.id,
|
||||||
|
&task.list_id,
|
||||||
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
|
).ok();
|
||||||
|
self.trigger_sync();
|
||||||
|
self.load_tasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_due_today(&mut self) {
|
||||||
|
self.update_task_due(chrono::Local::now().naive_local());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_due_tomorrow_9am(&mut self) {
|
||||||
|
let tomorrow = chrono::Local::now().naive_local() + chrono::Duration::days(1);
|
||||||
|
let time = NaiveTime::from_hms_opt(9, 0, 0).unwrap();
|
||||||
|
let due = chrono::NaiveDateTime::new(tomorrow.date(), time);
|
||||||
|
self.update_task_due(due);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_due_next_week_9am(&mut self) {
|
||||||
|
let next_week = chrono::Local::now().naive_local() + chrono::Duration::days(7);
|
||||||
|
let time = NaiveTime::from_hms_opt(9, 0, 0).unwrap();
|
||||||
|
let due = chrono::NaiveDateTime::new(next_week.date(), time);
|
||||||
|
self.update_task_due(due);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_due_next_month_9am(&mut self) {
|
||||||
|
let next_month = chrono::Local::now().naive_local() + chrono::Duration::days(30);
|
||||||
|
let time = NaiveTime::from_hms_opt(9, 0, 0).unwrap();
|
||||||
|
let due = chrono::NaiveDateTime::new(next_month.date(), time);
|
||||||
|
self.update_task_due(due);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_key(&mut self, key: KeyEvent) {
|
pub fn handle_key(&mut self, key: KeyEvent) {
|
||||||
if let Some(ref popup) = self.show_popup.clone() {
|
if let Some(ref popup) = self.show_popup.clone() {
|
||||||
self.handle_popup_key(key, popup);
|
self.handle_popup_key(key, popup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.pending_date_key {
|
||||||
|
self.pending_date_key = false;
|
||||||
|
if self.focus == Focus::TaskList && !self.tasks.is_empty() {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('d') => self.set_due_today(),
|
||||||
|
KeyCode::Char('t') => self.set_due_tomorrow_9am(),
|
||||||
|
KeyCode::Char('w') => self.set_due_next_week_9am(),
|
||||||
|
KeyCode::Char('m') => self.set_due_next_month_9am(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if key.code == KeyCode::Right && key.modifiers.contains(KeyModifiers::CONTROL) {
|
if key.code == KeyCode::Right && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
if !self.lists.is_empty() && self.selected_list + 1 < self.lists.len() {
|
if !self.lists.is_empty() && self.selected_list + 1 < self.lists.len() {
|
||||||
self.selected_list += 1;
|
self.selected_list += 1;
|
||||||
@@ -263,9 +330,20 @@ impl App {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('n') | KeyCode::Char('N') => {
|
KeyCode::Char('n') | KeyCode::Char('N') => {
|
||||||
if !self.needs_auth {
|
if !self.needs_auth {
|
||||||
|
if self.focus == Focus::Tabs {
|
||||||
|
self.editing_task_id = None;
|
||||||
self.popup_input.clear();
|
self.popup_input.clear();
|
||||||
self.popup_cursor = 0;
|
self.popup_cursor = 0;
|
||||||
self.show_popup = Some(Popup::Input);
|
self.show_popup = Some(Popup::Input);
|
||||||
|
} else if !self.lists.is_empty() {
|
||||||
|
self.editing_task_id = None;
|
||||||
|
self.popup_input.clear();
|
||||||
|
self.popup_cursor = 0;
|
||||||
|
self.popup_secondary.clear();
|
||||||
|
self.popup_secondary_cursor = 0;
|
||||||
|
self.edit_field = 0;
|
||||||
|
self.show_popup = Some(Popup::EditTask { field: 0 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('d') | KeyCode::Char('D') => {
|
KeyCode::Char('d') | KeyCode::Char('D') => {
|
||||||
@@ -276,14 +354,38 @@ impl App {
|
|||||||
KeyCode::Char('e') | KeyCode::Char('E') => {
|
KeyCode::Char('e') | KeyCode::Char('E') => {
|
||||||
if !self.needs_auth && self.focus == Focus::TaskList && !self.tasks.is_empty() {
|
if !self.needs_auth && self.focus == Focus::TaskList && !self.tasks.is_empty() {
|
||||||
let task = &self.tasks[self.selected_task];
|
let task = &self.tasks[self.selected_task];
|
||||||
|
self.editing_task_id = Some(task.id.clone());
|
||||||
self.popup_input = task.title.clone();
|
self.popup_input = task.title.clone();
|
||||||
self.popup_cursor = task.title.len();
|
self.popup_cursor = task.title.len();
|
||||||
self.show_popup = Some(Popup::Input);
|
self.popup_secondary = task.notes.clone().unwrap_or_default();
|
||||||
|
self.popup_secondary_cursor = self.popup_secondary.len();
|
||||||
|
self.edit_field = 0;
|
||||||
|
self.show_popup = Some(Popup::EditTask { field: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('t') | KeyCode::Char('T') => {
|
||||||
|
if self.focus == Focus::TaskList && !self.tasks.is_empty() {
|
||||||
|
self.pending_date_key = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if self.focus == Focus::Detail && !self.tasks.is_empty() {
|
if self.focus == Focus::Detail && !self.tasks.is_empty() {
|
||||||
self.show_popup = Some(Popup::DatePicker);
|
self.show_popup = Some(Popup::DatePicker);
|
||||||
|
} else if self.focus == Focus::TaskList && !self.tasks.is_empty() {
|
||||||
|
let task = &mut self.tasks[self.selected_task];
|
||||||
|
task.status = match task.status {
|
||||||
|
TaskStatus::Completed => TaskStatus::NeedsAction,
|
||||||
|
TaskStatus::NeedsAction => TaskStatus::Completed,
|
||||||
|
};
|
||||||
|
self.db.update_task(task).ok();
|
||||||
|
self.db.push_sync(
|
||||||
|
SyncAction::Update,
|
||||||
|
&task.id,
|
||||||
|
&task.list_id,
|
||||||
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
|
).ok();
|
||||||
|
self.trigger_sync();
|
||||||
|
self.load_tasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
@@ -330,8 +432,7 @@ impl App {
|
|||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let input = self.popup_input.trim().to_string();
|
let input = self.popup_input.trim().to_string();
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
match self.focus {
|
if self.focus == Focus::Tabs {
|
||||||
Focus::Tabs => {
|
|
||||||
let list = TaskList {
|
let list = TaskList {
|
||||||
id: uuid_v4(),
|
id: uuid_v4(),
|
||||||
title: input,
|
title: input,
|
||||||
@@ -346,14 +447,93 @@ impl App {
|
|||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_lists();
|
self.load_lists();
|
||||||
}
|
}
|
||||||
Focus::TaskList => {
|
}
|
||||||
if !self.lists.is_empty() {
|
self.show_popup = None;
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
self.popup_input.insert(self.popup_cursor, c);
|
||||||
|
self.popup_cursor += c.len_utf8();
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
if self.popup_cursor > 0 {
|
||||||
|
let before = self.popup_input.floor_char_boundary(self.popup_cursor - 1);
|
||||||
|
self.popup_input.replace_range(before..self.popup_cursor, "");
|
||||||
|
self.popup_cursor = before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Delete => {
|
||||||
|
if self.popup_cursor < self.popup_input.len() {
|
||||||
|
let s = &self.popup_input[self.popup_cursor..];
|
||||||
|
if let Some(c) = s.chars().next() {
|
||||||
|
self.popup_input.replace_range(self.popup_cursor..self.popup_cursor + c.len_utf8(), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
if self.popup_cursor > 0 {
|
||||||
|
self.popup_cursor = self.popup_input.floor_char_boundary(self.popup_cursor - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
if self.popup_cursor < self.popup_input.len() {
|
||||||
|
let s = &self.popup_input[self.popup_cursor..];
|
||||||
|
if let Some(c) = s.chars().next() {
|
||||||
|
self.popup_cursor += c.len_utf8();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Home => {
|
||||||
|
self.popup_cursor = 0;
|
||||||
|
}
|
||||||
|
KeyCode::End => {
|
||||||
|
self.popup_cursor = self.popup_input.len();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Popup::EditTask { field } => match key.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.editing_task_id = None;
|
||||||
|
self.show_popup = None;
|
||||||
|
}
|
||||||
|
KeyCode::Tab | KeyCode::Down => {
|
||||||
|
let new_field = (field + 1) % 2;
|
||||||
|
self.edit_field = new_field;
|
||||||
|
self.show_popup = Some(Popup::EditTask { field: new_field });
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
let new_field = (field + 1) % 2;
|
||||||
|
self.edit_field = new_field;
|
||||||
|
self.show_popup = Some(Popup::EditTask { field: new_field });
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let title = self.popup_input.trim().to_string();
|
||||||
|
if title.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let notes = self.popup_secondary.trim().to_string();
|
||||||
|
let notes_opt = if notes.is_empty() { None } else { Some(notes) };
|
||||||
|
|
||||||
|
if let Some(task_id) = self.editing_task_id.take() {
|
||||||
|
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == task_id) {
|
||||||
|
task.title = title;
|
||||||
|
task.notes = notes_opt;
|
||||||
|
self.db.update_task(task).ok();
|
||||||
|
self.db.push_sync(
|
||||||
|
SyncAction::Update,
|
||||||
|
&task.id,
|
||||||
|
&task.list_id,
|
||||||
|
&serde_json::to_string(task).unwrap_or_default(),
|
||||||
|
).ok();
|
||||||
|
self.trigger_sync();
|
||||||
|
self.load_tasks();
|
||||||
|
}
|
||||||
|
} else if !self.lists.is_empty() {
|
||||||
let list_id = &self.lists[self.selected_list].id;
|
let list_id = &self.lists[self.selected_list].id;
|
||||||
let task = Task {
|
let task = Task {
|
||||||
id: uuid_v4(),
|
id: uuid_v4(),
|
||||||
list_id: list_id.clone(),
|
list_id: list_id.clone(),
|
||||||
title: input,
|
title,
|
||||||
notes: None,
|
notes: notes_opt,
|
||||||
status: TaskStatus::NeedsAction,
|
status: TaskStatus::NeedsAction,
|
||||||
due: None,
|
due: None,
|
||||||
position: 0,
|
position: 0,
|
||||||
@@ -368,54 +548,90 @@ impl App {
|
|||||||
self.trigger_sync();
|
self.trigger_sync();
|
||||||
self.load_tasks();
|
self.load_tasks();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Focus::Detail => {
|
|
||||||
if !self.tasks.is_empty() {
|
|
||||||
let task = &mut self.tasks[self.selected_task];
|
|
||||||
task.title = input;
|
|
||||||
self.db.update_task(task).ok();
|
|
||||||
self.db.push_sync(
|
|
||||||
SyncAction::Update,
|
|
||||||
&task.id,
|
|
||||||
&task.list_id,
|
|
||||||
&serde_json::to_string(task).unwrap_or_default(),
|
|
||||||
).ok();
|
|
||||||
self.trigger_sync();
|
|
||||||
self.load_tasks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.show_popup = None;
|
self.show_popup = None;
|
||||||
}
|
}
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
|
if *field == 0 {
|
||||||
self.popup_input.insert(self.popup_cursor, c);
|
self.popup_input.insert(self.popup_cursor, c);
|
||||||
self.popup_cursor += 1;
|
self.popup_cursor += c.len_utf8();
|
||||||
|
} else {
|
||||||
|
self.popup_secondary.insert(self.popup_secondary_cursor, c);
|
||||||
|
self.popup_secondary_cursor += c.len_utf8();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
|
if *field == 0 {
|
||||||
if self.popup_cursor > 0 {
|
if self.popup_cursor > 0 {
|
||||||
self.popup_cursor -= 1;
|
let before = self.popup_input.floor_char_boundary(self.popup_cursor - 1);
|
||||||
self.popup_input.remove(self.popup_cursor);
|
self.popup_input.replace_range(before..self.popup_cursor, "");
|
||||||
|
self.popup_cursor = before;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.popup_secondary_cursor > 0 {
|
||||||
|
let before = self.popup_secondary.floor_char_boundary(self.popup_secondary_cursor - 1);
|
||||||
|
self.popup_secondary.replace_range(before..self.popup_secondary_cursor, "");
|
||||||
|
self.popup_secondary_cursor = before;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Delete => {
|
KeyCode::Delete => {
|
||||||
|
if *field == 0 {
|
||||||
if self.popup_cursor < self.popup_input.len() {
|
if self.popup_cursor < self.popup_input.len() {
|
||||||
self.popup_input.remove(self.popup_cursor);
|
let s = &self.popup_input[self.popup_cursor..];
|
||||||
|
if let Some(c) = s.chars().next() {
|
||||||
|
self.popup_input.replace_range(self.popup_cursor..self.popup_cursor + c.len_utf8(), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.popup_secondary_cursor < self.popup_secondary.len() {
|
||||||
|
let s = &self.popup_secondary[self.popup_secondary_cursor..];
|
||||||
|
if let Some(c) = s.chars().next() {
|
||||||
|
self.popup_secondary.replace_range(self.popup_secondary_cursor..self.popup_secondary_cursor + c.len_utf8(), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
self.popup_cursor = self.popup_cursor.saturating_sub(1);
|
if *field == 0 {
|
||||||
|
if self.popup_cursor > 0 {
|
||||||
|
self.popup_cursor = self.popup_input.floor_char_boundary(self.popup_cursor - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.popup_secondary_cursor > 0 {
|
||||||
|
self.popup_secondary_cursor = self.popup_secondary.floor_char_boundary(self.popup_secondary_cursor - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
|
if *field == 0 {
|
||||||
if self.popup_cursor < self.popup_input.len() {
|
if self.popup_cursor < self.popup_input.len() {
|
||||||
self.popup_cursor += 1;
|
let s = &self.popup_input[self.popup_cursor..];
|
||||||
|
if let Some(c) = s.chars().next() {
|
||||||
|
self.popup_cursor += c.len_utf8();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.popup_secondary_cursor < self.popup_secondary.len() {
|
||||||
|
let s = &self.popup_secondary[self.popup_secondary_cursor..];
|
||||||
|
if let Some(c) = s.chars().next() {
|
||||||
|
self.popup_secondary_cursor += c.len_utf8();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Home => {
|
KeyCode::Home => {
|
||||||
|
if *field == 0 {
|
||||||
self.popup_cursor = 0;
|
self.popup_cursor = 0;
|
||||||
|
} else {
|
||||||
|
self.popup_secondary_cursor = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::End => {
|
KeyCode::End => {
|
||||||
|
if *field == 0 {
|
||||||
self.popup_cursor = self.popup_input.len();
|
self.popup_cursor = self.popup_input.len();
|
||||||
|
} else {
|
||||||
|
self.popup_secondary_cursor = self.popup_secondary.len();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ fn main() -> io::Result<()> {
|
|||||||
show_popup: app.show_popup.as_ref(),
|
show_popup: app.show_popup.as_ref(),
|
||||||
popup_input: &app.popup_input,
|
popup_input: &app.popup_input,
|
||||||
popup_cursor: app.popup_cursor,
|
popup_cursor: app.popup_cursor,
|
||||||
|
popup_secondary: &app.popup_secondary,
|
||||||
|
popup_secondary_cursor: app.popup_secondary_cursor,
|
||||||
draft_date: app.draft_date,
|
draft_date: app.draft_date,
|
||||||
network_status: &app.network_status,
|
network_status: &app.network_status,
|
||||||
task_list_scroll: app.task_list_scroll,
|
task_list_scroll: app.task_list_scroll,
|
||||||
|
|||||||
+99
-5
@@ -1,7 +1,7 @@
|
|||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span, Text};
|
use ratatui::text::{Line, Span, Text};
|
||||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Tabs};
|
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Tabs};
|
||||||
use ratatui::layout::{Alignment, Rect};
|
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::domain::models::*;
|
use crate::domain::models::*;
|
||||||
@@ -238,7 +238,8 @@ pub fn render_input_popup(
|
|||||||
input: &str,
|
input: &str,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
) {
|
) {
|
||||||
let popup_area = centered_rect(60, 3, area);
|
let popup_area = centered_rect(75, 3, area);
|
||||||
|
frame.render_widget(Clear, popup_area);
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().bg(POPUP_BG))
|
.style(Style::default().bg(POPUP_BG))
|
||||||
@@ -256,12 +257,103 @@ pub fn render_input_popup(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_edit_task_popup(
|
||||||
|
frame: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
title: &str,
|
||||||
|
title_cursor: usize,
|
||||||
|
notes: &str,
|
||||||
|
notes_cursor: usize,
|
||||||
|
active_field: usize,
|
||||||
|
) {
|
||||||
|
let popup_area = centered_rect(75, 10, area);
|
||||||
|
|
||||||
|
// Clear the area first to prevent style/symbol bleed from previously rendered widgets
|
||||||
|
frame.render_widget(Clear, popup_area);
|
||||||
|
|
||||||
|
let outer_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().bg(POPUP_BG))
|
||||||
|
.border_style(Style::default().fg(POPUP_BORDER))
|
||||||
|
.title(" Edit Task ");
|
||||||
|
let inner_area = outer_block.inner(popup_area);
|
||||||
|
|
||||||
|
// Render outer block with borders and background
|
||||||
|
let outer_para = Paragraph::new(Text::raw(""))
|
||||||
|
.style(Style::default().bg(POPUP_BG))
|
||||||
|
.block(outer_block);
|
||||||
|
frame.render_widget(outer_para, popup_area);
|
||||||
|
|
||||||
|
// Split inner area into rows
|
||||||
|
let rows = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.split(inner_area);
|
||||||
|
|
||||||
|
// ── Title block ──
|
||||||
|
let title_style = if active_field == 0 {
|
||||||
|
Style::default().fg(FOCUS_COLOR).bg(POPUP_BG)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::DarkGray).bg(POPUP_BG)
|
||||||
|
};
|
||||||
|
let title_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().bg(POPUP_BG))
|
||||||
|
.border_style(title_style)
|
||||||
|
.title(" Title ")
|
||||||
|
.title_alignment(Alignment::Left);
|
||||||
|
let title_para = Paragraph::new(Text::from(Line::from(Span::raw(title))))
|
||||||
|
.style(Style::default().bg(POPUP_BG))
|
||||||
|
.block(title_block);
|
||||||
|
frame.render_widget(title_para, rows[0]);
|
||||||
|
|
||||||
|
// ── Notes block ──
|
||||||
|
let notes_style = if active_field == 1 {
|
||||||
|
Style::default().fg(FOCUS_COLOR).bg(POPUP_BG)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::DarkGray).bg(POPUP_BG)
|
||||||
|
};
|
||||||
|
let notes_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().bg(POPUP_BG))
|
||||||
|
.border_style(notes_style)
|
||||||
|
.title(" Notes ")
|
||||||
|
.title_alignment(Alignment::Left);
|
||||||
|
let notes_para = Paragraph::new(Text::from(Line::from(Span::raw(notes))))
|
||||||
|
.style(Style::default().bg(POPUP_BG))
|
||||||
|
.block(notes_block);
|
||||||
|
frame.render_widget(notes_para, rows[2]);
|
||||||
|
|
||||||
|
// ── Hint row ──
|
||||||
|
let hint = Paragraph::new(Line::from(Span::styled(
|
||||||
|
" Tab:switch field Enter:save Esc:cancel ",
|
||||||
|
Style::default().fg(Color::Gray),
|
||||||
|
)))
|
||||||
|
.style(Style::default().bg(POPUP_BG))
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
frame.render_widget(hint, rows[3]);
|
||||||
|
|
||||||
|
// ── Cursor ──
|
||||||
|
let (cursor_x, cursor_y) = if active_field == 0 {
|
||||||
|
(rows[0].x + 1 + title_cursor as u16, rows[0].y + 1)
|
||||||
|
} else {
|
||||||
|
(rows[2].x + 1 + notes_cursor as u16, rows[2].y + 1)
|
||||||
|
};
|
||||||
|
frame.set_cursor_position(ratatui::layout::Position::new(cursor_x, cursor_y));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_date_picker(
|
pub fn render_date_picker(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
date: chrono::NaiveDateTime,
|
date: chrono::NaiveDateTime,
|
||||||
) {
|
) {
|
||||||
let popup_area = centered_rect(50, 7, area);
|
let popup_area = centered_rect(60, 7, area);
|
||||||
|
frame.render_widget(Clear, popup_area);
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().bg(POPUP_BG))
|
.style(Style::default().bg(POPUP_BG))
|
||||||
@@ -292,7 +384,8 @@ pub fn render_date_picker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_confirm_popup(frame: &mut Frame, area: Rect) {
|
pub fn render_confirm_popup(frame: &mut Frame, area: Rect) {
|
||||||
let popup_area = centered_rect(40, 5, area);
|
let popup_area = centered_rect(50, 5, area);
|
||||||
|
frame.render_widget(Clear, popup_area);
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().bg(POPUP_BG))
|
.style(Style::default().bg(POPUP_BG))
|
||||||
@@ -328,6 +421,7 @@ pub fn render_device_auth_popup(
|
|||||||
error: Option<&str>,
|
error: Option<&str>,
|
||||||
) {
|
) {
|
||||||
let popup_area = centered_rect(80, 13, area);
|
let popup_area = centered_rect(80, 13, area);
|
||||||
|
frame.render_widget(Clear, popup_area);
|
||||||
|
|
||||||
let border_color = if error.is_some() {
|
let border_color = if error.is_some() {
|
||||||
Color::Red
|
Color::Red
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub enum Focus {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Popup {
|
pub enum Popup {
|
||||||
Input,
|
Input,
|
||||||
|
EditTask { field: usize },
|
||||||
DatePicker,
|
DatePicker,
|
||||||
ConfirmDelete,
|
ConfirmDelete,
|
||||||
DeviceAuth { url: String, code: String },
|
DeviceAuth { url: String, code: String },
|
||||||
@@ -38,6 +39,8 @@ pub struct AppView<'a> {
|
|||||||
pub show_popup: Option<&'a Popup>,
|
pub show_popup: Option<&'a Popup>,
|
||||||
pub popup_input: &'a str,
|
pub popup_input: &'a str,
|
||||||
pub popup_cursor: usize,
|
pub popup_cursor: usize,
|
||||||
|
pub popup_secondary: &'a str,
|
||||||
|
pub popup_secondary_cursor: usize,
|
||||||
pub draft_date: chrono::NaiveDateTime,
|
pub draft_date: chrono::NaiveDateTime,
|
||||||
pub network_status: &'a NetworkStatus,
|
pub network_status: &'a NetworkStatus,
|
||||||
pub task_list_scroll: u16,
|
pub task_list_scroll: u16,
|
||||||
@@ -94,6 +97,10 @@ pub fn draw(frame: &mut Frame, view: AppView) {
|
|||||||
if let Some(popup) = view.show_popup {
|
if let Some(popup) = view.show_popup {
|
||||||
match popup {
|
match popup {
|
||||||
Popup::Input => render_input_popup(frame, area, view.popup_input, view.popup_cursor),
|
Popup::Input => render_input_popup(frame, area, view.popup_input, view.popup_cursor),
|
||||||
|
Popup::EditTask { field } => render_edit_task_popup(
|
||||||
|
frame, area, view.popup_input, view.popup_cursor,
|
||||||
|
view.popup_secondary, view.popup_secondary_cursor, *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 => render_confirm_popup(frame, area),
|
||||||
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