Skip to content

Commit

Permalink
feat: added dragging cards with mouse
Browse files Browse the repository at this point in the history
  • Loading branch information
yashs662 committed Jan 3, 2024
1 parent 4ebf6a6 commit f981a51
Show file tree
Hide file tree
Showing 10 changed files with 1,145 additions and 742 deletions.
210 changes: 149 additions & 61 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rust-kanban"
version = "0.8.2"
version = "0.9.0"
authors = ["Yash Sharma <yashs662@gmail.com>"]
edition = "2021"
license = "MIT"
Expand All @@ -11,24 +11,24 @@ categories = ["command-line-utilities", "text-editors"]

[dependencies]
log = "0.4.20"
ratatui = { version = "0.24.0", features = ["serde"] }
ratatui = { version = "0.25.0", features = ["serde"] }
crossterm = "0.27.0"
tokio = { version = "1.34.0", features = ["full"] }
tokio = { version = "1.35.1", features = ["full"] }
chrono = "0.4.31"
textwrap = "0.16"
eyre = "0.6.9"
home = "0.5.5"
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
clap = { version = "4.4.8", features = ["derive"] }
uuid = { version = "1.5.0", features = ["v4"] }
eyre = "0.6.11"
home = "0.5.9"
serde = { version = "1.0.194", features = ["derive"] }
serde_json = "1.0.110"
clap = { version = "4.4.12", features = ["derive"] }
uuid = { version = "1.6.1", features = ["v4"] }
regex = "1.10.2"
linked-hash-map = "0.5.6"
ngrammatic = "0.4.0"
lazy_static = "1.4.0"
fxhash = "0.2.1"
parking_lot = "0.12.1"
reqwest = { version = "0.11.22", features = ["json"] }
reqwest = { version = "0.11.23", features = ["json"] }
aes-gcm = "0.10.3"
base64 = "0.21.5"
bunt = "0.2.8"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Feel free to make a pull request or make a new issue, I am open to suggestions

- [ ] Improve performance/optimize code (card view can take upwards of 1ms to render)
- [ ] Improve error handling by implementing best practices
- [ ] Allow for more mouse Interactions (Dragging cards maybe?)
- [ ] Implement animations for UI elements
- [ ] Implement a way to sync with other services like notion
- [ ] Write Tests
Expand All @@ -29,6 +28,7 @@ Feel free to make a pull request or make a new issue, I am open to suggestions

## Completed Features

- [x] Drag and Drop cards with the mouse
- [X] Allow for vertical movement in text fields (e.g. card description)
- [X] Encryption for Cloud Saves
- [X] Implement Cloud saves
Expand Down
858 changes: 494 additions & 364 deletions src/app/app_helper.rs

Large diffs are not rendered by default.

150 changes: 101 additions & 49 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,19 @@ pub enum AppReturn {

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ActionHistory {
/// card, board_id
DeleteCard(Card, (u64, u64)),
/// card, board_id
CreateCard(Card, (u64, u64)),
/// board
DeleteBoard(Board),
MoveCardBetweenBoards(Card, (u64, u64), (u64, u64)),
/// card, moved_from_board_id, moved_to_board_id, moved_from_index, moved_to_index
MoveCardBetweenBoards(Card, (u64, u64), (u64, u64), usize, usize),
/// board_id, moved_from_index, moved_to_index
MoveCardWithinBoard((u64, u64), usize, usize),
/// board
CreateBoard(Board),
/// old_card, new_card, board_id
EditCard(Card, Card, (u64, u64)),
}

Expand All @@ -81,6 +88,10 @@ impl ActionHistoryManager {
self.history.push(action);
self.history_index += 1;
}
pub fn reset(&mut self) {
self.history.clear();
self.history_index = 0;
}
}

pub struct App<'a> {
Expand Down Expand Up @@ -143,6 +154,14 @@ impl App<'_> {
app
}

pub fn get_mut_board(&mut self, board_id: (u64, u64)) -> Option<&mut Board> {
self.boards.iter_mut().find(|b| b.id == board_id)
}

pub fn get_board(&self, board_id: (u64, u64)) -> Option<&Board> {
self.boards.iter().find(|b| b.id == board_id)
}

pub async fn do_action(&mut self, key: Key) -> AppReturn {
if self.state.app_status == AppStatus::UserInput {
handle_user_input_mode(self, key).await
Expand Down Expand Up @@ -665,8 +684,8 @@ impl App<'_> {
}
self.state.keybinding_store = keybinding_action_list;
}
pub fn send_info_toast(&mut self, message: &str, duration: Option<Duration>) {
if let Some(duration) = duration {
pub fn send_info_toast(&mut self, message: &str, custom_duration: Option<Duration>) {
if let Some(duration) = custom_duration {
self.state.toasts.push(ToastWidget::new(
message.to_string(),
duration,
Expand All @@ -682,8 +701,8 @@ impl App<'_> {
));
}
}
pub fn send_error_toast(&mut self, message: &str, duration: Option<Duration>) {
if let Some(duration) = duration {
pub fn send_error_toast(&mut self, message: &str, custom_duration: Option<Duration>) {
if let Some(duration) = custom_duration {
self.state.toasts.push(ToastWidget::new(
message.to_string(),
duration,
Expand All @@ -699,8 +718,8 @@ impl App<'_> {
));
}
}
pub fn send_warning_toast(&mut self, message: &str, duration: Option<Duration>) {
if let Some(duration) = duration {
pub fn send_warning_toast(&mut self, message: &str, custom_duration: Option<Duration>) {
if let Some(duration) = custom_duration {
self.state.toasts.push(ToastWidget::new(
message.to_string(),
duration,
Expand All @@ -716,8 +735,8 @@ impl App<'_> {
));
}
}
pub fn send_loading_toast(&mut self, message: &str, duration: Option<Duration>) {
if let Some(duration) = duration {
pub fn send_loading_toast(&mut self, message: &str, custom_duration: Option<Duration>) {
if let Some(duration) = custom_duration {
self.state.toasts.push(ToastWidget::new(
message.to_string(),
duration,
Expand Down Expand Up @@ -1073,25 +1092,39 @@ impl App<'_> {
card,
moved_from_board_id,
moved_to_board_id,
moved_from_index,
moved_to_index,
) => {
if let Some(moved_to_board) =
self.boards.iter_mut().find(|b| b.id == moved_to_board_id)
{
moved_to_board.cards.retain(|c| c.id != card.id);
} else {
self.send_error_toast(&format!("Could not undo move card '{}' as the board with id '{:?}' was not found", card.name, moved_to_board_id), None);
let moved_to_board = self.get_board(moved_to_board_id);
let moved_from_board = self.get_board(moved_from_board_id);
if moved_to_board.is_none() || moved_from_board.is_none() {
debug!("Could not undo move card '{}' as the move to board with id '{:?}' or the move from board with id '{:?}' was not found", card.name, moved_to_board_id, moved_from_board_id);
return;
}
if let Some(moved_from_board) =
self.boards.iter_mut().find(|b| b.id == moved_from_board_id)
{
moved_from_board.cards.push(card.clone());
refresh_visible_boards_and_cards(self);
self.action_history_manager.history_index -= 1;
self.send_info_toast(&format!("Undo Move Card '{}'", card.name), None);
} else {
self.send_error_toast(&format!("Could not undo move card '{}' as the board with id '{:?}' was not found", card.name, moved_from_board_id), None);

let moved_from_board = moved_from_board.unwrap();
if moved_from_index > moved_from_board.cards.len() {
debug!("bad index for undo move card, from board {:?}, to board {:?}, from index {}, to index {}", moved_from_board_id, moved_to_board_id, moved_from_index, moved_to_index);
self.send_error_toast(
&format!(
"Could not undo move card '{}' as the index's were invalid",
card.name
),
None,
);
}

let moved_to_board = self.get_mut_board(moved_to_board_id).unwrap();
moved_to_board.cards.retain(|c| c.id != card.id);

let moved_from_board = self.get_mut_board(moved_from_board_id).unwrap();
moved_from_board
.cards
.insert(moved_from_index, card.clone());

refresh_visible_boards_and_cards(self);
self.action_history_manager.history_index -= 1;
self.send_info_toast(&format!("Undo Move Card '{}'", card.name), None);
}
ActionHistory::MoveCardWithinBoard(board_id, moved_from_index, moved_to_index) => {
if let Some(board) = self.boards.iter_mut().find(|b| b.id == board_id) {
Expand Down Expand Up @@ -1187,25 +1220,38 @@ impl App<'_> {
card,
moved_from_board_id,
moved_to_board_id,
moved_from_index,
moved_to_index,
) => {
if let Some(moved_to_board) =
self.boards.iter_mut().find(|b| b.id == moved_to_board_id)
{
moved_to_board.cards.push(card.clone());
} else {
self.send_error_toast(&format!("Could not redo move card '{}' as the board with id '{:?}' was not found", card.name, moved_to_board_id), None);
let moved_to_board = self.get_board(moved_to_board_id);
let moved_from_board = self.get_board(moved_from_board_id);
if moved_to_board.is_none() || moved_from_board.is_none() {
debug!("Could not undo move card '{}' as the move to board with id '{:?}' or the move from board with id '{:?}' was not found", card.name, moved_to_board_id, moved_from_board_id);
return;
}
if let Some(moved_from_board) =
self.boards.iter_mut().find(|b| b.id == moved_from_board_id)
{
moved_from_board.cards.retain(|c| c.id != card.id);
refresh_visible_boards_and_cards(self);
self.action_history_manager.history_index += 1;
self.send_info_toast(&format!("Redo Move Card '{}'", card.name), None);
} else {
self.send_error_toast(&format!("Could not redo move card '{}' as the board with id '{:?}' was not found", card.name, moved_from_board_id), None);

let moved_to_board = moved_to_board.unwrap();
if moved_to_index > moved_to_board.cards.len() {
debug!("bad index for redo move card, from board {:?}, to board {:?}, from index {}, to index {}", moved_from_board_id, moved_to_board_id, moved_from_index, moved_to_index);
self.send_error_toast(
&format!(
"Could not redo move card '{}' as the index's were invalid",
card.name
),
None,
);
return;
}

let moved_from_board = self.get_mut_board(moved_from_board_id).unwrap();
moved_from_board.cards.retain(|c| c.id != card.id);

let moved_to_board = self.get_mut_board(moved_to_board_id).unwrap();
moved_to_board.cards.insert(moved_to_index, card.clone());

refresh_visible_boards_and_cards(self);
self.action_history_manager.history_index += 1;
self.send_info_toast(&format!("Redo Move Card '{}'", card.name), None);
}
ActionHistory::MoveCardWithinBoard(board_id, moved_from_index, moved_to_index) => {
if let Some(board) = self.boards.iter_mut().find(|b| b.id == board_id) {
Expand Down Expand Up @@ -1479,6 +1525,12 @@ impl PopupMode {
}

pub fn render(self, rect: &mut Frame, app: &mut App) {
let current_focus = app.state.focus;
if !self.get_available_targets().contains(&current_focus)
&& !self.get_available_targets().is_empty()
{
app.state.focus = self.get_available_targets()[0];
}
match self {
PopupMode::ViewCard => {
ui_helper::render_view_card(rect, app);
Expand Down Expand Up @@ -1529,8 +1581,7 @@ impl PopupMode {
}
}

#[derive(Debug, Clone)]
#[derive(Default)]
#[derive(Debug, Clone, Default)]
pub struct AppListStates {
pub card_priority_selector: ListState,
pub card_status_selector: ListState,
Expand All @@ -1550,19 +1601,14 @@ pub struct AppListStates {
pub theme_selector: ListState,
}



#[derive(Debug, Clone)]
#[derive(Default)]
#[derive(Debug, Clone, Default)]
pub struct AppTableStates {
pub config: TableState,
pub edit_keybindings: TableState,
pub help: TableState,
pub theme_editor: TableState,
}



#[derive(Debug, Clone)]
pub struct AppFormStates {
pub login: (Vec<String>, bool),
Expand Down Expand Up @@ -1611,7 +1657,10 @@ pub struct AppState<'a> {
pub focus: Focus,
pub keybinding_store: Vec<Vec<String>>,
pub last_mouse_action: Option<Mouse>,
pub last_mouse_action_time: Option<Instant>,
pub hovered_card: Option<((u64, u64), (u64, u64))>,
pub hovered_board: Option<(u64, u64)>,
pub card_drag_mode: bool,
pub hovered_card_dimensions: Option<(u16, u16)>,
pub last_reset_password_link_sent_time: Option<Instant>,
pub mouse_focus: Option<Focus>,
pub mouse_list_index: Option<u16>,
Expand Down Expand Up @@ -1655,7 +1704,10 @@ impl Default for AppState<'_> {
focus: Focus::NoFocus,
keybinding_store: Vec::new(),
last_mouse_action: None,
last_mouse_action_time: None,
hovered_card: None,
hovered_board: None,
card_drag_mode: false,
hovered_card_dimensions: None,
last_reset_password_link_sent_time: None,
mouse_focus: None,
mouse_list_index: None,
Expand Down
15 changes: 13 additions & 2 deletions src/app/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,14 @@ impl UiMode {
}

pub fn render(self, rect: &mut Frame, app: &mut App) {
if app.state.popup_mode.is_none() {
let current_focus = app.state.focus;
if !self.get_available_targets().contains(&current_focus)
&& !self.get_available_targets().is_empty()
{
app.state.focus = self.get_available_targets()[0];
}
}
match self {
UiMode::Zen => {
ui_helper::render_zen_mode(rect, app);
Expand Down Expand Up @@ -613,7 +621,10 @@ impl KeyBindings {
(KeyBindingEnum::DeleteBoard.to_string(), &self.delete_board),
(KeyBindingEnum::DeleteCard.to_string(), &self.delete_card),
(KeyBindingEnum::Down.to_string(), &self.down),
(KeyBindingEnum::GoToMainMenu.to_string(), &self.go_to_main_menu),
(
KeyBindingEnum::GoToMainMenu.to_string(),
&self.go_to_main_menu,
),
(
KeyBindingEnum::HideUiElement.to_string(),
&self.hide_ui_element,
Expand Down Expand Up @@ -774,7 +785,7 @@ impl Default for KeyBindings {
change_card_status_to_stale: vec![Key::Char('3')],
clear_all_toasts: vec![Key::Char('t')],
delete_board: vec![Key::Char('D')],
delete_card: vec![Key::Char('d')],
delete_card: vec![Key::Char('d'), Key::Delete],
down: vec![Key::Down],
go_to_main_menu: vec![Key::Char('m')],
hide_ui_element: vec![Key::Char('h')],
Expand Down
4 changes: 4 additions & 0 deletions src/io/data_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,25 @@ pub fn get_local_kanban_state(
}
let file = fs::File::open(file_path);
if file.is_err() {
debug!("Error opening save file: {}", file.err().unwrap());
return Err("Error opening save file".to_string());
}
let file = file.unwrap();
let serde_object = serde_json::from_reader(file);
if serde_object.is_err() {
debug!("Error parsing save file: {}", serde_object.err().unwrap());
return Err("Error parsing save file".to_string());
}
let serde_object: serde_json::Value = serde_object.unwrap();
let boards = serde_object.get("boards");
if boards.is_none() {
debug!("Error parsing save file, no boards found");
return Err("Error parsing save file".to_string());
}
let boards = boards.unwrap();
let boards = boards.as_array();
if boards.is_none() {
debug!("Error parsing save file, boards is not an array");
return Err("Error parsing save file".to_string());
}
let boards = boards.unwrap();
Expand Down
Loading

0 comments on commit f981a51

Please sign in to comment.