This commit is contained in:
Martin Vrhovšek 2025-02-06 21:20:03 +01:00
parent 68411dc657
commit a798137f1f
2 changed files with 174 additions and 65 deletions

102
src/db.rs
View File

@ -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<Connection, Error> {
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<Vec<TaskColumn>, 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<Vec<TaskColumn>, 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<Vec<TaskColumn
}
pub(crate) fn insert_task(conn: &Connection, task: TaskColumn) -> Result<i64> {
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,23 +99,28 @@ 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<usize>) -> Result<()> {
pub(crate) fn delete_tasks(conn: &Connection, id: Option<usize>, 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![]
),
};
@ -84,3 +128,29 @@ pub(crate) fn delete_tasks(conn: &Connection, id: Option<usize>) -> Result<()> {
conn.execute(query, params)?;
Ok(())
}
pub(crate) fn insert_tasklist(conn: &Connection, task_name: &str) -> Result<i64> {
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<Vec<TaskListDB>, 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)
}

View File

@ -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<TaskData>,
completed_tasks: usize,
local_storage: bool,
conn: Connection,
select_all: bool,
task_list_input: String,
task_list_state: combo_box::State<String>,
task_list: Vec<String>,
current_task_group: Option<String>,
task_list: Vec<TaskListDB>,
current_task_list: Option<String>,
curr_tl_id: usize,
}
#[derive(Debug, Clone)]
@ -48,7 +49,6 @@ pub enum Message {
ContentUpdated(ContentType, String),
Event(Event),
TaskPush(Option<usize>),
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<Message> {
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());
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
]
.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}"),
}
}
}