7 Commits

Author SHA1 Message Date
dependabot[bot]
3944b7a5f4 chore(deps): bump tauri-build from 2.3.0 to 2.3.1 in /src-tauri
---
updated-dependencies:
- dependency-name: tauri-build
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-20 10:30:07 +00:00
Ryan Walters
8069d5a061 chore: solve clippy warnings, export directly into bindings/ dir, remove unreachable expression arms 2025-08-20 02:35:30 -05:00
Ryan Walters
ecc8380645 refactor: reorganize backend tauri code 2025-08-20 02:20:24 -05:00
Ryan Walters
3414880705 refactor: reorganize frontend code 2025-08-20 02:19:12 -05:00
Ryan Walters
f90f377277 feat: implement TypeScript bindings generation and enhance drop overlay component 2025-08-19 19:29:30 -05:00
Ryan Walters
b0cb176f17 ci: add code quality & release workflows 2025-08-19 18:46:15 -05:00
c172fe4e31 chore: fix prettier ignore, reformat everything 2025-07-14 16:44:28 -05:00
34 changed files with 1832 additions and 713 deletions

134
.github/workflows/code-quality.yml vendored Normal file
View File

@@ -0,0 +1,134 @@
name: Code Quality
permissions: read-all
on:
workflow_dispatch: # Allow manual triggering
pull_request:
branches: [master]
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
- "**/package.json"
- "**/pnpm-lock.yaml"
push:
branches: [master]
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
- "**/package.json"
- "**/pnpm-lock.yaml"
jobs:
rust-quality:
name: Rust Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt, clippy
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install cargo-udeps
uses: taiki-e/cache-cargo-install-action@v2
with:
tool: cargo-udeps
- name: Check for unused dependencies
run: cargo +nightly udeps --manifest-path src-tauri/Cargo.toml --all-targets
- name: Install cargo-machete
uses: taiki-e/cache-cargo-install-action@v2
with:
tool: cargo-machete
- name: Check for unused Cargo.toml dependencies
run: cargo machete src-tauri/
- name: Install cargo-outdated
uses: taiki-e/cache-cargo-install-action@v2
with:
tool: cargo-outdated
- name: Check for outdated dependencies
run: cargo outdated --manifest-path src-tauri/Cargo.toml --exit-code 1
continue-on-error: true
frontend-quality:
name: Frontend Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Check for unused dependencies
run: pnpm exec depcheck --ignore-bin-package=false --skip-missing=true
continue-on-error: true
- name: Check for outdated dependencies
run: pnpm outdated
continue-on-error: true
- name: Bundle size analysis
run: pnpm run build && du -sh dist/
continue-on-error: true
license-check:
name: License Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-license
uses: taiki-e/cache-cargo-install-action@v2
with:
tool: cargo-license
- name: Check Rust crate licenses
run: cargo license --manifest-path src-tauri/Cargo.toml --json > rust-licenses.json
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Check npm package licenses
run: pnpm exec license-checker --json > npm-licenses.json
continue-on-error: true

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

@@ -0,0 +1,68 @@
name: Release
on:
release:
types: [published]
jobs:
build-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
args: --target aarch64-apple-darwin
- platform: macos-latest
args: --target x86_64-apple-darwin
- platform: ubuntu-22.04
args: ""
- platform: windows-latest
args: ""
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install frontend dependencies
run: pnpm install
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: app-v__VERSION__
releaseName: "App v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
src/bindings/*.ts
src-tauri/bindings/*.ts
# Seed data # Seed data
.data/* .data/*
!.data/seed.ps1 !.data/seed.ps1

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
src/bindings.ts
src-tauri/target/**
src-tauri/gen/**

View File

@@ -1,5 +1,4 @@
{ {
"ignore": ["src/bindings.ts"],
"useTabs": true, "useTabs": true,
"tabWidth": 2 "tabWidth": 2
} }

View File

@@ -1,3 +1,3 @@
{ {
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
} }

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>byte-me</title> <title>byte-me</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -1,33 +1,36 @@
{ {
"name": "byte-me", "name": "byte-me",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "pnpm generate-types && tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri" "tauri": "tauri",
}, "generate-types": "tsx scripts/generate-types.ts"
"dependencies": { },
"@nivo/core": "^0.99.0", "dependencies": {
"@nivo/line": "^0.99.0", "@nivo/core": "^0.99.0",
"@tailwindcss/vite": "^4.1.11", "@nivo/line": "^0.99.0",
"@tauri-apps/api": "^2", "@tailwindcss/vite": "^4.1.11",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/api": "^2",
"lucide-react": "^0.525.0", "@tauri-apps/plugin-opener": "^2",
"react": "^18.3.1", "lucide-react": "^0.525.0",
"react-dom": "^18.3.1", "react": "^18.3.1",
"tailwindcss": "^4.1.11", "react-dom": "^18.3.1",
"ts-pattern": "^5.7.1" "tailwindcss": "^4.1.11",
}, "ts-pattern": "^5.7.1"
"devDependencies": { },
"@tauri-apps/cli": "^2", "devDependencies": {
"@types/react": "^18.3.1", "@tauri-apps/cli": "^2",
"@types/react-dom": "^18.3.1", "@types/react": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4", "@types/react-dom": "^18.3.1",
"typescript": "~5.6.2", "@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.3", "prettier": "^3.6.2",
"vitest": "^3.2.4" "tsx": "^4.19.2",
} "typescript": "~5.6.2",
"vite": "^6.0.3",
"vitest": "^3.2.4"
}
} }

72
pnpm-lock.yaml generated
View File

@@ -16,7 +16,7 @@ importers:
version: 0.99.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 0.99.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.11 specifier: ^4.1.11
version: 4.1.11(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)) version: 4.1.11(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4))
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^2 specifier: ^2
version: 2.6.0 version: 2.6.0
@@ -50,16 +50,22 @@ importers:
version: 18.3.7(@types/react@18.3.23) version: 18.3.7(@types/react@18.3.23)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^4.3.4 specifier: ^4.3.4
version: 4.6.0(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)) version: 4.6.0(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4))
prettier:
specifier: ^3.6.2
version: 3.6.2
tsx:
specifier: ^4.19.2
version: 4.20.4
typescript: typescript:
specifier: ~5.6.2 specifier: ~5.6.2
version: 5.6.3 version: 5.6.3
vite: vite:
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) version: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
vitest: vitest:
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(jiti@2.4.2)(lightningcss@1.30.1) version: 3.2.4(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
packages: packages:
@@ -924,6 +930,9 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -1081,6 +1090,11 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
react-dom@18.3.1: react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies: peerDependencies:
@@ -1100,6 +1114,9 @@ packages:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
robust-predicates@3.0.2: robust-predicates@3.0.2:
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
@@ -1167,6 +1184,11 @@ packages:
ts-pattern@5.7.1: ts-pattern@5.7.1:
resolution: {integrity: sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==} resolution: {integrity: sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==}
tsx@4.20.4:
resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==}
engines: {node: '>=18.0.0'}
hasBin: true
typescript@5.6.3: typescript@5.6.3:
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -1781,12 +1803,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11
'@tailwindcss/oxide-win32-x64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11
'@tailwindcss/vite@4.1.11(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1))': '@tailwindcss/vite@4.1.11(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4))':
dependencies: dependencies:
'@tailwindcss/node': 4.1.11 '@tailwindcss/node': 4.1.11
'@tailwindcss/oxide': 4.1.11 '@tailwindcss/oxide': 4.1.11
tailwindcss: 4.1.11 tailwindcss: 4.1.11
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
'@tauri-apps/api@2.6.0': {} '@tauri-apps/api@2.6.0': {}
@@ -1911,7 +1933,7 @@ snapshots:
'@types/prop-types': 15.7.15 '@types/prop-types': 15.7.15
csstype: 3.1.3 csstype: 3.1.3
'@vitejs/plugin-react@4.6.0(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1))': '@vitejs/plugin-react@4.6.0(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4))':
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
@@ -1919,7 +1941,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.19 '@rolldown/pluginutils': 1.0.0-beta.19
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
react-refresh: 0.17.0 react-refresh: 0.17.0
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -1931,13 +1953,13 @@ snapshots:
chai: 5.2.1 chai: 5.2.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1))': '@vitest/mocker@3.2.4(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4))':
dependencies: dependencies:
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.17 magic-string: 0.30.17
optionalDependencies: optionalDependencies:
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -2114,6 +2136,10 @@ snapshots:
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-tsconfig@4.10.1:
dependencies:
resolve-pkg-maps: 1.0.0
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
internmap@1.0.1: {} internmap@1.0.1: {}
@@ -2223,6 +2249,8 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
prettier@3.6.2: {}
react-dom@18.3.1(react@18.3.1): react-dom@18.3.1(react@18.3.1):
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@@ -2240,6 +2268,8 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
resolve-pkg-maps@1.0.0: {}
robust-predicates@3.0.2: {} robust-predicates@3.0.2: {}
rollup@4.45.0: rollup@4.45.0:
@@ -2316,6 +2346,13 @@ snapshots:
ts-pattern@5.7.1: {} ts-pattern@5.7.1: {}
tsx@4.20.4:
dependencies:
esbuild: 0.25.6
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
typescript@5.6.3: {} typescript@5.6.3: {}
update-browserslist-db@1.1.3(browserslist@4.25.1): update-browserslist-db@1.1.3(browserslist@4.25.1):
@@ -2328,13 +2365,13 @@ snapshots:
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
vite-node@3.2.4(jiti@2.4.2)(lightningcss@1.30.1): vite-node@3.2.4(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
debug: 4.4.1 debug: 4.4.1
es-module-lexer: 1.7.0 es-module-lexer: 1.7.0
pathe: 2.0.3 pathe: 2.0.3
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- jiti - jiti
@@ -2349,7 +2386,7 @@ snapshots:
- tsx - tsx
- yaml - yaml
vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1): vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4):
dependencies: dependencies:
esbuild: 0.25.6 esbuild: 0.25.6
fdir: 6.4.6(picomatch@4.0.2) fdir: 6.4.6(picomatch@4.0.2)
@@ -2361,12 +2398,13 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.4.2 jiti: 2.4.2
lightningcss: 1.30.1 lightningcss: 1.30.1
tsx: 4.20.4
vitest@3.2.4(jiti@2.4.2)(lightningcss@1.30.1): vitest@3.2.4(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4):
dependencies: dependencies:
'@types/chai': 5.2.2 '@types/chai': 5.2.2
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)) '@vitest/mocker': 3.2.4(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4))
'@vitest/pretty-format': 3.2.4 '@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4 '@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4 '@vitest/snapshot': 3.2.4
@@ -2384,8 +2422,8 @@ snapshots:
tinyglobby: 0.2.14 tinyglobby: 0.2.14
tinypool: 1.1.1 tinypool: 1.1.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1) vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
vite-node: 3.2.4(jiti@2.4.2)(lightningcss@1.30.1) vite-node: 3.2.4(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
transitivePeerDependencies: transitivePeerDependencies:
- jiti - jiti

View File

@@ -1,3 +1,3 @@
onlyBuiltDependencies: onlyBuiltDependencies:
- '@tailwindcss/oxide' - "@tailwindcss/oxide"
- esbuild - esbuild

51
scripts/generate-types.ts Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
import { execSync } from "child_process";
import { copyFileSync, mkdirSync, existsSync, readdirSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log("🔄 Generating TypeScript bindings...");
try {
// Run the test to generate bindings
execSync("cargo test export_bindings", {
cwd: "./src-tauri",
stdio: "inherit",
});
if (!existsSync(join(__dirname, "../src-tauri/bindings"))) {
throw new Error(
"Bindings directory not found. Bindings generation failed or improperly configured.",
);
}
console.log("✅ TypeScript bindings generated successfully!");
// Copy bindings to src directory
const srcBindingsDir = join(__dirname, "../src/bindings");
const files = readdirSync(join(__dirname, "../src-tauri/bindings")).filter(
(file) => file.endsWith(".ts"),
);
if (files.length === 0) {
throw new Error(
"No bindings files found. Bindings generation failed or improperly configured.",
);
}
for (const file of files) {
const source = join(__dirname, "../src-tauri/bindings", file);
const dest = join(srcBindingsDir, file);
copyFileSync(source, dest);
console.log(`📁 Copied ${file} to src/bindings/`);
}
console.log("🎉 All done! TypeScript bindings are up to date.");
} catch (error) {
console.error("❌ Failed to generate TypeScript bindings:", error);
process.exit(1);
}

570
src-tauri/Cargo.lock generated
View File

@@ -7,6 +7,10 @@ name = "Inflector"
version = "0.11.4" version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
dependencies = [
"lazy_static",
"regex",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@@ -23,6 +27,18 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@@ -47,6 +63,12 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -68,6 +90,18 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "ast_node"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9184f2b369b3e8625712493c89b785881f27eedc6cde480a81883cef78868b2"
dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.104",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@@ -263,6 +297,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "better_scoped_tls"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297b153aa5e573b5863108a6ddc9d5c968bd0b20e75cc614ee9821d2f45679c7"
dependencies = [
"scoped-tls",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -344,20 +387,22 @@ name = "bumpalo"
version = "3.19.0" version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
dependencies = [
"allocator-api2",
]
[[package]] [[package]]
name = "byte-me" name = "byte-me"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ffprobe", "ffprobe",
"infer",
"serde", "serde",
"serde_json", "serde_json",
"specta",
"specta-typescript",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-specta", "ts-rs",
] ]
[[package]] [[package]]
@@ -699,6 +744,56 @@ dependencies = [
"syn 2.0.104", "syn 2.0.104",
] ]
[[package]]
name = "data-url"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]]
name = "deno_ast"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "584547d27786a734536fde7088f8429d355569c39410427be44695c300618408"
dependencies = [
"deno_media_type",
"deno_terminal",
"dprint-swc-ext",
"once_cell",
"percent-encoding",
"serde",
"swc_atoms",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
"swc_eq_ignore_macros",
"text_lines",
"thiserror 1.0.69",
"unicode-width",
"url",
]
[[package]]
name = "deno_media_type"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8978229b82552bf8457a0125aa20863f023619cfc21ebb007b1e571d68fd85b"
dependencies = [
"data-url",
"serde",
"url",
]
[[package]]
name = "deno_terminal"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6337d4e7f375f8b986409a76fbeecfa4bd8a1343e63355729ae4befa058eaf"
dependencies = [
"once_cell",
"termcolor",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.4.0" version = "0.4.0"
@@ -812,6 +907,63 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "dprint-core"
version = "0.66.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ab0dd2bedc109d25f0d21afb09b7d329f6c6fa83b095daf31d2d967e091548"
dependencies = [
"anyhow",
"bumpalo",
"hashbrown 0.14.5",
"indexmap 2.10.0",
"rustc-hash",
"serde",
"unicode-width",
]
[[package]]
name = "dprint-core-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1675ad2b358481f3cc46202040d64ac7a36c4ade414a696df32e0e45421a6e9f"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "dprint-plugin-typescript"
version = "0.90.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c3c339020ebbbbbe5fc049350935ee2ea2ba5a3fc01f753588639a30404cda"
dependencies = [
"anyhow",
"deno_ast",
"dprint-core",
"dprint-core-macros",
"percent-encoding",
"rustc-hash",
"serde",
]
[[package]]
name = "dprint-swc-ext"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "019d17f2c2457c5a70a7cf4505b1a562ca8ab168c0ac0c005744efbd29fcb8fe"
dependencies = [
"allocator-api2",
"bumpalo",
"num-bigint",
"rustc-hash",
"swc_atoms",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
"text_lines",
]
[[package]] [[package]]
name = "dtoa" name = "dtoa"
version = "1.0.10" version = "1.0.10"
@@ -839,6 +991,12 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "embed-resource" name = "embed-resource"
version = "3.0.5" version = "3.0.5"
@@ -1020,6 +1178,17 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "from_variant"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4"
dependencies = [
"proc-macro2",
"swc_macros_common",
"syn 2.0.104",
]
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@@ -1427,6 +1596,16 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.4" version = "0.15.4"
@@ -1457,6 +1636,20 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a26def229ea95a8709dad32868d975d0dd40235bd2ce82920e4a8fe692b5e0"
dependencies = [
"hashbrown 0.14.5",
"new_debug_unreachable",
"once_cell",
"phf 0.11.3",
"rustc-hash",
"triomphe",
]
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.29.1" version = "0.29.1"
@@ -1766,6 +1959,18 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "is-macro"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "is-wsl" name = "is-wsl"
version = "0.4.0" version = "0.4.0"
@@ -2121,12 +2326,32 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
"serde",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2469,12 +2694,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "pathdiff" name = "pathdiff"
version = "0.2.3" version = "0.2.3"
@@ -2788,6 +3007,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "psm"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.38.0" version = "0.38.0"
@@ -3009,6 +3237,12 @@ version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@@ -3103,6 +3337,12 @@ dependencies = [
"syn 2.0.104", "syn 2.0.104",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -3351,6 +3591,17 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.10" version = "0.5.10"
@@ -3409,56 +3660,25 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "specta"
version = "2.0.0-rc.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971"
dependencies = [
"paste",
"specta-macros",
"thiserror 1.0.69",
]
[[package]]
name = "specta-macros"
version = "2.0.0-rc.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "specta-serde"
version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63"
dependencies = [
"specta",
"thiserror 1.0.69",
]
[[package]]
name = "specta-typescript"
version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d"
dependencies = [
"specta",
"specta-serde",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacker"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@@ -3490,12 +3710,146 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "string_enum"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90"
dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.104",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "swc_atoms"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb6567e4e67485b3e7662b486f1565bdae54bd5b9d6b16b2ba1a9babb1e42125"
dependencies = [
"hstr",
"once_cell",
"rustc-hash",
"serde",
]
[[package]]
name = "swc_common"
version = "0.33.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2f9706038906e66f3919028f9f7a37f3ed552f1b85578e93f4468742e2da438"
dependencies = [
"ast_node",
"better_scoped_tls",
"cfg-if",
"either",
"from_variant",
"new_debug_unreachable",
"num-bigint",
"once_cell",
"rustc-hash",
"serde",
"siphasher 0.3.11",
"swc_atoms",
"swc_eq_ignore_macros",
"swc_visit",
"tracing",
"unicode-width",
"url",
]
[[package]]
name = "swc_ecma_ast"
version = "0.113.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1690cc0c9ab60b44ac0225ba1e231ac532f7ba1d754df761c6ee607561afae"
dependencies = [
"bitflags 2.9.1",
"is-macro",
"num-bigint",
"phf 0.11.3",
"scoped-tls",
"serde",
"string_enum",
"swc_atoms",
"swc_common",
"unicode-id-start",
]
[[package]]
name = "swc_ecma_parser"
version = "0.144.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0499e69683ae5d67a20ff0279b94bc90f29df7922a46331b54d5dd367bf89570"
dependencies = [
"either",
"new_debug_unreachable",
"num-bigint",
"num-traits",
"phf 0.11.3",
"serde",
"smallvec",
"smartstring",
"stacker",
"swc_atoms",
"swc_common",
"swc_ecma_ast",
"tracing",
"typed-arena",
]
[[package]]
name = "swc_eq_ignore_macros"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "695a1d8b461033d32429b5befbf0ad4d7a2c4d6ba9cd5ba4e0645c615839e8e4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "swc_macros_common"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27e18fbfe83811ffae2bb23727e45829a0d19c6870bced7c0f545cc99ad248dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "swc_visit"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "043d11fe683dcb934583ead49405c0896a5af5face522e4682c16971ef7871b9"
dependencies = [
"either",
"swc_visit_macros",
]
[[package]]
name = "swc_visit_macros"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92807d840959f39c60ce8a774a3f83e8193c658068e6d270dbe0a05e40e90b41"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.104",
]
[[package]] [[package]]
name = "swift-rs" name = "swift-rs"
version = "1.0.7" version = "1.0.7"
@@ -3651,7 +4005,6 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serialize-to-javascript", "serialize-to-javascript",
"specta",
"swift-rs", "swift-rs",
"tauri-build", "tauri-build",
"tauri-macros", "tauri-macros",
@@ -3671,9 +4024,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.3.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83" checksum = "67945dbaf8920dbe3a1e56721a419a0c3d085254ab24cff5b9ad55e2b0016e0b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -3687,7 +4040,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"tauri-winres", "tauri-winres",
"toml 0.8.23", "toml 0.9.2",
"walkdir", "walkdir",
] ]
@@ -3820,39 +4173,11 @@ dependencies = [
"wry", "wry",
] ]
[[package]]
name = "tauri-specta"
version = "2.0.0-rc.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655"
dependencies = [
"heck 0.5.0",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-specta-macros",
"thiserror 2.0.12",
]
[[package]]
name = "tauri-specta-macros"
version = "2.0.0-rc.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.5.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41743bbbeb96c3a100d234e5a0b60a46d5aa068f266160862c7afdbf828ca02e" checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"brotli", "brotli",
@@ -3879,7 +4204,7 @@ dependencies = [
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror 2.0.12", "thiserror 2.0.12",
"toml 0.8.23", "toml 0.9.2",
"url", "url",
"urlpattern", "urlpattern",
"uuid", "uuid",
@@ -3921,6 +4246,24 @@ dependencies = [
"utf-8", "utf-8",
] ]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "text_lines"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd5828de7deaa782e1dd713006ae96b3bee32d3279b79eb67ecf8072c059bcf"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@@ -4232,12 +4575,51 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "triomphe"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85"
dependencies = [
"serde",
"stable_deref_trait",
]
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.5" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "11.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
dependencies = [
"dprint-plugin-typescript",
"thiserror 2.0.12",
"ts-rs-macros",
]
[[package]]
name = "ts-rs-macros"
version = "11.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"termcolor",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]] [[package]]
name = "typeid" name = "typeid"
version = "1.0.3" version = "1.0.3"
@@ -4302,6 +4684,12 @@ dependencies = [
"unic-common", "unic-common",
] ]
[[package]]
name = "unicode-id-start"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02aebfa694eccbbbffdd92922c7de136b9fe764396d2f10e21bce1681477cfc1"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@@ -4314,6 +4702,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"

View File

@@ -23,7 +23,5 @@ tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
ffprobe = "0.4.0" ffprobe = "0.4.0"
specta = "=2.0.0-rc.22" ts-rs = { version = "11.0", features = ["format"] }
specta-typescript = "0.0.9" infer = "0.19.0"
tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }

View File

@@ -1,10 +1,7 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": ["core:default", "opener:default"]
"core:default",
"opener:default"
]
} }

47
src-tauri/src/ff.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::models::StreamDetail;
pub fn extract_streams(info: &ffprobe::FfProbe) -> Vec<StreamDetail> {
let mut streams = Vec::new();
for stream in &info.streams {
match stream.codec_type.as_deref() {
Some("video") => {
streams.push(StreamDetail::Video {
codec: stream
.codec_name
.clone()
.unwrap_or_else(|| "unknown".to_string()),
width: stream.width.map(|w| w as u32),
height: stream.height.map(|h| h as u32),
bit_rate: stream.bit_rate.as_ref().map(|b| b.to_string()),
frame_rate: Some(stream.r_frame_rate.clone()),
});
}
Some("audio") => {
streams.push(StreamDetail::Audio {
codec: stream
.codec_name
.clone()
.unwrap_or_else(|| "unknown".to_string()),
sample_rate: stream.sample_rate.clone(),
channels: stream.channels.map(|c| c as u32),
bit_rate: stream.bit_rate.as_ref().map(|b| b.to_string()),
});
}
Some("subtitle") => {
streams.push(StreamDetail::Subtitle {
codec: stream
.codec_name
.clone()
.unwrap_or_else(|| "unknown".to_string()),
language: stream.tags.as_ref().and_then(|tags| tags.language.clone()),
});
}
_ => {}
}
}
streams
}

View File

@@ -1,67 +1,88 @@
use serde::{Deserialize, Serialize}; mod ff;
use specta::Type; mod media;
use specta_typescript::Typescript; mod models;
use ff::extract_streams;
use media::{detect_media_type, is_media_file};
use models::{StreamResult, StreamResultError};
use std::path::Path; use std::path::Path;
use tauri_specta::{collect_commands, Builder};
#[derive(Serialize, Deserialize, Debug, Clone, Type)] // detection, helpers moved to modules above
struct StreamResult {
path: String,
filename: String,
streams: Vec<StreamDetail>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
enum StreamDetail {
Video { codec: String },
Audio { codec: String },
Subtitle { codec: String },
}
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
struct StreamResultError {
filename: Option<String>,
reason: String,
}
#[tauri::command] #[tauri::command]
#[specta::specta]
fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultError> { fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultError> {
paths paths
.into_iter() .into_iter()
.map(|path_str| { .map(|path_str| {
let path = Path::new(&path_str); let path = Path::new(&path_str);
let filename = path.file_name().unwrap().to_str().unwrap().to_string(); let filename = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown")
.to_string();
// Check if file exists
if !path.exists() { if !path.exists() {
return Err(StreamResultError { return Err(StreamResultError {
filename: Some(filename), filename: Some(filename),
reason: "File does not exist".to_string(), reason: "File does not exist".to_string(),
}); error_type: "not_found".to_string(),
}
if !path.is_file() {
return Err(StreamResultError {
filename: Some(filename),
reason: "Not a file".to_string(),
}); });
} }
match ffprobe::ffprobe(&path_str) { // Check if it's a file (not directory)
Ok(info) => { if !path.is_file() {
dbg!(info); return Err(StreamResultError {
Ok(StreamResult { filename: Some(filename),
filename, reason: "Not a file (directory or other)".to_string(),
path: path_str, error_type: "not_file".to_string(),
streams: vec![], });
}) }
}
Err(err) => { // Get file size
eprintln!("Could not analyze file with ffprobe: {:?}", err); let size = std::fs::metadata(&path_str)
Err(StreamResultError { .map(|metadata| metadata.len())
filename: Some(filename), .unwrap_or(0);
reason: "Could not analyze file with ffprobe".to_string(),
}) // Detect media type using magic numbers and fallback to extensions
let media_type = detect_media_type(path);
// Only try to analyze media files with ffprobe
if is_media_file(&media_type) {
// Analyze with ffprobe
match ffprobe::ffprobe(&path_str) {
Ok(info) => {
let streams = extract_streams(&info);
let duration = info
.format
.duration
.and_then(|dur_str| dur_str.parse::<f64>().ok());
Ok(StreamResult {
filename,
path: path_str,
media_type,
duration,
size,
streams,
})
}
Err(err) => {
eprintln!("Could not analyze media file with ffprobe: {err:?}");
Err(StreamResultError {
filename: Some(filename),
reason: format!("Could not analyze media file: {err}"),
error_type: "analysis_failed".to_string(),
})
}
} }
} else {
// For non-media files, return an error indicating it's not a media file
Err(StreamResultError {
filename: Some(filename),
reason: format!("Not a media file (detected as {media_type:?})"),
error_type: "not_media".to_string(),
})
} }
}) })
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
@@ -69,23 +90,23 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let builder = Builder::<tauri::Wry>::new()
// Then register them (separated by a comma)
.commands(collect_commands![has_streams,]);
#[cfg(debug_assertions)] // <- Only export on non-release builds
builder
.export(Typescript::default(), "../src/bindings.ts")
.expect("Failed to export typescript bindings");
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![has_streams]) .invoke_handler(tauri::generate_handler![has_streams])
.setup(move |app| {
// Ensure you mount your events!
builder.mount_events(app);
Ok(())
})
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
#[cfg(test)]
mod tests {
use crate::models::StreamDetail;
use super::*;
use ts_rs::TS;
#[test]
fn export_bindings() {
// This will generate TypeScript bindings when you run `cargo test export_bindings`
TS::export_all_to("../../src/bindings")
}
}

141
src-tauri/src/media.rs Normal file
View File

@@ -0,0 +1,141 @@
use crate::models::MediaType;
use std::{fs::File, io::Read, path::Path};
pub fn detect_media_type(path: &Path) -> MediaType {
// First try to detect using infer crate (magic number detection)
if let Ok(mut file) = File::open(path) {
let mut buffer = [0; 512];
if let Ok(bytes_read) = file.read(&mut buffer) {
if let Some(kind) = infer::get(&buffer[..bytes_read]) {
return match kind.mime_type() {
// Audio types
"audio/mpeg" | "audio/mp3" | "audio/m4a" | "audio/ogg" | "audio/x-flac"
| "audio/x-wav" | "audio/amr" | "audio/aac" | "audio/x-aiff"
| "audio/x-dsf" | "audio/x-ape" | "audio/midi" => MediaType::Audio,
// Video types
"video/mp4" | "video/x-m4v" | "video/x-matroska" | "video/webm"
| "video/quicktime" | "video/x-msvideo" | "video/x-ms-wmv" | "video/mpeg"
| "video/x-flv" => MediaType::Video,
// Image types
"image/jpeg"
| "image/png"
| "image/gif"
| "image/webp"
| "image/x-canon-cr2"
| "image/tiff"
| "image/bmp"
| "image/heif"
| "image/avif"
| "image/vnd.ms-photo"
| "image/vnd.adobe.photoshop"
| "image/vnd.microsoft.icon"
| "image/openraster"
| "image/vnd.djvu" => MediaType::Image,
// Document types
"application/pdf"
| "application/rtf"
| "application/msword"
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
| "application/vnd.ms-excel"
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
| "application/vnd.ms-powerpoint"
| "application/vnd.openxmlformats-officedocument.presentationml.presentation"
| "application/vnd.oasis.opendocument.text"
| "application/vnd.oasis.opendocument.spreadsheet"
| "application/vnd.oasis.opendocument.presentation" => MediaType::Document,
// Archive types
"application/zip"
| "application/x-tar"
| "application/vnd.rar"
| "application/gzip"
| "application/x-bzip2"
| "application/vnd.bzip3"
| "application/x-7z-compressed"
| "application/x-xz"
| "application/x-shockwave-flash"
| "application/octet-stream"
| "application/postscript"
| "application/vnd.sqlite3"
| "application/x-nintendo-nes-rom"
| "application/x-google-chrome-extension"
| "application/vnd.ms-cab-compressed"
| "application/vnd.debian.binary-package"
| "application/x-unix-archive"
| "application/x-compress"
| "application/x-lzip"
| "application/x-rpm"
| "application/dicom"
| "application/zstd"
| "application/x-lz4"
| "application/x-ole-storage"
| "application/x-cpio"
| "application/x-par2"
| "application/epub+zip"
| "application/x-mobipocket-ebook" => MediaType::Archive,
// Executable types
"application/vnd.microsoft.portable-executable"
| "application/x-executable"
| "application/llvm"
| "application/x-mach-binary"
| "application/java"
| "application/vnd.android.dex"
| "application/vnd.android.dey"
| "application/x-x509-ca-cert" => MediaType::Executable,
// Library types (covered by executable types above, but keeping for clarity)
_ => MediaType::Unknown,
};
}
}
}
// Fallback to extension-based detection
if let Some(extension) = path.extension() {
match extension.to_str().unwrap_or("").to_lowercase().as_str() {
// Audio extensions
"mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "mid" | "amr" | "aiff"
| "dsf" | "ape" => MediaType::Audio,
// Video extensions
"mp4" | "mkv" | "webm" | "mov" | "avi" | "wmv" | "mpg" | "flv" | "m4v" => {
MediaType::Video
}
// Image extensions
"gif" | "png" | "jpg" | "jpeg" | "bmp" | "tiff" | "webp" | "cr2" | "heif" | "avif"
| "jxr" | "psd" | "ico" | "ora" | "djvu" => MediaType::Image,
// Document extensions
"txt" | "md" | "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt"
| "ods" | "odp" | "rtf" => MediaType::Document,
// Archive extensions
"zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "bz3" | "xz" | "swf" | "sqlite"
| "nes" | "crx" | "cab" | "deb" | "ar" | "Z" | "lz" | "rpm" | "dcm" | "zst" | "lz4"
| "cpio" | "par2" | "epub" | "mobi" => MediaType::Archive,
// Executable extensions
"exe" | "dll" | "msi" | "dmg" | "pkg" | "app" | "elf" | "bc" | "mach" | "class"
| "dex" | "dey" | "der" | "obj" => MediaType::Executable,
// Library extensions
"so" | "dylib" => MediaType::Library,
_ => MediaType::Unknown,
}
} else {
MediaType::Unknown
}
}
pub fn is_media_file(media_type: &MediaType) -> bool {
matches!(
media_type,
MediaType::Audio | MediaType::Video | MediaType::Image
)
}

56
src-tauri/src/models.rs Normal file
View File

@@ -0,0 +1,56 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
pub enum MediaType {
Audio,
Video,
Image,
Document,
Executable,
Archive,
Library,
Unknown,
}
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
pub struct StreamResult {
pub path: String,
pub filename: String,
pub media_type: MediaType,
pub duration: Option<f64>,
pub size: u64,
pub streams: Vec<StreamDetail>,
}
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
pub enum StreamDetail {
Video {
codec: String,
width: Option<u32>,
height: Option<u32>,
bit_rate: Option<String>,
frame_rate: Option<String>,
},
Audio {
codec: String,
sample_rate: Option<String>,
channels: Option<u32>,
bit_rate: Option<String>,
},
Subtitle {
codec: String,
language: Option<String>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
pub struct StreamResultError {
pub filename: Option<String>,
pub reason: String,
pub error_type: String,
}

View File

@@ -1,35 +1,35 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "byte-me", "productName": "byte-me",
"version": "0.1.0", "version": "0.1.0",
"identifier": "com.xevion.byteme", "identifier": "com.xevion.byteme",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build", "beforeBuildCommand": "pnpm build",
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "byte-me", "title": "byte-me",
"width": 800, "width": 800,
"height": 600 "height": 600
} }
], ],
"security": { "security": {
"csp": null "csp": null
} }
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
] ]
} }
} }

View File

@@ -1,51 +1,25 @@
type Frame = { import { useDragDropPaths } from "./hooks/useDragDropPaths.js";
id: string; import Graph from "./features/graph/graph.js";
data: { x: string | number; y: number }[]; import DropOverlay from "./features/drop/drop-overlay.js";
}; import type { Frame } from "./types/graph.js";
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { useEffect, useState } from "react";
import Graph from "./components/graph.js";
import DropOverlay from "./components/drop-overlay.js";
function App() { function App() {
const data: Frame[] = []; const data: Frame[] = [];
const [paths, setPaths] = useState<string[]>([]); const paths = useDragDropPaths();
useEffect(() => {
const unlistenPromise = getCurrentWebview().onDragDropEvent(
async ({ payload }) => {
if (payload.type === "enter") {
setPaths(payload.paths);
console.log("User hovering", payload);
} else if (payload.type === "leave" || payload.type === "drop") {
setPaths([]);
console.log("User left", payload);
}
}
);
// you need to call unlisten if your handler goes out of scope e.g. the component is unmounted const graph = <Graph data={data} />;
return () => {
unlistenPromise.then((unlisten) => {
unlisten();
console.log("Unlistened");
});
};
}, []);
const graph = <Graph data={data} />; return (
<div
return ( id="App"
<div className="min-h-screen min-w-screen overflow-hidden"
id="App" style={{ "--wails-drop-target": "drop" } as React.CSSProperties}
className="min-h-screen min-w-screen overflow-hidden" >
style={{ "--wails-drop-target": "drop" } as React.CSSProperties} <DropOverlay paths={paths} />
> {graph}
<DropOverlay paths={paths} /> </div>
{graph} );
</div>
);
} }
export default App; export default App;

View File

@@ -1,90 +1,25 @@
// Import generated TypeScript types from ts-rs
import type { StreamResult } from "./bindings/StreamResult";
import type { StreamDetail } from "./bindings/StreamDetail";
import type { StreamResultError } from "./bindings/StreamResultError";
import type { MediaType } from "./bindings/MediaType";
export type { StreamResult, StreamDetail, StreamResultError, MediaType };
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. // Tauri invoke wrapper
import { invoke } from "@tauri-apps/api/core";
/** user-defined commands **/
export const commands = {
async hasStreams(paths: string[]) : Promise<Result<StreamResult[], StreamResultError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("has_streams", { paths }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
}
}
/** user-defined events **/
/** user-defined constants **/
/** user-defined types **/
export type StreamDetail = { Video: { codec: string } } | { Audio: { codec: string } } | { Subtitle: { codec: string } }
export type StreamResult = { path: string; filename: string; streams: StreamDetail[] }
export type StreamResultError = { filename: string | null; reason: string }
/** tauri-specta globals **/
import {
invoke as TAURI_INVOKE,
Channel as TAURI_CHANNEL,
} from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> = export type Result<T, E> =
| { status: "ok"; data: T } | { status: "ok"; data: T }
| { status: "error"; error: E }; | { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>( export const commands = {
mappings: Record<keyof T, string>, async hasStreams(paths: string[]): Promise<Result<StreamResult[], StreamResultError>> {
) { try {
return new Proxy( const data = await invoke<StreamResult[]>("has_streams", { paths });
{} as unknown as { return { status: "ok", data };
[K in keyof T]: __EventObj__<T[K]> & { } catch (e) {
(handle: __WebviewWindow__): __EventObj__<T[K]>; if (e instanceof Error) throw e;
}; else return { status: "error", error: e as any };
}, }
{ }
get: (_, event) => { };
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
},
);
}

View File

@@ -1,148 +0,0 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import { match, P } from "ts-pattern";
type DropOverlayProps = {
paths: string[];
};
type State =
| { status: "hidden" }
| { status: "loading"; count: number }
| { status: "ready"; files: { name: string; key: string }[] }
| { status: "error"; reason: string; filename?: string };
import {
CircleQuestionMarkIcon,
File as FileIcon,
Film,
Image,
Music,
} from "lucide-react";
import { commands } from "../bindings";
type FileItemProps = {
filename: string;
error?: string;
};
const Item = ({ icon, text }: { icon: ReactNode; text: ReactNode }) => {
return (
<div
className="flex items-center gap-2 px-3 py-2 bg-neutral-800 rounded-md shadow-sm"
style={{
maxWidth: "100%",
marginBottom: "0.5rem",
}}
>
{icon}
<span className="truncate text-neutral-100 max-w-md">{text}</span>
</div>
);
};
const FileItem = ({ filename, error }: FileItemProps) => {
const ext = filename.split(".").pop()?.toLowerCase();
const icon =
error == null ? (
match(ext)
.with("mp3", "wav", "flac", "ogg", "m4a", "aac", () => (
<Music className="w-5 h-5 text-blue-400" />
))
.with("mp4", "mkv", "webm", "mov", "avi", () => (
<Film className="w-5 h-5 text-purple-400" />
))
.with("gif", () => <Image className="w-5 h-5 text-pink-400" />)
.otherwise(() => <FileIcon className="w-5 h-5 text-neutral-300" />)
) : (
<CircleQuestionMarkIcon className="w-5 h-5 text-neutral-300" />
);
return <Item icon={icon} text={filename} />;
};
const DropOverlay = ({ paths }: DropOverlayProps) => {
const [state, setState] = useState<State>({ status: "hidden" });
const aborterRef = useRef<AbortController | null>(null);
useEffect(() => {
if (paths.length === 0) {
setState({ status: "hidden" });
return;
}
setState({ status: "loading", count: paths.length });
aborterRef.current = new AbortController();
commands.hasStreams(paths).then((result) => {
setState((_state) => {
return match(result)
.with({ status: "ok" }, (r) => ({
status: "ready" as const,
files: r.data.map((item) => ({
name: item.filename,
key: item.path,
})),
}))
.with({ status: "error" }, (r) => {
if (r.error.filename) {
return {
status: "error" as const,
reason: r.error.reason,
filename: r.error.filename,
};
}
return { status: "error" as const, reason: r.error.reason };
})
.exhaustive();
});
});
}, [paths]);
if (state.status === "hidden") {
return null;
}
const inner = match(state)
.with({ status: "loading" }, ({ count }) =>
Array.from({ length: count }).map((_, i) => (
<Item
key={i}
icon={
<CircleQuestionMarkIcon className="w-5 h-5 text-neutral-300/50" />
}
text={
<span className="inline-block w-32 h-5 bg-neutral-300/10 rounded animate-pulse" />
}
/>
))
)
.with({ status: "ready" }, (r) => {
return r.files
.slice(0, 8)
.map((file) => <FileItem key={file.key} filename={file.name} />);
})
.with({ status: "error", filename: P.string }, (r) => {
return <FileItem filename={r.filename} error={r.reason} />;
})
.with({ status: "error" }, ({ reason }) => {
return (
<Item
icon={<CircleQuestionMarkIcon className="w-5 h-5 text-neutral-300" />}
text={reason}
/>
);
})
.exhaustive();
return (
<div className="absolute z-10 top-0 left-0 w-full h-full bg-black/40 backdrop-blur-sm transition-all duration-300 ease-in-out">
<div className="flex flex-col justify-center items-center h-full">
<span className="text-white text-2xl">{inner}</span>
</div>
</div>
);
};
export default DropOverlay;

View File

@@ -1,85 +0,0 @@
import { ResponsiveLine } from "@nivo/line";
import { formatBytes } from "../lib/format.js";
type Frame = {
id: string;
data: { x: string | number; y: number }[];
};
type GraphProps = {
data: Frame[];
};
const Graph = ({ data }: GraphProps) => (
<ResponsiveLine
data={data}
margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
xScale={{ type: "linear" }}
yScale={{
type: "linear",
min: 0,
max: "auto",
stacked: false,
reverse: false,
}}
theme={{
tooltip: {
container: {
backgroundColor: "#2e2b45",
},
},
grid: {
line: {
stroke: "rgb(252, 191, 212)",
strokeWidth: 0.35,
strokeOpacity: 0.75,
},
},
crosshair: {
line: {
stroke: "#fdd3e2",
strokeWidth: 1,
},
},
axis: {
legend: {},
domain: {
line: {
stroke: "rgb(252, 191, 212)",
strokeWidth: 0.5,
strokeOpacity: 0.5,
},
},
},
text: {
fill: "#6e6a86",
},
}}
axisBottom={{ legend: "transportation", legendOffset: 36 }}
axisLeft={{
legend: "count",
legendOffset: -40,
format: (v) => formatBytes(v * 1024 * 53),
}}
pointSize={10}
colors={["#3e8faf", "#c4a7e7", "#f5c276", "#EA9B96", "#EB7092", "#9CCFD8"]}
pointBorderWidth={0}
pointBorderColor={{ from: "seriesColor" }}
pointLabelYOffset={-12}
enableSlices={"x"}
enableTouchCrosshair={true}
useMesh={true}
legends={[
{
anchor: "bottom-right",
direction: "column",
translateX: 100,
itemWidth: 80,
itemHeight: 22,
symbolShape: "circle",
},
]}
/>
);
export default Graph;

View File

@@ -0,0 +1,381 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import { match, P } from "ts-pattern";
import {
CheckCircle,
File as FileIcon,
FileText,
Film,
Image,
Loader2,
Music,
XCircle,
} from "lucide-react";
import { commands } from "../../bindings";
import type { MediaType, StreamDetail } from "../../bindings";
type DropOverlayProps = {
paths: string[];
};
type State =
| { status: "hidden" }
| { status: "loading"; count: number }
| {
status: "ready";
files: {
name: string;
key: string;
media_type: MediaType;
duration?: number | null;
size: number;
streams: StreamDetail[];
}[];
}
| { status: "error"; reason: string; filename?: string; error_type?: string };
type FileItemProps = {
filename: string;
media_type: MediaType;
duration?: number | null;
size: number;
streams: StreamDetail[];
error?: string;
error_type?: string;
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
}
return `${minutes}:${secs.toString().padStart(2, "0")}`;
};
const getFileIcon = (
mediaType: MediaType,
error?: string,
errorType?: string,
) => {
// For non-media files, show a neutral icon instead of error icon
if (errorType === "not_media") {
switch (mediaType) {
case "Executable":
return <FileIcon className="w-5 h-5 text-orange-400" />;
case "Archive":
return <FileIcon className="w-5 h-5 text-yellow-400" />;
case "Library":
return <FileIcon className="w-5 h-5 text-indigo-400" />;
case "Document":
return <FileText className="w-5 h-5 text-green-400" />;
default:
return <FileIcon className="w-5 h-5 text-neutral-300" />;
}
}
if (error) {
return <XCircle className="w-5 h-5 text-red-400" />;
}
switch (mediaType) {
case "Audio":
return <Music className="w-5 h-5 text-blue-400" />;
case "Video":
return <Film className="w-5 h-5 text-purple-400" />;
case "Image":
return <Image className="w-5 h-5 text-pink-400" />;
case "Document":
return <FileText className="w-5 h-5 text-green-400" />;
case "Executable":
return <FileIcon className="w-5 h-5 text-orange-400" />;
case "Archive":
return <FileIcon className="w-5 h-5 text-yellow-400" />;
case "Library":
return <FileIcon className="w-5 h-5 text-indigo-400" />;
default:
return <FileIcon className="w-5 h-5 text-neutral-300" />;
}
};
const getStreamInfo = (
streams: StreamDetail[],
mediaType: MediaType,
): string => {
// For non-media files, return file type description
if (!["Audio", "Video", "Image"].includes(mediaType)) {
switch (mediaType) {
case "Executable":
return "Executable file";
case "Archive":
return "Archive file";
case "Library":
return "Library file";
case "Document":
return "Document file";
default:
return "Unknown file type";
}
}
// For media files, analyze streams
const videoStreams = streams.filter((s: any) => "Video" in s);
const audioStreams = streams.filter((s: any) => "Audio" in s);
const subtitleStreams = streams.filter((s: any) => "Subtitle" in s);
const parts = [] as string[];
if (videoStreams.length > 0) {
const video: any = videoStreams[0] as any;
if ("Video" in video) {
const width = (video as any).Video.width;
const height = (video as any).Video.height;
const codec = (video as any).Video.codec;
if (width && height) {
parts.push(`${width}x${height} ${codec}`);
} else {
parts.push(codec);
}
}
}
if (audioStreams.length > 0) {
const audio: any = audioStreams[0] as any;
if ("Audio" in audio) {
parts.push(`${(audio as any).Audio.codec} audio`);
}
}
if (subtitleStreams.length > 0) {
parts.push(`${subtitleStreams.length} subtitle(s)`);
}
return parts.join(", ");
};
const Item = ({
icon,
text,
subtitle,
status,
}: {
icon: ReactNode;
text: ReactNode;
subtitle?: ReactNode;
status?: "success" | "error" | "loading";
}) => {
const statusColor =
status === "success"
? "border-green-500"
: status === "error"
? "border-red-500"
: status === "loading"
? "border-blue-500"
: "border-neutral-600";
return (
<div
className={`flex items-center gap-3 px-4 py-3 bg-neutral-800 rounded-lg shadow-lg border-2 ${statusColor} transition-all duration-200`}
style={{
maxWidth: "100%",
marginBottom: "0.75rem",
}}
>
{icon}
<div className="flex-1 min-w-0">
<div className="truncate text-neutral-100 font-medium">{text}</div>
{subtitle && (
<div className="truncate text-neutral-400 text-sm mt-1">
{subtitle}
</div>
)}
</div>
</div>
);
};
const FileItem = ({
filename,
media_type,
duration,
size,
streams,
error,
error_type,
}: FileItemProps) => {
const icon = getFileIcon(media_type, error, error_type);
const fileSize = formatFileSize(size);
let subtitle: ReactNode;
let status: "success" | "error" | "loading" | undefined;
if (error) {
subtitle = error;
// For non-media files, show as neutral instead of error
status = error_type === "not_media" ? undefined : "error";
} else {
const streamInfo = getStreamInfo(streams, media_type);
const durationStr = duration ? formatDuration(duration) : null;
const details = [streamInfo, durationStr, fileSize].filter(
Boolean,
) as string[];
subtitle = details.join(" • ");
status = "success";
}
return (
<Item icon={icon} text={filename} subtitle={subtitle} status={status} />
);
};
const DropOverlay = ({ paths }: DropOverlayProps) => {
const [state, setState] = useState<State>({ status: "hidden" });
const aborterRef = useRef<AbortController | null>(null);
useEffect(() => {
if (paths.length === 0) {
setState({ status: "hidden" });
return;
}
setState({ status: "loading", count: paths.length });
aborterRef.current = new AbortController();
commands.hasStreams(paths).then((result) => {
setState((_state) => {
return match(result)
.with({ status: "ok" }, (r) => ({
status: "ready" as const,
files: r.data.map((item) => ({
name: item.filename,
key: item.path,
media_type: item.media_type,
duration: item.duration,
size: Number(item.size),
streams: item.streams,
})),
}))
.with({ status: "error" }, (r) => {
if (r.error.filename) {
return {
status: "error" as const,
reason: r.error.reason,
filename: r.error.filename,
error_type: r.error.error_type,
};
}
return {
status: "error" as const,
reason: r.error.reason,
error_type: r.error.error_type,
};
})
.exhaustive();
});
});
}, [paths]);
if (state.status === "hidden") {
return null;
}
const inner = match(state)
.with({ status: "loading" }, ({ count }) => (
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-8 h-8 text-blue-400 animate-spin" />
<div className="text-white text-lg font-medium">
Analyzing {count} file{count > 1 ? "s" : ""}...
</div>
{Array.from({ length: Math.min(count, 3) }).map((_, i) => (
<Item
key={i}
icon={
<Loader2 className="w-5 h-5 text-neutral-300/50 animate-spin" />
}
text={
<span className="inline-block w-32 h-5 bg-neutral-300/10 rounded animate-pulse" />
}
status="loading"
/>
))}
</div>
))
.with({ status: "ready" }, (r) => {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2 text-green-400">
<CheckCircle className="w-6 h-6" />
<span className="text-lg font-medium">Files Ready</span>
</div>
<div className="max-h-96 overflow-y-auto">
{r.files.slice(0, 8).map((file) => (
<FileItem
key={file.key}
filename={file.name}
media_type={file.media_type}
duration={file.duration}
size={file.size}
streams={file.streams}
/>
))}
</div>
</div>
);
})
.with({ status: "error", filename: P.string }, (r) => {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2 text-red-400">
<XCircle className="w-6 h-6" />
<span className="text-lg font-medium">Error</span>
</div>
<FileItem
filename={r.filename}
media_type="Unknown"
size={0}
streams={[]}
error={r.reason}
error_type={r.error_type}
/>
</div>
);
})
.with({ status: "error" }, ({ reason }) => {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2 text-red-400">
<XCircle className="w-6 h-6" />
<span className="text-lg font-medium">Error</span>
</div>
<Item
icon={<XCircle className="w-5 h-5 text-red-400" />}
text={reason}
status="error"
/>
</div>
);
})
.exhaustive();
return (
<div className="absolute z-10 top-0 left-0 w-full h-full bg-black/60 backdrop-blur-sm transition-all duration-300 ease-in-out">
<div className="flex flex-col justify-center items-center h-full p-8">
<div className="bg-neutral-900 rounded-xl p-6 shadow-2xl max-w-2xl w-full">
{inner}
</div>
</div>
</div>
);
};
export default DropOverlay;

View File

@@ -0,0 +1,81 @@
import { ResponsiveLine } from "@nivo/line";
import { formatBytes } from "../../lib/format.js";
import type { Frame } from "../../types/graph.js";
type GraphProps = {
data: Frame[];
};
const Graph = ({ data }: GraphProps) => (
<ResponsiveLine
data={data}
margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
xScale={{ type: "linear" }}
yScale={{
type: "linear",
min: 0,
max: "auto",
stacked: false,
reverse: false,
}}
theme={{
tooltip: {
container: {
backgroundColor: "#2e2b45",
},
},
grid: {
line: {
stroke: "rgb(252, 191, 212)",
strokeWidth: 0.35,
strokeOpacity: 0.75,
},
},
crosshair: {
line: {
stroke: "#fdd3e2",
strokeWidth: 1,
},
},
axis: {
legend: {},
domain: {
line: {
stroke: "rgb(252, 191, 212)",
strokeWidth: 0.5,
strokeOpacity: 0.5,
},
},
},
text: {
fill: "#6e6a86",
},
}}
axisBottom={{ legend: "transportation", legendOffset: 36 }}
axisLeft={{
legend: "count",
legendOffset: -40,
format: (v) => formatBytes(v * 1024 * 53),
}}
pointSize={10}
colors={["#3e8faf", "#c4a7e7", "#f5c276", "#EA9B96", "#EB7092", "#9CCFD8"]}
pointBorderWidth={0}
pointBorderColor={{ from: "seriesColor" }}
pointLabelYOffset={-12}
enableSlices={"x"}
enableTouchCrosshair={true}
useMesh={true}
legends={[
{
anchor: "bottom-right",
direction: "column",
translateX: 100,
itemWidth: 80,
itemHeight: 22,
symbolShape: "circle",
},
]}
/>
);
export default Graph;

View File

@@ -1,22 +1,22 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
font-weight: 400; font-weight: 400;
color: #e0def4; color: #e0def4;
background-color: #232136; background-color: #232136;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
} }
#app { #app {
height: 100vh; height: 100vh;
text-align: center; text-align: center;
} }

View File

@@ -0,0 +1,24 @@
import { useEffect, useState } from "react";
import { getCurrentWebview } from "@tauri-apps/api/webview";
export function useDragDropPaths(): string[] {
const [paths, setPaths] = useState<string[]>([]);
useEffect(() => {
const unlistenPromise = getCurrentWebview().onDragDropEvent(
async ({ payload }) => {
if (payload.type === "enter") {
setPaths(payload.paths);
} else if (payload.type === "leave" || payload.type === "drop") {
setPaths([]);
}
},
);
return () => {
unlistenPromise.then((unlisten) => unlisten());
};
}, []);
return paths;
}

View File

@@ -2,27 +2,27 @@ import { formatBytes } from "./format.js";
import { test, expect } from "vitest"; import { test, expect } from "vitest";
test("formats bytes less than 1024", () => { test("formats bytes less than 1024", () => {
expect(formatBytes(512)).toBe("512 B"); expect(formatBytes(512)).toBe("512 B");
}); });
test("formats KiB correctly", () => { test("formats KiB correctly", () => {
expect(formatBytes(2048)).toBe("2 KiB"); expect(formatBytes(2048)).toBe("2 KiB");
expect(formatBytes(1536)).toBe("1.5 KiB"); expect(formatBytes(1536)).toBe("1.5 KiB");
expect(formatBytes(1024)).toBe("1 KiB"); expect(formatBytes(1024)).toBe("1 KiB");
}); });
test("formats MiB correctly", () => { test("formats MiB correctly", () => {
expect(formatBytes(1048576)).toBe("1 MiB"); expect(formatBytes(1048576)).toBe("1 MiB");
expect(formatBytes(1572864)).toBe("1.5 MiB"); expect(formatBytes(1572864)).toBe("1.5 MiB");
expect(formatBytes(2097152)).toBe("2 MiB"); expect(formatBytes(2097152)).toBe("2 MiB");
}); });
test("formats GiB correctly", () => { test("formats GiB correctly", () => {
expect(formatBytes(1073741824)).toBe("1 GiB"); expect(formatBytes(1073741824)).toBe("1 GiB");
expect(formatBytes(1610612736)).toBe("1.5 GiB"); expect(formatBytes(1610612736)).toBe("1.5 GiB");
expect(formatBytes(2147483648)).toBe("2 GiB"); expect(formatBytes(2147483648)).toBe("2 GiB");
}); });
test("formats large values with no decimal if intValue >= 1000", () => { test("formats large values with no decimal if intValue >= 1000", () => {
expect(formatBytes(1024 * 1024 * 1000)).toBe("1000 MiB"); expect(formatBytes(1024 * 1024 * 1000)).toBe("1000 MiB");
}); });

View File

@@ -11,28 +11,28 @@
* @returns The formatted string with the appropriate unit. * @returns The formatted string with the appropriate unit.
*/ */
export function formatBytes(v: number): string { export function formatBytes(v: number): string {
if (v < 1024) return `${v} B`; if (v < 1024) return `${v} B`;
const units = ["KiB", "MiB", "GiB", "TiB"]; const units = ["KiB", "MiB", "GiB", "TiB"];
let unitIndex = -1; let unitIndex = -1;
let value = v; let value = v;
while (value >= 1024 && unitIndex < units.length - 1) { while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024; value /= 1024;
unitIndex++; unitIndex++;
} }
const intValue = Math.floor(value); const intValue = Math.floor(value);
const decimal = value - intValue; const decimal = value - intValue;
if (intValue >= 1000) { if (intValue >= 1000) {
// More than 3 digits, no decimal // More than 3 digits, no decimal
return `${intValue} ${units[unitIndex]}`; return `${intValue} ${units[unitIndex]}`;
} else if (decimal >= 0.1) { } else if (decimal >= 0.1) {
// Show 1 decimal if decimal >= 0.1 // Show 1 decimal if decimal >= 0.1
return `${value.toFixed(1)} ${units[unitIndex]}`; return `${value.toFixed(1)} ${units[unitIndex]}`;
} else { } else {
// No decimal // No decimal
return `${intValue} ${units[unitIndex]}`; return `${intValue} ${units[unitIndex]}`;
} }
} }

View File

@@ -4,7 +4,7 @@ import App from "./App";
import "./global.css"; import "./global.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>,
); );

4
src/types/graph.ts Normal file
View File

@@ -0,0 +1,4 @@
export type Frame = {
id: string;
data: { x: string | number; y: number }[];
};

View File

@@ -1,25 +1,25 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"skipLibCheck": true, "skipLibCheck": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

View File

@@ -7,27 +7,27 @@ const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(async () => ({ export default defineConfig(async () => ({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// //
// 1. prevent Vite from obscuring rust errors // 1. prevent Vite from obscuring rust errors
clearScreen: false, clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available // 2. tauri expects a fixed port, fail if that port is not available
server: { server: {
port: 1420, port: 1420,
strictPort: true, strictPort: true,
host: host || false, host: host || false,
hmr: host hmr: host
? { ? {
protocol: "ws", protocol: "ws",
host, host,
port: 1421, port: 1421,
} }
: undefined, : undefined,
watch: { watch: {
// 3. tell Vite to ignore watching `src-tauri` // 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"], ignored: ["**/src-tauri/**"],
}, },
}, },
})); }));