diff --git a/.gitignore b/.gitignore index 717aa10..e2a00d9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,3 @@ assets/vips/ .yarn .eslintcache .idea - -src-tauri/gen \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index 5ba1ecf..25bbe02 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -4,5 +4,4 @@ export default { singleQuote: false, printWidth: 80, tabWidth: 2, - endOfLine: "lf", }; diff --git a/package.json b/package.json index 1b60f0f..9e118e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devtools-x", - "version": "3.1.0", + "version": "3.2.0", "license": "MIT", "type": "module", "scripts": { diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index dd2a62e..76929ed 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -2,4 +2,4 @@ # will have compiled files and executables /target/ WixTools -/gen/ +gen/schemas \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cbff474..3325653 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "devtools-x" -version = "3.1.0" +version = "3.2.0" dependencies = [ "anyhow", "bardecoder", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 30d1ab7..45dc468 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "devtools-x" -version = "3.1.0" +version = "3.2.0" description = "Developer tools desktop application" authors = ["Sparkenstein"] license = "MIT" diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index 00ff1a6..4846064 100644 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -1,11 +1,5 @@ { "identifier": "desktop-capability", - "platforms": [ - "macOS", - "windows", - "linux" - ], - "permissions": [ - "updater:default" - ] -} \ No newline at end of file + "platforms": ["macOS", "windows", "linux"], + "permissions": ["updater:default"] +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d215251..bba96ac 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -22,10 +22,11 @@ use commands::qr::qr::read_qr; fn main() { // Todo move to different file - let migrations = vec![Migration { - version: 1, - description: "create_initial_tables", - sql: " + let migrations = vec![ + Migration { + version: 1, + description: "create_initial_tables", + sql: " CREATE TABLE IF NOT EXISTS snippets ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, @@ -56,8 +57,95 @@ fn main() { updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(snippet_id) REFERENCES snippets (id) );", - kind: MigrationKind::Up, - }]; + kind: MigrationKind::Up, + }, + Migration { + version: 2, + description: "new_structure", + sql: " + -- First create a temporary table with the new structure + CREATE TABLE snippets_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + code TEXT NOT NULL, + language TEXT NOT NULL, + tags TEXT, + note TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Delete the sqlite_sequence entry for snippets_new to reset AUTOINCREMENT + DELETE FROM sqlite_sequence WHERE name = 'snippets_new'; + + -- Copy and transform data from old tables to new structure + INSERT INTO snippets_new (id, title, code, language, tags, note, created_at) + SELECT + s.id, + s.name as title, + COALESCE(sf.content, '') as code, + COALESCE(sf.filetype, 'text') as language, + (SELECT GROUP_CONCAT(tag, ',') FROM snippets_tags WHERE snippet_id = s.id) as tags, + (SELECT GROUP_CONCAT(note, '\n') FROM snippets_notes WHERE snippet_id = s.id) as note, + s.created_at + FROM snippets s + LEFT JOIN snippets_files sf ON s.id = sf.snippet_id + GROUP BY s.id; + + -- Drop old tables + DROP TABLE snippets_files; + DROP TABLE snippets_notes; + DROP TABLE snippets_tags; + DROP TABLE snippets; + + -- Rename new table to snippets + ALTER TABLE snippets_new RENAME TO snippets; + + -- Update the sqlite_sequence to continue from the last used id + UPDATE sqlite_sequence + SET seq = (SELECT MAX(id) FROM snippets) + WHERE name = 'snippets'; + ", + kind: MigrationKind::Up, + }, + Migration { + version: 3, + description: "add_fts_search", + sql: " + -- Create FTS virtual table with multiple columns + CREATE VIRTUAL TABLE IF NOT EXISTS snippets_fts USING fts5( + title, + code, + note, + tags, + content='snippets', + content_rowid='id' + ); + + -- Populate FTS table with existing data + INSERT INTO snippets_fts(rowid, title, code, note, tags) + SELECT id, title, code, note, tags FROM snippets; + + -- Create triggers to keep FTS index up to date + CREATE TRIGGER snippets_ai AFTER INSERT ON snippets BEGIN + INSERT INTO snippets_fts(rowid, title, code, note, tags) + VALUES (new.id, new.title, new.code, new.note, new.tags); + END; + + CREATE TRIGGER snippets_ad AFTER DELETE ON snippets BEGIN + INSERT INTO snippets_fts(snippets_fts, rowid, title, code, note, tags) + VALUES('delete', old.id, old.title, old.code, old.note, old.tags); + END; + + CREATE TRIGGER snippets_au AFTER UPDATE ON snippets BEGIN + INSERT INTO snippets_fts(snippets_fts, rowid, title, code, note, tags) + VALUES('delete', old.id, old.title, old.code, old.note, old.tags); + INSERT INTO snippets_fts(rowid, title, code, note, tags) + VALUES (new.id, new.title, new.code, new.note, new.tags); + END; + ", + kind: MigrationKind::Up, + }, + ]; tauri::Builder::default() .plugin( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a45eb83..97df579 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -44,9 +44,14 @@ }, "productName": "devtools-x", "mainBinaryName": "devtools-x", - "version": "3.1.0", + "version": "3.2.0", "identifier": "com.fosslife.devtoolsx", "plugins": { + "sql": { + "preload": [ + "sqlite:devtools.db" + ] + }, "updater": { "endpoints": [ "https://github.com/fosslife/devtools-x/releases/latest/download/latest.json" diff --git a/src/App.tsx b/src/App.tsx index dee6f6c..da6ff27 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -69,7 +69,9 @@ const Base64Image = loadable(() => import("./Features/base64/Base64Image")); const Base64Text = loadable(() => import("./Features/base64/Base64Text")); const Quicktpe = loadable(() => import("./Features/quicktype/Quicktype")); const Ping = loadable(() => import("./Features/ping/Ping")); -const Minify = loadable(() => import("./Features/minifiers/Minify")); +const HtmlMinifier = loadable(() => import("./Features/minifiers/html")); +const CssMinifier = loadable(() => import("./Features/minifiers/css")); +const JsMinifier = loadable(() => import("./Features/minifiers/js")); const UrlParser = loadable(() => import("./Features/url/UrlParser")); const UrlEncoder = loadable(() => import("./Features/url/UrlEncoder")); const HtmlPreview = loadable( @@ -252,7 +254,9 @@ function App() { }> }> }> - }> + }> + }> + }> }> }> }> diff --git a/src/Components/Copy.tsx b/src/Components/Copy.tsx index 8057abf..38294fd 100644 --- a/src/Components/Copy.tsx +++ b/src/Components/Copy.tsx @@ -1,15 +1,15 @@ -import { Button, CopyButton, Tooltip } from "@mantine/core"; +import { Button, CopyButton, MantineSize, Tooltip } from "@mantine/core"; import {} from "@tauri-apps/api"; import { IconCheck, IconCopy } from "@tabler/icons-react"; import * as clipboard from "@tauri-apps/plugin-clipboard-manager"; -export function Copy({ - value, - label, -}: { +type CopyProps = { value: number | string; label: string; -}) { + size?: MantineSize; +}; + +export function Copy({ value, label, size }: CopyProps) { return ( {({ copied, copy }) => ( @@ -18,7 +18,7 @@ export function Copy({ leftSection={ copied ? : } - size="xs" + size={size ?? "xs"} fullWidth onClick={() => { copy(); // copy doesn't work but need this function for animation. diff --git a/src/Features/minifiers/Minify.tsx b/src/Features/minifiers/Minify.tsx deleted file mode 100644 index 1495f0c..0000000 --- a/src/Features/minifiers/Minify.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { - Button, - Checkbox, - Divider, - Group, - Select, - Stack, - Text, -} from "@mantine/core"; -import { useCallback, useEffect, useState } from "react"; -import * as prettier from "prettier"; - -import acorn from "prettier/plugins/acorn"; -// import angular from "prettier/plugins/angular"; -// import babel from "prettier/plugins/babel"; -import estree from "prettier/plugins/estree"; -// import flow from "prettier/plugins/flow"; -import graphql from "prettier/plugins/graphql"; -import html from "prettier/plugins/html"; -// import markdown from "prettier/plugins/markdown"; -import postcss from "prettier/plugins/postcss"; -// import typescript from "prettier/plugins/typescript"; -// import yaml from "prettier/plugins/yaml"; - -import { minify as cssMinify } from "csso"; -import { minify as terserMinify } from "terser"; - -import { Monaco } from "@/Components/MonacoWrapper"; -import { invoke } from "@tauri-apps/api/core"; - -const map: Record = { - css: "css", - html: "html", - javascript: "espree", -}; - -export default function Minify() { - const [lang, setLang] = useState("Javascript"); - const [output, setOutput] = useState(""); - const [mode, setMode] = useState<"Beautify" | "Minify">("Beautify"); - const [input, setInput] = useState(""); - const [ogSize, setOgSize] = useState(0); - const [minSize, setMinSize] = useState(0); - const [minifyControls, setMinifyControls] = useState({ - compress: true, - mangle: false, - }); - - useEffect(() => { - setOutput(""); - }, [mode]); - - const format = useCallback(async () => { - if (lang === "JSON") { - try { - const op = JSON.stringify(JSON.parse(input), null, 2); - setOutput(op); - } catch (e) { - setOutput("Error:" + (e as any).message); - } - return; - } - const op = await prettier.format(input, { - parser: map[lang.toLowerCase()], - plugins: [acorn, estree, graphql, html, postcss], - }); - setOutput(op); - }, [input, lang]); - - const minify = useCallback(async () => { - if (lang === "JSON") { - try { - const op = JSON.stringify(JSON.parse(input)); - setOutput(op); - } catch (e) { - setOutput("Error:" + (e as any).message); - } - return; - } - - if (lang === "CSS") { - try { - const op = cssMinify(input).css; - setOutput(op); - } catch (e) { - setOutput("Error:" + (e as any).message); - } - return; - } - - if (lang === "HTML") { - try { - const op = await invoke("minifyhtml", { input: input }); - setOutput(op); - } catch (e) { - console.error(e); - setOutput("Error:" + (e as any).message); - } - return; - } - - const { mangle, compress } = minifyControls; - try { - let op = await terserMinify(input, { - compress, - sourceMap: false, - mangle, - }); - setOutput(op.code!); - } catch (e) { - console.error(e); - setOutput("Error:" + (e as any).message); - } - }, [input, lang, minifyControls]); - - const getStrSize = (str: string) => new Blob([str]).size; - - const bytesToSize = (bytes: number) => { - const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; - if (bytes === 0) return "0 Byte"; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return (bytes / Math.pow(1024, i)).toFixed(2) + " " + sizes[i]; - }; - - useEffect(() => { - setOgSize(getStrSize(input)); - setMinSize(getStrSize(output)); - }, [input, output]); - - return ( - - - setMode(m as "Beautify" | "Minify")} - /> - - - {mode === "Minify" && ( - - {lang === "Javascript" && ( - <> - - setMinifyControls((p) => ({ - ...p, - mangle: e.currentTarget.checked, - })) - } - /> - - setMinifyControls((p) => ({ - ...p, - compress: e.currentTarget.checked, - })) - } - /> - - )} - - Original Size: {bytesToSize(ogSize)} bytes Minified Size:{" "} - {bytesToSize(minSize)} bytes - - - )} - setInput(e as string)} - language={lang.toLowerCase()} - /> - - - - ); -} - -//todo: replace prettier with some rust based formatter. OXC seems promising -//todo: html minify supports options, add them. diff --git a/src/Features/minifiers/css.tsx b/src/Features/minifiers/css.tsx new file mode 100644 index 0000000..0112304 --- /dev/null +++ b/src/Features/minifiers/css.tsx @@ -0,0 +1,70 @@ +import { Group, Select, Stack, Textarea } from "@mantine/core"; +import { useCallback, useEffect, useState } from "react"; + +import * as prettier from "prettier"; +import postcss from "prettier/plugins/postcss"; // CSS/SCSS/Less +import { minify as cssMinify } from "csso"; +import { Copy } from "@/Components/Copy"; +import { Monaco } from "@/Components/MonacoWrapper"; + +export default function CssMinifier() { + const [input, setInput] = useState(""); + const [output, setOutput] = useState(""); + const [mode, setMode] = useState("Minify"); + + const format = useCallback(async () => { + try { + const result = await prettier.format(input, { + parser: "css", + plugins: [postcss], + endOfLine: "auto", + }); + setOutput(result); + } catch (e) { + console.error(e); + setOutput("Error:" + (e as any).message); + } + }, [mode, input]); + + const minify = useCallback(async () => { + try { + const op = cssMinify(input).css; + setOutput(op); + } catch (e) { + setOutput("Error:" + (e as any).message); + } + }, [mode, input]); + + useEffect(() => { + if (mode === "Minify") minify(); + else format(); + }, [mode, input]); + + return ( + +