mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 00:49:17 +01:00
A bunch more theme stuff
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -36,11 +36,11 @@ jobs:
|
||||
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.2.1",
|
||||
"@codemirror/lang-javascript": "^6.1.4",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-xml": "^6.0.2",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.2.1",
|
||||
"@codemirror/lang-javascript": "^6.1.4",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-xml": "^6.0.2",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings SET (\n theme, appearance, update_channel\n ) = (?, ?, ?) WHERE id = 'default';\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-8e88c7070a34a6e151da66f521deeafaea9a12e2aa68081daaf235d2003b513d.json
generated
Normal file
12
src-tauri/.sqlx/query-8e88c7070a34a6e151da66f521deeafaea9a12e2aa68081daaf235d2003b513d.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings SET (\n theme, appearance, theme_dark, theme_light, update_channel\n ) = (?, ?, ?, ?, ?) WHERE id = 'default';\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8e88c7070a34a6e151da66f521deeafaea9a12e2aa68081daaf235d2003b513d"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance, update_channel\n FROM settings\n WHERE id = 'default'\n ",
|
||||
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel\n FROM settings\n WHERE id = 'default'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -34,9 +34,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "update_channel",
|
||||
"name": "theme_dark",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "theme_light",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "update_channel",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -49,8 +59,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb"
|
||||
"hash": "cae02809532d086fbde12a246d12e3839ec8610b66e08315106dbdbf25d8699c"
|
||||
}
|
||||
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -2350,6 +2350,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex_color"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.3"
|
||||
@@ -7650,6 +7659,7 @@ dependencies = [
|
||||
"cocoa",
|
||||
"datetime",
|
||||
"grpc",
|
||||
"hex_color",
|
||||
"http 0.2.10",
|
||||
"log",
|
||||
"objc",
|
||||
@@ -7674,6 +7684,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"uuid",
|
||||
"windows 0.56.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -20,6 +20,13 @@ tauri-build = { version = "2.0.0-beta", features = [] }
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.25.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.56.0", features = [
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_Controls",
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
|
||||
|
||||
@@ -51,3 +58,4 @@ reqwest_cookie_store = "0.6.0"
|
||||
grpc = { path = "./grpc" }
|
||||
tokio-stream = "0.1.15"
|
||||
regex = "1.10.2"
|
||||
hex_color = "3.0.0"
|
||||
|
||||
6791
src-tauri/gen/schemas/windows-schema.json
Normal file
6791
src-tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
4
src-tauri/migrations/20240522031045_theme-settings.sql
Normal file
4
src-tauri/migrations/20240522031045_theme-settings.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE settings
|
||||
ADD COLUMN theme_dark TEXT DEFAULT 'yaak-dark' NOT NULL;
|
||||
ALTER TABLE settings
|
||||
ADD COLUMN theme_light TEXT DEFAULT 'yaak-light' NOT NULL;
|
||||
@@ -34,7 +34,6 @@ use tokio::sync::Mutex;
|
||||
|
||||
use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition};
|
||||
use ::grpc::manager::{DynamicMessage, GrpcHandle};
|
||||
use window_ext::TrafficLightWindowExt;
|
||||
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||
use crate::grpc::metadata_to_map;
|
||||
@@ -69,8 +68,11 @@ mod notifications;
|
||||
mod plugin;
|
||||
mod render;
|
||||
mod updates;
|
||||
mod window_ext;
|
||||
mod window_menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
|
||||
async fn migrate_db(app_handle: &AppHandle, db: &Mutex<Pool<Sqlite>>) -> Result<(), String> {
|
||||
let pool = &*db.lock().await;
|
||||
@@ -230,8 +232,8 @@ async fn cmd_grpc_go(
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
};
|
||||
let conn_id = conn.id.clone();
|
||||
|
||||
@@ -328,8 +330,8 @@ async fn cmd_grpc_go(
|
||||
..base_msg.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -344,8 +346,8 @@ async fn cmd_grpc_go(
|
||||
..base_msg.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
Ok(IncomingMsg::Commit) => {
|
||||
@@ -384,8 +386,8 @@ async fn cmd_grpc_go(
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
async move {
|
||||
let (maybe_stream, maybe_msg) = match (
|
||||
@@ -431,8 +433,8 @@ async fn cmd_grpc_go(
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
match maybe_msg {
|
||||
@@ -446,13 +448,13 @@ async fn cmd_grpc_go(
|
||||
} else {
|
||||
"Received response with metadata"
|
||||
}
|
||||
.to_string(),
|
||||
.to_string(),
|
||||
event_type: GrpcEventType::Info,
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
upsert_grpc_event(
|
||||
&w,
|
||||
&GrpcEvent {
|
||||
@@ -461,8 +463,8 @@ async fn cmd_grpc_go(
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
upsert_grpc_event(
|
||||
&w,
|
||||
&GrpcEvent {
|
||||
@@ -472,8 +474,8 @@ async fn cmd_grpc_go(
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
upsert_grpc_event(
|
||||
@@ -496,8 +498,8 @@ async fn cmd_grpc_go(
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
None => {
|
||||
// Server streaming doesn't return initial message
|
||||
@@ -515,13 +517,13 @@ async fn cmd_grpc_go(
|
||||
} else {
|
||||
"Received response with metadata"
|
||||
}
|
||||
.to_string(),
|
||||
.to_string(),
|
||||
event_type: GrpcEventType::Info,
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
stream.into_inner()
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
@@ -545,8 +547,8 @@ async fn cmd_grpc_go(
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
None => return,
|
||||
@@ -564,8 +566,8 @@ async fn cmd_grpc_go(
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Ok(None) => {
|
||||
let trailers = stream
|
||||
@@ -583,8 +585,8 @@ async fn cmd_grpc_go(
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
Err(status) => {
|
||||
@@ -598,8 +600,8 @@ async fn cmd_grpc_go(
|
||||
..base_event.clone()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -700,7 +702,7 @@ async fn cmd_send_ephemeral_request(
|
||||
None,
|
||||
&mut cancel_rx,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -767,7 +769,7 @@ async fn cmd_import_data(
|
||||
AnalyticsAction::Import,
|
||||
Some(json!({ "plugin": plugin_name })),
|
||||
)
|
||||
.await;
|
||||
.await;
|
||||
result = Some(r);
|
||||
break;
|
||||
}
|
||||
@@ -798,7 +800,7 @@ async fn cmd_import_data(
|
||||
let maybe_gen_id_opt = |id: Option<String>,
|
||||
model: ModelType,
|
||||
ids: &mut HashMap<String, String>|
|
||||
-> Option<String> {
|
||||
-> Option<String> {
|
||||
match id {
|
||||
Some(id) => Some(maybe_gen_id(id.as_str(), model, ids)),
|
||||
None => None,
|
||||
@@ -944,7 +946,7 @@ async fn cmd_export_data(
|
||||
AnalyticsAction::Export,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -995,8 +997,8 @@ async fn cmd_send_http_request(
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
|
||||
let download_path = if let Some(p) = download_dir {
|
||||
Some(std::path::Path::new(p).to_path_buf())
|
||||
@@ -1021,7 +1023,7 @@ async fn cmd_send_http_request(
|
||||
download_path,
|
||||
&mut cancel_rx,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
async fn response_err(
|
||||
@@ -1129,8 +1131,8 @@ async fn cmd_create_cookie_jar(
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1149,8 +1151,8 @@ async fn cmd_create_environment(
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1171,8 +1173,8 @@ async fn cmd_create_grpc_request(
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1281,8 +1283,8 @@ async fn cmd_create_folder(
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1412,8 +1414,8 @@ async fn cmd_list_cookie_jars(
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create CookieJar");
|
||||
.await
|
||||
.expect("Failed to create CookieJar");
|
||||
Ok(vec![cookie_jar])
|
||||
} else {
|
||||
Ok(cookie_jars)
|
||||
@@ -1482,8 +1484,8 @@ async fn cmd_list_workspaces(w: WebviewWindow) -> Result<Vec<Workspace>, String>
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create Workspace");
|
||||
.await
|
||||
.expect("Failed to create Workspace");
|
||||
Ok(vec![workspace])
|
||||
} else {
|
||||
Ok(workspaces)
|
||||
@@ -1733,16 +1735,16 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow {
|
||||
window_id,
|
||||
WebviewUrl::App(url.unwrap_or_default().into()),
|
||||
)
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.disable_drag_drop_handler() // Required for frontend Dnd on windows
|
||||
.inner_size(1100.0, 600.0)
|
||||
.position(
|
||||
// Randomly offset so windows don't stack exactly
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
)
|
||||
.title(handle.package_info().name.to_string());
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.disable_drag_drop_handler() // Required for frontend Dnd on windows
|
||||
.inner_size(1100.0, 600.0)
|
||||
.position(
|
||||
// Randomly offset so windows don't stack exactly
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
)
|
||||
.title(handle.package_info().name.to_string());
|
||||
|
||||
// Add macOS-only things
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -1798,25 +1800,19 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow {
|
||||
}
|
||||
});
|
||||
|
||||
let win3 = win.clone();
|
||||
win.on_window_event(move |e| {
|
||||
let apply_offset = || {
|
||||
win3.position_traffic_lights();
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use mac::setup_mac_window;
|
||||
let mut m_win = win.clone();
|
||||
setup_mac_window(&mut m_win);
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use win::setup_win_window;
|
||||
let mut m_win = win.clone();
|
||||
setup_win_window(&mut m_win);
|
||||
}
|
||||
|
||||
match e {
|
||||
WindowEvent::Resized(..) => apply_offset(),
|
||||
WindowEvent::ThemeChanged(..) => apply_offset(),
|
||||
WindowEvent::Focused(..) => apply_offset(),
|
||||
WindowEvent::ScaleFactorChanged { .. } => apply_offset(),
|
||||
WindowEvent::CloseRequested { .. } => {
|
||||
// api.prevent_close();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
win.position_traffic_lights();
|
||||
win
|
||||
}
|
||||
|
||||
|
||||
355
src-tauri/src/mac.rs
Normal file
355
src-tauri/src/mac.rs
Normal file
@@ -0,0 +1,355 @@
|
||||
// Borrowed from our friends at Hoppscotch
|
||||
// https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs
|
||||
|
||||
use hex_color::HexColor;
|
||||
use tauri::{Manager, WebviewWindow};
|
||||
|
||||
struct UnsafeWindowHandle(*mut std::ffi::c_void);
|
||||
|
||||
unsafe impl Send for UnsafeWindowHandle {}
|
||||
|
||||
unsafe impl Sync for UnsafeWindowHandle {}
|
||||
|
||||
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
|
||||
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn update_window_theme(window: &WebviewWindow, color: HexColor) {
|
||||
use cocoa::appkit::{
|
||||
NSAppearance, NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight, NSWindow,
|
||||
};
|
||||
|
||||
let brightness = (color.r as u64 + color.g as u64 + color.b as u64) / 3;
|
||||
|
||||
unsafe {
|
||||
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
|
||||
|
||||
let _ = window.run_on_main_thread(move || {
|
||||
let handle = window_handle;
|
||||
|
||||
let selected_appearance = if brightness >= 128 {
|
||||
NSAppearance(NSAppearanceNameVibrantLight)
|
||||
} else {
|
||||
NSAppearance(NSAppearanceNameVibrantDark)
|
||||
};
|
||||
|
||||
NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance);
|
||||
set_window_controls_pos(
|
||||
handle.0 as cocoa::base::id,
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_window_controls_pos(window: cocoa::base::id, x: f64, y: f64) {
|
||||
use cocoa::{
|
||||
appkit::{NSView, NSWindow, NSWindowButton},
|
||||
foundation::NSRect,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
|
||||
let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
||||
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
||||
|
||||
let title_bar_container_view = close.superview().superview();
|
||||
|
||||
let close_rect: NSRect = msg_send![close, frame];
|
||||
let button_height = close_rect.size.height;
|
||||
|
||||
let title_bar_frame_height = button_height + y;
|
||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||
title_bar_rect.size.height = title_bar_frame_height;
|
||||
title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
|
||||
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
|
||||
|
||||
let window_buttons = vec![close, miniaturize, zoom];
|
||||
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
|
||||
|
||||
for (i, button) in window_buttons.into_iter().enumerate() {
|
||||
let mut rect: NSRect = NSView::frame(button);
|
||||
rect.origin.x = x + (i as f64 * space_between);
|
||||
button.setFrameOrigin(rect.origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug)]
|
||||
struct AppState {
|
||||
window: WebviewWindow,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn setup_mac_window(window: &mut WebviewWindow) {
|
||||
use cocoa::delegate;
|
||||
use cocoa::appkit::NSWindow;
|
||||
use cocoa::base::{BOOL, id};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use objc::runtime::{Object, Sel};
|
||||
use std::ffi::c_void;
|
||||
|
||||
fn with_app_state<F: FnOnce(&mut AppState) -> T, T>(this: &Object, func: F) {
|
||||
let ptr = unsafe {
|
||||
let x: *mut c_void = *this.get_ivar("yaakApp");
|
||||
&mut *(x as *mut AppState)
|
||||
};
|
||||
func(ptr);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ns_win = window.ns_window().unwrap() as id;
|
||||
|
||||
let current_delegate: id = ns_win.delegate();
|
||||
|
||||
extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, windowShouldClose: sender]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillClose: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_app_state(&*this, |state| {
|
||||
let id = state.window.ns_window().unwrap() as id;
|
||||
|
||||
set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidResize: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidMove: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_change_backing_properties(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidBecomeKey: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidResignKey: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, draggingEntered: notification]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_prepare_for_drag_operation(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, prepareForDragOperation: notification]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, performDragOperation: sender]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, concludeDragOperation: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, draggingExited: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_use_full_screen_presentation_options(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
window: id,
|
||||
proposed_options: NSUInteger,
|
||||
) -> NSUInteger {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_enter_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_app_state(&*this, |state| {
|
||||
state.window.emit("did-enter-fullscreen", ()).unwrap();
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_enter_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_app_state(&*this, |state| {
|
||||
state.window.emit("will-enter-fullscreen", ()).unwrap();
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_exit_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_app_state(&*this, |state| {
|
||||
state.window.emit("did-exit-fullscreen", ()).unwrap();
|
||||
|
||||
let id = state.window.ns_window().unwrap() as id;
|
||||
set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidExitFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_exit_full_screen(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_app_state(&*this, |state| {
|
||||
state.window.emit("will-exit-fullscreen", ()).unwrap();
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_fail_to_enter_full_screen(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
window: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_effective_appearance_did_change(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![
|
||||
super_del,
|
||||
effectiveAppearanceDidChangedOnMainThread: notification
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// extern fn on_dealloc(this: &Object, cmd: Sel) {
|
||||
// unsafe {
|
||||
// let super_del: id = *this.get_ivar("super_delegate");
|
||||
// let _: () = msg_send![super_del, dealloc];
|
||||
// }
|
||||
// }
|
||||
|
||||
// extern fn on_mark_is_checking_zoomed_in(this: &Object, cmd: Sel) {
|
||||
// unsafe {
|
||||
// let super_del: id = *this.get_ivar("super_delegate");
|
||||
// let _: () = msg_send![super_del, markIsCheckingZoomedIn];
|
||||
// }
|
||||
// }
|
||||
|
||||
// extern fn on_clear_is_checking_zoomed_in(this: &Object, cmd: Sel) {
|
||||
// unsafe {
|
||||
// let super_del: id = *this.get_ivar("super_delegate");
|
||||
// let _: () = msg_send![super_del, clearIsCheckingZoomedIn];
|
||||
// }
|
||||
// }
|
||||
|
||||
// Are we deallocing this properly ? (I miss safe Rust :( )
|
||||
let w = window.clone();
|
||||
let app_state = AppState { window: w };
|
||||
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
|
||||
set_window_controls_pos(ns_win, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
|
||||
|
||||
ns_win.setDelegate_(delegate!("MainWindowDelegate", {
|
||||
window: id = ns_win,
|
||||
yaakApp: *mut c_void = app_box,
|
||||
toolbar: id = cocoa::base::nil,
|
||||
super_delegate: id = current_delegate,
|
||||
// (dealloc) => on_dealloc as extern fn(&Object, Sel),
|
||||
// (markIsCheckingZoomedIn) => on_mark_is_checking_zoomed_in as extern fn(&Object, Sel),
|
||||
// (clearIsCheckingZoomedIn) => on_clear_is_checking_zoomed_in as extern fn(&Object, Sel),
|
||||
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
|
||||
(windowDidResize:) => on_window_did_resize as extern fn(&Object, Sel, id),
|
||||
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
|
||||
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
|
||||
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
|
||||
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
|
||||
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
|
||||
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
|
||||
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
|
||||
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowDidExitFullScreen:) => on_window_did_exit_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowWillExitFullScreen:) => on_window_will_exit_full_screen as extern fn(&Object, Sel, id),
|
||||
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
|
||||
}))
|
||||
}
|
||||
|
||||
let app = window.app_handle();
|
||||
let window = window.clone();
|
||||
update_window_theme(&window, HexColor::WHITE);
|
||||
|
||||
// Control window theme based on app update_window
|
||||
let window = window.clone();
|
||||
app.listen_any("yaak_bg_changed", move |ev| {
|
||||
let payload = serde_json::from_str::<&str>(ev.payload())
|
||||
.unwrap()
|
||||
.trim();
|
||||
|
||||
let color = HexColor::parse_rgb(payload).unwrap();
|
||||
|
||||
update_window_theme(&window, color);
|
||||
});
|
||||
}
|
||||
@@ -4,9 +4,9 @@ use std::fs;
|
||||
use log::error;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use tauri::{AppHandle, Manager, WebviewWindow, Wry};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -52,6 +52,8 @@ pub struct Settings {
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub theme: String,
|
||||
pub appearance: String,
|
||||
pub theme_dark: String,
|
||||
pub theme_light: String,
|
||||
pub update_channel: String,
|
||||
}
|
||||
|
||||
@@ -883,7 +885,8 @@ async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings, sqlx::Error>
|
||||
Settings,
|
||||
r#"
|
||||
SELECT
|
||||
id, model, created_at, updated_at, theme, appearance, update_channel
|
||||
id, model, created_at, updated_at, theme, appearance,
|
||||
theme_dark, theme_light, update_channel
|
||||
FROM settings
|
||||
WHERE id = 'default'
|
||||
"#,
|
||||
@@ -919,11 +922,13 @@ pub async fn update_settings(
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE settings SET (
|
||||
theme, appearance, update_channel
|
||||
) = (?, ?, ?) WHERE id = 'default';
|
||||
theme, appearance, theme_dark, theme_light, update_channel
|
||||
) = (?, ?, ?, ?, ?) WHERE id = 'default';
|
||||
"#,
|
||||
settings.theme,
|
||||
settings.appearance,
|
||||
settings.theme_dark,
|
||||
settings.theme_light,
|
||||
settings.update_channel
|
||||
)
|
||||
.execute(&db)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use log::{debug, info};
|
||||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
@@ -118,7 +118,6 @@ impl YaakUpdater {
|
||||
|
||||
// Don't check if dev
|
||||
if is_dev() {
|
||||
debug!("Not checking for updates in dev");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
||||
79
src-tauri/src/win.rs
Normal file
79
src-tauri/src/win.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
// Borrowed from our friends at Hoppscotch
|
||||
// https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs
|
||||
use std::mem::transmute;
|
||||
|
||||
use hex_color::HexColor;
|
||||
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_CAPTION_COLOR;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_USE_IMMERSIVE_DARK_MODE;
|
||||
use windows::Win32::UI::Controls::{WTA_NONCLIENT, WTNCA_NODRAWICON, WTNCA_NOMIRRORHELP, WTNCA_NOSYSMENU};
|
||||
use windows::Win32::UI::Controls::SetWindowThemeAttribute;
|
||||
use windows::Win32::UI::Controls::WTNCA_NODRAWCAPTION;
|
||||
|
||||
fn hex_color_to_colorref(color: HexColor) -> COLORREF {
|
||||
// TODO: Remove this unsafe, This operation doesn't need to be unsafe!
|
||||
unsafe {
|
||||
COLORREF(transmute::<[u8; 4], u32>([color.r, color.g, color.b, 0]))
|
||||
}
|
||||
}
|
||||
|
||||
struct WinThemeAttribute {
|
||||
flag: u32,
|
||||
mask: u32
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn update_bg_color(hwnd: &HWND, bg_color: HexColor) {
|
||||
|
||||
let use_dark_mode = BOOL::from(true);
|
||||
|
||||
let final_color = hex_color_to_colorref(bg_color);
|
||||
|
||||
unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
HWND(hwnd.0),
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
ptr::addr_of!(use_dark_mode) as *const c_void,
|
||||
size_of::<BOOL>().try_into().unwrap()
|
||||
).unwrap();
|
||||
|
||||
DwmSetWindowAttribute(
|
||||
HWND(hwnd.0),
|
||||
DWMWA_CAPTION_COLOR,
|
||||
ptr::addr_of!(final_color) as *const c_void,
|
||||
size_of::<COLORREF>().try_into().unwrap()
|
||||
).unwrap();
|
||||
|
||||
let flags = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON;
|
||||
let mask = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON | WTNCA_NOSYSMENU | WTNCA_NOMIRRORHELP;
|
||||
let options = WinThemeAttribute { flag: flags, mask };
|
||||
|
||||
SetWindowThemeAttribute(
|
||||
HWND(hwnd.0),
|
||||
WTA_NONCLIENT,
|
||||
ptr::addr_of!(options) as *const c_void,
|
||||
size_of::<WinThemeAttribute>().try_into().unwrap()
|
||||
).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn setup_win_window(window: &mut WebviewWindow) {
|
||||
let win_handle = window.hwnd().unwrap();
|
||||
let win_clone = win_handle.clone();
|
||||
|
||||
window.listen_global("yaak_bg_changed", move |ev| {
|
||||
let payload = serde_json::from_str::<&str>(ev.payload().unwrap())
|
||||
.unwrap()
|
||||
.trim();
|
||||
|
||||
let color = HexColor::parse_rgb(payload).unwrap();
|
||||
update_bg_color(&HWND(win_clone.0), color);
|
||||
});
|
||||
|
||||
update_bg_color(&HWND(win_handle.0), HexColor::rgb(23, 23, 23));
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
use tauri::WebviewWindow;
|
||||
|
||||
const TRAFFIC_LIGHT_OFFSET_X: f64 = 13.0;
|
||||
const TRAFFIC_LIGHT_OFFSET_Y: f64 = 18.0;
|
||||
|
||||
pub trait TrafficLightWindowExt {
|
||||
fn position_traffic_lights(&self);
|
||||
}
|
||||
|
||||
impl TrafficLightWindowExt for WebviewWindow {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn position_traffic_lights(&self) {
|
||||
// No-op on other platforms
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn position_traffic_lights(&self) {
|
||||
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
|
||||
use cocoa::foundation::NSRect;
|
||||
|
||||
let window = self.ns_window().unwrap() as cocoa::base::id;
|
||||
|
||||
let x = TRAFFIC_LIGHT_OFFSET_X;
|
||||
let y = TRAFFIC_LIGHT_OFFSET_Y;
|
||||
|
||||
unsafe {
|
||||
let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
|
||||
let miniaturize =
|
||||
window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
||||
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
||||
|
||||
let title_bar_container_view = close.superview().superview();
|
||||
|
||||
let close_rect: NSRect = msg_send![close, frame];
|
||||
let button_height = close_rect.size.height;
|
||||
|
||||
let title_bar_frame_height = button_height + y;
|
||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||
title_bar_rect.size.height = title_bar_frame_height;
|
||||
title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
|
||||
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
|
||||
|
||||
let window_buttons = vec![close, miniaturize, zoom];
|
||||
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
|
||||
|
||||
for (i, button) in window_buttons.into_iter().enumerate() {
|
||||
let mut rect: NSRect = NSView::frame(button);
|
||||
rect.origin.x = x + (i as f64 * space_between);
|
||||
button.setFrameOrigin(rect.origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getCurrent } from '@tauri-apps/api/webviewWindow';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCommandPalette } from '../hooks/useCommandPalette';
|
||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||
import { foldersQueryKey } from '../hooks/useFolders';
|
||||
@@ -19,12 +17,11 @@ import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { settingsQueryKey } from '../hooks/useSettings';
|
||||
import { useSyncAppearance } from '../hooks/useSyncAppearance';
|
||||
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
|
||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import type { Model } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
import { setPathname } from '../lib/persistPathname';
|
||||
|
||||
const DEFAULT_FONT_SIZE = 16;
|
||||
|
||||
@@ -35,7 +32,7 @@ export function GlobalHooks() {
|
||||
useRecentRequests();
|
||||
|
||||
// Other useful things
|
||||
useSyncAppearance();
|
||||
useSyncThemeToDocument();
|
||||
useSyncWindowTitle();
|
||||
useGlobalCommands();
|
||||
useCommandPalette();
|
||||
@@ -44,12 +41,6 @@ export function GlobalHooks() {
|
||||
const queryClient = useQueryClient();
|
||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||
|
||||
// Listen for location changes and update the pathname
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
setPathname(location.pathname).catch(console.error);
|
||||
}, [location.pathname]);
|
||||
|
||||
interface ModelPayload {
|
||||
model: Model;
|
||||
windowLabel: string;
|
||||
|
||||
15
src-web/components/IsDev.tsx
Normal file
15
src-web/components/IsDev.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAppInfo } from '../hooks/useAppInfo';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function IsDev({ children }: Props) {
|
||||
const appInfo = useAppInfo();
|
||||
if (!appInfo?.isDev) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -20,9 +20,11 @@ import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
|
||||
import { ResponseHeaders } from './ResponseHeaders';
|
||||
import { AudioViewer } from './responseViewers/AudioViewer';
|
||||
import { CsvViewer } from './responseViewers/CsvViewer';
|
||||
import { ImageViewer } from './responseViewers/ImageViewer';
|
||||
import { TextViewer } from './responseViewers/TextViewer';
|
||||
import { VideoViewer } from './responseViewers/VideoViewer';
|
||||
import { WebPageViewer } from './responseViewers/WebPageViewer';
|
||||
|
||||
interface Props {
|
||||
@@ -76,7 +78,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
'x-theme-responsePane',
|
||||
'max-h-full h-full',
|
||||
'bg-background rounded-md border border-background-highlight',
|
||||
'shadow relative',
|
||||
'relative',
|
||||
)}
|
||||
>
|
||||
{activeResponse == null ? (
|
||||
@@ -154,6 +156,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
</div>
|
||||
) : contentType?.startsWith('image') ? (
|
||||
<ImageViewer className="pb-2" response={activeResponse} />
|
||||
) : contentType?.startsWith('audio') ? (
|
||||
<AudioViewer response={activeResponse} />
|
||||
) : contentType?.startsWith('video') ? (
|
||||
<VideoViewer response={activeResponse} />
|
||||
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
|
||||
<EmptyStateText>Cannot preview text responses larger than 2MB</EmptyStateText>
|
||||
) : viewMode === 'pretty' && contentType?.includes('html') ? (
|
||||
@@ -161,7 +167,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
) : contentType?.match(/csv|tab-separated/) ? (
|
||||
<CsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
<TextViewer response={activeResponse} pretty={viewMode === 'pretty'} />
|
||||
<TextViewer
|
||||
className="-mr-2" // Pull to the right
|
||||
response={activeResponse}
|
||||
pretty={viewMode === 'pretty'}
|
||||
/>
|
||||
)}
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
|
||||
162
src-web/components/Settings/SettingsAppearance.tsx
Normal file
162
src-web/components/Settings/SettingsAppearance.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
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 { isThemeDark } from '../../lib/theme/window';
|
||||
import type { ButtonProps } from '../core/Button';
|
||||
import { Editor } from '../core/Editor';
|
||||
import type { IconProps } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import type { SelectOption } from '../core/Select';
|
||||
import { Select } from '../core/Select';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
|
||||
const buttonColors: ButtonProps['color'][] = [
|
||||
'primary',
|
||||
'info',
|
||||
'success',
|
||||
'notice',
|
||||
'warning',
|
||||
'danger',
|
||||
'secondary',
|
||||
'default',
|
||||
];
|
||||
|
||||
const icons: IconProps['icon'][] = [
|
||||
'info',
|
||||
'box',
|
||||
'update',
|
||||
'alert',
|
||||
'arrowBigRightDash',
|
||||
'download',
|
||||
'copy',
|
||||
'magicWand',
|
||||
'settings',
|
||||
'trash',
|
||||
'sparkles',
|
||||
'pencil',
|
||||
'paste',
|
||||
'search',
|
||||
'sendHorizontal',
|
||||
];
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lightThemes: SelectOption<string>[] = themes
|
||||
.filter((theme) => !isThemeDark(theme))
|
||||
.map((theme) => ({
|
||||
label: theme.name,
|
||||
value: theme.id,
|
||||
}));
|
||||
|
||||
const darkThemes: SelectOption<string>[] = themes
|
||||
.filter((theme) => isThemeDark(theme))
|
||||
.map((theme) => ({
|
||||
label: theme.name,
|
||||
value: theme.id,
|
||||
}));
|
||||
|
||||
return (
|
||||
<VStack space={2} className="mb-4">
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={async (appearance) => {
|
||||
await updateSettings.mutateAsync({ ...settings, appearance });
|
||||
trackEvent('setting', 'update', { appearance });
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Sync with OS', value: 'system' },
|
||||
{ label: 'Light', value: 'light' },
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
]}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Select
|
||||
name="lightTheme"
|
||||
label={'Light Theme' + (appearance !== 'dark' ? ' (active)' : '')}
|
||||
labelPosition="top"
|
||||
size="sm"
|
||||
value={activeTheme.light.id}
|
||||
options={lightThemes}
|
||||
onChange={async (themeLight) => {
|
||||
await updateSettings.mutateAsync({ ...settings, themeLight });
|
||||
trackEvent('setting', 'update', { themeLight });
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
name="darkTheme"
|
||||
label={'Dark Theme' + (appearance === 'dark' ? ' (active)' : '')}
|
||||
labelPosition="top"
|
||||
size="sm"
|
||||
value={activeTheme.dark.id}
|
||||
options={darkThemes}
|
||||
onChange={async (themeDark) => {
|
||||
await updateSettings.mutateAsync({ ...settings, themeDark });
|
||||
trackEvent('setting', 'update', { themeDark });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<VStack
|
||||
space={3}
|
||||
className="mt-3 w-full bg-background p-3 border border-dashed border-background-highlight rounded"
|
||||
>
|
||||
<div className="text-sm text-fg font-bold">
|
||||
Theme Preview <span className="text-fg-subtle">({appearance})</span>
|
||||
</div>
|
||||
<HStack space={1.5} alignItems="center" className="w-full">
|
||||
{buttonColors.map((c, i) => (
|
||||
<IconButton
|
||||
key={c}
|
||||
color={c}
|
||||
size="2xs"
|
||||
iconSize="xs"
|
||||
icon={icons[i % icons.length]!}
|
||||
iconClassName="text-fg"
|
||||
title={`${c}`}
|
||||
/>
|
||||
))}
|
||||
{buttonColors.map((c, i) => (
|
||||
<IconButton
|
||||
key={c}
|
||||
color={c}
|
||||
variant="border"
|
||||
size="2xs"
|
||||
iconSize="xs"
|
||||
icon={icons[i % icons.length]!}
|
||||
iconClassName="text-fg"
|
||||
title={`${c}`}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
<Editor
|
||||
defaultValue={[
|
||||
'let foo = { // Demo code editor',
|
||||
' foo: ("bar" || "baz" ?? \'qux\'),',
|
||||
' baz: [1, 10.2, null, false, true],',
|
||||
'};',
|
||||
].join('\n')}
|
||||
heightMode="auto"
|
||||
contentType="application/javascript"
|
||||
/>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
103
src-web/components/Settings/SettingsDesign.tsx
Normal file
103
src-web/components/Settings/SettingsDesign.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { capitalize } from '../../lib/capitalize';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { Button } from '../core/Button';
|
||||
import { Editor } from '../core/Editor';
|
||||
import type { IconProps } from '../core/Icon';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { Input } from '../core/Input';
|
||||
|
||||
const buttonColors = [
|
||||
'primary',
|
||||
'secondary',
|
||||
'info',
|
||||
'success',
|
||||
'warning',
|
||||
'danger',
|
||||
'default',
|
||||
] as const;
|
||||
|
||||
const icons: IconProps['icon'][] = [
|
||||
'info',
|
||||
'box',
|
||||
'update',
|
||||
'alert',
|
||||
'arrowBigRightDash',
|
||||
'download',
|
||||
'copy',
|
||||
'magicWand',
|
||||
'settings',
|
||||
'trash',
|
||||
'sparkles',
|
||||
'pencil',
|
||||
'paste',
|
||||
'search',
|
||||
'sendHorizontal',
|
||||
];
|
||||
|
||||
export function SettingsDesign() {
|
||||
return (
|
||||
<div className="p-2 flex flex-col gap-3">
|
||||
<Input
|
||||
label="Field Label"
|
||||
name="demo"
|
||||
placeholder="Placeholder"
|
||||
size="sm"
|
||||
rightSlot={<IconButton title="search" size="xs" className="w-8 m-0.5" icon="search" />}
|
||||
/>
|
||||
<Editor
|
||||
defaultValue={[
|
||||
'// Demo code editor',
|
||||
'let foo = {',
|
||||
' foo: ("bar" || "baz" ?? \'qux\'),',
|
||||
' baz: [1, 10.2, null, false, true],',
|
||||
'};',
|
||||
].join('\n')}
|
||||
heightMode="auto"
|
||||
contentType="application/javascript"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{buttonColors.map((c, i) => (
|
||||
<Button key={c} color={c} size="sm" leftSlot={<Icon size="sm" icon={icons[i]!} />}>
|
||||
{capitalize(c).slice(0, 4)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{buttonColors.map((c, i) => (
|
||||
<Button
|
||||
key={c}
|
||||
color={c}
|
||||
variant="border"
|
||||
size="sm"
|
||||
leftSlot={<Icon size="sm" icon={icons[i]!} />}
|
||||
>
|
||||
{capitalize(c).slice(0, 4)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{icons.map((v, i) => (
|
||||
<IconButton
|
||||
color={buttonColors[i % buttonColors.length]}
|
||||
title={v}
|
||||
variant="border"
|
||||
size="sm"
|
||||
key={v}
|
||||
icon={v}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Banner color="primary">Primary banner</Banner>
|
||||
<Banner color="secondary">Secondary banner</Banner>
|
||||
<Banner color="danger">Danger banner</Banner>
|
||||
<Banner color="warning">Warning banner</Banner>
|
||||
<Banner color="success">Success banner</Banner>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
src-web/components/Settings/SettingsDialog.tsx
Normal file
51
src-web/components/Settings/SettingsDialog.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import classNames from 'classnames';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useAppInfo } from '../../hooks/useAppInfo';
|
||||
import { capitalize } from '../../lib/capitalize';
|
||||
import { TabContent, Tabs } from '../core/Tabs/Tabs';
|
||||
import { SettingsAppearance } from './SettingsAppearance';
|
||||
import { SettingsDesign } from './SettingsDesign';
|
||||
import { SettingsGeneral } from './SettingsGeneral';
|
||||
|
||||
enum Tab {
|
||||
General = 'general',
|
||||
Appearance = 'appearance',
|
||||
|
||||
// Dev-only
|
||||
Design = 'design',
|
||||
}
|
||||
|
||||
const tabs = [Tab.General, Tab.Appearance, Tab.Design];
|
||||
|
||||
const useTabState = createGlobalState<string>(Tab.Appearance);
|
||||
|
||||
export const SettingsDialog = () => {
|
||||
const [tab, setTab] = useTabState();
|
||||
const appInfo = useAppInfo();
|
||||
const isDev = appInfo?.isDev ?? false;
|
||||
|
||||
return (
|
||||
<div className={classNames('w-[70vw] max-w-[40rem]', 'h-[80vh]')}>
|
||||
<Tabs
|
||||
value={tab}
|
||||
addBorders
|
||||
label="Settings"
|
||||
tabListClassName="h-md !-ml-1 mt-2"
|
||||
onChangeValue={setTab}
|
||||
tabs={tabs
|
||||
.filter((t) => t !== Tab.Design || isDev)
|
||||
.map((value) => ({ value, label: capitalize(value) }))}
|
||||
>
|
||||
<TabContent value={Tab.General} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsGeneral />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsAppearance />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Design} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsDesign />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppInfo } from '../hooks/useAppInfo';
|
||||
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Heading } from './core/Heading';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { Input } from './core/Input';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
import { Select } from './core/Select';
|
||||
import { Separator } from './core/Separator';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
|
||||
import { useAppInfo } from '../../hooks/useAppInfo';
|
||||
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
|
||||
import { useUpdateWorkspace } from '../../hooks/useUpdateWorkspace';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { Heading } from '../core/Heading';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { Input } from '../core/Input';
|
||||
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow';
|
||||
import { Select } from '../core/Select';
|
||||
import { Separator } from '../core/Separator';
|
||||
import { VStack } from '../core/Stacks';
|
||||
|
||||
export const SettingsDialog = () => {
|
||||
export function SettingsGeneral() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const updateWorkspace = useUpdateWorkspace(workspace?.id ?? null);
|
||||
const settings = useSettings();
|
||||
@@ -28,32 +28,6 @@ export const SettingsDialog = () => {
|
||||
|
||||
return (
|
||||
<VStack space={2} className="mb-4">
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={async (appearance) => {
|
||||
await updateSettings.mutateAsync({ ...settings, appearance });
|
||||
trackEvent('setting', 'update', { appearance });
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: 'System',
|
||||
value: 'system',
|
||||
},
|
||||
{
|
||||
label: 'Light',
|
||||
value: 'light',
|
||||
},
|
||||
{
|
||||
label: 'Dark',
|
||||
value: 'dark',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
|
||||
<Select
|
||||
name="updateChannel"
|
||||
@@ -133,10 +107,10 @@ export const SettingsDialog = () => {
|
||||
|
||||
<Heading size={2}>App Info</Heading>
|
||||
<KeyValueRows>
|
||||
<KeyValueRow label="Version" value={appInfo.data?.version} />
|
||||
<KeyValueRow label="Data Directory" value={appInfo.data?.appDataDir} />
|
||||
<KeyValueRow label="Logs Directory" value={appInfo.data?.appLogDir} />
|
||||
<KeyValueRow label="Version" value={appInfo?.version} />
|
||||
<KeyValueRow label="Data Directory" value={appInfo?.appDataDir} />
|
||||
<KeyValueRow label="Logs Directory" value={appInfo?.appLogDir} />
|
||||
</KeyValueRows>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
import { SettingsDialog } from './SettingsDialog';
|
||||
import { SettingsDialog } from './Settings/SettingsDialog';
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
@@ -24,8 +24,9 @@ export function SettingsDropdown() {
|
||||
const showSettings = () => {
|
||||
dialog.show({
|
||||
id: 'settings',
|
||||
size: 'md',
|
||||
title: 'Settings',
|
||||
size: 'dynamic',
|
||||
noScroll: true,
|
||||
noPadding: true,
|
||||
render: () => <SettingsDialog />,
|
||||
});
|
||||
};
|
||||
@@ -69,7 +70,7 @@ export function SettingsDropdown() {
|
||||
leftSlot: <Icon icon="folderOutput" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appInfo.data?.version}` },
|
||||
{ type: 'separator', label: `Yaak v${appInfo?.version}` },
|
||||
{
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
@@ -88,7 +89,7 @@ export function SettingsDropdown() {
|
||||
label: 'Changelog',
|
||||
leftSlot: <Icon icon="cake" />,
|
||||
rightSlot: <Icon icon="externalLink" />,
|
||||
onSelect: () => open(`https://yaak.app/changelog/${appInfo.data?.version}`),
|
||||
onSelect: () => open(`https://yaak.app/changelog/${appInfo?.version}`),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -468,47 +468,6 @@ export function Sidebar({ className }: Props) {
|
||||
handleEnd={handleEnd}
|
||||
handleDragStart={handleDragStart}
|
||||
/>
|
||||
{/*<div className="p-2 flex flex-col gap-1">*/}
|
||||
{/* <div className="flex flex-wrap gap-1">*/}
|
||||
{/* <Button color="primary">Primary</Button>*/}
|
||||
{/* <Button color="secondary">Secondary</Button>*/}
|
||||
{/* <Button color="info">Info</Button>*/}
|
||||
{/* <Button color="success">Success</Button>*/}
|
||||
{/* <Button color="warning">Warning</Button>*/}
|
||||
{/* <Button color="danger">Danger</Button>*/}
|
||||
{/* <Button color="default">Default</Button>*/}
|
||||
{/* </div>*/}
|
||||
{/* <div className="flex flex-wrap gap-1">*/}
|
||||
{/* <Button variant="border" color="primary">*/}
|
||||
{/* Primary*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button variant="border" color="secondary">*/}
|
||||
{/* Secondary*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button variant="border" color="info">*/}
|
||||
{/* Info*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button variant="border" color="success">*/}
|
||||
{/* Success*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button variant="border" color="warning">*/}
|
||||
{/* Warning*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button variant="border" color="danger">*/}
|
||||
{/* Danger*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button variant="border" color="default">*/}
|
||||
{/* Default*/}
|
||||
{/* </Button>*/}
|
||||
{/* </div>*/}
|
||||
{/* <div className="flex flex-col gap-1">*/}
|
||||
{/* <Banner color="primary">Primary banner</Banner>*/}
|
||||
{/* <Banner color="secondary">Secondary banner</Banner>*/}
|
||||
{/* <Banner color="danger">Danger banner</Banner>*/}
|
||||
{/* <Banner color="warning">Warning banner</Banner>*/}
|
||||
{/* <Banner color="success">Success banner</Banner>*/}
|
||||
{/* </div>*/}
|
||||
{/*</div>*/}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -800,11 +759,14 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
const name = await prompt({
|
||||
id: 'rename-request',
|
||||
title: 'Rename Request',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||
</>
|
||||
),
|
||||
description:
|
||||
itemName === '' ? (
|
||||
'Enter a new name'
|
||||
) : (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
placeholder: 'New Name',
|
||||
@@ -893,7 +855,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
{isResponseLoading(latestHttpResponse) ? (
|
||||
<Icon spin size="sm" icon="refresh" className="text-fg-subtler" />
|
||||
) : (
|
||||
<StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} />
|
||||
<StatusTag className="text-2xs" response={latestHttpResponse} />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -59,7 +59,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classNames(className)}>
|
||||
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
|
||||
<Input
|
||||
autocompleteVariables
|
||||
ref={inputRef}
|
||||
@@ -76,7 +76,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onPaste={onPaste}
|
||||
containerClassName="shadow bg-background border"
|
||||
containerClassName="bg-background border border-background-highlight"
|
||||
onChange={onUrlChange}
|
||||
defaultValue={url}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger';
|
||||
}
|
||||
|
||||
export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
|
||||
@@ -14,11 +14,12 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
|
||||
| 'primary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'notice'
|
||||
| 'warning'
|
||||
| 'danger';
|
||||
variant?: 'border' | 'solid';
|
||||
isLoading?: boolean;
|
||||
size?: 'xs' | 'sm' | 'md';
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md';
|
||||
justify?: 'start' | 'center';
|
||||
type?: 'button' | 'submit';
|
||||
forDropdown?: boolean;
|
||||
@@ -60,24 +61,27 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
`x-theme-button--${variant}`,
|
||||
`x-theme-button--${variant}--${color}`,
|
||||
'text-fg',
|
||||
'border', // They all have borders to ensure the same width
|
||||
'max-w-full min-w-0', // Help with truncation
|
||||
'hocus:opacity-100', // Force opacity for certain hover effects
|
||||
'whitespace-nowrap outline-none',
|
||||
'flex-shrink-0 flex items-center',
|
||||
'focus-visible-or-class:ring rounded-md',
|
||||
'focus-visible-or-class:ring',
|
||||
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
|
||||
justify === 'start' && 'justify-start',
|
||||
justify === 'center' && 'justify-center',
|
||||
size === 'md' && 'h-md px-3',
|
||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||
size === 'xs' && 'h-xs px-2 text-sm',
|
||||
size === 'md' && 'h-md px-3 rounded-md',
|
||||
size === 'sm' && 'h-sm px-2.5 text-sm rounded-md',
|
||||
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
|
||||
size === '2xs' && 'h-5 px-1 text-xs rounded',
|
||||
|
||||
// Solids
|
||||
variant === 'solid' && 'border-transparent',
|
||||
variant === 'solid' && color === 'custom' && 'ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color !== 'custom' &&
|
||||
color !== 'default' &&
|
||||
'bg-background enabled:hocus:bg-background-highlight ring-background-highlight-secondary',
|
||||
variant === 'solid' && color === 'custom' && 'ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color === 'default' &&
|
||||
'enabled:hocus:bg-background-highlight ring-fg-info',
|
||||
|
||||
@@ -30,7 +30,7 @@ export function Checkbox({
|
||||
alignItems="center"
|
||||
className={classNames(className, 'text-fg text-sm', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className={classNames(inputWrapperClassName, 'relative flex')}>
|
||||
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
|
||||
<input
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
|
||||
@@ -94,7 +94,7 @@ export function Dialog({
|
||||
className={classNames(
|
||||
'h-full w-full grid grid-cols-[minmax(0,1fr)]',
|
||||
!noPadding && 'px-6 py-2',
|
||||
!noScroll && 'overflow-y-auto',
|
||||
!noScroll && 'overflow-y-auto overflow-x-hidden',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -413,7 +413,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
)}
|
||||
{isOpen && (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div className="x-theme-dialog">
|
||||
<div className="x-theme-menu">
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={handleClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
.cm-cursor {
|
||||
@apply border-fg !important;
|
||||
/* Widen the cursor */
|
||||
@apply border-l-2;
|
||||
@apply border-l-[2px];
|
||||
}
|
||||
|
||||
&.cm-focused {
|
||||
|
||||
@@ -283,7 +283,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative h-full w-full">
|
||||
<div className="group relative h-full w-full x-theme-editor bg-background">
|
||||
{cmContainer}
|
||||
{decoratedActions && (
|
||||
<HStack
|
||||
|
||||
@@ -46,45 +46,23 @@ export const myHighlightStyle = HighlightStyle.define([
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
{
|
||||
tag: [t.paren],
|
||||
tag: [t.paren, t.bracket, t.brace],
|
||||
color: 'var(--fg)',
|
||||
},
|
||||
{
|
||||
tag: [t.name, t.tagName, t.angleBracket, t.docString, t.number],
|
||||
tag: [t.link, t.name, t.tagName, t.angleBracket, t.docString, t.number],
|
||||
color: 'var(--fg-info)',
|
||||
},
|
||||
{ tag: [t.variableName], color: 'var(--fg-success)' },
|
||||
{ tag: [t.bool], color: 'var(--fg-info)' }, // TODO: Should be pink
|
||||
{ tag: [t.bool], color: 'var(--fg-warning)' },
|
||||
{ tag: [t.attributeName, t.propertyName], color: 'var(--fg-primary)' },
|
||||
{ tag: [t.attributeValue], color: 'var(--fg-warning)' },
|
||||
{ tag: [t.string], color: 'var(--fg-warning)' }, // TODO: Should be yellow
|
||||
{ tag: [t.keyword, t.meta, t.operator], color: 'var(--fg-danger)' },
|
||||
{ tag: [t.string], color: 'var(--fg-notice)' },
|
||||
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--fg-danger)' },
|
||||
]);
|
||||
|
||||
const myTheme = EditorView.theme({}, { dark: true });
|
||||
|
||||
// export const defaultHighlightStyle = HighlightStyle.define([
|
||||
// { tag: t.meta, color: '#404740' },
|
||||
// { tag: t.link, textDecoration: 'underline' },
|
||||
// { tag: t.heading, textDecoration: 'underline', fontWeight: 'bold' },
|
||||
// { tag: t.emphasis, fontStyle: 'italic' },
|
||||
// { tag: t.strong, fontWeight: 'bold' },
|
||||
// { tag: t.strikethrough, textDecoration: 'line-through' },
|
||||
// { tag: t.keyword, color: '#708' },
|
||||
// { tag: [t.atom, t.bool, t.url, t.contentSeparator, t.labelName], color: '#219' },
|
||||
// { tag: [t.literal, t.inserted], color: '#164' },
|
||||
// { tag: [t.string, t.deleted], color: '#a11' },
|
||||
// { tag: [t.regexp, t.escape, t.special(t.string)], color: '#e40' },
|
||||
// { tag: t.definition(t.variableName), color: '#00f' },
|
||||
// { tag: t.local(t.variableName), color: '#30a' },
|
||||
// { tag: [t.typeName, t.namespace], color: '#085' },
|
||||
// { tag: t.className, color: '#167' },
|
||||
// { tag: [t.special(t.variableName), t.macroName], color: '#256' },
|
||||
// { tag: t.definition(t.propertyName), color: '#00c' },
|
||||
// { tag: t.comment, color: '#940' },
|
||||
// { tag: t.invalid, color: '#f00' },
|
||||
// ]);
|
||||
|
||||
const syntaxExtensions: Record<string, LanguageSupport> = {
|
||||
'application/graphql': graphqlLanguageSupport(),
|
||||
'application/json': json(),
|
||||
|
||||
@@ -66,7 +66,7 @@ const icons = {
|
||||
export interface IconProps {
|
||||
icon: keyof typeof icons;
|
||||
className?: string;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg';
|
||||
spin?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
@@ -83,6 +83,7 @@ export const Icon = memo(function Icon({ icon, spin, size = 'md', className, tit
|
||||
size === 'md' && 'h-4 w-4',
|
||||
size === 'sm' && 'h-3.5 w-3.5',
|
||||
size === 'xs' && 'h-3 w-3',
|
||||
size === '2xs' && 'h-2.5 w-2.5',
|
||||
spin && 'animate-spin',
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -57,6 +57,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
size === 'md' && 'w-9',
|
||||
size === 'sm' && 'w-8',
|
||||
size === 'xs' && 'w-6',
|
||||
size === '2xs' && 'w-5',
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -6,8 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
|
||||
<code
|
||||
className={classNames(
|
||||
className,
|
||||
'font-mono text-xs bg-background-highlight-secondary',
|
||||
'px-1.5 py-0.5 rounded text-fg shadow-inner',
|
||||
'font-mono text-xs bg-background-highlight-secondary border border-background-highlight',
|
||||
'px-1.5 py-0.5 rounded text-fg-info shadow-inner',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -145,6 +145,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
alignItems="stretch"
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text-fg',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-background-highlight',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
@@ -9,6 +10,7 @@ export type RadioDropdownItem<T = string | null> =
|
||||
label: string;
|
||||
shortLabel?: string;
|
||||
value: T;
|
||||
rightSlot?: ReactNode;
|
||||
}
|
||||
| DropdownItemSeparator;
|
||||
|
||||
@@ -37,9 +39,10 @@ export function RadioDropdown<T = string | null>({
|
||||
key: item.label,
|
||||
label: item.label,
|
||||
shortLabel: item.shortLabel,
|
||||
rightSlot: item.rightSlot,
|
||||
onSelect: () => onChange(item.value),
|
||||
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
|
||||
};
|
||||
} as DropdownProps['items'][0];
|
||||
}
|
||||
}),
|
||||
...((extraItems ? [{ type: 'separator' }, ...extraItems] : []) as DropdownItem[]),
|
||||
|
||||
@@ -7,12 +7,17 @@ interface Props<T extends string> {
|
||||
labelClassName?: string;
|
||||
hideLabel?: boolean;
|
||||
value: T;
|
||||
options: { label: string; value: T }[];
|
||||
options: SelectOption<T>[];
|
||||
onChange: (value: T) => void;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface SelectOption<T extends string> {
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export function Select<T extends string>({
|
||||
labelPosition = 'top',
|
||||
name,
|
||||
@@ -30,6 +35,7 @@ export function Select<T extends string>({
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'x-theme-input',
|
||||
'w-full',
|
||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||
labelPosition === 'left' && 'flex items-center gap-2',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { Button } from '../Button';
|
||||
import { Icon } from '../Icon';
|
||||
import type { RadioDropdownProps } from '../RadioDropdown';
|
||||
import { RadioDropdown } from '../RadioDropdown';
|
||||
@@ -25,6 +24,7 @@ interface Props {
|
||||
tabListClassName?: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
addBorders?: boolean;
|
||||
}
|
||||
|
||||
export function Tabs({
|
||||
@@ -35,6 +35,7 @@ export function Tabs({
|
||||
tabs,
|
||||
className,
|
||||
tabListClassName,
|
||||
addBorders,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -78,12 +79,15 @@ export function Tabs({
|
||||
'-ml-5 pl-3 pr-1 py-1',
|
||||
)}
|
||||
>
|
||||
<HStack space={2} className="flex-shrink-0">
|
||||
<HStack space={2} className="h-full flex-shrink-0">
|
||||
{tabs.map((t) => {
|
||||
const isActive = t.value === value;
|
||||
const btnClassName = classNames(
|
||||
isActive ? 'text-fg' : 'text-fg-subtler hover:text-fg-subtle',
|
||||
'h-full flex items-center text-sm rounded',
|
||||
'!px-2 ml-[1px]',
|
||||
addBorders && 'border',
|
||||
isActive ? 'text-fg' : 'text-fg-subtler hover:text-fg-subtle',
|
||||
isActive && addBorders ? 'border-background-highlight' : 'border-transparent',
|
||||
);
|
||||
|
||||
if ('options' in t) {
|
||||
@@ -97,39 +101,34 @@ export function Tabs({
|
||||
value={t.options.value}
|
||||
onChange={t.options.onChange}
|
||||
>
|
||||
<Button
|
||||
<button
|
||||
color="custom"
|
||||
size="sm"
|
||||
onClick={isActive ? undefined : () => handleTabChange(t.value)}
|
||||
className={btnClassName}
|
||||
rightSlot={
|
||||
<Icon
|
||||
size="sm"
|
||||
icon="chevronDown"
|
||||
className={classNames(
|
||||
'-mr-1.5 mt-0.5',
|
||||
isActive ? 'text-fg-subtle' : 'opacity-50',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{option && 'shortLabel' in option
|
||||
? option.shortLabel
|
||||
: option?.label ?? 'Unknown'}
|
||||
</Button>
|
||||
<TabAccent enabled isActive={isActive} />
|
||||
<Icon
|
||||
size="sm"
|
||||
icon="chevronDown"
|
||||
className={classNames('ml-1', isActive ? 'text-fg-subtle' : 'opacity-50')}
|
||||
/>
|
||||
</button>
|
||||
</RadioDropdown>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={t.value}
|
||||
color="custom"
|
||||
size="sm"
|
||||
onClick={() => handleTabChange(t.value)}
|
||||
className={btnClassName}
|
||||
>
|
||||
{t.label}
|
||||
</Button>
|
||||
<TabAccent enabled isActive={isActive} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
})}
|
||||
@@ -161,3 +160,14 @@ export const TabContent = memo(function TabContent({
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function TabAccent({ isActive, enabled }: { isActive: boolean; enabled: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full opacity-40 border-b-2',
|
||||
isActive && enabled ? 'border-b-background-highlight' : 'border-b-transparent',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ export function Toast({
|
||||
transition={{ duration: 0.2 }}
|
||||
className={classNames(
|
||||
className,
|
||||
'x-theme-dialog',
|
||||
'x-theme-toast',
|
||||
'pointer-events-auto',
|
||||
'relative bg-background pointer-events-auto',
|
||||
'rounded-lg',
|
||||
'border border-background-highlight dark:border-background-highlight-secondary shadow-xl',
|
||||
'border border-background-highlight shadow-lg',
|
||||
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
|
||||
'w-[22rem] max-h-[80vh]',
|
||||
'm-2 grid grid-cols-[1fr_auto]',
|
||||
|
||||
18
src-web/components/responseViewers/AudioViewer.tsx
Normal file
18
src-web/components/responseViewers/AudioViewer.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import React from 'react';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
}
|
||||
|
||||
export function AudioViewer({ response }: Props) {
|
||||
if (response.bodyPath === null) {
|
||||
return <div>Empty response body</div>;
|
||||
}
|
||||
|
||||
const src = convertFileSrc(response.bodyPath);
|
||||
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
return <audio className="w-full" controls src={src}></audio>;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
import { Button } from '../core/Button';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
@@ -23,13 +22,9 @@ export function ImageViewer({ response, className }: Props) {
|
||||
<>
|
||||
<div className="text-sm italic text-fg-subtler">
|
||||
Response body is too large to preview.{' '}
|
||||
<Button
|
||||
className="cursor-pointer underline hover:text-fg"
|
||||
color="secondary"
|
||||
onClick={() => setShow(true)}
|
||||
>
|
||||
<button className="cursor-pointer underline hover:text-fg" onClick={() => setShow(true)}>
|
||||
Show anyway
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -18,9 +18,10 @@ const extraExtensions = [hyperlink];
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
pretty: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TextViewer({ response, pretty }: Props) {
|
||||
export function TextViewer({ response, pretty, className }: Props) {
|
||||
const [isSearching, toggleIsSearching] = useToggle();
|
||||
const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState<string>('', 400);
|
||||
|
||||
@@ -85,6 +86,7 @@ export function TextViewer({ response, pretty }: Props) {
|
||||
return (
|
||||
<Editor
|
||||
readOnly
|
||||
className={className}
|
||||
forceUpdateKey={body}
|
||||
defaultValue={body}
|
||||
contentType={contentType}
|
||||
|
||||
18
src-web/components/responseViewers/VideoViewer.tsx
Normal file
18
src-web/components/responseViewers/VideoViewer.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import React from 'react';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
}
|
||||
|
||||
export function VideoViewer({ response }: Props) {
|
||||
if (response.bodyPath === null) {
|
||||
return <div>Empty response body</div>;
|
||||
}
|
||||
|
||||
const src = convertFileSrc(response.bodyPath);
|
||||
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
return <video className="w-full" controls src={src}></video>;
|
||||
}
|
||||
@@ -16,5 +16,5 @@ export function useAppInfo() {
|
||||
const metadata = await invoke('cmd_metadata');
|
||||
return metadata as AppInfo;
|
||||
},
|
||||
});
|
||||
}).data;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export function useCommandPalette() {
|
||||
const appInfo = useAppInfo();
|
||||
useHotKey('command_palette.toggle', () => {
|
||||
// Disabled in production for now
|
||||
if (!appInfo.data?.isDev) {
|
||||
if (!appInfo?.isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Appearance } from '../lib/theme/window';
|
||||
import {
|
||||
setAppearanceOnDocument,
|
||||
getPreferredAppearance,
|
||||
subscribeToPreferredAppearanceChange,
|
||||
} from '../lib/theme/window';
|
||||
import { getPreferredAppearance, subscribeToPreferredAppearanceChange } from '../lib/theme/window';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
export function useSyncAppearance() {
|
||||
export function useResolvedAppearance() {
|
||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
|
||||
getPreferredAppearance(),
|
||||
);
|
||||
|
||||
const settings = useSettings();
|
||||
|
||||
// Set appearance when preferred theme changes
|
||||
useEffect(() => {
|
||||
return subscribeToPreferredAppearanceChange(setPreferredAppearance);
|
||||
}, []);
|
||||
|
||||
const settings = useSettings();
|
||||
const appearance =
|
||||
settings == null || settings?.appearance === 'system'
|
||||
? preferredAppearance
|
||||
: settings.appearance;
|
||||
|
||||
useEffect(() => {
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||
}, [appearance, settings]);
|
||||
|
||||
return { appearance };
|
||||
return appearance;
|
||||
}
|
||||
20
src-web/hooks/useResolvedTheme.ts
Normal file
20
src-web/hooks/useResolvedTheme.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { isThemeDark } from '../lib/theme/window';
|
||||
import { useResolvedAppearance } from './useResolvedAppearance';
|
||||
import { useSettings } from './useSettings';
|
||||
import { useThemes } from './useThemes';
|
||||
|
||||
export function useResolvedTheme() {
|
||||
const appearance = useResolvedAppearance();
|
||||
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 };
|
||||
}
|
||||
23
src-web/hooks/useSyncThemeToDocument.ts
Normal file
23
src-web/hooks/useSyncThemeToDocument.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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.background == null) return;
|
||||
emit('yaak_bg_changed', t.background.hex()).catch(console.error);
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
// import { useEffect } from 'react';
|
||||
// import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
// import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
// import { useActiveRequest } from './useActiveRequest';
|
||||
// import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useEffect } from 'react';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useActiveRequest } from './useActiveRequest';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
|
||||
export function useSyncWindowTitle() {
|
||||
// const activeRequest = useActiveRequest();
|
||||
// const activeWorkspace = useActiveWorkspace();
|
||||
// const activeEnvironment = useActiveEnvironment();
|
||||
// useEffect(() => {
|
||||
// let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
|
||||
// if (activeEnvironment) {
|
||||
// newTitle += ` [${activeEnvironment.name}]`;
|
||||
// }
|
||||
// if (activeRequest) {
|
||||
// newTitle += ` – ${fallbackRequestName(activeRequest)}`;
|
||||
// }
|
||||
//
|
||||
// // TODO: This resets the stoplight position so we can't use it yet
|
||||
// // getCurrent().setTitle(newTitle).catch(console.error);
|
||||
// }, [activeEnvironment, activeRequest, activeWorkspace]);
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
useEffect(() => {
|
||||
let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
|
||||
if (activeEnvironment) {
|
||||
newTitle += ` [${activeEnvironment.name}]`;
|
||||
}
|
||||
if (activeRequest) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
newTitle += ` – ${fallbackRequestName(activeRequest)}`;
|
||||
}
|
||||
|
||||
// TODO: This resets the stoplight position so we can't use it yet
|
||||
// invoke('cmd_set_title', { title: newTitle }).catch(console.error);
|
||||
}, [activeEnvironment, activeRequest, activeWorkspace]);
|
||||
}
|
||||
|
||||
13
src-web/hooks/useThemes.ts
Normal file
13
src-web/hooks/useThemes.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { yaakDark, yaakLight, yaakThemes } from '../lib/theme/themes';
|
||||
|
||||
export function useThemes() {
|
||||
const dark = yaakDark;
|
||||
const light = yaakLight;
|
||||
|
||||
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 } };
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export function fallbackRequestName(r: HttpRequest | GrpcRequest | null): string
|
||||
try {
|
||||
const url = new URL(fixedUrl);
|
||||
const pathname = url.pathname === '/' ? '' : url.pathname;
|
||||
console.log('hello', fixedUrl);
|
||||
return `${url.host}${pathname}`;
|
||||
} catch (_) {
|
||||
// Nothing
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface Settings extends BaseModel {
|
||||
readonly model: 'settings';
|
||||
theme: string;
|
||||
appearance: string;
|
||||
themeLight: string;
|
||||
themeDark: string;
|
||||
updateChannel: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { getCurrent } from '@tauri-apps/api/webviewWindow';
|
||||
import { getKeyValue, setKeyValue } from './keyValueStore';
|
||||
|
||||
const key = ['window_pathname', getCurrent().label];
|
||||
const namespace = 'no_sync';
|
||||
const fallback = undefined;
|
||||
|
||||
export async function setPathname(value: string) {
|
||||
await setKeyValue<string | undefined>({ key, namespace, value });
|
||||
}
|
||||
|
||||
export async function maybeRestorePathname() {
|
||||
if (window.location.pathname !== '/') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathname = await getKeyValue<string | undefined>({ key, namespace, fallback });
|
||||
if (pathname != null) {
|
||||
window.location.replace(pathname);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,15 @@ export class Color {
|
||||
}
|
||||
|
||||
static transparent(): Color {
|
||||
return new Color('rgba(0, 0, 0, 0.1)', 'light');
|
||||
return new Color('rgb(0,0,0)', 'light').translucify(1);
|
||||
}
|
||||
|
||||
static white(): Color {
|
||||
return new Color('rgb(0,0,0)', 'light').lower(1);
|
||||
}
|
||||
|
||||
static black(): Color {
|
||||
return new Color('rgb(0,0,0)', 'light').lift(1);
|
||||
}
|
||||
|
||||
private clone(): Color {
|
||||
@@ -80,6 +88,10 @@ export class Color {
|
||||
return `hsla(${h}, ${s}%, ${l}%, ${a})`;
|
||||
}
|
||||
|
||||
hex(): string {
|
||||
return parseColor(this.css()).hex;
|
||||
}
|
||||
|
||||
private _lighten(mod: number): Color {
|
||||
const c = this.clone();
|
||||
c.lightness = this.lightness + (100 - this.lightness) * mod;
|
||||
|
||||
376
src-web/lib/theme/themes.ts
Normal file
376
src-web/lib/theme/themes.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { Color } from './color';
|
||||
import type { YaakTheme } from './window';
|
||||
|
||||
export const yaakLight: YaakTheme = {
|
||||
id: 'yaak-light',
|
||||
name: 'Yaak',
|
||||
background: new Color('#f2f4f7', 'light').lower(1),
|
||||
foreground: new Color('hsl(219,23%,15%)', 'light'),
|
||||
colors: {
|
||||
primary: new Color('hsl(266,100%,70%)', 'light'),
|
||||
secondary: new Color('hsl(220,24%,59%)', 'light'),
|
||||
info: new Color('hsl(206,100%,48%)', 'light'),
|
||||
success: new Color('hsl(155,95%,33%)', 'light'),
|
||||
notice: new Color('hsl(45,100%,41%)', 'light'),
|
||||
warning: new Color('hsl(30,100%,43%)', 'light'),
|
||||
danger: new Color('hsl(335,75%,57%)', 'light'),
|
||||
},
|
||||
components: {
|
||||
sidebar: {
|
||||
background: new Color('#f2f4f7', 'light').lower(1),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const yaakDark: YaakTheme = {
|
||||
id: 'yaak-dark',
|
||||
name: 'Yaak',
|
||||
background: new Color('hsl(244,23%,12%)', 'dark'),
|
||||
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.17),
|
||||
backgroundHighlightSecondary: new Color('hsl(244,23%,12%)', 'dark').lift(0.1),
|
||||
foreground: new Color('#bcbad4', 'dark'),
|
||||
|
||||
colors: {
|
||||
primary: new Color('hsl(266,100%,79%)', 'dark'),
|
||||
secondary: new Color('hsl(245,23%,60%)', 'dark'),
|
||||
info: new Color('hsl(206,100%,63%)', 'dark'),
|
||||
success: new Color('hsl(150,99%,44%)', 'dark'),
|
||||
notice: new Color('hsl(48,80%,63%)', 'dark'),
|
||||
warning: new Color('hsl(28,100%,61%)', 'dark'),
|
||||
danger: new Color('hsl(342,90%,68%)', 'dark'),
|
||||
},
|
||||
|
||||
components: {
|
||||
button: {
|
||||
colors: {
|
||||
primary: new Color('hsl(266,100%,79%)', 'dark').lower(0.1),
|
||||
secondary: new Color('hsl(245,23%,60%)', 'dark').lower(0.1),
|
||||
info: new Color('hsl(206,100%,63%)', 'dark').lower(0.1),
|
||||
success: new Color('hsl(150,99%,44%)', 'dark').lower(0.1),
|
||||
notice: new Color('hsl(48,80%,63%)', 'dark').lower(0.1),
|
||||
warning: new Color('hsl(28,100%,61%)', 'dark').lower(0.1),
|
||||
danger: new Color('hsl(342,90%,68%)', 'dark').lower(0.1),
|
||||
},
|
||||
},
|
||||
input: {
|
||||
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.18),
|
||||
},
|
||||
dialog: {
|
||||
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.11),
|
||||
},
|
||||
sidebar: {
|
||||
background: new Color('hsl(243,23%,15%)', 'dark'),
|
||||
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.09),
|
||||
},
|
||||
responsePane: {
|
||||
background: new Color('hsl(243,23%,15%)', 'dark'),
|
||||
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.09),
|
||||
},
|
||||
appHeader: {
|
||||
background: new Color('hsl(244,23%,12%)', 'dark'),
|
||||
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.1),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const monokaiProOctagon: YaakTheme = {
|
||||
id: 'monokai-pro-octagon',
|
||||
name: 'Monokai Pro Octagon',
|
||||
background: new Color('#282a3a', 'dark'),
|
||||
foreground: new Color('#eaf2f1', 'dark'),
|
||||
foregroundSubtle: new Color('#b2b9bd', 'dark'),
|
||||
foregroundSubtler: new Color('#767b81', 'dark'),
|
||||
|
||||
colors: {
|
||||
primary: new Color('#c39ac9', 'dark'),
|
||||
secondary: new Color('#b2b9bd', 'dark'),
|
||||
info: new Color('#9cd1bb', 'dark'),
|
||||
success: new Color('#bad761', 'dark'),
|
||||
notice: new Color('#ffd76d', 'dark'),
|
||||
warning: new Color('#ff9b5e', 'dark'),
|
||||
danger: new Color('#ff657a', 'dark'),
|
||||
},
|
||||
|
||||
components: {
|
||||
appHeader: {
|
||||
background: new Color('#1e1f2b', 'dark'),
|
||||
foreground: new Color('#b2b9bd', 'dark'),
|
||||
foregroundSubtle: new Color('#767b81', 'dark'),
|
||||
foregroundSubtler: new Color('#696d77', 'dark'),
|
||||
},
|
||||
button: {
|
||||
colors: {
|
||||
primary: new Color('#c39ac9', 'dark').lower(0.1).desaturate(0.1),
|
||||
secondary: new Color('#b2b9bd', 'dark').lower(0.1).desaturate(0.1),
|
||||
info: new Color('#9cd1bb', 'dark').lower(0.1).desaturate(0.1),
|
||||
success: new Color('#bad761', 'dark').lower(0.1).desaturate(0.1),
|
||||
notice: new Color('#ffd76d', 'dark').lower(0.1).desaturate(0.1),
|
||||
warning: new Color('#ff9b5e', 'dark').lower(0.1).desaturate(0.1),
|
||||
danger: new Color('#ff657a', 'dark').lower(0.1).desaturate(0.1),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const catppuccinLatte: YaakTheme = {
|
||||
name: 'Catppuccin Latte',
|
||||
id: 'catppuccin-light',
|
||||
background: new Color('#eff1f5', 'light'),
|
||||
foreground: new Color('#4c4f69', 'dark'),
|
||||
foregroundSubtle: new Color('#6c6f85', 'light'),
|
||||
foregroundSubtler: new Color('#8c8fa1', 'light'),
|
||||
colors: {
|
||||
primary: new Color('#8839ef', 'light'),
|
||||
secondary: new Color('#6c6f85', 'light'),
|
||||
info: new Color('#7287fd', 'light'),
|
||||
success: new Color('#179299', 'light'),
|
||||
notice: new Color('#df8e1d', 'light'),
|
||||
warning: new Color('#fe640b', 'light'),
|
||||
danger: new Color('#e64553', 'light'),
|
||||
},
|
||||
components: {
|
||||
sidebar: {
|
||||
background: new Color('#e6e9ef', 'light'),
|
||||
backgroundHighlight: new Color('#e6e9ef', 'light').lift(0.05),
|
||||
foregroundSubtler: new Color('#7287fd', 'light'),
|
||||
},
|
||||
appHeader: {
|
||||
background: new Color('#dce0e8', 'light'),
|
||||
backgroundHighlight: new Color('#e6e9ef', 'light').lift(0.05),
|
||||
foregroundSubtler: new Color('#7287fd', 'light'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const catppuccinMocha: YaakTheme = {
|
||||
name: 'Catppuccin Mocha',
|
||||
id: 'catppuccin-mocha',
|
||||
background: new Color('#181825', 'dark'),
|
||||
foreground: new Color('#cdd6f4', 'dark'),
|
||||
foregroundSubtle: new Color('#a6adc8', 'dark'),
|
||||
foregroundSubtler: new Color('#7f849c', 'dark'),
|
||||
colors: {
|
||||
primary: new Color('#c6a0f6', 'dark'),
|
||||
secondary: new Color('#bac2de', 'dark'),
|
||||
info: new Color('#89b4fa', 'dark'),
|
||||
success: new Color('#a6e3a1', 'dark'),
|
||||
notice: new Color('#f9e2af', 'dark'),
|
||||
warning: new Color('#fab387', 'dark'),
|
||||
danger: new Color('#f38ba8', 'dark'),
|
||||
},
|
||||
components: {
|
||||
dialog: {
|
||||
background: new Color('#181825', 'dark'),
|
||||
},
|
||||
sidebar: {
|
||||
background: new Color('#1e1e2e', 'dark'),
|
||||
backgroundHighlight: new Color('#1e1e2e', 'dark').lift(0.05),
|
||||
},
|
||||
appHeader: {
|
||||
background: new Color('#11111b', 'dark'),
|
||||
backgroundHighlight: new Color('#11111b', 'dark').lift(0.1),
|
||||
},
|
||||
responsePane: {
|
||||
background: new Color('#1e1e2e', 'dark'),
|
||||
backgroundHighlight: new Color('#1e1e2e', 'dark').lift(0.05),
|
||||
},
|
||||
button: {
|
||||
colors: {
|
||||
primary: new Color('#cba6f7', 'dark').lower(0.2).desaturate(0.2),
|
||||
secondary: new Color('#bac2de', 'dark').lower(0.2).desaturate(0.2),
|
||||
info: new Color('#89b4fa', 'dark').lower(0.2).desaturate(0.2),
|
||||
success: new Color('#a6e3a1', 'dark').lower(0.2).desaturate(0.2),
|
||||
notice: new Color('#f9e2af', 'dark').lower(0.2).desaturate(0.2),
|
||||
warning: new Color('#fab387', 'dark').lower(0.2).desaturate(0.2),
|
||||
danger: new Color('#f38ba8', 'dark').lower(0.2).desaturate(0.2),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const relaxing: YaakTheme = {
|
||||
name: 'Relaxing',
|
||||
id: 'relaxing',
|
||||
background: new Color('#2b1e3b', 'dark'),
|
||||
foreground: new Color('#ede2f5', 'dark'),
|
||||
colors: {
|
||||
primary: new Color('#cba6f7', 'dark'),
|
||||
secondary: new Color('#bac2de', 'dark'),
|
||||
info: new Color('#89b4fa', 'dark'),
|
||||
success: new Color('#a6e3a1', 'dark'),
|
||||
notice: new Color('#f9e2af', 'dark'),
|
||||
warning: new Color('#fab387', 'dark'),
|
||||
danger: new Color('#f38ba8', 'dark'),
|
||||
},
|
||||
};
|
||||
|
||||
export const rosePineMoon: YaakTheme = {
|
||||
id: 'rose-pine-moon',
|
||||
name: 'Rosé Pine Moon',
|
||||
background: new Color('#232136', 'dark'),
|
||||
foreground: new Color('#e0def4', 'dark'),
|
||||
foregroundSubtle: new Color('#908caa', 'dark'),
|
||||
foregroundSubtler: new Color('#6e6a86', 'dark'),
|
||||
colors: {
|
||||
primary: new Color('#c4a7e7', 'dark'),
|
||||
secondary: new Color('#908caa', 'dark'),
|
||||
info: new Color('#68aeca', 'dark'),
|
||||
success: new Color('#9ccfd8', 'dark'),
|
||||
notice: new Color('#f6c177', 'dark'),
|
||||
warning: new Color('#ea9a97', 'dark'),
|
||||
danger: new Color('#eb6f92', 'dark'),
|
||||
},
|
||||
components: {
|
||||
responsePane: {
|
||||
background: new Color('#2a273f', 'dark'),
|
||||
},
|
||||
sidebar: {
|
||||
background: new Color('#2a273f', 'dark'),
|
||||
},
|
||||
menu: {
|
||||
background: new Color('#393552', 'dark'),
|
||||
foregroundSubtle: new Color('#908caa', 'dark').lift(0.15),
|
||||
foregroundSubtler: new Color('#6e6a86', 'dark').lift(0.15),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const rosePineDawn: YaakTheme = {
|
||||
id: 'rose-pine-dawn',
|
||||
name: 'Rosé Pine Dawn',
|
||||
background: new Color('#faf4ed', 'light'),
|
||||
backgroundHighlight: new Color('#dfdad9', 'light'),
|
||||
backgroundHighlightSecondary: new Color('#f4ede8', 'light'),
|
||||
foreground: new Color('#575279', 'light'),
|
||||
foregroundSubtle: new Color('#797593', 'light'),
|
||||
foregroundSubtler: new Color('#9893a5', 'light'),
|
||||
colors: {
|
||||
primary: new Color('#9070ad', 'light'),
|
||||
secondary: new Color('#6e6a86', 'light'),
|
||||
info: new Color('#2d728d', 'light'),
|
||||
success: new Color('#4f8c96', 'light'),
|
||||
notice: new Color('#cb862d', 'light'),
|
||||
warning: new Color('#ce7b78', 'light'),
|
||||
danger: new Color('#b4637a', 'light'),
|
||||
},
|
||||
components: {
|
||||
responsePane: {
|
||||
backgroundHighlight: new Color('#e8e4e2', 'light'),
|
||||
},
|
||||
sidebar: {
|
||||
backgroundHighlight: new Color('#e8e4e2', 'light'),
|
||||
},
|
||||
appHeader: {
|
||||
backgroundHighlight: new Color('#e8e4e2', 'light'),
|
||||
},
|
||||
input: {
|
||||
backgroundHighlight: new Color('#dfdad9', 'light'),
|
||||
},
|
||||
dialog: {
|
||||
backgroundHighlight: new Color('#e8e4e2', 'light'),
|
||||
},
|
||||
menu: {
|
||||
background: new Color('#f2e9e1', 'light'),
|
||||
backgroundHighlight: new Color('#dfdad9', 'light'),
|
||||
backgroundHighlightSecondary: new Color('#6e6a86', 'light'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const rosePine: YaakTheme = {
|
||||
id: 'rose-pine',
|
||||
name: 'Rosé Pine',
|
||||
background: new Color('#191724', 'dark'),
|
||||
foreground: new Color('#e0def4', 'dark'),
|
||||
foregroundSubtle: new Color('#908caa', 'dark'),
|
||||
foregroundSubtler: new Color('#6e6a86', 'dark'),
|
||||
colors: {
|
||||
primary: new Color('#c4a7e7', 'dark'),
|
||||
secondary: new Color('#6e6a86', 'dark'),
|
||||
info: new Color('#67abcb', 'dark'),
|
||||
success: new Color('#9cd8d8', 'dark'),
|
||||
notice: new Color('#f6c177', 'dark'),
|
||||
warning: new Color('#f1a3a1', 'dark'),
|
||||
danger: new Color('#eb6f92', 'dark'),
|
||||
},
|
||||
components: {
|
||||
responsePane: {
|
||||
background: new Color('#1f1d2e', 'dark'),
|
||||
},
|
||||
sidebar: {
|
||||
background: new Color('#1f1d2e', 'dark'),
|
||||
},
|
||||
menu: {
|
||||
background: new Color('#393552', 'dark'),
|
||||
foregroundSubtle: new Color('#908caa', 'dark').lift(0.15),
|
||||
foregroundSubtler: new Color('#6e6a86', 'dark').lift(0.15),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const githubDark: YaakTheme = {
|
||||
id: 'github-dark',
|
||||
name: 'GitHub',
|
||||
background: new Color('#0d1218', 'dark'),
|
||||
backgroundHighlight: new Color('#171c23', 'dark'),
|
||||
backgroundHighlightSecondary: new Color('#1c2127', 'dark'),
|
||||
foreground: new Color('#dce3eb', 'dark'),
|
||||
foregroundSubtle: new Color('#88919b', 'dark'),
|
||||
foregroundSubtler: new Color('#6b727d', 'dark'),
|
||||
colors: {
|
||||
primary: new Color('#a579ef', 'dark').lift(0.1),
|
||||
secondary: new Color('#6b727d', 'dark').lift(0.1),
|
||||
info: new Color('#458def', 'dark').lift(0.1),
|
||||
success: new Color('#3eb24f', 'dark').lift(0.1),
|
||||
notice: new Color('#dca132', 'dark').lift(0.1),
|
||||
warning: new Color('#ec7934', 'dark').lift(0.1),
|
||||
danger: new Color('#ee5049', 'dark').lift(0.1),
|
||||
},
|
||||
components: {
|
||||
button: {
|
||||
colors: {
|
||||
primary: new Color('#a579ef', 'dark'),
|
||||
secondary: new Color('#6b727d', 'dark'),
|
||||
info: new Color('#458def', 'dark'),
|
||||
success: new Color('#3eb24f', 'dark'),
|
||||
notice: new Color('#dca132', 'dark'),
|
||||
warning: new Color('#ec7934', 'dark'),
|
||||
danger: new Color('#ee5049', 'dark'),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const githubLight: YaakTheme = {
|
||||
id: 'github-light',
|
||||
name: 'GitHub',
|
||||
background: new Color('#ffffff', 'light'),
|
||||
backgroundHighlight: new Color('#e8ebee', 'light'),
|
||||
backgroundHighlightSecondary: new Color('#f6f8fa', 'light'),
|
||||
foreground: new Color('#1f2328', 'light'),
|
||||
foregroundSubtle: new Color('#636c76', 'light'),
|
||||
foregroundSubtler: new Color('#828d94', 'light'),
|
||||
colors: {
|
||||
primary: new Color('#8250df', 'light'),
|
||||
secondary: new Color('#6e7781', 'light'),
|
||||
info: new Color('#0969da', 'light'),
|
||||
success: new Color('#1a7f37', 'light'),
|
||||
notice: new Color('#9a6700', 'light'),
|
||||
warning: new Color('#bc4c00', 'light'),
|
||||
danger: new Color('#d1242f', 'light'),
|
||||
},
|
||||
};
|
||||
|
||||
export const yaakThemes = [
|
||||
yaakLight,
|
||||
yaakDark,
|
||||
catppuccinMocha,
|
||||
catppuccinLatte,
|
||||
relaxing,
|
||||
monokaiProOctagon,
|
||||
rosePine,
|
||||
rosePineMoon,
|
||||
rosePineDawn,
|
||||
githubLight,
|
||||
githubDark,
|
||||
];
|
||||
@@ -1,10 +1,9 @@
|
||||
import { getCurrent } from '@tauri-apps/api/webviewWindow';
|
||||
import { indent } from '../indent';
|
||||
import { Color } from './color';
|
||||
|
||||
export type Appearance = 'dark' | 'light' | 'system';
|
||||
|
||||
const DEFAULT_APPEARANCE: Appearance = 'system';
|
||||
|
||||
interface ThemeComponent {
|
||||
background?: Color;
|
||||
backgroundHighlight?: Color;
|
||||
@@ -13,19 +12,26 @@ interface ThemeComponent {
|
||||
foreground?: Color;
|
||||
foregroundSubtle?: Color;
|
||||
foregroundSubtler?: Color;
|
||||
shadow?: Color;
|
||||
colors?: Partial<RootColors>;
|
||||
}
|
||||
|
||||
interface YaakTheme extends ThemeComponent {
|
||||
export interface YaakTheme extends ThemeComponent {
|
||||
id: string;
|
||||
name: string;
|
||||
components?: {
|
||||
dialog?: ThemeComponent;
|
||||
menu?: ThemeComponent;
|
||||
toast?: ThemeComponent;
|
||||
sidebar?: ThemeComponent;
|
||||
responsePane?: ThemeComponent;
|
||||
appHeader?: ThemeComponent;
|
||||
button?: ThemeComponent;
|
||||
banner?: ThemeComponent;
|
||||
placeholder?: ThemeComponent;
|
||||
urlBar?: ThemeComponent;
|
||||
editor?: ThemeComponent;
|
||||
input?: ThemeComponent;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,94 +48,6 @@ interface RootColors {
|
||||
type ColorName = keyof RootColors;
|
||||
type ComponentName = keyof NonNullable<YaakTheme['components']>;
|
||||
|
||||
const yaakThemes: Record<string, YaakTheme> = {
|
||||
yaakLight: {
|
||||
name: 'Yaak (Light)',
|
||||
background: new Color('#f2f4f7', 'light').lower(1),
|
||||
foreground: new Color('hsl(219,23%,15%)', 'light'),
|
||||
colors: {
|
||||
primary: new Color('hsl(266,100%,70%)', 'light'),
|
||||
secondary: new Color('hsl(220,24%,59%)', 'light'),
|
||||
info: new Color('hsl(206,100%,48%)', 'light'),
|
||||
success: new Color('hsl(155,95%,33%)', 'light'),
|
||||
notice: new Color('hsl(45,100%,41%)', 'light'),
|
||||
warning: new Color('hsl(30,100%,43%)', 'light'),
|
||||
danger: new Color('hsl(335,75%,57%)', 'light'),
|
||||
},
|
||||
components: {
|
||||
sidebar: {
|
||||
background: new Color('#f2f4f7', 'light'),
|
||||
},
|
||||
},
|
||||
} as YaakTheme,
|
||||
|
||||
yaakDark: {
|
||||
name: 'Yaak Dark',
|
||||
background: new Color('hsl(244,23%,12%)', 'dark'),
|
||||
foreground: new Color('#bcbad4', 'dark'),
|
||||
|
||||
colors: {
|
||||
primary: new Color('hsl(266,100%,79%)', 'dark'),
|
||||
secondary: new Color('hsl(245,23%,60%)', 'dark'),
|
||||
info: new Color('hsl(206,100%,63%)', 'dark'),
|
||||
success: new Color('hsl(150,100%,37%)', 'dark'),
|
||||
notice: new Color('hsl(48,80%,63%)', 'dark'),
|
||||
warning: new Color('hsl(28,100%,61%)', 'dark'),
|
||||
danger: new Color('hsl(342,90%,68%)', 'dark'),
|
||||
},
|
||||
|
||||
components: {
|
||||
sidebar: {
|
||||
background: new Color('hsl(243,23%,15%)', 'dark'),
|
||||
},
|
||||
responsePane: {
|
||||
background: new Color('hsl(243,23%,15%)', 'dark'),
|
||||
},
|
||||
},
|
||||
},
|
||||
catppuccin: {
|
||||
name: 'Catppuccin',
|
||||
background: new Color('#181825', 'dark'),
|
||||
foreground: new Color('#cdd6f4', 'dark'),
|
||||
foregroundSubtle: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.3),
|
||||
foregroundSubtler: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.55),
|
||||
colors: {
|
||||
primary: new Color('#cba6f7', 'dark'),
|
||||
secondary: new Color('#bac2de', 'dark'),
|
||||
info: new Color('#89b4fa', 'dark'),
|
||||
success: new Color('#a6e3a1', 'dark'),
|
||||
notice: new Color('#f9e2af', 'dark'),
|
||||
warning: new Color('#fab387', 'dark'),
|
||||
danger: new Color('#f38ba8', 'dark'),
|
||||
},
|
||||
components: {
|
||||
dialog: {
|
||||
background: new Color('#181825', 'dark'),
|
||||
},
|
||||
sidebar: {
|
||||
background: new Color('#1e1e2e', 'dark'),
|
||||
},
|
||||
appHeader: {
|
||||
background: new Color('#11111b', 'dark'),
|
||||
},
|
||||
responsePane: {
|
||||
background: new Color('#1e1e2e', 'dark'),
|
||||
},
|
||||
button: {
|
||||
colors: {
|
||||
primary: new Color('#cba6f7', 'dark').lower(0.2),
|
||||
secondary: new Color('#bac2de', 'dark').lower(0.2),
|
||||
info: new Color('#89b4fa', 'dark').lower(0.2),
|
||||
success: new Color('#a6e3a1', 'dark').lower(0.2),
|
||||
notice: new Color('#f9e2af', 'dark').lower(0.2),
|
||||
warning: new Color('#fab387', 'dark').lower(0.2),
|
||||
danger: new Color('#f38ba8', 'dark').lower(0.2),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type CSSVariables = Record<string, string | undefined>;
|
||||
|
||||
function themeVariables(theme?: ThemeComponent, base?: CSSVariables): CSSVariables | null {
|
||||
@@ -147,6 +65,7 @@ function themeVariables(theme?: ThemeComponent, base?: CSSVariables): CSSVariabl
|
||||
'--fg-subtle': theme?.foregroundSubtle?.css() ?? theme?.foreground?.lower(0.2).css(),
|
||||
'--fg-subtler': theme?.foregroundSubtler?.css() ?? theme?.foreground?.lower(0.3).css(),
|
||||
'--border-focus': theme?.colors?.info?.css(),
|
||||
'--shadow': theme?.shadow?.css() ?? Color.black().translucify(0.7).css(),
|
||||
};
|
||||
|
||||
for (const [color, value] of Object.entries(theme?.colors ?? {})) {
|
||||
@@ -261,7 +180,7 @@ function placeholderCSS(color: ColorName, colors?: Partial<RootColors>): string
|
||||
].join('\n\n');
|
||||
}
|
||||
|
||||
function isThemeDark(theme: YaakTheme): boolean {
|
||||
export function isThemeDark(theme: YaakTheme): boolean {
|
||||
if (theme.background && theme.foreground) {
|
||||
return theme.foreground.lighterThan(theme.background);
|
||||
}
|
||||
@@ -269,10 +188,11 @@ function isThemeDark(theme: YaakTheme): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
setThemeOnDocument(yaakThemes.yaakLight!);
|
||||
setThemeOnDocument(yaakThemes.yaakDark!);
|
||||
|
||||
export function getThemeCSS(theme: YaakTheme): string {
|
||||
theme.components = theme.components ?? {};
|
||||
// Toast defaults to menu styles
|
||||
theme.components.toast = theme.components.toast ?? theme.components.menu;
|
||||
|
||||
let themeCSS = '';
|
||||
try {
|
||||
const baseCss = variablesToCSS(null, themeVariables(theme));
|
||||
@@ -298,29 +218,24 @@ export function getThemeCSS(theme: YaakTheme): string {
|
||||
return themeCSS;
|
||||
}
|
||||
|
||||
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
|
||||
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
|
||||
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
|
||||
}
|
||||
|
||||
export function setThemeOnDocument(theme: YaakTheme) {
|
||||
document.documentElement.setAttribute('data-theme', theme.name);
|
||||
|
||||
const darkOrLight = isThemeDark(theme) ? 'dark' : 'light';
|
||||
let existingStyleEl = document.head.querySelector(`style[data-theme-definition=${darkOrLight}]`);
|
||||
if (!existingStyleEl) {
|
||||
const styleEl = document.createElement('style');
|
||||
export function addThemeStylesToDocument(theme: YaakTheme) {
|
||||
let styleEl = document.head.querySelector(`style[data-theme]`);
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style');
|
||||
document.head.appendChild(styleEl);
|
||||
existingStyleEl = styleEl;
|
||||
}
|
||||
|
||||
existingStyleEl.textContent = [
|
||||
styleEl.setAttribute('data-theme', theme.id);
|
||||
styleEl.textContent = [
|
||||
`/* ${theme.name} */`,
|
||||
`[data-resolved-appearance="${isThemeDark(theme) ? 'dark' : 'light'}"] {`,
|
||||
`[data-theme="${theme.id}"] {`,
|
||||
getThemeCSS(theme),
|
||||
'}',
|
||||
].join('\n');
|
||||
existingStyleEl.setAttribute('data-theme-definition', darkOrLight);
|
||||
}
|
||||
|
||||
export function setThemeOnDocument(theme: YaakTheme) {
|
||||
document.documentElement.setAttribute('data-theme', theme.id);
|
||||
}
|
||||
|
||||
export function getPreferredAppearance(): Appearance {
|
||||
@@ -330,8 +245,13 @@ export function getPreferredAppearance(): Appearance {
|
||||
export function subscribeToPreferredAppearanceChange(
|
||||
cb: (appearance: Appearance) => void,
|
||||
): () => void {
|
||||
const listener = (e: MediaQueryListEvent) => cb(e.matches ? 'dark' : 'light');
|
||||
const m = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
m.addEventListener('change', listener);
|
||||
return () => m.removeEventListener('change', listener);
|
||||
const container = { unsubscribe: () => {} };
|
||||
|
||||
getCurrent()
|
||||
.onThemeChanged((t) => cb(t.payload))
|
||||
.then((l) => {
|
||||
container.unsubscribe = l;
|
||||
});
|
||||
|
||||
return () => container.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ import { getCurrent } from '@tauri-apps/api/webviewWindow';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { attachConsole } from 'tauri-plugin-log-api';
|
||||
import { App } from './components/App';
|
||||
import './main.css';
|
||||
import { getSettings } from './lib/store';
|
||||
import type { Appearance } from './lib/theme/window';
|
||||
import { setAppearanceOnDocument } from './lib/theme/window';
|
||||
|
||||
// Hide decorations here because it doesn't work in Rust for some reason (bug?)
|
||||
const osType = await type();
|
||||
@@ -14,11 +12,7 @@ if (osType !== 'macos') {
|
||||
await getCurrent().setDecorations(false);
|
||||
}
|
||||
|
||||
// await attachConsole();
|
||||
// await maybeRestorePathname();
|
||||
|
||||
const settings = await getSettings();
|
||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||
await attachConsole();
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// Hack to not go back in history on backspace. Check for document body
|
||||
|
||||
@@ -59,6 +59,10 @@ module.exports = {
|
||||
'4xl': '2.5rem',
|
||||
'5xl': '3rem',
|
||||
},
|
||||
boxShadow: {
|
||||
DEFAULT: '0 1px 3px 0 var(--shadow);',
|
||||
lg: '0 10px 15px -3px var(--shadow)',
|
||||
},
|
||||
colors: {
|
||||
'transparent': 'transparent',
|
||||
'placeholder': 'var(--fg-subtler)',
|
||||
@@ -88,7 +92,7 @@ module.exports = {
|
||||
require('@tailwindcss/container-queries'),
|
||||
plugin(function ({addVariant}) {
|
||||
addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus']);
|
||||
addVariant('focus-visible-or-class', ['&:focus-visible']);
|
||||
addVariant('focus-visible-or-class', ['&:focus-visible', '&.focus:focus']);
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user