Compare commits

..

23 Commits

Author SHA1 Message Date
Gregory Schier
2b61257e50 Fix date 2024-12-03 11:48:39 -08:00
Gregory Schier
1f4eea89c5 Better license badge placement 2024-12-03 11:06:50 -08:00
Gregory Schier
1609e46660 Fix CI 2024-12-03 10:58:39 -08:00
Gregory Schier
28d5a2a019 Add copy 2024-12-03 10:09:44 -08:00
Gregory Schier
40f0f5387a Fix lint problems 2024-12-03 09:43:16 -08:00
Gregory Schier
88bcfb9e66 Changes for commercial use (#138) 2024-12-03 09:28:27 -08:00
Gregory Schier
2b076c90e4 Increase gRPC max message sizes 2024-12-03 09:24:14 -08:00
Leandro Otoni
0443fbdfdb Merge pull request #137
* add codeFolding config for json
2024-12-03 07:15:38 -08:00
Gregory Schier
36d24bdac0 Tweaked header size logic 2024-12-03 07:11:25 -08:00
Peiman Nourani
d4dfc1c820 Hide window controls in macOS fullscreen mode (#134) 2024-12-03 07:08:01 -08:00
Bad3r
00178ad197 Feat: Add Support for Nord Theme 🏔️ (#132)
Signed-off-by: Bad3r <bad3r@pm.me>
2024-12-03 07:00:47 -08:00
Bad3r
f8efd1a31a fix: Add Missing Themes Moonlight & Dracula (#133)
Signed-off-by: Bad3r <bad3r@pm.me>
2024-12-03 06:57:33 -08:00
Gregory Schier
e1363cf151 Make protoc writable before signing 2024-11-23 06:18:17 -08:00
Gregory Schier
38e0f5ede7 Remove unnecessary things in Windows signing conf 2024-11-23 05:27:12 -08:00
Gregory Schier
9663018e21 Update tauri.conf.json 2024-11-22 08:51:29 -08:00
Gregory Schier
80a7c2a9c7 Try updating protoc 2024-11-22 07:09:59 -08:00
Gregory Schier
4687723176 Fix Tauri signing account name 2024-11-22 06:17:37 -08:00
Gregory Schier
41ce2df00c Move signing into Tauri 2024-11-22 05:49:24 -08:00
Gregory Schier
794967904a Try signing again 2024-11-21 13:43:05 -08:00
Gregory Schier
74a7a1a21a Merge remote-tracking branch 'origin/master' 2024-11-21 13:13:10 -08:00
Gregory Schier
d9587aa314 First attempt at Windows signing 2024-11-21 13:13:05 -08:00
dependabot[bot]
6b208ef67c Bump cross-spawn (#135)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 21:15:30 -08:00
Gregory Schier
0cfec0ada6 Fix GraphQL body handling 2024-11-18 07:17:19 -08:00
62 changed files with 1264 additions and 187 deletions

View File

@@ -22,7 +22,7 @@ jobs:
- platform: 'macos-latest' # for Intel-based Macs.
args: '--target x86_64-apple-darwin'
yaak_arch: 'x64'
- platform: 'ubuntu-22.04' # for Tauri v1, you could replace this with ubuntu-20.04.
- platform: 'ubuntu-22.04'
args: ''
yaak_arch: 'x64'
- platform: 'windows-latest'
@@ -38,10 +38,6 @@ jobs:
with:
node-version: 22
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -56,7 +52,7 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
src-tauri/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
@@ -66,6 +62,10 @@ jobs:
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
run: cargo install --force trusted-signing-cli
- name: Install NPM Dependencies
run: |
npm ci
@@ -94,16 +94,24 @@ jobs:
env:
YAAK_PLUGINS_DIR: ${{ env.YAAK_PLUGINS_DIR }}
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
# Apple signing stuff
APPLE_CERTIFICATE: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_TEAM_ID }}
# Windows signing stuff
AZURE_CLIENT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_TENANT_ID }}
with:
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'

36
package-lock.json generated
View File

@@ -10,11 +10,12 @@
"workspaces": [
"plugin-runtime",
"plugin-runtime-types",
"src-tauri/yaak_license",
"src-tauri/yaak_models",
"src-tauri/yaak_plugin_runtime",
"src-tauri/yaak_sse",
"src-tauri/yaak_sync",
"src-tauri/yaak_templates",
"src-tauri/yaak_sse",
"src-web"
],
"devDependencies": {
@@ -3368,6 +3369,10 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
"license": "MIT"
},
"node_modules/@yaakapp-internal/license": {
"resolved": "src-tauri/yaak_license",
"link": true
},
"node_modules/@yaakapp-internal/models": {
"resolved": "src-tauri/yaak_models",
"link": true
@@ -3384,7 +3389,7 @@
"resolved": "src-tauri/yaak_sse",
"link": true
},
"node_modules/@yaakapp-internal/template": {
"node_modules/@yaakapp-internal/templates": {
"resolved": "src-tauri/yaak_templates",
"link": true
},
@@ -4929,10 +4934,9 @@
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"license": "MIT",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -9225,11 +9229,10 @@
"license": "MIT"
},
"node_modules/npm-run-all/node_modules/cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -10489,11 +10492,10 @@
}
},
"node_modules/react-devtools/node_modules/execa/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -13737,6 +13739,10 @@
"typescript": "^5.6.2"
}
},
"src-tauri/yaak_license": {
"name": "@yaakapp-internal/license",
"version": "1.0.0"
},
"src-tauri/yaak_models": {
"name": "@yaakapp-internal/models",
"version": "1.0.0"
@@ -13750,7 +13756,7 @@
"version": "1.0.0"
},
"src-tauri/yaak_templates": {
"name": "@yaakapp-internal/template",
"name": "@yaakapp-internal/templates",
"version": "1.0.0"
},
"src-web": {

View File

@@ -4,16 +4,17 @@
"version": "0.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/yaakapp/app.git"
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"workspaces": [
"plugin-runtime",
"plugin-runtime-types",
"src-tauri/yaak_license",
"src-tauri/yaak_models",
"src-tauri/yaak_plugin_runtime",
"src-tauri/yaak_sse",
"src-tauri/yaak_sync",
"src-tauri/yaak_templates",
"src-tauri/yaak_sse",
"src-web"
],
"scripts": {

View File

@@ -6,7 +6,10 @@ import { PluginHandle } from './PluginHandle';
const port = process.env.PORT || '50051';
const channel = createChannel(`localhost:${port}`);
const channel = createChannel(`localhost:${port}`, undefined, {
'grpc.max_receive_message_length': Number.MAX_SAFE_INTEGER,
'grpc.max_send_message_length': Number.MAX_SAFE_INTEGER,
});
const client: PluginRuntimeClient = createClient(PluginRuntimeDefinition, channel);
const events = new EventChannel();

View File

@@ -1,10 +1,10 @@
const decompress = require('decompress');
const Downloader = require('nodejs-file-downloader');
const path = require('node:path');
const { rmSync, mkdirSync, cpSync, existsSync } = require('node:fs');
const { rmSync, mkdirSync, cpSync, existsSync, statSync, chmodSync } = require('node:fs');
const { execSync } = require('node:child_process');
const VERSION = '27.2';
const VERSION = '28.3';
// `${process.platform}_${process.arch}`
const MAC_ARM = 'darwin_arm64';
@@ -67,6 +67,11 @@ mkdirSync(dstDir, { recursive: true });
cpSync(includeSrc, includeDst, { recursive: true });
rmSync(tmpDir, { recursive: true, force: true });
// Make binary writable, so we can sign it during release
const stat = statSync(binDst);
const newMode = stat.mode | 0o200;
chmodSync(binDst, newMode);
console.log('Downloaded protoc to', binDst);
})().catch((err) => console.log('Script failed:', err));

44
src-tauri/Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@@ -964,16 +964,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "command-group"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409"
dependencies = [
"nix",
"winapi",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -6197,9 +6187,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.0.1"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e6660a409963e4d57b9bfab4addd141eeff41bd3a7fb14e13004a832cf7ef6"
checksum = "e753f2a30933a9bbf0a202fa47d7cc4a3401f06e8d6dcc53b79aa62954828c79"
dependencies = [
"anyhow",
"glob",
@@ -6373,6 +6363,22 @@ dependencies = [
"thiserror 1.0.63",
]
[[package]]
name = "tauri-plugin-yaak-license"
version = "0.1.0"
dependencies = [
"chrono",
"log",
"reqwest",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.3",
"ts-rs",
"yaak_models",
]
[[package]]
name = "tauri-runtime"
version = "2.2.0"
@@ -6785,9 +6791,9 @@ dependencies = [
[[package]]
name = "tonic"
version = "0.12.3"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401"
dependencies = [
"async-stream",
"async-trait",
@@ -7991,6 +7997,7 @@ dependencies = [
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tauri-plugin-yaak-license",
"tokio",
"tokio-stream",
"urlencoding",
@@ -8051,22 +8058,19 @@ dependencies = [
name = "yaak_plugin_runtime"
version = "0.1.0"
dependencies = [
"anyhow",
"command-group",
"dunce",
"log",
"path-slash",
"prost 0.13.3",
"rand 0.8.5",
"regex",
"reqwest",
"serde",
"serde_json",
"tauri",
"tauri-plugin-shell",
"thiserror 1.0.63",
"tokio",
"tonic 0.12.3",
"tonic 0.12.1",
"tonic-build",
"ts-rs",
"yaak_models",
@@ -8077,7 +8081,6 @@ name = "yaak_sse"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"ts-rs",
]
@@ -8087,7 +8090,6 @@ version = "0.1.0"
dependencies = [
"log",
"serde",
"serde_json",
"tokio",
"ts-rs",
]

View File

@@ -1,11 +1,19 @@
[workspace]
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models", "yaak_sse"]
members = [
"yaak_grpc",
"yaak_license",
"yaak_models",
"yaak_plugin_runtime",
"yaak_sse",
"yaak_templates",
]
[package]
name = "yaak-app"
version = "0.0.0"
edition = "2021"
authors = ["Gregory Schier"]
publish = false
# Produce a library for mobile support
[lib]
@@ -27,10 +35,11 @@ openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installa
[dependencies]
yaak_grpc = { path = "yaak_grpc" }
yaak_templates = { path = "yaak_templates" }
tauri-plugin-yaak-license = { path = "yaak_license" }
yaak_models = { workspace = true }
yaak_plugin_runtime = { workspace = true }
yaak_sse = { workspace = true }
yaak_models = { workspace = true }
yaak_templates = { path = "yaak_templates" }
base64 = "0.22.0"
chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
@@ -39,7 +48,7 @@ http = "1"
log = "0.4.21"
rand = "0.8.5"
regex = "1.10.2"
reqwest = { version = "0.12.4", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "native-tls-alpn"] }
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "native-tls-alpn"] }
reqwest_cookie_store = "0.8.0"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
@@ -66,6 +75,7 @@ yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
serde = "1.0.215"
serde_json = "1.0.132"
tauri-plugin-shell = "2.0.2"
tauri = { version = "2.1.1", features = ["devtools", "protocol-asset"] }
tauri = "2.1.1"
thiserror = "2.0.3"
ts-rs = "10.0.0"
reqwest = "0.12.4"

View File

@@ -7,6 +7,7 @@
"*"
],
"permissions": [
"yaak-license:default",
"core:event:allow-emit",
"core:event:allow-listen",
"core:event:allow-unlisten",

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["yaak-license:default","core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}

View File

@@ -5141,6 +5141,31 @@
"description": "Denies the save_window_state command without any pre-configured scope.",
"type": "string",
"const": "window-state:deny-save-window-state"
},
{
"description": "Default permissions for the plugin",
"type": "string",
"const": "yaak-license:default"
},
{
"description": "Enables the activate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-activate"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-check"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-activate"
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-check"
}
]
},

View File

@@ -5141,6 +5141,31 @@
"description": "Denies the save_window_state command without any pre-configured scope.",
"type": "string",
"const": "window-state:deny-save-window-state"
},
{
"description": "Default permissions for the plugin",
"type": "string",
"const": "yaak-license:default"
},
{
"description": "Enables the activate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-activate"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:allow-check"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-activate"
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "yaak-license:deny-check"
}
]
},

View File

@@ -243,7 +243,7 @@ pub async fn send_http_request<R: Runtime>(
let request_body = rendered_request.body;
if let Some(body_type) = &rendered_request.body_type {
if request_body.contains_key("query") && request_body.contains_key("variables") {
if body_type == "graphql" {
let query = get_str_h(&request_body, "query");
let variables = get_str_h(&request_body, "variables");
let body = if variables.trim().is_empty() {
@@ -255,9 +255,6 @@ pub async fn send_http_request<R: Runtime>(
)
};
request_builder = request_builder.body(body.to_owned());
} else if request_body.contains_key("text") {
let body = get_str_h(&request_body, "text");
request_builder = request_builder.body(body.to_owned());
} else if body_type == "application/x-www-form-urlencoded"
&& request_body.contains_key("form")
{
@@ -359,6 +356,9 @@ pub async fn send_http_request<R: Runtime>(
}
headers.remove("Content-Type"); // reqwest will add this automatically
request_builder = request_builder.multipart(multipart_form);
} else if request_body.contains_key("text") {
let body = get_str_h(&request_body, "text");
request_builder = request_builder.body(body.to_owned());
} else {
warn!("Unsupported body type: {}", body_type);
}

View File

@@ -1687,6 +1687,7 @@ pub fn run() {
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init())
.plugin(yaak_models::plugin::Builder::default().build())
.plugin(tauri_plugin_yaak_license::init())
.plugin(yaak_plugin_runtime::plugin::init());
#[cfg(target_os = "macos")]

View File

@@ -45,8 +45,8 @@
"active": true,
"category": "DeveloperTool",
"externalBin": [
"vendored/protoc/yaakprotoc",
"vendored/node/yaaknode"
"vendored/node/yaaknode",
"vendored/protoc/yaakprotoc"
],
"icon": [
"icons/release/32x32.png",
@@ -79,8 +79,7 @@
"frameworks": []
},
"windows": {
"digestAlgorithm": "sha256",
"timestampUrl": ""
"signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1"
}
}
}

View File

@@ -2,6 +2,7 @@
name = "yaak_grpc"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
tonic = "0.10.2"
@@ -22,4 +23,4 @@ tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
md5 = "0.7.0"
dunce = "1.0.4"
async-recursion = "1.1.1"
async-recursion = "1.1.1"

View File

@@ -0,0 +1,20 @@
[package]
name = "tauri-plugin-yaak-license"
links = "tauri-plugin-yaak-license"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
reqwest = { workspace = true, features = ["json"] }
serde = { version = "1.0.208", features = ["derive"] }
ts-rs = { workspace = true }
thiserror = { workspace = true }
tauri = { workspace = true }
yaak_models = { workspace = true }
chrono = "0.4.38"
log = "0.4.22"
serde_json = "1.0.132"
[build-dependencies]
tauri-plugin = { version = "2.0.3", features = ["build"] }

View File

@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type APIErrorResponsePayload = { error: string, message: string, };
export type ActivateLicenseRequestPayload = { licenseKey: string, appVersion: string, appPlatform: string, };
export type ActivateLicenseResponsePayload = { activationId: string, };
export type CheckActivationResponsePayload = { active: boolean, };
export type LicenseCheckStatus = { "type": "personal_use" } | { "type": "commercial_use" } | { "type": "trialing", end: string, } | { "type": "trial_ended", end: string, };

View 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 CheckActivationRequestPayload = { activationId: string, };

View File

@@ -0,0 +1,5 @@
const COMMANDS: &[&str] = &["activate", "check"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

View File

@@ -0,0 +1,33 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { useListenToTauriEvent } from '@yaakapp/app/hooks/useListenToTauriEvent';
import { LicenseCheckStatus } from './bindings/license';
export * from './bindings/license';
export function useLicense() {
const queryClient = useQueryClient();
const activate = useMutation<void, string, { licenseKey: string }>({
mutationKey: ['license.activate'],
mutationFn: (payload) => invoke('plugin:yaak-license|activate', payload),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY });
},
});
// Check the license again after a license is activated
useListenToTauriEvent('license-activated', async () => {
await queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY });
});
const CHECK_QUERY_KEY = ['license.check'];
const check = useQuery<void, string, LicenseCheckStatus>({
queryKey: CHECK_QUERY_KEY,
queryFn: () => invoke('plugin:yaak-license|check'),
});
return {
activate,
check,
} as const;
}

View File

@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/license",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-activate"
description = "Enables the activate command without any pre-configured scope."
commands.allow = ["activate"]
[[permission]]
identifier = "deny-activate"
description = "Denies the activate command without any pre-configured scope."
commands.deny = ["activate"]

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-check"
description = "Enables the check command without any pre-configured scope."
commands.allow = ["check"]
[[permission]]
identifier = "deny-check"
description = "Denies the check command without any pre-configured scope."
commands.deny = ["check"]

View File

@@ -0,0 +1,68 @@
## Default Permission
Default permissions for the plugin
- `allow-check`
- `allow-activate`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`yaak-license:allow-activate`
</td>
<td>
Enables the activate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:deny-activate`
</td>
<td>
Denies the activate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:allow-check`
</td>
<td>
Enables the check command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:deny-check`
</td>
<td>
Denies the check command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-check", "allow-activate"]

View File

@@ -0,0 +1,325 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PermissionFile",
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
"type": "object",
"properties": {
"default": {
"description": "The default permission set for the plugin",
"anyOf": [
{
"$ref": "#/definitions/DefaultPermission"
},
{
"type": "null"
}
]
},
"set": {
"description": "A list of permissions sets defined",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionSet"
}
},
"permission": {
"description": "A list of inlined permissions",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/Permission"
}
}
},
"definitions": {
"DefaultPermission": {
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
"type": "object",
"required": [
"permissions"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"description": {
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"PermissionSet": {
"description": "A set of direct permissions grouped together under a new name.",
"type": "object",
"required": [
"description",
"identifier",
"permissions"
],
"properties": {
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does.",
"type": "string"
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionKind"
}
}
}
},
"Permission": {
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"commands": {
"description": "Allowed or denied commands when using this permission.",
"default": {
"allow": [],
"deny": []
},
"allOf": [
{
"$ref": "#/definitions/Commands"
}
]
},
"scope": {
"description": "Allowed or denied scoped when using this permission.",
"allOf": [
{
"$ref": "#/definitions/Scopes"
}
]
},
"platforms": {
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Target"
}
}
}
},
"Commands": {
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
"type": "object",
"properties": {
"allow": {
"description": "Allowed command.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"deny": {
"description": "Denied command, which takes priority.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
}
},
"Scopes": {
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
"type": "object",
"properties": {
"allow": {
"description": "Data that defines what is allowed by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
},
"deny": {
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
}
}
},
"Value": {
"description": "All supported ACL values.",
"anyOf": [
{
"description": "Represents a null JSON value.",
"type": "null"
},
{
"description": "Represents a [`bool`].",
"type": "boolean"
},
{
"description": "Represents a valid ACL [`Number`].",
"allOf": [
{
"$ref": "#/definitions/Number"
}
]
},
{
"description": "Represents a [`String`].",
"type": "string"
},
{
"description": "Represents a list of other [`Value`]s.",
"type": "array",
"items": {
"$ref": "#/definitions/Value"
}
},
{
"description": "Represents a map of [`String`] keys to [`Value`]s.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Value"
}
}
]
},
"Number": {
"description": "A valid ACL number.",
"anyOf": [
{
"description": "Represents an [`i64`].",
"type": "integer",
"format": "int64"
},
{
"description": "Represents a [`f64`].",
"type": "number",
"format": "double"
}
]
},
"Target": {
"description": "Platform target.",
"oneOf": [
{
"description": "MacOS.",
"type": "string",
"enum": [
"macOS"
]
},
{
"description": "Windows.",
"type": "string",
"enum": [
"windows"
]
},
{
"description": "Linux.",
"type": "string",
"enum": [
"linux"
]
},
{
"description": "Android.",
"type": "string",
"enum": [
"android"
]
},
{
"description": "iOS.",
"type": "string",
"enum": [
"iOS"
]
}
]
},
"PermissionKind": {
"type": "string",
"oneOf": [
{
"description": "Enables the activate command without any pre-configured scope.",
"type": "string",
"const": "allow-activate"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
"const": "deny-activate"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "allow-check"
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "deny-check"
},
{
"description": "Default permissions for the plugin",
"type": "string",
"const": "default"
}
]
}
}
}

View File

@@ -0,0 +1,37 @@
use crate::errors::Result;
use crate::{activate_license, check_license, ActivateLicenseRequestPayload, LicenseCheckStatus};
use log::{debug, info};
use std::string::ToString;
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
#[command]
pub async fn check<R: Runtime>(app_handle: AppHandle<R>) -> Result<LicenseCheckStatus> {
debug!("Checking license");
check_license(&app_handle).await
}
#[command]
pub async fn activate<R: Runtime>(license_key: &str, window: WebviewWindow<R>) -> Result<()> {
info!("Activating license {}", license_key);
activate_license(
&window,
ActivateLicenseRequestPayload {
license_key: license_key.to_string(),
app_platform: get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
},
)
.await
}
fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}

View File

@@ -0,0 +1,28 @@
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Reqwest error: {0}")]
APIError(#[from] reqwest::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("{message}")]
ClientError { message: String, error: String },
#[error("Internal server error")]
ServerError,
}
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>;

View File

@@ -0,0 +1,16 @@
use tauri::{
generate_handler,
plugin::{Builder, TauriPlugin},
Runtime,
};
mod commands;
mod errors;
mod license;
use crate::commands::{activate, check};
pub use license::*;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-license").invoke_handler(generate_handler![check, activate]).build()
}

View File

@@ -0,0 +1,155 @@
use crate::errors::Error::{ClientError, ServerError};
use crate::errors::Result;
use chrono::{NaiveDateTime, Utc};
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::ops::Add;
use std::time::Duration;
use tauri::{is_dev, AppHandle, Emitter, Runtime, WebviewWindow};
use ts_rs::TS;
const KV_NAMESPACE: &str = "license";
const KV_ACTIVATION_ID_KEY: &str = "activation_id";
const TRIAL_SECONDS: u64 = 3600 * 24 * 14;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
pub struct CheckActivationRequestPayload {
pub activation_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
pub struct CheckActivationResponsePayload {
pub active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
pub struct ActivateLicenseRequestPayload {
pub license_key: String,
pub app_version: String,
pub app_platform: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
pub struct ActivateLicenseResponsePayload {
pub activation_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "license.ts")]
pub struct APIErrorResponsePayload {
pub error: String,
pub message: String,
}
pub async fn activate_license<R: Runtime>(
window: &WebviewWindow<R>,
p: ActivateLicenseRequestPayload,
) -> Result<()> {
let client = reqwest::Client::new();
let response = client.post(build_url("/activate")).json(&p).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
return Err(ClientError {
message: body.message,
error: body.error,
});
}
if response.status().is_server_error() {
return Err(ServerError);
}
let body: ActivateLicenseResponsePayload = response.json().await?;
yaak_models::queries::set_key_value_string(
window,
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
body.activation_id.as_str(),
)
.await;
if let Err(e) = window.emit("license-activated", true) {
warn!("Failed to emit check-license event: {}", e);
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "license.ts")]
pub enum LicenseCheckStatus {
PersonalUse,
CommercialUse,
InvalidLicense,
Trialing { end: NaiveDateTime },
TrialEnded { end: NaiveDateTime },
}
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<LicenseCheckStatus> {
let activation_id = yaak_models::queries::get_key_value_string(
app_handle,
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
"",
)
.await;
let settings = yaak_models::queries::get_or_create_settings(app_handle).await;
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
debug!("Trial ending at {trial_end:?}");
let has_activation_id = !activation_id.is_empty();
let trial_period_active = Utc::now().naive_utc() < trial_end;
match (has_activation_id, trial_period_active) {
(false, true) => Ok(LicenseCheckStatus::Trialing { end: trial_end }),
(false, false) => Ok(LicenseCheckStatus::TrialEnded { end: trial_end }),
(true, _) => {
info!("Checking license activation");
// A license has been activated, so let's check the license server
let client = reqwest::Client::new();
let payload = CheckActivationRequestPayload {
activation_id: activation_id.clone(),
};
let response = client.post(build_url("/check")).json(&payload).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
return Err(ClientError {
message: body.message,
error: body.error,
});
}
if response.status().is_server_error() {
return Err(ServerError);
}
let body: CheckActivationResponsePayload = response.json().await?;
if !body.active {
return Ok(LicenseCheckStatus::InvalidLicense);
}
Ok(LicenseCheckStatus::CommercialUse)
}
}
}
fn build_url(path: &str) -> String {
if is_dev() {
format!("http://localhost:9444/licenses{path}")
} else {
format!("https://license.yaak.app/licenses{path}")
}
}

View File

@@ -2,6 +2,7 @@
name = "yaak_models"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
@@ -11,7 +12,7 @@ sea-query-rusqlite = { version = "0.6.0", features = ["with-chrono"] }
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
thiserror = "1.0.63"
ts-rs = { version = "10.0.0", features = ["chrono-impl", "serde-json-impl"] }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
tauri = { workspace = true }
sqlx = { version = "0.8.0", features = ["sqlite", "runtime-tokio-rustls"] }
log = "0.4.22"

View File

@@ -2,15 +2,13 @@
name = "yaak_plugin_runtime"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
anyhow = "1.0.86"
command-group = "5.0.1"
dunce = "1.0.4"
log = "0.4.21"
prost = "0.13.1"
rand = "0.8.5"
reqwest = { version = "0.12.5", features = ["stream"] }
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.113"
tauri = { workspace = true }

View File

@@ -31,7 +31,7 @@ pub struct PluginManager {
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
plugins: Arc<Mutex<Vec<PluginHandle>>>,
kill_tx: tokio::sync::watch::Sender<bool>,
server: Arc<PluginRuntimeServerImpl>,
grpc_service: Arc<PluginRuntimeServerImpl>,
}
#[derive(Clone)]
@@ -47,13 +47,13 @@ impl PluginManager {
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
let server =
let grpc_service =
PluginRuntimeServerImpl::new(events_tx, client_disconnect_tx, client_connect_tx);
let plugin_manager = PluginManager {
plugins: Arc::new(Mutex::new(Vec::new())),
subscribers: Arc::new(Mutex::new(HashMap::new())),
server: Arc::new(server.clone()),
grpc_service: Arc::new(grpc_service.clone()),
kill_tx: kill_server_tx,
};
@@ -78,15 +78,15 @@ impl PluginManager {
info!("Starting plugin server");
let svc = PluginRuntimeServer::new(server.to_owned());
let svc = PluginRuntimeServer::new(grpc_service.to_owned())
.max_encoding_message_size(usize::MAX)
.max_decoding_message_size(usize::MAX);
let listen_addr = match option_env!("PORT") {
None => "localhost:0".to_string(),
Some(port) => format!("localhost:{port}"),
};
let listener = tauri::async_runtime::block_on(async move {
TcpListener::bind(listen_addr)
.await
.expect("Failed to bind TCP listener")
TcpListener::bind(listen_addr).await.expect("Failed to bind TCP listener")
});
let addr = listener.local_addr().expect("Failed to get local address");
@@ -123,9 +123,7 @@ impl PluginManager {
// 2. Start Node.js runtime and initialize plugins
tauri::async_runtime::block_on(async move {
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx)
.await
.unwrap();
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx).await.unwrap();
});
plugin_manager
@@ -173,10 +171,7 @@ impl PluginManager {
}
pub async fn uninstall(&self, window_context: WindowContext, dir: &str) -> Result<()> {
let plugin = self
.get_plugin_by_dir(dir)
.await
.ok_or(PluginNotFoundErr(dir.to_string()))?;
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
self.remove_plugin(window_context, &plugin).await
}
@@ -205,7 +200,7 @@ impl PluginManager {
watch: bool,
) -> Result<()> {
info!("Adding plugin by dir {dir}");
let maybe_tx = self.server.app_to_plugin_events_tx.lock().await;
let maybe_tx = self.grpc_service.app_to_plugin_events_tx.lock().await;
let tx = match &*maybe_tx {
None => return Err(ClientNotInitializedErr),
Some(tx) => tx,
@@ -251,9 +246,8 @@ impl PluginManager {
warn!("Failed to remove plugin {} {e:?}", d.dir);
}
}
if let Err(e) = self
.add_plugin_by_dir(window_context.to_owned(), d.dir.as_str(), d.watch)
.await
if let Err(e) =
self.add_plugin_by_dir(window_context.to_owned(), d.dir.as_str(), d.watch).await
{
warn!("Failed to add plugin {} {e:?}", d.dir);
}
@@ -307,21 +301,11 @@ impl PluginManager {
}
pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option<PluginHandle> {
self.plugins
.lock()
.await
.iter()
.find(|p| p.ref_id == ref_id)
.cloned()
self.plugins.lock().await.iter().find(|p| p.ref_id == ref_id).cloned()
}
pub async fn get_plugin_by_dir(&self, dir: &str) -> Option<PluginHandle> {
self.plugins
.lock()
.await
.iter()
.find(|p| p.dir == dir)
.cloned()
self.plugins.lock().await.iter().find(|p| p.dir == dir).cloned()
}
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
@@ -340,9 +324,8 @@ impl PluginManager {
plugin: &PluginHandle,
payload: &InternalEventPayload,
) -> Result<InternalEvent> {
let events = self
.send_to_plugins_and_wait(window_context, payload, vec![plugin.to_owned()])
.await?;
let events =
self.send_to_plugins_and_wait(window_context, payload, vec![plugin.to_owned()]).await?;
Ok(events.first().unwrap().to_owned())
}
@@ -352,8 +335,7 @@ impl PluginManager {
payload: &InternalEventPayload,
) -> Result<Vec<InternalEvent>> {
let plugins = { self.plugins.lock().await.clone() };
self.send_to_plugins_and_wait(window_context, payload, plugins)
.await
self.send_to_plugins_and_wait(window_context, payload, plugins).await
}
async fn send_to_plugins_and_wait(
@@ -440,8 +422,7 @@ impl PluginManager {
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
self.get_template_functions_with_context(WindowContext::from_window(window))
.await
self.get_template_functions_with_context(WindowContext::from_window(window)).await
}
pub async fn get_template_functions_with_context(
@@ -449,10 +430,7 @@ impl PluginManager {
window_context: WindowContext,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self
.send_and_wait(
window_context,
&InternalEventPayload::GetTemplateFunctionsRequest,
)
.send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest)
.await?;
let mut all_actions = Vec::new();
@@ -471,10 +449,8 @@ impl PluginManager {
req: CallHttpRequestActionRequest,
) -> Result<()> {
let ref_id = req.plugin_ref_id.clone();
let plugin = self
.get_plugin_by_ref_id(ref_id.as_str())
.await
.ok_or(PluginNotFoundErr(ref_id))?;
let plugin =
self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?;
let event = plugin.build_event_to_send(
WindowContext::from_window(window),
&InternalEventPayload::CallHttpRequestActionRequest(req),
@@ -500,10 +476,7 @@ impl PluginManager {
};
let events = self
.send_and_wait(
window_context,
&InternalEventPayload::CallTemplateFunctionRequest(req),
)
.send_and_wait(window_context, &InternalEventPayload::CallTemplateFunctionRequest(req))
.await?;
let value = events.into_iter().find_map(|e| match e.payload {
@@ -537,9 +510,7 @@ impl PluginManager {
});
match result {
None => Err(PluginErr(
"No importers found for file contents".to_string(),
)),
None => Err(PluginErr("No importers found for file contents".to_string())),
Some((resp, ref_id)) => {
let plugin = self
.get_plugin_by_ref_id(ref_id.as_str())
@@ -613,14 +584,10 @@ fn fix_windows_paths(p: &PathBuf) -> String {
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
// 2. Remove the drive letter
let safe_path = Regex::new("^[a-zA-Z]:")
.unwrap()
.replace(safe_path.as_str(), "");
let safe_path = Regex::new("^[a-zA-Z]:").unwrap().replace(safe_path.as_str(), "");
// 3. Convert backslashes to forward
let safe_path = PathBuf::from(safe_path.to_string())
.to_slash_lossy()
.to_string();
let safe_path = PathBuf::from(safe_path.to_string()).to_slash_lossy().to_string();
safe_path
}

View File

@@ -57,9 +57,7 @@ impl PluginRuntime for PluginRuntimeServerImpl {
let plugin_to_app_events_tx = self.plugin_to_app_events_tx.clone();
let client_disconnect_tx = self.client_disconnect_tx.clone();
self.client_connect_tx
.send(true)
.expect("Failed to send client ready event");
self.client_connect_tx.send(true).expect("Failed to send client ready event");
tokio::spawn(async move {
while let Some(result) = in_stream.next().await {
@@ -96,8 +94,6 @@ impl PluginRuntime for PluginRuntimeServerImpl {
// Write the same data that was received
let out_stream = ReceiverStream::new(to_plugin_rx);
Ok(Response::new(
Box::pin(out_stream) as Self::EventStreamStream
))
Ok(Response::new(Box::pin(out_stream) as Self::EventStreamStream))
}
}

View File

@@ -2,8 +2,8 @@
name = "yaak_sse"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
ts-rs = { version = "10.0.0", features = ["serde-json-impl"] }

View File

@@ -2,10 +2,10 @@
name = "yaak_templates"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
log = "0.4.22"
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
ts-rs = { version = "10.0.0" }
tokio = { version = "1.39.3", features = ["macros", "rt"] }

View File

@@ -1,4 +1 @@
export * from './bindings/parser';
export type COOL = {
bar: string;
};

View File

@@ -1,5 +1,5 @@
{
"name": "@yaakapp-internal/template",
"name": "@yaakapp-internal/templates",
"private": true,
"version": "1.0.0",
"main": "index.ts"

View File

@@ -44,11 +44,11 @@ const router = createBrowserRouter([
element: <RedirectLegacyEnvironmentURLs />,
},
{
path: paths.workspaceSettings({
workspaceId: ':workspaceId',
environmentId: null,
cookieJarId: null,
}),
path: paths
.workspaceSettings({
workspaceId: ':workspaceId',
})
.replace(/\?.*/, ''),
element: <LazySettings />,
},
],

View File

@@ -2,6 +2,7 @@ import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import React from 'react';
import { useSettings } from '../hooks/useSettings';
import { useOsInfo } from '../hooks/useOsInfo';
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
import { WINDOW_CONTROLS_WIDTH, WindowControls } from './WindowControls';
@@ -23,6 +24,7 @@ export function HeaderSize({
onlyXWindowControl,
children,
}: HeaderSizeProps) {
const osInfo = useOsInfo();
const settings = useSettings();
const stoplightsVisible = useStoplightsVisible();
return (
@@ -31,10 +33,10 @@ export function HeaderSize({
style={{
...style,
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
paddingLeft: stoplightsVisible ? 72 / settings.interfaceScale : undefined,
paddingLeft: (stoplightsVisible && !ignoreControlsSpacing) ? 72 / settings.interfaceScale : undefined,
...(size === 'md' ? { height: HEADER_SIZE_MD } : {}),
...(size === 'lg' ? { height: HEADER_SIZE_LG } : {}),
...(stoplightsVisible || ignoreControlsSpacing
...(osInfo.osType === 'macos' || ignoreControlsSpacing
? { paddingRight: '2px' }
: { paddingLeft: '2px', paddingRight: WINDOW_CONTROLS_WIDTH }),
}}

View File

@@ -28,9 +28,10 @@ export function ImportCurlButton() {
transition={{ delay: 0.5 }}
>
<Button
size="xs"
size="2xs"
variant="border"
color="primary"
color="success"
className="rounded-full"
leftSlot={<Icon icon="paste" size="sm" />}
isLoading={isLoading}
onClick={async () => {

View File

@@ -0,0 +1,42 @@
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
import { useLicense } from '@yaakapp-internal/license';
import { useOpenSettings } from '../hooks/useOpenSettings';
import { Button } from './core/Button';
import { SettingsTab } from './Settings/Settings';
const labels: Record<LicenseCheckStatus['type'], string | null> = {
commercial_use: null,
personal_use: 'Personal Use',
trial_ended: 'Personal Use',
trialing: 'Active Trial',
};
export function LicenseBadge() {
const openSettings = useOpenSettings(SettingsTab.License);
const { check } = useLicense();
if (check.data == null) {
return null;
}
const label = labels[check.data.type];
if (label == null) {
return null;
}
return (
<Button
size="2xs"
variant="border"
className="!rounded-full mx-1"
onClick={() => openSettings.mutate()}
color={
check.data.type == 'trial_ended' || check.data.type === 'personal_use'
? 'primary'
: 'success'
}
>
{label}
</Button>
);
}

View File

@@ -1,6 +1,7 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import classNames from 'classnames';
import React, { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useKeyPressEvent } from 'react-use';
import { useOsInfo } from '../../hooks/useOsInfo';
import { capitalize } from '../../lib/capitalize';
@@ -9,25 +10,34 @@ import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsLicense } from './SettingsLicense';
import { SettingsPlugins } from './SettingsPlugins';
import {SettingsProxy} from "./SettingsProxy";
import { SettingsProxy } from './SettingsProxy';
interface Props {
hide?: () => void;
}
enum Tab {
export enum SettingsTab {
General = 'general',
Proxy = 'proxy',
Appearance = 'appearance',
Plugins = 'plugins',
License = 'license',
}
const tabs = [Tab.General, Tab.Appearance, Tab.Proxy, Tab.Plugins];
const tabs = [
SettingsTab.General,
SettingsTab.Appearance,
SettingsTab.Proxy,
SettingsTab.Plugins,
SettingsTab.License,
];
export default function Settings({ hide }: Props) {
const osInfo = useOsInfo();
const [tab, setTab] = useState<string>(Tab.General);
const [params] = useSearchParams();
const [tab, setTab] = useState<string>(params.get('tab') ?? SettingsTab.General);
// Close settings window on escape
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
@@ -71,18 +81,21 @@ export default function Settings({ hide }: Props) {
onChangeValue={setTab}
tabs={tabs.map((value) => ({ value, label: capitalize(value) }))}
>
<TabContent value={Tab.General} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={SettingsTab.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">
<TabContent value={SettingsTab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
</TabContent>
<TabContent value={Tab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={SettingsTab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
<SettingsPlugins />
</TabContent>
<TabContent value={Tab.Proxy} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={SettingsTab.Proxy} className="pt-3 overflow-y-auto h-full px-4">
<SettingsProxy />
</TabContent>
<TabContent value={SettingsTab.License} className="pt-3 overflow-y-auto h-full px-4">
<SettingsLicense />
</TabContent>
</Tabs>
</div>
);

View File

@@ -0,0 +1,147 @@
import { open } from '@tauri-apps/plugin-shell';
import { useLicense } from '@yaakapp-internal/license';
import classNames from 'classnames';
import { format, formatDistanceToNow } from 'date-fns';
import React, { useState } from 'react';
import { useCopy } from '../../hooks/useCopy';
import { useSettings } from '../../hooks/useSettings';
import { useTimedBoolean } from '../../hooks/useTimedBoolean';
import { useToggle } from '../../hooks/useToggle';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode';
import { Link } from '../core/Link';
import { PlainInput } from '../core/PlainInput';
import { HStack, VStack } from '../core/Stacks';
export function SettingsLicense() {
const { check, activate } = useLicense();
const [key, setKey] = useState<string>('');
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);
const settings = useSettings();
const specialAnnouncement =
settings.createdAt < '2024-12-03' && check.data?.type !== 'commercial_use';
const [copied, setCopied] = useTimedBoolean();
const copy = useCopy({ disableToast: true });
return (
<div className="flex flex-col gap-6">
{check.data?.type === 'personal_use' && <Banner color="info">You&apos;re</Banner>}
{check.data?.type === 'commercial_use' && (
<Banner color="success">
<strong>License active!</strong> Enjoy using Yaak for commercial use.
</Banner>
)}
{check.data?.type === 'trialing' && (
<Banner color="success">
<strong>Your trial ends in {formatDistanceToNow(check.data.end)}</strong>. If you&apos;re
using Yaak for commercial use, please purchase a commercial use license.
</Banner>
)}
{check.data?.type === 'trial_ended' && !specialAnnouncement && (
<Banner color="primary">
<strong>Your trial ended on {format(check.data.end, 'MMMM dd, yyyy')}</strong>. A
commercial-use license is required if you use Yaak within a for-profit organization of two
or more people.
</Banner>
)}
{check.error && <Banner color="danger">{check.error}</Banner>}
{activate.error && <Banner color="danger">{activate.error}</Banner>}
{specialAnnouncement && (
<VStack className="max-w-lg" space={4}>
<p>
<strong>Thank you for being an early supporter of Yaak!</strong>
</p>
<p>
To support the ongoing development of the best local-first API client, Yaak now requires
a paid license for the commercial use of prebuilt binaries (personal use and running the
open-source code remains free.)
</p>
<p>
For details, see the{' '}
<Link href="https://yaak.app/blog/commercial-use">Announcement Post</Link>.
</p>
<p>
As a thank-you, enter code{' '}
<button
title="Copy coupon code"
className="hover:text-notice"
onClick={() => {
setCopied();
copy('EARLYAAK');
}}
>
<InlineCode className="inline-flex items-center gap-1">
EARLYAAK{' '}
<Icon
icon={copied ? 'check' : 'copy'}
size="xs"
className={classNames(copied && 'text-success')}
/>
</InlineCode>
</button>{' '}
at checkout for 50% off your first year of the individual plan.
</p>
<p>~ Greg</p>
</VStack>
)}
{check.data?.type === 'commercial_use' ? (
<HStack space={2}>
<Button variant="border" color="secondary" size="sm" onClick={toggleActivateFormVisible}>
Activate Another License
</Button>
<Button
color="secondary"
size="sm"
onClick={() => open('https://yaak.app/dashboard')}
rightSlot={<Icon icon="external_link" />}
>
Direct Support
</Button>
</HStack>
) : (
<HStack space={2}>
<Button
color="secondary"
size="sm"
onClick={() => open('https://yaak.app/pricing')}
rightSlot={<Icon icon="external_link" />}
>
Purchase
</Button>
<Button color="primary" size="sm" onClick={toggleActivateFormVisible}>
Activate License
</Button>
</HStack>
)}
{activateFormVisible && (
<VStack
as="form"
space={3}
className="max-w-sm"
onSubmit={async (e) => {
e.preventDefault();
toggleActivateFormVisible();
activate.mutate({ licenseKey: key });
}}
>
<PlainInput
autoFocus
label="License Key"
name="key"
onChange={setKey}
placeholder="YK1-XXXXX-XXXXX-XXXXX-XXXXX"
/>
<Button type="submit" color="primary" size="sm" isLoading={activate.isPending}>
Submit
</Button>
</VStack>
)}
</div>
);
}

View File

@@ -8,7 +8,7 @@ import type {
TemplateFunctionSelectArg,
TemplateFunctionTextArg,
} from '@yaakapp-internal/plugin';
import type { FnArg, Tokens } from '@yaakapp-internal/template';
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
import classNames from 'classnames';
import { useCallback, useMemo, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';

View File

@@ -1,4 +1,4 @@
import type { Tokens } from '@yaakapp-internal/template';
import type { Tokens } from '@yaakapp-internal/templates';
import { useCallback, useMemo, useState } from 'react';
import { useActiveEnvironmentVariables } from '../hooks/useActiveEnvironmentVariables';
import { useRenderTemplate } from '../hooks/useRenderTemplate';

View File

@@ -1,21 +1,24 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import classNames from 'classnames';
import React, { useState } from 'react';
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
import { useOsInfo } from '../hooks/useOsInfo';
import { Button } from './core/Button';
import { HStack } from './core/Stacks';
interface Props {
className?: string;
onlyX?: boolean;
macos?: boolean;
}
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
export function WindowControls({ className, onlyX }: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
const stoplightsVisible = useStoplightsVisible();
if (stoplightsVisible) {
const osInfo = useOsInfo();
// Never show controls on macOS
if (osInfo.osType === 'macos') {
return null;
}

View File

@@ -7,6 +7,7 @@ import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
import { ImportCurlButton } from './ImportCurlButton';
import { LicenseBadge } from './LicenseBadge';
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
import { SettingsDropdown } from './SettingsDropdown';
import { SidebarActions } from './SidebarActions';
@@ -22,7 +23,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
<div
className={classNames(
className,
'grid grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center w-full h-full',
'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center w-full h-full',
)}
>
<HStack space={0.5} className="flex-1 pointer-events-none">
@@ -34,10 +35,11 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
<EnvironmentActionsDropdown className="w-auto pointer-events-auto" />
</HStack>
</HStack>
<div className="pointer-events-none w-full max-w-[30vw] mx-auto">
<div className="pointer-events-none w-full max-w-[30vw] mx-auto flex justify-center">
<RecentRequestsDropdown />
</div>
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-0.5">
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
<LicenseBadge />
<ImportCurlButton />
<IconButton
icon="search"

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
interface Props {
children: ReactNode;
className?: string;
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger';
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
}
export function Banner({ children, className, color = 'secondary' }: Props) {

View File

@@ -73,7 +73,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
size === 'md' && 'h-md px-3 rounded-md',
size === 'sm' && 'h-sm px-2.5 rounded-md',
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
size === '2xs' && 'h-2xs px-1 text-xs rounded',
size === '2xs' && 'h-2xs px-2 text-xs rounded',
// Solids
variant === 'solid' && 'border-transparent',

View File

@@ -10,6 +10,7 @@ import { json } from '@codemirror/lang-json';
import { xml } from '@codemirror/lang-xml';
import type { LanguageSupport } from '@codemirror/language';
import {
codeFolding,
foldGutter,
foldKeymap,
HighlightStyle,
@@ -150,6 +151,49 @@ export const multiLineExtensions = [
return el;
},
}),
codeFolding({
placeholderDOM(view, onclick, prepared) {
const el = document.createElement('span');
el.onclick = onclick;
el.className = 'cm-foldPlaceholder';
el.innerText = prepared;
el.title = 'unfold';
el.ariaLabel = 'folded code';
return el;
},
preparePlaceholder(state, range) {
let count: number | undefined;
let startToken = '{';
let endToken = '}';
const prevLine = state.doc.lineAt(range.from).text;
const isArray = prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{');
if (isArray) {
startToken = '[';
endToken = ']';
}
const internal = state.sliceDoc(range.from, range.to);
const toParse = startToken + internal + endToken;
try {
const parsed = JSON.parse(toParse);
count = Object.keys(parsed).length;
} catch {
/* empty */
}
if (count !== undefined) {
const label = isArray ? 'item' : 'prop';
const plural = count === 1 ? '' : 's';
return `${count} ${label}${plural}`;
}
return '…';
},
}),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
closeBrackets(),

View File

@@ -18,11 +18,11 @@ export function Link({ href, children, className, ...other }: Props) {
href={href}
target="_blank"
rel="noopener noreferrer"
className={classNames(className, 'pr-4')}
className={classNames(className, 'pr-4 inline-flex items-center')}
{...other}
>
<span className="underline">{children}</span>
<Icon className="inline absolute right-0.5 top-0.5" size="xs" icon="external_link" />
<Icon className="inline absolute right-0.5 top-1.5" size="xs" icon="external_link" />
</a>
);
}

View File

@@ -8,7 +8,7 @@ export interface AppInfo {
appLogDir: string;
}
const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
export const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
export function useAppInfo() {
return appInfo;

View File

@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { SettingsTab } from '../components/Settings/Settings';
import { QUERY_COOKIE_JAR_ID } from './useActiveCookieJar';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironment';
@@ -13,12 +14,18 @@ export type RouteParamsRequest = RouteParamsWorkspace & {
requestId: string;
};
export type RouteParamsSettings = {
workspaceId: string;
tab?: SettingsTab;
};
export const paths = {
workspaces() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
workspaces(_ = {}) {
return '/workspaces';
},
workspaceSettings({ workspaceId } = { workspaceId: ':workspaceId' } as RouteParamsWorkspace) {
return `/workspaces/${workspaceId}/settings`;
workspaceSettings({ workspaceId, tab } = { workspaceId: ':workspaceId' } as RouteParamsSettings) {
return `/workspaces/${workspaceId}/settings?tab=${tab ?? SettingsTab.General}`;
},
workspace(
{ workspaceId, environmentId, cookieJarId } = {

View File

@@ -19,7 +19,7 @@ export function useKeyValue<T extends object | boolean | number | string | null>
key,
fallback,
}: {
namespace?: 'global' | 'no_sync';
namespace?: 'global' | 'no_sync' | 'license';
key: string | string[];
fallback: T;
}) {

View File

@@ -1,9 +1,10 @@
import { useMutation } from '@tanstack/react-query';
import type { SettingsTab } from '../components/Settings/Settings';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
export function useOpenSettings() {
export function useOpenSettings(tab?: SettingsTab) {
const routes = useAppRoutes();
const workspace = useActiveWorkspace();
return useMutation({
@@ -12,11 +13,7 @@ export function useOpenSettings() {
if (workspace == null) return;
await invokeCmd('cmd_new_child_window', {
url: routes.paths.workspaceSettings({
workspaceId: workspace.id,
cookieJarId: null,
environmentId: null,
}),
url: routes.paths.workspaceSettings({ workspaceId: workspace.id, tab }),
label: 'settings',
title: 'Yaak Settings',
innerSize: [600, 550],

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import type { Tokens } from '@yaakapp-internal/template';
import type { Tokens } from '@yaakapp-internal/templates';
import { invokeCmd } from '../lib/tauri';
export function useParseTemplate(template: string) {

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import type { Tokens } from '@yaakapp-internal/template';
import type { Tokens } from '@yaakapp-internal/templates';
import { invokeCmd } from '../lib/tauri';
export function useTemplateTokensToString(tokens: Tokens) {

View File

@@ -1,9 +1,12 @@
import type { Appearance } from './appearance';
import { resolveAppearance } from './appearance';
import { catppuccin } from './themes/catppuccin';
import { dracula } from './themes/dracula';
import { github } from './themes/github';
import { hotdogStand } from './themes/hotdog-stand';
import { monokaiPro } from './themes/monokai-pro';
import { nord } from './themes/nord';
import { moonlight } from './themes/moonlight';
import { relaxing } from './themes/relaxing';
import { rosePine } from './themes/rose-pine';
import { yaak, yaakDark, yaakLight } from './themes/yaak';
@@ -15,10 +18,13 @@ export const defaultLightTheme = yaakLight;
const allThemes = [
...yaak,
...catppuccin,
...dracula,
...relaxing,
...rosePine,
...github,
...monokaiPro,
...nord,
...moonlight,
...hotdogStand,
];

View File

@@ -0,0 +1,30 @@
import type { YaakTheme } from '../window';
import { YaakColor } from '../yaakColor';
const nordDefault: YaakTheme = {
id: 'nord',
name: 'Nord',
surface: new YaakColor('hsl(220, 16%, 22%)', 'dark'), // Nord0 (#2e3440)
surfaceHighlight: new YaakColor('hsl(220, 14%, 28%)', 'dark'), // Nord1 (#3b4252)
text: new YaakColor('hsl(220, 28%, 93%)', 'dark'), // Nord6 (#ECEFF4)
textSubtle: new YaakColor('hsl(220, 26%, 90%)', 'dark'), // Nord5 (#E5E9F0)
textSubtlest: new YaakColor('hsl(220, 24%, 86%)', 'dark'), // Nord4 (#D8DEE9)
primary: new YaakColor('hsl(193, 38%, 68%)', 'dark'), // Nord8 (#88C0D0)
secondary: new YaakColor('hsl(210, 34%, 63%)', 'dark'), // Nord9 (#81A1C1)
info: new YaakColor('hsl(174, 25%, 69%)', 'dark'), // Nord7 (#8FBCBB)
success: new YaakColor('hsl(89, 26%, 66%)', 'dark'), // Nord14 (#A3BE8C)
notice: new YaakColor('hsl(40, 66%, 73%)', 'dark'), // Nord13 (#EBCB8B)
warning: new YaakColor('hsl(17, 48%, 64%)', 'dark'), // Nord12 (#D08770)
danger: new YaakColor('hsl(353, 43%, 56%)', 'dark'), // Nord11 (#BF616A)
components: {
sidebar: {
backdrop: new YaakColor('hsl(220, 16%, 22%)', 'dark'), // Nord0 (#2e3440)
},
appHeader: {
backdrop: new YaakColor('hsl(220, 14%, 28%)', 'dark'), // Nord1 (#3b4252)
},
},
};
export const nord = [nordDefault];

View File

@@ -1,7 +1,7 @@
const plugin = require('tailwindcss/plugin');
const sizes = {
'2xs': '1.25rem',
'2xs': '1.4rem',
xs: '1.8rem',
sm: '2.0rem',
md: '2.5rem',
@@ -58,6 +58,7 @@ module.exports = {
xs: '0.8rem',
sm: '0.9rem',
base: '1rem',
lg: '1.12rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '2rem',