2026-06-20 19:35:19 +01:00
|
|
|
pub mod components;
|
2026-06-20 19:37:13 +01:00
|
|
|
|
|
|
|
|
use ratatui::layout::{Constraint, Direction, Layout};
|
|
|
|
|
use ratatui::Frame;
|
|
|
|
|
|
2026-06-21 14:21:14 +01:00
|
|
|
use crate::app::SyncStats;
|
2026-06-20 19:37:13 +01:00
|
|
|
use crate::domain::models::*;
|
|
|
|
|
use components::*;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum Focus {
|
|
|
|
|
Tabs,
|
|
|
|
|
TaskList,
|
|
|
|
|
Detail,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum Popup {
|
|
|
|
|
Input,
|
|
|
|
|
DatePicker,
|
|
|
|
|
ConfirmDelete,
|
|
|
|
|
DeviceAuth { url: String, code: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum NetworkStatus {
|
|
|
|
|
Online,
|
|
|
|
|
Offline,
|
|
|
|
|
Syncing,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct AppView<'a> {
|
|
|
|
|
pub lists: &'a [TaskList],
|
|
|
|
|
pub tasks: &'a [Task],
|
|
|
|
|
pub selected_list: usize,
|
|
|
|
|
pub selected_task: usize,
|
|
|
|
|
pub focus: Focus,
|
|
|
|
|
pub show_popup: Option<&'a Popup>,
|
|
|
|
|
pub popup_input: &'a str,
|
|
|
|
|
pub popup_cursor: usize,
|
|
|
|
|
pub draft_date: chrono::NaiveDateTime,
|
|
|
|
|
pub network_status: &'a NetworkStatus,
|
|
|
|
|
pub task_list_scroll: u16,
|
|
|
|
|
pub detail_scroll: u16,
|
2026-06-20 19:56:41 +01:00
|
|
|
pub auth_error: Option<&'a str>,
|
2026-06-21 14:21:14 +01:00
|
|
|
pub sync_stats: &'a SyncStats,
|
2026-06-20 19:37:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn draw(frame: &mut Frame, view: AppView) {
|
|
|
|
|
let area = frame.area();
|
|
|
|
|
|
|
|
|
|
let main_layout = Layout::default()
|
|
|
|
|
.direction(Direction::Vertical)
|
|
|
|
|
.constraints([
|
|
|
|
|
Constraint::Length(3),
|
|
|
|
|
Constraint::Min(0),
|
|
|
|
|
Constraint::Length(1),
|
|
|
|
|
])
|
|
|
|
|
.split(area);
|
|
|
|
|
|
|
|
|
|
let tabs_area = main_layout[0];
|
|
|
|
|
let body_area = main_layout[1];
|
|
|
|
|
let status_area = main_layout[2];
|
|
|
|
|
|
|
|
|
|
let is_tabs_focused = view.focus == Focus::Tabs;
|
|
|
|
|
render_tabs_bar(frame, tabs_area, view.lists, view.selected_list, is_tabs_focused);
|
|
|
|
|
|
|
|
|
|
let body_layout = Layout::default()
|
|
|
|
|
.direction(Direction::Horizontal)
|
|
|
|
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
|
|
|
.split(body_area);
|
|
|
|
|
|
|
|
|
|
let is_task_list_focused = view.focus == Focus::TaskList;
|
|
|
|
|
render_task_list(
|
|
|
|
|
frame,
|
|
|
|
|
body_layout[0],
|
|
|
|
|
view.tasks,
|
|
|
|
|
view.selected_task,
|
|
|
|
|
is_task_list_focused,
|
|
|
|
|
view.task_list_scroll,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let is_detail_focused = view.focus == Focus::Detail;
|
|
|
|
|
render_detail(
|
|
|
|
|
frame,
|
|
|
|
|
body_layout[1],
|
|
|
|
|
view.tasks.get(view.selected_task),
|
|
|
|
|
is_detail_focused,
|
|
|
|
|
view.detail_scroll,
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-21 14:21:14 +01:00
|
|
|
render_status_bar(frame, status_area, view.network_status, view.sync_stats);
|
2026-06-20 19:37:13 +01:00
|
|
|
|
|
|
|
|
if let Some(popup) = view.show_popup {
|
|
|
|
|
match popup {
|
|
|
|
|
Popup::Input => render_input_popup(frame, area, view.popup_input, view.popup_cursor),
|
|
|
|
|
Popup::DatePicker => render_date_picker(frame, area, view.draft_date),
|
|
|
|
|
Popup::ConfirmDelete => render_confirm_popup(frame, area),
|
2026-06-20 19:56:41 +01:00
|
|
|
Popup::DeviceAuth { url, code } => render_device_auth_popup(frame, area, url, code, view.auth_error),
|
2026-06-20 19:37:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|