From 796e215f3fa248b5663cda4a097282dd7743e5b6 Mon Sep 17 00:00:00 2001 From: sekoia Date: Mon, 10 Apr 2023 00:35:20 +0200 Subject: [PATCH] Settings and toasts 1.0.0 is just around the corner! Just need to fix the styling in the settings page, add syntax highlighting, and make the autocomplete a bit better! --- README.md | 9 +++--- src/main.rs | 81 +++++++++++++++++++++++++++++++++++++++--------- ui/common.css | 42 +++++++++++++++++++++++++ ui/index.css | 2 +- ui/index.html | 3 +- ui/index.js | 72 +++++++++++++++++++++++++++++++++++++----- ui/settings.css | 35 ++++++++++++++++++++- ui/settings.html | 13 ++++++++ ui/settings.js | 23 +++++++++++++- 9 files changed, 251 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 47f63a1..30d6e21 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ Originally almost identical to the web version, but now has several added featur ### TODO Ordered by probable priority -- [ ] Add extra commands like Ctrl-S, Ctrl-O -- [ ] Add options like config-defined functions - [ ] Better autocomplete with variables - [ ] Syntax highlighting @@ -14,9 +12,12 @@ Ordered by probable priority - Slight syntax highlighting! - Input hints and autocompletion! (slightly minimal currently due to library constraints) - Shortcuts! - - Ctrl-W quits the program (will be configurable; can also be Ctrl-D or both) - - Ctrl-C copies the current input (if nothing is selected) + - Ctrl+W or Ctrl+D quits the program + - Ctrl+C copies the current input (if nothing is selected) - This will be configurable in the future, with options like previous result, hint or input + - Ctrl+S saves all the calculations you've run (can be limited to a certain amount) to a file. One line per calculation. + - Ctrl+O loads a file saved that way, and runs all the calculations in it. - A nice UI! (mostly taken from the website) +- A load of configurable settings for all your function needs! Attribution: Settings cog taken from: https://game-icons.net/1x1/lorc/cog.html \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 4a42f03..2f04dbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,11 @@ windows_subsystem = "windows" )] use std::collections::HashMap; +use std::fs; use std::fs::File; -use std::io::BufReader; +use std::io::{BufRead, BufReader, Write}; use std::path::PathBuf; -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; use std::time::Instant; use dialog::{DialogBox, FileSelectionMode}; use serde::{Deserialize, Serialize}; @@ -49,18 +50,38 @@ fn copy_to_clipboard(value: String, app_handle: AppHandle) { } #[tauri::command] -fn save_to_file(_state: tauri::State) -> Result<(), String> { +fn load_from_file() -> Result<(Vec, bool), String> { + let dialog = dialog::FileSelection::new("Select file to open") + .mode(FileSelectionMode::Open) + .title("Open a list of things to run") + .show().map_err(|x| x.to_string())?; + + if let Some(x) = dialog { + let file = BufReader::new(File::open(x).map_err(|e| e.to_string())?); + return Ok((file.lines().filter_map(|x| x.ok()).collect(), false)); + } + + Ok((vec![], true)) +} + +#[tauri::command] +fn save_to_file(input: Vec) -> Result { let dialog = dialog::FileSelection::new("Select file to save to") .mode(FileSelectionMode::Save) - .title("Saving variables") + .title("Saving input") .show().map_err(|x| x.to_string())?; - // TODO: error reporting; toasts for error, cancelled, and success - if let Some(_x) = dialog { - todo!() // we need to fork fend to get access to variables and such. - } + if let Some(x) = dialog { + let mut file = File::create(x).map_err(|e| e.to_string())?; - Ok(()) + for i in input { + writeln!(file, "{}", i).map_err(|e| e.to_string())?; + } + + Ok(true) + } else { + Ok(false) + } } #[tauri::command] @@ -115,18 +136,40 @@ async fn open_settings(handle: AppHandle) { }) } +const SETTINGS_CORRUPTED: &str = "The settings were corrupted. Should never happen, please report."; + #[tauri::command] fn set_setting(id: String, value: serde_json::Value, settings: tauri::State) { - settings.0.lock().expect("The settings were corrupted. Should never happen, please report.")[id] = value; + settings.0.lock().expect(SETTINGS_CORRUPTED)[id] = value; +} + +#[tauri::command] +fn save_settings(settings: tauri::State) -> Result<(), String> { + if let Some(x) = tauri::api::path::config_dir() { + let settings : MutexGuard = settings.0.lock().expect(SETTINGS_CORRUPTED); + fs::create_dir_all(x.join("fendesk")).map_err(|e| e.to_string())?; + let mut file = File::create(x.join("fendesk/settings.json")).map_err(|e| e.to_string())?; + file.write(serde_json::to_string(&*settings).map_err(|e| e.to_string())?.as_bytes()).map_err(|e| e.to_string())?; + Ok(()) + } else { + Err("Configuration folder not found!".to_string()) + } } #[tauri::command] fn get_settings(settings: tauri::State) -> serde_json::Value { - settings.0.lock().expect("The settings were corrupted. Should never happen, please report.").clone() + settings.0.lock().expect(SETTINGS_CORRUPTED).clone() } fn main() { let context = create_context(); + let default_settings = json!({ + "ctrl_d_closes": true, + "ctrl_w_closes": false, + "save_back_count": -1, + "ctrl_c_behavior": "input", + "global_inputs": "", + }); tauri::Builder::default() .setup(|app| { @@ -141,16 +184,26 @@ fn main() { Ok(()) }) .manage(FendContext(Mutex::new(context))) - .manage(SettingsState(Mutex::new(json!({})))) + .manage(SettingsState(Mutex::new(get_saved_settings().unwrap_or(default_settings)))) .invoke_handler(tauri::generate_handler![ setup_exchanges, fend_prompt, fend_preview_prompt, fend_completion, // core fend - quit, copy_to_clipboard, save_to_file, // ctrl- shortcuts - open_settings, set_setting, get_settings // settings + quit, copy_to_clipboard, save_to_file, load_from_file, // ctrl- shortcuts + open_settings, set_setting, get_settings, save_settings // settings ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } +fn get_saved_settings() -> Result { + if let Some(x) = tauri::api::path::config_dir() { + fs::create_dir_all(x.join("fendesk")).map_err(|_| ())?; + let file = File::open(x.join("fendesk/settings.json")).map_err(|_| ())?; + serde_json::from_reader(file).map_err(|_| ()) + } else { + Err(()) + } +} + fn create_context() -> fend_core::Context { let mut context = fend_core::Context::new(); let current_time = chrono::Local::now(); diff --git a/ui/common.css b/ui/common.css index 653e70b..55b9c72 100644 --- a/ui/common.css +++ b/ui/common.css @@ -68,4 +68,46 @@ b { ::-webkit-scrollbar-thumb:hover { background: #606060; border: 0; +} + +#toast { + visibility: hidden; + min-width: 250px; + margin-left: -125px; + text-align: center; + border-radius: 1em; + padding: 8px; + position: fixed; + z-index: 1; + left: 50%; + bottom: 30px; + + color: hsl(30, 25%, 10%); +} + +#toast.ok { + background-color: hsl(90, 70%, 30%); +} + +#toast.note { + background-color: hsl(30, 25%, 55%); +} + +#toast.error { + background-color: hsl(0, 75%, 40%); +} + +#toast.show { + visibility: visible; + animation: toast_fadein 0.5s, toast_fadeout 0.5s 2.5s; +} + +@keyframes toast_fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@keyframes toast_fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} } \ No newline at end of file diff --git a/ui/index.css b/ui/index.css index bb7169f..01a23a3 100644 --- a/ui/index.css +++ b/ui/index.css @@ -44,7 +44,7 @@ p, textarea { display: grid; grid-template-columns: 2ch 1fr; grid-template-rows: auto auto; - min-height: 4.5em; /* just enough for two liens of text. Not super clean but should be robust enough with different font sizes? */ + min-height: 4.5em; /* just enough for two lines of text. Not super clean but should be robust enough with different font sizes? */ } #input p { diff --git a/ui/index.html b/ui/index.html index 1ff7492..e5dc897 100644 --- a/ui/index.html +++ b/ui/index.html @@ -37,7 +37,8 @@ -
+
+ \ No newline at end of file diff --git a/ui/index.js b/ui/index.js index 9eee7b7..2de10fb 100644 --- a/ui/index.js +++ b/ui/index.js @@ -8,6 +8,7 @@ let inputHint = document.getElementById("input-hint"); let inputHighlighting = document.getElementById("highlighting"); let input = document.getElementById("input"); let settingsIcon = document.getElementById("settings-icon"); +let toast = document.getElementById("toast"); let history = []; let navigation = 0; @@ -19,11 +20,19 @@ async function get_settings() { let settings; -get_settings(); +get_settings().then(async () => { + if (typeof settings["global_inputs"] === typeof "") { + let split = settings["global_inputs"].split("\n"); + for (let i = 0; i < split.length; i++) { + await evaluateFendWithTimeout(split[i], 500); + } + } +}); window.__TAURI__.event.listen('settings-closed', () => { settingsIcon.style.opacity = ""; get_settings(); + invoke("save_settings").catch(x => set_toast("Error saving settings: " + x, "error")); }); invoke("setup_exchanges"); @@ -45,6 +54,16 @@ function open_settings() { invoke("open_settings"); } +function set_toast(text, type) { + toast.innerText = text; + + // Add the "show" class to DIV + toast.className = "show " + type; + + // After 3 seconds, remove the show class from DIV + setTimeout(function(){ toast.className = ""; }, 3000); +} + async function commands(event) { if (!event.ctrlKey) { return; @@ -53,13 +72,52 @@ async function commands(event) { if ((event.key === "w" && settings["ctrl_w_closes"]) || (event.key === "d" && settings["ctrl_d_closes"])) { invoke("quit"); } else if (event.key === "c" && document.getSelection().isCollapsed) { - invoke("copy_to_clipboard", {"value": inputText.value}) - // TODO; optionally make this copy other values, such as the hint or the previous output - // Also add a toast (https://www.w3schools.com/howto/howto_js_snackbar.asp) + let clipboard_text; + if (settings["ctrl_c_behavior"] === "prev_result") { + clipboard_text = output.lastChild.innerText; + if (clipboard_text === undefined) { + return; + } + } else if (settings["ctrl_c_behavior"] === "hint") { + clipboard_text = inputHint.innerText; + } else { + clipboard_text = inputText.value; + } + invoke("copy_to_clipboard", {"value": clipboard_text}); + set_toast("Copied to clipboard!", "note"); } else if (event.key === "s") { - invoke("save_to_file") + let history_segment; + if (settings["save_back_count"] < 0) { + history_segment = history; + } else { + history_segment = history.slice(-settings["save_back_count"]); + } + invoke("save_to_file", {"input": history_segment}).then(x => { + if (x) { + set_toast("Successfully saved file!", "ok"); + } else { + set_toast("Cancelled!", "note") + } + }).catch(x => set_toast("Error saving file: " + x, "error")); } else if (event.key === "o") { - // TODO + let inputs = await invoke("load_from_file").then(x => { + console.log(x); + if (x[1]) { + set_toast("Cancelled!", "note"); + } + return x[0]; + }).catch(x => { + set_toast("Error loading file: " + x, "error"); + return []; + }); + + if (inputs.length === 0) { + return; + } + for (let i = 0; i < inputs.length; i++) { + await evaluateFendWithTimeout(inputs[i], 500); + } + set_toast("Finished loading file!", "ok"); } } @@ -206,7 +264,7 @@ async function updateReplicatedText() { + hint; } -function updateHint() { +async function updateHint() { evaluateFendPreviewWithTimeout(inputText.value, 100).then(x => { inputHint.className = "valid-hint"; setHintInnerText(x); diff --git a/ui/settings.css b/ui/settings.css index 73805df..eb184d3 100644 --- a/ui/settings.css +++ b/ui/settings.css @@ -2,16 +2,49 @@ body { display: grid; grid-template-columns: auto auto; align-items: center; + padding: 10px 1em; } label { grid-column: 1; } -input { +input, textarea, select { grid-column: 2; } +textarea { + line-height: inherit; + font-family: inherit; + outline: none; + overflow: scroll; + resize: none; + + border: 1px solid var(--highlight); + border-radius: 5px; +} + +select { + appearance: none; + + border: 1px solid var(--highlight); + border-radius: 5px; + + background-color: var(--bg); + padding: 1px 0.75ch; +} + +select:after { + content: 'v'; +} + +input[type="number"] { + appearance: none; + border: 1px solid var(--highlight); + border-radius: 5px; + padding: 1px 0.75ch; +} + input[type="checkbox"] { appearance: none; position: relative; diff --git a/ui/settings.html b/ui/settings.html index 51d30c7..7a84da4 100644 --- a/ui/settings.html +++ b/ui/settings.html @@ -16,6 +16,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/settings.js b/ui/settings.js index cb3c9b2..552bceb 100644 --- a/ui/settings.js +++ b/ui/settings.js @@ -1,7 +1,26 @@ const invoke = window.__TAURI__.invoke; +async function apply_settings() { + invoke("get_settings").then(x => { + for (let key in x) { + let input = document.getElementById(key); + + if (input === null) { + console.error("Unknown settings key: " + key); + continue; + } + + if (input.getAttribute("type") === "checkbox") { + input.checked = x[key]; + } else { + input.value = x[key]; + } + } + }) +} + (function() { - let ranges = document.querySelectorAll("input"); + let ranges = document.querySelectorAll("input, select"); for (let i = 0; i < ranges.length; i++) { if (ranges[i].getAttribute("type") === "checkbox") { ranges[i].addEventListener("input", function (e) { @@ -13,4 +32,6 @@ const invoke = window.__TAURI__.invoke; }); } } + + apply_settings(); })(); \ No newline at end of file