diff --git a/scripts/task_view_to_pdf.py b/scripts/task_view_to_pdf.py new file mode 100644 index 0000000..762ed5b --- /dev/null +++ b/scripts/task_view_to_pdf.py @@ -0,0 +1,28 @@ +import json +import sys +import tkinter as tk +from tkinter import filedialog +from weasyprint import HTML, CSS + +def create_detailed_pdf(file_path, pdf_html, pdf_css): + html = HTML(string=pdf_html) + css = CSS(string=pdf_css) + html.write_pdf(file_path, stylesheets=[css]) + +# Create the main window (it will not be displayed) +root = tk.Tk() +root.withdraw() # Hide the main window + +# Open the file dialog to choose the save location and filename +file_path = filedialog.asksaveasfilename( + defaultextension=".pdf", + filetypes=[("PDF files", "*.pdf")], + title="Choose location to save the PDF" +) + +# Check if the user provided a file path +if file_path: + pdf_html = sys.argv[1] + pdf_css = sys.argv[2] + + create_detailed_pdf(file_path, pdf_html, pdf_css) diff --git a/src-tauri/src/db/ops.rs b/src-tauri/src/db/ops.rs index 33864b1..044eb32 100644 --- a/src-tauri/src/db/ops.rs +++ b/src-tauri/src/db/ops.rs @@ -4,6 +4,7 @@ use std::sync::Mutex; const ROOT_GROUP: &str = "/"; const TASK: &str = "Task"; const TASK_GROUP: &str = "TaskGroup"; +const TODAY: &str = "today"; // Singleton because we need a single point of access to the db across the app. // Mutex because only one process should be able to use the singleton at a time, @@ -20,7 +21,7 @@ use rusqlite::{params, Connection, Result}; use serde::Serialize; use std::{collections::HashMap, path::PathBuf}; -#[derive(Serialize, Debug, Clone, Copy)] +#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(tag = "type")] pub enum Type { Task, @@ -365,12 +366,14 @@ impl Db { } pub mod crud_commands { + use core::panic; use std::sync::MutexGuard; use rusqlite::{params, Connection, Result}; use serde::{Deserialize, Serialize}; - use super::{FetchBasis, Type, DB_SINGLETON, TASK, TASK_GROUP}; + + use super::{FetchBasis, Type, DB_SINGLETON, TASK, TASK_GROUP, TODAY}; #[tauri::command] /// C(R)UD - Reads the database and sends appropriate structure to the frontend. @@ -575,4 +578,46 @@ pub mod crud_commands { ), } } + + /// C(R)UD - Reads database and gets all tasks' names and their parent_group_ids. + pub fn get_all_tasks(fetch_basis: FetchBasis) -> Vec<(u64, String)> { + let db = DB_SINGLETON.lock().unwrap(); + let mut all_tasks: Vec<(u64, String)> = Vec::new(); + + match db.fetch_records(TODAY, fetch_basis) { + Ok(records) => { + all_tasks.extend( + records + .into_iter() + .filter(|(_, _, record_type, _, _)| *record_type != Type::TaskGroup) + .map(|(_, name, _, _, parent_id)| { + if let Some(id) = parent_id { + (id, name) + } else { + panic!("[ERROR] While fetching task names, the task: [{name}] had no parent."); + } + }), + ); + } + Err(err) => panic!("{err}"), + } + + all_tasks + } + + pub fn get_item(id: u64) -> String { + let db = DB_SINGLETON.lock().unwrap(); + let mut name = String::new(); + + if let Some(conn) = &db.db_conn { + let res: Option = match conn.query_row("SELECT name FROM today WHERE id=(?1)", params![id], |row| row.get(0)) { + Ok(name) => name, + Err(err) => panic!("{err}"), + }; + + name = res.unwrap(); + } + + name + } } diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs new file mode 100644 index 0000000..20fe4a2 --- /dev/null +++ b/src-tauri/src/export.rs @@ -0,0 +1,147 @@ +use std::{collections::HashMap, process::Command}; + +use crate::db::ops::{ + crud_commands::{get_all_tasks, get_item}, + FetchBasis, +}; + +fn get_python_input(tasks: &[(u64, String)]) -> Vec<(String, String)> { + let mut res: Vec<(String, String)> = Vec::new(); + for (parent_id, name) in tasks.iter() { + let parent_name = get_item(*parent_id); + res.push((parent_name, name.clone())); + } + res +} + +fn map_parent_to_tasks(task_list: Vec<(String, String)>) -> HashMap> { + let mut res: HashMap> = HashMap::new(); + + for (parent, task) in task_list { + res.entry(parent).or_insert(Vec::new()).push(task); + } + + res +} + +fn generate_ordered_list(map: HashMap>, is_completed: bool) -> String { + let mut html = String::from("
    "); + + let checked = match is_completed { + true => "checked", + false => "", + }; + + for (parent, tasks) in map.into_iter() { + match parent.as_str() { + "/" => { + html.push_str(&format!( + "{}", + tasks.into_iter() + .map(|task| format!( + "
  • {task}
  • " + )) + .collect::>() + .concat() + )); + } + _ => { + html.push_str(&format!( + "
  • {parent}

      {}
  • ", + tasks.into_iter() + .map(|task| format!("
  • {task}
  • ")) + .collect::>() + .concat() + )); + } + } + } + + html.push_str("
"); + html +} + +#[tauri::command] +pub fn export_to_pdf() { + let active_tasks = get_all_tasks(FetchBasis::Active); + let completed_tasks = get_all_tasks(FetchBasis::Completed); + + let active_tasks_inp: Vec<(String, String)> = get_python_input(&active_tasks); + let completed_tasks_inp: Vec<(String, String)> = get_python_input(&completed_tasks); + + let pdf_html = format!( + " + + + + + Tasks for Today + +

Active Tasks

+ {} +
+

Completed Tasks

+ {} + + ", + generate_ordered_list(map_parent_to_tasks(active_tasks_inp), false), + generate_ordered_list(map_parent_to_tasks(completed_tasks_inp), true) + ); + + let pdf_css = format!( + " + @import url(\"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap\"); + + body {{ + font-family: 'Inter', sans-serif; + }} + + h1 {{ + text-align: center; + }} + + ul {{ + list-style-type: none; + }} + + li {{ + margin-top: 20px; + margin-bottom: 20px; + }} + + p {{ + font-weight: bold; + }} + + input:checked:after {{ + color: black; + content: '✔'; + }} + + .sub-task {{ + display: flex; + flex-direction: row; + justify-items: space-between; + align-items: center; + }} + + span {{ + margin-left: 10px; + }} + + .page-break {{ + page-break-after: always; + }} + " + ); + + let output = Command::new("python3") + .arg("../scripts/task_view_to_pdf.py") + .args([pdf_html, pdf_css]) + .output() + .expect("ok the python script idea didn't work"); + + if !output.status.success() { + eprintln!("[ERROR]: {}", String::from_utf8_lossy(&output.stderr)); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 375c353..d7f5411 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,2 +1,3 @@ pub mod db; -pub mod window; \ No newline at end of file +pub mod window; +pub mod export; \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6f8f04a..80761ad 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,7 +3,7 @@ // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command -use habtrack::{db::{init::DbInitializer, ops}, window}; +use habtrack::{db::{init::DbInitializer, ops}, window, export}; // Start work on database. // TODO: Once done, merge with main, and pull changes into FEATURE-add-delete-task. @@ -27,6 +27,7 @@ fn main() { ops::crud_commands::update_item, window::open_tomorrow_window, window::close_tomorrow_window, + export::export_to_pdf, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/Constants.jsx b/src/Constants.jsx index d3d55e2..e0f8f65 100644 --- a/src/Constants.jsx +++ b/src/Constants.jsx @@ -25,6 +25,9 @@ const TAURI_UPDATE_ITEM = "update_item"; const TAURI_OPEN_TOMORROW_WINDOW = "open_tomorrow_window"; const TAURI_CLOSE_TOMORROW_WINDOW = "close_tomorrow_window"; +// Export. +const TAURI_EXPORT_TO_PDF = "export_to_pdf"; + export { // Table names TODAY, @@ -53,4 +56,7 @@ export { // New Window, TAURI_OPEN_TOMORROW_WINDOW, TAURI_CLOSE_TOMORROW_WINDOW, + + // Export + TAURI_EXPORT_TO_PDF, }; diff --git a/src/views/TasksView/Navbar.jsx b/src/views/TasksView/Navbar.jsx index fab5cde..affe6ca 100644 --- a/src/views/TasksView/Navbar.jsx +++ b/src/views/TasksView/Navbar.jsx @@ -2,12 +2,13 @@ import React from "react"; import PlaylistAddCheckIcon from "@mui/icons-material/PlaylistAddCheck"; import FastForwardIcon from "@mui/icons-material/FastForward"; import AddIcon from "@mui/icons-material/Add"; +import { Download } from "@mui/icons-material"; import { invoke } from "@tauri-apps/api"; import { ROOT, TAURI_OPEN_TOMORROW_WINDOW } from "../../Constants"; // TODO: style this component. -const Navbar = ({ isSidebarOpen, onAdd, toggleCompleted }) => { +const Navbar = ({ isSidebarOpen, onExport, onAdd, toggleCompleted }) => { const seeTomorrow = async () => { try { await invoke(TAURI_OPEN_TOMORROW_WINDOW); @@ -29,6 +30,11 @@ const Navbar = ({ isSidebarOpen, onAdd, toggleCompleted }) => { Tasks

    +
  • + +