use crate::db; use iced::keyboard::key; use iced::widget::text::danger; use iced::widget::{ button, center, checkbox, column, combo_box, row, scrollable, text_input, Text, }; use iced::{event, keyboard, widget, Center, Element, Event, Length, Subscription, Task, Theme}; use rusqlite::Connection; #[derive(Debug)] pub struct TaskColumn { pub(crate) id: usize, pub(crate) checked: bool, pub(crate) value: String, } pub struct TaskData { db_id: usize, checked: bool, value: String, edit: bool, can_update: bool, can_delete: bool, already_exists: bool, } pub(crate) struct Todo { new_task: String, updated_task: String, tasks: Vec, completed_tasks: usize, local_storage: bool, conn: Connection, select_all: bool, task_list_input: String, task_list_state: combo_box::State, task_list: Vec, current_task_group: Option, } #[derive(Debug, Clone)] pub enum Message { AddTask, CheckTask(bool, usize), EditTask(usize), DeleteTask(usize), DeleteAll, ContentUpdated(ContentType, String), Event(Event), TaskPush(Option), StorageToggle(bool), ToggleUnselect(bool), SelectedTaskList(String), AddTaskList, } pub enum ChangeType { Checked, Value, } #[derive(Clone, Debug)] enum ContentType { NewTask, ExistingTask, NewList, } impl Default for Todo { fn default() -> Self { Todo::new() } } impl Todo { fn new() -> Self { let mut init = Self { new_task: String::new(), updated_task: String::new(), tasks: Vec::new(), completed_tasks: 0, local_storage: true, // todo eventually get this info from db - if no db set it to true conn: db::startup("./todolist-db.db3".as_ref()) .expect("[ERROR] Failed to access the local storage"), select_all: false, task_list_input: String::from(""), task_list_state: Default::default(), task_list: vec![String::from("general")], current_task_group: Some(String::from("")), }; Self::load_data_from_db(&mut init); init.task_list_state = combo_box::State::new(init.task_list.clone()); init.current_task_group = Some(init.task_list[0].clone()); init } pub(crate) fn update(&mut self, message: Message) -> Task { match message { Message::ContentUpdated(new, value) => { /*if new { self.new_task = String::from(&value); } else { self.updated_task = String::from(&value) }*/ match new { ContentType::NewTask => self.new_task = String::from(&value), ContentType::ExistingTask => self.updated_task = String::from(&value), ContentType::NewList => { self.task_list_input = String::from(&value); return Task::none(); } } for task in &mut self.tasks { task.already_exists = task.value == value.to_string(); } Task::none() } Message::AddTask => { if self.new_task.is_empty() { return Task::none(); } if self.duplicate_exists(None) { return Task::none(); } let result = db::insert_task( &self.conn, TaskColumn { id: 0, checked: false, value: self.new_task.to_string(), }, ); match result { Ok(t) => { let data = TaskData { db_id: t as usize, checked: false, value: self.new_task.to_string(), edit: false, can_update: true, can_delete: true, already_exists: false, }; self.tasks.push(data); } Err(e) => eprintln!("[ERROR] Failed to insert new task into DB:\n{e}"), } self.new_task = String::new(); Task::none() } Message::DeleteTask(index) => { if self.tasks[index].checked { self.completed_tasks -= 1; } if let Err(e) = db::delete_tasks(&self.conn, Some(self.tasks[index].db_id)) { eprintln!( "[ERROR] Failed to delete task '{}':\n{e}", self.tasks[index].value ); } self.tasks.remove(index); Task::none() } Message::CheckTask(choice, id) => { self.completed_tasks = if choice { self.completed_tasks + 1 } else { self.completed_tasks - 1 }; self.tasks[id].checked = choice; let result = db::update_task( &self.conn, TaskColumn { id: self.tasks[id].db_id, checked: self.tasks[id].checked, value: self.tasks[id].value.to_string(), }, ChangeType::Checked, ); if let Err(e) = result { eprintln!("[ERROR] Failed to update checkbox in local storage:\n{e}"); } Task::none() } Message::EditTask(index) => { let set_update; if self.tasks[index].edit { if self.updated_task.is_empty() { return Task::none(); } if self.duplicate_exists(Some(&self.tasks[index].value)) { return Task::none(); } self.tasks[index].value = self.updated_task.clone(); let result = db::update_task( &self.conn, TaskColumn { id: self.tasks[index].db_id, checked: self.tasks[index].checked, value: self.tasks[index].value.to_string(), }, ChangeType::Value, ); if let Err(e) = result { eprintln!( "[ERROR] Failed to update task '{}' in local storage:\n{e}", self.tasks[index].value ); } set_update = true; self.updated_task = String::new(); for item in &mut self.tasks { if item.already_exists { item.already_exists = false; } } } else { set_update = false; } self.tasks[index].edit = !self.tasks[index].edit; self.set_other_update(index, set_update); widget::focus_previous() } Message::DeleteAll => { if let Err(e) = db::delete_tasks(&self.conn, None) { eprintln!("[ERROR] Failed to delete all tasks:\n{e}") } self.new_task = String::from(""); self.updated_task = String::from(""); self.tasks.clear(); self.completed_tasks = 0; Task::none() } Message::Event(event) => match event { Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(key::Named::Tab), modifiers, .. }) => { if modifiers.shift() { widget::focus_previous() } else { widget::focus_next() } } Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(key::Named::Delete), modifiers, .. }) => { if modifiers.control() { Task::perform(async move { Message::DeleteAll }, |result| result) } else { Task::none() } } _ => Task::none(), }, Message::TaskPush(option_id) => match option_id { Some(i) => Task::perform(async move { Message::EditTask(i) }, |result| result), None => Task::perform(async { Message::AddTask }, |result| result), }, Message::StorageToggle(_toggle) => { // todo must be enabled eventually //self.local_storage = toggle; /* todo here we only call for storage change not implement the whole system as the system should be running since the program startup */ // also how do we even implement that Task::none() } Message::ToggleUnselect(toggle) => { self.select_all = toggle; for task in &mut self.tasks { task.checked = toggle; } if toggle { self.completed_tasks = self.tasks.len(); } else { self.completed_tasks = 0; } if let Err(e) = db::update_unselect_tasks(&self.conn, toggle) { eprintln!("[ERROR] Failed to update un/select all operation in database:\n{e}"); } Task::none() } Message::SelectedTaskList(task) => { self.current_task_group = Some(task); Task::none() } Message::AddTaskList => { if self.task_list_input.is_empty() { return Task::none(); } self.task_list.push(self.task_list_input.clone()); self.task_list_state = combo_box::State::new(self.task_list.clone()); self.task_list_input = String::from(""); Task::none() } } } pub(crate) fn view(&self) -> Element { let input = text_input("Enter new task", &self.new_task) .width(300) .size(25) .on_input(|str| Message::ContentUpdated(ContentType::NewTask, str)) .on_submit(Message::TaskPush(None)); let add_btn = button(Text::new("Add").size(19).center()) .width(Length::Shrink) .padding(10) .on_press(Message::AddTask); let clear_btn = button(Text::new("Clear").size(19).center()) .width(Length::Shrink) .padding(10) .style(button::danger) .on_press(Message::DeleteAll); let new_task = row![input, add_btn, clear_btn].spacing(10); let mut saved_tasks = column![]; for (i, task) in self.tasks.iter().enumerate() { let chk = checkbox("", task.checked) .size(25) .on_toggle(move |b| Message::CheckTask(b, i)); let mut label = Text::new(&task.value).width(200); if task.already_exists { label = label.style(danger); } let input = text_input("Enter new name", &self.updated_task) .width(200) .on_input(|str| Message::ContentUpdated(ContentType::ExistingTask, str)) .on_submit(Message::TaskPush(Some(i))); let mut edit = button(if task.edit { "save" } else { "edit" }).width(Length::Shrink); if task.can_update { edit = edit.on_press(Message::EditTask(i)); } let mut delete = button("delete").width(Length::Shrink); if task.can_delete { delete = delete.on_press(Message::DeleteTask(i)); } let mut task_line = row![chk]; task_line = if task.edit { task_line.push(input) } else { task_line.push(label) }; task_line = task_line .push(edit) .push(delete) .spacing(10) .padding([5, 10]); saved_tasks = saved_tasks.push(task_line); } let status = Text::new(format!("{} / {}", self.completed_tasks, self.tasks.len())); let tasklist = combo_box( &self.task_list_state, "Test...", self.current_task_group.as_ref(), Message::SelectedTaskList, ) .on_input(|str| Message::ContentUpdated(ContentType::NewList, str)); let add_tasklist = button("+").on_press(Message::AddTaskList); let tasklist_group = row![tasklist.width(Length::Fill), add_tasklist].spacing(5); let unselect = checkbox("Select all", self.select_all).on_toggle(Message::ToggleUnselect); let storage = checkbox("Local storage", self.local_storage).on_toggle(Message::StorageToggle); let footer = row![ status, tasklist_group, unselect, storage ] .padding(10) .spacing(10); let mut output = column![new_task.padding(10)]; output = if self.tasks.is_empty() { output.push(saved_tasks.height(Length::Fill)) } else { output.push(scrollable(saved_tasks).height(Length::Fill).spacing(10)) }; output = output.align_x(Center).push(footer); center(output).into() } pub(crate) fn theme(&self) -> Theme { Theme::Dark } pub(crate) fn subscription(&self) -> Subscription { event::listen().map(Message::Event) } fn set_other_update(&mut self, id: usize, enable: bool) { for (i, task) in self.tasks.iter_mut().enumerate() { if id != i { task.can_update = enable; } task.can_delete = enable; } } fn load_data_from_db(&mut self) { if let Ok(t) = db::get_tasks(&self.conn) { for item in t { let data = TaskData { db_id: item.id, checked: item.checked, value: item.value, edit: false, can_update: true, can_delete: true, already_exists: false, }; if item.checked { self.completed_tasks += 1; } self.tasks.push(data); } } } fn duplicate_exists(&self, task_name: Option<&String>) -> bool { for task in &self.tasks { if task.already_exists { match task_name { Some(t) => { if task.value != *t { return true; } } None => return true, } } } false } }