Compare commits

...

74 Commits

Author SHA1 Message Date
Gregory Schier
36d8c56872 Toast after data export 2024-05-14 00:36:15 -07:00
Gregory Schier
a7bb5605ab Fix selection of HTTP Request on create dropdown hotkey 2024-05-14 00:17:33 -07:00
Gregory Schier
31147475f3 Fix curl export with multi-line body 2024-05-14 00:05:54 -07:00
Gregory Schier
5a2d510d07 Autocomplete URLs of other requests 2024-05-13 23:54:52 -07:00
Gregory Schier
4a70c5415b Fixed asset:// loading and tweak curl stuff 2024-05-13 23:20:30 -07:00
Gregory Schier
ad796275b6 Bump version 2024-05-13 16:52:32 -07:00
Gregory Schier
31f5163ee3 Better notifications 2024-05-13 16:52:20 -07:00
Gregory Schier
1a6a75ca13 Improve copy-as-curl 2024-05-13 11:30:10 -07:00
Gregory Schier
edad9e2d68 Refactor UpdateMode 2024-05-13 07:28:45 -07:00
Gregory Schier
d5d0edb0b0 Hide large GRPC messages by default 2024-05-13 07:19:26 -07:00
Gregory Schier
8cf45ba97f Bump version 2024-05-12 12:11:14 -07:00
Gregory Schier
6749fa9348 Add curl banner to import dialog 2024-05-10 13:36:30 -07:00
Gregory Schier
0f739834c8 Change curl import to post-toast 2024-05-10 13:06:40 -07:00
Gregory Schier
2d69b83765 Toast component and use for copy-as-curl 2024-05-10 12:37:04 -07:00
Gregory Schier
fa1765f356 Insomnia YAML and loading state on import 2024-05-10 09:46:20 -07:00
Gregory Schier
d1a0265ea5 Some fixes after upgrading react-query 2024-05-10 09:19:29 -07:00
Gregory Schier
bc191fec95 Update deps 2024-05-10 08:52:06 -07:00
Gregory Schier
43d042ae68 Fix paste handler in Editor.tsx 2024-05-09 23:17:43 -07:00
Gregory Schier
bb47fda7e1 Fix release targets 2024-05-09 15:38:27 -07:00
Gregory Schier
f98f541a3d Don't commit .cargo 2024-05-09 10:17:35 -07:00
Gregory Schier
4ea943d7f1 Fix args 2024-05-09 09:35:39 -07:00
Gregory Schier
a6fac2460b Update GH action 2024-05-09 09:33:47 -07:00
Gregory Schier
50410d262f Try fix linux 2024-05-09 08:37:18 -07:00
Gregory Schier
388d227572 Fix env var in GH Action 2024-05-09 08:29:45 -07:00
Gregory Schier
d07e80cc19 Fix curl export tests 2024-05-09 08:18:06 -07:00
Gregory Schier
8d987cff31 Default .app/.dev/etc domains to https protos 2024-05-09 08:16:06 -07:00
Gregory Schier
c46e976932 Bump version for beta 2024-05-09 07:52:36 -07:00
Gregory Schier
e33f273d7b Fix GRPC event.emit permissions 2024-05-09 07:45:00 -07:00
Gregory Schier
f16ced534c Import from Curl 2024-05-09 07:31:52 -07:00
Gregory Schier
f05abf97a4 Package lock 2024-05-08 15:37:53 -07:00
Gregory Schier
71b06c5261 Slight refactor to copy-as-curl 2024-05-08 00:28:40 -07:00
Gregory Schier
6639b07568 Add rename request to context menu
Closes #21
2024-05-08 00:08:18 -07:00
Gregory Schier
b196e51f1f Copy as curl 2024-05-08 00:00:50 -07:00
Gregory Schier
edc4fe3d9a Curl import (#24) 2024-05-07 21:57:03 -07:00
Gregory Schier
74065a320c Upgrade to Tauri 2.0 (#23) 2024-05-04 14:14:19 -07:00
Gregory Schier
87946c6c9f Fix horizontal scroll on GraphQL variables editor 2024-04-18 10:53:36 -07:00
Gregory Schier
4a58f73aa4 Oops 2024-04-02 10:11:37 +02:00
Gregory Schier
bd17650799 Postman text body import 2024-04-02 10:10:16 +02:00
Gregory Schier
db6a7dcabb Bump version 2024-04-01 08:48:26 +02:00
Gregory Schier
00b1f90074 Separate floating sidebar hidden state 2024-03-22 10:43:10 -07:00
Gregory Schier
e292235792 Filtering for cmd palette 2024-03-22 10:42:45 -07:00
Gregory Schier
5f86802d88 Space between var placeholders and code fold cursor 2024-03-22 10:42:35 -07:00
Gregory Schier
acb7f2e49b Fix Postman variable import 2024-03-22 10:40:51 -07:00
Gregory Schier
e2a15609bf Adjust highlight color 2024-03-22 10:37:45 -07:00
Gregory Schier
aa3bfd78c4 Some scrolling tweaks 2024-03-20 17:27:47 -07:00
Gregory Schier
23c4971127 Fix URL bar buttons in expanded state 2024-03-20 16:17:05 -07:00
Gregory Schier
40669217fb Bump version 2024-03-20 16:05:14 -07:00
Gregory Schier
8089ea87e8 Fix dialog height 2024-03-20 16:05:01 -07:00
Gregory Schier
9de24e3a40 Remove openOnHotKeyAction in favor of putting hotkey on the trigger button= 2024-03-20 15:56:39 -07:00
Gregory Schier
5afb8e7383 Use SQLite connect options 2024-03-20 13:33:11 -07:00
Gregory Schier
b3e3f22211 Pass workspace id to import 2024-03-20 07:30:59 -07:00
Gregory Schier
1ff6ff16b3 Handle import errors 2024-03-20 07:27:12 -07:00
Gregory Schier
b8a692f1a5 Postman bearer, global auth, global vars 2024-03-20 07:26:46 -07:00
Gregory Schier
5506cdd05f Implement select for command palette 2024-03-19 17:24:57 -07:00
Gregory Schier
4180fecb4b Tweak checkbox and autocomplete styles 2024-03-19 17:08:06 -07:00
Gregory Schier
fa257fdb18 Fix sidebar border 2024-03-19 16:44:37 -07:00
Gregory Schier
2da141ea16 Export multiple workspaces 2024-03-19 13:43:33 -07:00
Gregory Schier
1993361f87 Fix settings query store and analytics 2024-03-19 10:23:21 -07:00
Gregory Schier
a5dd3beb73 Start of command palette 2024-03-18 17:09:01 -07:00
Gregory Schier
17423f8c54 useRequests hook 2024-03-18 13:49:36 -07:00
Gregory Schier
8f495b9ade Fix editor key events 2024-03-18 13:40:15 -07:00
Gregory Schier
46b9b758fe Simple tests for Postman and Yaak importers 2024-03-18 13:40:00 -07:00
Gregory Schier
b0e84aac0c Set filename on Multipart part 2024-03-18 13:24:27 -07:00
Gregory Schier
20de2aeacc Fix GraphQL editor large variables quirk 2024-03-18 13:10:55 -07:00
Gregory Schier
7198534640 Fix postman import and import Insomnia gRPC 2024-03-18 08:18:04 -07:00
Gregory Schier
7e8ec36474 Better padding 2024-03-16 13:59:06 -07:00
Gregory Schier
52d1602d35 Remove debug log 2024-03-16 12:50:27 -07:00
Gregory Schier
e5731ceb1f Custom content-type for multipart items 2024-03-16 12:49:17 -07:00
Gregory Schier
3ed5a47a83 Content menu on entire sidebar 2024-03-16 10:47:10 -07:00
Gregory Schier
262a29ca5d Obfuscate environment variables 2024-03-16 10:42:46 -07:00
Gregory Schier
4a3e599128 Fix light mode text selection 2024-03-16 09:48:55 -07:00
Gregory Schier
7ebe844643 Stubbed out global commands helper 2024-03-16 09:46:11 -07:00
Gregory Schier
a49b72eebc Fix deleting workspace staying on deleted workspace path 2024-03-15 13:07:02 -07:00
Gregory Schier
bba3afa0b7 Bump version 2024-03-10 18:15:00 -07:00
223 changed files with 34303 additions and 5587 deletions

View File

@@ -1,72 +0,0 @@
name: Generate Artifacts
on:
push:
tags: [ v* ]
permissions: write-all
jobs:
build-artifacts:
name: Build
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: ubuntu-20.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
./src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'npm'
- name: install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
# Pin dev version to get non-default targets
# https://github.com/tauri-apps/tauri-action/issues/356
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
with:
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'
releaseBody: 'https://yaak.app/changelog/__VERSION__'
releaseDraft: true
prerelease: false
args: '--target ${{ matrix.target }}'

72
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Generate Artifacts
on:
push:
tags: [ v* ]
permissions: write-all
jobs:
build-artifacts:
permissions:
contents: write
name: Build
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest' # for Arm based macs (M1 and above).
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest' # for Intel based macs.
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
with:
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'
releaseBody: 'https://yaak.app/changelog/__VERSION__'
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}

2
.gitignore vendored
View File

@@ -26,3 +26,5 @@ dist-ssr
*.sqlite
*.sqlite-*
.cargo

2981
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,9 @@
"version": "0.0.0",
"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",
"start": "npm run build:plugins && npm run tauri-dev:desktop",
"tauri-dev:desktop": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"tauri-dev:ios": "tauri ios dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"tauri-build": "tauri build",
"tauri": "tauri",
"build": "npm run build:frontend",
@@ -15,9 +16,11 @@
"build:icon:dev": "tauri icon design/icon-dev.png --output ./src-tauri/icons/dev",
"build:frontend": "vite build",
"build:plugins": "run-p build:plugin:*",
"build:plugin:exporter-curl": "cd plugins/exporter-curl && vite build --emptyOutDir",
"build:plugin:importer-insomnia": "cd plugins/importer-insomnia && vite build --emptyOutDir",
"build:plugin:importer-postman": "cd plugins/importer-postman && vite build --emptyOutDir",
"build:plugin:importer-yaak": "cd plugins/importer-yaak && vite build --emptyOutDir",
"build:plugin:importer-curl": "cd plugins/importer-curl && vite build --emptyOutDir",
"build:plugin:filter-jsonpath": "cd plugins/filter-jsonpath && vite build --emptyOutDir",
"build:plugin:filter-xpath": "cd plugins/filter-xpath && vite build --emptyOutDir",
"test": "vitest",
@@ -36,11 +39,13 @@
"@lezer/lr": "^1.3.3",
"@react-hook/resize-observer": "^1.2.6",
"@tailwindcss/container-queries": "^0.1.0",
"@tanstack/query-sync-storage-persister": "^4.27.1",
"@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.3",
"@tanstack/react-query": "^5.35.5",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-clipboard-manager": "^2.1.0-beta.1",
"@tauri-apps/plugin-dialog": ">=2.0.0-beta.0",
"@tauri-apps/plugin-fs": ">=2.0.0-beta.0",
"@tauri-apps/plugin-os": ">=2.0.0-beta.0",
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -62,13 +67,14 @@
"react-router-dom": "^6.8.1",
"react-use": "^17.4.0",
"slugify": "^1.6.6",
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1",
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v2",
"uuid": "^9.0.0",
"xml-formatter": "^3.6.2"
},
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tauri-apps/cli": "^1.5.10",
"@tanstack/react-query-devtools": "^5.35.5",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7",
"@types/parse-color": "^1.0.1",
@@ -87,6 +93,7 @@
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"internal-ip": "^8.0.0",
"lint-staged": "^15.0.2",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
@@ -95,7 +102,7 @@
"react-devtools": "^4.27.2",
"tailwindcss": "^3.2.7",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vite": "^5.0.0",
"vite-plugin-svgr": "^4.2.0",
"vite-plugin-top-level-await": "^1.4.1",
"vitest": "^1.3.0"

1544
plugins/exporter-curl/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"name": "exporter-curl",
"version": "0.0.1",
"devDependencies": {
"vitest": "^1.4.0"
}
}

View File

@@ -0,0 +1,76 @@
import { HttpRequest } from '../../../src-web/lib/models';
const NEWLINE = '\\\n ';
export function pluginHookExport(request: Partial<HttpRequest>) {
const xs = ['curl'];
// Add method and URL all on first line
if (request.method) xs.push('-X', request.method);
if (request.url) xs.push(quote(request.url));
xs.push(NEWLINE);
// Add URL params
for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {
xs.push('--url-query', quote(`${p.name}=${p.value}`));
xs.push(NEWLINE);
}
// Add headers
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
xs.push('--header', quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add form params
if (Array.isArray(request.body?.form)) {
const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
if (p.file) {
let v = `${p.name}=@${p.file}`;
v += p.contentType ? `;type=${p.contentType}` : '';
xs.push(flag, v);
} else {
xs.push(flag, quote(`${p.name}=${p.value}`));
}
xs.push(NEWLINE);
}
} else if (typeof request.body?.text === 'string') {
// --data-raw $'...' to do special ANSI C quoting
xs.push('--data-raw', `$${quote(request.body.text)}`);
xs.push(NEWLINE);
}
// Add basic/digest authentication
if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {
if (request.authenticationType === 'digest') xs.push('--digest');
xs.push(
'--user',
quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`),
);
xs.push(NEWLINE);
}
// Add bearer authentication
if (request.authenticationType === 'bearer') {
xs.push('--header', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
xs.push(NEWLINE);
}
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(' ');
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}

View File

@@ -0,0 +1,175 @@
import { describe, expect, test } from 'vitest';
import { pluginHookExport } from '../src';
describe('exporter-curl', () => {
test('Exports GET with params', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
urlParameters: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual(
[`curl 'https://yaak.app'`, `--url-query 'a=aaa'`, `--url-query 'b=bbb'`].join(` \\\n `),
);
});
test('Exports POST with url form data', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(` \\\n `),
);
});
test('Exports PUT with multipart form', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
method: 'PUT',
bodyType: 'multipart/form-data',
body: {
form: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
{ name: 'f', file: '/foo/bar.png', contentType: 'image/png' },
],
},
}),
).toEqual(
[
`curl -X PUT 'https://yaak.app'`,
`--form 'a=aaa'`,
`--form 'b=bbb'`,
`--form f=@/foo/bar.png;type=image/png`,
].join(` \\\n `),
);
});
test('Exports JSON body', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
body: {
text: `{"foo":"bar's"}`,
},
headers: [{ name: 'Content-Type', value: 'application/json' }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw $'{"foo":"bar\\'s"}'`,
].join(` \\\n `),
);
});
test('Exports multi-line JSON body', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
body: {
text: `{"foo":"bar",\n"baz":"qux"}`,
},
headers: [{ name: 'Content-Type', value: 'application/json' }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw $'{"foo":"bar",\n"baz":"qux"}'`,
].join(` \\\n `),
);
});
test('Exports headers', () => {
expect(
pluginHookExport({
headers: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `));
});
test('Basic auth', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(` \\\n `));
});
test('Broken basic auth', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {},
}),
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(` \\\n `));
});
test('Digest auth', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
authenticationType: 'digest',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(` \\\n `));
});
test('Bearer auth', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'tok',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `));
});
test('Broken bearer auth', () => {
expect(
pluginHookExport({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `));
});
});

View File

@@ -0,0 +1,13 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
outDir: resolve(__dirname, '../../src-tauri/plugins/exporter-curl'),
},
});

1562
plugins/importer-curl/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"name": "importer-curl",
"version": "0.0.1",
"dependencies": {
"shell-quote": "^1.8.1"
},
"devDependencies": {
"@types/shell-quote": "^1.7.5",
"vitest": "^1.4.0"
}
}

View File

@@ -0,0 +1,403 @@
import { ControlOperator, parse, ParseEntry } from 'shell-quote';
import {
Environment,
Folder,
HttpRequest,
HttpUrlParameter,
Model,
Workspace,
} from '../../../src-web/lib/models';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export const id = 'curl';
export const name = 'cURL';
export const description = 'cURL command line tool';
const DATA_FLAGS = ['d', 'data', 'data-raw', 'data-urlencode', 'data-binary', 'data-ascii'];
const SUPPORTED_ARGS = [
['url'], // Specify the URL explicitly
['user', 'u'], // Authentication
['digest'], // Apply auth as digest
['header', 'H'],
['cookie', 'b'],
['get', 'G'], // Put the post data in the URL
['d', 'data'], // Add url encoded data
['data-raw'],
['data-urlencode'],
['data-binary'],
['data-ascii'],
['form', 'F'], // Add multipart data
['request', 'X'], // Request method
DATA_FLAGS,
].flatMap((v) => v);
type Pair = string | boolean;
type PairsByName = Record<string, Pair[]>;
export function pluginHookImport(rawData: string) {
if (!rawData.match(/^\s*curl /)) {
return null;
}
const commands: ParseEntry[][] = [];
// Replace non-escaped newlines with semicolons to make parsing easier
// NOTE: This is really slow in debug build but fast in release mode
const normalizedData = rawData.replace(/\ncurl/g, '; curl');
let currentCommand: ParseEntry[] = [];
const parsed = parse(normalizedData);
// Break up `-XPOST` into `-X POST`
const normalizedParseEntries = parsed.flatMap((entry) => {
if (
typeof entry === 'string' &&
entry.startsWith('-') &&
!entry.startsWith('--') &&
entry.length > 2
) {
return [entry.slice(0, 2), entry.slice(2)];
}
return entry;
});
for (const parseEntry of normalizedParseEntries) {
if (typeof parseEntry === 'string') {
if (parseEntry.startsWith('$')) {
currentCommand.push(parseEntry.slice(1));
} else {
currentCommand.push(parseEntry);
}
continue;
}
if ('comment' in parseEntry) {
continue;
}
const { op } = parseEntry as { op: 'glob'; pattern: string } | { op: ControlOperator };
// `;` separates commands
if (op === ';') {
commands.push(currentCommand);
currentCommand = [];
continue;
}
if (op?.startsWith('$')) {
// Handle the case where literal like -H $'Header: \'Some Quoted Thing\''
const str = op.slice(2, op.length - 1).replace(/\\'/g, "'");
currentCommand.push(str);
continue;
}
if (op === 'glob') {
currentCommand.push((parseEntry as { op: 'glob'; pattern: string }).pattern);
}
}
commands.push(currentCommand);
const workspace: ExportResources['workspaces'][0] = {
model: 'workspace',
id: generateId('workspace'),
name: 'Curl Import',
};
const requests: ExportResources['httpRequests'] = commands
.filter((command) => command[0] === 'curl')
.map((v) => importCommand(v, workspace.id));
return {
resources: {
httpRequests: requests,
workspaces: [workspace],
},
};
}
export function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
// ~~~~~~~~~~~~~~~~~~~~~ //
// Collect all the flags //
// ~~~~~~~~~~~~~~~~~~~~~ //
const pairsByName: PairsByName = {};
const singletons: ParseEntry[] = [];
// Start at 1 so we can skip the ^curl part
for (let i = 1; i < parseEntries.length; i++) {
let parseEntry = parseEntries[i];
if (typeof parseEntry === 'string') {
parseEntry = parseEntry.trim();
}
if (typeof parseEntry === 'string' && parseEntry.match(/^-{1,2}[\w-]+/)) {
const isSingleDash = parseEntry[0] === '-' && parseEntry[1] !== '-';
let name = parseEntry.replace(/^-{1,2}/, '');
if (!SUPPORTED_ARGS.includes(name)) {
continue;
}
let value;
const nextEntry = parseEntries[i + 1];
if (isSingleDash && name.length > 1) {
// Handle squished arguments like -XPOST
value = name.slice(1);
name = name.slice(0, 1);
} else if (typeof nextEntry === 'string' && !nextEntry.startsWith('-')) {
// Next arg is not a flag, so assign it as the value
value = nextEntry;
i++; // Skip next one
} else {
value = true;
}
pairsByName[name] = pairsByName[name] || [];
pairsByName[name]!.push(value);
} else if (parseEntry) {
singletons.push(parseEntry);
}
}
// ~~~~~~~~~~~~~~~~~ //
// Build the request //
// ~~~~~~~~~~~~~~~~~ //
// Url & parameters
let urlParameters: HttpUrlParameter[];
let url: string;
const urlArg = getPairValue(pairsByName, (singletons[0] as string) || '', ['url']);
const [baseUrl, search] = splitOnce(urlArg, '?');
urlParameters =
search?.split('&').map((p) => {
const v = splitOnce(p, '=');
return { name: v[0] ?? '', value: v[1] ?? '' };
}) ?? [];
url = baseUrl ?? urlArg;
// Authentication
const [username, password] = getPairValue(pairsByName, '', ['u', 'user']).split(/:(.*)$/);
const isDigest = getPairValue(pairsByName, false, ['digest']);
const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null;
const authentication = username
? {
username: username.trim(),
password: (password ?? '').trim(),
}
: {};
// Headers
const headers = [
...((pairsByName.header as string[] | undefined) || []),
...((pairsByName.H as string[] | undefined) || []),
].map((header) => {
const [name, value] = header.split(/:(.*)$/);
// remove final colon from header name if present
if (!value) {
return {
name: (name ?? '').trim().replace(/;$/, ''),
value: '',
};
}
return {
name: (name ?? '').trim(),
value: value.trim(),
};
});
// Cookies
const cookieHeaderValue = [
...((pairsByName.cookie as string[] | undefined) || []),
...((pairsByName.b as string[] | undefined) || []),
]
.map((str) => {
const name = str.split('=', 1)[0];
const value = str.replace(`${name}=`, '');
return `${name}=${value}`;
})
.join('; ');
// Convert cookie value to header
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === 'cookie');
if (cookieHeaderValue && existingCookieHeader) {
// Has existing cookie header, so let's update it
existingCookieHeader.value += `; ${cookieHeaderValue}`;
} else if (cookieHeaderValue) {
// No existing cookie header, so let's make a new one
headers.push({
name: 'Cookie',
value: cookieHeaderValue,
});
}
///Body (Text or Blob)
const dataParameters = pairsToDataParameters(pairsByName);
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === 'content-type');
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(';')[0] : null;
// Body (Multipart Form Data)
const formDataParams = [
...((pairsByName.form as string[] | undefined) || []),
...((pairsByName.F as string[] | undefined) || []),
].map((str) => {
const parts = str.split('=');
const name = parts[0] ?? '';
const value = parts[1] ?? '';
const item: { name: string; value?: string; file?: string; enabled: boolean } = {
name,
enabled: true,
};
if (value.indexOf('@') === 0) {
item.file = value.slice(1);
} else {
item.value = value;
}
return item;
});
// Body
let body = {};
let bodyType: string | null = null;
const bodyAsGET = getPairValue(pairsByName, false, ['G', 'get']);
if (dataParameters.length > 0 && bodyAsGET) {
urlParameters.push(...dataParameters);
} else if (
dataParameters.length > 0 &&
(mimeType == null || mimeType === 'application/x-www-form-urlencoded')
) {
bodyType = mimeType ?? 'application/x-www-form-urlencoded';
body = {
params: dataParameters.map((parameter) => ({
...parameter,
name: decodeURIComponent(parameter.name || ''),
value: decodeURIComponent(parameter.value || ''),
})),
};
} else if (dataParameters.length > 0) {
bodyType =
mimeType === 'application/json' || mimeType === 'text/xml' || mimeType === 'text/plain'
? mimeType
: 'other';
body = {
text: dataParameters
.map(({ name, value }) => (name && value ? `${name}=${value}` : name || value))
.join('&'),
};
} else if (formDataParams.length) {
bodyType = mimeType ?? 'multipart/form-data';
body = {
form: formDataParams,
};
}
// Method
let method = getPairValue(pairsByName, '', ['X', 'request']).toUpperCase();
if (method === '' && body) {
method = 'text' in body || 'params' in body ? 'POST' : 'GET';
}
const request: ExportResources['httpRequests'][0] = {
id: generateId('http_request'),
model: 'http_request',
workspaceId,
name: '',
urlParameters,
url,
method,
headers,
authentication,
authenticationType,
body,
bodyType,
folderId: null,
sortPriority: 0,
};
return request;
}
const pairsToDataParameters = (keyedPairs: PairsByName) => {
let dataParameters: {
name: string;
value: string;
contentType?: string;
filePath?: string;
}[] = [];
for (const flagName of DATA_FLAGS) {
const pairs = keyedPairs[flagName];
if (!pairs || pairs.length === 0) {
continue;
}
for (const p of pairs) {
if (typeof p !== 'string') continue;
const [name, value] = p.split('=');
if (p.startsWith('@')) {
// Yaak doesn't support files in url-encoded data, so
dataParameters.push({
name: name ?? '',
value: '',
filePath: p.slice(1),
});
} else {
dataParameters.push({
name: name ?? '',
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : value ?? '',
});
}
}
}
return dataParameters;
};
const getPairValue = <T extends string | boolean>(
pairsByName: PairsByName,
defaultValue: T,
names: string[],
) => {
for (const name of names) {
if (pairsByName[name] && pairsByName[name]!.length) {
return pairsByName[name]![0] as T;
}
}
return defaultValue;
};
function splitOnce(str: string, sep: string): string[] {
const index = str.indexOf(sep);
if (index > -1) {
return [str.slice(0, index), str.slice(index + 1)];
}
return [str];
}
const idCount: Partial<Record<Model['model'], number>> = {};
function generateId(model: Model['model']): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}

View File

@@ -0,0 +1,321 @@
import { describe, expect, test } from 'vitest';
import { HttpRequest, Model, Workspace } from '../../../src-web/lib/models';
import { pluginHookImport } from '../src';
describe('importer-curl', () => {
test('Imports basic GET', () => {
expect(pluginHookImport('curl https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Explicit URL', () => {
expect(pluginHookImport('curl --url https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Missing URL', () => {
expect(pluginHookImport('curl -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
}),
],
},
});
});
test('URL between', () => {
expect(pluginHookImport('curl -v https://yaak.app -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Random flags', () => {
expect(pluginHookImport('curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Imports --request method', () => {
expect(pluginHookImport('curl --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Imports -XPOST method', () => {
expect(pluginHookImport('curl -XPOST --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Imports multiple requests', () => {
expect(
pluginHookImport('curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({ url: 'https://yaak.app' }),
baseRequest({ url: 'example.com' }),
baseRequest({ url: 'foo.com' }),
],
},
});
});
test('Imports form data', () => {
expect(
pluginHookImport('curl -X POST -F "a=aaa" -F b=bbb" -F f=@filepath https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
bodyType: 'multipart/form-data',
body: {
form: [
{ enabled: true, name: 'a', value: 'aaa' },
{ enabled: true, name: 'b', value: 'bbb' },
{ enabled: true, name: 'f', file: 'filepath' },
],
},
}),
],
},
});
});
test('Imports data params as form url-encoded', () => {
expect(pluginHookImport('curl -d a -d b -d c=ccc https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
bodyType: 'application/x-www-form-urlencoded',
body: {
params: [
{ name: 'a', value: '' },
{ name: 'b', value: '' },
{ name: 'c', value: 'ccc' },
],
},
}),
],
},
});
});
test('Imports data params as text', () => {
expect(
pluginHookImport('curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
headers: [{ name: 'Content-Type', value: 'text/plain' }],
bodyType: 'text/plain',
body: { text: 'a&b&c=ccc' },
}),
],
},
});
});
test('Imports multi-line JSON', () => {
expect(
pluginHookImport(
`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`,
),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
headers: [{ name: 'Content-Type', value: 'application/json' }],
bodyType: 'application/json',
body: { text: '{\n "foo":"bar"\n}' },
}),
],
},
});
});
test('Imports multiple headers', () => {
expect(
pluginHookImport('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
headers: [
{ name: 'Name', value: '' },
{ name: 'Foo', value: 'bar' },
{ name: 'AAA', value: 'bbb' },
{ name: '', value: 'ccc' },
],
}),
],
},
});
});
test('Imports basic auth', () => {
expect(pluginHookImport('curl --user user:pass https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
username: 'user',
password: 'pass',
},
}),
],
},
});
});
test('Imports digest auth', () => {
expect(pluginHookImport('curl --digest --user user:pass https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
authenticationType: 'digest',
authentication: {
username: 'user',
password: 'pass',
},
}),
],
},
});
});
test('Imports cookie as header', () => {
expect(pluginHookImport('curl --cookie "foo=bar" https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
headers: [{ name: 'Cookie', value: 'foo=bar' }],
}),
],
},
});
});
test('Imports query params from the URL', () => {
expect(pluginHookImport('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
urlParameters: [
{ name: 'foo', value: 'bar' },
{ name: 'baz', value: 'a%20a' },
],
}),
],
},
});
});
});
const idCount: Partial<Record<Model['model'], number>> = {};
function baseRequest(mergeWith: Partial<HttpRequest>) {
idCount.http_request = (idCount.http_request ?? -1) + 1;
return {
id: `GENERATE_ID::HTTP_REQUEST_${idCount.http_request}`,
model: 'http_request',
authentication: {},
authenticationType: null,
body: {},
bodyType: null,
folderId: null,
headers: [],
method: 'GET',
name: '',
sortPriority: 0,
url: '',
urlParameters: [],
workspaceId: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
...mergeWith,
};
}
function baseWorkspace(mergeWith: Partial<Workspace> = {}) {
idCount.workspace = (idCount.workspace ?? -1) + 1;
return {
id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
model: 'workspace',
name: 'Curl Import',
...mergeWith,
};
}

View File

@@ -0,0 +1,13 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-curl'),
},
});

View File

@@ -6,7 +6,21 @@
"packages": {
"": {
"name": "importer-insomnia",
"version": "0.0.1"
"version": "0.0.1",
"dependencies": {
"yaml": "^2.4.2"
}
},
"node_modules/yaml": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
}
}
}

View File

@@ -1,4 +1,7 @@
{
"name": "importer-insomnia",
"version": "0.0.1"
"version": "0.0.1",
"dependencies": {
"yaml": "^2.4.2"
}
}

View File

@@ -1,23 +0,0 @@
export function isWorkspace(obj) {
return isJSObject(obj) && obj._type === 'workspace';
}
export function isRequestGroup(obj) {
return isJSObject(obj) && obj._type === 'request_group';
}
export function isRequest(obj) {
return isJSObject(obj) && obj._type === 'request';
}
export function isEnvironment(obj) {
return isJSObject(obj) && obj._type === 'environment';
}
export function isJSObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
export function isJSString(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}

View File

@@ -1,18 +0,0 @@
import { isJSString } from './types.js';
export function parseVariables(data) {
return Object.entries(data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
}));
}
/**
* Convert Insomnia syntax to Yaak syntax
* @param {string} variable - Text to convert
*/
export function convertSyntax(variable) {
if (!isJSString(variable)) return variable;
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
}

View File

@@ -1,21 +0,0 @@
/**
* Import an Insomnia environment object.
* @param {Object} e - The environment object to import.
* @param workspaceId - Workspace to import into.
*/
export function importEnvironment(e, workspaceId) {
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2));
return {
id: e._id,
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId,
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}

View File

@@ -1,17 +0,0 @@
/**
* Import an Insomnia folder object.
* @param {Object} f - The environment object to import.
* @param workspaceId - Workspace to import into.
*/
export function importFolder(f, workspaceId) {
console.log('IMPORTING FOLDER', f._id, f.name, JSON.stringify(f, null, 2));
return {
id: f._id,
createdAt: new Date(f.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace('Z', ''),
folderId: f.parentId === workspaceId ? null : f.parentId,
workspaceId,
model: 'folder',
name: f.name,
};
}

View File

@@ -1,60 +0,0 @@
import { convertSyntax } from '../helpers/variables.js';
/**
* Import an Insomnia request object.
* @param {Object} r - The request object to import.
* @param workspaceId - The workspace ID to use for the request.
* @param {number} sortPriority - The sort priority to use for the request.
*/
export function importRequest(r, workspaceId, sortPriority = 0) {
console.log('IMPORTING REQUEST', r._id, r.name, JSON.stringify(r, null, 2));
let bodyType = null;
let body = null;
if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql';
body = convertSyntax(r.body.text);
} else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json';
body = convertSyntax(r.body.text);
}
let authenticationType = null;
let authentication = {};
if (r.authentication.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(r.authentication.token),
};
} else if (r.authentication.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(r.authentication.username),
password: convertSyntax(r.authentication.password),
};
}
return {
id: r._id,
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId,
folderId: r.parentId === workspaceId ? null : r.parentId,
model: 'http_request',
sortPriority,
name: r.name,
url: convertSyntax(r.url),
body,
bodyType,
authentication,
authenticationType,
method: r.method,
headers: (r.headers ?? [])
.map(({ name, value, disabled }) => ({
enabled: !disabled,
name,
value,
}))
.filter(({ name, value }) => name !== '' || value !== ''),
};
}

View File

@@ -1,76 +0,0 @@
import { importEnvironment } from './importers/environment.js';
import { importRequest } from './importers/request.js';
import {
isEnvironment,
isJSObject,
isRequest,
isRequestGroup,
isWorkspace,
} from './helpers/types.js';
import { parseVariables } from './helpers/variables.js';
import { importFolder } from './importers/folder.js';
export function pluginHookImport(contents) {
console.log('RUNNING INSOMNIA');
let parsed;
try {
parsed = JSON.parse(contents);
} catch (e) {
return;
}
if (!isJSObject(parsed)) return;
if (!Array.isArray(parsed.resources)) return;
const resources = {
workspaces: [],
requests: [],
environments: [],
folders: [],
};
// Import workspaces
const workspacesToImport = parsed.resources.filter(isWorkspace);
for (const workspaceToImport of workspacesToImport) {
const baseEnvironment = parsed.resources.find(
(r) => isEnvironment(r) && r.parentId === workspaceToImport._id,
);
resources.workspaces.push({
id: workspaceToImport._id,
createdAt: new Date(workspacesToImport.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(workspacesToImport.updated ?? Date.now()).toISOString().replace('Z', ''),
model: 'workspace',
name: workspaceToImport.name,
variables: baseEnvironment ? parseVariables(baseEnvironment.data) : [],
});
const environmentsToImport = parsed.resources.filter(
(r) => isEnvironment(r) && r.parentId === baseEnvironment?._id,
);
resources.environments.push(
...environmentsToImport.map((r) => importEnvironment(r, workspaceToImport._id)),
);
const nextFolder = (parentId) => {
const children = parsed.resources.filter((r) => r.parentId === parentId);
let sortPriority = 0;
for (const child of children) {
if (isRequestGroup(child)) {
resources.folders.push(importFolder(child, workspaceToImport._id));
nextFolder(child._id);
} else if (isRequest(child)) {
resources.requests.push(importRequest(child, workspaceToImport._id, sortPriority++));
}
}
};
// Import folders
nextFolder(workspaceToImport._id);
}
// Filter out any `null` values
resources.requests = resources.requests.filter(Boolean);
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources };
}

View File

@@ -0,0 +1,278 @@
import {
Environment,
Folder,
GrpcRequest,
HttpRequest,
Workspace,
} from '../../../src-web/lib/models';
import { parse as parseYaml } from 'yaml';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
export interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
grpcRequests: AtLeast<GrpcRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export function pluginHookImport(contents: string) {
let parsed: any;
try {
parsed = JSON.parse(contents);
} catch (e) {}
try {
parsed = parseYaml(contents);
} catch (e) {}
if (!isJSObject(parsed)) return;
if (!Array.isArray(parsed.resources)) return;
const resources: ExportResources = {
workspaces: [],
httpRequests: [],
grpcRequests: [],
environments: [],
folders: [],
};
// Import workspaces
const workspacesToImport = parsed.resources.filter(isWorkspace);
for (const workspaceToImport of workspacesToImport) {
const baseEnvironment = parsed.resources.find(
(r: any) => isEnvironment(r) && r.parentId === workspaceToImport._id,
);
resources.workspaces.push({
id: convertId(workspaceToImport._id),
createdAt: new Date(workspacesToImport.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(workspacesToImport.updated ?? Date.now()).toISOString().replace('Z', ''),
model: 'workspace',
name: workspaceToImport.name,
variables: baseEnvironment ? parseVariables(baseEnvironment.data) : [],
});
const environmentsToImport = parsed.resources.filter(
(r: any) => isEnvironment(r) && r.parentId === baseEnvironment?._id,
);
resources.environments.push(
...environmentsToImport.map((r: any) => importEnvironment(r, workspaceToImport._id)),
);
const nextFolder = (parentId: string) => {
const children = parsed.resources.filter((r: any) => r.parentId === parentId);
let sortPriority = 0;
for (const child of children) {
if (isRequestGroup(child)) {
resources.folders.push(importFolder(child, workspaceToImport._id));
nextFolder(child._id);
} else if (isHttpRequest(child)) {
resources.httpRequests.push(
importHttpRequest(child, workspaceToImport._id, sortPriority++),
);
} else if (isGrpcRequest(child)) {
resources.grpcRequests.push(
importGrpcRequest(child, workspaceToImport._id, sortPriority++),
);
}
}
};
// Import folders
nextFolder(workspaceToImport._id);
}
// Filter out any `null` values
resources.httpRequests = resources.httpRequests.filter(Boolean);
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources };
}
function importEnvironment(e: any, workspaceId: string): ExportResources['environments'][0] {
return {
id: convertId(e._id),
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: convertId(workspaceId),
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}
function importFolder(f: any, workspaceId: string): ExportResources['folders'][0] {
return {
id: convertId(f._id),
createdAt: new Date(f.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace('Z', ''),
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
workspaceId: convertId(workspaceId),
model: 'folder',
name: f.name,
};
}
function importGrpcRequest(
r: any,
workspaceId: string,
sortPriority = 0,
): ExportResources['grpcRequests'][0] {
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
const service = parts[0] ?? null;
const method = parts[1] ?? null;
return {
id: convertId(r._id),
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'grpc_request',
sortPriority,
name: r.name,
url: convertSyntax(r.url),
service,
method,
message: r.body?.text ?? '',
metadata: (r.metadata ?? [])
.map((h: any) => ({
enabled: !h.disabled,
name: h.name ?? '',
value: h.value ?? '',
}))
.filter(({ name, value }: any) => name !== '' || value !== ''),
};
}
function importHttpRequest(
r: any,
workspaceId: string,
sortPriority = 0,
): ExportResources['httpRequests'][0] {
let bodyType = null;
let body = {};
if (r.body.mimeType === 'application/octet-stream') {
bodyType = 'binary';
body = { filePath: r.body.fileName ?? '' };
} else if (r.body?.mimeType === 'application/x-www-form-urlencoded') {
bodyType = 'application/x-www-form-urlencoded';
body = {
form: (r.body.params ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
};
} else if (r.body?.mimeType === 'multipart/form-data') {
bodyType = 'multipart/form-data';
body = {
form: (r.body.params ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
file: p.fileName ?? null,
})),
};
} else if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql';
body = { text: convertSyntax(r.body.text ?? '') };
} else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json';
body = { text: convertSyntax(r.body.text ?? '') };
}
let authenticationType = null;
let authentication = {};
if (r.authentication.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(r.authentication.token),
};
} else if (r.authentication.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(r.authentication.username),
password: convertSyntax(r.authentication.password),
};
}
return {
id: convertId(r._id),
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'http_request',
sortPriority,
name: r.name,
url: convertSyntax(r.url),
body,
bodyType,
authentication,
authenticationType,
method: r.method,
headers: (r.headers ?? [])
.map((h: any) => ({
enabled: !h.disabled,
name: h.name ?? '',
value: h.value ?? '',
}))
.filter(({ name, value }: any) => name !== '' || value !== ''),
};
}
function parseVariables(data: Record<string, string>) {
return Object.entries(data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
}));
}
function convertSyntax(variable: string): string {
if (!isJSString(variable)) return variable;
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
}
function isWorkspace(obj: any) {
return isJSObject(obj) && obj._type === 'workspace';
}
function isRequestGroup(obj: any) {
return isJSObject(obj) && obj._type === 'request_group';
}
function isHttpRequest(obj: any) {
return isJSObject(obj) && obj._type === 'request';
}
function isGrpcRequest(obj: any) {
return isJSObject(obj) && obj._type === 'grpc_request';
}
function isEnvironment(obj: any) {
return isJSObject(obj) && obj._type === 'environment';
}
function isJSObject(obj: any) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isJSString(obj: any) {
return Object.prototype.toString.call(obj) === '[object String]';
}
function convertId(id: string): string {
if (id.startsWith('GENERATE_ID::')) {
return id;
}
return `GENERATE_ID::${id}`;
}

View File

@@ -4,7 +4,7 @@ import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,7 @@
{
"name": "importer-postman",
"version": "0.0.1"
"version": "0.0.1",
"devDependencies": {
"vitest": "^1.4.0"
}
}

View File

@@ -1,4 +1,4 @@
import { Environment, Folder, HttpRequest, Workspace } from '../../../src-web/lib/models';
import { Environment, Folder, HttpRequest, Model, Workspace } from '../../../src-web/lib/models';
const POSTMAN_2_1_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
const POSTMAN_2_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json';
@@ -9,7 +9,7 @@ type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
requests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
@@ -23,18 +23,25 @@ export function pluginHookImport(contents: string): { resources: ExportResources
return;
}
const globalAuth = importAuth(root.auth);
const exportResources: ExportResources = {
workspaces: [],
environments: [],
requests: [],
httpRequests: [],
folders: [],
};
const workspace: ExportResources['workspaces'][0] = {
model: 'workspace',
id: generateId('wk'),
id: generateId('workspace'),
name: info.name || 'Postman Import',
description: info.description || '',
variables:
root.variable?.map((v: any) => ({
name: v.key,
value: v.value,
})) ?? [],
};
exportResources.workspaces.push(workspace);
@@ -43,7 +50,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
const folder: ExportResources['folders'][0] = {
model: 'folder',
workspaceId: workspace.id,
id: generateId('fl'),
id: generateId('folder'),
name: v.name,
folderId,
};
@@ -54,10 +61,11 @@ export function pluginHookImport(contents: string): { resources: ExportResources
} else if (typeof v.name === 'string' && 'request' in v) {
const r = toRecord(v.request);
const bodyPatch = importBody(r.body);
const authPatch = importAuth(r.auth);
const request: ExportResources['requests'][0] = {
const requestAuthPath = importAuth(r.auth);
const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath;
const request: ExportResources['httpRequests'][0] = {
model: 'http_request',
id: generateId('rq'),
id: generateId('http_request'),
workspaceId: workspace.id,
folderId,
name: v.name,
@@ -79,7 +87,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
}),
],
};
exportResources.requests.push(request);
exportResources.httpRequests.push(request);
} else {
console.log('Unknown item', v, folderId);
}
@@ -105,6 +113,14 @@ function importAuth(
password: auth.basic.password || '',
},
};
} else if ('bearer' in auth) {
return {
headers: [],
authenticationType: 'bearer',
authentication: {
token: auth.bearer.token || '',
},
};
} else {
// TODO: support other auth types
return { headers: [], authenticationType: null, authentication: {} };
@@ -175,6 +191,20 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
),
},
};
} else if ('raw' in body) {
return {
headers: [
{
name: 'Content-Type',
value: body.options?.raw?.language === 'json' ? 'application/json' : '',
enabled: true,
},
],
bodyType: body.options?.raw?.language === 'json' ? 'application/json' : 'other',
body: {
text: body.raw ?? '',
},
};
} else {
// TODO: support other body types
return { headers: [], bodyType: null, body: {} };
@@ -213,11 +243,8 @@ function convertTemplateSyntax<T>(obj: T): T {
}
}
function generateId(prefix: 'wk' | 'rq' | 'fl'): string {
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = `${prefix}_`;
for (let i = 0; i < 10; i++) {
id += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return id;
const idCount: Partial<Record<Model['model'], number>> = {};
function generateId(model: Model['model']): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}

View File

@@ -0,0 +1,38 @@
{
"info": {
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
"name": "New Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "18798"
},
"item": [
{
"name": "Top Folder",
"item": [
{
"name": "Nested Folder",
"item": [
{
"name": "Request 1",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 2",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 3",
"request": {
"method": "GET"
}
}
]
}

View File

@@ -0,0 +1,90 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { Model } from '../../../src-web/lib/models';
import { pluginHookImport } from '../src';
let originalRandom = Math.random;
describe('importer-postman', () => {
beforeEach(() => {
let i = 0;
// Psuedo-random number generator to ensure consistent ID generation
Math.random = vi.fn(() => ((i++ * 1000) % 133) / 100);
});
afterEach(() => {
Math.random = originalRandom;
});
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
for (const fixture of fixtures) {
test('Imports ' + fixture, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const imported = pluginHookImport(contents);
const folder0 = newId('folder');
const folder1 = newId('folder');
expect(imported).toEqual({
resources: expect.objectContaining({
workspaces: [
expect.objectContaining({
id: newId('workspace'),
model: 'workspace',
name: 'New Collection',
}),
],
folders: expect.arrayContaining([
expect.objectContaining({
id: folder0,
model: 'folder',
workspaceId: existingId('workspace'),
name: 'Top Folder',
}),
expect.objectContaining({
folderId: folder0,
id: folder1,
model: 'folder',
workspaceId: existingId('workspace'),
name: 'Nested Folder',
}),
]),
httpRequests: expect.arrayContaining([
expect.objectContaining({
id: newId('http_request'),
model: 'http_request',
name: 'Request 1',
workspaceId: existingId('workspace'),
folderId: folder1,
}),
expect.objectContaining({
id: newId('http_request'),
model: 'http_request',
name: 'Request 2',
workspaceId: existingId('workspace'),
folderId: folder0,
}),
expect.objectContaining({
id: newId('http_request'),
model: 'http_request',
name: 'Request 3',
workspaceId: existingId('workspace'),
folderId: null,
}),
]),
}),
});
});
}
});
const idCount: Partial<Record<Model['model'], number>> = {};
function newId(model: Model['model']): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}
function existingId(model: Model['model']): string {
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model] ?? 0}`;
}

View File

@@ -1,4 +1,4 @@
export function pluginHookImport(contents) {
export function pluginHookImport(contents: string) {
let parsed;
try {
parsed = JSON.parse(contents);
@@ -10,23 +10,20 @@ export function pluginHookImport(contents) {
return undefined;
}
if (!('yaakSchema' in parsed)) {
const isYaakExport = 'yaakSchema' in parsed;
if (!isYaakExport) {
return;
}
// Migrate v1 to v2 -- changes requests to httpRequests
if (parsed.yaakSchema === 1) {
if ('requests' in parsed.resources) {
parsed.resources.httpRequests = parsed.resources.requests;
parsed.yaakSchema = 2;
delete parsed.resources.requests;
}
if (parsed.yaakSchema === 2) {
return { resources: parsed.resources }; // Should already be in the correct format
}
return undefined;
return { resources: parsed.resources }; // Should already be in the correct format
}
export function isJSObject(obj) {
export function isJSObject(obj: any) {
return Object.prototype.toString.call(obj) === '[object Object]';
}

View File

@@ -0,0 +1,29 @@
import { describe, expect, test } from 'vitest';
import { pluginHookImport } from '../src';
describe('importer-yaak', () => {
test('Skips invalid imports', () => {
expect(pluginHookImport('not JSON')).toBeUndefined();
expect(pluginHookImport('[]')).toBeUndefined();
expect(pluginHookImport(JSON.stringify({ resources: {} }))).toBeUndefined();
});
test('converts schema 1 to 2', () => {
const imported = pluginHookImport(
JSON.stringify({
yaakSchema: 1,
resources: {
requests: [],
},
}),
);
expect(imported).toEqual(
expect.objectContaining({
resources: {
httpRequests: [],
},
}),
);
});
});

View File

@@ -4,7 +4,7 @@ import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es'],
},

3404
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,20 @@
workspace = { members = ["grpc"] }
[package]
name = "yaak-app"
version = "0.0.0"
description = "A network protocol testing utility app"
authors = ["Gregory Schier"]
license = "MIT"
repository = "https://github.com/gschier/yaak-app"
edition = "2021"
# Produce a library for mobile support
[lib]
name = "tauri_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[profile.release]
strip = true # Automatically strip symbols from the binary.
[build-dependencies]
tauri-build = { version = "1.5", features = [] }
tauri-build = { version = "2.0.0-beta", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
@@ -22,53 +24,30 @@ cocoa = "0.25.0"
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
base64 = "0.21.0"
boa_engine = { version = "0.17.3", features = ["annex-b"] }
boa_runtime = { version = "0.17.3" }
base64 = "0.22.0"
boa_engine = { version = "0.18.0", features = ["annex-b"] }
boa_runtime = { version = "0.18.0" }
chrono = { version = "0.4.31", features = ["serde"] }
futures = "0.3.26"
http = "0.2.8"
http = "0.2.10"
rand = "0.8.5"
reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "brotli", "deflate"] }
cookie = { version = "0.18.0" }
serde = { version = "1.0.195", features = ["derive"] }
serde_json = { version = "1.0.111", features = ["raw_value"] }
sqlx = { version = "0.7.3", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
tauri = { version = "1.5.4", features = [
"config-toml",
"path-all",
"devtools",
"dialog-open",
"dialog-save",
"fs-read-file",
"os-all",
"protocol-asset",
"shell-open",
"shell-sidecar",
"updater",
"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"] }
tokio = { version = "1.25.0", features = ["sync"] }
uuid = "1.3.0"
log = "0.4.20"
reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json"] }
serde = { version = "1.0.198", features = ["derive"] }
serde_json = { version = "1.0.116", features = ["raw_value"] }
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
tauri = { version = "2.0.0-beta.19", features = ["config-toml", "devtools", "protocol-asset"] }
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2", features = ["colored"] }
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tokio = { version = "1.36.0", features = ["sync"] }
uuid = "1.7.0"
log = "0.4.21"
datetime = "0.5.2"
window-shadows = "0.2.2"
reqwest_cookie_store = "0.6.0"
grpc = { path = "./grpc" }
tokio-stream = "0.1.14"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
tokio-stream = "0.1.15"
regex = "1.10.2"

View File

@@ -0,0 +1,53 @@
{
"$schema": "../gen/schemas/capabilities.json",
"identifier": "main",
"description": "Main permissions",
"local": true,
"windows": [
"*"
],
"permissions": [
"os:allow-os-type",
"event:allow-emit",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"dialog:allow-open",
"dialog:allow-save",
"event:allow-listen",
"event:allow-unlisten",
"fs:allow-read-file",
"fs:allow-read-text-file",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$APPDATA"
},
{
"path": "$APPDATA/**"
}
]
},
"shell:allow-open",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "protoc",
"sidecar": true,
"args": true
}
]
},
"window:allow-close",
"window:allow-is-fullscreen",
"window:allow-maximize",
"window:allow-minimize",
"window:allow-set-decorations",
"window:allow-set-title",
"window:allow-start-dragging",
"window:allow-unmaximize",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text"
]
}

3
src-tauri/gen/apple/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
xcuserdata/
build/
Externals/

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,116 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "AppIcon-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "AppIcon-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "AppIcon-29x29@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "AppIcon-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "AppIcon-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "AppIcon-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "AppIcon-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "AppIcon-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "AppIcon-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "AppIcon-20x20@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "AppIcon-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "AppIcon-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "AppIcon-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "AppIcon-40x40@2x-1.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "AppIcon-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "AppIcon-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "AppIcon-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "AppIcon-512@2x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>development</string>
</dict>
</plist>

View File

@@ -0,0 +1,21 @@
# Uncomment the next line to define a global platform for your project
target 'yaak-app_iOS' do
platform :ios, '13.0'
# Pods for yaak-app_iOS
end
target 'yaak-app_macOS' do
platform :osx, '11.0'
# Pods for yaak-app_macOS
end
# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET'
end
end
end

View File

@@ -0,0 +1,8 @@
#pragma once
namespace ffi {
extern "C" {
void start_app();
}
}

View File

@@ -0,0 +1,6 @@
#include "bindings/bindings.h"
int main(int argc, char * argv[]) {
ffi::start_app();
return 0;
}

View File

@@ -0,0 +1,90 @@
name: yaak-app
options:
bundleIdPrefix: app.yaak
deploymentTarget:
iOS: 13.0
fileGroups: [../../src]
configs:
debug: debug
release: release
settingGroups:
app:
base:
PRODUCT_NAME: Yaak
PRODUCT_BUNDLE_IDENTIFIER: app.yaak.yaak-app
DEVELOPMENT_TEAM: 7PU3P6ELJ8
targetTemplates:
app:
type: application
sources:
- path: Sources
scheme:
environmentVariables:
RUST_BACKTRACE: full
RUST_LOG: info
settings:
groups: [app]
targets:
yaak-app_iOS:
type: application
platform: iOS
sources:
- path: Sources
- path: Assets.xcassets
- path: Externals
- path: yaak-app_iOS
- path: assets
buildPhase: resources
type: folder
info:
path: yaak-app_iOS/Info.plist
properties:
LSRequiresIPhoneOS: true
UILaunchStoryboardName: LaunchScreen
UIRequiredDeviceCapabilities: [arm64, metal]
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
CFBundleShortVersionString: 2024.3.10
CFBundleVersion: 2024.3.10
entitlements:
path: yaak-app_iOS/yaak-app_iOS.entitlements
scheme:
environmentVariables:
RUST_BACKTRACE: full
RUST_LOG: info
settings:
base:
ENABLE_BITCODE: false
ARCHS: [arm64, arm64-sim]
VALID_ARCHS: arm64 arm64-sim
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
LIBRARY_SEARCH_PATHS[arch=arm64-sim]: $(inherited) $(PROJECT_DIR)/Externals/arm64-sim/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
EXCLUDED_ARCHS[sdk=iphonesimulator*]: arm64
EXCLUDED_ARCHS[sdk=iphoneos*]: arm64-sim x86_64
groups: [app]
dependencies:
- framework: libapp_lib.a
embed: false
- sdk: CoreGraphics.framework
- sdk: Metal.framework
- sdk: MetalKit.framework
- sdk: QuartzCore.framework
- sdk: Security.framework
- sdk: UIKit.framework
- sdk: WebKit.framework
preBuildScripts:
- script: node tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
name: Build Rust Code
basedOnDependencyAnalysis: false
outputFiles:
- $(SRCROOT)/target/aarch64-apple-ios/${CONFIGURATION}/deps/libapp_lib.a
- $(SRCROOT)/target/x86_64-apple-ios/${CONFIGURATION}/deps/libapp_lib.a

View File

@@ -0,0 +1,481 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
0AC23E163631EF3775908406 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDEF0D2E01608E7F464F71B6 /* WebKit.framework */; };
1B1BFDF8BC345D0D980E4427 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF0B8CF73BE8166011E2CEAB /* QuartzCore.framework */; };
36588BE1A75B386BB2FEDAC5 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A93A95F6B2F31FA92AA099E0 /* MetalKit.framework */; };
38E2C1B0E9FCC9A5FDE8876D /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF2908761467DF191C2A8939 /* Metal.framework */; };
8D518C1A67069BD7D339D055 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F34A96C5084EFDF1802A634 /* CoreGraphics.framework */; };
8DF67739DC49E577EB0FAE3F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 396F45DCFBE2C71866817528 /* Assets.xcassets */; };
A1D932F0E7399066AD07DC6D /* libapp_lib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 75D938BE0FA8770BA965AE1E /* libapp_lib.a */; };
AF0EEC868306E1D1C85994D0 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B5BF2E39256494269E65F8E /* Security.framework */; };
BE9FFDF51EB7DEBF707BB39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5415A3F2D217A47DD3BA40B3 /* UIKit.framework */; };
F0627C04787F4E187EF416F4 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 2A615609009B3AE2728043E4 /* assets */; };
FEE5934F5FFB0FBE10883AF2 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = C754460F1DAF2D414038A7EA /* main.mm */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
106BE62BE01A35403424018C /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = "<group>"; };
14F240DAC31C5C52D7B4BB96 /* window_ext.rs */ = {isa = PBXFileReference; path = window_ext.rs; sourceTree = "<group>"; };
1B5226A88D8B805E878524C8 /* updates.rs */ = {isa = PBXFileReference; path = updates.rs; sourceTree = "<group>"; };
1F34A96C5084EFDF1802A634 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
1F5A647F82A24722F3C830BB /* plugin.rs */ = {isa = PBXFileReference; path = plugin.rs; sourceTree = "<group>"; };
2A615609009B3AE2728043E4 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; };
396F45DCFBE2C71866817528 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
53872C1120171EDC4A6DFEDD /* analytics.rs */ = {isa = PBXFileReference; path = analytics.rs; sourceTree = "<group>"; };
5415A3F2D217A47DD3BA40B3 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
5C1B6610F62B56E1947BEBBD /* http.rs */ = {isa = PBXFileReference; path = http.rs; sourceTree = "<group>"; };
6286C385ABAD2E04237679D7 /* yaak-app_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "yaak-app_iOS.entitlements"; sourceTree = "<group>"; };
75D938BE0FA8770BA965AE1E /* libapp_lib.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp_lib.a; sourceTree = "<group>"; };
7B5BF2E39256494269E65F8E /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
A2CC02313D71CECB68031D53 /* grpc.rs */ = {isa = PBXFileReference; path = grpc.rs; sourceTree = "<group>"; };
A6DA9B210723CA84891876F8 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
A93A95F6B2F31FA92AA099E0 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
C754460F1DAF2D414038A7EA /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
D69BFB768591FDEEF65198EE /* yaak-app_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "yaak-app_iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
DDDE197D9C6BC5680EEEEA00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
DF2908761467DF191C2A8939 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
DF45D08D97DE587CABF9537E /* window_menu.rs */ = {isa = PBXFileReference; path = window_menu.rs; sourceTree = "<group>"; };
E1E84E267D81D6437901B1C6 /* render.rs */ = {isa = PBXFileReference; path = render.rs; sourceTree = "<group>"; };
E964D3637BAED49E34B91739 /* models.rs */ = {isa = PBXFileReference; path = models.rs; sourceTree = "<group>"; };
EDEF0D2E01608E7F464F71B6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
EF0B8CF73BE8166011E2CEAB /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
FB34CB48BB9F25D49F80D513 /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D8E8888B0F3E4411B98AE8EE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A1D932F0E7399066AD07DC6D /* libapp_lib.a in Frameworks */,
8D518C1A67069BD7D339D055 /* CoreGraphics.framework in Frameworks */,
38E2C1B0E9FCC9A5FDE8876D /* Metal.framework in Frameworks */,
36588BE1A75B386BB2FEDAC5 /* MetalKit.framework in Frameworks */,
1B1BFDF8BC345D0D980E4427 /* QuartzCore.framework in Frameworks */,
AF0EEC868306E1D1C85994D0 /* Security.framework in Frameworks */,
BE9FFDF51EB7DEBF707BB39A /* UIKit.framework in Frameworks */,
0AC23E163631EF3775908406 /* WebKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0844ACEFE550685042AC6029 /* Products */ = {
isa = PBXGroup;
children = (
D69BFB768591FDEEF65198EE /* yaak-app_iOS.app */,
);
name = Products;
sourceTree = "<group>";
};
6D0B6FF641B88DAF74C78B00 /* Externals */ = {
isa = PBXGroup;
children = (
);
path = Externals;
sourceTree = "<group>";
};
8A07575CB8654BB9107F9A32 /* bindings */ = {
isa = PBXGroup;
children = (
A6DA9B210723CA84891876F8 /* bindings.h */,
);
path = bindings;
sourceTree = "<group>";
};
8F0B46911FBEF2B246BE3385 /* yaak-app */ = {
isa = PBXGroup;
children = (
C754460F1DAF2D414038A7EA /* main.mm */,
8A07575CB8654BB9107F9A32 /* bindings */,
);
path = "yaak-app";
sourceTree = "<group>";
};
90E982C0E9B45CBAAE66E16D /* Frameworks */ = {
isa = PBXGroup;
children = (
1F34A96C5084EFDF1802A634 /* CoreGraphics.framework */,
75D938BE0FA8770BA965AE1E /* libapp_lib.a */,
DF2908761467DF191C2A8939 /* Metal.framework */,
A93A95F6B2F31FA92AA099E0 /* MetalKit.framework */,
EF0B8CF73BE8166011E2CEAB /* QuartzCore.framework */,
7B5BF2E39256494269E65F8E /* Security.framework */,
5415A3F2D217A47DD3BA40B3 /* UIKit.framework */,
EDEF0D2E01608E7F464F71B6 /* WebKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
C88F9D29DC52F052255C35A3 = {
isa = PBXGroup;
children = (
2A615609009B3AE2728043E4 /* assets */,
396F45DCFBE2C71866817528 /* Assets.xcassets */,
6D0B6FF641B88DAF74C78B00 /* Externals */,
EBC83899FBFA4A3D0A92837F /* Sources */,
F3A6B45E25E23922AB1BDB34 /* src */,
D49CF68C9105CE84E2084C14 /* yaak-app_iOS */,
90E982C0E9B45CBAAE66E16D /* Frameworks */,
0844ACEFE550685042AC6029 /* Products */,
);
sourceTree = "<group>";
};
D49CF68C9105CE84E2084C14 /* yaak-app_iOS */ = {
isa = PBXGroup;
children = (
DDDE197D9C6BC5680EEEEA00 /* Info.plist */,
6286C385ABAD2E04237679D7 /* yaak-app_iOS.entitlements */,
);
path = "yaak-app_iOS";
sourceTree = "<group>";
};
EBC83899FBFA4A3D0A92837F /* Sources */ = {
isa = PBXGroup;
children = (
8F0B46911FBEF2B246BE3385 /* yaak-app */,
);
path = Sources;
sourceTree = "<group>";
};
F3A6B45E25E23922AB1BDB34 /* src */ = {
isa = PBXGroup;
children = (
53872C1120171EDC4A6DFEDD /* analytics.rs */,
A2CC02313D71CECB68031D53 /* grpc.rs */,
5C1B6610F62B56E1947BEBBD /* http.rs */,
FB34CB48BB9F25D49F80D513 /* lib.rs */,
106BE62BE01A35403424018C /* main.rs */,
E964D3637BAED49E34B91739 /* models.rs */,
1F5A647F82A24722F3C830BB /* plugin.rs */,
E1E84E267D81D6437901B1C6 /* render.rs */,
1B5226A88D8B805E878524C8 /* updates.rs */,
14F240DAC31C5C52D7B4BB96 /* window_ext.rs */,
DF45D08D97DE587CABF9537E /* window_menu.rs */,
);
name = src;
path = ../../src;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
7C3E2AC18A0A227C2DF356E2 /* yaak-app_iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = C05E07AE7C7B25CACFADCDD4 /* Build configuration list for PBXNativeTarget "yaak-app_iOS" */;
buildPhases = (
5454ED506FC51D41C81A0318 /* Build Rust Code */,
C3495A2849227C6276D3876E /* Sources */,
E148188313FB67F061AB4E59 /* Resources */,
D8E8888B0F3E4411B98AE8EE /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "yaak-app_iOS";
productName = "yaak-app_iOS";
productReference = D69BFB768591FDEEF65198EE /* yaak-app_iOS.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
A8F6206BC76F061F1FEFD439 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
7C3E2AC18A0A227C2DF356E2 = {
DevelopmentTeam = 7PU3P6ELJ8;
};
};
};
buildConfigurationList = 24EF8D1B948FFF6B275FB0F4 /* Build configuration list for PBXProject "yaak-app" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = C88F9D29DC52F052255C35A3;
projectDirPath = "";
projectRoot = "";
targets = (
7C3E2AC18A0A227C2DF356E2 /* yaak-app_iOS */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
E148188313FB67F061AB4E59 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8DF67739DC49E577EB0FAE3F /* Assets.xcassets in Resources */,
F0627C04787F4E187EF416F4 /* assets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
5454ED506FC51D41C81A0318 /* Build Rust Code */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Build Rust Code";
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/target/aarch64-apple-ios/${CONFIGURATION}/deps/libapp_lib.a",
"$(SRCROOT)/target/x86_64-apple-ios/${CONFIGURATION}/deps/libapp_lib.a",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "node tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C3495A2849227C6276D3876E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FEE5934F5FFB0FBE10883AF2 /* main.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
35D1DB294FFD067C835186C7 /* debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = debug;
};
368BB1E364597E7675463634 /* release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ARCHS = (
arm64,
"arm64-sim",
);
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "yaak-app_iOS/yaak-app_iOS.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = 7PU3P6ELJ8;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"\".\"",
);
INFOPLIST_FILE = "yaak-app_iOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[arch=arm64-sim]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64-sim/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = "app.yaak.yaak-app";
PRODUCT_NAME = Yaak;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALID_ARCHS = "arm64 arm64-sim";
};
name = release;
};
45382E89556BF93E8D1F1C2D /* debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ARCHS = (
arm64,
"arm64-sim",
);
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "yaak-app_iOS/yaak-app_iOS.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = 7PU3P6ELJ8;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"\".\"",
);
INFOPLIST_FILE = "yaak-app_iOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[arch=arm64-sim]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64-sim/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = "app.yaak.yaak-app";
PRODUCT_NAME = Yaak;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALID_ARCHS = "arm64 arm64-sim";
};
name = debug;
};
ABD0A3DD3D5C66C839496F44 /* release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
};
name = release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
24EF8D1B948FFF6B275FB0F4 /* Build configuration list for PBXProject "yaak-app" */ = {
isa = XCConfigurationList;
buildConfigurations = (
35D1DB294FFD067C835186C7 /* debug */,
ABD0A3DD3D5C66C839496F44 /* release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = debug;
};
C05E07AE7C7B25CACFADCDD4 /* Build configuration list for PBXNativeTarget "yaak-app_iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
45382E89556BF93E8D1F1C2D /* debug */,
368BB1E364597E7675463634 /* release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = debug;
};
/* End XCConfigurationList section */
};
rootObject = A8F6206BC76F061F1FEFD439 /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Original</string>
<key>DisableBuildSystemDeprecationDiagnostic</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7C3E2AC18A0A227C2DF356E2"
BuildableName = "Yaak.app"
BlueprintName = "yaak-app_iOS"
ReferencedContainer = "container:yaak-app.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7C3E2AC18A0A227C2DF356E2"
BuildableName = "Yaak.app"
BlueprintName = "yaak-app_iOS"
ReferencedContainer = "container:yaak-app.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "RUST_BACKTRACE"
value = "full"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "RUST_LOG"
value = "info"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7C3E2AC18A0A227C2DF356E2"
BuildableName = "Yaak.app"
BlueprintName = "yaak-app_iOS"
ReferencedContainer = "container:yaak-app.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "RUST_BACKTRACE"
value = "full"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "RUST_LOG"
value = "info"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "release"
shouldUseLaunchSchemeArgsEnv = "NO"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7C3E2AC18A0A227C2DF356E2"
BuildableName = "Yaak.app"
BlueprintName = "yaak-app_iOS"
ReferencedContainer = "container:yaak-app.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "RUST_BACKTRACE"
value = "full"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "RUST_LOG"
value = "info"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2024.3.10</string>
<key>CFBundleVersion</key>
<string>2024.3.10</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
<string>metal</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -14,10 +14,9 @@ serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
prost-reflect = { version = "0.12.0", features = ["serde", "derive"] }
log = "0.4.20"
once_cell = { version = "1.19.0", features = [] }
anyhow = "1.0.79"
hyper = { version = "0.14" }
hyper-rustls = { version = "0.24.0", features = ["http2"] }
protoc-bin-vendored = "3.0.0"
uuid = { version = "1.7.0", features = ["v4"] }
tauri = { version = "1.5.4", features = ["process-command-api"]}
tauri = { version = "2.0.0-beta.16" }
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }

View File

@@ -174,7 +174,10 @@ pub struct GrpcHandle {
impl GrpcHandle {
pub fn new(app_handle: &AppHandle) -> Self {
let pools = HashMap::new();
Self { pools, app_handle: app_handle.clone() }
Self {
pools,
app_handle: app_handle.clone(),
}
}
}

View File

@@ -1,7 +1,7 @@
use std::env::temp_dir;
use std::ops::Deref;
use std::path::PathBuf;
use std::str::FromStr;
use std::str::{from_utf8, FromStr};
use anyhow::anyhow;
use hyper::client::HttpConnector;
@@ -11,8 +11,10 @@ use log::{debug, info, warn};
use prost::Message;
use prost_reflect::{DescriptorPool, MethodDescriptor};
use prost_types::{FileDescriptorProto, FileDescriptorSet};
use tauri::api::process::{Command, CommandEvent};
use tauri::AppHandle;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager};
use tauri_plugin_shell::process::CommandEvent;
use tauri_plugin_shell::ShellExt;
use tokio::fs;
use tokio_stream::StreamExt;
use tonic::body::BoxBody;
@@ -32,8 +34,8 @@ pub async fn fill_pool_from_files(
let random_file_name = format!("{}.desc", uuid::Uuid::new_v4());
let desc_path = temp_dir().join(random_file_name);
let global_import_dir = app_handle
.path_resolver()
.resolve_resource("protoc-vendored/include")
.path()
.resolve("protoc-vendored/include", BaseDirectory::Resource)
.expect("failed to resolve protoc include directory");
let mut args = vec![
@@ -63,7 +65,9 @@ pub async fn fill_pool_from_files(
}
}
let (mut rx, _child) = Command::new_sidecar("protoc")
let (mut rx, _child) = app_handle
.shell()
.sidecar("protoc")
.expect("protoc not found")
.args(args)
.spawn()
@@ -72,10 +76,16 @@ pub async fn fill_pool_from_files(
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
info!("protoc stdout: {}", line);
info!(
"protoc stdout: {}",
from_utf8(line.as_slice()).unwrap_or_default().to_string()
);
}
CommandEvent::Stderr(line) => {
info!("protoc stderr: {}", line);
info!(
"protoc stderr: {}",
from_utf8(line.as_slice()).unwrap_or_default().to_string()
);
}
CommandEvent::Error(e) => {
return Err(e.to_string());

View File

@@ -0,0 +1,36 @@
const o = `\\
`;
function d(n) {
var h, f, r, u, l, s;
const t = ["curl"];
n.method && t.push("-X", n.method), n.url && t.push(i(n.url)), t.push(o);
for (const a of (n.urlParameters ?? []).filter(p))
t.push("--url-query", i(`${a.name}=${a.value}`)), t.push(o);
for (const a of (n.headers ?? []).filter(p))
t.push("--header", i(`${a.name}: ${a.value}`)), t.push(o);
if (Array.isArray((h = n.body) == null ? void 0 : h.form)) {
const a = n.bodyType === "multipart/form-data" ? "--form" : "--data";
for (const e of (((f = n.body) == null ? void 0 : f.form) ?? []).filter(p)) {
if (e.file) {
let c = `${e.name}=@${e.file}`;
c += e.contentType ? `;type=${e.contentType}` : "", t.push(a, c);
} else
t.push(a, i(`${e.name}=${e.value}`));
t.push(o);
}
} else
typeof ((r = n.body) == null ? void 0 : r.text) == "string" && (t.push("--data-raw", `$${i(n.body.text)}`), t.push(o));
return (n.authenticationType === "basic" || n.authenticationType === "digest") && (n.authenticationType === "digest" && t.push("--digest"), t.push(
"--user",
i(`${((u = n.authentication) == null ? void 0 : u.username) ?? ""}:${((l = n.authentication) == null ? void 0 : l.password) ?? ""}`)
), t.push(o)), n.authenticationType === "bearer" && (t.push("--header", i(`Authorization: Bearer ${((s = n.authentication) == null ? void 0 : s.token) ?? ""}`)), t.push(o)), t[t.length - 1] === o && t.splice(t.length - 1, 1), t.join(" ");
}
function i(n) {
return `'${n.replace(/'/g, "\\'")}'`;
}
function p(n) {
return n.enabled !== !1 && !!n.name;
}
export {
d as pluginHookExport
};

View File

@@ -0,0 +1,284 @@
var j = "(?:" + [
"\\|\\|",
"\\&\\&",
";;",
"\\|\\&",
"\\<\\(",
"\\<\\<\\<",
">>",
">\\&",
"<\\&",
"[&;()|<>]"
].join("|") + ")", D = new RegExp("^" + j + "$"), q = "|&;()<> \\t", M = '"((\\\\"|[^"])*?)"', Q = "'((\\\\'|[^'])*?)'", V = /^#$/, _ = "'", G = '"', U = "$", R = "", z = 4294967296;
for (var L = 0; L < 4; L++)
R += (z * Math.random()).toString(16);
var J = new RegExp("^" + R);
function X(n, s) {
for (var e = s.lastIndex, t = [], c; c = s.exec(n); )
t.push(c), s.lastIndex === c.index && (s.lastIndex += 1);
return s.lastIndex = e, t;
}
function F(n, s, e) {
var t = typeof n == "function" ? n(e) : n[e];
return typeof t > "u" && e != "" ? t = "" : typeof t > "u" && (t = "$"), typeof t == "object" ? s + R + JSON.stringify(t) + R : s + t;
}
function K(n, s, e) {
e || (e = {});
var t = e.escape || "\\", c = "(\\" + t + `['"` + q + `]|[^\\s'"` + q + "])+", m = new RegExp([
"(" + j + ")",
// control chars
"(" + c + "|" + M + "|" + Q + ")+"
].join("|"), "g"), f = X(n, m);
if (f.length === 0)
return [];
s || (s = {});
var w = !1;
return f.map(function(r) {
var a = r[0];
if (!a || w)
return;
if (D.test(a))
return { op: a };
var x = !1, O = !1, p = "", A = !1, i;
function b() {
i += 1;
var v, d, T = a.charAt(i);
if (T === "{") {
if (i += 1, a.charAt(i) === "}")
throw new Error("Bad substitution: " + a.slice(i - 2, i + 1));
if (v = a.indexOf("}", i), v < 0)
throw new Error("Bad substitution: " + a.slice(i));
d = a.slice(i, v), i = v;
} else if (/[*@#?$!_-]/.test(T))
d = T, i += 1;
else {
var g = a.slice(i);
v = g.match(/[^\w\d_]/), v ? (d = g.slice(0, v.index), i += v.index - 1) : (d = g, i = a.length);
}
return F(s, "", d);
}
for (i = 0; i < a.length; i++) {
var u = a.charAt(i);
if (A = A || !x && (u === "*" || u === "?"), O)
p += u, O = !1;
else if (x)
u === x ? x = !1 : x == _ ? p += u : u === t ? (i += 1, u = a.charAt(i), u === G || u === t || u === U ? p += u : p += t + u) : u === U ? p += b() : p += u;
else if (u === G || u === _)
x = u;
else {
if (D.test(u))
return { op: a };
if (V.test(u)) {
w = !0;
var E = { comment: n.slice(r.index + i + 1) };
return p.length ? [p, E] : [E];
} else
u === t ? O = !0 : u === U ? p += b() : p += u;
}
}
return A ? { op: "glob", pattern: p } : p;
}).reduce(function(r, a) {
return typeof a > "u" ? r : r.concat(a);
}, []);
}
var Y = function(s, e, t) {
var c = K(s, e, t);
return typeof e != "function" ? c : c.reduce(function(m, f) {
if (typeof f == "object")
return m.concat(f);
var w = f.split(RegExp("(" + R + ".*?" + R + ")", "g"));
return w.length === 1 ? m.concat(w[0]) : m.concat(w.filter(Boolean).map(function(r) {
return J.test(r) ? JSON.parse(r.split(R)[1]) : r;
}));
}, []);
}, Z = Y;
const ae = "curl", se = "cURL", ie = "cURL command line tool", H = ["d", "data", "data-raw", "data-urlencode", "data-binary", "data-ascii"], ee = [
["url"],
// Specify the URL explicitly
["user", "u"],
// Authentication
["digest"],
// Apply auth as digest
["header", "H"],
["cookie", "b"],
["get", "G"],
// Put the post data in the URL
["d", "data"],
// Add url encoded data
["data-raw"],
["data-urlencode"],
["data-binary"],
["data-ascii"],
["form", "F"],
// Add multipart data
["request", "X"],
// Request method
H
].flatMap((n) => n);
function oe(n) {
if (!n.match(/^\s*curl /))
return null;
const s = [], e = n.replace(/\ncurl/g, "; curl");
let t = [];
const m = Z(e).flatMap((r) => typeof r == "string" && r.startsWith("-") && !r.startsWith("--") && r.length > 2 ? [r.slice(0, 2), r.slice(2)] : r);
for (const r of m) {
if (typeof r == "string") {
r.startsWith("$") ? t.push(r.slice(1)) : t.push(r);
continue;
}
if ("comment" in r)
continue;
const { op: a } = r;
if (a === ";") {
s.push(t), t = [];
continue;
}
if (a != null && a.startsWith("$")) {
const x = a.slice(2, a.length - 1).replace(/\\'/g, "'");
t.push(x);
continue;
}
a === "glob" && t.push(r.pattern);
}
s.push(t);
const f = {
model: "workspace",
id: N("workspace"),
name: "Curl Import"
};
return {
resources: {
httpRequests: s.filter((r) => r[0] === "curl").map((r) => te(r, f.id)),
workspaces: [f]
}
};
}
function te(n, s) {
const e = {}, t = [];
for (let o = 1; o < n.length; o++) {
let l = n[o];
if (typeof l == "string" && (l = l.trim()), typeof l == "string" && l.match(/^-{1,2}[\w-]+/)) {
const y = l[0] === "-" && l[1] !== "-";
let h = l.replace(/^-{1,2}/, "");
if (!ee.includes(h))
continue;
let $;
const S = n[o + 1];
y && h.length > 1 ? ($ = h.slice(1), h = h.slice(0, 1)) : typeof S == "string" && !S.startsWith("-") ? ($ = S, o++) : $ = !0, e[h] = e[h] || [], e[h].push($);
} else
l && t.push(l);
}
let c, m;
const f = C(e, t[0] || "", ["url"]), [w, r] = W(f, "?");
c = (r == null ? void 0 : r.split("&").map((o) => {
const l = W(o, "=");
return { name: l[0] ?? "", value: l[1] ?? "" };
})) ?? [], m = w ?? f;
const [a, x] = C(e, "", ["u", "user"]).split(/:(.*)$/), O = C(e, !1, ["digest"]), p = a ? O ? "digest" : "basic" : null, A = a ? {
username: a.trim(),
password: (x ?? "").trim()
} : {}, i = [
...e.header || [],
...e.H || []
].map((o) => {
const [l, y] = o.split(/:(.*)$/);
return y ? {
name: (l ?? "").trim(),
value: y.trim()
} : {
name: (l ?? "").trim().replace(/;$/, ""),
value: ""
};
}), b = [
...e.cookie || [],
...e.b || []
].map((o) => {
const l = o.split("=", 1)[0], y = o.replace(`${l}=`, "");
return `${l}=${y}`;
}).join("; "), u = i.find((o) => o.name.toLowerCase() === "cookie");
b && u ? u.value += `; ${b}` : b && i.push({
name: "Cookie",
value: b
});
const E = ne(e), v = i.find((o) => o.name.toLowerCase() === "content-type"), d = v ? v.value.split(";")[0] : null, T = [
...e.form || [],
...e.F || []
].map((o) => {
const l = o.split("="), y = l[0] ?? "", h = l[1] ?? "", $ = {
name: y,
enabled: !0
};
return h.indexOf("@") === 0 ? $.file = h.slice(1) : $.value = h, $;
});
let g = {}, I = null;
const B = C(e, !1, ["G", "get"]);
E.length > 0 && B ? c.push(...E) : E.length > 0 && (d == null || d === "application/x-www-form-urlencoded") ? (I = d ?? "application/x-www-form-urlencoded", g = {
params: E.map((o) => ({
...o,
name: decodeURIComponent(o.name || ""),
value: decodeURIComponent(o.value || "")
}))
}) : E.length > 0 ? (I = d === "application/json" || d === "text/xml" || d === "text/plain" ? d : "other", g = {
text: E.map(({ name: o, value: l }) => o && l ? `${o}=${l}` : o || l).join("&")
}) : T.length && (I = d ?? "multipart/form-data", g = {
form: T
});
let P = C(e, "", ["X", "request"]).toUpperCase();
return P === "" && g && (P = "text" in g || "params" in g ? "POST" : "GET"), {
id: N("http_request"),
model: "http_request",
workspaceId: s,
name: "",
urlParameters: c,
url: m,
method: P,
headers: i,
authentication: A,
authenticationType: p,
body: g,
bodyType: I,
folderId: null,
sortPriority: 0
};
}
const ne = (n) => {
let s = [];
for (const e of H) {
const t = n[e];
if (!(!t || t.length === 0))
for (const c of t) {
if (typeof c != "string")
continue;
const [m, f] = c.split("=");
c.startsWith("@") ? s.push({
name: m ?? "",
value: "",
filePath: c.slice(1)
}) : s.push({
name: m ?? "",
value: e === "data-urlencode" ? encodeURIComponent(f ?? "") : f ?? ""
});
}
}
return s;
}, C = (n, s, e) => {
for (const t of e)
if (n[t] && n[t].length)
return n[t][0];
return s;
};
function W(n, s) {
const e = n.indexOf(s);
return e > -1 ? [n.slice(0, e), n.slice(e + 1)] : [n];
}
const k = {};
function N(n) {
return k[n] = (k[n] ?? -1) + 1, `GENERATE_ID::${n.toUpperCase()}_${k[n]}`;
}
export {
ie as description,
ae as id,
te as importCommand,
se as name,
oe as pluginHookImport
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +1,73 @@
const T = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", w = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", A = [w, T];
function q(e) {
const t = b(e);
const S = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", _ = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", O = [_, S];
function v(e) {
var g;
const t = k(e);
if (t == null)
return;
const n = a(t.info);
if (!A.includes(n.schema) || !Array.isArray(t.item))
const o = i(t.info);
if (!O.includes(o.schema) || !Array.isArray(t.item))
return;
const i = {
const u = A(t.auth), s = {
workspaces: [],
environments: [],
requests: [],
httpRequests: [],
folders: []
}, c = {
}, n = {
model: "workspace",
id: m("wk"),
name: n.name || "Postman Import",
description: n.description || ""
id: h("workspace"),
name: o.name || "Postman Import",
description: o.description || "",
variables: ((g = t.variable) == null ? void 0 : g.map((r) => ({
name: r.key,
value: r.value
}))) ?? []
};
i.workspaces.push(c);
const f = (r, u = null) => {
s.workspaces.push(n);
const T = (r, p = null) => {
if (typeof r.name == "string" && Array.isArray(r.item)) {
const o = {
const a = {
model: "folder",
workspaceId: c.id,
id: m("fl"),
workspaceId: n.id,
id: h("folder"),
name: r.name,
folderId: u
folderId: p
};
i.folders.push(o);
for (const s of r.item)
f(s, o.id);
s.folders.push(a);
for (const l of r.item)
T(l, a.id);
} else if (typeof r.name == "string" && "request" in r) {
const o = a(r.request), s = k(o.body), d = S(o.auth), g = {
const a = i(r.request), l = j(a.body), w = A(a.auth), d = w.authenticationType == null ? u : w, q = {
model: "http_request",
id: m("rq"),
workspaceId: c.id,
folderId: u,
id: h("http_request"),
workspaceId: n.id,
folderId: p,
name: r.name,
method: o.method || "GET",
url: typeof o.url == "string" ? o.url : a(o.url).raw,
body: s.body,
bodyType: s.bodyType,
method: a.method || "GET",
url: typeof a.url == "string" ? a.url : i(a.url).raw,
body: l.body,
bodyType: l.bodyType,
authentication: d.authentication,
authenticationType: d.authenticationType,
headers: [
...s.headers,
...l.headers,
...d.headers,
...y(o.header).map((p) => ({
name: p.key,
value: p.value,
enabled: !p.disabled
...b(a.header).map((m) => ({
name: m.key,
value: m.value,
enabled: !m.disabled
}))
]
};
i.requests.push(g);
s.httpRequests.push(q);
} else
console.log("Unknown item", r, u);
console.log("Unknown item", r, p);
};
for (const r of t.item)
f(r);
return { resources: h(i) };
T(r);
return { resources: f(s) };
}
function S(e) {
const t = a(e);
function A(e) {
const t = i(e);
return "basic" in t ? {
headers: [],
authenticationType: "basic",
@@ -70,10 +75,17 @@ function S(e) {
username: t.basic.username || "",
password: t.basic.password || ""
}
} : "bearer" in t ? {
headers: [],
authenticationType: "bearer",
authentication: {
token: t.bearer.token || ""
}
} : { headers: [], authenticationType: null, authentication: {} };
}
function k(e) {
const t = a(e);
function j(e) {
var o, c, u, s;
const t = i(e);
return "graphql" in t ? {
headers: [
{
@@ -85,7 +97,7 @@ function k(e) {
bodyType: "graphql",
body: {
text: JSON.stringify(
{ query: t.graphql.query, variables: b(t.graphql.variables) },
{ query: t.graphql.query, variables: k(t.graphql.variables) },
null,
2
)
@@ -100,7 +112,7 @@ function k(e) {
],
bodyType: "application/x-www-form-urlencoded",
body: {
form: y(t.urlencoded).map((n) => ({
form: b(t.urlencoded).map((n) => ({
enabled: !n.disabled,
name: n.key ?? "",
value: n.value ?? ""
@@ -116,7 +128,7 @@ function k(e) {
],
bodyType: "multipart/form-data",
body: {
form: y(t.formdata).map(
form: b(t.formdata).map(
(n) => n.src != null ? {
enabled: !n.disabled,
name: n.key ?? "",
@@ -128,33 +140,42 @@ function k(e) {
}
)
}
} : "raw" in t ? {
headers: [
{
name: "Content-Type",
value: ((c = (o = t.options) == null ? void 0 : o.raw) == null ? void 0 : c.language) === "json" ? "application/json" : "",
enabled: !0
}
],
bodyType: ((s = (u = t.options) == null ? void 0 : u.raw) == null ? void 0 : s.language) === "json" ? "application/json" : "other",
body: {
text: t.raw ?? ""
}
} : { headers: [], bodyType: null, body: {} };
}
function b(e) {
function k(e) {
try {
return a(JSON.parse(e));
return i(JSON.parse(e));
} catch {
}
return null;
}
function a(e) {
function i(e) {
return Object.prototype.toString.call(e) === "[object Object]" ? e : {};
}
function y(e) {
function b(e) {
return Object.prototype.toString.call(e) === "[object Array]" ? e : [];
}
function h(e) {
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(h) : typeof e == "object" && e != null ? Object.fromEntries(
Object.entries(e).map(([t, n]) => [t, h(n)])
function f(e) {
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(f) : typeof e == "object" && e != null ? Object.fromEntries(
Object.entries(e).map(([t, o]) => [t, f(o)])
) : e;
}
function m(e) {
const t = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let n = `${e}_`;
for (let l = 0; l < 10; l++)
n += t[Math.floor(Math.random() * t.length)];
return n;
const y = {};
function h(e) {
return y[e] = (y[e] ?? -1) + 1, `GENERATE_ID::${e.toUpperCase()}_${y[e]}`;
}
export {
q as pluginHookImport
v as pluginHookImport
};

View File

@@ -5,8 +5,8 @@ function u(r) {
} catch {
return;
}
if (t(e) && "yaakSchema" in e && (e.yaakSchema === 1 && (e.resources.httpRequests = e.resources.requests, e.yaakSchema = 2), e.yaakSchema === 2))
return { resources: e.resources };
if (!(!t(e) || !("yaakSchema" in e)))
return "requests" in e.resources && (e.resources.httpRequests = e.resources.requests, delete e.resources.requests), { resources: e.resources };
}
function t(r) {
return Object.prototype.toString.call(r) === "[object Object]";

View File

@@ -1,13 +1,15 @@
use std::fmt::Display;
use log::{debug, warn};
use log::warn;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::types::JsonValue;
use tauri::{AppHandle, Manager};
use crate::is_dev;
use crate::models::{generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string};
use crate::models::{
generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string,
};
// serializable
#[derive(Serialize, Deserialize, Debug)]
@@ -26,6 +28,7 @@ pub enum AnalyticsResource {
KeyValue,
Sidebar,
Workspace,
Setting,
}
impl AnalyticsResource {
@@ -182,7 +185,7 @@ pub async fn track_event(
// Disable analytics actual sending in dev
if is_dev() {
debug!("track: {} {} {:?}", event, attributes_json, params);
// debug!("track: {} {} {:?}", event, attributes_json, params);
return;
}
@@ -207,7 +210,7 @@ fn get_os() -> &'static str {
}
fn get_window_size(app_handle: &AppHandle) -> String {
let window = match app_handle.windows().into_values().next() {
let window = match app_handle.webview_windows().into_values().next() {
Some(w) => w,
None => return "unknown".to_string(),
};
@@ -232,7 +235,7 @@ fn get_window_size(app_handle: &AppHandle) -> String {
async fn get_id(app_handle: &AppHandle) -> String {
let id = get_key_value_string(app_handle, "analytics", "id", "").await;
if id.is_empty() {
let new_id = generate_id(None);
let new_id = generate_id();
set_key_value_string(app_handle, "analytics", "id", new_id.as_str()).await;
new_id
} else {

View File

@@ -7,20 +7,20 @@ use std::sync::Arc;
use std::time::Duration;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use http::header::{ACCEPT, USER_AGENT};
use log::{error, info, warn};
use reqwest::redirect::Policy;
use reqwest::{multipart, Url};
use reqwest::redirect::Policy;
use sqlx::types::{Json, JsonValue};
use tauri::{Manager, Window};
use tauri::{Manager, WebviewWindow};
use tokio::sync::oneshot;
use tokio::sync::watch::Receiver;
use crate::{models, render, response_err};
pub async fn send_http_request(
window: &Window,
window: &WebviewWindow,
request: models::HttpRequest,
response: &models::HttpResponse,
environment: Option<models::Environment>,
@@ -35,6 +35,7 @@ pub async fn send_http_request(
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
@@ -112,7 +113,6 @@ pub async fn send_http_request(
// everything manually to know that).
// if let Some(cookie_store) = maybe_cookie_store.clone() {
// let values1 = cookie_store.get_request_values(&url);
// println!("COOKIE VLUAES: {:?}", values1.collect::<Vec<_>>());
// let raw_value = cookie_store.get_request_values(&url)
// .map(|(name, value)| format!("{}={}", name, value))
// .collect::<Vec<_>>()
@@ -259,7 +259,6 @@ pub async fn send_http_request(
return response_err(response, e, window).await;
}
}
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
let mut multipart_form = multipart::Form::new();
if let Some(form_definition) = request_body.get("form") {
@@ -269,36 +268,64 @@ pub async fn send_http_request(
.unwrap_or(empty_bool)
.as_bool()
.unwrap_or(false);
let name = p
let name_raw = p
.get("name")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
if !enabled || name.is_empty() {
if !enabled || name_raw.is_empty() {
continue;
}
let file = p
let file_path = p
.get("file")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
let value = p
let value_raw = 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())?)
let name = render::render(name_raw, &workspace, environment_ref);
let part = if file_path.is_empty() {
multipart::Part::text(render::render(
value_raw,
&workspace,
environment_ref,
))
} else {
match fs::read(file_path) {
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return response_err(response, e.to_string(), window).await;
}
false => multipart::Part::text(render::render(
value,
&workspace,
environment_ref,
)),
}
};
let ct_raw = p
.get("contentType")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
multipart_form = multipart_form.part(
name,
if ct_raw.is_empty() {
part
} else {
let content_type = render::render(ct_raw, &workspace, environment_ref);
let filename = PathBuf::from(file_path)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string();
part.file_name(filename)
.mime_str(content_type.as_str())
.map_err(|e| e.to_string())?
},
);
}
@@ -374,7 +401,7 @@ pub async fn send_http_request(
{
// Write body to FS
let dir = window.app_handle().path_resolver().app_data_dir().unwrap();
let dir = window.app_handle().path().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
let body_path = match response.id.is_empty() {
@@ -439,3 +466,26 @@ pub async fn send_http_request(
Err(e) => response_err(response, e.to_string(), window).await,
}
}
fn ensure_proto(url_str: &str) -> String {
if url_str.starts_with("http://") || url_str.starts_with("https://") {
return url_str.to_string();
}
// Url::from_str will fail without a proto, so add one
let parseable_url = format!("http://{}", url_str);
if let Ok(u) = Url::from_str(parseable_url.as_str()) {
match u.host() {
Some(host) => {
let h = host.to_string();
// These TLDs force HTTPS
if h.ends_with(".app") || h.ends_with(".dev") || h.ends_with(".page") {
return format!("https://{url_str}");
}
}
None => {}
}
}
format!("http://{url_str}")
}

1832
src-tauri/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,38 @@ use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite};
use tauri::{AppHandle, Manager, Wry};
use tauri::{AppHandle, Manager, WebviewWindow, Wry};
use tokio::sync::Mutex;
pub enum ModelType {
TypeCookieJar,
TypeEnvironment,
TypeFolder,
TypeGrpcConnection,
TypeGrpcEvent,
TypeGrpcRequest,
TypeHttpRequest,
TypeHttpResponse,
TypeWorkspace,
}
impl ModelType {
pub fn id_prefix(&self) -> String {
match self {
ModelType::TypeCookieJar => "cj",
ModelType::TypeEnvironment => "ev",
ModelType::TypeFolder => "fl",
ModelType::TypeGrpcConnection => "gc",
ModelType::TypeGrpcEvent => "ge",
ModelType::TypeGrpcRequest => "gr",
ModelType::TypeHttpRequest => "rq",
ModelType::TypeHttpResponse => "rs",
ModelType::TypeWorkspace => "wk",
}
.to_string()
}
}
fn default_true() -> bool {
true
}
@@ -426,9 +455,9 @@ pub async fn get_workspace(mgr: &impl Manager<Wry>, id: &str) -> Result<Workspac
.await
}
pub async fn delete_workspace(mgr: &impl Manager<Wry>, id: &str) -> Result<Workspace, sqlx::Error> {
let db = get_db(mgr).await;
let workspace = get_workspace(mgr, id).await?;
pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result<Workspace, sqlx::Error> {
let db = get_db(window).await;
let workspace = get_workspace(window, id).await?;
let _ = sqlx::query!(
r#"
DELETE FROM workspaces
@@ -439,11 +468,11 @@ pub async fn delete_workspace(mgr: &impl Manager<Wry>, id: &str) -> Result<Works
.execute(&db)
.await;
for r in list_responses_by_workspace_id(mgr, id).await? {
delete_http_response(mgr, &r.id).await?;
for r in list_responses_by_workspace_id(window, id).await? {
delete_http_response(window, &r.id).await?;
}
emit_deleted_model(mgr, workspace)
emit_deleted_model(window, workspace)
}
pub async fn get_cookie_jar(mgr: &impl Manager<Wry>, id: &str) -> Result<CookieJar, sqlx::Error> {
@@ -481,12 +510,9 @@ pub async fn list_cookie_jars(
.await
}
pub async fn delete_cookie_jar(
mgr: &impl Manager<Wry>,
id: &str,
) -> Result<CookieJar, sqlx::Error> {
let cookie_jar = get_cookie_jar(mgr, id).await?;
let db = get_db(mgr).await;
pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result<CookieJar, sqlx::Error> {
let cookie_jar = get_cookie_jar(window, id).await?;
let db = get_db(window).await;
let _ = sqlx::query!(
r#"
@@ -498,25 +524,25 @@ pub async fn delete_cookie_jar(
.execute(&db)
.await;
emit_deleted_model(mgr, cookie_jar)
emit_deleted_model(window, cookie_jar)
}
pub async fn duplicate_grpc_request(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
id: &str,
) -> Result<GrpcRequest, sqlx::Error> {
let mut request = get_grpc_request(mgr, id).await?.clone();
let mut request = get_grpc_request(window, id).await?.clone();
request.id = "".to_string();
upsert_grpc_request(mgr, &request).await
upsert_grpc_request(window, &request).await
}
pub async fn upsert_grpc_request(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
request: &GrpcRequest,
) -> Result<GrpcRequest, sqlx::Error> {
let db = get_db(mgr).await;
let db = get_db(window).await;
let id = match request.id.as_str() {
"" => generate_id(Some("gr")),
"" => generate_model_id(ModelType::TypeGrpcRequest),
_ => request.id.to_string(),
};
let trimmed_name = request.name.trim();
@@ -556,8 +582,8 @@ pub async fn upsert_grpc_request(
.execute(&db)
.await?;
match get_grpc_request(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_grpc_request(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
@@ -607,12 +633,12 @@ pub async fn list_grpc_requests(
}
pub async fn upsert_grpc_connection(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
connection: &GrpcConnection,
) -> Result<GrpcConnection, sqlx::Error> {
let db = get_db(mgr).await;
let db = get_db(window).await;
let id = match connection.id.as_str() {
"" => generate_id(Some("gc")),
"" => generate_model_id(ModelType::TypeGrpcConnection),
_ => connection.id.to_string(),
};
sqlx::query!(
@@ -646,8 +672,8 @@ pub async fn upsert_grpc_connection(
.execute(&db)
.await?;
match get_grpc_connection(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_grpc_connection(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
@@ -696,12 +722,12 @@ pub async fn list_grpc_connections(
}
pub async fn upsert_grpc_event(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
event: &GrpcEvent,
) -> Result<GrpcEvent, sqlx::Error> {
let db = get_db(mgr).await;
let db = get_db(window).await;
let id = match event.id.as_str() {
"" => generate_id(Some("ge")),
"" => generate_model_id(ModelType::TypeGrpcEvent),
_ => event.id.to_string(),
};
sqlx::query!(
@@ -732,8 +758,8 @@ pub async fn upsert_grpc_event(
.execute(&db)
.await?;
match get_grpc_event(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_grpc_event(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
@@ -778,16 +804,16 @@ pub async fn list_grpc_events(
}
pub async fn upsert_cookie_jar(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
cookie_jar: &CookieJar,
) -> Result<CookieJar, sqlx::Error> {
let id = match cookie_jar.id.as_str() {
"" => generate_id(Some("cj")),
"" => generate_model_id(ModelType::TypeCookieJar),
_ => cookie_jar.id.to_string(),
};
let trimmed_name = cookie_jar.name.trim();
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
INSERT INTO cookie_jars (
@@ -807,8 +833,8 @@ pub async fn upsert_cookie_jar(
.execute(&db)
.await?;
match get_cookie_jar(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_cookie_jar(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
@@ -833,11 +859,11 @@ pub async fn list_environments(
}
pub async fn delete_environment(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
id: &str,
) -> Result<Environment, sqlx::Error> {
let db = get_db(mgr).await;
let env = get_environment(mgr, id).await?;
let db = get_db(window).await;
let env = get_environment(window, id).await?;
let _ = sqlx::query!(
r#"
DELETE FROM environments
@@ -848,7 +874,7 @@ pub async fn delete_environment(
.execute(&db)
.await;
emit_deleted_model(mgr, env)
emit_deleted_model(window, env)
}
async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings, sqlx::Error> {
@@ -886,10 +912,10 @@ pub async fn get_or_create_settings(mgr: &impl Manager<Wry>) -> Settings {
}
pub async fn update_settings(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
settings: Settings,
) -> Result<Settings, sqlx::Error> {
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
UPDATE settings SET (
@@ -903,22 +929,22 @@ pub async fn update_settings(
.execute(&db)
.await?;
match get_settings(mgr).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_settings(window).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
pub async fn upsert_environment(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
environment: Environment,
) -> Result<Environment, sqlx::Error> {
let id = match environment.id.as_str() {
"" => generate_id(Some("ev")),
"" => generate_model_id(ModelType::TypeEnvironment),
_ => environment.id.to_string(),
};
let trimmed_name = environment.name.trim();
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
INSERT INTO environments (
@@ -938,8 +964,8 @@ pub async fn upsert_environment(
.execute(&db)
.await?;
match get_environment(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_environment(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
@@ -999,9 +1025,9 @@ pub async fn list_folders(
.await
}
pub async fn delete_folder(mgr: &impl Manager<Wry>, id: &str) -> Result<Folder, sqlx::Error> {
let folder = get_folder(mgr, id).await?;
let db = get_db(mgr).await;
pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result<Folder, sqlx::Error> {
let folder = get_folder(window, id).await?;
let db = get_db(window).await;
let _ = sqlx::query!(
r#"
DELETE FROM folders
@@ -1012,17 +1038,17 @@ pub async fn delete_folder(mgr: &impl Manager<Wry>, id: &str) -> Result<Folder,
.execute(&db)
.await;
emit_deleted_model(mgr, folder)
emit_deleted_model(window, folder)
}
pub async fn upsert_folder(mgr: &impl Manager<Wry>, r: Folder) -> Result<Folder, sqlx::Error> {
pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result<Folder, sqlx::Error> {
let id = match r.id.as_str() {
"" => generate_id(Some("fl")),
"" => generate_model_id(ModelType::TypeFolder),
_ => r.id.to_string(),
};
let trimmed_name = r.name.trim();
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
INSERT INTO folders (
@@ -1044,32 +1070,32 @@ pub async fn upsert_folder(mgr: &impl Manager<Wry>, r: Folder) -> Result<Folder,
.execute(&db)
.await?;
match get_folder(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_folder(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
pub async fn duplicate_http_request(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
id: &str,
) -> Result<HttpRequest, sqlx::Error> {
let mut request = get_http_request(mgr, id).await?.clone();
let mut request = get_http_request(window, id).await?.clone();
request.id = "".to_string();
upsert_http_request(mgr, request).await
upsert_http_request(window, request).await
}
pub async fn upsert_http_request(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
r: HttpRequest,
) -> Result<HttpRequest, sqlx::Error> {
let id = match r.id.as_str() {
"" => generate_id(Some("rq")),
"" => generate_model_id(ModelType::TypeHttpRequest),
_ => r.id.to_string(),
};
let trimmed_name = r.name.trim();
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
@@ -1109,13 +1135,13 @@ pub async fn upsert_http_request(
.execute(&db)
.await?;
match get_http_request(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_http_request(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
pub async fn list_requests(
pub async fn list_http_requests(
mgr: &impl Manager<Wry>,
workspace_id: &str,
) -> Result<Vec<HttpRequest>, sqlx::Error> {
@@ -1165,15 +1191,15 @@ pub async fn get_http_request(
}
pub async fn delete_http_request(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
id: &str,
) -> Result<HttpRequest, sqlx::Error> {
let req = get_http_request(mgr, id).await?;
let req = get_http_request(window, id).await?;
// DB deletes will cascade but this will delete the files
delete_all_http_responses(mgr, id).await?;
delete_all_http_responses(window, id).await?;
let db = get_db(mgr).await;
let db = get_db(window).await;
let _ = sqlx::query!(
r#"
DELETE FROM http_requests
@@ -1184,12 +1210,12 @@ pub async fn delete_http_request(
.execute(&db)
.await;
emit_deleted_model(mgr, req)
emit_deleted_model(window, req)
}
#[allow(clippy::too_many_arguments)]
pub async fn create_http_response(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
request_id: &str,
elapsed: i64,
elapsed_headers: i64,
@@ -1202,10 +1228,10 @@ pub async fn create_http_response(
version: Option<&str>,
remote_addr: Option<&str>,
) -> Result<HttpResponse, sqlx::Error> {
let req = get_http_request(mgr, request_id).await?;
let id = generate_id(Some("rp"));
let req = get_http_request(window, request_id).await?;
let id = generate_model_id(ModelType::TypeHttpResponse);
let headers_json = Json(headers);
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
INSERT INTO http_responses (
@@ -1231,14 +1257,14 @@ pub async fn create_http_response(
.execute(&db)
.await?;
match get_http_response(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_http_response(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
pub async fn cancel_pending_grpc_connections(mgr: &impl Manager<Wry>) -> Result<(), sqlx::Error> {
let db = get_db(mgr).await;
pub async fn cancel_pending_grpc_connections(app: &AppHandle) -> Result<(), sqlx::Error> {
let db = get_db(app).await;
sqlx::query!(
r#"
UPDATE grpc_connections
@@ -1251,8 +1277,8 @@ pub async fn cancel_pending_grpc_connections(mgr: &impl Manager<Wry>) -> Result<
Ok(())
}
pub async fn cancel_pending_responses(mgr: &impl Manager<Wry>) -> Result<(), sqlx::Error> {
let db = get_db(mgr).await;
pub async fn cancel_pending_responses(app: &AppHandle) -> Result<(), sqlx::Error> {
let db = get_db(app).await;
sqlx::query!(
r#"
UPDATE http_responses
@@ -1266,27 +1292,27 @@ pub async fn cancel_pending_responses(mgr: &impl Manager<Wry>) -> Result<(), sql
}
pub async fn update_response_if_id(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
response: &HttpResponse,
) -> Result<HttpResponse, sqlx::Error> {
if response.id.is_empty() {
Ok(response.clone())
} else {
update_response(mgr, response).await
update_response(window, response).await
}
}
pub async fn upsert_workspace(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
workspace: Workspace,
) -> Result<Workspace, sqlx::Error> {
let id = match workspace.id.as_str() {
"" => generate_id(Some("wk")),
"" => generate_model_id(ModelType::TypeWorkspace),
_ => workspace.id.to_string(),
};
let trimmed_name = workspace.name.trim();
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
INSERT INTO workspaces (
@@ -1314,17 +1340,17 @@ pub async fn upsert_workspace(
.execute(&db)
.await?;
match get_workspace(mgr, &id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_workspace(window, &id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
pub async fn update_response(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
response: &HttpResponse,
) -> Result<HttpResponse, sqlx::Error> {
let db = get_db(mgr).await;
let db = get_db(window).await;
sqlx::query!(
r#"
UPDATE http_responses SET (
@@ -1348,8 +1374,8 @@ pub async fn update_response(
.execute(&db)
.await?;
match get_http_response(mgr, &response.id).await {
Ok(m) => Ok(emit_upserted_model(mgr, m)),
match get_http_response(window, &response.id).await {
Ok(m) => Ok(emit_upserted_model(window, m)),
Err(e) => Err(e),
}
}
@@ -1427,12 +1453,12 @@ pub async fn list_responses_by_workspace_id(
}
pub async fn delete_grpc_request(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
id: &str,
) -> Result<GrpcRequest, sqlx::Error> {
let req = get_grpc_request(mgr, id).await?;
let req = get_grpc_request(window, id).await?;
let db = get_db(mgr).await;
let db = get_db(window).await;
let _ = sqlx::query!(
r#"
DELETE FROM grpc_requests
@@ -1443,16 +1469,16 @@ pub async fn delete_grpc_request(
.execute(&db)
.await;
emit_deleted_model(mgr, req)
emit_deleted_model(window, req)
}
pub async fn delete_grpc_connection(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
id: &str,
) -> Result<GrpcConnection, sqlx::Error> {
let resp = get_grpc_connection(mgr, id).await?;
let resp = get_grpc_connection(window, id).await?;
let db = get_db(mgr).await;
let db = get_db(window).await;
let _ = sqlx::query!(
r#"
DELETE FROM grpc_connections
@@ -1463,14 +1489,14 @@ pub async fn delete_grpc_connection(
.execute(&db)
.await;
emit_deleted_model(mgr, resp)
emit_deleted_model(window, resp)
}
pub async fn delete_http_response(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
id: &str,
) -> Result<HttpResponse, sqlx::Error> {
let resp = get_http_response(mgr, id).await?;
let resp = get_http_response(window, id).await?;
// Delete the body file if it exists
if let Some(p) = resp.body_path.clone() {
@@ -1479,7 +1505,7 @@ pub async fn delete_http_response(
};
}
let db = get_db(mgr).await;
let db = get_db(window).await;
let _ = sqlx::query!(
r#"
DELETE FROM http_responses
@@ -1490,92 +1516,129 @@ pub async fn delete_http_response(
.execute(&db)
.await;
emit_deleted_model(mgr, resp)
emit_deleted_model(window, resp)
}
pub async fn delete_all_grpc_connections(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
request_id: &str,
) -> Result<(), sqlx::Error> {
for r in list_grpc_connections(mgr, request_id).await? {
delete_grpc_connection(mgr, &r.id).await?;
for r in list_grpc_connections(window, request_id).await? {
delete_grpc_connection(window, &r.id).await?;
}
Ok(())
}
pub async fn delete_all_http_responses(
mgr: &impl Manager<Wry>,
window: &WebviewWindow,
request_id: &str,
) -> Result<(), sqlx::Error> {
for r in list_responses(mgr, request_id, None).await? {
delete_http_response(mgr, &r.id).await?;
for r in list_responses(window, request_id, None).await? {
delete_http_response(window, &r.id).await?;
}
Ok(())
}
pub fn generate_id(prefix: Option<&str>) -> String {
let id = Alphanumeric.sample_string(&mut rand::thread_rng(), 10);
match prefix {
None => id,
Some(p) => format!("{p}_{id}"),
}
pub fn generate_model_id(model: ModelType) -> String {
let id = generate_id();
format!("{}_{}", model.id_prefix(), id)
}
pub fn generate_id() -> String {
Alphanumeric.sample_string(&mut rand::thread_rng(), 10)
}
#[derive(Default, Debug, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct WorkspaceExport {
pub yaak_version: String,
pub yaak_schema: i64,
pub timestamp: NaiveDateTime,
pub resources: WorkspaceExportResources,
pub yaak_version: String,
pub yaak_schema: i64,
pub timestamp: NaiveDateTime,
pub resources: WorkspaceExportResources,
}
#[derive(Default, Debug, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct WorkspaceExportResources {
pub workspaces: Vec<Workspace>,
pub environments: Vec<Environment>,
pub folders: Vec<Folder>,
pub http_requests: Vec<HttpRequest>,
pub grpc_requests: Vec<GrpcRequest>,
pub workspaces: Vec<Workspace>,
pub environments: Vec<Environment>,
pub folders: Vec<Folder>,
pub http_requests: Vec<HttpRequest>,
pub grpc_requests: Vec<GrpcRequest>,
}
pub async fn get_workspace_export_resources(
app_handle: &AppHandle,
workspace_id: &str,
window: &WebviewWindow,
workspace_ids: Vec<&str>,
) -> WorkspaceExport {
let workspace = get_workspace(app_handle, workspace_id)
.await
.expect("Failed to get workspace");
return WorkspaceExport {
let app_handle = window.app_handle();
let mut data = WorkspaceExport {
yaak_version: app_handle.package_info().version.clone().to_string(),
yaak_schema: 2,
timestamp: chrono::Utc::now().naive_utc(),
resources: WorkspaceExportResources {
workspaces: vec![workspace],
environments: list_environments(app_handle, workspace_id)
.await
.expect("Failed to get environments"),
folders: list_folders(app_handle, workspace_id)
.await
.expect("Failed to get folders"),
http_requests: list_requests(app_handle, workspace_id)
.await
.expect("Failed to get requests"),
grpc_requests: list_grpc_requests(app_handle, workspace_id)
.await
.expect("Failed to get grpc requests"),
workspaces: Vec::new(),
environments: Vec::new(),
folders: Vec::new(),
http_requests: Vec::new(),
grpc_requests: Vec::new(),
},
};
for workspace_id in workspace_ids {
data.resources.workspaces.push(
get_workspace(window, workspace_id)
.await
.expect("Failed to get workspace"),
);
data.resources.environments.append(
&mut list_environments(window, workspace_id)
.await
.expect("Failed to get environments"),
);
data.resources.folders.append(
&mut list_folders(window, workspace_id)
.await
.expect("Failed to get folders"),
);
data.resources.http_requests.append(
&mut list_http_requests(window, workspace_id)
.await
.expect("Failed to get http requests"),
);
data.resources.grpc_requests.append(
&mut list_grpc_requests(window, workspace_id)
.await
.expect("Failed to get grpc requests"),
);
}
return data;
}
fn emit_upserted_model<S: Serialize + Clone>(mgr: &impl Manager<Wry>, model: S) -> S {
mgr.emit_all("upserted_model", model.clone()).unwrap();
#[derive(Clone, Serialize)]
#[serde(default, rename_all = "camelCase")]
struct ModelPayload<M: Serialize + Clone> {
pub model: M,
pub window_label: String,
}
fn emit_upserted_model<M: Serialize + Clone>(window: &WebviewWindow, model: M) -> M {
let payload = ModelPayload {
model: model.clone(),
window_label: window.label().to_string(),
};
window.emit("upserted_model", payload).unwrap();
model
}
fn emit_deleted_model<S: Serialize + Clone, E>(mgr: &impl Manager<Wry>, model: S) -> Result<S, E> {
mgr.emit_all("deleted_model", model.clone()).unwrap();
fn emit_deleted_model<M: Serialize + Clone, E>(window: &WebviewWindow, model: M) -> Result<M, E> {
let payload = ModelPayload {
model: model.clone(),
window_label: window.label().to_string(),
};
window.emit("deleted_model", payload).unwrap();
Ok(model)
}

View File

@@ -0,0 +1,94 @@
use std::time::SystemTime;
use chrono::{Duration, NaiveDateTime, Utc};
use http::Method;
use log::debug;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};
use crate::models::{get_key_value_raw, set_key_value_raw};
// Check for updates every hour
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
const KV_NAMESPACE: &str = "notifications";
const KV_KEY: &str = "seen";
// Create updater struct
pub struct YaakNotifier {
last_check: SystemTime,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct YaakNotification {
timestamp: NaiveDateTime,
id: String,
message: String,
action: Option<YaakNotificationAction>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct YaakNotificationAction {
label: String,
url: String,
}
impl YaakNotifier {
pub fn new() -> Self {
Self {
last_check: SystemTime::UNIX_EPOCH,
}
}
pub async fn seen(&mut self, app: &AppHandle, id: &str) -> Result<(), String> {
let mut seen = get_kv(app).await?;
seen.push(id.to_string());
debug!("Marked notification as seen {}", id);
let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?;
set_key_value_raw(app, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
Ok(())
}
pub async fn check(&mut self, app: &AppHandle) -> Result<(), String> {
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
return Ok(());
}
self.last_check = SystemTime::now();
let info = app.package_info().clone();
let req = reqwest::Client::default()
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[("version", info.version)]);
let resp = req.send().await.map_err(|e| e.to_string())?;
let notification = resp
.json::<YaakNotification>()
.await
.map_err(|e| e.to_string())?;
let age = notification
.timestamp
.signed_duration_since(Utc::now().naive_utc());
let seen = get_kv(app).await?;
if seen.contains(&notification.id) || (age > Duration::days(1)) {
debug!("Already seen notification {}", notification.id);
return Ok(());
}
debug!("Got notification {:?}", notification);
let _ = app.emit("notification", notification.clone());
Ok(())
}
}
async fn get_kv(app: &AppHandle) -> Result<Vec<String>, String> {
match get_key_value_raw(app, "notifications", "seen").await {
None => Ok(Vec::new()),
Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()),
}
}

View File

@@ -1,18 +1,18 @@
use std::fs;
use std::rc::Rc;
use boa_engine::{
Context, js_string, JsNativeError, JsValue, Module, module::SimpleModuleLoader,
property::Attribute, Source,
};
use boa_engine::builtins::promise::PromiseState;
use boa_engine::module::ModuleLoader;
use boa_runtime::Console;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tauri::AppHandle;
use tauri::{AppHandle, Manager};
use tauri::path::BaseDirectory;
use crate::models::{WorkspaceExportResources};
use crate::models::{HttpRequest, WorkspaceExportResources};
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct FilterResult {
@@ -47,14 +47,28 @@ pub async fn run_plugin_filter(
Some(resources)
}
pub fn run_plugin_export_curl(
app_handle: &AppHandle,
request: &HttpRequest,
) -> Result<String, String> {
let mut context = Context::default();
let request_json = serde_json::to_value(request).map_err(|e| e.to_string())?;
let result_json = run_plugin(
app_handle,
"exporter-curl",
"pluginHookExport",
&[JsValue::from_json(&request_json, &mut context).map_err(|e| e.to_string())?],
);
let resources: String = serde_json::from_value(result_json).map_err(|e| e.to_string())?;
Ok(resources)
}
pub async fn run_plugin_import(
app_handle: &AppHandle,
plugin_name: &str,
file_path: &str,
) -> Option<ImportResult> {
let file = fs::read_to_string(file_path)
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
file_contents: &str,
) -> Result<Option<ImportResult>, String> {
let result_json = run_plugin(
app_handle,
plugin_name,
@@ -63,12 +77,11 @@ pub async fn run_plugin_import(
);
if result_json.is_null() {
return None;
return Ok(None);
}
let resources: ImportResult =
serde_json::from_value(result_json).expect("failed to parse result json");
Some(resources)
let resources: ImportResult = serde_json::from_value(result_json).map_err(|e| e.to_string())?;
Ok(Some(resources))
}
fn run_plugin(
@@ -78,8 +91,8 @@ fn run_plugin(
js_args: &[JsValue],
) -> serde_json::Value {
let plugin_dir = app_handle
.path_resolver()
.resolve_resource("plugins")
.path()
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource")
.join(plugin_name);
let plugin_index_file = plugin_dir.join("index.mjs");
@@ -89,12 +102,9 @@ fn run_plugin(
plugin_dir, plugin_index_file
);
// Module loader for the specific plugin
let loader = &SimpleModuleLoader::new(plugin_dir).expect("failed to create module loader");
let dyn_loader: &dyn ModuleLoader = loader;
let loader = Rc::new(SimpleModuleLoader::new(plugin_dir).unwrap());
let context = &mut Context::builder()
.module_loader(dyn_loader)
.module_loader(loader.clone())
.build()
.expect("failed to create context");
@@ -108,15 +118,13 @@ fn run_plugin(
// Insert parsed entrypoint into the module loader
loader.insert(plugin_index_file, module.clone());
let promise_result = module
.load_link_evaluate(context)
.expect("failed to evaluate module");
let promise_result = module.load_link_evaluate(context);
// Very important to push forward the job queue after queueing promises.
context.run_jobs();
// Checking if the final promise didn't return an error.
match promise_result.state().expect("failed to get promise state") {
match promise_result.state() {
PromiseState::Pending => {
panic!("Promise was pending");
}

View File

@@ -1,6 +1,67 @@
use crate::models::{Environment, Workspace};
use std::collections::HashMap;
use tauri::regex::Regex;
use regex::Regex;
use sqlx::types::{Json, JsonValue};
use crate::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace};
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
let r = r.clone();
HttpRequest {
url: render(r.url.as_str(), w, e),
url_parameters: Json(
r.url_parameters
.0
.iter()
.map(|p| HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), w, e),
value: render(p.value.as_str(), w, e),
})
.collect::<Vec<HttpUrlParameter>>(),
),
headers: Json(
r.headers
.0
.iter()
.map(|p| HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), w, e),
value: render(p.value.as_str(), w, e),
})
.collect::<Vec<HttpRequestHeader>>(),
),
body: Json(
r.body
.0
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), w, e)
} else {
v.to_string()
};
(render(k, w, e), JsonValue::from(v))
})
.collect::<HashMap<String, JsonValue>>(),
),
authentication: Json(
r.authentication
.0
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), w, e)
} else {
v.to_string()
};
(render(k, w, e), JsonValue::from(v))
})
.collect::<HashMap<String, JsonValue>>(),
),
..r
}
}
pub fn render(template: &str, workspace: &Workspace, environment: Option<&Environment>) -> String {
let mut map = HashMap::new();
@@ -24,7 +85,7 @@ pub fn render(template: &str, workspace: &Workspace, environment: Option<&Enviro
Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}")
.expect("Failed to create regex")
.replace_all(template, |caps: &tauri::regex::Captures| {
.replace_all(template, |caps: &regex::Captures| {
let key = caps.get(1).unwrap().as_str();
map.get(key).unwrap_or(&"")
})

View File

@@ -1,8 +1,10 @@
use std::fmt::{Display, Formatter};
use std::time::SystemTime;
use log::info;
use tauri::api::dialog;
use tauri::{updater, AppHandle, Window};
use tauri::AppHandle;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_updater::UpdaterExt;
use crate::is_dev;
@@ -19,6 +21,25 @@ pub enum UpdateMode {
Beta,
}
impl Display for UpdateMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
UpdateMode::Stable => "stable",
UpdateMode::Beta => "beta",
};
write!(f, "{}", s)
}
}
impl UpdateMode {
pub fn new(mode: &str) -> UpdateMode {
match mode {
"beta" => UpdateMode::Beta,
_ => UpdateMode::Stable,
}
}
}
impl YaakUpdater {
pub fn new() -> Self {
Self {
@@ -29,14 +50,13 @@ impl YaakUpdater {
&mut self,
app_handle: &AppHandle,
mode: UpdateMode,
) -> Result<bool, updater::Error> {
) -> Result<bool, tauri_plugin_updater::Error> {
self.last_update_check = SystemTime::now();
let update_mode = get_update_mode_str(mode);
let enabled = !is_dev();
info!(
"Checking for updates mode={} enabled={}",
update_mode, enabled
mode, enabled
);
if !enabled {
@@ -44,49 +64,46 @@ impl YaakUpdater {
}
match app_handle
.updater()
.header("X-Update-Mode", update_mode)?
.updater_builder()
.header("X-Update-Mode", mode.to_string())?
.build()?
.check()
.await
{
Ok(update) => {
Ok(Some(update)) => {
let h = app_handle.clone();
dialog::ask(
None::<&Window>,
"Update Available",
format!(
app_handle
.dialog()
.message(format!(
"{} is available. Would you like to download and install it now?",
update.latest_version()
),
|confirmed| {
update.version
))
.title("Update Available")
.show(|confirmed| {
if !confirmed {
return;
}
tauri::async_runtime::spawn(async move {
match update.download_and_install().await {
match update.download_and_install(|_, _| {}, || {}).await {
Ok(_) => {
if dialog::blocking::ask(
None::<&Window>,
"Update Installed",
"Would you like to restart the app?",
) {
if h.dialog()
.message("Would you like to restart the app?")
.title("Update Installed")
.blocking_show()
{
h.restart();
}
}
Err(e) => {
dialog::message(
None::<&Window>,
"Update Failed",
format!("The update failed to install: {}", e),
);
h.dialog()
.message(format!("The update failed to install: {}", e));
}
}
});
},
);
});
Ok(true)
}
Err(updater::Error::UpToDate) => Ok(false),
Ok(None) => Ok(false),
Err(e) => Err(e),
}
}
@@ -94,7 +111,7 @@ impl YaakUpdater {
&mut self,
app_handle: &AppHandle,
mode: UpdateMode,
) -> Result<bool, updater::Error> {
) -> Result<bool, tauri_plugin_updater::Error> {
let ignore_check =
self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
@@ -104,17 +121,3 @@ impl YaakUpdater {
self.force_check(app_handle, mode).await
}
}
pub fn update_mode_from_str(mode: &str) -> UpdateMode {
match mode {
"beta" => UpdateMode::Beta,
_ => UpdateMode::Stable,
}
}
fn get_update_mode_str(mode: UpdateMode) -> &'static str {
match mode {
UpdateMode::Stable => "stable",
UpdateMode::Beta => "beta",
}
}

View File

@@ -1,4 +1,4 @@
use tauri::{Runtime, Window};
use tauri::WebviewWindow;
const TRAFFIC_LIGHT_OFFSET_X: f64 = 13.0;
const TRAFFIC_LIGHT_OFFSET_Y: f64 = 18.0;
@@ -7,7 +7,7 @@ pub trait TrafficLightWindowExt {
fn position_traffic_lights(&self);
}
impl<R: Runtime> TrafficLightWindowExt for Window<R> {
impl TrafficLightWindowExt for WebviewWindow {
#[cfg(not(target_os = "macos"))]
fn position_traffic_lights(&self) {
// No-op on other platforms

View File

@@ -1,140 +1,141 @@
use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu};
use crate::is_dev;
use tauri::menu::{
AboutMetadata, Menu, MenuItemBuilder, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID,
WINDOW_SUBMENU_ID,
};
pub use tauri::AppHandle;
use tauri::Wry;
pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
let mut menu = Menu::new();
#[cfg(target_os = "macos")]
{
menu = menu.add_submenu(Submenu::new(
app_name,
Menu::new()
.add_native_item(MenuItem::About(
app_name.to_string(),
AboutMetadata::default(),
))
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("toggle_settings".to_string(), "Settings")
.accelerator("CmdOrCtrl+,"),
)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Services)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
));
}
pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
let pkg_info = app_handle.package_info();
let config = app_handle.config();
let about_metadata = AboutMetadata {
name: Some(pkg_info.name.clone()),
version: Some(pkg_info.version.to_string()),
copyright: config.bundle.copyright.clone(),
authors: config.bundle.publisher.clone().map(|p| vec![p]),
..Default::default()
};
let mut file_menu = Menu::new();
file_menu = file_menu.add_native_item(MenuItem::CloseWindow);
#[cfg(not(target_os = "macos"))]
{
file_menu = file_menu.add_native_item(MenuItem::Quit);
}
menu = menu.add_submenu(Submenu::new("File", file_menu));
let window_menu = Submenu::with_id_and_items(
app_handle,
WINDOW_SUBMENU_ID,
"Window",
true,
&[
&PredefinedMenuItem::minimize(app_handle, None)?,
&PredefinedMenuItem::maximize(app_handle, None)?,
#[cfg(target_os = "macos")]
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::close_window(app_handle, None)?,
],
)?;
#[cfg(not(target_os = "linux"))]
let mut edit_menu = Menu::new();
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::Undo);
edit_menu = edit_menu.add_native_item(MenuItem::Redo);
edit_menu = edit_menu.add_native_item(MenuItem::Separator);
}
#[cfg(not(target_os = "linux"))]
{
edit_menu = edit_menu.add_native_item(MenuItem::Cut);
edit_menu = edit_menu.add_native_item(MenuItem::Copy);
edit_menu = edit_menu.add_native_item(MenuItem::Paste);
}
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::SelectAll);
}
#[cfg(not(target_os = "linux"))]
{
menu = menu.add_submenu(Submenu::new("Edit", edit_menu));
}
let mut view_menu = Menu::new();
#[cfg(target_os = "macos")]
{
view_menu = view_menu
.add_native_item(MenuItem::EnterFullScreen)
.add_native_item(MenuItem::Separator);
}
view_menu = view_menu
.add_item(
CustomMenuItem::new("zoom_reset".to_string(), "Zoom to Actual Size")
.accelerator("CmdOrCtrl+0"),
)
.add_item(
CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"),
)
.add_item(
CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"),
);
// .add_native_item(MenuItem::Separator)
// .add_item(
// CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
// .accelerator("CmdOrCtrl+b"),
// )
// .add_item(
// CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
// .accelerator("CmdOrCtrl+1"),
// )
// .add_item(
// CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
// .accelerator("CmdOrCtrl+,"),
// )
// .add_item(
// CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
// );
menu = menu.add_submenu(Submenu::new("View", view_menu));
let help_menu = Submenu::with_id_and_items(
app_handle,
HELP_SUBMENU_ID,
"Help",
true,
&[
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
#[cfg(target_os = "macos")]
&MenuItemBuilder::with_id("open_feedback".to_string(), "Give Feedback")
.build(app_handle)?,
],
)?;
let mut window_menu = Menu::new();
window_menu = window_menu.add_native_item(MenuItem::Minimize);
#[cfg(target_os = "macos")]
{
window_menu = window_menu.add_native_item(MenuItem::Zoom);
window_menu = window_menu.add_native_item(MenuItem::Separator);
}
window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
menu = menu.add_submenu(Submenu::new("Window", window_menu));
let menu = Menu::with_items(
app_handle,
&[
#[cfg(target_os = "macos")]
&Submenu::with_items(
app_handle,
pkg_info.name.clone(),
true,
&[
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
&PredefinedMenuItem::separator(app_handle)?,
&MenuItemBuilder::with_id("settings".to_string(), "Settings")
.accelerator("CmdOrCtrl+,")
.build(app_handle)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::services(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::hide(app_handle, None)?,
&PredefinedMenuItem::hide_others(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::quit(app_handle, None)?,
],
)?,
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
&Submenu::with_items(
app_handle,
"File",
true,
&[
&PredefinedMenuItem::close_window(app_handle, None)?,
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::quit(app_handle, None)?,
],
)?,
&Submenu::with_items(
app_handle,
"Edit",
true,
&[
&PredefinedMenuItem::undo(app_handle, None)?,
&PredefinedMenuItem::redo(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::cut(app_handle, None)?,
&PredefinedMenuItem::copy(app_handle, None)?,
&PredefinedMenuItem::paste(app_handle, None)?,
&PredefinedMenuItem::select_all(app_handle, None)?,
],
)?,
&Submenu::with_items(
app_handle,
"View",
true,
&[
#[cfg(target_os = "macos")]
&PredefinedMenuItem::fullscreen(app_handle, None)?,
#[cfg(target_os = "macos")]
&PredefinedMenuItem::separator(app_handle)?,
&MenuItemBuilder::with_id("zoom_reset".to_string(), "Zoom to Actual Size")
.accelerator("CmdOrCtrl+0")
.build(app_handle)?,
&MenuItemBuilder::with_id("zoom_in".to_string(), "Zoom In")
.accelerator("CmdOrCtrl+=")
.build(app_handle)?,
&MenuItemBuilder::with_id("zoom_out".to_string(), "Zoom Out")
.accelerator("CmdOrCtrl+-")
.build(app_handle)?,
],
)?,
&window_menu,
&help_menu,
#[cfg(dev)]
&Submenu::with_items(
app_handle,
"Develop",
true,
&[
&MenuItemBuilder::with_id("refresh".to_string(), "Refresh")
.accelerator("CmdOrCtrl+Shift+r")
.build(app_handle)?,
&MenuItemBuilder::with_id("toggle_devtools".to_string(), "Open Devtools")
.accelerator("CmdOrCtrl+Option+i")
.build(app_handle)?,
],
)?,
],
)?;
// menu = menu.add_submenu(Submenu::new(
// "Workspace",
// Menu::new()
// .add_item(
// CustomMenuItem::new("send_request".to_string(), "Send Request")
// .accelerator("CmdOrCtrl+r"),
// )
// .add_item(
// CustomMenuItem::new("new_request".to_string(), "New Request")
// .accelerator("CmdOrCtrl+n"),
// )
// .add_item(
// CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
// .accelerator("CmdOrCtrl+d"),
// ),
// ));
if is_dev() {
menu = menu.add_submenu(Submenu::new(
"Developer",
Menu::new()
.add_item(
CustomMenuItem::new("refresh".to_string(), "Refresh")
.accelerator("CmdOrCtrl + Shift + r"),
)
.add_item(
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
.accelerator("CmdOrCtrl + Option + i"),
),
));
}
menu
Ok(menu)
}

View File

@@ -1,17 +1,13 @@
{
"package": {
"productName": "Daak"
},
"tauri": {
"bundle": {
"icon": [
"icons/dev/32x32.png",
"icons/dev/128x128.png",
"icons/dev/128x128@2x.png",
"icons/dev/icon.icns",
"icons/dev/icon.ico"
],
"identifier": "app.yaak.desktop.dev"
}
"productName": "Daak",
"identifier": "app.yaak.desktop.dev",
"bundle": {
"icon": [
"icons/dev/32x32.png",
"icons/dev/128x128.png",
"icons/dev/128x128@2x.png",
"icons/dev/icon.icns",
"icons/dev/icon.ico"
]
}
}

View File

@@ -2,113 +2,69 @@
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"package": {
"productName": "Yaak",
"version": "2024.3.4"
"productName": "Yaak",
"version": "2024.4.0-beta.3",
"identifier": "app.yaak.desktop",
"app": {
"withGlobalTauri": false,
"security": {
"assetProtocol": {
"enable": true,
"scope": {
"allow": [
"$APPDATA/responses/*"
]
}
}
}
},
"tauri": {
"windows": [],
"allowlist": {
"all": false,
"os": {
"all": true
},
"protocol": {
"assetScope": [
"$APPDATA/responses/*"
],
"asset": true
},
"fs": {
"readFile": true,
"scope": [
"$RESOURCE/*",
"$APPDATA/responses/*"
]
},
"shell": {
"all": false,
"open": true,
"sidecar": true,
"scope": [
{ "name": "protoc", "sidecar": true,
"args": true
}
]
},
"window": {
"close": true,
"maximize": true,
"minimize": true,
"setDecorations": true,
"setTitle": true,
"startDragging": true,
"unmaximize": true
},
"dialog": {
"all": false,
"open": true,
"save": true
},
"path": {
"all": true
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [
"protoc-vendored/protoc"
],
"icon": [
"icons/release/32x32.png",
"icons/release/128x128.png",
"icons/release/128x128@2x.png",
"icons/release/icon.icns",
"icons/release/icon.ico"
],
"identifier": "app.yaak.desktop",
"longDescription": "The best cross-platform visual API client",
"resources": [
"migrations/*",
"plugins/*",
"protoc-vendored/include/*"
],
"shortDescription": "The best API client",
"targets": [
"deb",
"appimage",
"nsis",
"app",
"dmg",
"updater"
],
"deb": {
"depends": []
},
"macOS": {
"exceptionDomain": "",
"entitlements": "macos/entitlements.plist",
"frameworks": []
},
"windows": {
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {},
"plugins": {
"updater": {
"active": true,
"dialog": false,
"endpoints": [
"https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEMxRDJFREQ1MjExQjdGN0IKUldSN2Z4c2gxZTNTd1FHNCtmYnFXMHVVQzhuNkJOM1cwOFBodmdLall3ckhKenpKUytHSTR1MlkK"
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"externalBin": [
"protoc-vendored/protoc"
],
"icon": [
"icons/release/32x32.png",
"icons/release/128x128.png",
"icons/release/128x128@2x.png",
"icons/release/icon.icns",
"icons/release/icon.ico"
],
"longDescription": "The best cross-platform visual API client",
"resources": [
"migrations/*",
"plugins/*",
"protoc-vendored/include/*"
],
"shortDescription": "The best API client",
"targets": [
"deb",
"appimage",
"nsis",
"app",
"dmg",
"updater"
],
"macOS": {
"exceptionDomain": "",
"entitlements": "macos/entitlements.plist",
"frameworks": []
},
"windows": {
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
}
}

View File

@@ -7,7 +7,6 @@ import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './AppRouter';
const queryClient = new QueryClient({
logger: undefined,
defaultOptions: {
queries: {
retry: false,

View File

@@ -1,10 +1,9 @@
import { createBrowserRouter, Navigate, Outlet, RouterProvider, useParams } from 'react-router-dom';
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
import { DialogProvider } from './DialogContext';
import { GlobalHooks } from './GlobalHooks';
import { DefaultLayout } from './DefaultLayout';
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
import RouteError from './RouteError';
import Workspace from './Workspace';
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
const router = createBrowserRouter([
{
@@ -58,7 +57,7 @@ function RedirectLegacyEnvironmentURLs() {
}>();
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
let to = '/';
let to;
if (workspaceId != null && requestId != null) {
to = routes.paths.request({ workspaceId, environmentId, requestId });
} else if (workspaceId != null) {
@@ -69,12 +68,3 @@ function RedirectLegacyEnvironmentURLs() {
return <Navigate to={to} />;
}
function DefaultLayout() {
return (
<DialogProvider>
<Outlet />
<GlobalHooks />
</DialogProvider>
);
}

View File

@@ -13,7 +13,7 @@ export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Prop
const updateGrpcRequest = useUpdateGrpcRequest(request.id);
return (
<VStack className="my-2" space={2}>
<VStack className="py-2 overflow-y-auto h-full" space={2}>
<Input
useTemplating
autocompleteVariables

View File

@@ -1,4 +1,4 @@
import { open } from '@tauri-apps/api/dialog';
import { open } from '@tauri-apps/plugin-dialog';
import mime from 'mime';
import { useKeyValue } from '../hooks/useKeyValue';
import type { HttpRequest } from '../lib/models';
@@ -30,15 +30,14 @@ export function BinaryFileEditor({
const handleClick = async () => {
await ignoreContentType.set(false);
const path = await open({
const selected = await open({
title: 'Select File',
multiple: false,
});
if (path) {
onChange({ filePath: path });
} else {
onChange({ filePath: undefined });
if (selected == null) {
return;
}
onChange({ filePath: selected.path });
};
const filePath = typeof body.filePath === 'string' ? body.filePath : undefined;

View File

@@ -0,0 +1,127 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useMemo, useCallback, useState } from 'react';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRequests } from '../hooks/useRequests';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { Input } from './core/Input';
export function CommandPalette({ onClose }: { onClose: () => void }) {
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const routes = useAppRoutes();
const activeEnvironmentId = useActiveEnvironmentId();
const workspaces = useWorkspaces();
const requests = useRequests();
const [command, setCommand] = useState<string>('');
const items = useMemo<{ label: string; onSelect: () => void; key: string }[]>(() => {
const items = [];
for (const r of requests) {
items.push({
key: `switch-request-${r.id}`,
label: `Switch Request → ${fallbackRequestName(r)}`,
onSelect: () => {
return routes.navigate('request', {
workspaceId: r.workspaceId,
requestId: r.id,
environmentId: activeEnvironmentId ?? undefined,
});
},
});
}
for (const w of workspaces) {
items.push({
key: `switch-workspace-${w.id}`,
label: `Switch Workspace → ${w.name}`,
onSelect: async () => {
const environmentId = (await getRecentEnvironments(w.id))[0];
return routes.navigate('workspace', {
workspaceId: w.id,
environmentId,
});
},
});
}
return items;
}, [activeEnvironmentId, requests, routes, workspaces]);
const filteredItems = useMemo(() => {
return items.filter((v) => v.label.toLowerCase().includes(command.toLowerCase()));
}, [command, items]);
const handleSelectAndClose = useCallback(
(cb: () => void) => {
onClose();
cb();
},
[onClose],
);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
setSelectedIndex((prev) => prev + 1);
} else if (e.key === 'ArrowUp') {
setSelectedIndex((prev) => prev - 1);
} else if (e.key === 'Enter') {
const item = filteredItems[selectedIndex];
if (item) {
handleSelectAndClose(item.onSelect);
}
}
},
[filteredItems, handleSelectAndClose, selectedIndex],
);
return (
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
<div className="px-2 py-2 w-full">
<Input
hideLabel
name="command"
label="Command"
placeholder="Type a command"
defaultValue=""
onChange={setCommand}
onKeyDown={handleKeyDown}
/>
</div>
<div className="h-full px-1.5 overflow-y-auto">
{filteredItems.map((v, i) => (
<CommandPaletteItem
active={i === selectedIndex}
key={v.key}
onClick={() => handleSelectAndClose(v.onSelect)}
>
{v.label}
</CommandPaletteItem>
))}
</div>
</div>
);
}
function CommandPaletteItem({
children,
active,
onClick,
}: {
children: ReactNode;
active: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className={classNames(
'w-full h-xs flex items-center rounded px-1.5 text-gray-600',
active && 'bg-highlightSecondary text-gray-800',
)}
>
{children}
</button>
);
}

Some files were not shown because too many files have changed in this diff Show More