mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-19 09:07:49 +01:00
Compare commits
18 Commits
v2024.0.0
...
v2024.0.1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3539642491 | ||
|
|
08abea6a6f | ||
|
|
0045b85f00 | ||
|
|
4b34c3d101 | ||
|
|
4af0a15d9f | ||
|
|
3a4a76c58d | ||
|
|
3086d815c1 | ||
|
|
a48a9eab4a | ||
|
|
48664c66e5 | ||
|
|
7aee5176a9 | ||
|
|
0da68ced18 | ||
|
|
39f7d9c113 | ||
|
|
138943bfb6 | ||
|
|
c1c9f882a6 | ||
|
|
1bcf26f656 | ||
|
|
7c2466da5e | ||
|
|
7dc78a1f6f | ||
|
|
88d024023b |
11
index.html
11
index.html
@@ -5,17 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak App</title>
|
||||
<!-- <script src="http://localhost:8097"></script>-->
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
110
package-lock.json
generated
110
package-lock.json
generated
@@ -24,7 +24,7 @@
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-query-devtools": "^4.28.0",
|
||||
"@tanstack/react-query-persist-client": "^4.28.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.2",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||
"@tauri-apps/cli": "^1.5.4",
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"@types/parse-color": "^1.0.1",
|
||||
@@ -1934,9 +1934,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.1.tgz",
|
||||
"integrity": "sha512-6unsZDOdlXTmauU3NhWhn+Cx0rODV+rvNvTdvolE5Kls5ybA6cqndQENDt1+FS0tF7ozCP66jwWoH6a5h90BrA==",
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.3.tgz",
|
||||
"integrity": "sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==",
|
||||
"engines": {
|
||||
"node": ">= 14.6.0",
|
||||
"npm": ">= 6.6.0",
|
||||
@@ -1948,9 +1948,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.6.tgz",
|
||||
"integrity": "sha512-k4Y19oVCnt7WZb2TnDzLqfs7o98Jq0tUoVMv+JQSzuRDJqaVu2xMBZ8dYplEn+EccdR5SOMyzaLBJWu38TVK1A==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.9.tgz",
|
||||
"integrity": "sha512-knSt/9AvCTeyfC6wkyeouF9hBW/0Mzuw+5vBKEvzaGPQsfFJo1ZCp5FkdiZpGBBfnm09BhugasGRTGofzatfqQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
@@ -1963,22 +1963,22 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "1.5.6",
|
||||
"@tauri-apps/cli-darwin-x64": "1.5.6",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "1.5.6",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "1.5.6",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "1.5.6",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "1.5.6",
|
||||
"@tauri-apps/cli-linux-x64-musl": "1.5.6",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "1.5.6",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "1.5.6",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "1.5.6"
|
||||
"@tauri-apps/cli-darwin-arm64": "1.5.9",
|
||||
"@tauri-apps/cli-darwin-x64": "1.5.9",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "1.5.9",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "1.5.9",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "1.5.9",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "1.5.9",
|
||||
"@tauri-apps/cli-linux-x64-musl": "1.5.9",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "1.5.9",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "1.5.9",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "1.5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.6.tgz",
|
||||
"integrity": "sha512-NNvG3XLtciCMsBahbDNUEvq184VZmOveTGOuy0So2R33b/6FDkuWaSgWZsR1mISpOuP034htQYW0VITCLelfqg==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.9.tgz",
|
||||
"integrity": "sha512-7C2Jf8f0gzv778mLYb7Eszqqv1bm9Wzews81MRTqKrUIcC+eZEtDXLex+JaEkEzFEUrgIafdOvMBVEavF030IA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1992,9 +1992,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.6.tgz",
|
||||
"integrity": "sha512-nkiqmtUQw3N1j4WoVjv81q6zWuZFhBLya/RNGUL94oafORloOZoSY0uTZJAoeieb3Y1YK0rCHSDl02MyV2Fi4A==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.9.tgz",
|
||||
"integrity": "sha512-LHKytpkofPYgH8RShWvwDa3hD1ws131x7g7zNasJPfOiCWLqYVQFUuQVmjEUt8+dpHe/P/err5h4z+YZru2d0A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2008,9 +2008,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.6.tgz",
|
||||
"integrity": "sha512-z6SPx+axZexmWXTIVPNs4Tg7FtvdJl9EKxYN6JPjOmDZcqA13iyqWBQal2DA/GMZ1Xqo3vyJf6EoEaKaliymPQ==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.9.tgz",
|
||||
"integrity": "sha512-teGK20IYKx+dVn8wFq/Lg57Q9ce7foq1KHSfyHi464LVt1T0V1rsmULSgZpQPPj/NYPF5BG78PcWYv64yH86jw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2024,9 +2024,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.6.tgz",
|
||||
"integrity": "sha512-QuQjMQmpsCbzBrmtQiG4uhnfAbdFx3nzm+9LtqjuZlurc12+Mj5MTgqQ3AOwQedH3f7C+KlvbqD2AdXpwTg7VA==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.9.tgz",
|
||||
"integrity": "sha512-onJ/DW5Crw38qVx+wquY4uBbfCxVhzhdJmlCYqnYyXsZZmSiPUfSyhV58y+5TYB0q1hG8eYdB5x8VAwzByhGzw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2040,9 +2040,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.6.tgz",
|
||||
"integrity": "sha512-8j5dH3odweFeom7bRGlfzDApWVOT4jIq8/214Wl+JeiNVehouIBo9lZGeghZBH3XKFRwEvU23i7sRVjuh2s8mg==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.9.tgz",
|
||||
"integrity": "sha512-23AYoLD3acakLp9NtheKQDJl8F66eTOflxoPzdJNRy13hUSxb+W9qpz4rRA+CIzkjICFvO2i3UWjeV9QwDVpsQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2056,9 +2056,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.6.tgz",
|
||||
"integrity": "sha512-gbFHYHfdEGW0ffk8SigDsoXks6USpilF6wR0nqB/JbWzbzFR/sBuLVNQlJl1RKNakyJHu+lsFxGy0fcTdoX8xA==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.9.tgz",
|
||||
"integrity": "sha512-9PQA1rE7gh41W2ylyKd5qOGOds55ymaYPml9KOpM0g+cxmCXa+8Wf9K5NKvACnJldJJ6cekWzIyB4eN6o5T+yQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2072,9 +2072,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.6.tgz",
|
||||
"integrity": "sha512-9v688ogoLkeFYQNgqiSErfhTreLUd8B3prIBSYUt+x4+5Kcw91zWvIh+VSxL1n3KCGGsM7cuXhkGPaxwlEh1ug==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.9.tgz",
|
||||
"integrity": "sha512-5hdbNFeDsrJ/pXZ4cSQV4bJwUXPPxXxN3/pAtNUqIph7q+vLcBXOXIMoS64iuyaluJC59lhEwlWZFz+EPv0Hqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2088,9 +2088,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.6.tgz",
|
||||
"integrity": "sha512-DRNDXFNZb6y5IZrw+lhTTA9l4wbzO4TNRBAlHAiXUrH+pRFZ/ZJtv5WEuAj9ocVSahVw2NaK5Yaold4NPAxHog==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.9.tgz",
|
||||
"integrity": "sha512-O18JufjSB3hSJYu5WWByONouGeX7DraLAtXLErsG1r/VS3zHd/zyuzycrVUaObNXk5bfGlIP0Ypt+RvZJILN2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2104,9 +2104,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.6.tgz",
|
||||
"integrity": "sha512-oUYKNR/IZjF4fsOzRpw0xesl2lOjhsQEyWlgbpT25T83EU113Xgck9UjtI7xemNI/OPCv1tPiaM1e7/ABdg5iA==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.9.tgz",
|
||||
"integrity": "sha512-FQxtxTZu0JVBihfd/lmpxo7jyMOesjWQehfyVUqtgMfm5+Pvvw0Y+ZioeDi1TZkFVrT3QDYy8R4LqDLSZVMQRA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2120,9 +2120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.6.tgz",
|
||||
"integrity": "sha512-RmEf1os9C8//uq2hbjXi7Vgz9ne7798ZxqemAZdUwo1pv3oLVZSz1/IvZmUHPdy2e6zSeySqWu1D0Y3QRNN+dg==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.9.tgz",
|
||||
"integrity": "sha512-EeI1+L518cIBLKw0qUFwnLIySBeSmPQjPLIlNwSukHSro4tAQPHycEVGgKrdToiCWgaZJBA0e5aRSds0Du2TWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -9013,6 +9013,20 @@
|
||||
"@tauri-apps/api": "1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tauri-plugin-log-api/node_modules/@tauri-apps/api": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.1.tgz",
|
||||
"integrity": "sha512-6unsZDOdlXTmauU3NhWhn+Cx0rODV+rvNvTdvolE5Kls5ybA6cqndQENDt1+FS0tF7ozCP66jwWoH6a5h90BrA==",
|
||||
"engines": {
|
||||
"node": ">= 14.6.0",
|
||||
"npm": ">= 6.6.0",
|
||||
"yarn": ">= 1.19.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/term-size": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
|
||||
|
||||
12
package.json
12
package.json
@@ -5,18 +5,18 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "npm run build:plugins && npm run tauri-dev",
|
||||
"tauri-dev": "tauri dev --no-watch --config src-tauri/tauri-dev.conf.json",
|
||||
"tauri-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
|
||||
"tauri-build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"build": "npm run build:frontend",
|
||||
"dev": "vite dev",
|
||||
"lint": "tsc && eslint . --ext .ts,.tsx",
|
||||
"build:icon:release": "tauri icon design/icon.png --output src-tauri/icons/release",
|
||||
"build:icon:dev": "tauri icon design/icon-dev.png --output src-tauri/icons/dev",
|
||||
"build:icon:release": "tauri icon design/icon.png --output ./src-tauri/icons/release",
|
||||
"build:icon:dev": "tauri icon design/icon-dev.png --output ./src-tauri/icons/dev",
|
||||
"build:frontend": "vite build",
|
||||
"build:plugins": "run-p build:plugin:importer-insomnia build:plugin:importer-postman build:plugin:importer-yaak",
|
||||
"build:plugin:importer-insomnia": "cd src-tauri/plugins/importer-insomnia && vite build",
|
||||
"build:plugin:importer-postman": "cd src-tauri/plugins/importer-postman && vite build",
|
||||
"build:plugin:importer-insomnia": "cd ./src-tauri/plugins/importer-insomnia && vite build",
|
||||
"build:plugin:importer-postman": "cd ./src-tauri/plugins/importer-postman && vite build",
|
||||
"build:plugin:importer-yaak": "cd src-tauri/plugins/importer-yaak && vite build",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-query-devtools": "^4.28.0",
|
||||
"@tanstack/react-query-persist-client": "^4.28.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.2",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
|
||||
12
src-tauri/.sqlx/query-09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796.json
generated
Normal file
12
src-tauri/.sqlx/query-09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings SET (\n follow_redirects,\n validate_certificates,\n theme,\n appearance\n ) = (?, ?, ?, ?) WHERE id = 'default';\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO settings (id)\n VALUES ('default')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88"
|
||||
}
|
||||
68
src-tauri/.sqlx/query-f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c.json
generated
Normal file
68
src-tauri/.sqlx/query-f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c.json
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n follow_redirects,\n validate_certificates,\n request_timeout,\n theme,\n appearance\n FROM settings\n WHERE id = 'default'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "follow_redirects",
|
||||
"ordinal": 4,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "validate_certificates",
|
||||
"ordinal": 5,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "request_timeout",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "theme",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "appearance",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c"
|
||||
}
|
||||
154
src-tauri/Cargo.lock
generated
154
src-tauri/Cargo.lock
generated
@@ -81,6 +81,20 @@ version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.15.1"
|
||||
@@ -769,12 +783,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.26"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
|
||||
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -961,11 +975,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2"
|
||||
checksum = "3bde55e389bea6a966bd467ad1ad7da0ae14546a5bc794d16d1e55e7fca44881"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.8",
|
||||
"vswhom",
|
||||
@@ -2013,9 +2028,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3"
|
||||
checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc"
|
||||
dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
@@ -2342,9 +2357,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -2724,6 +2739,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.1.6+3.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.95"
|
||||
@@ -2732,6 +2756,7 @@ checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -2854,9 +2879,7 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
||||
dependencies = [
|
||||
"phf_macros 0.10.0",
|
||||
"phf_shared 0.10.0",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2933,20 +2956,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
|
||||
dependencies = [
|
||||
"phf_generator 0.10.0",
|
||||
"phf_shared 0.10.0",
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.2"
|
||||
@@ -3337,6 +3346,7 @@ version = "0.11.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
@@ -4401,9 +4411,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
|
||||
checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -4530,9 +4540,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.5.0"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34d55e185904a84a419308d523c2c6891d5e2dbcee740c4997eb42e75a7b0f46"
|
||||
checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
@@ -4545,7 +4555,7 @@ dependencies = [
|
||||
"kuchikiki",
|
||||
"log",
|
||||
"memchr",
|
||||
"phf 0.10.1",
|
||||
"phf 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
@@ -4553,10 +4563,10 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"thiserror",
|
||||
"toml 0.5.11",
|
||||
"toml 0.7.8",
|
||||
"url",
|
||||
"walkdir",
|
||||
"windows 0.39.0",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5318,6 +5328,18 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "window-shadows"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ff424735b1ac21293b0492b069394b0a189c8a463fb015a16dea7c2e221c08"
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"objc",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.37.0"
|
||||
@@ -5452,12 +5474,36 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.0",
|
||||
"windows_aarch64_msvc 0.52.0",
|
||||
"windows_i686_gnu 0.52.0",
|
||||
"windows_i686_msvc 0.52.0",
|
||||
"windows_x86_64_gnu 0.52.0",
|
||||
"windows_x86_64_gnullvm 0.52.0",
|
||||
"windows_x86_64_msvc 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-tokens"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -5470,6 +5516,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.37.0"
|
||||
@@ -5494,6 +5546,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.37.0"
|
||||
@@ -5518,6 +5576,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.37.0"
|
||||
@@ -5542,6 +5606,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.37.0"
|
||||
@@ -5566,6 +5636,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -5578,6 +5654,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.37.0"
|
||||
@@ -5602,6 +5684,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.19"
|
||||
@@ -5725,6 +5813,7 @@ dependencies = [
|
||||
"http",
|
||||
"log",
|
||||
"objc",
|
||||
"openssl-sys",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -5736,6 +5825,7 @@ dependencies = [
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"window-shadows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -17,6 +17,9 @@ tauri-build = { version = "1.2", features = [] }
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.25.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = {version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21.0"
|
||||
boa_engine = "0.17.3"
|
||||
@@ -25,22 +28,27 @@ chrono = { version = "0.4.23", features = ["serde"] }
|
||||
futures = "0.3.26"
|
||||
http = "0.2.8"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.11.14", features = ["json", "multipart"] }
|
||||
reqwest = { version = "0.11.14", features = ["json", "multipart", "gzip", "brotli", "deflate"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
|
||||
tauri = { version = "1.3", features = [
|
||||
tauri = { version = "1.5.2", features = [
|
||||
"config-toml",
|
||||
"devtools",
|
||||
"dialog-open",
|
||||
"dialog-save",
|
||||
"fs-read-file",
|
||||
"os-all",
|
||||
"protocol-asset",
|
||||
"shell-open",
|
||||
"updater",
|
||||
"window-start-dragging",
|
||||
"dialog-open",
|
||||
"dialog-save",
|
||||
"window-close",
|
||||
"window-maximize",
|
||||
"window-minimize",
|
||||
"window-set-decorations",
|
||||
"window-set-title",
|
||||
"window-start-dragging",
|
||||
"window-unmaximize",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] }
|
||||
@@ -48,6 +56,7 @@ tokio = { version = "1.25.0", features = ["sync"] }
|
||||
uuid = "1.3.0"
|
||||
log = "0.4.20"
|
||||
datetime = "0.5.2"
|
||||
window-shadows = "0.2.2"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
13
src-tauri/migrations/20240111221224_settings.sql
Normal file
13
src-tauri/migrations/20240111221224_settings.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE settings
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'settings' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
follow_redirects BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
validate_certificates BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
request_timeout INTEGER DEFAULT 0 NOT NULL,
|
||||
theme TEXT DEFAULT 'default' NOT NULL,
|
||||
appearance TEXT DEFAULT 'system' NOT NULL
|
||||
);
|
||||
@@ -1,89 +1,109 @@
|
||||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::JsonValue;
|
||||
use tauri::{async_runtime, AppHandle, Manager};
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
// serializable
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsResource {
|
||||
App,
|
||||
// Workspace,
|
||||
// Environment,
|
||||
// Folder,
|
||||
// HttpRequest,
|
||||
// HttpResponse,
|
||||
Workspace,
|
||||
Environment,
|
||||
Folder,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsAction {
|
||||
Launch,
|
||||
// Create,
|
||||
// Update,
|
||||
// Upsert,
|
||||
// Delete,
|
||||
// Send,
|
||||
// Duplicate,
|
||||
Create,
|
||||
Update,
|
||||
Upsert,
|
||||
Delete,
|
||||
DeleteMany,
|
||||
Send,
|
||||
Duplicate,
|
||||
}
|
||||
|
||||
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||
match resource {
|
||||
AnalyticsResource::App => "app",
|
||||
// AnalyticsResource::Workspace => "workspace",
|
||||
// AnalyticsResource::Environment => "environment",
|
||||
// AnalyticsResource::Folder => "folder",
|
||||
// AnalyticsResource::HttpRequest => "http_request",
|
||||
// AnalyticsResource::HttpResponse => "http_response",
|
||||
AnalyticsResource::Workspace => "workspace",
|
||||
AnalyticsResource::Environment => "environment",
|
||||
AnalyticsResource::Folder => "folder",
|
||||
AnalyticsResource::HttpRequest => "http_request",
|
||||
AnalyticsResource::HttpResponse => "http_response",
|
||||
}
|
||||
}
|
||||
|
||||
fn action_name(action: AnalyticsAction) -> &'static str {
|
||||
match action {
|
||||
AnalyticsAction::Launch => "launch",
|
||||
// AnalyticsAction::Create => "create",
|
||||
// AnalyticsAction::Update => "update",
|
||||
// AnalyticsAction::Upsert => "upsert",
|
||||
// AnalyticsAction::Delete => "delete",
|
||||
// AnalyticsAction::Send => "send",
|
||||
// AnalyticsAction::Duplicate => "duplicate",
|
||||
AnalyticsAction::Create => "create",
|
||||
AnalyticsAction::Update => "update",
|
||||
AnalyticsAction::Upsert => "upsert",
|
||||
AnalyticsAction::Delete => "delete",
|
||||
AnalyticsAction::DeleteMany => "delete_many",
|
||||
AnalyticsAction::Send => "send",
|
||||
AnalyticsAction::Duplicate => "duplicate",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_event(
|
||||
pub fn track_event_blocking(
|
||||
app_handle: &AppHandle,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
async_runtime::block_on(async move {
|
||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", "site_zOK0d7jeBy2TLxFCnZ".to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(app_handle)),
|
||||
];
|
||||
let url = "https://t.yaak.app/t/e".to_string();
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.get(&url)
|
||||
.query(¶ms);
|
||||
track_event(app_handle, resource, action, attributes).await;
|
||||
});
|
||||
}
|
||||
|
||||
if is_dev() {
|
||||
debug!("Send event (dev): {}", event);
|
||||
} else if let Err(e) = req.send().await {
|
||||
warn!(
|
||||
pub async fn track_event(
|
||||
app_handle: &AppHandle,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let site = match is_dev() {
|
||||
true => "site_TkHWjoXwZPq3HfhERb",
|
||||
false => "site_zOK0d7jeBy2TLxFCnZ",
|
||||
};
|
||||
let base_url = match is_dev() {
|
||||
true => "http://localhost:7194",
|
||||
false => "https://t.yaak.app"
|
||||
};
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", site.to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(app_handle)),
|
||||
];
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.get(format!("{base_url}/t/e"))
|
||||
.query(¶ms);
|
||||
|
||||
if let Err(e) = req.send().await {
|
||||
warn!(
|
||||
"Error sending analytics event: {} {} {:?}",
|
||||
e, event, params
|
||||
);
|
||||
} else {
|
||||
debug!("Send event: {}: {:?}", event, params);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug!("Send event: {}: {:?}", event, params);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_os() -> &'static str {
|
||||
|
||||
@@ -16,20 +16,22 @@ use fern::colors::ColoredLevelConfig;
|
||||
use log::{debug, info, warn};
|
||||
use rand::random;
|
||||
use serde::Serialize;
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
use serde_json::Value;
|
||||
use sqlx::migrate::Migrator;
|
||||
use sqlx::types::Json;
|
||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri_plugin_log::{fern, LogTarget};
|
||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||
use tokio::sync::Mutex;
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
use window_ext::TrafficLightWindowExt;
|
||||
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event};
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||
use crate::plugin::{ImportResources, ImportResult};
|
||||
use crate::send::actually_send_request;
|
||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||
@@ -67,7 +69,7 @@ async fn migrate_db(
|
||||
info!("Running migrations at {}", p.to_string_lossy());
|
||||
let m = Migrator::new(p).await.expect("Failed to load migrations");
|
||||
m.run(pool).await.expect("Failed to run migrations");
|
||||
info!("Migrations complete");
|
||||
info!("Migrations complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -224,6 +226,17 @@ async fn response_err(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn track_event(
|
||||
window: Window<Wry>,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<Value>,
|
||||
) -> Result<(), String> {
|
||||
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_update_mode(
|
||||
update_mode: &str,
|
||||
@@ -504,6 +517,31 @@ async fn list_environments(
|
||||
Ok(environments)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_settings(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::get_or_create_settings(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_settings(
|
||||
settings: models::Settings,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_settings = models::update_settings(pool, settings)
|
||||
.await
|
||||
.expect("Failed to update settings");
|
||||
|
||||
emit_and_return(&window, "updated_model", updated_settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_folder(
|
||||
id: &str,
|
||||
@@ -659,9 +697,16 @@ fn main() {
|
||||
)
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.setup(|app| {
|
||||
let app_data_dir = app.path_resolver().app_data_dir().unwrap();
|
||||
let app_config_dir = app.path_resolver().app_config_dir().unwrap();
|
||||
info!(
|
||||
"App Config Dir: {}",
|
||||
app_config_dir.as_path().to_string_lossy(),
|
||||
);
|
||||
info!("App Data Dir: {}", app_data_dir.as_path().to_string_lossy(),);
|
||||
let dir = match is_dev() {
|
||||
true => current_dir().unwrap(),
|
||||
false => app.path_resolver().app_data_dir().unwrap(),
|
||||
false => app_data_dir,
|
||||
};
|
||||
|
||||
create_dir_all(dir.clone()).expect("Problem creating App directory!");
|
||||
@@ -714,6 +759,7 @@ fn main() {
|
||||
get_environment,
|
||||
get_folder,
|
||||
get_request,
|
||||
get_settings,
|
||||
get_workspace,
|
||||
import_data,
|
||||
list_environments,
|
||||
@@ -726,9 +772,11 @@ fn main() {
|
||||
send_request,
|
||||
set_key_value,
|
||||
set_update_mode,
|
||||
track_event,
|
||||
update_environment,
|
||||
update_folder,
|
||||
update_request,
|
||||
update_settings,
|
||||
update_workspace,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
@@ -762,7 +810,7 @@ fn main() {
|
||||
w.restore_state(StateFlags::all())
|
||||
.expect("Failed to restore window state");
|
||||
|
||||
track_event(
|
||||
analytics::track_event_blocking(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
@@ -790,10 +838,12 @@ fn main() {
|
||||
}
|
||||
|
||||
fn is_dev() -> bool {
|
||||
#[cfg(dev)] {
|
||||
#[cfg(dev)]
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#[cfg(not(dev))] {
|
||||
#[cfg(not(dev))]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -826,8 +876,19 @@ fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
.title_bar_style(TitleBarStyle::Overlay);
|
||||
}
|
||||
|
||||
// Add non-MacOS things
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// Doesn't seem to work from Rust, here, so we do it in JS
|
||||
win_builder = win_builder.decorations(false);
|
||||
}
|
||||
|
||||
let win = win_builder.build().expect("failed to build window");
|
||||
|
||||
// Tauri doesn't support shadows when hiding decorations, so we add our own
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
set_shadow(&win, true).unwrap();
|
||||
|
||||
let win2 = win.clone();
|
||||
let handle2 = handle.clone();
|
||||
win.on_menu_event(move |event| match event.menu_item_id() {
|
||||
|
||||
@@ -3,11 +3,27 @@ use std::fs;
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Settings {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
// Settings
|
||||
pub validate_certificates: bool,
|
||||
pub follow_redirects: bool,
|
||||
pub theme: String,
|
||||
pub appearance: String,
|
||||
pub request_timeout: i64,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Workspace {
|
||||
@@ -192,7 +208,11 @@ pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> O
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn get_key_value_string(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<String> {
|
||||
pub async fn get_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Option<String> {
|
||||
let kv = get_key_value(namespace, key, pool).await?;
|
||||
let result = serde_json::from_str(&kv.value);
|
||||
match result {
|
||||
@@ -283,6 +303,68 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
async fn get_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Settings,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
follow_redirects,
|
||||
validate_certificates,
|
||||
request_timeout,
|
||||
theme,
|
||||
appearance
|
||||
FROM settings
|
||||
WHERE id = 'default'
|
||||
"#,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
let existing = get_settings(pool).await;
|
||||
if let Ok(s) = existing {
|
||||
Ok(s)
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO settings (id)
|
||||
VALUES ('default')
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_settings(
|
||||
pool: &Pool<Sqlite>,
|
||||
settings: Settings,
|
||||
) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE settings SET (
|
||||
follow_redirects,
|
||||
validate_certificates,
|
||||
theme,
|
||||
appearance
|
||||
) = (?, ?, ?, ?) WHERE id = 'default';
|
||||
"#,
|
||||
settings.follow_redirects,
|
||||
settings.validate_certificates,
|
||||
settings.theme,
|
||||
settings.appearance,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
}
|
||||
|
||||
pub async fn upsert_environment(
|
||||
pool: &Pool<Sqlite>,
|
||||
environment: Environment,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::fs;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use log::warn;
|
||||
use reqwest::multipart;
|
||||
use reqwest::redirect::Policy;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::Json;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::{AppHandle, Wry};
|
||||
|
||||
use crate::{emit_side_effect, models, render, response_err};
|
||||
@@ -34,11 +35,31 @@ pub async fn actually_send_request(
|
||||
url_string = format!("http://{}", url_string);
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
// .danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
let settings = models::get_or_create_settings(pool)
|
||||
.await
|
||||
.expect("Failed to get settings");
|
||||
|
||||
let mut client_builder = reqwest::Client::builder()
|
||||
.redirect(match settings.follow_redirects {
|
||||
true => Policy::limited(10), // TODO: Handle redirects natively
|
||||
false => Policy::none(),
|
||||
})
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.deflate(true)
|
||||
.referer(false)
|
||||
.danger_accept_invalid_certs(!settings.validate_certificates)
|
||||
.connection_verbose(true) // TODO: Capture this log somehow
|
||||
.tls_info(true);
|
||||
|
||||
if settings.request_timeout > 0 {
|
||||
client_builder = client_builder.timeout(Duration::from_millis(
|
||||
settings.request_timeout.unsigned_abs(),
|
||||
));
|
||||
}
|
||||
|
||||
// .use_rustls_tls() // TODO: Make this configurable (maybe)
|
||||
let client = client_builder.build().expect("Failed to build client");
|
||||
|
||||
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
|
||||
.expect("Failed to create method");
|
||||
@@ -114,7 +135,9 @@ pub async fn actually_send_request(
|
||||
|
||||
let mut query_params = Vec::new();
|
||||
for p in request.url_parameters.0 {
|
||||
if !p.enabled || p.name.is_empty() { continue; }
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
query_params.push((
|
||||
render::render(&p.name, &workspace, environment_ref),
|
||||
render::render(&p.value, &workspace, environment_ref),
|
||||
@@ -128,18 +151,38 @@ pub async fn actually_send_request(
|
||||
let request_body = request.body.0;
|
||||
|
||||
if request_body.contains_key("text") {
|
||||
let raw_text = request_body.get("text").unwrap_or(empty_string).as_str().unwrap_or("");
|
||||
let raw_text = request_body
|
||||
.get("text")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let body = render::render(raw_text, &workspace, environment_ref);
|
||||
request_builder = request_builder.body(body);
|
||||
} else if body_type == "application/x-www-form-urlencoded" && request_body.contains_key("form") {
|
||||
} else if body_type == "application/x-www-form-urlencoded"
|
||||
&& request_body.contains_key("form")
|
||||
{
|
||||
let mut form_params = Vec::new();
|
||||
let form = request_body.get("form");
|
||||
if let Some(f) = form {
|
||||
for p in f.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
form_params.push((
|
||||
render::render(name, &workspace, environment_ref),
|
||||
render::render(value, &workspace, environment_ref),
|
||||
@@ -151,17 +194,41 @@ pub async fn actually_send_request(
|
||||
let mut multipart_form = multipart::Form::new();
|
||||
if let Some(form_definition) = request_body.get("form") {
|
||||
for p in form_definition.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = p.get("file").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let file = p
|
||||
.get("file")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
multipart_form = multipart_form.part(
|
||||
render::render(name, &workspace, environment_ref),
|
||||
match !file.is_empty() {
|
||||
true => multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?),
|
||||
false => multipart::Part::text(render::render(value, &workspace, environment_ref)),
|
||||
true => {
|
||||
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
|
||||
}
|
||||
false => multipart::Part::text(render::render(
|
||||
value,
|
||||
&workspace,
|
||||
environment_ref,
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -202,6 +269,7 @@ pub async fn actually_send_request(
|
||||
response.url = v.url().to_string();
|
||||
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
|
||||
response.content_length = Some(body_bytes.len() as i64);
|
||||
println!("Response: {:?}", body_bytes.len());
|
||||
|
||||
{
|
||||
// Write body to FS
|
||||
@@ -237,6 +305,9 @@ pub async fn actually_send_request(
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||
Err(e) => {
|
||||
println!("Yo: {}", e);
|
||||
response_err(response, e.to_string(), app_handle, pool).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2024.0.0"
|
||||
"version": "2024.0.1-beta.2"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
@@ -35,8 +35,13 @@
|
||||
"open": true
|
||||
},
|
||||
"window": {
|
||||
"close": true,
|
||||
"maximize": true,
|
||||
"minimize": true,
|
||||
"setDecorations": true,
|
||||
"setTitle": true,
|
||||
"startDragging": true,
|
||||
"setTitle": true
|
||||
"unmaximize": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { Suspense } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
@@ -26,7 +25,7 @@ export function App() {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Suspense>
|
||||
<AppRouter />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
|
||||
</Suspense>
|
||||
</DndProvider>
|
||||
</HelmetProvider>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import { useHotkey } from '../hooks/useHotkey';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
@@ -35,8 +34,6 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
});
|
||||
}, [dialog, activeEnvironment]);
|
||||
|
||||
useHotkey('environmentEditor.toggle', showEnvironmentDialog, { enable: environments.length > 0 });
|
||||
|
||||
const items: DropdownItem[] = useMemo(
|
||||
() => [
|
||||
...environments.map(
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
@@ -11,9 +10,9 @@ import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { requestsQueryKey } from '../hooks/useRequests';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { responsesQueryKey } from '../hooks/useResponses';
|
||||
import { settingsQueryKey } from '../hooks/useSettings';
|
||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { trackPage } from '../lib/analytics';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
@@ -39,10 +38,6 @@ export function GlobalHooks() {
|
||||
setPathname(location.pathname).catch(console.error);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
trackPage('/');
|
||||
});
|
||||
|
||||
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
|
||||
if (shouldIgnoreEvent(payload, windowLabel)) return;
|
||||
|
||||
@@ -55,6 +50,8 @@ export function GlobalHooks() {
|
||||
? workspacesQueryKey(payload)
|
||||
: payload.model === 'key_value'
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: null;
|
||||
|
||||
if (queryKey === null) {
|
||||
@@ -80,6 +77,8 @@ export function GlobalHooks() {
|
||||
? workspacesQueryKey(payload)
|
||||
: payload.model === 'key_value'
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: null;
|
||||
|
||||
if (queryKey === null) {
|
||||
@@ -113,6 +112,8 @@ export function GlobalHooks() {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
||||
} else if (payload.model === 'settings') {
|
||||
queryClient.setQueryData(settingsQueryKey(), undefined);
|
||||
}
|
||||
});
|
||||
useListenToTauriEvent<number>('zoom', ({ payload: zoomDelta, windowLabel }) => {
|
||||
|
||||
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { hotkeyActions } from '../hooks/useHotKey';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
|
||||
export const KeyboardShortcutsDialog = () => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<HotKeyList hotkeys={hotkeyActions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
@@ -33,25 +34,19 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
|
||||
// Handle key-up
|
||||
useKeyPressEvent('Control', undefined, () => {
|
||||
if (!dropdownRef.current?.isOpen) return;
|
||||
dropdownRef.current?.select?.();
|
||||
});
|
||||
|
||||
useKey(
|
||||
'Tab',
|
||||
(e) => {
|
||||
if (!e.ctrlKey || recentRequestIds.length === 0) return;
|
||||
useHotKey('requestSwitcher.prev', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(1);
|
||||
dropdownRef.current?.next?.();
|
||||
});
|
||||
|
||||
if (!dropdownRef.current?.isOpen) {
|
||||
dropdownRef.current?.open(e.shiftKey ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) dropdownRef.current?.prev?.();
|
||||
else dropdownRef.current?.next?.();
|
||||
},
|
||||
undefined,
|
||||
[recentRequestIds.length],
|
||||
);
|
||||
useHotKey('requestSwitcher.next', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(-1);
|
||||
dropdownRef.current?.prev?.();
|
||||
});
|
||||
|
||||
const items = useMemo<DropdownItem[]>(() => {
|
||||
if (activeWorkspaceId === null) return [];
|
||||
|
||||
@@ -5,8 +5,6 @@ import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { clamp } from '../lib/clamp';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { RequestPane } from './RequestPane';
|
||||
@@ -29,8 +27,6 @@ const STACK_VERTICAL_WIDTH = 600;
|
||||
export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const createRequest = useCreateRequest();
|
||||
const requests = useRequests();
|
||||
const [vertical, setVertical] = useState<boolean>(false);
|
||||
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`);
|
||||
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`);
|
||||
@@ -42,7 +38,8 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
);
|
||||
|
||||
useResizeObserver(containerRef, ({ contentRect }) => {
|
||||
setVertical(contentRect.width < STACK_VERTICAL_WIDTH);
|
||||
const doIt = contentRect.width < STACK_VERTICAL_WIDTH;
|
||||
setVertical(doIt);
|
||||
});
|
||||
|
||||
const styles = useMemo<CSSProperties>(
|
||||
|
||||
83
src-web/components/SettingsDialog.tsx
Normal file
83
src-web/components/SettingsDialog.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import classNames from 'classnames';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import type { Appearance } from '../lib/theme/window';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
export const SettingsDialog = () => {
|
||||
const { appearance, setAppearance } = useTheme();
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
|
||||
if (settings == null) {
|
||||
return null;
|
||||
}
|
||||
console.log('SETTINGS', settings);
|
||||
|
||||
return (
|
||||
<VStack space={2}>
|
||||
<div className="w-full gap-2 grid grid-cols-[auto_1fr] gap-x-6 auto-rows-[2rem] items-center">
|
||||
<Checkbox
|
||||
className="col-span-full"
|
||||
checked={settings.validateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
onChange={(validateCertificates) =>
|
||||
updateSettings.mutateAsync({ ...settings, validateCertificates })
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
className="col-span-full"
|
||||
checked={settings.followRedirects}
|
||||
title="Follow Redirects"
|
||||
onChange={(followRedirects) =>
|
||||
updateSettings.mutateAsync({ ...settings, followRedirects })
|
||||
}
|
||||
/>
|
||||
|
||||
<div>Request Timeout (ms)</div>
|
||||
<div>
|
||||
<Input
|
||||
size="sm"
|
||||
name="requestTimeout"
|
||||
label="Request Timeout (ms)"
|
||||
containerClassName="col-span-2"
|
||||
hideLabel
|
||||
defaultValue={`${settings.requestTimeout}`}
|
||||
validate={(value) => parseInt(value) >= 0}
|
||||
onChange={(v) =>
|
||||
updateSettings.mutateAsync({ ...settings, requestTimeout: parseInt(v) || 0 })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Appearance</div>
|
||||
<select
|
||||
value={settings.appearance}
|
||||
style={selectBackgroundStyles}
|
||||
onChange={(e) => updateSettings.mutateAsync({ ...settings, appearance: e.target.value })}
|
||||
className={classNames(
|
||||
'border w-full px-2 outline-none bg-transparent',
|
||||
'border-highlight focus:border-focus',
|
||||
'h-sm',
|
||||
)}
|
||||
>
|
||||
<option value="system">Match System</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
{/*<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />*/}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
const selectBackgroundStyles = {
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||
backgroundPosition: 'right 0.5rem center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '1.5em 1.5em',
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import { useRef } from 'react';
|
||||
import { useAppVersion } from '../hooks/useAppVersion';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useUpdateMode } from '../hooks/useUpdateMode';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
@@ -12,11 +11,12 @@ import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
import { SettingsDialog } from './SettingsDialog';
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
const exportData = useExportData();
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
const appVersion = useAppVersion();
|
||||
const [updateMode, setUpdateMode] = useUpdateMode();
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
@@ -61,17 +61,39 @@ export function SettingsDropdown() {
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: 'Toggle Theme',
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
hotkeyAction: 'hotkeys.showHelp',
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'hotkey-help',
|
||||
title: 'Keyboard Shortcuts',
|
||||
size: 'sm',
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{ type: 'separator', label: `v${appVersion.data}` },
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hotkeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'settings',
|
||||
size: 'md',
|
||||
title: 'Settings',
|
||||
render: () => <SettingsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appVersion.data}` },
|
||||
{
|
||||
key: 'update-mode',
|
||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||
onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'),
|
||||
leftSlot: <Icon icon="camera" />,
|
||||
leftSlot: <Icon icon="rocket" />,
|
||||
},
|
||||
{
|
||||
key: 'update-check',
|
||||
@@ -87,7 +109,7 @@ export function SettingsDropdown() {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton size="sm" title="Request Options" icon="gear" className="pointer-events-auto" />
|
||||
<IconButton size="sm" title="Main Menu" icon="hamburger" className="pointer-events-auto" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useFolders } from '../hooks/useFolders';
|
||||
import { useHotkey } from '../hooks/useHotkey';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
@@ -55,7 +55,6 @@ export function Sidebar({ className }: Props) {
|
||||
const { hidden } = useSidebarHidden();
|
||||
const sidebarRef = useRef<HTMLLIElement>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const duplicateRequest = useDuplicateRequest({ id: activeRequestId ?? '', navigateAfter: true });
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const requests = useRequests();
|
||||
const folders = useFolders();
|
||||
@@ -76,8 +75,6 @@ export function Sidebar({ className }: Props) {
|
||||
namespace: NAMESPACE_NO_SYNC,
|
||||
});
|
||||
|
||||
useHotkey('request.duplicate', () => duplicateRequest.mutate());
|
||||
|
||||
const isCollapsed = useCallback(
|
||||
(id: string) => collapsed.value?.[id] ?? false,
|
||||
[collapsed.value],
|
||||
@@ -209,7 +206,7 @@ export function Sidebar({ className }: Props) {
|
||||
useKeyPressEvent('Backspace', handleDeleteKey);
|
||||
useKeyPressEvent('Delete', handleDeleteKey);
|
||||
|
||||
useHotkey('sidebar.focus', () => {
|
||||
useHotKey('sidebar.focus', () => {
|
||||
if (hidden || hasFocus) return;
|
||||
// Select 0 index on focus if none selected
|
||||
focusActiveRequest(
|
||||
@@ -649,10 +646,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
label: 'Duplicate',
|
||||
hotkeyAction: 'request.duplicate',
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () => {
|
||||
console.log('DUPLICATE');
|
||||
duplicateRequest.mutate();
|
||||
},
|
||||
onSelect: () => duplicateRequest.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'deleteRequest',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useHotkey } from '../hooks/useHotkey';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { IconButton } from './core/IconButton';
|
||||
@@ -12,8 +11,6 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
const createFolder = useCreateFolder();
|
||||
const { hidden, toggle } = useSidebarHidden();
|
||||
|
||||
useHotkey('request.create', () => createRequest.mutate({}));
|
||||
|
||||
return (
|
||||
<HStack>
|
||||
<IconButton
|
||||
|
||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import type { FormEvent } from 'react';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { useHotkey } from '../hooks/useHotkey';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
@@ -40,7 +40,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
[sendRequest],
|
||||
);
|
||||
|
||||
useHotkey('urlBar.focus', () => {
|
||||
useHotKey('urlBar.focus', () => {
|
||||
const head = inputRef.current?.state.doc.length ?? 0;
|
||||
inputRef.current?.dispatch({
|
||||
selection: { anchor: 0, head },
|
||||
|
||||
@@ -177,8 +177,8 @@ function HeaderSize({ className, ...props }: HeaderSizeProps) {
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'h-md pt-[1px] flex items-center w-full pr-3 border-b',
|
||||
platform?.osType === 'Darwin' && 'pl-20',
|
||||
'h-md pt-[1px] flex items-center w-full border-b',
|
||||
platform?.osType === 'Darwin' ? 'pl-20 pr-1' : 'pl-1',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { Icon } from './core/Icon';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
@@ -7,12 +7,17 @@ import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { SettingsDropdown } from './SettingsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { Button } from './core/Button';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
const osInfo = useOsInfo();
|
||||
const [maximized, setMaximized] = useState<boolean>(false);
|
||||
return (
|
||||
<HStack
|
||||
space={2}
|
||||
@@ -31,8 +36,51 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<div className="pointer-events-none">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||
<div className="flex-1 flex items-center h-full justify-end pointer-events-none">
|
||||
<SettingsDropdown />
|
||||
{osInfo?.osType !== 'Darwin' && (
|
||||
<HStack className="ml-4" space={1} alignItems="center">
|
||||
<Button className="!text-gray-600 rounded-none" onClick={() => appWindow.minimize()}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M14 8v1H3V8z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
className="!text-gray-600 rounded-none"
|
||||
onClick={async () => {
|
||||
await appWindow.toggleMaximize();
|
||||
setMaximized(await appWindow.isMaximized());
|
||||
}}
|
||||
>
|
||||
{maximized ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M3 3v10h10V3zm9 9H4V4h8z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="currentColor">
|
||||
<path d="M3 5v9h9V5zm8 8H4V6h7z" />
|
||||
<path fillRule="evenodd" d="M5 5h1V4h7v7h-1v1h2V3H5z" clipRule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
color="custom"
|
||||
className="text-gray-600 rounded-none hocus:bg-red-200 hocus:text-gray-800"
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="m7.116 8l-4.558 4.558l.884.884L8 8.884l4.558 4.558l.884-.884L8.884 8l4.558-4.558l-.884-.884L8 7.116L3.442 2.558l-.884.884z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||
import { useFormattedHotkey, useHotkey } from '../../hooks/useHotkey';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const colorStyles = {
|
||||
@@ -52,7 +52,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
}: ButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null);
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join('');
|
||||
const fullTitle = hotkeyTrigger ? `${title} ${hotkeyTrigger}` : title;
|
||||
|
||||
const classes = useMemo(
|
||||
@@ -80,7 +80,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
() => buttonRef.current,
|
||||
);
|
||||
|
||||
useHotkey(hotkeyAction ?? null, () => {
|
||||
useHotKey(hotkeyAction ?? null, () => {
|
||||
buttonRef.current?.click();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
@@ -8,33 +8,47 @@ interface Props {
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(!checked);
|
||||
}, [onChange, checked]);
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
|
||||
return (
|
||||
<button
|
||||
role="checkbox"
|
||||
aria-checked={checked ? 'true' : 'false'}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
|
||||
'focus:border-focus',
|
||||
'disabled:opacity-disabled',
|
||||
checked && 'bg-gray-200/10',
|
||||
// Remove focus style
|
||||
'outline-none',
|
||||
)}
|
||||
<HStack
|
||||
as="label"
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className={classNames(className, disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
<div className="relative flex">
|
||||
<input
|
||||
aria-hidden
|
||||
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(!checked)}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/*<button*/}
|
||||
{/* role="checkbox"*/}
|
||||
{/* aria-checked={checked ? 'true' : 'false'}*/}
|
||||
{/* disabled={disabled}*/}
|
||||
{/* onClick={handleClick}*/}
|
||||
{/* title={title}*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* className,*/}
|
||||
{/* 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',*/}
|
||||
{/* 'focus:border-focus',*/}
|
||||
{/* 'disabled:opacity-disabled',*/}
|
||||
{/* checked && 'bg-gray-200/10',*/}
|
||||
{/* // Remove focus style*/}
|
||||
{/* 'outline-none',*/}
|
||||
{/* )}*/}
|
||||
{/*>*/}
|
||||
{/*</button>*/}
|
||||
{!hideLabel && title}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
|
||||
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../hooks/useHotKey';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Button } from './Button';
|
||||
import { HotKey } from './HotKey';
|
||||
@@ -50,6 +51,7 @@ export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
|
||||
export interface DropdownProps {
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
items: DropdownItem[];
|
||||
openOnHotKeyAction?: HotkeyAction;
|
||||
}
|
||||
|
||||
export interface DropdownRef {
|
||||
@@ -63,20 +65,24 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items }: DropdownProps,
|
||||
{ children, items, openOnHotKeyAction }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number>();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||
|
||||
useHotKey(openOnHotKeyAction ?? null, () => {
|
||||
setIsOpen(true);
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...menuRef.current,
|
||||
isOpen: open,
|
||||
isOpen: isOpen,
|
||||
toggle(activeIndex?: number) {
|
||||
if (!open) this.open(activeIndex);
|
||||
else setOpen(false);
|
||||
if (!isOpen) this.open(activeIndex);
|
||||
else setIsOpen(false);
|
||||
},
|
||||
open(activeIndex?: number) {
|
||||
if (activeIndex === undefined) {
|
||||
@@ -84,7 +90,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
} else {
|
||||
setDefaultSelectedIndex(activeIndex >= 0 ? activeIndex : items.length + activeIndex);
|
||||
}
|
||||
setOpen(true);
|
||||
setIsOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -101,41 +107,40 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDefaultSelectedIndex(undefined);
|
||||
setOpen((o) => !o);
|
||||
setIsOpen((o) => !o);
|
||||
}),
|
||||
};
|
||||
return cloneElement(existingChild, props);
|
||||
}, [children]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.setAttribute('aria-expanded', open.toString());
|
||||
}, [open]);
|
||||
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
|
||||
}, [isOpen]);
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const triggerRect = useMemo(() => {
|
||||
if (!windowSize) return null; // No-op to TS happy with this dep
|
||||
if (!open) return null;
|
||||
if (!isOpen) return null;
|
||||
return buttonRef.current?.getBoundingClientRect();
|
||||
}, [open, windowSize]);
|
||||
}, [isOpen, windowSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{child}
|
||||
{open && triggerRect && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerShape={triggerRect}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerShape={triggerRect ?? null}
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -161,15 +166,12 @@ export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function Co
|
||||
[show],
|
||||
);
|
||||
|
||||
if (show === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={className}
|
||||
ref={ref}
|
||||
items={items}
|
||||
isOpen={show != null}
|
||||
onClose={onClose}
|
||||
triggerShape={triggerShape}
|
||||
/>
|
||||
@@ -180,13 +182,22 @@ interface MenuProps {
|
||||
className?: string;
|
||||
defaultSelectedIndex?: number;
|
||||
items: DropdownProps['items'];
|
||||
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'>;
|
||||
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
|
||||
onClose: () => void;
|
||||
showTriangle?: boolean;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuProps>(function Menu(
|
||||
{ className, items, onClose, triggerShape, defaultSelectedIndex, showTriangle }: MenuProps,
|
||||
{
|
||||
className,
|
||||
isOpen,
|
||||
items,
|
||||
onClose,
|
||||
triggerShape,
|
||||
defaultSelectedIndex,
|
||||
showTriangle,
|
||||
}: MenuProps,
|
||||
ref,
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -291,6 +302,8 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
containerStyles: CSSProperties;
|
||||
triangleStyles: CSSProperties | null;
|
||||
}>(() => {
|
||||
if (triggerShape == null) return { containerStyles: {}, triangleStyles: null };
|
||||
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
const width = triggerShape.right - triggerShape.left;
|
||||
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||
@@ -322,61 +335,76 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div>
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={containerRef}
|
||||
style={containerStyles}
|
||||
className={classNames(className, 'outline-none my-1 pointer-events-auto fixed z-50')}
|
||||
>
|
||||
{triangleStyles && showTriangle && (
|
||||
<span
|
||||
aria-hidden
|
||||
style={triangleStyles}
|
||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||
<>
|
||||
{items.map(
|
||||
(item) =>
|
||||
item.type !== 'separator' && (
|
||||
<MenuItemHotKey
|
||||
key={item.key}
|
||||
onSelect={handleSelect}
|
||||
item={item}
|
||||
action={item.hotkeyAction}
|
||||
/>
|
||||
)}
|
||||
{containerStyles && (
|
||||
<VStack
|
||||
space={0.5}
|
||||
ref={initMenu}
|
||||
style={menuStyles}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
||||
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
||||
)}
|
||||
),
|
||||
)}
|
||||
{isOpen && (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div>
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={containerRef}
|
||||
style={containerStyles}
|
||||
className={classNames(className, 'outline-none my-1 pointer-events-auto fixed z-50')}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
return <Separator key={i} className="my-1.5" label={item.label} />;
|
||||
}
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Overlay>
|
||||
{triangleStyles && showTriangle && (
|
||||
<span
|
||||
aria-hidden
|
||||
style={triangleStyles}
|
||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||
/>
|
||||
)}
|
||||
{containerStyles && (
|
||||
<VStack
|
||||
space={0.5}
|
||||
ref={initMenu}
|
||||
style={menuStyles}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
||||
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
||||
)}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
return <Separator key={i} className="my-1.5" label={item.label} />;
|
||||
}
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -443,3 +471,14 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface MenuItemHotKeyProps {
|
||||
action: HotkeyAction | undefined;
|
||||
onSelect: MenuItemProps['onSelect'];
|
||||
item: MenuItemProps['item'];
|
||||
}
|
||||
|
||||
function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) {
|
||||
useHotKey(action ?? null, () => onSelect(item));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -290,6 +290,8 @@ function getExtensions({
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
// Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when
|
||||
// changing pages (one request to another in header editor)
|
||||
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||
import { useFormattedHotkey } from '../../hooks/useHotkey';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey } from '../../hooks/useHotKey';
|
||||
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
action: HotkeyAction | null;
|
||||
@@ -11,20 +12,24 @@ interface Props {
|
||||
|
||||
export function HotKey({ action, className, variant }: Props) {
|
||||
const osInfo = useOsInfo();
|
||||
const label = useFormattedHotkey(action);
|
||||
if (label === null || osInfo == null) {
|
||||
const labelParts = useFormattedHotkey(action);
|
||||
if (labelParts === null || osInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
<HStack
|
||||
className={classNames(
|
||||
className,
|
||||
variant === 'with-bg' && 'rounded border',
|
||||
'text-sm text-gray-1000 text-opacity-disabled',
|
||||
'text-gray-1000 text-opacity-disabled',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{labelParts.map((char, index) => (
|
||||
<div key={index} className="min-w-[1.1em] text-center">
|
||||
{char}
|
||||
</div>
|
||||
))}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||
import { useHotKeyLabel } from '../../hooks/useHotkey';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotKeyLabel } from '../../hooks/useHotKey';
|
||||
|
||||
interface Props {
|
||||
action: HotkeyAction | null;
|
||||
action: HotkeyAction;
|
||||
}
|
||||
|
||||
export function HotKeyLabel({ action }: Props) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { HotKey } from './HotKey';
|
||||
import { HotKeyLabel } from './HotKeyLabel';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
hotkeys: HotkeyAction[];
|
||||
@@ -10,14 +11,14 @@ interface Props {
|
||||
export const HotKeyList = ({ hotkeys }: Props) => {
|
||||
return (
|
||||
<div className="mx-auto h-full flex items-center text-gray-700 text-sm">
|
||||
<div className="flex flex-col gap-1">
|
||||
<VStack space={2}>
|
||||
{hotkeys.map((hotkey) => (
|
||||
<div key={hotkey} className="grid grid-cols-2">
|
||||
<HStack key={hotkey} className="grid grid-cols-2">
|
||||
<HotKeyLabel action={hotkey} />
|
||||
<HotKey className="ml-auto" action={hotkey} />
|
||||
</div>
|
||||
</HStack>
|
||||
))}
|
||||
</div>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as ReactIcons from '@radix-ui/react-icons';
|
||||
import * as I from '@radix-ui/react-icons';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { memo } from 'react';
|
||||
@@ -6,46 +6,49 @@ import { ReactComponent as LeftPanelHiddenIcon } from '../../assets/icons/LeftPa
|
||||
import { ReactComponent as LeftPanelVisibleIcon } from '../../assets/icons/LeftPanelVisibleIcon.svg';
|
||||
|
||||
const icons = {
|
||||
archive: ReactIcons.ArchiveIcon,
|
||||
camera: ReactIcons.CameraIcon,
|
||||
chat: ReactIcons.ChatBubbleIcon,
|
||||
check: ReactIcons.CheckIcon,
|
||||
checkbox: ReactIcons.CheckboxIcon,
|
||||
clock: ReactIcons.ClockIcon,
|
||||
chevronDown: ReactIcons.ChevronDownIcon,
|
||||
chevronRight: ReactIcons.ChevronRightIcon,
|
||||
code: ReactIcons.CodeIcon,
|
||||
colorWheel: ReactIcons.ColorWheelIcon,
|
||||
copy: ReactIcons.CopyIcon,
|
||||
dividerH: ReactIcons.DividerHorizontalIcon,
|
||||
dotsH: ReactIcons.DotsHorizontalIcon,
|
||||
dotsV: ReactIcons.DotsVerticalIcon,
|
||||
download: ReactIcons.DownloadIcon,
|
||||
drag: ReactIcons.DragHandleDots2Icon,
|
||||
eye: ReactIcons.EyeOpenIcon,
|
||||
eyeClosed: ReactIcons.EyeClosedIcon,
|
||||
gear: ReactIcons.GearIcon,
|
||||
hamburger: ReactIcons.HamburgerMenuIcon,
|
||||
home: ReactIcons.HomeIcon,
|
||||
listBullet: ReactIcons.ListBulletIcon,
|
||||
magicWand: ReactIcons.MagicWandIcon,
|
||||
magnifyingGlass: ReactIcons.MagnifyingGlassIcon,
|
||||
moon: ReactIcons.MoonIcon,
|
||||
openNewWindow: ReactIcons.OpenInNewWindowIcon,
|
||||
paperPlane: ReactIcons.PaperPlaneIcon,
|
||||
pencil: ReactIcons.Pencil2Icon,
|
||||
plus: ReactIcons.PlusIcon,
|
||||
plusCircle: ReactIcons.PlusCircledIcon,
|
||||
question: ReactIcons.QuestionMarkIcon,
|
||||
rows: ReactIcons.RowsIcon,
|
||||
sun: ReactIcons.SunIcon,
|
||||
trash: ReactIcons.TrashIcon,
|
||||
triangleDown: ReactIcons.TriangleDownIcon,
|
||||
triangleLeft: ReactIcons.TriangleLeftIcon,
|
||||
triangleRight: ReactIcons.TriangleRightIcon,
|
||||
update: ReactIcons.UpdateIcon,
|
||||
upload: ReactIcons.UploadIcon,
|
||||
x: ReactIcons.Cross2Icon,
|
||||
archive: I.ArchiveIcon,
|
||||
chat: I.ChatBubbleIcon,
|
||||
check: I.CheckIcon,
|
||||
checkbox: I.CheckboxIcon,
|
||||
chevronDown: I.ChevronDownIcon,
|
||||
chevronRight: I.ChevronRightIcon,
|
||||
clock: I.ClockIcon,
|
||||
code: I.CodeIcon,
|
||||
colorWheel: I.ColorWheelIcon,
|
||||
copy: I.CopyIcon,
|
||||
dividerH: I.DividerHorizontalIcon,
|
||||
dotsH: I.DotsHorizontalIcon,
|
||||
dotsV: I.DotsVerticalIcon,
|
||||
download: I.DownloadIcon,
|
||||
drag: I.DragHandleDots2Icon,
|
||||
eye: I.EyeOpenIcon,
|
||||
eyeClosed: I.EyeClosedIcon,
|
||||
gear: I.GearIcon,
|
||||
hamburger: I.HamburgerMenuIcon,
|
||||
home: I.HomeIcon,
|
||||
keyboard: I.KeyboardIcon,
|
||||
listBullet: I.ListBulletIcon,
|
||||
magicWand: I.MagicWandIcon,
|
||||
magnifyingGlass: I.MagnifyingGlassIcon,
|
||||
minus: I.MinusIcon,
|
||||
moon: I.MoonIcon,
|
||||
openNewWindow: I.OpenInNewWindowIcon,
|
||||
paperPlane: I.PaperPlaneIcon,
|
||||
pencil: I.Pencil2Icon,
|
||||
plus: I.PlusIcon,
|
||||
plusCircle: I.PlusCircledIcon,
|
||||
question: I.QuestionMarkIcon,
|
||||
rocket: I.RocketIcon,
|
||||
rows: I.RowsIcon,
|
||||
square: I.SquareIcon,
|
||||
sun: I.SunIcon,
|
||||
trash: I.TrashIcon,
|
||||
triangleDown: I.TriangleDownIcon,
|
||||
triangleLeft: I.TriangleLeftIcon,
|
||||
triangleRight: I.TriangleRightIcon,
|
||||
update: I.UpdateIcon,
|
||||
upload: I.UploadIcon,
|
||||
x: I.Cross2Icon,
|
||||
|
||||
// Custom
|
||||
leftPanelHidden: LeftPanelHiddenIcon,
|
||||
|
||||
@@ -335,7 +335,8 @@ const FormRow = memo(function FormRow({
|
||||
<span className="w-3" />
|
||||
)}
|
||||
<Checkbox
|
||||
title={pairContainer.pair.enabled ? 'disable entry' : 'Enable item'}
|
||||
hideLabel
|
||||
title={pairContainer.pair.enabled ? 'Disable item' : 'Enable item'}
|
||||
disabled={isLast}
|
||||
checked={isLast ? false : !!pairContainer.pair.enabled}
|
||||
className={classNames('mr-2', isLast && '!opacity-disabled')}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
|
||||
});
|
||||
|
||||
type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: ComponentType | 'ul' | 'form';
|
||||
as?: ComponentType | 'ul' | 'label' | 'form';
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center' | 'stretch';
|
||||
justifyContent?: 'start' | 'center' | 'end' | 'between';
|
||||
|
||||
@@ -13,8 +13,6 @@ export function useCreateEnvironment() {
|
||||
const prompt = usePrompt();
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const queryClient = useQueryClient();
|
||||
const environments = useEnvironments();
|
||||
const workspaces = useWorkspaces();
|
||||
|
||||
return useMutation<Environment, unknown, void>({
|
||||
mutationFn: async () => {
|
||||
@@ -26,7 +24,7 @@ export function useCreateEnvironment() {
|
||||
});
|
||||
return invoke('create_environment', { name, variables: [], workspaceId });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'create'),
|
||||
onSettled: () => trackEvent('Environment', 'Create'),
|
||||
onSuccess: async (environment) => {
|
||||
if (workspaceId == null) return;
|
||||
routes.setEnvironment(environment);
|
||||
|
||||
@@ -18,7 +18,7 @@ export function useCreateFolder() {
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
return invoke('create_folder', { workspaceId, ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
onSettled: () => trackEvent('Folder', 'Create'),
|
||||
onSuccess: async (request) => {
|
||||
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useCreateRequest() {
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invoke('create_request', { workspaceId, name: '', ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'create'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Create'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
|
||||
mutationFn: (patch) => {
|
||||
return invoke('create_workspace', patch);
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
onSettled: () => trackEvent('Workspace', 'Create'),
|
||||
onSuccess: async (workspace) => {
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => [
|
||||
...(workspaces ?? []),
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useDeleteAnyRequest() {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_request', { requestId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'delete'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Delete'),
|
||||
onSuccess: async (request) => {
|
||||
// Was it cancelled?
|
||||
if (request === null) return;
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_environment', { environmentId: environment?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'delete'),
|
||||
onSettled: () => trackEvent('Environment', 'Delete'),
|
||||
onSuccess: async (environment) => {
|
||||
if (environment === null) return;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export function useDeleteFolder(id: string | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_folder', { folderId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'delete'),
|
||||
onSettled: () => trackEvent('Folder', 'Delete'),
|
||||
onSuccess: async (folder) => {
|
||||
// Was it cancelled?
|
||||
if (folder === null) return;
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useDeleteResponse(id: string | null) {
|
||||
mutationFn: async () => {
|
||||
return await invoke('delete_response', { id: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete'),
|
||||
onSettled: () => trackEvent('HttpResponse', 'Delete'),
|
||||
onSuccess: ({ requestId, id: responseId }) => {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
|
||||
(responses ?? []).filter((response) => response.id !== responseId),
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useDeleteResponses(requestId?: string) {
|
||||
if (requestId === undefined) return;
|
||||
await invoke('delete_all_responses', { requestId });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete_many'),
|
||||
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
|
||||
onSuccess: async () => {
|
||||
if (requestId === undefined) return;
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_workspace', { workspaceId: workspace?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'delete'),
|
||||
onSettled: () => trackEvent('Workspace', 'Delete'),
|
||||
onSuccess: async (workspace) => {
|
||||
if (workspace === null) return;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useDuplicateRequest({
|
||||
if (id === null) throw new Error("Can't duplicate a null request");
|
||||
return invoke('duplicate_request', { id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'duplicate'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Duplicate'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { OsType } from '@tauri-apps/api/os';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { debounce } from '../lib/debounce';
|
||||
import { useOsInfo } from './useOsInfo';
|
||||
import type {OsType} from '@tauri-apps/api/os';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {capitalize} from '../lib/capitalize';
|
||||
import {debounce} from '../lib/debounce';
|
||||
import {useOsInfo} from './useOsInfo';
|
||||
|
||||
export type HotkeyAction =
|
||||
| 'request.send'
|
||||
@@ -10,7 +11,11 @@ export type HotkeyAction =
|
||||
| 'sidebar.toggle'
|
||||
| 'sidebar.focus'
|
||||
| 'urlBar.focus'
|
||||
| 'environmentEditor.toggle';
|
||||
| 'environmentEditor.toggle'
|
||||
| 'hotkeys.showHelp'
|
||||
| 'requestSwitcher.prev'
|
||||
| 'requestSwitcher.next'
|
||||
| 'settings.show';
|
||||
|
||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
@@ -20,13 +25,33 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'sidebar.focus': ['CmdCtrl+1'],
|
||||
'urlBar.focus': ['CmdCtrl+l'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+e'],
|
||||
'hotkeys.showHelp': ['CmdCtrl+/'],
|
||||
'settings.show': ['CmdCtrl+,'],
|
||||
'requestSwitcher.prev': ['Control+Tab'],
|
||||
'requestSwitcher.next': ['Control+Shift+Tab'],
|
||||
};
|
||||
|
||||
const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'request.send': 'Send Request',
|
||||
'request.create': 'New Request',
|
||||
'request.duplicate': 'Duplicate Request',
|
||||
'sidebar.toggle': 'Toggle Sidebar',
|
||||
'sidebar.focus': 'Focus Sidebar',
|
||||
'urlBar.focus': 'Focus URL',
|
||||
'environmentEditor.toggle': 'Edit Environments',
|
||||
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
||||
'requestSwitcher.prev': 'Go To Next Request',
|
||||
'requestSwitcher.next': 'Go To Previous Request',
|
||||
'settings.show': 'Open Settings',
|
||||
};
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
|
||||
interface Options {
|
||||
enable?: boolean;
|
||||
}
|
||||
|
||||
export function useHotkey(
|
||||
export function useHotKey(
|
||||
action: HotkeyAction | null,
|
||||
callback: (e: KeyboardEvent) => void,
|
||||
options: Options = {},
|
||||
@@ -62,6 +87,8 @@ export function useAnyHotkey(
|
||||
|
||||
currentKeys.current.add(normalizeKey(e.key, os));
|
||||
|
||||
console.log("HOTKEY", e.key);
|
||||
|
||||
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||
for (const hkKey of hkKeys) {
|
||||
const keys = hkKey.split('+');
|
||||
@@ -93,28 +120,11 @@ export function useAnyHotkey(
|
||||
}, [options.enable, os]);
|
||||
}
|
||||
|
||||
export function useHotKeyLabel(action: HotkeyAction | null): string {
|
||||
switch (action) {
|
||||
case 'request.send':
|
||||
return 'Send Request';
|
||||
case 'request.create':
|
||||
return 'New Request';
|
||||
case 'request.duplicate':
|
||||
return 'Duplicate Request';
|
||||
case 'sidebar.toggle':
|
||||
return 'Toggle Sidebar';
|
||||
case 'sidebar.focus':
|
||||
return 'Focus Sidebar';
|
||||
case 'urlBar.focus':
|
||||
return 'Focus URL';
|
||||
case 'environmentEditor.toggle':
|
||||
return 'Edit Environments';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
export function useHotKeyLabel(action: HotkeyAction): string {
|
||||
return hotkeyLabels[action];
|
||||
}
|
||||
|
||||
export function useFormattedHotkey(action: HotkeyAction | null): string | null {
|
||||
export function useFormattedHotkey(action: HotkeyAction | null): string[] | null {
|
||||
const osInfo = useOsInfo();
|
||||
const trigger = action != null ? hotkeys[action]?.[0] ?? null : null;
|
||||
if (trigger == null || osInfo == null) {
|
||||
@@ -135,22 +145,24 @@ export function useFormattedHotkey(action: HotkeyAction | null): string | null {
|
||||
labelParts.push('⌃');
|
||||
} else if (p === 'Enter') {
|
||||
labelParts.push('↩');
|
||||
} else if (p === 'Tab') {
|
||||
labelParts.push('⇥');
|
||||
} else {
|
||||
labelParts.push(p.toUpperCase());
|
||||
labelParts.push(capitalize(p));
|
||||
}
|
||||
} else {
|
||||
if (p === 'CmdCtrl') {
|
||||
labelParts.push('Ctrl');
|
||||
} else {
|
||||
labelParts.push(p);
|
||||
labelParts.push(capitalize(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'Darwin') {
|
||||
return labelParts.join('');
|
||||
return labelParts;
|
||||
} else {
|
||||
return labelParts.join('+');
|
||||
return [labelParts.join('+')];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useSendAnyRequest() {
|
||||
const alert = useAlert();
|
||||
return useMutation<HttpResponse, string, string | null>({
|
||||
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
||||
onSettled: () => trackEvent('http_request', 'send'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Send'),
|
||||
onError: (err) => alert({ title: 'Export Failed', body: err }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,5 @@ export function useSendManyRequests() {
|
||||
sendAnyRequest.mutate(id);
|
||||
}
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'send'),
|
||||
});
|
||||
}
|
||||
|
||||
18
src-web/hooks/useSettings.ts
Normal file
18
src-web/hooks/useSettings.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Settings } from '../lib/models';
|
||||
|
||||
export function settingsQueryKey() {
|
||||
return ['settings'];
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
return (
|
||||
useQuery({
|
||||
queryKey: settingsQueryKey(),
|
||||
queryFn: async () => {
|
||||
return (await invoke('get_settings')) as Settings;
|
||||
},
|
||||
}).data ?? undefined
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Appearance } from '../lib/theme/window';
|
||||
import {
|
||||
getAppearance,
|
||||
setAppearance,
|
||||
setAppearanceOnDocument,
|
||||
getPreferredAppearance,
|
||||
subscribeToPreferredAppearanceChange,
|
||||
} from '../lib/theme/window';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
export function useTheme() {
|
||||
const appearanceKv = useKeyValue<Appearance>({
|
||||
key: 'appearance',
|
||||
defaultValue: getAppearance(),
|
||||
});
|
||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
|
||||
getPreferredAppearance(),
|
||||
);
|
||||
|
||||
const handleToggleAppearance = async () => {
|
||||
appearanceKv.set(appearanceKv.value === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
const settings = useSettings();
|
||||
|
||||
// Set appearance when preferred theme changes
|
||||
useEffect(() => subscribeToPreferredAppearanceChange(appearanceKv.set), [appearanceKv.set]);
|
||||
useEffect(() => {
|
||||
return subscribeToPreferredAppearanceChange(setPreferredAppearance);
|
||||
}, []);
|
||||
|
||||
// Sync appearance when k/v changes
|
||||
useEffect(() => setAppearance(appearanceKv.value), [appearanceKv.value]);
|
||||
const appearance =
|
||||
settings == null || settings?.appearance === 'system'
|
||||
? preferredAppearance
|
||||
: settings.appearance;
|
||||
|
||||
return {
|
||||
appearance: appearanceKv.value,
|
||||
toggleAppearance: handleToggleAppearance,
|
||||
};
|
||||
useEffect(() => {
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||
}, [appearance, settings]);
|
||||
|
||||
return { appearance };
|
||||
}
|
||||
|
||||
18
src-web/hooks/useUpdateSettings.ts
Normal file
18
src-web/hooks/useUpdateSettings.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest, Settings } from '../lib/models';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { settingsQueryKey } from './useSettings';
|
||||
|
||||
export function useUpdateSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, unknown, Settings>({
|
||||
mutationFn: async (settings) => {
|
||||
await invoke('update_settings', { settings });
|
||||
},
|
||||
onMutate: async (settings) => {
|
||||
queryClient.setQueryData<Settings>(settingsQueryKey(), settings);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,58 +1,20 @@
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import type { Environment, Folder, HttpRequest, HttpResponse, KeyValue, Workspace } from './models';
|
||||
|
||||
const appVersion = await getVersion();
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export function trackEvent(
|
||||
resource:
|
||||
| Workspace['model']
|
||||
| Environment['model']
|
||||
| Folder['model']
|
||||
| HttpRequest['model']
|
||||
| HttpResponse['model']
|
||||
| KeyValue['model'],
|
||||
event: 'create' | 'update' | 'delete' | 'delete_many' | 'send' | 'duplicate',
|
||||
| 'App'
|
||||
| 'Workspace'
|
||||
| 'Environment'
|
||||
| 'Folder'
|
||||
| 'HttpRequest'
|
||||
| 'HttpResponse'
|
||||
| 'KeyValue',
|
||||
action: 'Launch' | 'Create' | 'Update' | 'Delete' | 'DeleteMany' | 'Send' | 'Duplicate',
|
||||
attributes: Record<string, string | number> = {},
|
||||
) {
|
||||
send('/e', [
|
||||
{ name: 'e', value: `${resource}.${event}` },
|
||||
{ name: 'a', value: JSON.stringify({ ...attributes, version: appVersion }) },
|
||||
]);
|
||||
}
|
||||
|
||||
export function trackPage(pathname: string) {
|
||||
if (pathname === sessionStorage.lastPathName) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.lastPathName = pathname;
|
||||
send('/p', [
|
||||
{
|
||||
name: 'h',
|
||||
value: 'desktop.yaak.app',
|
||||
},
|
||||
{ name: 'p', value: pathname },
|
||||
]);
|
||||
}
|
||||
|
||||
function send(path: string, params: { name: string; value: string | number }[]) {
|
||||
if (localStorage.disableAnalytics === 'true') {
|
||||
console.log('Analytics disabled', path, params);
|
||||
}
|
||||
|
||||
params.push({ name: 'id', value: 'site_zOK0d7jeBy2TLxFCnZ' });
|
||||
params.push({
|
||||
name: 'tz',
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
});
|
||||
params.push({ name: 'xy', value: screensize() });
|
||||
const qs = params.map((v) => `${v.name}=${encodeURIComponent(v.value)}`).join('&');
|
||||
const url = `https://t.yaak.app/t${path}?${qs}`;
|
||||
fetch(url, { mode: 'no-cors' }).catch((err) => console.log('Error:', err));
|
||||
}
|
||||
|
||||
function screensize() {
|
||||
const w = window.screen.width;
|
||||
const h = window.screen.height;
|
||||
return `${Math.round(w / 100) * 100}x${Math.round(h / 100) * 100}`;
|
||||
invoke('track_event', {
|
||||
resource: resource,
|
||||
action,
|
||||
attributes,
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
3
src-web/lib/capitalize.ts
Normal file
3
src-web/lib/capitalize.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const AUTH_TYPE_NONE = null;
|
||||
export const AUTH_TYPE_BASIC = 'basic';
|
||||
export const AUTH_TYPE_BEARER = 'bearer';
|
||||
|
||||
export type Model = Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
|
||||
export type Model = Settings | Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
|
||||
|
||||
export interface BaseModel {
|
||||
readonly id: string;
|
||||
@@ -17,6 +17,15 @@ export interface BaseModel {
|
||||
readonly updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Settings extends BaseModel {
|
||||
readonly model: 'settings';
|
||||
validateCertificates: boolean;
|
||||
followRedirects: boolean;
|
||||
requestTimeout: number;
|
||||
theme: string;
|
||||
appearance: string;
|
||||
}
|
||||
|
||||
export interface Workspace extends BaseModel {
|
||||
readonly model: 'workspace';
|
||||
name: string;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Environment, Folder, HttpRequest, Workspace } from './models';
|
||||
import type { Environment, Folder, HttpRequest, Settings, Workspace } from './models';
|
||||
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
return invoke('get_settings', {});
|
||||
}
|
||||
|
||||
export async function getRequest(id: string | null): Promise<HttpRequest | null> {
|
||||
if (id === null) return null;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { AppTheme, AppThemeColors } from './theme';
|
||||
import { generateCSS, toTailwindVariable } from './theme';
|
||||
|
||||
export type Appearance = 'dark' | 'light';
|
||||
export type Appearance = 'dark' | 'light' | 'system';
|
||||
|
||||
const DEFAULT_APPEARANCE: Appearance = 'system';
|
||||
|
||||
enum Theme {
|
||||
yaak = 'yaak',
|
||||
@@ -61,19 +63,11 @@ const lightTheme: AppTheme = {
|
||||
},
|
||||
};
|
||||
|
||||
export function getAppearance(): Appearance {
|
||||
const docAppearance = document.documentElement.getAttribute('data-appearance');
|
||||
if (docAppearance === 'dark' || docAppearance === 'light') {
|
||||
return docAppearance;
|
||||
}
|
||||
return getPreferredAppearance();
|
||||
}
|
||||
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
|
||||
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
|
||||
const theme = resolvedAppearance === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
export function setAppearance(a?: Appearance) {
|
||||
const appearance = a ?? getPreferredAppearance();
|
||||
const theme = appearance === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
document.documentElement.setAttribute('data-appearance', appearance);
|
||||
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
|
||||
document.documentElement.setAttribute('data-theme', theme.name);
|
||||
|
||||
let existingStyleEl = document.head.querySelector(`style[data-theme-definition]`);
|
||||
@@ -85,11 +79,11 @@ export function setAppearance(a?: Appearance) {
|
||||
|
||||
existingStyleEl.textContent = [
|
||||
`/* ${darkTheme.name} */`,
|
||||
`[data-appearance="dark"] {`,
|
||||
`[data-resolved-appearance="dark"] {`,
|
||||
...generateCSS(darkTheme).map(toTailwindVariable),
|
||||
'}',
|
||||
`/* ${lightTheme.name} */`,
|
||||
`[data-appearance="light"] {`,
|
||||
`[data-resolved-appearance="light"] {`,
|
||||
...generateCSS(lightTheme).map(toTailwindVariable),
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@apply w-full h-full overflow-hidden bg-gray-50 text-gray-900;
|
||||
@apply w-full h-full overflow-hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
@apply bg-gray-50 text-gray-900;
|
||||
}
|
||||
|
||||
/* Setup default transitions for elements */
|
||||
@@ -68,4 +72,14 @@
|
||||
--color-white: 255 100% 100%;
|
||||
--color-black: 255 0% 0%;
|
||||
}
|
||||
|
||||
select {
|
||||
@apply appearance-none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,31 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { attachConsole } from 'tauri-plugin-log-api';
|
||||
import { App } from './components/App';
|
||||
import { getKeyValue } from './lib/keyValueStore';
|
||||
import { maybeRestorePathname } from './lib/persistPathname';
|
||||
import { getPreferredAppearance, setAppearance } from './lib/theme/window';
|
||||
import './main.css';
|
||||
import { getSettings } from './lib/store';
|
||||
import type { Appearance } from './lib/theme/window';
|
||||
import { setAppearanceOnDocument } from './lib/theme/window';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { type } from '@tauri-apps/api/os';
|
||||
|
||||
// Hide decorations here because it doesn't work in Rust for some reason (bug?)
|
||||
const osType = await type();
|
||||
if (osType !== 'Darwin') {
|
||||
await appWindow.setDecorations(false);
|
||||
}
|
||||
|
||||
await attachConsole();
|
||||
await maybeRestorePathname();
|
||||
|
||||
const settings = await getSettings();
|
||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Don't go back in history on backspace
|
||||
if (e.key === 'Backspace') e.preventDefault();
|
||||
});
|
||||
|
||||
setAppearance(
|
||||
await getKeyValue({
|
||||
key: 'appearance',
|
||||
fallback: getPreferredAppearance(),
|
||||
}),
|
||||
);
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -8,7 +8,7 @@ const height = {
|
||||
|
||||
/** @type {import("tailwindcss").Config} */
|
||||
module.exports = {
|
||||
darkMode: ['class', '[data-appearance="dark"]'],
|
||||
darkMode: ['class', '[data-resolved-appearance="dark"]'],
|
||||
content: ['./index.html', './src-web/**/*.{html,js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
Reference in New Issue
Block a user