Redesign UI, project stats panel, show project name, fixes and refactorings

This commit is contained in:
Joseph Ferano 2023-06-16 15:14:39 +07:00
parent a98b82f3e4
commit d23f458444
3 changed files with 90 additions and 41 deletions

View File

@ -1,9 +1,9 @@
use anyhow::Error;
use int_enum::IntEnum;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::cmp::min;
use tui_textarea::TextArea;
use int_enum::IntEnum;
use crate::db;
@ -55,6 +55,7 @@ impl Default for TaskState<'_> {
}
pub struct State<'a> {
pub project_name: String,
pub selected_column_idx: usize,
pub columns: Vec<Column>,
pub db_conn: db::DBConn,
@ -72,7 +73,15 @@ impl<'a> State<'a> {
let db_conn = db::DBConn::new(conn);
let columns = db_conn.get_all_columns()?;
let selected_column = db_conn.get_selected_column()?;
let project_name = std::env::current_dir()?
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("KANBAN PROJECT")
.to_string();
Ok(State {
project_name,
columns,
selected_column_idx: selected_column,
quit: false,

View File

@ -1,10 +1,10 @@
use crate::app::{State, TaskEditFocus, TaskState, EDIT_WINDOW_FOCUS_STATES};
use anyhow::Error;
use crossterm::event;
use crossterm::event::{Event, KeyCode};
use int_enum::IntEnum;
use anyhow::Error;
pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error>{
pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error> {
let cycle;
if forward {
cycle = (task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES;
@ -21,14 +21,14 @@ pub fn handle_task_edit(state: &mut State<'_>, key: event::KeyEvent) -> Result<(
// assign later to task_edit_state
let updated_task = if let Some(mut task) = state.task_edit_state.take() {
match (key.code, task.focus) {
(KeyCode::Tab, _) => {
(KeyCode::Tab, _) => {
cycle_focus(&mut task, true)?;
Some(task)
},
}
(KeyCode::BackTab, _) => {
cycle_focus(&mut task, false)?;
Some(task)
},
}
(KeyCode::Enter, TaskEditFocus::ConfirmBtn) => {
// The structure of this function is so we avoid an
// unncessary clone() here. We can just transfer
@ -49,12 +49,12 @@ pub fn handle_task_edit(state: &mut State<'_>, key: event::KeyEvent) -> Result<(
(_, TaskEditFocus::Title) => {
task.title.input(key);
Some(task)
},
}
(_, TaskEditFocus::Description) => {
task.description.input(key);
Some(task)
}
_ => Some(task)
_ => Some(task),
}
} else {
None
@ -79,7 +79,7 @@ pub fn handle_main(state: &mut State<'_>, key: event::KeyEvent) -> Result<(), Er
KeyCode::Char('n') => Ok(state.task_edit_state = Some(TaskState::default())),
KeyCode::Char('e') => Ok(state.task_edit_state = state.get_task_state_from_current()),
KeyCode::Char('D') => state.delete_task(),
_ => Ok(())
_ => Ok(()),
}
}

104
src/ui.rs
View File

@ -35,9 +35,12 @@ fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
span = Span::raw(&task.title);
}
span.style = style;
// TODO: This is unoptimized, we can actually construct the list
// inside a single ListItem it seems
ListItem::new(vec![Spans::from(span)])
})
.collect();
let mut style = Style::default();
if i == state.selected_column_idx {
style = style.add_modifier(Modifier::REVERSED);
@ -49,8 +52,10 @@ fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
let inner_area = block.inner(columns[i]);
let inner_block = Block::default().style(style);
let list = List::new(items).block(inner_block);
let mut list_state = ListState::default();
list_state.select(Some(column.selected_task_idx + 1));
f.render_widget(block, columns[i]);
f.render_stateful_widget(list, inner_area, &mut list_state);
}
@ -95,7 +100,7 @@ fn centered_rect_for_popup(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
.split(popup_layout[1])[1]
}
pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>, popup_title: &str) {
fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>, popup_title: &str) {
let area = centered_rect_for_popup(45, 60, f.size());
let block = Block::default()
.title(popup_title)
@ -130,30 +135,22 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
)
.split(layout[2]);
let create_style;
let create_txt;
let cancel_style;
let cancel_txt;
match task.focus {
TaskEditFocus::ConfirmBtn => {
create_style = Style::default().add_modifier(Modifier::BOLD);
cancel_style = Style::default();
create_txt = "[Confirm]";
cancel_txt = " Cancel ";
}
TaskEditFocus::CancelBtn => {
create_style = Style::default();
cancel_style = Style::default().add_modifier(Modifier::BOLD);
create_txt = " Confirm ";
cancel_txt = "[Cancel]";
}
_ => {
create_style = Style::default();
cancel_style = Style::default();
create_txt = " Confirm ";
cancel_txt = " Cancel ";
}
}
let (create_style, cancel_style, create_txt, cancel_txt) = match task.focus {
TaskEditFocus::ConfirmBtn => (
Style::default().add_modifier(Modifier::BOLD),
Style::default(),
"[Confirm]",
" Cancel ",
),
TaskEditFocus::CancelBtn => (
Style::default(),
Style::default().add_modifier(Modifier::BOLD),
" Confirm ",
"[Cancel]",
),
_ => (Style::default(), Style::default(), " Confirm ", " Cancel "),
};
let create_btn = Paragraph::new(create_txt).style(create_style);
let cancel_btn = Paragraph::new(cancel_txt).style(cancel_style);
f.render_widget(create_btn, buttons[1]);
@ -194,6 +191,40 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
}
}
fn draw_project_stats<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &mut State<'_>) {
let block = Block::default()
.title("PROJECT STATS")
.borders(Borders::ALL);
let c1_len = state.columns[0].tasks.len();
let c2_len = state.columns[1].tasks.len();
let c3_len = state.columns[2].tasks.len();
let c4_len = state.columns[3].tasks.len();
let tocomplete_total = c1_len + c2_len + c3_len;
let percentage = (c3_len as f32 / tocomplete_total as f32 * 100.0) as u8;
let list = List::new(
vec![
ListItem::new(vec![
Spans::from("Tasks per Column:"),
Spans::from(format!(" Todo ({})", c1_len)),
Spans::from(format!(" In Progress ({})", c2_len)),
Spans::from(format!(" Done ({})", c3_len)),
Spans::from(format!(" Ideas ({})", c4_len)),
Spans::from(
format!(
"Progress: {} / {} - {}%",
c3_len,
tocomplete_total,
percentage
))
]
)]
)
.block(block);
f.render_widget(list, area);
}
/// Macro to generate keybindings string at compile time
macro_rules! unroll {
(($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => {
@ -206,26 +237,35 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>) {
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(65),
Constraint::Percentage(20),
Constraint::Length(3),
Constraint::Length(2),
Constraint::Min(10),
Constraint::Max(10),
Constraint::Length(2),
]
.as_ref(),
)
.split(f.size());
let block = Block::default().title("KANBAN BOARD").borders(Borders::ALL);
let block = Block::default()
.title(format!("{}", state.project_name))
.title_alignment(Alignment::Center)
.borders(Borders::TOP);
f.render_widget(block, main_layout[0]);
draw_tasks(f, main_layout[1], state);
draw_task_info(f, main_layout[2], state);
let info_area = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Min(60), Constraint::Max(60)].as_ref())
.split(main_layout[2]);
draw_task_info(f, info_area[0], state);
draw_project_stats(f, info_area[1], state);
let block = Block::default().title("KEYBINDINGS").borders(Borders::TOP);
let foot_txt = unroll![
("quit", "c"),
("quit", "q"),
("navigation", "hjkl"),
("move task", "HJKL"),
("new task", "n"),