diff --git a/.gitignore b/.gitignore index a828a06..60bc314 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target /.idea -/todo-db.db3 \ No newline at end of file +/todolist-db.db3 \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index b1b20c2..cd9be6a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,10 +1,8 @@ +use crate::todo::{ChangeType, TaskColumn}; +use rusqlite::{params, Connection, Error, Result}; use std::path::Path; -use rusqlite::{params, Connection, Result}; -use crate::def; -use crate::def::Task; pub(crate) fn startup(path: &Path) -> Result<(Connection)> { - // let conn = Connection::open_in_memory()?; let conn = Connection::open(path)?; conn.execute( @@ -16,32 +14,74 @@ pub(crate) fn startup(path: &Path) -> Result<(Connection)> { () )?; - // todo - Ok(conn) } -pub(crate) fn get_tasks(conn: &Connection) -> Result<()> { - let mut stmt = conn.prepare("SELECT checked, value FROM tasks")?; - let task_iter = stmt.query_map([], |row| { - Ok(Task { - checked: row.get(0)?, - value: row.get(1)?, +pub(crate) fn get_tasks(conn: &Connection) -> std::result::Result, Error> { + let mut stmt = conn.prepare("SELECT id, checked, value FROM tasks")?; + let tasks_iter = stmt.query_map([], |row| { + Ok(TaskColumn { + id: row.get(0)?, + checked: row.get(1)?, + value: row.get(2)?, }) })?; - for task in task_iter { - println!("{:?}", task); + let mut tasks = Vec::new(); + for task in tasks_iter { + tasks.push(task?); } - - Ok(()) + + Ok(tasks) } -pub(crate) fn insert_task(task: Task, conn: &Connection) -> Result<()> { +pub(crate) fn insert_task(conn: &Connection, task: TaskColumn) -> Result { conn.execute( "INSERT INTO tasks (checked, value) VALUES (?1, ?2)", (&task.checked, &task.value), )?; + Ok(conn.last_insert_rowid()) +} + +// happens if we un/select task or we rename the task +pub(crate) fn update_task(conn: &Connection, task: TaskColumn, change: ChangeType) -> Result<()> { + let (query, params) = match change { + ChangeType::Checked => ( + "UPDATE tasks SET checked = ?1 WHERE id = ?2", + params![&task.checked, &task.id] + ), + ChangeType::Value => ( + "UPDATE tasks SET value = ?1 WHERE id = ?2", + params![&task.value, &task.id] + ), + }; + + conn.execute(query, params)?; + Ok(()) +} + +pub(crate) fn update_unselect_tasks(conn: &Connection, checked: bool) -> Result<()> { + conn.execute( + "UPDATE tasks SET checked = ?1", + (&checked,) + )?; + + Ok(()) +} + +pub(crate) fn delete_tasks(conn: &Connection, id: Option) -> Result<()> { + let (query, params) = match id { + Some(t) => ( + "DELETE FROM tasks WHERE id = ?1", + params![t.clone()] + ), + None => ( + "DELETE FROM tasks", + params![] + ), + }; + + conn.execute(query, params)?; Ok(()) } \ No newline at end of file diff --git a/src/def.rs b/src/def.rs deleted file mode 100644 index 772868a..0000000 --- a/src/def.rs +++ /dev/null @@ -1,35 +0,0 @@ -use iced::Event; -#[derive(Debug)] -pub struct Task { - pub(crate) checked: bool, - pub(crate) value: String, -} - -pub struct TaskData { - pub(crate) checked: bool, - pub(crate) value: String, - pub(crate) edit: bool, - pub(crate) can_update: bool, - pub(crate) can_delete: bool, -} - -pub struct Todo { - pub(crate) new_task: String, - pub(crate) updated_task: String, - pub(crate) tasks: Vec, - pub(crate) completed_tasks: usize, - pub(crate) local_storage: bool, -} - -#[derive(Debug, Clone)] -pub enum Message { - AddTask, - CheckTask(bool, usize), - EditTask(usize), - DeleteTask(usize), - DeleteAll, - ContentUpdated(bool, String), - Event(Event), - TaskPush(Option), - StorageToggle(bool), -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index bbeef61..934abaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,14 @@ -use std::path::Path; use iced::window::Settings; use iced::Size; -use def::Todo; -use crate::db::startup; -use crate::def::Task; +use todo::Todo; mod todo; -mod def; mod db; -/*fn main() -> iced::Result { +fn main() -> iced::Result { let settings = Settings { size: Size::new(500.0, 600.0), - resizable: false, + //resizable: false, ..Settings::default() }; @@ -21,18 +17,20 @@ mod db; .theme(Todo::theme) .window(settings) .run() -}*/ +} -fn main() { - let path: &Path = Path::new("./todo-db.db3"); +/*fn main() { + let path: &Path = Path::new("./todolist-db.db3"); let conn = startup(path); let t = Task { checked: true, - value: String::from("Troll Ink"), + value: String::from("JJ"), }; let c = conn.unwrap(); - /*let r = db::insert_task(t, &c); - println!("{:?}", r);*/ + let r = db::insert_task(t, &c); + println!("{:?}", r); let rr = db::get_tasks(&c); -} \ No newline at end of file + + let x = c.close(); +}*/ \ No newline at end of file diff --git a/src/todo.rs b/src/todo.rs index 584f207..738faac 100644 --- a/src/todo.rs +++ b/src/todo.rs @@ -1,26 +1,77 @@ use iced::{event, keyboard, widget, Center, Element, Event, Length, Subscription, Task, Theme}; use iced::keyboard::key; use iced::widget::{button, center, checkbox, column, row, scrollable, text_input, Space, Text}; -use crate::def::{Message, TaskData, Todo}; +use rusqlite::Connection; +use crate::db; + +#[derive(Debug)] +pub struct TaskColumn { + pub(crate) id: usize, + pub(crate) checked: bool, + pub(crate) value: String, +} + +pub struct TaskData { + id: usize, + checked: bool, + value: String, + edit: bool, + can_update: bool, + can_delete: bool, +} + +pub(crate) struct Todo { + new_task: String, + updated_task: String, + tasks: Vec, + completed_tasks: usize, + local_storage: bool, + conn: Connection, + select_all: bool, +} + +#[derive(Debug, Clone)] +pub enum Message { + AddTask, + CheckTask(bool, usize), + EditTask(usize), + DeleteTask(usize), + DeleteAll, + ContentUpdated(bool, String), + Event(Event), + TaskPush(Option), + StorageToggle(bool), + ToggleUnselect(bool), +} + +pub enum ChangeType { + Checked, + Value, +} impl Default for Todo { fn default() -> Self { Todo::new() - // startup checks } } impl Todo { fn new() -> Self { - Self { + let mut init = Self { new_task: String::new(), updated_task: String::new(), tasks: Vec::new(), completed_tasks: 0, - local_storage: true, - } + 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, + }; + + Self::load_data_from_db(&mut init); + + init } - pub(crate) fn update(&mut self, message: Message) -> Task { + pub(crate) fn update(&mut self, message: Message) -> Task { match message { Message::ContentUpdated(new, value) => { if new { @@ -34,31 +85,60 @@ impl Todo { Message::AddTask => { if self.new_task.is_empty() { return Task::none(); } - let data = TaskData { + + let result = db::insert_task(&self.conn, TaskColumn { + id: 0, checked: false, value: self.new_task.to_string(), - edit: false, - can_update: true, - can_delete: true, - }; + }); + + match result { + Ok(t) => { + let data = TaskData { + id: t as usize, + checked: false, + value: self.new_task.to_string(), + edit: false, + can_update: true, + can_delete: true, + }; + self.tasks.push(data); + }, + Err(e) => eprintln!("[ERROR] Failed to insert new task into DB:\n{e}"), + } - self.tasks.push(data); self.new_task = String::new(); Task::none() }, - Message::DeleteTask(id) => { - if self.tasks[id].checked { + Message::DeleteTask(index) => { + if self.tasks[index].checked { self.completed_tasks -= 1; } - self.tasks.remove(id); + + if let Err(e) = db::delete_tasks(&self.conn, Some(self.tasks[index].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].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(id) => { @@ -70,6 +150,16 @@ impl Todo { } self.tasks[id].value = self.updated_task.clone(); + let result = db::update_task(&self.conn, TaskColumn { + id: self.tasks[id].id, + checked: self.tasks[id].checked, + value: self.tasks[id].value.to_string(), + }, ChangeType::Value); + + if let Err(e) = result { + eprintln!("[ERROR] Failed to update task '{}' in local storage:\n{e}", self.tasks[id].value); + } + set_update = true; self.updated_task = String::new(); } else { @@ -82,6 +172,10 @@ impl Todo { 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(); @@ -121,20 +215,41 @@ impl Todo { } }, Message::StorageToggle(toggle) => { - self.local_storage = 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() }, } } - pub(crate) fn view(&self) -> Element { + pub(crate) fn view(&self) -> Element { let input = text_input("Enter new task", &self.new_task) .width(300) .size(25) @@ -186,8 +301,9 @@ impl Todo { } let status = Text::new(format!("{} / {}", self.completed_tasks, self.tasks.len())); + let unselect = checkbox("Un/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, Space::with_width(Length::Fill), storage].padding(10); + let footer = row![status, Space::with_width(Length::Fill), 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)) }; @@ -196,11 +312,11 @@ impl Todo { center(output).into() } - pub(crate) fn theme(&self) -> Theme { + pub(crate) fn theme(&self) -> Theme { Theme::Dark } - pub(crate) fn subscription(&self) -> Subscription { + pub(crate) fn subscription(&self) -> Subscription { event::listen().map(Message::Event) } @@ -212,4 +328,25 @@ impl Todo { 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 { + id: item.id, + checked: item.checked, + value: item.value, + edit: false, + can_update: true, + can_delete: true, + }; + + if item.checked { + self.completed_tasks += 1; + } + + self.tasks.push(data); + } + } + } } \ No newline at end of file