mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-23 16:14:53 +01:00
Compare commits
9 Commits
v2024.10.0
...
v2024.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
598bbd6f69 | ||
|
|
b19748c42e | ||
|
|
2be45d6101 | ||
|
|
d2c33f821c | ||
|
|
215fcef3ea | ||
|
|
7d97404c11 | ||
|
|
de7097ff1d | ||
|
|
0100a3983d | ||
|
|
aa82ef8636 |
976
package-lock.json
generated
976
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@
|
||||
"name": "@yaakapp-internal/plugin-runtime",
|
||||
"scripts": {
|
||||
"build": "run-p build:*",
|
||||
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=build/index.cjs",
|
||||
"build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=build/index.worker.cjs",
|
||||
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../src-tauri/vendored/plugin-runtime/index.cjs",
|
||||
"build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=../src-tauri/vendored/plugin-runtime/index.worker.cjs",
|
||||
"build:proto": "grpc_tools_node_protoc --ts_proto_out=src/gen --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false --proto_path=../proto ../proto/plugins/*.proto"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
1
scripts/.gitignore
vendored
Normal file
1
scripts/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tmp-*
|
||||
@@ -47,10 +47,11 @@ if (existsSync(binDest) && tryExecSync(`${binDest} --version`).trim() === NODE_V
|
||||
rmSync(destDir, { recursive: true, force: true });
|
||||
mkdirSync(destDir, { recursive: true });
|
||||
|
||||
(async function () {
|
||||
const url = URL_MAP[key];
|
||||
const tmpDir = path.join(__dirname, 'tmp', Date.now().toString());
|
||||
const url = URL_MAP[key];
|
||||
const tmpDir = path.join(__dirname, 'tmp-node');
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
(async function () {
|
||||
// Download GitHub release artifact
|
||||
const { filePath } = await new Downloader({ url, directory: tmpDir }).download();
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}
|
||||
console.log(`Vendoring protoc ${VERSION} for ${key}`);
|
||||
|
||||
const url = URL_MAP[key];
|
||||
const tmpDir = path.join(__dirname, 'tmp', Date.now().toString());
|
||||
const tmpDir = path.join(__dirname, 'tmp-protoc');
|
||||
const binSrc = path.join(tmpDir, SRC_BIN_MAP[key]);
|
||||
const binDst = path.join(dstDir, DST_BIN_MAP[key]);
|
||||
|
||||
@@ -47,6 +47,7 @@ if (existsSync(binDst) && tryExecSync(`${binDst} --version`).trim().includes(VER
|
||||
return;
|
||||
}
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
rmSync(dstDir, { recursive: true, force: true });
|
||||
mkdirSync(dstDir, { recursive: true });
|
||||
|
||||
@@ -64,7 +65,6 @@ mkdirSync(dstDir, { recursive: true });
|
||||
const includeSrc = path.join(tmpDir, 'include');
|
||||
const includeDst = path.join(dstDir, 'include');
|
||||
cpSync(includeSrc, includeDst, { recursive: true });
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
console.log('Downloaded protoc to', binDst);
|
||||
|
||||
2
src-tauri/.gitignore
vendored
2
src-tauri/.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
target/
|
||||
|
||||
vendored
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
"core:window:allow-is-fullscreen",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-set-decorations",
|
||||
"core:window:allow-set-title",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-theme",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text"
|
||||
]
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-start-dragging","core:window:allow-unmaximize","core:window:allow-theme","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
@@ -225,7 +225,22 @@ pub async fn send_http_request<R: Runtime>(
|
||||
|
||||
let request_body = rendered_request.body;
|
||||
if let Some(body_type) = &rendered_request.body_type {
|
||||
if request_body.contains_key("text") {
|
||||
if request_body.contains_key("query") && request_body.contains_key("variables") {
|
||||
let query = get_str_h(&request_body, "query");
|
||||
let variables = get_str_h(&request_body, "variables");
|
||||
let body = if variables.trim().is_empty() {
|
||||
format!(
|
||||
r#"{{"query":{}}}"#,
|
||||
serde_json::to_string(query).unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"{{"query":{},"variables":{variables}}}"#,
|
||||
serde_json::to_string(query).unwrap_or_default()
|
||||
)
|
||||
};
|
||||
request_builder = request_builder.body(body.to_owned());
|
||||
} else if request_body.contains_key("text") {
|
||||
let body = get_str_h(&request_body, "text");
|
||||
request_builder = request_builder.body(body.to_owned());
|
||||
} else if body_type == "application/x-www-form-urlencoded"
|
||||
@@ -498,7 +513,7 @@ fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
|
||||
if !p.enabled {
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
|
||||
if !p.name.starts_with(":") {
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ extern crate core;
|
||||
#[cfg(target_os = "macos")]
|
||||
extern crate objc;
|
||||
|
||||
use std::collections::{BTreeMap};
|
||||
use std::fs;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{create_dir_all, read_to_string, File};
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use std::{fs, panic};
|
||||
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
@@ -83,7 +83,9 @@ const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
||||
|
||||
const MIN_WINDOW_WIDTH: f64 = 300.0;
|
||||
const MIN_WINDOW_HEIGHT: f64 = 300.0;
|
||||
|
||||
const MAIN_WINDOW_PREFIX: &str = "main_";
|
||||
const OTHER_WINDOW_PREFIX: &str = "other_";
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
@@ -1649,19 +1651,83 @@ async fn cmd_list_workspaces(w: WebviewWindow) -> Result<Vec<Workspace>, String>
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_new_window(app_handle: AppHandle, url: &str) -> Result<(), String> {
|
||||
create_window(&app_handle, url);
|
||||
async fn cmd_new_child_window(
|
||||
parent_window: WebviewWindow,
|
||||
url: &str,
|
||||
label: &str,
|
||||
title: &str,
|
||||
inner_size: (f64, f64),
|
||||
) -> Result<(), String> {
|
||||
let app_handle = parent_window.app_handle();
|
||||
let label = format!("{OTHER_WINDOW_PREFIX}_{label}");
|
||||
let scale_factor = parent_window.scale_factor().unwrap();
|
||||
|
||||
let current_pos = parent_window
|
||||
.inner_position()
|
||||
.unwrap()
|
||||
.to_logical::<f64>(scale_factor);
|
||||
let current_size = parent_window
|
||||
.inner_size()
|
||||
.unwrap()
|
||||
.to_logical::<f64>(scale_factor);
|
||||
|
||||
// Position the new window in the middle of the parent
|
||||
let position = (
|
||||
current_pos.x + current_size.width / 2.0 - inner_size.0 / 2.0,
|
||||
current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0,
|
||||
);
|
||||
|
||||
let config = CreateWindowConfig {
|
||||
label: label.as_str(),
|
||||
title,
|
||||
url,
|
||||
inner_size,
|
||||
position,
|
||||
};
|
||||
|
||||
let child_window = create_window(&app_handle, config);
|
||||
|
||||
// NOTE: These listeners will remain active even when the windows close. Unfortunately,
|
||||
// there's no way to unlisten to events for now, so we just have to be defensive.
|
||||
|
||||
{
|
||||
let parent_window = parent_window.clone();
|
||||
let child_window = child_window.clone();
|
||||
child_window.clone().on_window_event(move |e| match e {
|
||||
// When the new window is destroyed, bring the other up behind it
|
||||
WindowEvent::Destroyed => {
|
||||
if let Some(w) = parent_window.get_webview_window(child_window.label()) {
|
||||
w.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let parent_window = parent_window.clone();
|
||||
let child_window = child_window.clone();
|
||||
parent_window.clone().on_window_event(move |e| match e {
|
||||
// When the parent window is closed, close the child
|
||||
WindowEvent::CloseRequested { .. } => child_window.destroy().unwrap(),
|
||||
// When the parent window is focused, bring the child above
|
||||
WindowEvent::Focused(focus) => {
|
||||
if *focus {
|
||||
if let Some(w) = parent_window.get_webview_window(child_window.label()) {
|
||||
w.set_focus().unwrap();
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_new_nested_window(
|
||||
window: WebviewWindow,
|
||||
url: &str,
|
||||
label: &str,
|
||||
title: &str,
|
||||
) -> Result<(), String> {
|
||||
create_nested_window(&window, label, url, title);
|
||||
async fn cmd_new_main_window(app_handle: AppHandle, url: &str) -> Result<(), String> {
|
||||
create_main_window(&app_handle, url);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1721,7 +1787,18 @@ pub fn run() {
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::default()
|
||||
.with_denylist(&["ignored"])
|
||||
.map_label(|label| {
|
||||
if label.starts_with(OTHER_WINDOW_PREFIX) {
|
||||
"ignored"
|
||||
} else {
|
||||
label
|
||||
}
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_updater::Builder::default().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@@ -1810,8 +1887,8 @@ pub fn run() {
|
||||
cmd_list_plugins,
|
||||
cmd_list_workspaces,
|
||||
cmd_metadata,
|
||||
cmd_new_nested_window,
|
||||
cmd_new_window,
|
||||
cmd_new_child_window,
|
||||
cmd_new_main_window,
|
||||
cmd_parse_template,
|
||||
cmd_plugin_info,
|
||||
cmd_reload_plugins,
|
||||
@@ -1844,7 +1921,7 @@ pub fn run() {
|
||||
.run(|app_handle, event| {
|
||||
match event {
|
||||
RunEvent::Ready => {
|
||||
let w = create_window(app_handle, "/");
|
||||
let w = create_main_window(app_handle, "/");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let info = analytics::track_launch_event(&w).await;
|
||||
debug!("Launched Yaak {:?}", info);
|
||||
@@ -1866,7 +1943,9 @@ pub fn run() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let val: State<'_, Mutex<YaakUpdater>> = h.state();
|
||||
let update_mode = get_update_mode(&h).await;
|
||||
_ = val.lock().await.check(&h, update_mode).await;
|
||||
if let Err(e) = val.lock().await.check(&h, update_mode).await {
|
||||
warn!("Failed to check for updates {e:?}");
|
||||
};
|
||||
});
|
||||
|
||||
let h = app_handle.clone();
|
||||
@@ -1897,47 +1976,31 @@ fn is_dev() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_nested_window(
|
||||
window: &WebviewWindow,
|
||||
label: &str,
|
||||
url: &str,
|
||||
title: &str,
|
||||
) -> WebviewWindow {
|
||||
info!("Create new nested window label={label}");
|
||||
let mut win_builder = tauri::WebviewWindowBuilder::new(
|
||||
window,
|
||||
format!("nested_{}_{}", window.label(), label),
|
||||
WebviewUrl::App(url.into()),
|
||||
)
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.disable_drag_drop_handler() // Required for frontend Dnd on windows
|
||||
.title(title)
|
||||
.parent(&window)
|
||||
.unwrap()
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||
.inner_size(DEFAULT_WINDOW_WIDTH * 0.7, DEFAULT_WINDOW_HEIGHT * 0.9);
|
||||
|
||||
// Add macOS-only things
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
win_builder = win_builder
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay);
|
||||
}
|
||||
|
||||
// Add non-macOS things
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
win_builder = win_builder.decorations(false);
|
||||
}
|
||||
|
||||
let win = win_builder.build().expect("failed to build window");
|
||||
|
||||
win
|
||||
fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
let label = format!("{MAIN_WINDOW_PREFIX}{}", handle.webview_windows().len());
|
||||
let config = CreateWindowConfig {
|
||||
url,
|
||||
label: label.as_str(),
|
||||
title: "Yaak",
|
||||
inner_size: (DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT),
|
||||
position: (
|
||||
// Offset by random amount so it's easier to differentiate
|
||||
100.0 + random::<f64>() * 20.0,
|
||||
100.0 + random::<f64>() * 20.0,
|
||||
),
|
||||
};
|
||||
create_window(handle, config)
|
||||
}
|
||||
|
||||
fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
struct CreateWindowConfig<'s> {
|
||||
url: &'s str,
|
||||
label: &'s str,
|
||||
title: &'s str,
|
||||
inner_size: (f64, f64),
|
||||
position: (f64, f64),
|
||||
}
|
||||
|
||||
fn create_window(handle: &AppHandle, config: CreateWindowConfig) -> WebviewWindow {
|
||||
#[allow(unused_variables)]
|
||||
let menu = app_menu(handle).unwrap();
|
||||
|
||||
@@ -1945,22 +2008,18 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
handle.set_menu(menu).expect("Failed to set app menu");
|
||||
|
||||
let window_num = handle.webview_windows().len();
|
||||
let label = format!("{MAIN_WINDOW_PREFIX}{window_num}");
|
||||
info!("Create new window label={label}");
|
||||
info!("Create new window label={}", config.label);
|
||||
|
||||
let mut win_builder =
|
||||
tauri::WebviewWindowBuilder::new(handle, label, WebviewUrl::App(url.into()))
|
||||
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
|
||||
.title(config.title)
|
||||
.resizable(true)
|
||||
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
|
||||
.fullscreen(false)
|
||||
.disable_drag_drop_handler() // Required for frontend Dnd on windows
|
||||
.inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
|
||||
.position(
|
||||
// Randomly offset so windows don't stack exactly
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
)
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||
.title(handle.package_info().name.to_string());
|
||||
.inner_size(config.inner_size.0, config.inner_size.1)
|
||||
.position(config.position.0, config.position.1)
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
|
||||
|
||||
// Add macOS-only things
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -1977,7 +2036,16 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
win_builder = win_builder.decorations(false);
|
||||
}
|
||||
|
||||
let win = win_builder.build().expect("failed to build window");
|
||||
if let Some(w) = handle.webview_windows().get(config.label) {
|
||||
info!(
|
||||
"Webview with label {} already exists. Focusing existing",
|
||||
config.label
|
||||
);
|
||||
w.set_focus().unwrap();
|
||||
return w.to_owned();
|
||||
}
|
||||
|
||||
let win = win_builder.build().unwrap();
|
||||
|
||||
let webview_window = win.clone();
|
||||
win.on_menu_event(move |w, event| {
|
||||
@@ -1994,10 +2062,13 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
"zoom_out" => w.emit("zoom_out", true).unwrap(),
|
||||
"settings" => w.emit("settings", true).unwrap(),
|
||||
"open_feedback" => {
|
||||
_ = webview_window
|
||||
if let Err(e) = webview_window
|
||||
.app_handle()
|
||||
.shell()
|
||||
.open("https://yaak.app/roadmap", None)
|
||||
.open("https://yaak.app/feedback", None)
|
||||
{
|
||||
warn!("Failed to open feedback {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
// Commands for development
|
||||
|
||||
@@ -2,7 +2,11 @@ use hex_color::HexColor;
|
||||
use log::warn;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use tauri::{plugin::{Builder, TauriPlugin}, Manager, Runtime, Window, WindowEvent, Emitter, Listener};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Emitter, Listener, Manager, Runtime, Window, WindowEvent,
|
||||
};
|
||||
use crate::MAIN_WINDOW_PREFIX;
|
||||
|
||||
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
|
||||
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
|
||||
@@ -127,7 +131,7 @@ fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor) {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, label: String) {
|
||||
if label.starts_with("nested_") {
|
||||
if !label.starts_with(MAIN_WINDOW_PREFIX) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +56,12 @@
|
||||
"icons/release/icon.ico"
|
||||
],
|
||||
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
|
||||
"resources": {
|
||||
"migrations": "migrations",
|
||||
"vendored/protoc/include": "protoc-include",
|
||||
"vendored/plugins": "plugins",
|
||||
"../plugin-runtime/build": "plugin-runtime"
|
||||
},
|
||||
"resources": [
|
||||
"migrations",
|
||||
"vendored/protoc/include",
|
||||
"vendored/plugins",
|
||||
"vendored/plugin-runtime"
|
||||
],
|
||||
"shortDescription": "Play with APIs, intuitively",
|
||||
"targets": [
|
||||
"app",
|
||||
|
||||
@@ -34,7 +34,7 @@ pub async fn fill_pool_from_files(
|
||||
let desc_path = temp_dir().join(random_file_name);
|
||||
let global_import_dir = app_handle
|
||||
.path()
|
||||
.resolve("protoc-include", BaseDirectory::Resource)
|
||||
.resolve("vendored/protoc/protoc-include", BaseDirectory::Resource)
|
||||
.expect("failed to resolve protoc include directory");
|
||||
|
||||
// HACK: Remove UNC prefix for Windows paths
|
||||
|
||||
@@ -126,7 +126,7 @@ impl PluginManager {
|
||||
pub async fn list_plugin_dirs<R: Runtime>(&self, app_handle: &AppHandle<R>) -> Vec<String> {
|
||||
let plugins_dir = app_handle
|
||||
.path()
|
||||
.resolve("plugins", BaseDirectory::Resource)
|
||||
.resolve("vendored/plugins", BaseDirectory::Resource)
|
||||
.expect("failed to resolve plugin directory resource");
|
||||
|
||||
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn start_nodejs_plugin_runtime<R: Runtime>(
|
||||
) -> Result<()> {
|
||||
let plugin_runtime_main = app
|
||||
.path()
|
||||
.resolve("plugin-runtime", BaseDirectory::Resource)?
|
||||
.resolve("vendored/plugin-runtime", BaseDirectory::Resource)?
|
||||
.join("index.cjs");
|
||||
|
||||
// HACK: Remove UNC prefix for Windows paths to pass to sidecar
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { lazy } from 'react';
|
||||
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
|
||||
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { DefaultLayout } from './DefaultLayout';
|
||||
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
|
||||
import RouteError from './RouteError';
|
||||
import { Settings } from './Settings/Settings';
|
||||
import Workspace from './Workspace';
|
||||
|
||||
const LazyWorkspace = lazy(() => import('./Workspace'));
|
||||
const LazySettings = lazy(() => import('./Settings/Settings'));
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -24,14 +26,14 @@ const router = createBrowserRouter([
|
||||
path: routePaths.workspace({
|
||||
workspaceId: ':workspaceId',
|
||||
}),
|
||||
element: <Workspace />,
|
||||
element: <LazyWorkspace />,
|
||||
},
|
||||
{
|
||||
path: routePaths.request({
|
||||
workspaceId: ':workspaceId',
|
||||
requestId: ':requestId',
|
||||
}),
|
||||
element: <Workspace />,
|
||||
element: <LazyWorkspace />,
|
||||
},
|
||||
{
|
||||
path: '/workspaces/:workspaceId/environments/:environmentId/requests/:requestId',
|
||||
@@ -41,7 +43,7 @@ const router = createBrowserRouter([
|
||||
path: routePaths.workspaceSettings({
|
||||
workspaceId: ':workspaceId',
|
||||
}),
|
||||
element: <Settings />,
|
||||
element: <LazySettings />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
import { search } from 'fast-fuzzy';
|
||||
import type { KeyboardEvent, ReactNode } from 'react';
|
||||
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
|
||||
@@ -17,6 +16,7 @@ import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import type { HotkeyAction } from '../hooks/useHotKey';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
|
||||
import { useOpenSettings } from '../hooks/useOpenSettings';
|
||||
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
@@ -28,7 +28,6 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { CookieDialog } from './CookieDialog';
|
||||
import { Button } from './core/Button';
|
||||
import { Heading } from './core/Heading';
|
||||
@@ -74,11 +73,11 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
||||
const createGrpcRequest = useCreateGrpcRequest();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const dialog = useDialog();
|
||||
const workspace = useActiveWorkspace();
|
||||
const sendRequest = useSendAnyHttpRequest();
|
||||
const renameRequest = useRenameRequest(activeRequest?.id ?? null);
|
||||
const deleteRequest = useDeleteRequest(activeRequest?.id ?? null);
|
||||
const [, setSidebarHidden] = useSidebarHidden();
|
||||
const openSettings = useOpenSettings();
|
||||
|
||||
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
|
||||
const commands: CommandPaletteItem[] = [
|
||||
@@ -86,14 +85,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
||||
key: 'settings.open',
|
||||
label: 'Open Settings',
|
||||
action: 'settings.show',
|
||||
onSelect: async () => {
|
||||
if (workspace == null) return;
|
||||
await invokeCmd('cmd_new_nested_window', {
|
||||
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
|
||||
label: 'settings',
|
||||
title: 'Yaak Settings',
|
||||
});
|
||||
},
|
||||
onSelect: openSettings.mutate,
|
||||
},
|
||||
{
|
||||
key: 'app.create',
|
||||
@@ -195,11 +187,10 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
||||
deleteRequest.mutate,
|
||||
dialog,
|
||||
httpRequestActions,
|
||||
openSettings.mutate,
|
||||
renameRequest.mutate,
|
||||
routes.paths,
|
||||
sendRequest,
|
||||
setSidebarHidden,
|
||||
workspace,
|
||||
]);
|
||||
|
||||
const sortedRequests = useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { DialogProvider, Dialogs } from './DialogContext';
|
||||
@@ -16,17 +15,14 @@ export function DefaultLayout() {
|
||||
<Toasts />
|
||||
<Dialogs />
|
||||
</>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1, delay: 0.1 }}
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full h-full',
|
||||
osInfo?.osType === 'linux' && 'border border-border-subtle',
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
</motion.div>
|
||||
</div>
|
||||
<GlobalHooks />
|
||||
</ToastProvider>
|
||||
</DialogProvider>
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { settingsAtom, useSettings } from '../hooks/useSettings';
|
||||
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
|
||||
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
||||
import { workspacesAtom } from '../hooks/useWorkspaces';
|
||||
import { useZoom } from '../hooks/useZoom';
|
||||
@@ -39,6 +38,11 @@ import { rosePineDefault } from '../lib/theme/themes/rose-pine';
|
||||
import { yaakDark } from '../lib/theme/themes/yaak';
|
||||
import { getThemeCSS } from '../lib/theme/window';
|
||||
|
||||
export interface ModelPayload {
|
||||
model: AnyModel;
|
||||
windowLabel: string;
|
||||
}
|
||||
|
||||
export function GlobalHooks() {
|
||||
// Include here so they always update, even if no component references them
|
||||
useRecentWorkspaces();
|
||||
@@ -47,7 +51,6 @@ export function GlobalHooks() {
|
||||
useRecentRequests();
|
||||
|
||||
// Other useful things
|
||||
useSyncThemeToDocument();
|
||||
useNotificationToast();
|
||||
useActiveWorkspaceChangedToast();
|
||||
useEnsureActiveCookieJar();
|
||||
@@ -61,11 +64,6 @@ export function GlobalHooks() {
|
||||
const queryClient = useQueryClient();
|
||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||
|
||||
interface ModelPayload {
|
||||
model: AnyModel;
|
||||
windowLabel: string;
|
||||
}
|
||||
|
||||
const setSettings = useSetAtom(settingsAtom);
|
||||
const setWorkspaces = useSetAtom(workspacesAtom);
|
||||
const setPlugins = useSetAtom(pluginsAtom);
|
||||
@@ -162,9 +160,8 @@ export function GlobalHooks() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { interfaceScale, interfaceFontSize, editorFontSize } = settings;
|
||||
const { interfaceScale, editorFontSize } = settings;
|
||||
getCurrentWebviewWindow().setZoom(interfaceScale).catch(console.error);
|
||||
document.documentElement.style.setProperty('font-size', `${interfaceFontSize}px`);
|
||||
document.documentElement.style.setProperty('--editor-font-size', `${editorFontSize}px`);
|
||||
}, [settings]);
|
||||
|
||||
|
||||
@@ -1,77 +1,48 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { updateSchema } from 'cm6-graphql';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { Button } from './core/Button';
|
||||
import type { EditorProps } from './core/Editor';
|
||||
import { Editor, formatGraphQL } from './core/Editor';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Separator } from './core/Separator';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { updateSchema } from 'cm6-graphql';
|
||||
|
||||
type Props = Pick<
|
||||
EditorProps,
|
||||
'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'forceUpdateKey'
|
||||
> & {
|
||||
type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & {
|
||||
baseRequest: HttpRequest;
|
||||
onChange: (body: HttpRequest['body']) => void;
|
||||
body: HttpRequest['body'];
|
||||
};
|
||||
|
||||
interface GraphQLBody {
|
||||
query: string;
|
||||
variables?: Record<string, string | number | boolean | null>;
|
||||
operationName?: string;
|
||||
}
|
||||
|
||||
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||
export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||
const editorViewRef = useRef<EditorView>(null);
|
||||
const { schema, isLoading, error, refetch } = useIntrospectGraphQL(baseRequest);
|
||||
const { query, variables } = useMemo<GraphQLBody>(() => {
|
||||
if (defaultValue === undefined) {
|
||||
return { query: '', variables: {} };
|
||||
const [currentBody, setCurrentBody] = useState<{ query: string; variables: string }>(() => {
|
||||
// Migrate text bodies to GraphQL format
|
||||
// NOTE: This is how GraphQL used to be stored
|
||||
if ('text' in body) {
|
||||
const b = tryParseJson(body.text, {});
|
||||
const variables = JSON.stringify(b.variables ?? '', null, 2);
|
||||
return { query: b.query ?? '', variables };
|
||||
}
|
||||
try {
|
||||
const p = JSON.parse(defaultValue || '{}');
|
||||
const query = p.query ?? '';
|
||||
const variables = p.variables;
|
||||
const operationName = p.operationName;
|
||||
return { query, variables, operationName };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
return { query: '' };
|
||||
}
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(b: GraphQLBody) => {
|
||||
try {
|
||||
onChange?.(JSON.stringify(b, null, 2));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
// Meh, not much we can do here
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
return { query: body.query ?? '', variables: body.variables ?? '' };
|
||||
});
|
||||
|
||||
const handleChangeQuery = useCallback(
|
||||
(query: string) => handleChange({ query, variables }),
|
||||
[handleChange, variables],
|
||||
);
|
||||
const handleChangeQuery = (query: string) => {
|
||||
const newBody = { query, variables: currentBody.variables };
|
||||
setCurrentBody(newBody);
|
||||
onChange(newBody);
|
||||
};
|
||||
|
||||
const handleChangeVariables = useCallback(
|
||||
(variables: string) => {
|
||||
try {
|
||||
handleChange({ query, variables: JSON.parse(variables || '{}') });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
// Don't do anything if invalid JSON. The user probably hasn't finished
|
||||
// typing yet.
|
||||
}
|
||||
},
|
||||
[handleChange, query],
|
||||
);
|
||||
const handleChangeVariables = (variables: string) => {
|
||||
const newBody = { query: currentBody.query, variables };
|
||||
setCurrentBody(newBody);
|
||||
onChange(newBody);
|
||||
};
|
||||
|
||||
// Refetch the schema when the URL changes
|
||||
useEffect(() => {
|
||||
@@ -132,9 +103,9 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]">
|
||||
<Editor
|
||||
language="graphql"
|
||||
defaultValue={query ?? ''}
|
||||
format={formatGraphQL}
|
||||
heightMode="auto"
|
||||
format={formatGraphQL}
|
||||
defaultValue={currentBody.query}
|
||||
onChange={handleChangeQuery}
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
@@ -148,8 +119,8 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
<Editor
|
||||
format={tryFormatJson}
|
||||
language="json"
|
||||
defaultValue={JSON.stringify(variables, null, 2)}
|
||||
heightMode="auto"
|
||||
defaultValue={currentBody.variables}
|
||||
onChange={handleChangeVariables}
|
||||
placeholder="{}"
|
||||
useTemplating
|
||||
@@ -160,3 +131,12 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function tryParseJson(text: string, fallback: unknown) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,10 +323,11 @@ export const RequestPane = memo(function RequestPane({
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
placeholder="https://example.com"
|
||||
onPaste={(text) => {
|
||||
onPasteOverwrite={(text) => {
|
||||
if (text.startsWith('curl ')) {
|
||||
importCurl.mutate({ overwriteRequestId: activeRequestId, command: text });
|
||||
} else {
|
||||
// Only import query if pasted text contains entire querystring
|
||||
importQuerystring.mutate(text);
|
||||
}
|
||||
}}
|
||||
@@ -418,8 +419,8 @@ export const RequestPane = memo(function RequestPane({
|
||||
<GraphQLEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
baseRequest={activeRequest}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
onChange={handleBodyTextChange}
|
||||
body={activeRequest.body}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
|
||||
<FormUrlencodedEditor
|
||||
|
||||
@@ -117,9 +117,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
>
|
||||
{activeResponse && (
|
||||
<HStack
|
||||
as="p"
|
||||
space={2}
|
||||
className="whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm"
|
||||
className={classNames(
|
||||
'cursor-default select-none',
|
||||
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm',
|
||||
)}
|
||||
>
|
||||
<StatusTag showReason response={activeResponse} />
|
||||
{activeResponse.elapsed > 0 && (
|
||||
|
||||
@@ -20,7 +20,7 @@ enum Tab {
|
||||
|
||||
const tabs = [Tab.General, Tab.Appearance, Tab.Plugins];
|
||||
|
||||
export const Settings = () => {
|
||||
export default function Settings() {
|
||||
const osInfo = useOsInfo();
|
||||
const [tab, setTab] = useState<string>(Tab.General);
|
||||
|
||||
@@ -66,4 +66,4 @@ export const Settings = () => {
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
|
||||
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
|
||||
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { useThemes } from '../../hooks/useThemes';
|
||||
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { clamp } from '../../lib/clamp';
|
||||
import { getThemes } from '../../lib/theme/themes';
|
||||
import { isThemeDark } from '../../lib/theme/window';
|
||||
import type { ButtonProps } from '../core/Button';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
@@ -52,12 +52,13 @@ const icons: IconProps['icon'][] = [
|
||||
'send_horizontal',
|
||||
];
|
||||
|
||||
const { themes } = getThemes();
|
||||
|
||||
export function SettingsAppearance() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
const appearance = useResolvedAppearance();
|
||||
const { themes } = useThemes();
|
||||
const activeTheme = useResolvedTheme();
|
||||
|
||||
if (settings == null || workspace == null) {
|
||||
@@ -161,9 +162,10 @@ export function SettingsAppearance() {
|
||||
space={3}
|
||||
className="mt-3 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
|
||||
>
|
||||
<HStack className="text font-bold" space={2}>
|
||||
Theme Preview{' '}
|
||||
<HStack className="text" space={1.5}>
|
||||
<Icon icon={appearance === 'dark' ? 'moon' : 'sun'} className="text-text-subtle" />
|
||||
<strong>{activeTheme.active.name}</strong>
|
||||
<em>(preview)</em>
|
||||
</HStack>
|
||||
<HStack space={1.5} className="w-full">
|
||||
{buttonColors.map((c, i) => (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import React, { useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useThemes } from '../../hooks/useThemes';
|
||||
import { capitalize } from '../../lib/capitalize';
|
||||
import { invokeCmd } from '../../lib/tauri';
|
||||
import { getThemes } from '../../lib/theme/themes';
|
||||
import { yaakDark } from '../../lib/theme/themes/yaak';
|
||||
import { getThemeCSS } from '../../lib/theme/window';
|
||||
import { Banner } from '../core/Banner';
|
||||
@@ -45,9 +45,9 @@ const icons: IconProps['icon'][] = [
|
||||
'send_horizontal',
|
||||
];
|
||||
|
||||
export function SettingsDesign() {
|
||||
const themes = useThemes();
|
||||
const themes = getThemes();
|
||||
|
||||
export function SettingsDesign() {
|
||||
const [exportDir, setExportDir] = useLocalStorage<string | null>('theme_export_dir', null);
|
||||
const [loadingExport, setLoadingExport] = useState<boolean>(false);
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useRef } from 'react';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppInfo } from '../hooks/useAppInfo';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useOpenSettings } from '../hooks/useOpenSettings';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
@@ -22,19 +20,9 @@ export function SettingsDropdown() {
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const dialog = useDialog();
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
const routes = useAppRoutes();
|
||||
const workspace = useActiveWorkspace();
|
||||
const openSettings = useOpenSettings();
|
||||
|
||||
const showSettings = async () => {
|
||||
if (!workspace) return;
|
||||
await invokeCmd('cmd_new_nested_window', {
|
||||
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
|
||||
label: 'settings',
|
||||
title: 'Yaak Settings',
|
||||
});
|
||||
};
|
||||
|
||||
useListenToTauriEvent('settings', showSettings);
|
||||
useListenToTauriEvent('settings', () => openSettings.mutate());
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@@ -45,7 +33,7 @@ export function SettingsDropdown() {
|
||||
label: 'Settings',
|
||||
hotKeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: showSettings,
|
||||
onSelect: openSettings.mutate,
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
|
||||
@@ -17,6 +17,7 @@ type Props = Pick<HttpRequest, 'url'> & {
|
||||
onSend: () => void;
|
||||
onUrlChange: (url: string) => void;
|
||||
onPaste?: (v: string) => void;
|
||||
onPasteOverwrite?: (v: string) => void;
|
||||
onCancel: () => void;
|
||||
submitIcon?: IconProps['icon'] | null;
|
||||
onMethodChange?: (method: string) => void;
|
||||
@@ -37,6 +38,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
onCancel,
|
||||
onMethodChange,
|
||||
onPaste,
|
||||
onPasteOverwrite,
|
||||
submitIcon = 'send_horizontal',
|
||||
autocomplete,
|
||||
rightSlot,
|
||||
@@ -70,13 +72,14 @@ export const UrlBar = memo(function UrlBar({
|
||||
useTemplating
|
||||
language="url"
|
||||
className="pl-0 pr-1.5 py-0.5"
|
||||
name="url"
|
||||
label="Enter URL"
|
||||
name="url"
|
||||
autocomplete={autocomplete}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onPaste={onPaste}
|
||||
onPasteOverwrite={onPasteOverwrite}
|
||||
onChange={onUrlChange}
|
||||
defaultValue={url}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface EditorProps {
|
||||
useTemplating?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onPaste?: (value: string) => void;
|
||||
onPasteOverwrite?: (value: string) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
@@ -83,6 +84,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
forceUpdateKey,
|
||||
onChange,
|
||||
onPaste,
|
||||
onPasteOverwrite,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
@@ -121,6 +123,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
handlePaste.current = onPaste;
|
||||
}, [onPaste]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handlePasteOverwrite = useRef<EditorProps['onPasteOverwrite']>(onPaste);
|
||||
useEffect(() => {
|
||||
handlePasteOverwrite.current = onPasteOverwrite;
|
||||
}, [onPasteOverwrite]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleFocus = useRef<EditorProps['onFocus']>(onFocus);
|
||||
useEffect(() => {
|
||||
@@ -303,6 +311,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
singleLine,
|
||||
onChange: handleChange,
|
||||
onPaste: handlePaste,
|
||||
onPasteOverwrite: handlePasteOverwrite,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
@@ -420,6 +429,7 @@ function getExtensions({
|
||||
singleLine,
|
||||
onChange,
|
||||
onPaste,
|
||||
onPasteOverwrite,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
@@ -427,6 +437,7 @@ function getExtensions({
|
||||
container: HTMLDivElement | null;
|
||||
onChange: MutableRefObject<EditorProps['onChange']>;
|
||||
onPaste: MutableRefObject<EditorProps['onPaste']>;
|
||||
onPasteOverwrite: MutableRefObject<EditorProps['onPasteOverwrite']>;
|
||||
onFocus: MutableRefObject<EditorProps['onFocus']>;
|
||||
onBlur: MutableRefObject<EditorProps['onBlur']>;
|
||||
onKeyDown: MutableRefObject<EditorProps['onKeyDown']>;
|
||||
@@ -449,8 +460,12 @@ function getExtensions({
|
||||
keydown: (e) => {
|
||||
onKeyDown.current?.(e);
|
||||
},
|
||||
paste: (e) => {
|
||||
onPaste.current?.(e.clipboardData?.getData('text/plain') ?? '');
|
||||
paste: (e, v) => {
|
||||
const textData = e.clipboardData?.getData('text/plain') ?? '';
|
||||
onPaste.current?.(textData);
|
||||
if (v.state.selection.main.from === 0 && v.state.selection.main.to === v.state.doc.length) {
|
||||
onPasteOverwrite.current?.(textData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
tooltips({ parent }),
|
||||
|
||||
@@ -35,6 +35,7 @@ export type InputProps = Omit<
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onPaste?: (value: string) => void;
|
||||
onPasteOverwrite?: (value: string) => void;
|
||||
defaultValue?: string;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
@@ -62,6 +63,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
onChange,
|
||||
onFocus,
|
||||
onPaste,
|
||||
onPasteOverwrite,
|
||||
placeholder,
|
||||
require,
|
||||
rightSlot,
|
||||
@@ -179,6 +181,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onPaste={onPaste}
|
||||
onPasteOverwrite={onPasteOverwrite}
|
||||
className={editorClassName}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
|
||||
15
src-web/font-size.ts
Normal file
15
src-web/font-size.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Listen for settings changes, the re-compute theme
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import type { ModelPayload } from './components/GlobalHooks';
|
||||
import { getSettings } from './lib/store';
|
||||
|
||||
function setFontSizeOnDocument(fontSize: number) {
|
||||
document.documentElement.style.fontSize = `${fontSize}px`;
|
||||
}
|
||||
|
||||
listen<ModelPayload>('upserted_model', async (event) => {
|
||||
if (event.payload.model.model !== 'settings') return;
|
||||
setFontSizeOnDocument(event.payload.model.interfaceFontSize);
|
||||
}).catch(console.error);
|
||||
|
||||
getSettings().then((settings) => setFontSizeOnDocument(settings.interfaceFontSize));
|
||||
@@ -14,31 +14,20 @@ export function useImportQuerystring(requestId: string) {
|
||||
return useMutation({
|
||||
mutationKey: ['import_querystring'],
|
||||
mutationFn: async (url: string) => {
|
||||
const [baseUrl, ...rest] = url.split('?');
|
||||
if (rest.length === 0) return;
|
||||
const split = url.split(/\?(.*)/s);
|
||||
const baseUrl = split[0] ?? '';
|
||||
const querystring = split[1] ?? '';
|
||||
if (!querystring) return;
|
||||
|
||||
const request = await getHttpRequest(requestId);
|
||||
if (request == null) return;
|
||||
|
||||
const querystring = rest.join('?');
|
||||
const parsedParams = Array.from(new URLSearchParams(querystring).entries());
|
||||
const additionalUrlParameters: HttpUrlParameter[] = parsedParams.map(
|
||||
([name, value]): HttpUrlParameter => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const urlParameters: HttpUrlParameter[] = [...request.urlParameters];
|
||||
for (const newParam of additionalUrlParameters) {
|
||||
const index = urlParameters.findIndex((p) => p.name === newParam.name);
|
||||
if (index >= 0) {
|
||||
urlParameters[index]!.value = decodeURIComponent(newParam.value);
|
||||
} else {
|
||||
urlParameters.push(newParam);
|
||||
}
|
||||
}
|
||||
const urlParameters: HttpUrlParameter[] = parsedParams.map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
}));
|
||||
|
||||
await updateRequest.mutateAsync({
|
||||
id: requestId,
|
||||
@@ -48,11 +37,11 @@ export function useImportQuerystring(requestId: string) {
|
||||
},
|
||||
});
|
||||
|
||||
if (additionalUrlParameters.length > 0) {
|
||||
if (urlParameters.length > 0) {
|
||||
toast.show({
|
||||
id: 'querystring-imported',
|
||||
color: 'info',
|
||||
message: `Imported ${additionalUrlParameters.length} ${pluralize('param', additionalUrlParameters.length)} from URL`,
|
||||
message: `Extracted ${urlParameters.length} ${pluralize('parameter', urlParameters.length)} from URL`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
22
src-web/hooks/useOpenSettings.ts
Normal file
22
src-web/hooks/useOpenSettings.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
|
||||
export function useOpenSettings() {
|
||||
const routes = useAppRoutes();
|
||||
const workspace = useActiveWorkspace();
|
||||
return useMutation({
|
||||
mutationKey: ['open_settings'],
|
||||
mutationFn: async () => {
|
||||
if (workspace == null) return;
|
||||
|
||||
await invokeCmd('cmd_new_child_window', {
|
||||
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
|
||||
label: 'settings',
|
||||
title: 'Yaak Settings',
|
||||
innerSize: [600, 550],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function useOpenWorkspace() {
|
||||
requestId != null
|
||||
? routes.paths.request({ ...baseArgs, requestId })
|
||||
: routes.paths.workspace({ ...baseArgs });
|
||||
await invokeCmd('cmd_new_window', { url: path });
|
||||
await invokeCmd('cmd_new_main_window', { url: path });
|
||||
} else {
|
||||
if (requestId != null) {
|
||||
routes.navigate('request', { ...baseArgs, requestId });
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Appearance } from '../lib/theme/appearance';
|
||||
import {
|
||||
getCSSAppearance,
|
||||
getWindowAppearance,
|
||||
subscribeToWindowAppearanceChange,
|
||||
} from '../lib/theme/appearance';
|
||||
import { getCSSAppearance, subscribeToPreferredAppearance } from '../lib/theme/appearance';
|
||||
|
||||
export function usePreferredAppearance() {
|
||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(getCSSAppearance());
|
||||
|
||||
useEffect(() => {
|
||||
getWindowAppearance().then(setPreferredAppearance);
|
||||
return subscribeToWindowAppearanceChange(setPreferredAppearance);
|
||||
}, []);
|
||||
|
||||
useEffect(() => subscribeToPreferredAppearance(setPreferredAppearance), []);
|
||||
return preferredAppearance;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { resolveAppearance } from '../lib/theme/appearance';
|
||||
import { usePreferredAppearance } from './usePreferredAppearance';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
export function useResolvedAppearance() {
|
||||
const preferredAppearance = usePreferredAppearance();
|
||||
|
||||
const settings = useSettings();
|
||||
const appearance =
|
||||
settings == null || settings?.appearance === 'system'
|
||||
? preferredAppearance
|
||||
: settings.appearance;
|
||||
|
||||
return appearance;
|
||||
return resolveAppearance(preferredAppearance, settings.appearance);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { isThemeDark } from '../lib/theme/window';
|
||||
import { useResolvedAppearance } from './useResolvedAppearance';
|
||||
import { getResolvedTheme } from '../lib/theme/themes';
|
||||
import { usePreferredAppearance } from './usePreferredAppearance';
|
||||
import { useSettings } from './useSettings';
|
||||
import { useThemes } from './useThemes';
|
||||
|
||||
export function useResolvedTheme() {
|
||||
const appearance = useResolvedAppearance();
|
||||
const preferredAppearance = usePreferredAppearance();
|
||||
const settings = useSettings();
|
||||
const { themes, fallback } = useThemes();
|
||||
|
||||
const darkThemes = themes.filter((t) => isThemeDark(t));
|
||||
const lightThemes = themes.filter((t) => !isThemeDark(t));
|
||||
|
||||
const dark = darkThemes.find((t) => t.id === settings?.themeDark) ?? fallback.dark;
|
||||
const light = lightThemes.find((t) => t.id === settings?.themeLight) ?? fallback.light;
|
||||
|
||||
const active = appearance === 'dark' ? dark : light;
|
||||
|
||||
return { dark, light, active };
|
||||
return getResolvedTheme(
|
||||
preferredAppearance,
|
||||
settings.appearance,
|
||||
settings.themeLight,
|
||||
settings.themeDark,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { useEffect } from 'react';
|
||||
import type { YaakTheme } from '../lib/theme/window';
|
||||
import { addThemeStylesToDocument, setThemeOnDocument } from '../lib/theme/window';
|
||||
import { useResolvedTheme } from './useResolvedTheme';
|
||||
|
||||
export function useSyncThemeToDocument() {
|
||||
const theme = useResolvedTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setThemeOnDocument(theme.active);
|
||||
emitBgChange(theme.active);
|
||||
}, [theme.active]);
|
||||
|
||||
useEffect(() => {
|
||||
addThemeStylesToDocument(theme.active);
|
||||
}, [theme.active]);
|
||||
}
|
||||
|
||||
function emitBgChange(t: YaakTheme) {
|
||||
if (t.surface == null) return;
|
||||
emit('yaak_bg_changed', t.surface.hexNoAlpha()).catch(console.error);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { defaultDarkTheme, defaultLightTheme, yaakThemes } from '../lib/theme/themes';
|
||||
|
||||
export function useThemes() {
|
||||
const dark = defaultDarkTheme;
|
||||
const light = defaultLightTheme;
|
||||
|
||||
const otherThemes = yaakThemes
|
||||
.filter((t) => t.id !== dark.id && t.id !== light.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const themes = [dark, light, ...otherThemes];
|
||||
return { themes, fallback: { dark, light } };
|
||||
}
|
||||
@@ -26,6 +26,8 @@
|
||||
<div id="cm-portal" class="cm-portal"></div>
|
||||
<div id="react-portal"></div>
|
||||
<div id="radix-portal" class="cm-portal"></div>
|
||||
<script type="module" src="/theme.ts"></script>
|
||||
<script type="module" src="/font-size.ts"></script>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -51,8 +51,8 @@ type TauriCmd =
|
||||
| 'cmd_list_plugins'
|
||||
| 'cmd_list_workspaces'
|
||||
| 'cmd_metadata'
|
||||
| 'cmd_new_nested_window'
|
||||
| 'cmd_new_window'
|
||||
| 'cmd_new_main_window'
|
||||
| 'cmd_new_child_window'
|
||||
| 'cmd_parse_template'
|
||||
| 'cmd_plugin_info'
|
||||
| 'cmd_render_template'
|
||||
|
||||
@@ -18,7 +18,9 @@ export async function getWindowAppearance(): Promise<Appearance> {
|
||||
export function subscribeToWindowAppearanceChange(
|
||||
cb: (appearance: Appearance) => void,
|
||||
): () => void {
|
||||
const container = { unsubscribe: () => {} };
|
||||
const container = {
|
||||
unsubscribe: () => {},
|
||||
};
|
||||
|
||||
getCurrentWebviewWindow()
|
||||
.onThemeChanged((t) => {
|
||||
@@ -30,3 +32,17 @@ export function subscribeToWindowAppearanceChange(
|
||||
|
||||
return () => container.unsubscribe();
|
||||
}
|
||||
|
||||
export function resolveAppearance(
|
||||
preferredAppearance: Appearance,
|
||||
appearanceSetting: string,
|
||||
): Appearance {
|
||||
const appearance = appearanceSetting === 'system' ? preferredAppearance : appearanceSetting;
|
||||
return appearance === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function subscribeToPreferredAppearance(cb: (a: Appearance) => void) {
|
||||
cb(getCSSAppearance());
|
||||
getWindowAppearance().then(cb);
|
||||
subscribeToWindowAppearanceChange(cb);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Appearance } from './appearance';
|
||||
import { resolveAppearance } from './appearance';
|
||||
import { catppuccin } from './themes/catppuccin';
|
||||
import { github } from './themes/github';
|
||||
import { hotdogStand } from './themes/hotdog-stand';
|
||||
@@ -5,11 +7,12 @@ import { monokaiPro } from './themes/monokai-pro';
|
||||
import { relaxing } from './themes/relaxing';
|
||||
import { rosePine } from './themes/rose-pine';
|
||||
import { yaak, yaakDark, yaakLight } from './themes/yaak';
|
||||
import { isThemeDark } from './window';
|
||||
|
||||
export const defaultDarkTheme = yaakDark;
|
||||
export const defaultLightTheme = yaakLight;
|
||||
|
||||
export const yaakThemes = [
|
||||
const allThemes = [
|
||||
...yaak,
|
||||
...catppuccin,
|
||||
...relaxing,
|
||||
@@ -18,3 +21,35 @@ export const yaakThemes = [
|
||||
...monokaiPro,
|
||||
...hotdogStand,
|
||||
];
|
||||
|
||||
export function getThemes() {
|
||||
const dark = defaultDarkTheme;
|
||||
const light = defaultLightTheme;
|
||||
|
||||
const otherThemes = allThemes
|
||||
.filter((t) => t.id !== dark.id && t.id !== light.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const themes = [dark, light, ...otherThemes];
|
||||
return { themes, fallback: { dark, light } };
|
||||
}
|
||||
|
||||
export function getResolvedTheme(
|
||||
preferredAppearance: Appearance,
|
||||
appearanceSetting: string,
|
||||
themeLight: string,
|
||||
themeDark: string,
|
||||
) {
|
||||
const appearance = resolveAppearance(preferredAppearance, appearanceSetting);
|
||||
const { themes, fallback } = getThemes();
|
||||
|
||||
const darkThemes = themes.filter((t) => isThemeDark(t));
|
||||
const lightThemes = themes.filter((t) => !isThemeDark(t));
|
||||
|
||||
const dark = darkThemes.find((t) => t.id === themeDark) ?? fallback.dark;
|
||||
const light = lightThemes.find((t) => t.id === themeLight) ?? fallback.light;
|
||||
|
||||
const active = appearance === 'dark' ? dark : light;
|
||||
|
||||
return { dark, light, active };
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { pdfjs } from 'react-pdf';
|
||||
import { App } from './components/App';
|
||||
import './main.css';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
import('react-pdf').then(({ pdfjs }) => {
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
});
|
||||
|
||||
// Hide decorations here because it doesn't work in Rust for some reason (bug?)
|
||||
const osType = type();
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
"codemirror": "^6.0.1",
|
||||
"codemirror-json-schema": "^0.7.8",
|
||||
"codemirror-json-schema": "^0.6.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
|
||||
50
src-web/theme.ts
Normal file
50
src-web/theme.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { emit, listen } from '@tauri-apps/api/event';
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import type { ModelPayload } from './components/GlobalHooks';
|
||||
import { getSettings } from './lib/store';
|
||||
import type { Appearance } from './lib/theme/appearance';
|
||||
import { getCSSAppearance, subscribeToPreferredAppearance } from './lib/theme/appearance';
|
||||
import { getResolvedTheme } from './lib/theme/themes';
|
||||
import type { YaakTheme } from './lib/theme/window';
|
||||
import { addThemeStylesToDocument, setThemeOnDocument } from './lib/theme/window';
|
||||
|
||||
// NOTE: CSS appearance isn't as accurate as getting it async from the window (next step), but we want
|
||||
// a good appearance guess so we're not waiting too long
|
||||
let preferredAppearance: Appearance = getCSSAppearance();
|
||||
subscribeToPreferredAppearance(async (a) => {
|
||||
preferredAppearance = a;
|
||||
await configureTheme();
|
||||
});
|
||||
|
||||
configureTheme().then(
|
||||
async () => {
|
||||
// To prevent theme flashing, the backend hides new windows by default, so we
|
||||
// need to show it here, after configuring the theme for the first time.
|
||||
await getCurrentWebviewWindow().show();
|
||||
},
|
||||
(err) => console.log('Failed to configure theme', err),
|
||||
);
|
||||
|
||||
// Listen for settings changes, the re-compute theme
|
||||
listen<ModelPayload>('upserted_model', async (event) => {
|
||||
if (event.payload.model.model !== 'settings') return;
|
||||
await configureTheme();
|
||||
}).catch(console.error);
|
||||
|
||||
async function configureTheme() {
|
||||
const settings = await getSettings();
|
||||
const theme = getResolvedTheme(
|
||||
preferredAppearance,
|
||||
settings.appearance,
|
||||
settings.themeLight,
|
||||
settings.themeDark,
|
||||
);
|
||||
addThemeStylesToDocument(theme.active);
|
||||
setThemeOnDocument(theme.active);
|
||||
emitBgChange(theme.active);
|
||||
}
|
||||
|
||||
function emitBgChange(t: YaakTheme) {
|
||||
if (t.surface == null) return;
|
||||
emit('yaak_bg_changed', t.surface.hexNoAlpha()).catch(console.error);
|
||||
}
|
||||
Reference in New Issue
Block a user