diff --git a/Cargo.lock b/Cargo.lock index 211c288..b89795c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,7 +212,7 @@ dependencies = [ "pathdiff", "serde", "toml", - "winnow", + "winnow 0.7.10", ] [[package]] @@ -1807,6 +1807,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "sqlformat" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d7b3e8a3b6f2ee93ac391a0f757c13790caa0147892e3545cd549dd5b54bc0" +dependencies = [ + "unicode_categories", + "winnow 0.6.26", +] + [[package]] name = "sqlight" version = "0.1.0" @@ -1829,6 +1839,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "split-grid", + "sqlformat", "sqlite-wasm-rs", "thiserror 2.0.12", "tokio", @@ -2041,7 +2052,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.7.10", ] [[package]] @@ -2088,6 +2099,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "url" version = "2.5.4" @@ -2349,6 +2366,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.10" diff --git a/Cargo.toml b/Cargo.toml index 41109f6..b49cae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,4 @@ log = "0.4.27" fragile = "2.0.1" hex = "0.4.3" prettytable-rs = "0.10.0" +sqlformat = "0.3.5" diff --git a/assets/module.css/button_menu_item.module.css b/assets/module.css/button_menu_item.module.css new file mode 100644 index 0000000..f32e439 --- /dev/null +++ b/assets/module.css/button_menu_item.module.css @@ -0,0 +1,38 @@ +.-buttonReset { + color: var(--font-color); + border: none; + background: inherit; + background-color: transparent; /* IE 11 */ + padding: 0; + font: inherit; + line-height: inherit; + text-align: inherit; +} + +.-menuItemFullButton { + composes: -buttonReset; + transition: color var(--header-transition); + width: 100%; + user-select: text; +} + +.container { + composes: -menuItemFullButton; + + &:hover { + color: var(--header-tint); + } +} + +.-menuItemTitle { + font-weight: 600; +} + +.name { + composes: -menuItemTitle; + margin: 0; +} + +.description { + margin: 0; +} diff --git a/assets/module.css/menu_aside.module.css b/assets/module.css/menu_aside.module.css new file mode 100644 index 0000000..0ec000c --- /dev/null +++ b/assets/module.css/menu_aside.module.css @@ -0,0 +1,4 @@ +.aside { + margin: 0.25em 0 0; + color: #888; +} diff --git a/crates/aceditor/src/lib.rs b/crates/aceditor/src/lib.rs index d5b9643..608ded1 100644 --- a/crates/aceditor/src/lib.rs +++ b/crates/aceditor/src/lib.rs @@ -28,6 +28,9 @@ mod bindgen { #[wasm_bindgen(method, js_name = getValue)] pub fn get_value(this: &Editor) -> String; + #[wasm_bindgen(method, js_name = setValue)] + pub fn set_value(this: &Editor, value: String); + #[wasm_bindgen(method, js_name = getSelectedText)] pub fn get_selected_text(this: &Editor) -> String; } @@ -201,6 +204,10 @@ impl Editor { self.js.get_value() } + pub fn set_value(&self, value: String) { + self.js.set_value(value); + } + pub fn get_selected_value(&self) -> String { self.js.get_selected_text() } diff --git a/index.html b/index.html index 4719429..bdd53c9 100644 --- a/index.html +++ b/index.html @@ -45,6 +45,8 @@ + + diff --git a/src/app/button_menu_item.rs b/src/app/button_menu_item.rs new file mode 100644 index 0000000..75a386a --- /dev/null +++ b/src/app/button_menu_item.rs @@ -0,0 +1,25 @@ +use istyles::istyles; +use leptos::prelude::*; +use web_sys::MouseEvent; + +use crate::app::menu_item::MenuItem; + +istyles!( + styles, + "assets/module.postcss/button_menu_item.module.css.map" +); + +#[component] +pub fn ButtonMenuItem(name: String, on_click: C, children: Children) -> impl IntoView +where + C: Fn(MouseEvent) + Send + 'static, +{ + view! { + + + + } +} diff --git a/src/app/database_menu.rs b/src/app/database_menu.rs new file mode 100644 index 0000000..1f4abe3 --- /dev/null +++ b/src/app/database_menu.rs @@ -0,0 +1,22 @@ +use leptos::prelude::*; +use web_sys::MouseEvent; + +use crate::app::{button_menu_item::ButtonMenuItem, menu_aside::MenuAside, menu_group::MenuGroup}; + +#[component] +pub fn DatabaseMenu(load: L, download: D) -> impl IntoView +where + L: Fn(MouseEvent) + Send + 'static, + D: Fn(MouseEvent) + Send + 'static, +{ + view! { + + + "The KEEP CONTEXT will be automatically enabled." + + + "Will be downloaded as test.db" + + + } +} diff --git a/src/app/header.rs b/src/app/header.rs index 78f3acb..fdfc900 100644 --- a/src/app/header.rs +++ b/src/app/header.rs @@ -3,7 +3,7 @@ use leptos::{html::Input, prelude::*, tachys::html}; use prettytable::{Cell, Row, Table}; use reactive_stores::Store; use wasm_bindgen::{JsCast, prelude::Closure}; -use web_sys::{Blob, Event, FileReader, HtmlInputElement, Url, UrlSearchParams}; +use web_sys::{Blob, Event, FileReader, HtmlInputElement, MouseEvent, Url, UrlSearchParams}; use crate::{ FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, SQLiteStatementResult, @@ -14,10 +14,12 @@ use crate::{ button_set::{Button, ButtonSet, IconButton, LinkButton, Rule}, config_menu::ConfigMenu, context_menu::ContextMenu, + database_menu::DatabaseMenu, icon::{build_icon, config_icon, expandable_icon, github_icon, more_options_icon}, output::change_focus, pop_button::PopButton, state::{Focus, GlobalState, GlobalStateStoreFields}, + tools_menu::ToolsMenu, vfs_menu::VfsMenu, }, }; @@ -48,14 +50,16 @@ pub fn Header() -> impl IntoView {
- - - - - + + + + + + + @@ -114,140 +118,6 @@ fn ExecuteButton() -> impl IntoView { } } -#[component] -fn DownloadButton() -> impl IntoView { - let state = expect_context::>(); - - Effect::new(move || { - state.exported().track(); - - let Some(downloaded) = state.exported().write_untracked().take() else { - return; - }; - let filename = downloaded.filename; - let buffer = downloaded.data; - let array = js_sys::Array::new(); - array.push(&buffer); - - let blob = Blob::new_with_u8_array_sequence(&array).unwrap(); - let url = Url::create_object_url_with_blob(&blob).unwrap(); - - let document = document(); - let a = document - .create_element("a") - .unwrap() - .dyn_into::() - .unwrap(); - - a.set_href(&url); - a.set_download(&filename); - a.click(); - - Url::revoke_object_url(&url).unwrap(); - }); - - let on_click = move |_| { - if let Some(worker) = &*state.worker().read() { - worker.send_task(WorkerRequest::DownloadDb); - } - }; - - view! { } -} - -#[component] -fn LoadButton(input_ref: NodeRef) -> impl IntoView { - let state = expect_context::>(); - - let (file, set_file) = signal::>>(None); - - Effect::new(move || { - if let Some(file) = &*file.read() { - let filename = file.name(); - - if let Ok(reader) = FileReader::new() { - let on_progress = FragileComfirmed::new(Closure::wrap(Box::new( - move |ev: web_sys::ProgressEvent| { - if ev.length_computable() { - state.import_progress().set(Some(ImportProgress { - filename: filename.clone(), - loaded: ev.loaded(), - total: ev.total(), - opened: None, - })); - } - }, - ) - as Box)); - - let on_error = FragileComfirmed::new(Closure::wrap(Box::new( - move |ev: web_sys::ProgressEvent| { - let target = ev.target().unwrap(); - let reader = target.unchecked_into::(); - let dom_error = reader.error().unwrap(); - state.last_error().set(Some(FragileComfirmed::new( - SQLightError::ImportDb(dom_error.message().to_string()), - ))); - }, - ) - as Box)); - - let on_load = - FragileComfirmed::new(Closure::wrap(Box::new(move |ev: web_sys::Event| { - let target = ev.target().unwrap(); - let reader = target.unchecked_into::(); - let result = reader.result().unwrap(); - let array_buffer = result.unchecked_into::(); - let data = js_sys::Uint8Array::new(&array_buffer); - if let Some(worker) = &*state.worker().read() { - worker.send_task(WorkerRequest::LoadDb(LoadDbOptions { data })); - } - }) - as Box)); - - reader.set_onprogress(Some(on_progress.as_ref().unchecked_ref())); - reader.set_onerror(Some(on_error.as_ref().unchecked_ref())); - reader.set_onload(Some(on_load.as_ref().unchecked_ref())); - reader.read_as_array_buffer(file).unwrap(); - - on_cleanup(move || { - drop(on_progress); - drop(on_error); - drop(on_load); - }); - } - } - }); - - let on_change = move |ev: Event| { - if let Some(target) = ev.target() { - let input = target.unchecked_into::(); - if let Some(files) = input.files() { - if files.length() > 0 { - let file = FragileComfirmed::new(files.get(0).unwrap()); - set_file.set(Some(file)); - } else { - set_file.set(None); - } - } - } - }; - - let on_click = move |_| { - if let Some(input) = &*input_ref.read() { - if input.onchange().is_none() { - let callback = Closure::wrap(Box::new(on_change) as Box); - input.set_onchange(Some(callback.as_ref().unchecked_ref::())); - callback.forget(); - } - input.set_value(""); - input.click(); - } - }; - - view! { } -} - #[component] fn VfsMenuButton(menu_container: NodeRef) -> impl IntoView { let state = expect_context::>(); @@ -337,6 +207,173 @@ fn AdvancedOptionsMenuButton(menu_container: NodeRef) -> imp } } +#[component] +fn ToolsButton(menu_container: NodeRef) -> impl IntoView { + let button = |toggle, node_ref| { + view! { + + } + .into_any() + }; + + view! { + }.into_any() }) + menu_container=menu_container + > + } +} + +#[component] +fn DatabaseButton( + input_ref: NodeRef, + menu_container: NodeRef, +) -> impl IntoView { + let state = expect_context::>(); + + let button = |toggle, node_ref| { + view! { + + } + .into_any() + }; + + let (file, set_file) = signal::>>(None); + + Effect::new(move || { + if let Some(file) = &*file.read() { + let filename = file.name(); + + if let Ok(reader) = FileReader::new() { + let on_progress = FragileComfirmed::new(Closure::wrap(Box::new( + move |ev: web_sys::ProgressEvent| { + if ev.length_computable() { + state.import_progress().set(Some(ImportProgress { + filename: filename.clone(), + loaded: ev.loaded(), + total: ev.total(), + opened: None, + })); + } + }, + ) + as Box)); + + let on_error = FragileComfirmed::new(Closure::wrap(Box::new( + move |ev: web_sys::ProgressEvent| { + let target = ev.target().unwrap(); + let reader = target.unchecked_into::(); + let dom_error = reader.error().unwrap(); + state.last_error().set(Some(FragileComfirmed::new( + SQLightError::ImportDb(dom_error.message().to_string()), + ))); + }, + ) + as Box)); + + let on_load = + FragileComfirmed::new(Closure::wrap(Box::new(move |ev: web_sys::Event| { + let target = ev.target().unwrap(); + let reader = target.unchecked_into::(); + let result = reader.result().unwrap(); + let array_buffer = result.unchecked_into::(); + let data = js_sys::Uint8Array::new(&array_buffer); + if let Some(worker) = &*state.worker().read_untracked() { + worker.send_task(WorkerRequest::LoadDb(LoadDbOptions { data })); + } + }) + as Box)); + + reader.set_onprogress(Some(on_progress.as_ref().unchecked_ref())); + reader.set_onerror(Some(on_error.as_ref().unchecked_ref())); + reader.set_onload(Some(on_load.as_ref().unchecked_ref())); + reader.read_as_array_buffer(file).unwrap(); + + on_cleanup(move || { + drop(on_progress); + drop(on_error); + drop(on_load); + }); + } + } + }); + + let on_change = move |ev: Event| { + if let Some(target) = ev.target() { + let input = target.unchecked_into::(); + if let Some(files) = input.files() { + if files.length() > 0 { + let file = FragileComfirmed::new(files.get(0).unwrap()); + set_file.set(Some(file)); + } else { + set_file.set(None); + } + } + } + }; + + let on_load = move |_: MouseEvent| { + if let Some(input) = &*input_ref.read() { + if input.onchange().is_none() { + let callback = Closure::wrap(Box::new(on_change) as Box); + input.set_onchange(Some(callback.as_ref().unchecked_ref::())); + callback.forget(); + } + input.set_value(""); + input.click(); + } + }; + + Effect::new(move || { + state.exported().track(); + + let Some(downloaded) = state.exported().write_untracked().take() else { + return; + }; + let filename = downloaded.filename; + let buffer = downloaded.data; + let array = js_sys::Array::new(); + array.push(&buffer); + + let blob = Blob::new_with_u8_array_sequence(&array).unwrap(); + let url = Url::create_object_url_with_blob(&blob).unwrap(); + + let document = document(); + let a = document + .create_element("a") + .unwrap() + .dyn_into::() + .unwrap(); + + a.set_href(&url); + a.set_download(&filename); + a.click(); + + Url::revoke_object_url(&url).unwrap(); + }); + + let on_download = move |_| { + if let Some(worker) = &*state.worker().read() { + worker.send_task(WorkerRequest::DownloadDb); + } + }; + + view! { + }.into_any() + }) + menu_container=menu_container + > + } +} + #[component] fn ShareButton() -> impl IntoView { let state = expect_context::>(); diff --git a/src/app/menu_aside.rs b/src/app/menu_aside.rs new file mode 100644 index 0000000..ac50e55 --- /dev/null +++ b/src/app/menu_aside.rs @@ -0,0 +1,9 @@ +use istyles::istyles; +use leptos::prelude::*; + +istyles!(styles, "assets/module.postcss/menu_aside.module.css.map"); + +#[component] +pub fn MenuAside(children: Children) -> impl IntoView { + view! {

{children()}

} +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 1b24d4e..f5390ad 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,12 +1,15 @@ mod advanced_options_menu; +mod button_menu_item; mod button_set; mod config_element; mod config_menu; mod context_menu; +mod database_menu; mod editor; mod header; mod icon; mod loader; +mod menu_aside; mod menu_group; mod menu_item; mod output; @@ -15,6 +18,7 @@ mod pop_button; mod select_one; mod selectable_menu_item; mod state; +mod tools_menu; mod vfs_menu; pub use playground::playground; diff --git a/src/app/state.rs b/src/app/state.rs index 77a6148..2c88fff 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -20,7 +20,7 @@ INSERT INTO blobs(data) VALUES (randomblob(12)); SELECT 'Hello World!', datetime('now','localtime') AS TM, - x'73716c69676874' AS BLOB_VAL, + unhex('73716c69676874') AS BLOB_VAL, NULL as NULL_VAL; SELECT * FROM blobs;"; diff --git a/src/app/tools_menu.rs b/src/app/tools_menu.rs new file mode 100644 index 0000000..423d056 --- /dev/null +++ b/src/app/tools_menu.rs @@ -0,0 +1,41 @@ +use leptos::prelude::*; +use reactive_stores::Store; +use sqlformat::{FormatOptions, QueryParams}; + +use crate::app::{ + GlobalState, GlobalStateStoreFields, button_menu_item::ButtonMenuItem, menu_aside::MenuAside, + menu_group::MenuGroup, +}; + +#[component] +pub fn ToolsMenu() -> impl IntoView { + let state = expect_context::>(); + let format = move |_event| { + let Some(editor) = &*state.editor().read() else { + return; + }; + + let format_options = FormatOptions { + uppercase: Some(true), + lines_between_queries: 2, + ..Default::default() + }; + + let sql = sqlformat::format( + &editor.get_value(), + &QueryParams::default(), + &format_options, + ); + + editor.set_value(sql); + }; + + view! { + + +
Format this code with sqlformat.
+ "https://crates.io/crates/sqlformat" +
+
+ } +}