mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-18 15:06:58 +01:00
Compare commits
19 Commits
v2025.4.0-
...
v2025.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aadfbfdfca | ||
|
|
383fd05c6c | ||
|
|
be0a8fc27a | ||
|
|
648a1ac53c | ||
|
|
9fab37fb17 | ||
|
|
e0aaa33ccb | ||
|
|
20f7d20031 | ||
|
|
4d90bc78b1 | ||
|
|
97763a1301 | ||
|
|
d8b5a201b6 | ||
|
|
88e87a1999 | ||
|
|
2c4c1abd20 | ||
|
|
67026fc5b3 | ||
|
|
423a1a0a52 | ||
|
|
1abe01aa5a | ||
|
|
d0fde99b1c | ||
|
|
27901231dc | ||
|
|
1d9d80319b | ||
|
|
f62e90297d |
@@ -50,11 +50,9 @@ New migrations can be created from the `src-tauri/` directory:
|
||||
npm run migration
|
||||
```
|
||||
|
||||
Run the app to apply the migrations.
|
||||
Rerun the app to apply the migrations.
|
||||
|
||||
If nothing happens, try `cargo clean` and run the app again.
|
||||
|
||||
_Note: Development builds use a separate database location from production builds._
|
||||
_Note: For safety, development builds use a separate database location from production builds._
|
||||
|
||||
## Lezer Grammer Generation
|
||||
|
||||
|
||||
72
package-lock.json
generated
72
package-lock.json
generated
@@ -36,6 +36,7 @@
|
||||
"plugins/template-function-xml",
|
||||
"src-tauri/yaak-crypto",
|
||||
"src-tauri/yaak-git",
|
||||
"src-tauri/yaak-fonts",
|
||||
"src-tauri/yaak-license",
|
||||
"src-tauri/yaak-mac-window",
|
||||
"src-tauri/yaak-models",
|
||||
@@ -50,7 +51,7 @@
|
||||
"jotai": "^2.12.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.4.1",
|
||||
"@tauri-apps/cli": "2.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
||||
"@typescript-eslint/parser": "^8.27.0",
|
||||
"@yaakapp/cli": "^0.1.5",
|
||||
@@ -63,7 +64,7 @@
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"workspaces-run": "^1.0.2"
|
||||
}
|
||||
},
|
||||
@@ -2801,9 +2802,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
|
||||
"integrity": "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz",
|
||||
"integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -3037,36 +3038,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.1.tgz",
|
||||
"integrity": "sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.2.tgz",
|
||||
"integrity": "sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-fs": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.1.tgz",
|
||||
"integrity": "sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.3.0.tgz",
|
||||
"integrity": "sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-log": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.3.1.tgz",
|
||||
"integrity": "sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.4.0.tgz",
|
||||
"integrity": "sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz",
|
||||
"integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==",
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.7.tgz",
|
||||
"integrity": "sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
@@ -3606,6 +3607,10 @@
|
||||
"resolved": "src-tauri/yaak-crypto",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/fonts": {
|
||||
"resolved": "src-tauri/yaak-fonts",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/git": {
|
||||
"resolved": "src-tauri/yaak-git",
|
||||
"link": true
|
||||
@@ -12946,6 +12951,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
|
||||
@@ -16097,9 +16112,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -17284,7 +17299,7 @@
|
||||
},
|
||||
"packages/plugin-runtime-types": {
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.4",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.5.4"
|
||||
},
|
||||
@@ -17533,6 +17548,10 @@
|
||||
"name": "@yaakapp-internal/crypto",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"src-tauri/yaak-fonts": {
|
||||
"name": "@yaakapp-internal/fonts",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"src-tauri/yaak-git": {
|
||||
"name": "@yaakapp-internal/git",
|
||||
"version": "1.0.0"
|
||||
@@ -17598,12 +17617,12 @@
|
||||
"@tanstack/react-query": "^5.76.1",
|
||||
"@tanstack/react-router": "^1.120.3",
|
||||
"@tanstack/react-virtual": "^3.13.8",
|
||||
"@tauri-apps/api": "^2.4.1",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-fs": "^2.2.1",
|
||||
"@tauri-apps/plugin-log": "^2.2.1",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-log": "^2.4.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -17626,6 +17645,7 @@
|
||||
"papaparse": "^5.4.1",
|
||||
"parse-color": "^1.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"plugins/template-function-xml",
|
||||
"src-tauri/yaak-crypto",
|
||||
"src-tauri/yaak-git",
|
||||
"src-tauri/yaak-fonts",
|
||||
"src-tauri/yaak-license",
|
||||
"src-tauri/yaak-mac-window",
|
||||
"src-tauri/yaak-models",
|
||||
@@ -66,7 +67,7 @@
|
||||
"jotai": "^2.12.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.4.1",
|
||||
"@tauri-apps/cli": "2.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.27.0",
|
||||
"@typescript-eslint/parser": "^8.27.0",
|
||||
"@yaakapp/cli": "^0.1.5",
|
||||
@@ -79,7 +80,7 @@
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"workspaces-run": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/plugin-runtime-types/README.md
Normal file
28
packages/plugin-runtime-types/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Yaak Plugin API
|
||||
|
||||
Yaak is a desktop [API client](https://yaak.app/blog/yet-another-api-client) for
|
||||
interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC APIs. It's
|
||||
built using Tauri, Rust, and ReactJS.
|
||||
|
||||
Plugins can be created in TypeScript, which are executed alongside Yaak in a NodeJS
|
||||
runtime. This package contains the TypeScript type definitions required to make building
|
||||
Yaak plugins a breeze.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The easiest way to get started is by generating a plugin with the Yaak CLI:
|
||||
|
||||
```shell
|
||||
npx @yaakapp/cli generate
|
||||
```
|
||||
|
||||
For more details on creating plugins, check out
|
||||
the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
|
||||
|
||||
## Installation
|
||||
|
||||
If you prefer starting from scratch, manually install the types package:
|
||||
|
||||
```shell
|
||||
npm install @yaakapp/api
|
||||
```
|
||||
@@ -1,6 +1,20 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.4",
|
||||
"keywords": [
|
||||
"api-client",
|
||||
"insomnia-alternative",
|
||||
"bruno-alternative",
|
||||
"postman-alternative"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://feedback.yaak.app"
|
||||
},
|
||||
"homepage": "https://yaak.app",
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Environment } from "./gen_models.js";
|
||||
import type { Folder } from "./gen_models.js";
|
||||
import type { GrpcRequest } from "./gen_models.js";
|
||||
import type { HttpRequest } from "./gen_models.js";
|
||||
import type { HttpResponse } from "./gen_models.js";
|
||||
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
|
||||
import type { JsonValue } from "./serde_json/JsonValue.js";
|
||||
import type { WebsocketRequest } from "./gen_models.js";
|
||||
import type { Workspace } from "./gen_models.js";
|
||||
|
||||
export type BootRequest = { dir: string, watch: boolean, };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ async function createMigration() {
|
||||
|
||||
const timestamp = generateTimestamp();
|
||||
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
|
||||
const migrationsDir = path.join(__dirname, '../src-tauri/migrations');
|
||||
const migrationsDir = path.join(__dirname, '../src-tauri/yaak-models/migrations');
|
||||
const filePath = path.join(migrationsDir, fileName);
|
||||
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
|
||||
2760
src-tauri/Cargo.lock
generated
2760
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ members = [
|
||||
"yaak-crypto",
|
||||
"yaak-git",
|
||||
"yaak-grpc",
|
||||
"yaak-fonts",
|
||||
"yaak-http",
|
||||
"yaak-license",
|
||||
"yaak-mac-window",
|
||||
@@ -33,7 +34,7 @@ strip = true # Automatically strip symbols from the binary.
|
||||
cargo-clippy = []
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.1.1", features = [] }
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
|
||||
@@ -48,7 +49,7 @@ log = "0.4.27"
|
||||
md5 = "0.7.0"
|
||||
mime_guess = "2.0.5"
|
||||
rand = "0.9.0"
|
||||
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
|
||||
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks"] }
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
@@ -75,6 +76,7 @@ yaak-http = { workspace = true }
|
||||
yaak-license = { path = "yaak-license" }
|
||||
yaak-mac-window = { path = "yaak-mac-window" }
|
||||
yaak-models = { workspace = true }
|
||||
yaak-fonts = { workspace = true }
|
||||
yaak-plugins = { workspace = true }
|
||||
yaak-sse = { workspace = true }
|
||||
yaak-sync = { workspace = true }
|
||||
@@ -82,19 +84,20 @@ yaak-templates = { workspace = true }
|
||||
yaak-ws = { path = "yaak-ws" }
|
||||
|
||||
[workspace.dependencies]
|
||||
reqwest = "0.12.15"
|
||||
reqwest = "0.12.19"
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
tauri = "2.4.1"
|
||||
tauri-plugin = "2.1.1"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri = "2.5.1"
|
||||
tauri-plugin = "2.2.0"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tokio = "1.44.2"
|
||||
tokio = "1.45.1"
|
||||
thiserror = "2.0.12"
|
||||
ts-rs = "10.1.0"
|
||||
rustls = { version = "0.23.25", default-features = false }
|
||||
rustls-platform-verifier = "0.5.1"
|
||||
ts-rs = "11.0.1"
|
||||
rustls = { version = "0.23.27", default-features = false }
|
||||
rustls-platform-verifier = "0.6.0"
|
||||
yaak-common = { path = "yaak-common" }
|
||||
yaak-fonts = { path = "yaak-fonts" }
|
||||
yaak-http = { path = "yaak-http" }
|
||||
yaak-models = { path = "yaak-models" }
|
||||
yaak-plugins = { path = "yaak-plugins" }
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"shell:allow-open",
|
||||
"yaak-crypto:default",
|
||||
"yaak-git:default",
|
||||
"yaak-fonts:default",
|
||||
"yaak-license:default",
|
||||
"yaak-mac-window:default",
|
||||
"yaak-models:default",
|
||||
|
||||
@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
use log::{debug, error, warn};
|
||||
use mime_guess::Mime;
|
||||
use reqwest::redirect::Policy;
|
||||
use reqwest::{Method, Response};
|
||||
use reqwest::{Method, NoProxy, Response};
|
||||
use reqwest::{Proxy, Url, multipart};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -120,25 +120,39 @@ pub async fn send_http_request<R: Runtime>(
|
||||
https,
|
||||
auth,
|
||||
disabled,
|
||||
bypass,
|
||||
}) if !disabled => {
|
||||
debug!("Using proxy http={http} https={https}");
|
||||
let mut proxy = Proxy::custom(move |url| {
|
||||
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
|
||||
let https = if https.is_empty() { None } else { Some(https.to_owned()) };
|
||||
let proxy_url = match (url.scheme(), http, https) {
|
||||
("http", Some(proxy_url), _) => Some(proxy_url),
|
||||
("https", _, Some(proxy_url)) => Some(proxy_url),
|
||||
_ => None,
|
||||
debug!("Using proxy http={http} https={https} bypass={bypass}");
|
||||
if !http.is_empty() {
|
||||
match Proxy::http(http) {
|
||||
Ok(mut proxy) => {
|
||||
if let Some(ProxySettingAuth { user, password }) = auth.clone() {
|
||||
debug!("Using http proxy auth");
|
||||
proxy = proxy.basic_auth(user.as_str(), password.as_str());
|
||||
}
|
||||
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
|
||||
client_builder = client_builder.proxy(proxy);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to apply http proxy {e:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
if !https.is_empty() {
|
||||
match Proxy::https(https) {
|
||||
Ok(mut proxy) => {
|
||||
if let Some(ProxySettingAuth { user, password }) = auth {
|
||||
debug!("Using https proxy auth");
|
||||
proxy = proxy.basic_auth(user.as_str(), password.as_str());
|
||||
}
|
||||
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
|
||||
client_builder = client_builder.proxy(proxy);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to apply https proxy {e:?}");
|
||||
}
|
||||
};
|
||||
proxy_url
|
||||
});
|
||||
|
||||
if let Some(ProxySettingAuth { user, password }) = auth {
|
||||
debug!("Using proxy auth");
|
||||
proxy = proxy.basic_auth(user.as_str(), password.as_str());
|
||||
}
|
||||
|
||||
client_builder = client_builder.proxy(proxy);
|
||||
}
|
||||
_ => {} // Nothing to do for this one, as it is the default
|
||||
}
|
||||
|
||||
@@ -1265,6 +1265,7 @@ pub fn run() {
|
||||
.plugin(yaak_models::init())
|
||||
.plugin(yaak_plugins::init())
|
||||
.plugin(yaak_crypto::init())
|
||||
.plugin(yaak_fonts::init())
|
||||
.plugin(yaak_git::init())
|
||||
.plugin(yaak_ws::init())
|
||||
.plugin(yaak_sync::init());
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
],
|
||||
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
|
||||
"resources": [
|
||||
"migrations",
|
||||
"vendored/protoc/include",
|
||||
"vendored/plugins",
|
||||
"vendored/plugin-runtime"
|
||||
|
||||
16
src-tauri/yaak-fonts/Cargo.toml
Normal file
16
src-tauri/yaak-fonts/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "yaak-fonts"
|
||||
links = "yaak-fonts"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
font-loader = "0.11.0"
|
||||
tauri = { workspace = true }
|
||||
ts-rs = { workspace = true }
|
||||
serde = "1.0"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { workspace = true, features = ["build"] }
|
||||
3
src-tauri/yaak-fonts/bindings/gen_fonts.ts
Normal file
3
src-tauri/yaak-fonts/bindings/gen_fonts.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Fonts = { editorFonts: Array<string>, uiFonts: Array<string>, };
|
||||
5
src-tauri/yaak-fonts/build.rs
Normal file
5
src-tauri/yaak-fonts/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
const COMMANDS: &[&str] = &["list"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS).build();
|
||||
}
|
||||
14
src-tauri/yaak-fonts/index.ts
Normal file
14
src-tauri/yaak-fonts/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Fonts } from './bindings/gen_fonts';
|
||||
|
||||
export async function listFonts() {
|
||||
return invoke<Fonts>('plugin:yaak-fonts|list', {});
|
||||
}
|
||||
|
||||
export function useFonts() {
|
||||
return useQuery({
|
||||
queryKey: ['list_fonts'],
|
||||
queryFn: () => listFonts(),
|
||||
});
|
||||
}
|
||||
6
src-tauri/yaak-fonts/package.json
Normal file
6
src-tauri/yaak-fonts/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/fonts",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts"
|
||||
}
|
||||
3
src-tauri/yaak-fonts/permissions/default.toml
Normal file
3
src-tauri/yaak-fonts/permissions/default.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = ["allow-list"]
|
||||
41
src-tauri/yaak-fonts/src/commands.rs
Normal file
41
src-tauri/yaak-fonts/src/commands.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use crate::Result;
|
||||
use font_loader::system_fonts;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use tauri::command;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_fonts.ts")]
|
||||
pub struct Fonts {
|
||||
pub editor_fonts: Vec<String>,
|
||||
pub ui_fonts: Vec<String>,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn list() -> Result<Fonts> {
|
||||
let mut ui_fonts = HashSet::new();
|
||||
let mut editor_fonts = HashSet::new();
|
||||
|
||||
let mut property = system_fonts::FontPropertyBuilder::new().monospace().build();
|
||||
for font in &system_fonts::query_specific(&mut property) {
|
||||
editor_fonts.insert(font.to_string());
|
||||
}
|
||||
for font in &system_fonts::query_all() {
|
||||
if !editor_fonts.contains(font) {
|
||||
ui_fonts.insert(font.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let mut ui_fonts: Vec<String> = ui_fonts.into_iter().collect();
|
||||
let mut editor_fonts: Vec<String> = editor_fonts.into_iter().collect();
|
||||
|
||||
ui_fonts.sort();
|
||||
editor_fonts.sort();
|
||||
|
||||
Ok(Fonts {
|
||||
ui_fonts,
|
||||
editor_fonts,
|
||||
})
|
||||
}
|
||||
15
src-tauri/yaak-fonts/src/error.rs
Normal file
15
src-tauri/yaak-fonts/src/error.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
15
src-tauri/yaak-fonts/src/lib.rs
Normal file
15
src-tauri/yaak-fonts/src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use tauri::{
|
||||
generate_handler,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Runtime,
|
||||
};
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
|
||||
use crate::commands::list;
|
||||
pub use error::{Error, Result};
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-fonts").invoke_handler(generate_handler![list]).build()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ publish = false
|
||||
anyhow = "1.0.97"
|
||||
async-recursion = "1.1.1"
|
||||
dunce = "1.0.4"
|
||||
hyper = "1.5.2"
|
||||
hyper-rustls = { version = "0.27.5", default-features = false, features = ["http2"] }
|
||||
hyper-util = { version = "0.1.10", default-features = false, features = ["client-legacy"] }
|
||||
hyper-rustls = { version = "0.27.7", default-features = false, features = ["http2"] }
|
||||
hyper-util = { version = "0.1.13", default-features = false, features = ["client-legacy"] }
|
||||
log = "0.4.20"
|
||||
md5 = "0.7.0"
|
||||
prost = "0.13.4"
|
||||
@@ -25,4 +24,4 @@ tokio-stream = "0.1.14"
|
||||
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
|
||||
tonic-reflection = "0.12.3"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
yaak-http = { workspace = true }
|
||||
yaak-http = { workspace = true }
|
||||
|
||||
@@ -6,7 +6,7 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
yaak-models = { workspace = true }
|
||||
regex = "1.11.0"
|
||||
regex = "1.11.1"
|
||||
rustls = { workspace = true, default-features = false, features = ["ring"] }
|
||||
rustls-platform-verifier = { workspace = true }
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
||||
use rustls::crypto::ring;
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
||||
use rustls_platform_verifier::BuilderVerifierExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn get_config(validate_certificates: bool) -> ClientConfig {
|
||||
let arc_crypto_provider = Arc::new(ring::default_provider());
|
||||
@@ -12,9 +12,7 @@ pub fn get_config(validate_certificates: bool) -> ClientConfig {
|
||||
.unwrap();
|
||||
if validate_certificates {
|
||||
// Use platform-native verifier to validate certificates
|
||||
config_builder
|
||||
.with_platform_verifier()
|
||||
.with_no_client_auth()
|
||||
config_builder.with_platform_verifier().unwrap().with_no_client_auth()
|
||||
} else {
|
||||
config_builder
|
||||
.dangerous()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
use hex_color::HexColor;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use tauri::{Emitter, Runtime, Window};
|
||||
@@ -65,12 +66,7 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor
|
||||
}
|
||||
}
|
||||
|
||||
fn position_traffic_lights(
|
||||
ns_window_handle: UnsafeWindowHandle,
|
||||
x: f64,
|
||||
y: f64,
|
||||
label: String,
|
||||
) {
|
||||
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, label: String) {
|
||||
if !label.starts_with(MAIN_WINDOW_PREFIX) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
hex = "0.4.3"
|
||||
include_dir = "0.7"
|
||||
log = "0.4.22"
|
||||
nanoid = "0.4.0"
|
||||
r2d2 = "0.8.10"
|
||||
@@ -22,7 +24,6 @@ tauri-plugin-dialog = { workspace = true }
|
||||
thiserror = "2.0.11"
|
||||
tokio = { workspace = true }
|
||||
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
|
||||
hex = "0.4.3"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
@@ -58,11 +58,11 @@ export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt
|
||||
|
||||
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
||||
|
||||
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
|
||||
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, bypass: string, } | { "type": "disabled" };
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, coloredMethods: boolean, };
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, };
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Environment } from "./gen_models.js";
|
||||
import type { Folder } from "./gen_models.js";
|
||||
import type { GrpcRequest } from "./gen_models.js";
|
||||
import type { HttpRequest } from "./gen_models.js";
|
||||
import type { WebsocketRequest } from "./gen_models.js";
|
||||
import type { Workspace } from "./gen_models.js";
|
||||
import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models.js";
|
||||
|
||||
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE settings ADD COLUMN interface_font TEXT;
|
||||
ALTER TABLE settings ADD COLUMN editor_font TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE environments ADD COLUMN color TEXT;
|
||||
@@ -59,7 +59,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
.build(manager)
|
||||
.unwrap();
|
||||
|
||||
if let Err(e) = migrate_db(app_handle.app_handle(), &pool) {
|
||||
if let Err(e) = migrate_db(&pool) {
|
||||
error!("Failed to run database migration {e:?}");
|
||||
app_handle
|
||||
.dialog()
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
use crate::error::Error::MigrationError;
|
||||
use crate::error::Result;
|
||||
use log::info;
|
||||
use include_dir::{include_dir, Dir, DirEntry};
|
||||
use log::{debug, info};
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::{OptionalExtension, TransactionBehavior, params};
|
||||
use rusqlite::{params, OptionalExtension, TransactionBehavior};
|
||||
use sha2::{Digest, Sha384};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::result::Result as StdResult;
|
||||
use tauri::path::BaseDirectory;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
pub(crate) fn migrate_db<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
pool: &Pool<SqliteConnectionManager>,
|
||||
) -> Result<()> {
|
||||
let migrations_dir = app_handle
|
||||
.path()
|
||||
.resolve("migrations", BaseDirectory::Resource)
|
||||
.expect("failed to resolve resource");
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
|
||||
info!("Running database migrations from: {:?}", migrations_dir);
|
||||
pub(crate) fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
|
||||
info!("Running database migrations");
|
||||
|
||||
// Ensure the table exists
|
||||
// NOTE: Yaak used to use sqlx for migrations, so we need to mirror that table structure. We
|
||||
@@ -39,20 +29,22 @@ pub(crate) fn migrate_db<R: Runtime>(
|
||||
)?;
|
||||
|
||||
// Read and sort all .sql files
|
||||
let mut entries = fs::read_dir(migrations_dir)
|
||||
.expect("Failed to find migrations directory")
|
||||
.filter_map(StdResult::ok)
|
||||
let mut entries = MIGRATIONS_DIR
|
||||
.entries()
|
||||
.into_iter()
|
||||
.filter(|e| e.path().extension().map(|ext| ext == "sql").unwrap_or(false))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Ensure they're in the correct order
|
||||
entries.sort_by_key(|e| e.file_name());
|
||||
entries.sort_by_key(|e| e.path());
|
||||
|
||||
// Run each migration in a transaction
|
||||
let mut num_migrations = 0;
|
||||
for entry in entries {
|
||||
num_migrations += 1;
|
||||
let mut conn = pool.get()?;
|
||||
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||
match run_migration(entry.path().as_path(), &mut tx) {
|
||||
match run_migration(entry, &mut tx) {
|
||||
Ok(_) => tx.commit()?,
|
||||
Err(e) => {
|
||||
let msg = format!(
|
||||
@@ -66,16 +58,15 @@ pub(crate) fn migrate_db<R: Runtime>(
|
||||
};
|
||||
}
|
||||
|
||||
info!("Finished running migrations");
|
||||
info!("Finished running {} migrations", num_migrations);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_migration(migration_path: &Path, tx: &mut rusqlite::Transaction) -> Result<bool> {
|
||||
fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> {
|
||||
let start = std::time::Instant::now();
|
||||
let (version, description) =
|
||||
split_migration_filename(migration_path.file_name().unwrap().to_str().unwrap())
|
||||
.expect("Failed to parse migration filename");
|
||||
let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap())
|
||||
.expect("Failed to parse migration filename");
|
||||
|
||||
// Skip if already applied
|
||||
let row: Option<i64> = tx
|
||||
@@ -85,11 +76,13 @@ fn run_migration(migration_path: &Path, tx: &mut rusqlite::Transaction) -> Resul
|
||||
.optional()?;
|
||||
|
||||
if row.is_some() {
|
||||
debug!("Skipping migration {description}");
|
||||
// Migration was already run
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let sql = fs::read_to_string(migration_path).expect("Failed to read migration file");
|
||||
let sql =
|
||||
migration_path.as_file().unwrap().contents_utf8().expect("Failed to read migration file");
|
||||
info!("Applying migration {description}");
|
||||
|
||||
// Split on `;`? → optional depending on how your SQL is structured
|
||||
|
||||
@@ -31,12 +31,15 @@ macro_rules! impl_model {
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub enum ProxySetting {
|
||||
Enabled {
|
||||
#[serde(default)]
|
||||
// This was added after on so give it a default to be able to deserialize older values
|
||||
disabled: bool,
|
||||
http: String,
|
||||
https: String,
|
||||
auth: Option<ProxySettingAuth>,
|
||||
|
||||
// These were added later, so give them defaults
|
||||
#[serde(default)]
|
||||
bypass: String,
|
||||
#[serde(default)]
|
||||
disabled: bool,
|
||||
},
|
||||
Disabled,
|
||||
}
|
||||
@@ -103,9 +106,13 @@ pub struct Settings {
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
pub appearance: String,
|
||||
pub colored_methods: bool,
|
||||
pub editor_font: Option<String>,
|
||||
pub editor_font_size: i32,
|
||||
pub editor_keymap: EditorKeymap,
|
||||
pub editor_soft_wrap: bool,
|
||||
pub hide_window_controls: bool,
|
||||
pub interface_font: Option<String>,
|
||||
pub interface_font_size: i32,
|
||||
pub interface_scale: f32,
|
||||
pub open_workspace_new_window: Option<bool>,
|
||||
@@ -113,8 +120,6 @@ pub struct Settings {
|
||||
pub theme_dark: String,
|
||||
pub theme_light: String,
|
||||
pub update_channel: String,
|
||||
pub editor_keymap: EditorKeymap,
|
||||
pub colored_methods: bool,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Settings {
|
||||
@@ -154,6 +159,8 @@ impl UpsertModelInfo for Settings {
|
||||
(EditorFontSize, self.editor_font_size.into()),
|
||||
(EditorKeymap, self.editor_keymap.to_string().into()),
|
||||
(EditorSoftWrap, self.editor_soft_wrap.into()),
|
||||
(EditorFont, self.editor_font.into()),
|
||||
(InterfaceFont, self.interface_font.into()),
|
||||
(InterfaceFontSize, self.interface_font_size.into()),
|
||||
(InterfaceScale, self.interface_scale.into()),
|
||||
(HideWindowControls, self.hide_window_controls.into()),
|
||||
@@ -173,8 +180,10 @@ impl UpsertModelInfo for Settings {
|
||||
SettingsIden::EditorFontSize,
|
||||
SettingsIden::EditorKeymap,
|
||||
SettingsIden::EditorSoftWrap,
|
||||
SettingsIden::EditorFont,
|
||||
SettingsIden::InterfaceFontSize,
|
||||
SettingsIden::InterfaceScale,
|
||||
SettingsIden::InterfaceFont,
|
||||
SettingsIden::HideWindowControls,
|
||||
SettingsIden::OpenWorkspaceNewWindow,
|
||||
SettingsIden::Proxy,
|
||||
@@ -198,10 +207,12 @@ impl UpsertModelInfo for Settings {
|
||||
updated_at: row.get("updated_at")?,
|
||||
appearance: row.get("appearance")?,
|
||||
editor_font_size: row.get("editor_font_size")?,
|
||||
editor_font: row.get("editor_font")?,
|
||||
editor_keymap: EditorKeymap::from_str(editor_keymap.as_str()).unwrap(),
|
||||
editor_soft_wrap: row.get("editor_soft_wrap")?,
|
||||
interface_font_size: row.get("interface_font_size")?,
|
||||
interface_scale: row.get("interface_scale")?,
|
||||
interface_font: row.get("interface_font")?,
|
||||
open_workspace_new_window: row.get("open_workspace_new_window")?,
|
||||
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
|
||||
theme_dark: row.get("theme_dark")?,
|
||||
@@ -520,6 +531,7 @@ pub struct Environment {
|
||||
pub public: bool,
|
||||
pub base: bool,
|
||||
pub variables: Vec<EnvironmentVariable>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Environment {
|
||||
@@ -553,6 +565,7 @@ impl UpsertModelInfo for Environment {
|
||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||
(WorkspaceId, self.workspace_id.into()),
|
||||
(Base, self.base.into()),
|
||||
(Color, self.color.into()),
|
||||
(Name, self.name.trim().into()),
|
||||
(Public, self.public.into()),
|
||||
(Variables, serde_json::to_string(&self.variables)?.into()),
|
||||
@@ -563,6 +576,7 @@ impl UpsertModelInfo for Environment {
|
||||
vec. Do not edit this file manually.
|
||||
import type { Environment } from "./gen_models.js";
|
||||
import type { Folder } from "./gen_models.js";
|
||||
import type { GrpcRequest } from "./gen_models.js";
|
||||
import type { HttpRequest } from "./gen_models.js";
|
||||
import type { HttpResponse } from "./gen_models.js";
|
||||
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
|
||||
import type { JsonValue } from "./serde_json/JsonValue.js";
|
||||
import type { WebsocketRequest } from "./gen_models.js";
|
||||
import type { Workspace } from "./gen_models.js";
|
||||
|
||||
export type BootRequest = { dir: string, watch: boolean, };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { SyncModel } from "./gen_models.js";
|
||||
import type { SyncState } from "./gen_models.js";
|
||||
import type { SyncModel, SyncState } from "./gen_models.js";
|
||||
|
||||
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||
import { toggleDialog } from '../lib/dialog';
|
||||
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||
|
||||
type Props = {
|
||||
@@ -38,6 +39,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
(e) => ({
|
||||
key: e.id,
|
||||
label: e.name,
|
||||
rightSlot: <EnvironmentColorIndicator environment={e} />,
|
||||
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
||||
onSelect: async () => {
|
||||
if (e.id !== activeEnvironment?.id) {
|
||||
@@ -80,6 +82,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
|
||||
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
29
src-web/components/EnvironmentColorIndicator.tsx
Normal file
29
src-web/components/EnvironmentColorIndicator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { showColorPicker } from '../lib/showColorPicker';
|
||||
|
||||
export function EnvironmentColorIndicator({
|
||||
environment,
|
||||
clickToEdit,
|
||||
}: {
|
||||
environment: Environment | null;
|
||||
clickToEdit?: boolean;
|
||||
}) {
|
||||
if (environment?.color == null) return null;
|
||||
|
||||
const style = { backgroundColor: environment.color };
|
||||
const className =
|
||||
'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent';
|
||||
|
||||
if (clickToEdit) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => showColorPicker(environment)}
|
||||
style={style}
|
||||
className={classNames(className, 'hover:border-text')}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <span style={style} className={className} />;
|
||||
}
|
||||
}
|
||||
32
src-web/components/EnvironmentColorPicker.tsx
Normal file
32
src-web/components/EnvironmentColorPicker.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from './core/Button';
|
||||
import { ColorPicker } from './core/ColorPicker';
|
||||
|
||||
export function EnvironmentColorPicker({
|
||||
color: defaultColor,
|
||||
onChange,
|
||||
}: {
|
||||
color: string | null;
|
||||
onChange: (color: string | null) => void;
|
||||
}) {
|
||||
const [color, setColor] = useState<string | null>(defaultColor);
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col items-stretch gap-3 pb-2 w-full"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onChange(color);
|
||||
}}
|
||||
>
|
||||
<ColorPicker color={color} onChange={setColor} />
|
||||
<div className="grid grid-cols-[1fr_1fr] gap-1.5">
|
||||
<Button variant="border" color="secondary" onClick={() => onChange(null)}>
|
||||
Clear
|
||||
</Button>
|
||||
<Button type="submit" color="primary">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
setupOrConfigureEncryption,
|
||||
withEncryptionEnabled,
|
||||
} from '../lib/setupOrConfigureEncryption';
|
||||
import { showColorPicker } from '../lib/showColorPicker';
|
||||
import { BadgeButton } from './core/BadgeButton';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
@@ -35,6 +36,7 @@ import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||
import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
|
||||
interface Props {
|
||||
initialEnvironment: Environment | null;
|
||||
@@ -123,7 +125,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
))}
|
||||
{subEnvironments.length > 0 && (
|
||||
<div className="px-2">
|
||||
<Separator className="my-3"></Separator>
|
||||
<Separator className="my-3" />
|
||||
</div>
|
||||
)}
|
||||
{subEnvironments.map((e) => (
|
||||
@@ -237,6 +239,7 @@ const EnvironmentEditor = function ({
|
||||
return (
|
||||
<VStack space={4} className={classNames(className, 'pl-4')}>
|
||||
<Heading className="w-full flex items-center gap-0.5">
|
||||
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} />
|
||||
<div className="mr-2">{selectedEnvironment?.name}</div>
|
||||
{isEncryptionEnabled ? (
|
||||
promptToEncrypt ? (
|
||||
@@ -344,6 +347,7 @@ function SidebarButton({
|
||||
onContextMenu={handleContextMenu}
|
||||
rightSlot={rightSlot}
|
||||
>
|
||||
<EnvironmentColorIndicator environment={environment} />
|
||||
{children}
|
||||
</Button>
|
||||
{outerRightSlot}
|
||||
@@ -385,6 +389,12 @@ function SidebarButton({
|
||||
},
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
{
|
||||
label: environment.color ? 'Change Color' : 'Assign Color',
|
||||
leftSlot: <Icon icon="palette" />,
|
||||
hidden: environment.base,
|
||||
onSelect: async () => showColorPicker(environment),
|
||||
},
|
||||
{
|
||||
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
||||
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
||||
|
||||
@@ -8,22 +8,24 @@ import { capitalize } from '../../lib/capitalize';
|
||||
import { HStack } from '../core/Stacks';
|
||||
import { TabContent, Tabs } from '../core/Tabs/Tabs';
|
||||
import { HeaderSize } from '../HeaderSize';
|
||||
import { SettingsAppearance } from './SettingsAppearance';
|
||||
import { SettingsInterface } from './SettingsInterface';
|
||||
import { SettingsGeneral } from './SettingsGeneral';
|
||||
import { SettingsLicense } from './SettingsLicense';
|
||||
import { SettingsPlugins } from './SettingsPlugins';
|
||||
import { SettingsProxy } from './SettingsProxy';
|
||||
import { SettingsTheme } from './SettingsTheme';
|
||||
|
||||
interface Props {
|
||||
hide?: () => void;
|
||||
}
|
||||
|
||||
const TAB_GENERAL = 'general';
|
||||
const TAB_APPEARANCE = 'appearance';
|
||||
const TAB_INTERFACE = 'interface';
|
||||
const TAB_THEME = 'theme';
|
||||
const TAB_PROXY = 'proxy';
|
||||
const TAB_PLUGINS = 'plugins';
|
||||
const TAB_LICENSE = 'license';
|
||||
const tabs = [TAB_GENERAL, TAB_APPEARANCE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
|
||||
const tabs = [TAB_GENERAL, TAB_THEME, TAB_INTERFACE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
|
||||
export type SettingsTab = (typeof tabs)[number];
|
||||
|
||||
export default function Settings({ hide }: Props) {
|
||||
@@ -73,8 +75,11 @@ export default function Settings({ hide }: Props) {
|
||||
<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 value={TAB_INTERFACE} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsInterface />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_THEME} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsTheme />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_PLUGINS} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsPlugins />
|
||||
|
||||
137
src-web/components/Settings/SettingsInterface.tsx
Normal file
137
src-web/components/Settings/SettingsInterface.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { useFonts } from '@yaakapp-internal/fonts';
|
||||
import type { EditorKeymap } from '@yaakapp-internal/models';
|
||||
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import React from 'react';
|
||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
import { clamp } from '../../lib/clamp';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { Select } from '../core/Select';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
|
||||
const NULL_FONT_VALUE = '__NULL_FONT__';
|
||||
|
||||
const fontSizeOptions = [
|
||||
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||
].map((n) => ({ label: `${n}`, value: `${n}` }));
|
||||
|
||||
const keymaps: { value: EditorKeymap; label: string }[] = [
|
||||
{ value: 'default', label: 'Default' },
|
||||
{ value: 'vim', label: 'Vim' },
|
||||
{ value: 'vscode', label: 'VSCode' },
|
||||
{ value: 'emacs', label: 'Emacs' },
|
||||
];
|
||||
|
||||
export function SettingsInterface() {
|
||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const fonts = useFonts();
|
||||
|
||||
if (settings == null || workspace == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack space={3} className="mb-4">
|
||||
<HStack space={2} alignItems="end">
|
||||
{fonts.data && (
|
||||
<Select
|
||||
size="sm"
|
||||
name="uiFont"
|
||||
label="Interface Font"
|
||||
value={settings.interfaceFont ?? NULL_FONT_VALUE}
|
||||
options={[
|
||||
{ label: 'System Default', value: NULL_FONT_VALUE },
|
||||
...(fonts.data.uiFonts.map((f) => ({
|
||||
label: f,
|
||||
value: f,
|
||||
})) ?? []),
|
||||
// Some people like monospace fonts for the UI
|
||||
...(fonts.data.editorFonts.map((f) => ({
|
||||
label: f,
|
||||
value: f,
|
||||
})) ?? []),
|
||||
]}
|
||||
onChange={async (v) => {
|
||||
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
|
||||
await patchModel(settings, { interfaceFont });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
hideLabel
|
||||
size="sm"
|
||||
name="interfaceFontSize"
|
||||
label="Interface Font Size"
|
||||
defaultValue="15"
|
||||
value={`${settings.interfaceFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) => patchModel(settings, { interfaceFontSize: parseInt(v) })}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack space={2} alignItems="end">
|
||||
{fonts.data && (
|
||||
<Select
|
||||
size="sm"
|
||||
name="editorFont"
|
||||
label="Editor Font"
|
||||
value={settings.editorFont ?? NULL_FONT_VALUE}
|
||||
options={[
|
||||
{ label: 'System Default', value: NULL_FONT_VALUE },
|
||||
...(fonts.data.editorFonts.map((f) => ({
|
||||
label: f,
|
||||
value: f,
|
||||
})) ?? []),
|
||||
]}
|
||||
onChange={async (v) => {
|
||||
const editorFont = v === NULL_FONT_VALUE ? null : v;
|
||||
await patchModel(settings, { editorFont });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
hideLabel
|
||||
size="sm"
|
||||
name="editorFontSize"
|
||||
label="Editor Font Size"
|
||||
defaultValue="13"
|
||||
value={`${settings.editorFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) =>
|
||||
patchModel(settings, { editorFontSize: clamp(parseInt(v) || 14, 8, 30) })
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
<Select
|
||||
leftSlot={<Icon icon="keyboard" color="secondary" />}
|
||||
size="sm"
|
||||
name="editorKeymap"
|
||||
label="Editor Keymap"
|
||||
value={`${settings.editorKeymap}`}
|
||||
options={keymaps}
|
||||
onChange={(v) => patchModel(settings, { editorKeymap: v })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.editorSoftWrap}
|
||||
title="Wrap Editor Lines"
|
||||
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.coloredMethods}
|
||||
title="Colorize Request Methods"
|
||||
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
|
||||
/>
|
||||
|
||||
{type() !== 'macos' && (
|
||||
<Checkbox
|
||||
checked={settings.hideWindowControls}
|
||||
title="Hide Window Controls"
|
||||
help="Hide the close/maximize/minimize controls on Windows or Linux"
|
||||
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import React from 'react';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { Select } from '../core/Select';
|
||||
import { Separator } from '../core/Separator';
|
||||
@@ -29,6 +30,7 @@ export function SettingsProxy() {
|
||||
http: '',
|
||||
https: '',
|
||||
auth: { user: '', password: '' },
|
||||
bypass: '',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -52,22 +54,28 @@ export function SettingsProxy() {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
|
||||
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
|
||||
const disabled = !enabled;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<HStack space={1.5}>
|
||||
<PlainInput
|
||||
size="sm"
|
||||
label="HTTP"
|
||||
label={
|
||||
<>
|
||||
Proxy for <InlineCode>http://</InlineCode> traffic
|
||||
</>
|
||||
}
|
||||
placeholder="localhost:9090"
|
||||
defaultValue={settings.proxy?.http}
|
||||
onChange={async (http) => {
|
||||
const { proxy } = settings;
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
|
||||
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
await patchModel(settings, {
|
||||
@@ -77,22 +85,28 @@ export function SettingsProxy() {
|
||||
https,
|
||||
auth,
|
||||
disabled,
|
||||
bypass,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PlainInput
|
||||
size="sm"
|
||||
label="HTTPS"
|
||||
label={
|
||||
<>
|
||||
Proxy for <InlineCode>https://</InlineCode> traffic
|
||||
</>
|
||||
}
|
||||
placeholder="localhost:9090"
|
||||
defaultValue={settings.proxy?.https}
|
||||
onChange={async (https) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
|
||||
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -106,9 +120,10 @@ export function SettingsProxy() {
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
|
||||
const auth = enabled ? { user: '', password: '' } : null;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -126,10 +141,11 @@ export function SettingsProxy() {
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
|
||||
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -144,15 +160,39 @@ export function SettingsProxy() {
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
|
||||
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled },
|
||||
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
{settings.proxy.type === 'enabled' && (
|
||||
<>
|
||||
<Separator className="my-6" />
|
||||
<PlainInput
|
||||
label="Proxy Bypass"
|
||||
help="Comma-separated list to bypass the proxy."
|
||||
defaultValue={settings.proxy.bypass}
|
||||
placeholder="127.0.0.1, *.example.com, localhost:3000"
|
||||
onChange={async (bypass) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === 'enabled' ? proxy.http : '';
|
||||
const https = proxy?.type === 'enabled' ? proxy.https : '';
|
||||
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
|
||||
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
|
||||
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, {
|
||||
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
@@ -1,35 +1,19 @@
|
||||
import type { EditorKeymap } from '@yaakapp-internal/models';
|
||||
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import React from 'react';
|
||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
|
||||
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
||||
import { clamp } from '../../lib/clamp';
|
||||
import { getThemes } from '../../lib/theme/themes';
|
||||
import { isThemeDark } from '../../lib/theme/window';
|
||||
import type { ButtonProps } from '../core/Button';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { Editor } from '../core/Editor/Editor';
|
||||
import type { IconProps } from '../core/Icon';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import type { SelectProps } from '../core/Select';
|
||||
import { Select } from '../core/Select';
|
||||
import { Separator } from '../core/Separator';
|
||||
import type { SelectProps } from '../core/Select';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
|
||||
const fontSizeOptions = [
|
||||
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||
].map((n) => ({ label: `${n}`, value: `${n}` }));
|
||||
|
||||
const keymaps: { value: EditorKeymap; label: string }[] = [
|
||||
{ value: 'default', label: 'Default' },
|
||||
{ value: 'vim', label: 'Vim' },
|
||||
{ value: 'vscode', label: 'VSCode' },
|
||||
{ value: 'emacs', label: 'Emacs' },
|
||||
];
|
||||
|
||||
const buttonColors: ButtonProps['color'][] = [
|
||||
'primary',
|
||||
@@ -62,7 +46,7 @@ const icons: IconProps['icon'][] = [
|
||||
|
||||
const { themes } = getThemes();
|
||||
|
||||
export function SettingsAppearance() {
|
||||
export function SettingsTheme() {
|
||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const appearance = useResolvedAppearance();
|
||||
@@ -87,58 +71,7 @@ export function SettingsAppearance() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<VStack space={2} className="mb-4">
|
||||
<Select
|
||||
size="sm"
|
||||
name="interfaceFontSize"
|
||||
label="Font Size"
|
||||
labelPosition="left"
|
||||
defaultValue="15"
|
||||
value={`${settings.interfaceFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) => patchModel(settings, { interfaceFontSize: parseInt(v) })}
|
||||
/>
|
||||
<Select
|
||||
size="sm"
|
||||
name="editorFontSize"
|
||||
label="Editor Font Size"
|
||||
labelPosition="left"
|
||||
defaultValue="13"
|
||||
value={`${settings.editorFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) => patchModel(settings, { editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
|
||||
/>
|
||||
<Select
|
||||
size="sm"
|
||||
name="editorKeymap"
|
||||
label="Editor Keymap"
|
||||
labelPosition="left"
|
||||
value={`${settings.editorKeymap}`}
|
||||
options={keymaps}
|
||||
onChange={(v) => patchModel(settings, { editorKeymap: v })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.editorSoftWrap}
|
||||
title="Wrap Editor Lines"
|
||||
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.coloredMethods}
|
||||
title="Colorize Request Methods"
|
||||
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
|
||||
/>
|
||||
|
||||
{type() !== 'macos' && (
|
||||
<Checkbox
|
||||
checked={settings.hideWindowControls}
|
||||
title="Hide Window Controls"
|
||||
help="Hide the close/maximize/minimize controls on Windows or Linux"
|
||||
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<VStack space={3} className="mb-4">
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
@@ -156,7 +89,7 @@ export function SettingsAppearance() {
|
||||
{(settings.appearance === 'system' || settings.appearance === 'light') && (
|
||||
<Select
|
||||
hideLabel
|
||||
leftSlot={<Icon icon="sun" />}
|
||||
leftSlot={<Icon icon="sun" color="secondary" />}
|
||||
name="lightTheme"
|
||||
label="Light Theme"
|
||||
size="sm"
|
||||
@@ -172,7 +105,7 @@ export function SettingsAppearance() {
|
||||
name="darkTheme"
|
||||
className="flex-1"
|
||||
label="Dark Theme"
|
||||
leftSlot={<Icon icon="moon" />}
|
||||
leftSlot={<Icon icon="moon" color="secondary" />}
|
||||
size="sm"
|
||||
value={activeTheme.dark.id}
|
||||
options={darkThemes}
|
||||
@@ -4,11 +4,8 @@ import { useAtomValue } from 'jotai';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
useEnsureActiveCookieJar,
|
||||
useSubscribeActiveCookieJarId,
|
||||
} from '../hooks/useActiveCookieJar';
|
||||
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
|
||||
import { useEnsureActiveCookieJar, useSubscribeActiveCookieJarId } from '../hooks/useActiveCookieJar';
|
||||
import { activeEnvironmentAtom, useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
|
||||
import { activeRequestAtom } from '../hooks/useActiveRequest';
|
||||
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
|
||||
@@ -32,6 +29,7 @@ import { HotKeyList } from './core/HotKeyList';
|
||||
import { FeedbackLink } from './core/Link';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { CreateDropdown } from './CreateDropdown';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
||||
import { HeaderSize } from './HeaderSize';
|
||||
import { HttpRequestLayout } from './HttpRequestLayout';
|
||||
@@ -41,7 +39,6 @@ import { Sidebar } from './sidebar/Sidebar';
|
||||
import { SidebarActions } from './sidebar/SidebarActions';
|
||||
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
|
||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
const side = { gridArea: 'side' };
|
||||
const head = { gridArea: 'head' };
|
||||
@@ -56,6 +53,7 @@ export function Workspace() {
|
||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
|
||||
const floating = useShouldFloatSidebar();
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
@@ -117,6 +115,12 @@ export function Workspace() {
|
||||
[sideWidth, floating],
|
||||
);
|
||||
|
||||
const environmentBgStyle = useMemo(() => {
|
||||
if (activeEnvironment?.color == null) return undefined;
|
||||
const background = `linear-gradient(to right, ${activeEnvironment.color} 15%, transparent 40%)`;
|
||||
return { background };
|
||||
}, [activeEnvironment?.color]);
|
||||
|
||||
// We're loading still
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
@@ -175,9 +179,19 @@ export function Workspace() {
|
||||
<HeaderSize
|
||||
data-tauri-drag-region
|
||||
size="lg"
|
||||
className="x-theme-appHeader bg-surface"
|
||||
className="relative x-theme-appHeader bg-surface"
|
||||
style={head}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div // Add subtle background
|
||||
style={environmentBgStyle}
|
||||
className="absolute inset-0 opacity-5"
|
||||
/>
|
||||
<div // Add subtle border bottom
|
||||
style={environmentBgStyle}
|
||||
className="absolute left-0 right-0 bottom-0 h-[0.5px] opacity-20"
|
||||
/>
|
||||
</div>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</HeaderSize>
|
||||
<ErrorBoundary name="Workspace Body">
|
||||
|
||||
32
src-web/components/core/ColorPicker.tsx
Normal file
32
src-web/components/core/ColorPicker.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { useRandomKey } from '../../hooks/useRandomKey';
|
||||
import { PlainInput } from './PlainInput';
|
||||
|
||||
interface Props {
|
||||
onChange: (value: string | null) => void;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
export function ColorPicker({ onChange, color }: Props) {
|
||||
const [updateKey, regenerateKey] = useRandomKey();
|
||||
return (
|
||||
<div>
|
||||
<HexColorPicker
|
||||
color={color ?? undefined}
|
||||
className="!w-full"
|
||||
onChange={(color) => {
|
||||
onChange(color);
|
||||
regenerateKey(); // To force input to change
|
||||
}}
|
||||
/>
|
||||
<PlainInput
|
||||
hideLabel
|
||||
label="Plain Color"
|
||||
forceUpdateKey={updateKey}
|
||||
defaultValue={color ?? ''}
|
||||
onChange={onChange}
|
||||
validate={(color) => color.match(/#[0-9a-fA-F]{6}$/) !== null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,9 +26,9 @@ const icons = {
|
||||
cake: lucide.CakeIcon,
|
||||
chat: lucide.MessageSquare,
|
||||
check: lucide.CheckIcon,
|
||||
check_circle: lucide.CheckCircleIcon,
|
||||
check_square_checked: lucide.SquareCheckIcon,
|
||||
check_square_unchecked: lucide.SquareIcon,
|
||||
check_circle: lucide.CheckCircleIcon,
|
||||
chevron_down: lucide.ChevronDownIcon,
|
||||
chevron_right: lucide.ChevronRightIcon,
|
||||
circle_alert: lucide.CircleAlertIcon,
|
||||
@@ -78,6 +78,7 @@ const icons = {
|
||||
minus_circle: lucide.MinusCircleIcon,
|
||||
moon: lucide.MoonIcon,
|
||||
more_vertical: lucide.MoreVerticalIcon,
|
||||
palette: lucide.PaletteIcon,
|
||||
paste: lucide.ClipboardPasteIcon,
|
||||
pencil: lucide.PencilIcon,
|
||||
pin: lucide.PinIcon,
|
||||
|
||||
@@ -16,11 +16,15 @@ export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type
|
||||
};
|
||||
|
||||
export function PlainInput({
|
||||
autoFocus,
|
||||
autoSelect,
|
||||
className,
|
||||
containerClassName,
|
||||
defaultValue,
|
||||
forceUpdateKey,
|
||||
help,
|
||||
hideLabel,
|
||||
hideObscureToggle,
|
||||
label,
|
||||
labelClassName,
|
||||
labelPosition = 'top',
|
||||
@@ -29,19 +33,16 @@ export function PlainInput({
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
onFocusRaw,
|
||||
onKeyDownCapture,
|
||||
onPaste,
|
||||
placeholder,
|
||||
required,
|
||||
rightSlot,
|
||||
hideObscureToggle,
|
||||
size = 'md',
|
||||
type = 'text',
|
||||
tint,
|
||||
type = 'text',
|
||||
validate,
|
||||
autoSelect,
|
||||
placeholder,
|
||||
autoFocus,
|
||||
onKeyDownCapture,
|
||||
onFocusRaw,
|
||||
}: PlainInputProps) {
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -101,7 +102,7 @@ export function PlainInput({
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
)}
|
||||
>
|
||||
<Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required}>
|
||||
<Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required} help={help}>
|
||||
{label}
|
||||
</Label>
|
||||
<HStack
|
||||
|
||||
19
src-web/font.ts
Normal file
19
src-web/font.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Listen for settings changes, the re-compute theme
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import type { ModelPayload, Settings } from '@yaakapp-internal/models';
|
||||
import { getSettings } from './lib/settings';
|
||||
|
||||
function setFonts(settings: Settings) {
|
||||
document.documentElement.style.setProperty('--font-family-editor', settings.editorFont ?? '');
|
||||
document.documentElement.style.setProperty(
|
||||
'--font-family-interface',
|
||||
settings.interfaceFont ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
listen<ModelPayload>('upserted_model', async (event) => {
|
||||
if (event.payload.model.model !== 'settings') return;
|
||||
setFonts(event.payload.model);
|
||||
}).catch(console.error);
|
||||
|
||||
getSettings().then((settings) => setFonts(settings));
|
||||
@@ -26,18 +26,23 @@ export function useRecentWorkspaces() {
|
||||
|
||||
export function useSubscribeRecentWorkspaces() {
|
||||
useEffect(() => {
|
||||
return jotaiStore.sub(activeWorkspaceIdAtom, async () => {
|
||||
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
if (activeWorkspaceId == null) return;
|
||||
|
||||
const key = kvKey();
|
||||
|
||||
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
|
||||
if (recentIds[0] === activeWorkspaceId) return; // Short-circuit
|
||||
|
||||
const withoutActiveId = recentIds.filter((id) => id !== activeWorkspaceId);
|
||||
const value = [activeWorkspaceId, ...withoutActiveId];
|
||||
await setKeyValue({ namespace, key, value });
|
||||
});
|
||||
const unsub = jotaiStore.sub(activeWorkspaceIdAtom, updateRecentWorkspaces);
|
||||
updateRecentWorkspaces().catch(console.error); // Update when opened in a new window
|
||||
return unsub;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async function updateRecentWorkspaces() {
|
||||
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
if (activeWorkspaceId == null) return;
|
||||
|
||||
const key = kvKey();
|
||||
|
||||
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
|
||||
if (recentIds[0] === activeWorkspaceId) return; // Short-circuit
|
||||
|
||||
const withoutActiveId = recentIds.filter((id) => id !== activeWorkspaceId);
|
||||
const value = [activeWorkspaceId, ...withoutActiveId];
|
||||
console.log('Recent workspaces update', activeWorkspaceId);
|
||||
await setKeyValue({ namespace, key, value });
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<div id="radix-portal" class="cm-portal"></div>
|
||||
<script type="module" src="/theme.ts"></script>
|
||||
<script type="module" src="/font-size.ts"></script>
|
||||
<script type="module" src="/font.ts"></script>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user