Redesign UI, project stats panel, show project name, fixes and refactorings
This commit is contained in:
parent
a98b82f3e4
commit
d23f458444
11
src/app.rs
11
src/app.rs
@ -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,
|
||||
|
16
src/input.rs
16
src/input.rs
@ -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
104
src/ui.rs
@ -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"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user