diff --git a/src/db.rs b/src/db.rs index 391b830..8c78ed6 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,24 +1,58 @@ -use crate::todo::{ChangeType, TaskColumn}; +use crate::todo; +use crate::todo::{ChangeType, TaskColumn, TaskListDB}; use rusqlite::{params, Connection, Error, Result}; use std::path::Path; -pub(crate) fn startup(path: &Path) -> Result<(Connection)> { +pub(crate) fn startup(path: &Path) -> std::result::Result { let conn = Connection::open(path)?; - + conn.execute( - "CREATE TABLE IF NOT EXISTS tasks ( + "CREATE TABLE IF NOT EXISTS task_group ( id INTEGER PRIMARY KEY, - checked INTEGER NOT NULL, - value TEXT NOT NULL + value TEXT NOT NULL, + UNIQUE(value) )", () )?; + + conn.execute( + "INSERT OR IGNORE INTO task_group (value) VALUES (?1)", + (&todo::DEFAULT_NAME,) + )?; + + if let Err(e) = create_tasklist_tables(&conn) { + eprintln!("[ERROR] Failed to create tasklist tables:\n{e}"); + return Err(e); + } Ok(conn) } -pub(crate) fn get_tasks(conn: &Connection) -> std::result::Result, Error> { - let mut stmt = conn.prepare("SELECT id, checked, value FROM tasks")?; +fn create_tasklist_tables(conn: &Connection) -> Result<()> { + let tl = get_tasklists(&conn)?; + if tl.len() < 1 { + return Ok(()); + } + + for item in 1..tl.len()+1 { + let stmt = format!("CREATE TABLE IF NOT EXISTS t{item} ( + id INTEGER PRIMARY KEY, + checked INTEGER NOT NULL, + value TEXT NOT NULL + )"); + + conn.execute( + &stmt.to_string(), + () + )?; + } + + Ok(()) +} + +pub(crate) fn get_tasks(conn: &Connection, tl_id: usize) -> std::result::Result, Error> { + let stmt_format = format!("SELECT id, checked, value FROM t{tl_id}"); + let mut stmt = conn.prepare(stmt_format.as_ref())?; let tasks_iter = stmt.query_map([], |row| { Ok(TaskColumn { id: row.get(0)?, @@ -36,22 +70,27 @@ pub(crate) fn get_tasks(conn: &Connection) -> std::result::Result Result { + let stmt = format!("INSERT INTO t{} (checked, value) VALUES (?1, ?2)", &task.id); + conn.execute( - "INSERT INTO tasks (checked, value) VALUES (?1, ?2)", + &stmt, (&task.checked, &task.value), )?; Ok(conn.last_insert_rowid()) } -pub(crate) fn update_task(conn: &Connection, task: TaskColumn, change: ChangeType) -> Result<()> { +pub(crate) fn update_task(conn: &Connection, task: TaskColumn, change: ChangeType, tl_id: usize) -> Result<()> { + let val = format!("UPDATE t{tl_id} SET value = ?1 WHERE id = ?2"); + let check = format!("UPDATE t{tl_id} SET checked = ?1 WHERE id = ?2"); + let (query, params) = match change { ChangeType::Checked => ( - "UPDATE tasks SET checked = ?1 WHERE id = ?2", + check.as_ref(), params![&task.checked, &task.id] ), ChangeType::Value => ( - "UPDATE tasks SET value = ?1 WHERE id = ?2", + val.as_ref(), params![&task.value, &task.id] ), }; @@ -60,27 +99,58 @@ pub(crate) fn update_task(conn: &Connection, task: TaskColumn, change: ChangeTyp Ok(()) } -pub(crate) fn update_unselect_tasks(conn: &Connection, checked: bool) -> Result<()> { +pub(crate) fn update_unselect_tasks(conn: &Connection, checked: bool, tl_id: usize) -> Result<()> { + let stmt = format!("UPDATE t{tl_id} SET checked = ?1"); + conn.execute( - "UPDATE tasks SET checked = ?1", + stmt.as_ref(), (&checked,) )?; Ok(()) } -pub(crate) fn delete_tasks(conn: &Connection, id: Option) -> Result<()> { +pub(crate) fn delete_tasks(conn: &Connection, id: Option, tl_id: usize) -> Result<()> { + let del_id = format!("DELETE FROM t{tl_id} WHERE id = ?1").to_string(); + let full_del = format!("DELETE FROM t{tl_id}").to_string(); + let (query, params) = match id { Some(t) => ( - "DELETE FROM tasks WHERE id = ?1", + del_id.as_ref(), params![t.clone()] ), None => ( - "DELETE FROM tasks", + full_del.as_ref(), params![] ), }; conn.execute(query, params)?; Ok(()) +} + +pub(crate) fn insert_tasklist(conn: &Connection, task_name: &str) -> Result { + conn.execute( + "INSERT OR IGNORE INTO task_group (value) VALUES (?1)", + (&task_name,) + )?; + + Ok(conn.last_insert_rowid()) +} + +pub(super) fn get_tasklists(conn: &Connection) -> std::result::Result, Error> { + let mut stmt = conn.prepare("SELECT id, value FROM task_group")?; + let tasks_iter = stmt.query_map([], |row| { + Ok(TaskListDB { + id: row.get(0)?, + value: row.get(1)?, + }) + })?; + + let mut tasks = Vec::new(); + for task in tasks_iter { + tasks.push(task?); + } + + Ok(tasks) } \ No newline at end of file diff --git a/src/todo.rs b/src/todo.rs index e0a61b5..1fdb0e9 100644 --- a/src/todo.rs +++ b/src/todo.rs @@ -1,3 +1,4 @@ +use std::default::Default; use crate::db; use iced::keyboard::key; use iced::widget::text::danger; @@ -29,13 +30,13 @@ pub(crate) struct Todo { 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, + task_list: Vec, + current_task_list: Option, + curr_tl_id: usize, } #[derive(Debug, Clone)] @@ -48,7 +49,6 @@ pub enum Message { ContentUpdated(ContentType, String), Event(Event), TaskPush(Option), - StorageToggle(bool), ToggleUnselect(bool), SelectedTaskList(String), AddTaskList, @@ -66,12 +66,19 @@ enum ContentType { NewList, } +pub(crate) struct TaskListDB { + pub(crate) id: usize, + pub(crate) value: String, +} + impl Default for Todo { fn default() -> Self { Todo::new() } } +pub const DEFAULT_NAME: &str = "general"; + impl Todo { fn new() -> Self { let mut init = Self { @@ -79,31 +86,35 @@ impl Todo { 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"), + 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_input: String::new(), task_list_state: Default::default(), - task_list: vec![String::from("general")], - current_task_group: Some(String::from("")), + task_list: Vec::new(), + current_task_list: None, + curr_tl_id: 1, }; - + + Self::set_task_list(&mut init); 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()); + + let mut combo_state = Vec::new(); + + for item in &init.task_list { + combo_state.push(item.value.clone()); + } + + init.task_list_state = combo_box::State::new(combo_state); + if !init.task_list.is_empty() { + init.current_task_list = Some(init.task_list[0].value.clone()); + init.curr_tl_id = init.task_list[0].id; + } 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), @@ -130,7 +141,7 @@ impl Todo { let result = db::insert_task( &self.conn, TaskColumn { - id: 0, + id: self.curr_tl_id, checked: false, value: self.new_task.to_string(), }, @@ -161,7 +172,7 @@ impl Todo { self.completed_tasks -= 1; } - if let Err(e) = db::delete_tasks(&self.conn, Some(self.tasks[index].db_id)) { + if let Err(e) = db::delete_tasks(&self.conn, Some(self.tasks[index].db_id), self.curr_tl_id) { eprintln!( "[ERROR] Failed to delete task '{}':\n{e}", self.tasks[index].value @@ -188,6 +199,7 @@ impl Todo { value: self.tasks[id].value.to_string(), }, ChangeType::Checked, + self.curr_tl_id, ); if let Err(e) = result { @@ -216,6 +228,7 @@ impl Todo { value: self.tasks[index].value.to_string(), }, ChangeType::Value, + self.curr_tl_id, ); if let Err(e) = result { @@ -243,7 +256,7 @@ impl Todo { widget::focus_previous() } Message::DeleteAll => { - if let Err(e) = db::delete_tasks(&self.conn, None) { + if let Err(e) = db::delete_tasks(&self.conn, None, self.curr_tl_id) { eprintln!("[ERROR] Failed to delete all tasks:\n{e}") } @@ -283,20 +296,6 @@ impl Todo { 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 { @@ -309,21 +308,50 @@ impl Todo { self.completed_tasks = 0; } - if let Err(e) = db::update_unselect_tasks(&self.conn, toggle) { + if let Err(e) = db::update_unselect_tasks(&self.conn, toggle, self.curr_tl_id) { eprintln!("[ERROR] Failed to update un/select all operation in database:\n{e}"); } Task::none() } Message::SelectedTaskList(task) => { - self.current_task_group = Some(task); + for item in &self.task_list { + if item.value == task { + self.curr_tl_id = item.id; + } + } + self.current_task_list = Some(task); + + // reload tasks + self.tasks.clear(); + self.completed_tasks = 0; + let _ = self.load_data_from_db(); + 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(""); + let mut combo_state_vec = Vec::new(); + + match db::insert_tasklist(&self.conn, &self.task_list_input) { + Ok(t) => { + self.task_list.push(TaskListDB { + id: t as usize, + value: self.task_list_input.clone(), + }); + self.current_task_list = Some(self.task_list_input.clone()); + self.task_list_input = String::from(""); + + for item in &self.task_list { + combo_state_vec.push(item.value.clone()); + } + self.task_list_state = combo_box::State::new(combo_state_vec); + } + Err(e) => { + eprintln!("[ERROR] tasklist insertion failed:\n{e}"); + return Task::none(); + } + } Task::none() } } @@ -388,8 +416,8 @@ impl Todo { 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(), + "Enter desired task list ...", + self.current_task_list.as_ref(), Message::SelectedTaskList, ) .on_input(|str| Message::ContentUpdated(ContentType::NewList, str)); @@ -397,13 +425,10 @@ impl Todo { 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 + unselect, ] .padding(10) .spacing(10); @@ -437,7 +462,7 @@ impl Todo { } fn load_data_from_db(&mut self) { - if let Ok(t) = db::get_tasks(&self.conn) { + if let Ok(t) = db::get_tasks(&self.conn, self.curr_tl_id) { for item in t { let data = TaskData { db_id: item.id, @@ -474,4 +499,18 @@ impl Todo { false } + + fn set_task_list(&mut self) { + match db::get_tasklists(&self.conn) { + Ok(tl_db) => { + for item in tl_db { + self.task_list.push(TaskListDB { + id: item.id, + value: item.value, + }); + } + } + Err(e) => eprintln!("[ERROR] Failed to get tasklists from DB:\n{e}"), + } + } }