diff --git a/src/app.rs b/src/app.rs index c56f3e9..64ae730 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,9 +3,9 @@ use std::collections::HashMap; use gpui::prelude::*; use crate::models::{FilterState, ProjectTree}; -use crate::task::{self, TaskFilter, TaskOverview, TaskService}; +use crate::task::{self, TaskOverview, TaskService}; use crate::theme::ActiveTheme; -use crate::ui::{ROOT_PADDING, SECTION_GAP, card_style}; +use crate::ui::{ROOT_PADDING, SECTION_GAP, SIDEBAR_WIDTH, card_style}; use crate::view::sidebar::{Sidebar, TagItem}; use crate::view::status_bar::{StatusBar, StatusBarEvent, SyncState}; use crate::view::task_table::TaskTable; @@ -28,7 +28,7 @@ impl gpui::Render for App { let theme = cx.theme(); let sidebar = card_style(div(), theme) - .w(gpui::px(250.)) + .w(SIDEBAR_WIDTH) .h_full() .flex_shrink_0() .overflow_hidden() @@ -68,6 +68,10 @@ impl App { let mut tag_counts: HashMap = HashMap::new(); for task in tasks { + if !matches!(task.status, task::TaskStatus::Pending) { + continue; + } + if let Some(project) = &task.project { *project_counts.entry(project.clone()).or_insert(0) += 1; } @@ -91,16 +95,8 @@ impl App { (projects, tag_items) } - fn reload_tasks(&mut self, cx: &mut gpui::Context) { - let all_tasks = self.task_service.get_all_tasks().unwrap_or_else(|e| { - log::error!("[App] Failed to load tasks: {}", e); - vec![] - }); - - let filter_state = self.filter_state.read(cx).clone(); - let task_filter = TaskFilter::from(&filter_state); - let filtered_tasks = task_filter.apply(&all_tasks); - let (projects, tags) = Self::build_sidebar_data(&filtered_tasks); + fn update_ui_from_tasks(&mut self, all_tasks: Vec, cx: &mut gpui::Context) { + let (projects, tags) = Self::build_sidebar_data(&all_tasks); let mut project_tree = ProjectTree::new(); project_tree.build_from_projects(&projects); @@ -115,6 +111,24 @@ impl App { }); } + fn reload_tasks(&mut self, cx: &mut gpui::Context) { + match self.task_service.get_all_tasks() { + Ok(all_tasks) => { + self.status_bar.update(cx, |bar, cx| { + bar.clear_error(cx); + }); + self.update_ui_from_tasks(all_tasks, cx); + } + Err(e) => { + log::error!("[App] Failed to load tasks: {}", e); + self.status_bar.update(cx, |bar, cx| { + bar.set_error(format!("Failed to load tasks: {}", e), cx); + }); + self.update_ui_from_tasks(vec![], cx); + } + } + } + fn handle_sync(&mut self, cx: &mut gpui::Context) { self.status_bar.update(cx, |bar, cx| { bar.set_sync_state(SyncState::Syncing, cx); @@ -123,22 +137,7 @@ impl App { match self.task_service.get_all_tasks() { Ok(all_tasks) => { - let filter_state = self.filter_state.read(cx).clone(); - let task_filter = TaskFilter::from(&filter_state); - let filtered_tasks = task_filter.apply(&all_tasks); - let (projects, tags) = Self::build_sidebar_data(&filtered_tasks); - - let mut project_tree = ProjectTree::new(); - project_tree.build_from_projects(&projects); - - self.sidebar.update(cx, |sidebar, cx| { - sidebar.update_projects(project_tree, cx); - sidebar.update_tags(tags, cx); - }); - - self.task_table.update(cx, |table, cx| { - table.reload_tasks_from_all(all_tasks, cx); - }); + self.update_ui_from_tasks(all_tasks, cx); self.status_bar.update(cx, |bar, cx| { bar.set_sync_state(SyncState::Success, cx); diff --git a/src/models/filter_state.rs b/src/models/filter_state.rs index 9b96421..170a05e 100644 --- a/src/models/filter_state.rs +++ b/src/models/filter_state.rs @@ -2,6 +2,8 @@ use std::collections::HashSet; use chrono::NaiveDate; +use crate::ui::DATE_FORMAT; + #[derive(Debug, Clone, Default)] pub struct FilterState { pub selected_project: Option, @@ -147,7 +149,7 @@ impl DueFilter { Self::Today => "Today".to_string(), Self::ThisWeek => "This Week".to_string(), Self::NoDate => "No Date".to_string(), - Self::OnDate(date) => date.format("%d-%m-%Y").to_string(), + Self::OnDate(date) => date.format(DATE_FORMAT).to_string(), } } @@ -158,7 +160,7 @@ impl DueFilter { Self::Today => "today".to_string(), Self::ThisWeek => "this_week".to_string(), Self::NoDate => "none".to_string(), - Self::OnDate(date) => format!("date:{}", date.format("%Y-%m-%d")), + Self::OnDate(date) => format!("date:{}", date.format(DATE_FORMAT)), } } diff --git a/src/ui.rs b/src/ui.rs index 32daae4..46fd7a8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,5 @@ use gpui::prelude::*; -use gpui::{Pixels, px}; +use gpui::{Pixels, px, rems}; use crate::theme::Theme; @@ -11,6 +11,43 @@ pub const ROOT_PADDING: Pixels = px(12.0); pub const CONTROL_RADIUS: Pixels = px(6.0); pub const CONTROL_BORDER: Pixels = px(1.0); +pub const SIDEBAR_WIDTH: Pixels = px(250.0); + +pub const TABLE_MAX_DESCRIPTION_LENGTH: usize = 50; +pub const TABLE_FILTER_BAR_INITIAL_HEIGHT: Pixels = px(52.0); + +#[inline(always)] +pub fn table_col_id_width() -> gpui::Rems { + rems(3.0) +} + +#[inline(always)] +pub fn table_col_desc_min_width() -> gpui::Rems { + rems(10.0) +} + +#[inline(always)] +pub fn table_col_project_width() -> gpui::Rems { + rems(10.0) +} + +#[inline(always)] +pub fn table_col_due_width() -> gpui::Rems { + rems(7.0) +} + +#[inline(always)] +pub fn table_col_priority_width() -> gpui::Rems { + rems(5.0) +} + +#[inline(always)] +pub fn table_col_status_width() -> gpui::Rems { + rems(6.0) +} + +pub const DATE_FORMAT: &str = "%Y-%m-%d"; + pub fn card_style(div: gpui::Div, theme: &Theme) -> gpui::Div { div.bg(theme.card) .border_1() diff --git a/src/view/status_bar.rs b/src/view/status_bar.rs index d677418..2b52716 100644 --- a/src/view/status_bar.rs +++ b/src/view/status_bar.rs @@ -1,7 +1,7 @@ use gpui::{Context, IntoElement, MouseButton, Render, Window, div, prelude::*, rems}; use crate::components::label::Label; -use crate::theme::ActiveTheme; +use crate::theme::{ActiveTheme, Theme}; use crate::ui::divider_v; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -21,6 +21,7 @@ impl Default for SyncState { pub struct StatusBar { sync_state: SyncState, last_sync_message: String, + error_message: Option, } impl StatusBar { @@ -28,6 +29,7 @@ impl StatusBar { Self { sync_state: SyncState::default(), last_sync_message: String::new(), + error_message: None, } } @@ -47,6 +49,16 @@ impl StatusBar { cx.notify(); } + pub fn set_error(&mut self, message: String, cx: &mut Context) { + self.error_message = Some(message); + cx.notify(); + } + + pub fn clear_error(&mut self, cx: &mut Context) { + self.error_message = None; + cx.notify(); + } + fn sync_icon(&self) -> &'static str { match self.sync_state { SyncState::Idle => "↻", @@ -98,7 +110,52 @@ impl Render for StatusBar { Label::new("") }; - div() + let error_banner = if let Some(ref error) = self.error_message { + Some( + div() + .flex() + .items_center() + .justify_between() + .w_full() + .px_3() + .py_2() + .bg(Theme::alpha(theme.error, 0.12)) + .border_1() + .border_color(theme.error) + .rounded_md() + .child( + div() + .flex() + .items_center() + .gap_2() + .child(Label::new("✕").text_color(theme.error).text_sm()) + .child( + Label::new(error.clone()) + .text_color(theme.error) + .text_sm(), + ), + ) + .child( + div() + .px_2() + .py_1() + .rounded_sm() + .cursor_pointer() + .hover(|s| s.bg(Theme::alpha(theme.error, 0.2))) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _event, _window, cx| { + this.clear_error(cx); + }), + ) + .child(Label::new("✕").text_color(theme.error).text_sm()), + ), + ) + } else { + None + }; + + let status_bar_content = div() .flex() .items_center() .justify_between() @@ -124,7 +181,20 @@ impl Render for StatusBar { .gap_2() .child(divider_v(&theme).h(rems(1.0))) .child(sync_button), - ) + ); + + if let Some(error) = error_banner { + div() + .flex() + .flex_col() + .w_full() + .gap_2() + .p_2() + .child(error) + .child(status_bar_content) + } else { + status_bar_content + } } } diff --git a/src/view/task_table.rs b/src/view/task_table.rs index f513035..725fe25 100644 --- a/src/view/task_table.rs +++ b/src/view/task_table.rs @@ -13,7 +13,11 @@ use crate::{ models::{DueFilter, FilterState, PriorityFilter, StatusFilter}, task::{self, TaskFilter, TaskService}, theme::{self, ActiveTheme}, - ui::priority_badge, + ui::{ + priority_badge, table_col_desc_min_width, table_col_due_width, table_col_id_width, + table_col_priority_width, table_col_project_width, table_col_status_width, + DATE_FORMAT, TABLE_FILTER_BAR_INITIAL_HEIGHT, TABLE_MAX_DESCRIPTION_LENGTH, + }, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -183,7 +187,7 @@ impl TaskRow { if is_today { "Today".to_string() } else { - dt.format("%d-%m-%Y").to_string() + dt.format(DATE_FORMAT).to_string() } } } @@ -201,7 +205,7 @@ impl From<&task::Task> for TaskRow { Self { uuid: value.uuid, id_display: value.working_id.unwrap_or(0).to_string(), - description: Self::truncate(&value.description, 50), + description: Self::truncate(&value.description, TABLE_MAX_DESCRIPTION_LENGTH), project: value.project.clone().unwrap_or(String::new()), due: Self::format_date(&value.due, value.is_due_today()), priority: value.priority.into(), @@ -320,7 +324,7 @@ impl TaskTable { selected_page_idx: None, selected_global_idx: None, need_reload: true, - filter_bar_height: gpui::px(52.0), + filter_bar_height: TABLE_FILTER_BAR_INITIAL_HEIGHT, search_input, status_dropdown, priority_dropdown, @@ -515,7 +519,7 @@ impl TaskTable { continue; } let label = Self::format_due_label(date); - let value = format!("date:{}", date.format("%Y-%m-%d")); + let value = format!("date:{}", date.format(DATE_FORMAT)); items.push(DropdownItem::with_value(label, value)); } @@ -538,7 +542,7 @@ impl TaskTable { if date == today { "Today".to_string() } else { - date.format("%d-%m-%Y").to_string() + date.format(DATE_FORMAT).to_string() } } @@ -729,7 +733,7 @@ impl TaskTable { .font_weight(gpui::FontWeight::MEDIUM) .child( gpui::div() - .min_w(gpui::rems(3.0)) + .min_w(table_col_id_width()) .flex() .items_center() .gap_1() @@ -739,22 +743,22 @@ impl TaskTable { .child( gpui::div() .flex_1() - .min_w(gpui::rems(10.0)) + .min_w(table_col_desc_min_width()) .child(self.render_header_column(SortColumn::Description, "header-desc", cx)), ) .child( gpui::div() - .w(gpui::rems(10.0)) + .w(table_col_project_width()) .child(self.render_header_column(SortColumn::Project, "header-project", cx)), ) .child( gpui::div() - .w(gpui::rems(7.0)) + .w(table_col_due_width()) .child(self.render_header_column(SortColumn::Due, "header-due", cx)), ) .child( gpui::div() - .w(gpui::rems(5.0)) + .w(table_col_priority_width()) .child(self.render_header_column(SortColumn::Priority, "header-priority", cx)), ) .child( @@ -788,7 +792,7 @@ impl TaskTable { ) .child( gpui::div() - .min_w(gpui::rems(3.0)) + .min_w(table_col_id_width()) .flex() .items_center() .gap_1() @@ -801,7 +805,7 @@ impl TaskTable { .child( gpui::div() .flex_1() - .min_w(gpui::rems(10.0)) + .min_w(table_col_desc_min_width()) .overflow_x_hidden() .child( components::label::Label::new(row.description.clone()) @@ -822,11 +826,11 @@ impl TaskTable { )) .child( gpui::div() - .w(gpui::rems(5.0)) + .w(table_col_priority_width()) .child(priority_badge(&row.priority, theme)), ) .child( - gpui::div().w(gpui::rems(6.0)).child( + gpui::div().w(table_col_status_width()).child( components::label::Label::new(row.status.clone()) .text_color(self.status_color(row, cx)), ),