Files
sqlight/src/app/header.rs
2025-05-27 01:04:02 +08:00

447 lines
14 KiB
Rust

use istyles::istyles;
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, MouseEvent, Url, UrlSearchParams};
use crate::{
FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, SQLiteStatementResult,
WorkerRequest,
app::{
ImportProgress,
advanced_options_menu::AdvancedOptionsMenu,
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,
},
};
istyles!(styles, "assets/module.postcss/header.module.css.map");
#[component]
pub fn Header() -> impl IntoView {
let menu_container = NodeRef::new();
let input_ref = NodeRef::new();
view! {
<>
<div id="header" class=styles::container>
<div class=styles::left>
<ButtonSet>
<ExecuteButton />
</ButtonSet>
<ButtonSet>
<VfsMenuButton menu_container=menu_container />
<Rule />
<ContextMenuButton menu_container=menu_container />
<Rule />
<AdvancedOptionsMenuButton menu_container=menu_container />
</ButtonSet>
</div>
<div class=styles::right>
<input type="file" node_ref=input_ref style="display: none" />
<ButtonSet>
<ShareButton />
</ButtonSet>
<ButtonSet>
<DatabaseButton input_ref=input_ref menu_container=menu_container />
</ButtonSet>
<ButtonSet>
<ToolsButton menu_container=menu_container />
</ButtonSet>
<ButtonSet>
<ConfigMenuButton menu_container=menu_container />
</ButtonSet>
<ButtonSet>
<GithubButton />
</ButtonSet>
</div>
</div>
<div node_ref=menu_container></div>
</>
}
}
pub fn execute(state: Store<GlobalState>) -> Box<dyn Fn() + Send + 'static> {
Box::new(move || {
let Some((code, selected_code)) = state
.editor()
.read_untracked()
.as_ref()
.map(|editor| (editor.get_value(), editor.get_selected_value()))
else {
return;
};
let run_selected_code = state.run_selected_sql().get();
state.sql().set(code.clone());
change_focus(state, Some(Focus::Execute));
std::mem::take(&mut *state.output().write());
if let Some(worker) = &*state.worker().read_untracked() {
worker.send_task(WorkerRequest::Prepare(PrepareOptions {
sql: if !selected_code.is_empty() && run_selected_code {
selected_code
} else {
code
},
clear_on_prepare: !*state.keep_ctx().read_untracked(),
}));
worker.send_task(WorkerRequest::Continue);
}
})
}
#[component]
fn ExecuteButton() -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let on_click = execute(state);
view! {
<Button is_primary=true icon_right=build_icon() on_click=move |_| on_click()>
"Run"
</Button>
}
}
#[component]
fn VfsMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let button = move |toggle, node_ref| {
view! {
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
{move || state.vfs().read().value()}
</Button>
}
.into_any()
};
view! {
<PopButton
button=button
menu=Box::new(|_close| { view! { <VfsMenu /> }.into_any() })
menu_container=menu_container
></PopButton>
}
}
#[component]
fn ContextMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let button = move |toggle, node_ref| {
view! {
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
{move || if *state.keep_ctx().read() { "Keep Context" } else { "Drop Context" }}
</Button>
}
.into_any()
};
view! {
<PopButton
button=button
menu=Box::new(|_close| { view! { <ContextMenu /> }.into_any() })
menu_container=menu_container
></PopButton>
}
}
#[component]
fn ConfigMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let button = |toggle, node_ref| {
view! {
<Button
icon_left=config_icon()
icon_right=expandable_icon()
on_click=toggle
node_ref=node_ref
>
"Config"
</Button>
}
.into_any()
};
view! {
<PopButton
button=button
menu=Box::new(|_close| { view! { <ConfigMenu /> }.into_any() })
menu_container=menu_container
></PopButton>
}
}
#[component]
fn AdvancedOptionsMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let button = |toggle, node_ref| {
view! {
<IconButton on_click=toggle node_ref=node_ref>
{more_options_icon()}
</IconButton>
}
.into_any()
};
view! {
<PopButton
button=button
menu=Box::new(|_close| { view! { <AdvancedOptionsMenu /> }.into_any() })
menu_container=menu_container
></PopButton>
}
}
#[component]
fn ToolsButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let button = |toggle, node_ref| {
view! {
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
"Tools"
</Button>
}
.into_any()
};
view! {
<PopButton
button=button
menu=Box::new(|_close| { view! { <ToolsMenu /> }.into_any() })
menu_container=menu_container
></PopButton>
}
}
#[component]
fn DatabaseButton(
input_ref: NodeRef<Input>,
menu_container: NodeRef<html::element::Div>,
) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let button = |toggle, node_ref| {
view! {
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
"Database"
</Button>
}
.into_any()
};
let (file, set_file) = signal::<Option<FragileComfirmed<web_sys::File>>>(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<dyn FnMut(_)>));
let on_error = FragileComfirmed::new(Closure::wrap(Box::new(
move |ev: web_sys::ProgressEvent| {
let target = ev.target().unwrap();
let reader = target.unchecked_into::<FileReader>();
let dom_error = reader.error().unwrap();
state.last_error().set(Some(FragileComfirmed::new(
SQLightError::ImportDb(dom_error.message().to_string()),
)));
},
)
as Box<dyn FnMut(_)>));
let on_load =
FragileComfirmed::new(Closure::wrap(Box::new(move |ev: web_sys::Event| {
let target = ev.target().unwrap();
let reader = target.unchecked_into::<FileReader>();
let result = reader.result().unwrap();
let array_buffer = result.unchecked_into::<js_sys::ArrayBuffer>();
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<dyn FnMut(_)>));
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::<HtmlInputElement>();
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<dyn Fn(Event)>);
input.set_onchange(Some(callback.as_ref().unchecked_ref::<js_sys::Function>()));
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::<web_sys::HtmlAnchorElement>()
.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! {
<PopButton
button=button
menu=Box::new(move |_close| {
view! { <DatabaseMenu load=on_load download=on_download /> }.into_any()
})
menu_container=menu_container
></PopButton>
}
}
#[component]
fn ShareButton() -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let click = move |_| {
let Some(code) = state
.editor()
.read()
.as_ref()
.map(|editor| editor.get_value())
else {
return;
};
let mut sqls = vec![];
for result in &*state.output().read() {
let mut table_s = Table::new();
match result {
SQLiteStatementResult::Finish => continue,
SQLiteStatementResult::Step(table) => {
let mut sql = table.sql.trim().to_string();
if let Some(values) = &table.values {
table_s.add_row(Row::new(
values.columns.iter().map(|s| Cell::new(s)).collect(),
));
for row in &values.rows {
table_s.add_row(Row::new(row.iter().map(|s| Cell::new(s)).collect()));
}
let result = table_s
.to_string()
.lines()
.map(|x| format!("-- {x}"))
.collect::<Vec<String>>()
.join("\n");
sql.push('\n');
sql.push_str(&result);
}
sqls.push(sql);
}
}
}
state.share_sql_with_result().set(Some(sqls.join("\n\n")));
if let Ok(href) = window().location().href().and_then(|href| {
let url = Url::new(&href)?;
let params = UrlSearchParams::new()?;
params.set("code", &code);
url.set_search(&params.to_string().as_string().unwrap());
Ok(url.href())
}) {
state.share_href().set(Some(href));
}
change_focus(state, Some(Focus::Share));
};
view! { <Button on_click=click>"Share"</Button> }
}
#[component]
fn GithubButton() -> impl IntoView {
view! { <LinkButton href="https://github.com/Spxg/sqlight".into()>{github_icon()}</LinkButton> }
}