mirror of
https://github.com/Xevion/byte-me.git
synced 2026-01-31 14:23:47 -06:00
Compare commits
79 Commits
cd8feeabd2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
442cd156e9 | ||
|
|
55cd4ae921 | ||
|
|
7cce282f0e | ||
|
|
d724a17e21 | ||
|
|
a806af3bba | ||
|
|
8e241898d4 | ||
|
|
3e9e3097f5 | ||
|
|
83d6eb972e | ||
|
|
7b42cf25b9 | ||
|
|
49cca26d1c | ||
|
|
9c62083c0f | ||
|
|
a80ee58596 | ||
|
|
5b575132ea | ||
|
|
2d6d60d1a3 | ||
|
|
3683f60052 | ||
|
|
73e36e3385 | ||
|
|
9578194e3a | ||
|
|
bd33e5f52f | ||
|
|
56cfd1c431 | ||
|
|
e5f8d3b20e | ||
|
|
0de6a9e894 | ||
|
|
2a5cefafd5 | ||
|
|
05413fdd3d | ||
|
|
69e4cca414 | ||
|
|
3e603bde39 | ||
|
|
27c980596d | ||
|
|
78f57b4c3b | ||
|
|
41dfe9f7ef | ||
|
|
80ebc4f920 | ||
|
|
5c74051126 | ||
|
|
5e287b0b80 | ||
|
|
65b10074bc | ||
|
|
a322fe14c3 | ||
|
|
815df37222 | ||
|
|
2e46ffe8ac | ||
|
|
d1fa3d0ce2 | ||
|
|
11551abb02 | ||
|
|
2946048a59 | ||
|
|
2c83fa226d | ||
|
|
906bef8fcf | ||
|
|
34bfbe7646 | ||
|
|
fc474f854b | ||
|
|
037ec4e87e | ||
|
|
0cfa8eebaf | ||
|
|
62b8ea1ace | ||
|
|
cecc9cc79d | ||
|
|
f8b4ff9206 | ||
|
|
29797df2d3 | ||
|
|
0a2bf9fec7 | ||
|
|
17b48a9d66 | ||
|
|
a7e91e7b3f | ||
|
|
55ca73b963 | ||
|
|
44ee9838f6 | ||
|
|
423043f97f | ||
|
|
164559eb70 | ||
|
|
103e94651f | ||
|
|
ae351112e9 | ||
|
|
d867ba61d9 | ||
|
|
8f731d27cc | ||
|
|
bd13077d6c | ||
|
|
9ac0b8f296 | ||
|
|
39f4951612 | ||
|
|
d80de0cf0e | ||
|
|
6118916fda | ||
|
|
5b89e8cd15 | ||
|
|
a50b6b9d0e | ||
|
|
7806536217 | ||
|
|
5979cec141 | ||
|
|
994c954789 | ||
|
|
9155e7a153 | ||
|
|
3345ce9ef0 | ||
|
|
dcfd24ad09 | ||
|
|
b88673eb56 | ||
|
|
7e5112cfec | ||
|
|
ccbd9198d3 | ||
|
|
eb3c651c7c | ||
|
|
6b6f18a27e | ||
|
|
9645e1b6b5 | ||
|
|
22d73fabfd |
Vendored
-43
@@ -1,43 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
# Enable version updates for npm
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
reviewers:
|
|
||||||
- "dependabot[bot]"
|
|
||||||
assignees:
|
|
||||||
- "dependabot[bot]"
|
|
||||||
commit-message:
|
|
||||||
prefix: "chore"
|
|
||||||
include: "scope"
|
|
||||||
|
|
||||||
# Enable version updates for Cargo
|
|
||||||
- package-ecosystem: "cargo"
|
|
||||||
directory: "/src-tauri"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
reviewers:
|
|
||||||
- "dependabot[bot]"
|
|
||||||
assignees:
|
|
||||||
- "dependabot[bot]"
|
|
||||||
commit-message:
|
|
||||||
prefix: "chore"
|
|
||||||
include: "scope"
|
|
||||||
|
|
||||||
# Enable version updates for GitHub Actions
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
reviewers:
|
|
||||||
- "dependabot[bot]"
|
|
||||||
assignees:
|
|
||||||
- "dependabot[bot]"
|
|
||||||
commit-message:
|
|
||||||
prefix: "chore"
|
|
||||||
include: "scope"
|
|
||||||
Vendored
+6
-6
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -60,12 +60,12 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10.26.2
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24.12.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache apt packages
|
- name: Cache apt packages
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: /var/cache/apt/archives
|
path: /var/cache/apt/archives
|
||||||
key: ${{ runner.os }}-apt-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-apt-${{ hashFiles('**/Cargo.lock') }}
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
run: cargo tauri build --target ${{ matrix.target }}
|
run: cargo tauri build --target ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Upload binary artifact
|
- name: Upload binary artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.artifact_name }}
|
name: ${{ matrix.artifact_name }}
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
Vendored
+5
-5
@@ -14,7 +14,7 @@ jobs:
|
|||||||
name: Rust Code Quality
|
name: Rust Code Quality
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@nightly
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
@@ -65,17 +65,17 @@ jobs:
|
|||||||
name: Frontend Code Quality
|
name: Frontend Code Quality
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10.26.2
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24.12.0
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
Vendored
+4
-4
@@ -23,17 +23,17 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10.26.2
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24.12.0
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
|
|||||||
Vendored
+5
-5
@@ -9,7 +9,7 @@ jobs:
|
|||||||
name: Rust Security Audit
|
name: Rust Security Audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install cargo-deny
|
- name: Install cargo-deny
|
||||||
uses: taiki-e/install-action@cargo-deny
|
uses: taiki-e/install-action@cargo-deny
|
||||||
@@ -21,17 +21,17 @@ jobs:
|
|||||||
name: NPM Security Audit
|
name: NPM Security Audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10.26.2
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "24.12.0"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Default recipe - show available commands
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
dev:
|
||||||
|
cargo tauri dev
|
||||||
|
|
||||||
|
dev-build:
|
||||||
|
cargo tauri build --debug
|
||||||
|
|
||||||
|
generate-types:
|
||||||
|
cargo test --manifest-path src-tauri/Cargo.toml -- --test export_bindings
|
||||||
|
|
||||||
|
check-frontend:
|
||||||
|
pnpm exec tsc --noEmit
|
||||||
|
|
||||||
|
check-backend:
|
||||||
|
cargo clippy --manifest-path src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
check: check-frontend check-backend
|
||||||
|
|
||||||
|
build-frontend: generate-types
|
||||||
|
pnpm exec tsc
|
||||||
|
pnpm exec vite build
|
||||||
|
|
||||||
|
build-backend:
|
||||||
|
cargo build --manifest-path src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
build: build-frontend build-backend
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
pnpm exec vitest run
|
||||||
|
|
||||||
|
test-backend:
|
||||||
|
cargo nextest run --manifest-path src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
test: test-frontend test-backend
|
||||||
+19
-18
@@ -7,31 +7,32 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "pnpm generate-types && tsc && vite build",
|
"build": "pnpm generate-types && tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"generate-types": "cargo test --manifest-path src-tauri/Cargo.toml -- --test export_bindings"
|
"generate-types": "cargo test --manifest-path src-tauri/Cargo.toml -- --test export_bindings"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nivo/core": "^0.99.0",
|
"@nivo/core": "^0.99.0",
|
||||||
"@nivo/line": "^0.99.0",
|
"@nivo/line": "^0.99.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2.9.1",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||||
"lucide-react": "^0.540.0",
|
"lucide-react": "^0.562.0",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.3",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.18",
|
||||||
"ts-pattern": "^5.7.1"
|
"ts-pattern": "^5.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@tsconfig/vite-react": "^7.0.0",
|
"@tsconfig/vite-react": "^7.0.2",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^19.2.8",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.8.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^6.0.3",
|
"vite": "^7.3.1",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^4.0.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+886
-974
File diff suppressed because it is too large
Load Diff
+153
@@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":enableVulnerabilityAlertsWithLabel(security)"
|
||||||
|
],
|
||||||
|
"schedule": ["before 6am on Monday"],
|
||||||
|
"timezone": "UTC",
|
||||||
|
"prConcurrentLimit": 10,
|
||||||
|
"prHourlyLimit": 0,
|
||||||
|
"semanticCommits": "enabled",
|
||||||
|
"dependencyDashboard": true,
|
||||||
|
"labels": ["dependencies"],
|
||||||
|
"rangeStrategy": "bump",
|
||||||
|
"postUpdateOptions": ["pnpmDedupe"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Automerge non-major dev dependencies",
|
||||||
|
"matchDepTypes": ["devDependencies"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeType": "pr",
|
||||||
|
"ignoreTests": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Automerge patch updates for production dependencies",
|
||||||
|
"matchDepTypes": ["dependencies"],
|
||||||
|
"matchUpdateTypes": ["patch"],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeType": "pr",
|
||||||
|
"ignoreTests": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group all Tauri packages together",
|
||||||
|
"groupName": "Tauri",
|
||||||
|
"matchManagers": ["npm"],
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["dependencies", "tauri"],
|
||||||
|
"matchPackageNames": ["/^@tauri-apps//", "/^tauri-/"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group Tauri Rust dependencies",
|
||||||
|
"groupName": "Tauri (Rust)",
|
||||||
|
"matchPackageNames": ["tauri", "tauri-build"],
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["dependencies", "tauri", "rust"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group React ecosystem updates",
|
||||||
|
"groupName": "React",
|
||||||
|
"matchPackageNames": ["react", "react-dom", "/^@types/react/"],
|
||||||
|
"labels": ["dependencies", "react"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group TypeScript and build tooling",
|
||||||
|
"groupName": "Build tooling",
|
||||||
|
"matchPackageNames": [
|
||||||
|
"typescript",
|
||||||
|
"vite",
|
||||||
|
"@vitejs/plugin-react",
|
||||||
|
"vite-tsconfig-paths"
|
||||||
|
],
|
||||||
|
"labels": ["dependencies", "tooling"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group ESLint and related plugins",
|
||||||
|
"groupName": "ESLint",
|
||||||
|
"labels": ["dependencies", "linting"],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"/^eslint/",
|
||||||
|
"/^@eslint//",
|
||||||
|
"/^@typescript-eslint//"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group testing frameworks",
|
||||||
|
"groupName": "Testing",
|
||||||
|
"labels": ["dependencies", "testing"],
|
||||||
|
"matchPackageNames": ["/^vitest/", "/^@vitest//"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group TailwindCSS and plugins",
|
||||||
|
"groupName": "TailwindCSS",
|
||||||
|
"labels": ["dependencies", "styling"],
|
||||||
|
"matchPackageNames": ["/^tailwindcss/", "/^@tailwindcss//"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group Nivo chart libraries",
|
||||||
|
"groupName": "Nivo",
|
||||||
|
"labels": ["dependencies", "charts"],
|
||||||
|
"matchPackageNames": ["/^@nivo//"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Separate major updates for manual review",
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["dependencies", "major-update"],
|
||||||
|
"reviewers": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "High priority security updates",
|
||||||
|
"matchUpdateTypes": ["security"],
|
||||||
|
"labels": ["dependencies", "security"],
|
||||||
|
"automerge": false,
|
||||||
|
"schedule": ["at any time"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Rust patch updates - automerge",
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"matchUpdateTypes": ["patch"],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeType": "pr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Rust minor updates - review required",
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"matchUpdateTypes": ["minor"],
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["dependencies", "rust", "minor-update"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Rust major updates - careful review",
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["dependencies", "rust", "major-update"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Pin ts-rs (type generation critical)",
|
||||||
|
"matchPackageNames": ["ts-rs"],
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["dependencies", "rust", "type-generation"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cargo": {
|
||||||
|
"enabled": true,
|
||||||
|
"rangeStrategy": "bump"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"enabled": true,
|
||||||
|
"rangeStrategy": "bump"
|
||||||
|
},
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true,
|
||||||
|
"automerge": true,
|
||||||
|
"schedule": ["before 6am on Monday"]
|
||||||
|
},
|
||||||
|
"platformAutomerge": true,
|
||||||
|
"ignoreTests": false,
|
||||||
|
"commitMessagePrefix": "chore(deps):"
|
||||||
|
}
|
||||||
Generated
+955
-971
File diff suppressed because it is too large
Load Diff
@@ -15,14 +15,14 @@ name = "byte_me_lib"
|
|||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.4.0", features = [] }
|
tauri-build = { version = "2.5.3", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.8.2", features = [] }
|
tauri = { version = "2.9.5", features = [] }
|
||||||
tauri-plugin-opener = "2.5.0"
|
tauri-plugin-opener = "2.5.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
ffprobe = "0.4.0"
|
ffprobe = "0.4.0"
|
||||||
ts-rs = { version = "11.0", features = ["format"] }
|
ts-rs = { version = "11.1.0", features = ["format"] }
|
||||||
infer = "0.19.0"
|
infer = "0.19.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
|
|||||||
+165
-2
@@ -5,9 +5,10 @@ pub mod strings;
|
|||||||
|
|
||||||
use ff::extract_streams;
|
use ff::extract_streams;
|
||||||
use media::{detect_media_type, is_media_file};
|
use media::{detect_media_type, is_media_file};
|
||||||
use models::{StreamResult, StreamResultError};
|
use models::{StreamResult, StreamResultError, File, FileCandidacy, BitrateData, BitrateFrame};
|
||||||
use strings::transform_filename;
|
use strings::transform_filename;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
use tracing::{debug, error, info, instrument, warn};
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
|
|
||||||
// detection, helpers moved to modules above
|
// detection, helpers moved to modules above
|
||||||
@@ -132,12 +133,174 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
|||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[instrument(skip(paths), fields(file_count = paths.len()))]
|
||||||
|
fn analyze_files(paths: Vec<String>) -> Vec<File> {
|
||||||
|
info!(file_count = paths.len(), "Analyzing files for candidacy");
|
||||||
|
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, path_str)| {
|
||||||
|
let path = Path::new(&path_str);
|
||||||
|
let filename = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Log full path only on first occurrence, then use truncated filename
|
||||||
|
if index == 0 {
|
||||||
|
debug!(full_path = %path_str, filename = %filename, "Processing first file");
|
||||||
|
} else {
|
||||||
|
let truncated_name = transform_filename(&filename, 15);
|
||||||
|
debug!(filename = %truncated_name, "Processing file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file size
|
||||||
|
let size = std::fs::metadata(&path_str)
|
||||||
|
.map(|metadata| metadata.len())
|
||||||
|
.unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let truncated_name = transform_filename(&filename, 15);
|
||||||
|
debug!(filename = %truncated_name, size = size, "File metadata retrieved");
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if !path.exists() {
|
||||||
|
let truncated_name = transform_filename(&filename, 15);
|
||||||
|
warn!(filename = %truncated_name, "File does not exist");
|
||||||
|
return File {
|
||||||
|
filename,
|
||||||
|
size,
|
||||||
|
candidacy: FileCandidacy::Error {
|
||||||
|
reason: "File does not exist".to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a file (not directory)
|
||||||
|
if !path.is_file() {
|
||||||
|
let truncated_name = transform_filename(&filename, 15);
|
||||||
|
warn!(filename = %truncated_name, "Path is not a file");
|
||||||
|
return File {
|
||||||
|
filename,
|
||||||
|
size,
|
||||||
|
candidacy: FileCandidacy::Error {
|
||||||
|
reason: "Not a file (directory or other)".to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect media type using magic numbers and fallback to extensions
|
||||||
|
let media_type = detect_media_type(path);
|
||||||
|
debug!(filename = %truncated_name, media_type = ?media_type, "Media type detected");
|
||||||
|
|
||||||
|
// Check if it's a media file
|
||||||
|
if is_media_file(&media_type) {
|
||||||
|
info!(filename = %truncated_name, media_type = ?media_type, "Valid media file detected");
|
||||||
|
File {
|
||||||
|
filename,
|
||||||
|
size,
|
||||||
|
candidacy: FileCandidacy::Success {
|
||||||
|
file_type: media_type,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!(filename = %truncated_name, media_type = ?media_type, "Non-media file detected");
|
||||||
|
File {
|
||||||
|
filename,
|
||||||
|
size,
|
||||||
|
candidacy: FileCandidacy::Error {
|
||||||
|
reason: format!("Not a media file (detected as {media_type:?})"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[instrument(skip(path), fields(path = %path))]
|
||||||
|
fn extract_bitrate_data(path: String) -> Result<BitrateData, String> {
|
||||||
|
info!(path = %path, "Extracting bitrate data from video file");
|
||||||
|
|
||||||
|
let path_obj = Path::new(&path);
|
||||||
|
let filename = path_obj
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if !path_obj.exists() {
|
||||||
|
error!(filename = %filename, "File does not exist");
|
||||||
|
return Err("File does not exist".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run ffprobe to get frame packet sizes
|
||||||
|
// -v quiet: suppress ffprobe info
|
||||||
|
// -select_streams v:0: only first video stream
|
||||||
|
// -show_entries frame=pkt_size: only show packet size
|
||||||
|
// -of csv=p=0: output as CSV without headers
|
||||||
|
info!(filename = %filename, "Running ffprobe to extract frame data");
|
||||||
|
|
||||||
|
let output = Command::new("ffprobe")
|
||||||
|
.args([
|
||||||
|
"-v", "quiet",
|
||||||
|
"-select_streams", "v:0",
|
||||||
|
"-show_entries", "frame=pkt_size",
|
||||||
|
"-of", "csv=p=0",
|
||||||
|
&path
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = %e, "Failed to execute ffprobe");
|
||||||
|
format!("Failed to execute ffprobe: {e}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
error!(stderr = %stderr, "ffprobe command failed");
|
||||||
|
return Err(format!("ffprobe failed: {stderr}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
debug!(line_count = stdout.lines().count(), "Parsing ffprobe output");
|
||||||
|
|
||||||
|
let frames: Vec<BitrateFrame> = stdout
|
||||||
|
.lines()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, line)| {
|
||||||
|
line.trim().parse::<u64>().ok().map(|packet_size| BitrateFrame {
|
||||||
|
frame_num: index as u32,
|
||||||
|
packet_size,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if frames.is_empty() {
|
||||||
|
warn!(filename = %filename, "No frame data extracted");
|
||||||
|
return Err("No frame data could be extracted from file".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
filename = %filename,
|
||||||
|
frame_count = frames.len(),
|
||||||
|
"Successfully extracted bitrate data"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(BitrateData {
|
||||||
|
id: filename,
|
||||||
|
frames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
info!("Initializing Tauri application");
|
info!("Initializing Tauri application");
|
||||||
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, analyze_files, extract_bitrate_data])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,39 @@ pub struct StreamResultError {
|
|||||||
pub error_type: String,
|
pub error_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New types for simplified drop overlay
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||||
|
pub struct File {
|
||||||
|
pub filename: String,
|
||||||
|
pub size: u32,
|
||||||
|
pub candidacy: FileCandidacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||||
|
pub enum FileCandidacy {
|
||||||
|
Success {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
file_type: MediaType,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
Loading,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitrate visualization types
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||||
|
pub struct BitrateFrame {
|
||||||
|
pub frame_num: u32,
|
||||||
|
pub packet_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||||
|
pub struct BitrateData {
|
||||||
|
pub id: String,
|
||||||
|
pub frames: Vec<BitrateFrame>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
@@ -62,5 +95,9 @@ mod tests {
|
|||||||
StreamResult::export_all_to("../src/bindings").expect("Failed to export bindings");
|
StreamResult::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||||
StreamResultError::export_all_to("../src/bindings").expect("Failed to export bindings");
|
StreamResultError::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||||
MediaType::export_all_to("../src/bindings").expect("Failed to export bindings");
|
MediaType::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||||
|
File::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||||
|
FileCandidacy::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||||
|
BitrateFrame::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||||
|
BitrateData::export_all_to("../src/bindings").expect("Failed to export bindings");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-2
@@ -1,13 +1,45 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useDragDropPaths } from "@/hooks/useDragDropPaths";
|
import { useDragDropPaths } from "@/hooks/useDragDropPaths";
|
||||||
import Graph from "@/components/graph";
|
import Graph from "@/components/graph";
|
||||||
import DropOverlay from "@/components/drop-overlay";
|
import DropOverlay from "@/components/drop-overlay";
|
||||||
import type { Frame } from "@/types/graph";
|
import type { Frame } from "@/types/graph";
|
||||||
|
import { commands } from "@/bindings";
|
||||||
|
import type { BitrateData } from "@/bindings";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const data: Frame[] = [];
|
const [data, setData] = useState<Frame[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const paths = useDragDropPaths();
|
const paths = useDragDropPaths();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paths.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For minimal prototype, just process the first file
|
||||||
|
const firstPath = paths[0];
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
commands
|
||||||
|
.extractBitrateData(firstPath)
|
||||||
|
.then((bitrateData: BitrateData) => {
|
||||||
|
// Transform BitrateData to Nivo's Frame format
|
||||||
|
const frame: Frame = {
|
||||||
|
id: bitrateData.id,
|
||||||
|
data: bitrateData.frames.map((frame) => ({
|
||||||
|
x: frame.frame_num,
|
||||||
|
y: Number(frame.packet_size),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
setData([frame]);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to extract bitrate data:", error);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [paths]);
|
||||||
|
|
||||||
const graph = <Graph data={data} />;
|
const graph = <Graph data={data} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,6 +49,11 @@ function App() {
|
|||||||
style={{ "--wails-drop-target": "drop" } as React.CSSProperties}
|
style={{ "--wails-drop-target": "drop" } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<DropOverlay paths={paths} />
|
<DropOverlay paths={paths} />
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute z-20 top-4 right-4 text-white bg-blue-600 px-4 py-2 rounded-lg">
|
||||||
|
Extracting bitrate data...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{graph}
|
{graph}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+13
-1
@@ -3,7 +3,11 @@ import type { StreamResult } from "@/bindings/StreamResult";
|
|||||||
import type { StreamDetail } from "@/bindings/StreamDetail";
|
import type { StreamDetail } from "@/bindings/StreamDetail";
|
||||||
import type { StreamResultError } from "@/bindings/StreamResultError";
|
import type { StreamResultError } from "@/bindings/StreamResultError";
|
||||||
import type { MediaType } from "@/bindings/MediaType";
|
import type { MediaType } from "@/bindings/MediaType";
|
||||||
export type { StreamResult, StreamDetail, StreamResultError, MediaType };
|
import type { File } from "@/bindings/File";
|
||||||
|
import type { FileCandidacy } from "@/bindings/FileCandidacy";
|
||||||
|
import type { BitrateData } from "@/bindings/BitrateData";
|
||||||
|
import type { BitrateFrame } from "@/bindings/BitrateFrame";
|
||||||
|
export type { StreamResult, StreamDetail, StreamResultError, MediaType, File, FileCandidacy, BitrateData, BitrateFrame };
|
||||||
|
|
||||||
// Tauri invoke wrapper
|
// Tauri invoke wrapper
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@@ -21,5 +25,13 @@ export const commands = {
|
|||||||
if (e instanceof Error) throw e;
|
if (e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async analyzeFiles(paths: string[]): Promise<File[]> {
|
||||||
|
return await invoke<File[]>("analyze_files", { paths });
|
||||||
|
},
|
||||||
|
|
||||||
|
async extractBitrateData(path: string): Promise<BitrateData> {
|
||||||
|
return await invoke<BitrateData>("extract_bitrate_data", { path });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+131
-316
@@ -1,7 +1,6 @@
|
|||||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { match, P } from "ts-pattern";
|
import { match, P } from "ts-pattern";
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
|
||||||
File as FileIcon,
|
File as FileIcon,
|
||||||
FileText,
|
FileText,
|
||||||
Film,
|
Film,
|
||||||
@@ -11,38 +10,12 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { commands } from "@/bindings";
|
import { commands } from "@/bindings";
|
||||||
import type { MediaType, StreamDetail } from "@/bindings";
|
import type { File, FileCandidacy, MediaType } from "@/bindings";
|
||||||
|
|
||||||
type DropOverlayProps = {
|
type DropOverlayProps = {
|
||||||
paths: string[];
|
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 => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return "0 B";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -51,139 +24,76 @@ const formatFileSize = (bytes: number): string => {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDuration = (seconds: number): string => {
|
const getFileIcon = (candidacy: FileCandidacy): ReactNode => {
|
||||||
const hours = Math.floor(seconds / 3600);
|
return match(candidacy)
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
.with("Loading", () => (
|
||||||
const secs = Math.floor(seconds % 60);
|
<Loader2 className="w-5 h-5 text-blue-400 animate-spin" />
|
||||||
|
))
|
||||||
if (hours > 0) {
|
.with({ Error: P._ }, () => <XCircle className="w-5 h-5 text-red-400" />)
|
||||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${secs
|
.with({ Success: { type: P.select() } }, (mediaType: MediaType) => {
|
||||||
.toString()
|
switch (mediaType) {
|
||||||
.padStart(2, "0")}`;
|
case "Audio":
|
||||||
}
|
return <Music className="w-5 h-5 text-blue-400" />;
|
||||||
return `${minutes}:${secs.toString().padStart(2, "0")}`;
|
case "Video":
|
||||||
};
|
return <Film className="w-5 h-5 text-purple-400" />;
|
||||||
|
case "Image":
|
||||||
const getFileIcon = (
|
return <Image className="w-5 h-5 text-pink-400" />;
|
||||||
mediaType: MediaType,
|
case "Document":
|
||||||
error?: string,
|
return <FileText className="w-5 h-5 text-green-400" />;
|
||||||
errorType?: string,
|
case "Executable":
|
||||||
) => {
|
return <FileIcon className="w-5 h-5 text-orange-400" />;
|
||||||
// For non-media files, show a neutral icon instead of error icon
|
case "Archive":
|
||||||
if (errorType === "not_media") {
|
return <FileIcon className="w-5 h-5 text-yellow-400" />;
|
||||||
switch (mediaType) {
|
case "Library":
|
||||||
case "Executable":
|
return <FileIcon className="w-5 h-5 text-indigo-400" />;
|
||||||
return <FileIcon className="w-5 h-5 text-orange-400" />;
|
default:
|
||||||
case "Archive":
|
return <FileIcon className="w-5 h-5 text-neutral-300" />;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
.exhaustive();
|
||||||
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 = ({
|
const getStatusColor = (candidacy: FileCandidacy): string => {
|
||||||
icon,
|
return match(candidacy)
|
||||||
text,
|
.with("Loading", () => "border-blue-500/50")
|
||||||
subtitle,
|
.with({ Error: P._ }, () => "border-red-500/50")
|
||||||
status,
|
.with({ Success: P._ }, () => "border-green-500/50")
|
||||||
}: {
|
.exhaustive();
|
||||||
icon: ReactNode;
|
};
|
||||||
text: ReactNode;
|
|
||||||
subtitle?: ReactNode;
|
const FileItem = ({ file }: { file: File }) => {
|
||||||
status?: "success" | "error" | "loading";
|
const icon = getFileIcon(file.candidacy);
|
||||||
}) => {
|
const statusColor = getStatusColor(file.candidacy);
|
||||||
const statusColor =
|
const fileSize = formatFileSize(file.size);
|
||||||
status === "success"
|
|
||||||
? "border-green-500"
|
const subtitle = match(file.candidacy)
|
||||||
: status === "error"
|
.with("Loading", () => "Analyzing...")
|
||||||
? "border-red-500"
|
.with({ Error: { reason: P.select() } }, (reason: string) => reason)
|
||||||
: status === "loading"
|
.with({ Success: { type: P.select() } }, (mediaType: MediaType) => {
|
||||||
? "border-blue-500"
|
switch (mediaType) {
|
||||||
: "border-neutral-600";
|
case "Audio":
|
||||||
|
return "Audio file";
|
||||||
|
case "Video":
|
||||||
|
return "Video file";
|
||||||
|
case "Image":
|
||||||
|
return "Image file";
|
||||||
|
case "Document":
|
||||||
|
return "Document file";
|
||||||
|
case "Executable":
|
||||||
|
return "Executable file";
|
||||||
|
case "Archive":
|
||||||
|
return "Archive file";
|
||||||
|
case "Library":
|
||||||
|
return "Library file";
|
||||||
|
default:
|
||||||
|
return "Unknown file type";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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`}
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg bg-neutral-800 border ${statusColor} transition-all duration-200`}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
marginBottom: "0.75rem",
|
marginBottom: "0.75rem",
|
||||||
@@ -191,187 +101,92 @@ const Item = ({
|
|||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="truncate text-neutral-100 font-medium">{text}</div>
|
<div className="truncate text-neutral-100 font-medium">
|
||||||
{subtitle && (
|
{file.filename}
|
||||||
<div className="truncate text-neutral-400 text-sm mt-1">
|
</div>
|
||||||
{subtitle}
|
<div className="truncate text-neutral-400 text-sm mt-1">
|
||||||
</div>
|
{fileSize} • {subtitle}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</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 DropOverlay = ({ paths }: DropOverlayProps) => {
|
||||||
const [state, setState] = useState<State>({ status: "hidden" });
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const aborterRef = useRef<AbortController | null>(null);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (paths.length === 0) {
|
if (paths.length === 0) {
|
||||||
setState({ status: "hidden" });
|
setFiles([]);
|
||||||
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({ status: "loading", count: paths.length });
|
setIsLoading(true);
|
||||||
|
setFiles([]);
|
||||||
|
|
||||||
aborterRef.current = new AbortController();
|
// Initialize with loading state for all files
|
||||||
|
const loadingFiles: File[] = paths.map((path) => {
|
||||||
commands.hasStreams(paths).then((result) => {
|
const filename = path.split(/[/\\]/).pop() || "unknown";
|
||||||
setState((_state) => {
|
return {
|
||||||
return match(result)
|
filename,
|
||||||
.with({ status: "ok" }, (r) => ({
|
size: 0,
|
||||||
status: "ready" as const,
|
candidacy: "Loading" 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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
setFiles(loadingFiles);
|
||||||
|
|
||||||
|
// Analyze files
|
||||||
|
commands
|
||||||
|
.analyzeFiles(paths)
|
||||||
|
.then((analyzedFiles) => {
|
||||||
|
setFiles(analyzedFiles);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to analyze files:", error);
|
||||||
|
// Set all files to error state
|
||||||
|
const errorFiles: File[] = paths.map((path) => {
|
||||||
|
const filename = path.split(/[/\\]/).pop() || "unknown";
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
size: 0,
|
||||||
|
candidacy: {
|
||||||
|
Error: {
|
||||||
|
reason: "Failed to analyze file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setFiles(errorFiles);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
}, [paths]);
|
}, [paths]);
|
||||||
|
|
||||||
if (state.status === "hidden") {
|
if (files.length === 0) {
|
||||||
return null;
|
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 (
|
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="absolute z-10 top-0 left-0 w-full h-full backdrop-blur-[1px] backdrop-saturate-0 transition-all duration-300 ease-in-out">
|
||||||
<div className="flex flex-col justify-center items-center h-full p-8">
|
<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">
|
<div className="rounded-xl p-6 max-w-2xl w-full">
|
||||||
{inner}
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex items-center gap-2 text-blue-400 mb-4">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
|
<span className="text-lg font-medium">
|
||||||
|
Analyzing {files.length} file{files.length > 1 ? "s" : ""}...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="max-h-96 overflow-y-auto w-full">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<FileItem key={`${file.filename}-${index}`} file={file} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ const Graph = ({ data }: GraphProps) => (
|
|||||||
fill: "#6e6a86",
|
fill: "#6e6a86",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
axisBottom={{ legend: "transportation", legendOffset: 36 }}
|
axisBottom={{ legend: "Frame Number", legendOffset: 36 }}
|
||||||
axisLeft={{
|
axisLeft={{
|
||||||
legend: "count",
|
legend: "Packet Size",
|
||||||
legendOffset: -40,
|
legendOffset: -40,
|
||||||
format: (v) => formatBytes(v * 1024 * 53),
|
format: (v) => formatBytes(v),
|
||||||
}}
|
}}
|
||||||
pointSize={10}
|
pointSize={10}
|
||||||
colors={["#3e8faf", "#c4a7e7", "#f5c276", "#EA9B96", "#EB7092", "#9CCFD8"]}
|
colors={["#3e8faf", "#c4a7e7", "#f5c276", "#EA9B96", "#EB7092", "#9CCFD8"]}
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ export function useDragDropPaths(): string[] {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlistenPromise = getCurrentWebview().onDragDropEvent(
|
const unlistenPromise = getCurrentWebview().onDragDropEvent(
|
||||||
async ({ payload }) => {
|
async ({ payload }) => {
|
||||||
if (payload.type === "enter") {
|
if (payload.type === "drop") {
|
||||||
setPaths(payload.paths);
|
setPaths(payload.paths);
|
||||||
} else if (payload.type === "leave" || payload.type === "drop") {
|
} else if (payload.type === "leave") {
|
||||||
setPaths([]);
|
setPaths([]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlistenPromise.then((unlisten) => unlisten());
|
unlistenPromise.then((unlisten) => unlisten());
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user