v0.1.1: fix .app bundle PATH for pyenv/nvm, detect yt-dlp pot-provider plugin
- fix_path_env() now adds ~/.pyenv/shims (with PYENV_ROOT) before Homebrew paths so pyenv-managed yt-dlp (with pip-installed plugins) takes priority over Homebrew's bare yt-dlp binary - Also handles ~/.nvm, ~/.deno/bin, ~/.cargo/bin for broader coverage - check_pot_plugin uses lightweight 'yt-dlp -v' instead of network simulation for reliable plugin detection - Bump version to 0.1.1 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# Build Cleanup & Release Packaging
|
||||
|
||||
**Date:** 2026-09-22 13:39
|
||||
**Commits:** 4bf6545..1ea7f19 (5 commits)
|
||||
|
||||
## Task Description
|
||||
|
||||
Resolved all build warnings/errors across the frontend (svelte-check, Vite) and backend (cargo), fixed a failing test, and implemented a release build script that produces a distributable `GUI Video Clipper.app` bundle.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Task 1: TypeScript Error Fixes (`3eebe01`)
|
||||
- Installed `@types/node` as dev dependency
|
||||
- Added `"types": ["node"]` to `tsconfig.json`
|
||||
- Changed `defineConfig` import in `vite.config.ts` from `vite` to `vitest/config` (extends config type with `test` property)
|
||||
- Result: 4 TS errors → 0
|
||||
|
||||
### Task 2: Svelte A11y Warning Fixes (`9d1f1cf`)
|
||||
- **CaptionSettingsPanel.svelte**: 9 label/control association fixes (id/for pairs and wrapping)
|
||||
- **ClipList.svelte**: Added `tabindex="0"` and keyboard handler to listbox container
|
||||
- **PreferencesPanel.svelte, ExportDialog.svelte, AboutDialog.svelte**: Added `tabindex="-1"` to dialog divs, keyboard handlers on overlays
|
||||
- **VideoPlayer.svelte**: Added `<track kind="captions" />` to `<video>` element
|
||||
- **App.svelte**: Added keyboard support to resize handle (`role="slider"`, arrow keys, aria-value attrs)
|
||||
- Result: 19 a11y warnings → 0
|
||||
|
||||
### Task 3: Rust Dead Code Annotations (`d85fbda`)
|
||||
- Added `#[allow(dead_code)]` with comments on 6 items across 4 files:
|
||||
- `ExportEvent::Error`, `DownloadEvent::Error` (reserved for future use)
|
||||
- `sanitize_vtt_for_ffmpeg`, `opacity_to_ass_back_colour`, `caption_style_to_force_style` (caption burn-in pipeline)
|
||||
- `SubtitleFormat.ext`/`.url` (serde deserialization fields)
|
||||
- Result: 6 warnings → 0
|
||||
|
||||
### Task 4: Test Fix (`30b04b4`)
|
||||
- Changed `markInPoint(5)` to `markInPoint(12)` in clips.test.ts
|
||||
- Time 5 was outside `EDIT_TOLERANCE` (0.5s) of clip boundary (10-20), so `isTimeInsideClip` returned false and the edit path didn't execute
|
||||
- Result: 1 failing test → 31/31 pass
|
||||
|
||||
### Task 5: Release Build Script (`1ea7f19`)
|
||||
- Created `scripts/build-release.sh`: builds, ad-hoc signs, copies to `dist/release/`
|
||||
- Changed `tauri.conf.json` bundle targets from `"all"` to `["app", "dmg"]`
|
||||
- Added `dist/` to `.gitignore`
|
||||
- Added `"release"` npm script
|
||||
- Updated README with Release Build section
|
||||
- Script parses tauri build output for .app/.dmg paths (handles custom CARGO_TARGET_DIR)
|
||||
- Result: `npm run release` → `GUI Video Clipper.app` (6.2M) + `.dmg` (2.8M)
|
||||
|
||||
## Verification Results
|
||||
- `npm run check`: 0 errors, 0 warnings
|
||||
- `npm test`: 31/31 pass
|
||||
- `cargo build`: 0 warnings
|
||||
- `npm run release`: produces signed .app + .dmg
|
||||
|
||||
## Lessons Learned
|
||||
- **Cursor sandbox CARGO_TARGET_DIR**: The Tauri build outputs to a sandboxed cache directory, not `src-tauri/target/`. The build script must parse the actual output paths from `npx tauri build` output rather than hardcoding the expected location.
|
||||
- **Svelte a11y linter strictness**: `role="separator"` with keyboard support still triggers `a11y_no_noninteractive_element_interactions`. Using `role="slider"` satisfies the linter for resize handles.
|
||||
- **Dialog overlay keyboard patterns**: Adding `onkeydown` + `tabindex="-1"` to overlays and `e.stopPropagation()` to inner dialogs satisfies the click-needs-keyboard-equivalent rule, but the stopPropagation means Escape from inside the dialog won't bubble to the overlay handler. Not a regression since dialogs previously had no keyboard close.
|
||||
- **Test EDIT_TOLERANCE awareness**: When testing functions that use proximity checks (`isTimeInsideClip`), test values must account for tolerance thresholds. Values outside tolerance trigger different code paths.
|
||||
|
||||
## Follow-up Items
|
||||
- Push new commits to remote: `git push origin master`
|
||||
- Consider re-tagging `v0.1.0` to include these cleanup commits, or tag as `v0.1.1`
|
||||
- Dialog Escape-from-inside behavior could be improved in a future pass (add Escape handler on the dialog div itself, not just the overlay)
|
||||
@@ -0,0 +1,733 @@
|
||||
# Build Cleanup & Release Packaging Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Eliminate all build warnings/errors across frontend and backend, fix the failing test, and implement a release build script that produces a distributable `GUI Video Clipper.app`.
|
||||
|
||||
**Architecture:** Direct fixes to existing files — no new components or modules except `scripts/build-release.sh`. TypeScript config fixes, Svelte a11y attribute additions, Rust `#[allow]` annotations, one test assertion update, and a new shell script.
|
||||
|
||||
**Tech Stack:** Svelte 5, TypeScript, Vite/Vitest, Rust/Cargo, Tauri v2, Bash
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- macOS is the only target platform
|
||||
- All changes must result in zero warnings from `npm run check`, `cargo build`, and `npm test`
|
||||
- Preserve existing runtime behavior — these are lint/warning fixes, not behavioral changes
|
||||
- Follow existing code patterns and styles in each file
|
||||
|
||||
---
|
||||
|
||||
### Task 1: TypeScript Error Fixes
|
||||
|
||||
**Files:**
|
||||
- Modify: `tsconfig.json`
|
||||
- Modify: `vite.config.ts:1` (import line only)
|
||||
- Test: `npm run check` (svelte-check)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing
|
||||
- Produces: Clean `npm run check` (0 errors for T1–T4; a11y warnings will still be present until Task 2)
|
||||
|
||||
- [ ] **Step 1: Install `@types/node`**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
npm install -D @types/node
|
||||
```
|
||||
|
||||
Expected: Package added to `devDependencies` in `package.json`.
|
||||
|
||||
- [ ] **Step 2: Add `"types": ["node"]` to `tsconfig.json`**
|
||||
|
||||
Edit `tsconfig.json` — add `"types": ["node"]` inside `compilerOptions`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"isolatedModules": true,
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"$lib/*": ["./src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.svelte", "tests/**/*.ts", "vite.config.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Change `defineConfig` import in `vite.config.ts`**
|
||||
|
||||
Change line 4 from:
|
||||
```typescript
|
||||
import { defineConfig } from 'vite';
|
||||
```
|
||||
to:
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config';
|
||||
```
|
||||
|
||||
This re-export extends `UserConfigExport` with vitest's `test` property.
|
||||
|
||||
- [ ] **Step 4: Verify TS errors are resolved**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
npm run check 2>&1 | grep -E "^(Error|.*Error:)"
|
||||
```
|
||||
|
||||
Expected: No lines matching "Error" — only a11y warnings remain.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add tsconfig.json vite.config.ts package.json package-lock.json
|
||||
git commit -m "fix: resolve TypeScript errors in vite.config.ts
|
||||
|
||||
- Install @types/node for node:path, node:process, node:url
|
||||
- Import defineConfig from vitest/config for test config type
|
||||
- Add types: [\"node\"] to tsconfig.json"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Svelte A11y Warning Fixes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/components/CaptionSettingsPanel.svelte:48-209` (label associations)
|
||||
- Modify: `src/lib/components/ClipList.svelte:46` (tabindex + keydown)
|
||||
- Modify: `src/lib/components/PreferencesPanel.svelte:60-61` (tabindex + keydown)
|
||||
- Modify: `src/lib/components/ExportDialog.svelte:139-140` (tabindex + keydown)
|
||||
- Modify: `src/lib/components/AboutDialog.svelte:15-16` (tabindex + keydown)
|
||||
- Modify: `src/lib/components/VideoPlayer.svelte:187-197` (track element)
|
||||
- Modify: `src/App.svelte:188-193` (resize handle keyboard support)
|
||||
- Test: `npm run check` (svelte-check)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 completed (TS errors resolved)
|
||||
- Produces: Zero a11y warnings from `npm run check` and `npm run build`
|
||||
|
||||
- [ ] **Step 1: Fix CaptionSettingsPanel labels (9 warnings)**
|
||||
|
||||
The labels at lines 48, 61, 74, 90, 108, 120, 165, 195, and 209 are not associated with their controls. Fix each by adding `id` attributes to controls and `for` attributes to labels. Use unique IDs prefixed with `caption-`.
|
||||
|
||||
**Line 48 — Font label + select:** Change:
|
||||
```svelte
|
||||
<label>Font</label>
|
||||
<select
|
||||
value={settings.fontFamily}
|
||||
```
|
||||
to:
|
||||
```svelte
|
||||
<label for="caption-font">Font</label>
|
||||
<select
|
||||
id="caption-font"
|
||||
value={settings.fontFamily}
|
||||
```
|
||||
|
||||
**Line 61 — Font Size label + range input:** The label wraps the text but not the input. Wrap the input inside the label:
|
||||
```svelte
|
||||
<label>
|
||||
Font Size
|
||||
<span class="value">{settings.fontSize}px</span>
|
||||
<input
|
||||
type="range" min="12" max="36" step="1"
|
||||
value={settings.fontSize}
|
||||
oninput={(e) => update({ fontSize: parseInt((e.target as HTMLInputElement).value, 10) })}
|
||||
/>
|
||||
</label>
|
||||
```
|
||||
|
||||
Remove the standalone `<input>` that was a sibling below the closing `</label>`.
|
||||
|
||||
**Line 74 — Text Color label:** Change:
|
||||
```svelte
|
||||
<label>Text Color</label>
|
||||
<div class="row-controls">
|
||||
<input
|
||||
type="color" value={settings.textColor}
|
||||
```
|
||||
to:
|
||||
```svelte
|
||||
<label for="caption-text-color">Text Color</label>
|
||||
<div class="row-controls">
|
||||
<input
|
||||
id="caption-text-color"
|
||||
type="color" value={settings.textColor}
|
||||
```
|
||||
|
||||
**Line 90 — Dimmed Text label:** This is a heading-style label for a radio group. Use `id`/`for` pointing to the first radio:
|
||||
```svelte
|
||||
<label for="caption-dimmed-auto">Dimmed Text</label>
|
||||
<div class="radio-row">
|
||||
<label>
|
||||
<input id="caption-dimmed-auto" type="radio" name="dimmed-mode" value="auto"
|
||||
```
|
||||
|
||||
**Line 108 — Dim Opacity label + range:** Wrap the input inside the label (same pattern as Font Size):
|
||||
```svelte
|
||||
<label>
|
||||
Dim Opacity
|
||||
<span class="value">{Math.round(settings.dimmedOpacity * 100)}%</span>
|
||||
<input
|
||||
type="range" min="10" max="90" step="5"
|
||||
value={Math.round(settings.dimmedOpacity * 100)}
|
||||
oninput={(e) => update({ dimmedOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
|
||||
/>
|
||||
</label>
|
||||
```
|
||||
|
||||
Remove the standalone `<input>` below.
|
||||
|
||||
**Line 120 — Dimmed Color label:** Change:
|
||||
```svelte
|
||||
<label>Dimmed Color</label>
|
||||
<div class="row-controls">
|
||||
<input
|
||||
type="color" value={settings.dimmedColor}
|
||||
```
|
||||
to:
|
||||
```svelte
|
||||
<label for="caption-dimmed-color">Dimmed Color</label>
|
||||
<div class="row-controls">
|
||||
<input
|
||||
id="caption-dimmed-color"
|
||||
type="color" value={settings.dimmedColor}
|
||||
```
|
||||
|
||||
**Line 165 — BG Opacity label + range:** Wrap input inside label (same as Font Size/Dim Opacity):
|
||||
```svelte
|
||||
<label>
|
||||
BG Opacity
|
||||
<span class="value">{Math.round(settings.backgroundOpacity * 100)}%</span>
|
||||
<input
|
||||
type="range" min="0" max="100" step="5"
|
||||
value={Math.round(settings.backgroundOpacity * 100)}
|
||||
oninput={(e) => update({ backgroundOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
|
||||
/>
|
||||
</label>
|
||||
```
|
||||
|
||||
Remove the standalone `<input>` below.
|
||||
|
||||
**Line 195 — Shadow Depth label + range:** Wrap input inside label:
|
||||
```svelte
|
||||
<label>
|
||||
Shadow Depth
|
||||
<span class="value">{settings.shadowDepth}px</span>
|
||||
<input
|
||||
type="range" min="1" max="5" step="1"
|
||||
value={settings.shadowDepth}
|
||||
oninput={(e) => update({ shadowDepth: parseInt((e.target as HTMLInputElement).value, 10) })}
|
||||
/>
|
||||
</label>
|
||||
```
|
||||
|
||||
Remove the standalone `<input>` below.
|
||||
|
||||
**Line 209 — Position label:** Same as Dimmed Text — heading for radio group:
|
||||
```svelte
|
||||
<label for="caption-pos-bottom">Position</label>
|
||||
<div class="radio-row">
|
||||
<label>
|
||||
<input id="caption-pos-bottom" type="radio" name="caption-position" value="bottom"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Fix ClipList (2 warnings: tabindex + key handler)**
|
||||
|
||||
In `ClipList.svelte` line 46, the `<div class="clip-list" role="listbox">` needs `tabindex="0"` and a `onkeydown` handler. Change:
|
||||
|
||||
```svelte
|
||||
<div class="clip-list" role="listbox" onclick={handleContainerClick}>
|
||||
```
|
||||
to:
|
||||
```svelte
|
||||
<div
|
||||
class="clip-list"
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
onclick={handleContainerClick}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
selectClip(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Fix PreferencesPanel (2 warnings: tabindex + key handler)**
|
||||
|
||||
In `PreferencesPanel.svelte`, change lines 60-61:
|
||||
|
||||
```svelte
|
||||
<div class="overlay" role="presentation" onclick={onClose}>
|
||||
<div class="panel" role="dialog" onclick={(e) => e.stopPropagation()}>
|
||||
```
|
||||
to:
|
||||
```svelte
|
||||
<div class="overlay" role="presentation" onclick={onClose} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
||||
<div class="panel" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()}>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Fix ExportDialog (2 warnings: tabindex + key handler)**
|
||||
|
||||
In `ExportDialog.svelte`, change lines 139-140:
|
||||
|
||||
```svelte
|
||||
<div class="overlay" role="presentation" onclick={onClose}>
|
||||
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
|
||||
```
|
||||
to:
|
||||
```svelte
|
||||
<div class="overlay" role="presentation" onclick={onClose} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
||||
<div class="dialog" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()}>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Fix AboutDialog (2 warnings: tabindex + key handler)**
|
||||
|
||||
In `AboutDialog.svelte`, change lines 15-16:
|
||||
|
||||
```svelte
|
||||
<div class="overlay" role="presentation" onclick={onClose}>
|
||||
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
|
||||
```
|
||||
to:
|
||||
```svelte
|
||||
<div class="overlay" role="presentation" onclick={onClose} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
||||
<div class="dialog" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()}>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Fix VideoPlayer (1 warning: video without track)**
|
||||
|
||||
In `VideoPlayer.svelte`, add a `<track>` inside the `<video>` element after the `playsinline` attribute (around line 197):
|
||||
|
||||
```svelte
|
||||
<video
|
||||
bind:this={videoElement}
|
||||
src={videoSrc}
|
||||
ontimeupdate={handleTimeUpdate}
|
||||
onplay={handlePlay}
|
||||
onpause={handlePause}
|
||||
onerror={handleError}
|
||||
onloadeddata={handleLoadedData}
|
||||
preload="metadata"
|
||||
playsinline
|
||||
>
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
```
|
||||
|
||||
Note: change the self-closing `></video>` to wrap the `<track>` element.
|
||||
|
||||
- [ ] **Step 7: Fix App.svelte resize handle (1 warning: non-interactive element with mouse handler)**
|
||||
|
||||
In `App.svelte`, update the resize handle div (lines 188-193) to add keyboard support:
|
||||
|
||||
```svelte
|
||||
<div
|
||||
class="resize-handle"
|
||||
class:active={isResizing}
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuenow={timelineHeight}
|
||||
aria-valuemin={40}
|
||||
aria-valuemax={500}
|
||||
tabindex="0"
|
||||
onmousedown={handleResizeStart}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
timelineHeight = Math.min(500, timelineHeight + 10);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
timelineHeight = Math.max(40, timelineHeight - 10);
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Verify all a11y warnings are resolved**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
npm run check 2>&1
|
||||
```
|
||||
|
||||
Expected: `svelte-check found 0 errors and 0 warnings`
|
||||
|
||||
Also verify the production build:
|
||||
```bash
|
||||
npm run build 2>&1 | grep -c "svelte"
|
||||
```
|
||||
|
||||
Expected: No a11y warnings in the Vite build output (only the normal build success lines).
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/components/CaptionSettingsPanel.svelte \
|
||||
src/lib/components/ClipList.svelte \
|
||||
src/lib/components/PreferencesPanel.svelte \
|
||||
src/lib/components/ExportDialog.svelte \
|
||||
src/lib/components/AboutDialog.svelte \
|
||||
src/lib/components/VideoPlayer.svelte \
|
||||
src/App.svelte
|
||||
git commit -m "fix: resolve all Svelte a11y warnings
|
||||
|
||||
- Associate labels with controls in CaptionSettingsPanel (9 warnings)
|
||||
- Add tabindex and keyboard handlers to dialog overlays (8 warnings)
|
||||
- Add <track> to video element for caption accessibility
|
||||
- Add keyboard support to resize handle separator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Rust Dead Code Annotations
|
||||
|
||||
**Files:**
|
||||
- Modify: `src-tauri/src/commands/export.rs:23`
|
||||
- Modify: `src-tauri/src/commands/video.rs:23`
|
||||
- Modify: `src-tauri/src/services/clip_exporter.rs:10,204,210`
|
||||
- Modify: `src-tauri/src/services/video_resolver.rs:7`
|
||||
- Test: `cargo build`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing
|
||||
- Produces: Zero warnings from `cargo build`
|
||||
|
||||
- [ ] **Step 1: Annotate `ExportEvent::Error` in `commands/export.rs`**
|
||||
|
||||
Add `#[allow(dead_code)]` on the `Error` variant (line 23):
|
||||
|
||||
```rust
|
||||
Finished {
|
||||
paths: Vec<String>,
|
||||
},
|
||||
#[allow(dead_code)] // Reserved for future error-channel reporting
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Annotate `DownloadEvent::Error` in `commands/video.rs`**
|
||||
|
||||
Add `#[allow(dead_code)]` on the `Error` variant (line 23):
|
||||
|
||||
```rust
|
||||
Finished { success: bool, path: String },
|
||||
#[allow(dead_code)] // Reserved for future error-channel reporting
|
||||
Error { message: String },
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Annotate unused functions in `services/clip_exporter.rs`**
|
||||
|
||||
Add `#[allow(dead_code)]` on each unused function:
|
||||
|
||||
Line 10:
|
||||
```rust
|
||||
/// Strip YouTube auto-generated VTT karaoke tags and positioning metadata
|
||||
/// that confuse ffmpeg's VTT parser / mov_text conversion.
|
||||
/// Returns the path to a cleaned temp VTT file.
|
||||
#[allow(dead_code)] // Scaffolded for caption burn-in pipeline
|
||||
pub fn sanitize_vtt_for_ffmpeg(caption_path: &str, temp_dir: &str) -> Result<String, String> {
|
||||
```
|
||||
|
||||
Line 204:
|
||||
```rust
|
||||
#[allow(dead_code)] // Scaffolded for caption burn-in pipeline
|
||||
fn opacity_to_ass_back_colour(opacity: f64) -> String {
|
||||
```
|
||||
|
||||
Line 210:
|
||||
```rust
|
||||
#[allow(dead_code)] // Scaffolded for caption burn-in pipeline
|
||||
fn caption_style_to_force_style(style: &CaptionStyle) -> String {
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Annotate `SubtitleFormat` in `services/video_resolver.rs`**
|
||||
|
||||
Add `#[allow(dead_code)]` on the struct (line 7):
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)] // Fields used for serde deserialization
|
||||
pub struct SubtitleFormat {
|
||||
pub ext: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify zero Rust warnings**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd src-tauri && cargo build 2>&1 | grep "warning:"
|
||||
```
|
||||
|
||||
Expected: No output (zero warnings).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src-tauri/src/commands/export.rs \
|
||||
src-tauri/src/commands/video.rs \
|
||||
src-tauri/src/services/clip_exporter.rs \
|
||||
src-tauri/src/services/video_resolver.rs
|
||||
git commit -m "fix: suppress Rust dead code warnings with annotations
|
||||
|
||||
- ExportEvent::Error and DownloadEvent::Error reserved for future use
|
||||
- clip_exporter functions scaffolded for caption burn-in pipeline
|
||||
- SubtitleFormat fields required for serde deserialization"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Test Fix
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/lib/stores/clips.test.ts:60-68`
|
||||
- Test: `npm test`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing (test-only change)
|
||||
- Produces: 31/31 tests passing
|
||||
|
||||
- [ ] **Step 1: Update the failing test assertion**
|
||||
|
||||
In `tests/lib/stores/clips.test.ts`, change the test "updates selected clip start when I is pressed" (lines 60-68):
|
||||
|
||||
```typescript
|
||||
it('updates selected clip start when I is pressed', () => {
|
||||
addClip(10, 20);
|
||||
const id = getClips()[0].id;
|
||||
selectClip(id);
|
||||
|
||||
markInPoint(12);
|
||||
expect(getClips()[0].startTime).toBe(12);
|
||||
expect(getPendingInPoint()).toBeNull();
|
||||
});
|
||||
```
|
||||
|
||||
The change: `markInPoint(5)` → `markInPoint(12)` and `toBe(5)` → `toBe(12)`.
|
||||
|
||||
Time 12 is inside the clip (10–20) so `isTimeInsideClip` returns true and the edit path runs. Time 5 was outside the 0.5s tolerance (needed ≥ 9.5).
|
||||
|
||||
- [ ] **Step 2: Verify all tests pass**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Expected:
|
||||
```
|
||||
Test Files 5 passed (5)
|
||||
Tests 31 passed (31)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/lib/stores/clips.test.ts
|
||||
git commit -m "fix: correct clip in-point edit test for EDIT_TOLERANCE
|
||||
|
||||
markInPoint(5) is outside the clip (10-20) with 0.5s tolerance.
|
||||
Use markInPoint(12) which is inside the clip and triggers the edit path."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Release Build Script & Packaging
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/build-release.sh`
|
||||
- Modify: `src-tauri/tauri.conf.json:38` (bundle targets)
|
||||
- Modify: `.gitignore` (add `dist/`)
|
||||
- Modify: `package.json` (add `release` script)
|
||||
- Modify: `README.md` (add Release Build section)
|
||||
- Test: `npm run release`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tasks 1–4 completed (clean builds)
|
||||
- Produces: `dist/release/GUI Video Clipper.app` — a runnable, ad-hoc signed macOS app bundle
|
||||
|
||||
- [ ] **Step 1: Change bundle targets in `tauri.conf.json`**
|
||||
|
||||
In `src-tauri/tauri.conf.json`, change line 38 from:
|
||||
|
||||
```json
|
||||
"targets": "all",
|
||||
```
|
||||
to:
|
||||
```json
|
||||
"targets": ["app", "dmg"],
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `dist/` to `.gitignore`**
|
||||
|
||||
Append to `.gitignore`:
|
||||
|
||||
```
|
||||
dist/
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add `release` script to `package.json`**
|
||||
|
||||
Add to the `"scripts"` section of `package.json`:
|
||||
|
||||
```json
|
||||
"release": "bash scripts/build-release.sh"
|
||||
```
|
||||
|
||||
Place it after the existing `"tauri"` script.
|
||||
|
||||
- [ ] **Step 4: Create `scripts/build-release.sh`**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
VERSION_FILE="$PROJECT_ROOT/VERSION"
|
||||
|
||||
# ── Read and validate version ──────────────────────────────────────
|
||||
if [ ! -f "$VERSION_FILE" ]; then
|
||||
echo "ERROR: VERSION file not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(cat "$VERSION_FILE" | sed 's/^[vV]//' | tr -d '[:space:]')
|
||||
|
||||
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "ERROR: '$VERSION' is not valid semver (expected X.Y.Z)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Building GUI Video Clipper v$VERSION ==="
|
||||
echo ""
|
||||
|
||||
# ── Install frontend dependencies ──────────────────────────────────
|
||||
echo "→ Installing dependencies…"
|
||||
cd "$PROJECT_ROOT"
|
||||
npm install --silent
|
||||
|
||||
# ── Run tauri build ────────────────────────────────────────────────
|
||||
echo "→ Building release (this may take a few minutes)…"
|
||||
npx tauri build 2>&1 | tail -5
|
||||
|
||||
# ── Locate built artifacts ─────────────────────────────────────────
|
||||
# Tauri outputs to src-tauri/target/release/bundle/
|
||||
BUNDLE_DIR="$PROJECT_ROOT/src-tauri/target/release/bundle"
|
||||
APP_SRC="$BUNDLE_DIR/macos/GUI Video Clipper.app"
|
||||
DMG_GLOB="$BUNDLE_DIR/dmg/GUI Video Clipper_*.dmg"
|
||||
|
||||
if [ ! -d "$APP_SRC" ]; then
|
||||
echo "ERROR: .app bundle not found at '$APP_SRC'" >&2
|
||||
echo "Check the build output above for errors." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Ad-hoc code sign ──────────────────────────────────────────────
|
||||
echo "→ Ad-hoc signing .app…"
|
||||
codesign --force --deep -s - "$APP_SRC"
|
||||
|
||||
# ── Copy to dist/release/ ─────────────────────────────────────────
|
||||
DIST_DIR="$PROJECT_ROOT/dist/release"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
echo "→ Copying to dist/release/…"
|
||||
rm -rf "$DIST_DIR/GUI Video Clipper.app"
|
||||
cp -R "$APP_SRC" "$DIST_DIR/"
|
||||
|
||||
# Copy DMG if it was built
|
||||
DMG_FILE=$(ls $DMG_GLOB 2>/dev/null | head -1 || true)
|
||||
if [ -n "$DMG_FILE" ]; then
|
||||
cp "$DMG_FILE" "$DIST_DIR/"
|
||||
echo " ✓ DMG: $(basename "$DMG_FILE")"
|
||||
fi
|
||||
|
||||
# ── Summary ────────────────────────────────────────────────────────
|
||||
APP_SIZE=$(du -sh "$DIST_DIR/GUI Video Clipper.app" | cut -f1)
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo " Version: v$VERSION"
|
||||
echo " App: dist/release/GUI Video Clipper.app ($APP_SIZE)"
|
||||
if [ -n "$DMG_FILE" ]; then
|
||||
DMG_SIZE=$(du -sh "$DIST_DIR/$(basename "$DMG_FILE")" | cut -f1)
|
||||
echo " DMG: dist/release/$(basename "$DMG_FILE") ($DMG_SIZE)"
|
||||
fi
|
||||
echo ""
|
||||
echo "To run: open \"dist/release/GUI Video Clipper.app\""
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
```bash
|
||||
chmod +x scripts/build-release.sh
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update README with Release Build section**
|
||||
|
||||
Add a new section after "## Build & Run" in `README.md`:
|
||||
|
||||
```markdown
|
||||
## Release Build
|
||||
|
||||
```bash
|
||||
npm run release
|
||||
```
|
||||
|
||||
This runs the full Tauri production build, ad-hoc code signs the `.app` bundle, and copies the output to `dist/release/`.
|
||||
|
||||
The resulting `GUI Video Clipper.app` can be launched directly or dragged to `/Applications`.
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Verify the release build**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
npm run release
|
||||
```
|
||||
|
||||
Expected:
|
||||
- Script prints version, build progress, and summary
|
||||
- `dist/release/GUI Video Clipper.app` exists
|
||||
- App launches: `open "dist/release/GUI Video Clipper.app"`
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/build-release.sh \
|
||||
src-tauri/tauri.conf.json \
|
||||
.gitignore \
|
||||
package.json \
|
||||
README.md
|
||||
git commit -m "feat: add release build script
|
||||
|
||||
- scripts/build-release.sh: builds, signs, and copies .app to dist/release/
|
||||
- Bundle targets narrowed to app + dmg
|
||||
- npm run release convenience script
|
||||
- README documents the release build workflow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final Verification
|
||||
|
||||
After all tasks, run the full check suite:
|
||||
|
||||
```bash
|
||||
npm run check # 0 errors, 0 warnings
|
||||
npm test # 31/31 pass
|
||||
cd src-tauri && cargo build 2>&1 | grep "warning:" # no output
|
||||
npm run release # produces dist/release/GUI Video Clipper.app
|
||||
```
|
||||
@@ -0,0 +1,112 @@
|
||||
# Build Cleanup & Release Packaging
|
||||
|
||||
**Date:** 2026-09-22
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Resolve all build warnings and errors across the frontend (svelte-check, Vite) and backend (cargo), fix the failing test, and implement a release build script that produces a distributable `GUI Video Clipper.app` bundle.
|
||||
|
||||
## Scope
|
||||
|
||||
Two workstreams:
|
||||
|
||||
1. **Warning/Error Cleanup** — zero warnings from `npm run check`, `cargo build`, and `npm test`
|
||||
2. **Release Build Packaging** — `scripts/build-release.sh` that produces a signed `.app` in `dist/release/`
|
||||
|
||||
## 1. TypeScript Errors (4)
|
||||
|
||||
All in `vite.config.ts`:
|
||||
|
||||
| Error | Root Cause | Fix |
|
||||
|-------|-----------|-----|
|
||||
| Cannot find `node:path` | Missing `@types/node` | `npm install -D @types/node`, add `"types": ["node"]` to `tsconfig.json` |
|
||||
| Cannot find `node:process` | Same | Same |
|
||||
| Cannot find `node:url` | Same | Same |
|
||||
| `test` not in `UserConfigExport` | `defineConfig` from `vite` doesn't know vitest's `test` field | Import `defineConfig` from `vitest/config` instead |
|
||||
|
||||
## 2. Svelte A11y Warnings (19)
|
||||
|
||||
### 2a. Labels not associated with controls (9 in CaptionSettingsPanel.svelte)
|
||||
|
||||
Labels are siblings of their controls rather than wrapping them. Fix by either:
|
||||
- Wrapping the `<input>`/`<select>` inside the `<label>` element, or
|
||||
- Adding `id` to the control and `for` to the label
|
||||
|
||||
Prefer the wrapping approach where layout allows.
|
||||
|
||||
### 2b. Dialog divs missing tabindex (4 files)
|
||||
|
||||
Add `tabindex="-1"` to `<div role="dialog">` in `PreferencesPanel`, `ExportDialog`, `AboutDialog`, and `<div role="listbox">` in `ClipList`. Makes them programmatically focusable without entering the tab order.
|
||||
|
||||
### 2c. Click handlers without key handlers (4 files)
|
||||
|
||||
Dialog overlay `onclick={onClose}` divs need corresponding `onkeydown` handlers. Add Escape key handling on the overlay. For `ClipList`'s listbox container, add an `onkeydown` handler mirroring the container click logic.
|
||||
|
||||
### 2d. `<video>` without `<track>` (1 in VideoPlayer.svelte)
|
||||
|
||||
Add an empty `<track kind="captions" default />` to the `<video>` element. Our custom caption overlay handles actual rendering.
|
||||
|
||||
### 2e. Resize handle (1 in App.svelte)
|
||||
|
||||
The resize handle `<div>` with `role="separator"` and `onmousedown` needs:
|
||||
- `tabindex="0"` for keyboard focusability
|
||||
- `onkeydown` handler for arrow key resizing
|
||||
- `aria-valuenow`, `aria-valuemin`, `aria-valuemax` attributes
|
||||
|
||||
## 3. Rust Dead Code Warnings (6)
|
||||
|
||||
Add `#[allow(dead_code)]` with explanatory comments:
|
||||
|
||||
| Warning | Location | Annotation Rationale |
|
||||
|---------|----------|---------------------|
|
||||
| `ExportEvent::Error` unused | `commands/export.rs:23` | Reserved for future error-channel reporting |
|
||||
| `DownloadEvent::Error` unused | `commands/video.rs:23` | Reserved for future error-channel reporting |
|
||||
| `sanitize_vtt_for_ffmpeg` unused | `clip_exporter.rs:10` | Scaffolded for caption burn-in pipeline |
|
||||
| `opacity_to_ass_back_colour` unused | `clip_exporter.rs:204` | Scaffolded for caption burn-in pipeline |
|
||||
| `caption_style_to_force_style` unused | `clip_exporter.rs:210` | Scaffolded for caption burn-in pipeline |
|
||||
| `SubtitleFormat.ext`/`.url` unread | `video_resolver.rs:8-9` | Fields required for serde deserialization |
|
||||
|
||||
## 4. Test Fix
|
||||
|
||||
**File:** `tests/lib/stores/clips.test.ts`
|
||||
**Test:** "updates selected clip start when I is pressed"
|
||||
|
||||
The test calls `markInPoint(5)` on a clip spanning 10–20. With `EDIT_TOLERANCE` of 0.5s, time 5 is outside the clip boundary (needs ≥ 9.5). The test was written before the tolerance check existed.
|
||||
|
||||
**Fix:** Change `markInPoint(5)` to `markInPoint(12)` (inside the clip, within tolerance). Update the assertion to `expect(getClips()[0].startTime).toBe(12)`. This verifies the edit actually changes the start time (unlike `markInPoint(10)` which would pass trivially since start is already 10).
|
||||
|
||||
After fix: 31/31 tests pass.
|
||||
|
||||
## 5. Release Build Script
|
||||
|
||||
### 5a. `scripts/build-release.sh`
|
||||
|
||||
A shell script that:
|
||||
|
||||
1. Reads `VERSION` from the project root
|
||||
2. Runs `npm install`
|
||||
3. Runs `npx tauri build` (builds frontend + Rust + bundles .app and .dmg)
|
||||
4. Ad-hoc code signs the `.app`: `codesign --force --deep -s - <path>`
|
||||
5. Copies `.app` (and `.dmg` if built) to `dist/release/`
|
||||
6. Prints summary: version, output path, bundle size
|
||||
|
||||
### 5b. Configuration changes
|
||||
|
||||
- `tauri.conf.json`: Change `"targets": "all"` to `"targets": ["app", "dmg"]`
|
||||
- `.gitignore`: Add `dist/`
|
||||
- `package.json`: Add `"release": "bash scripts/build-release.sh"`
|
||||
|
||||
### 5c. README update
|
||||
|
||||
Add a "Release Build" section documenting `npm run release` and its outputs.
|
||||
|
||||
## Verification
|
||||
|
||||
After all changes:
|
||||
|
||||
- `npm run check` exits 0 with 0 errors, 0 warnings
|
||||
- `cargo build 2>&1 | grep warning` returns nothing
|
||||
- `npm test` shows 31/31 pass
|
||||
- `npm run release` produces `dist/release/GUI Video Clipper.app`
|
||||
- The `.app` launches without the Gatekeeper right-click workaround
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gui-video-clipper",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "macOS GUI video clipper (Tauri + Svelte)",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -1281,7 +1281,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gui-video-clipper"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"dirs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gui-video-clipper"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "A macOS GUI app for clipping online videos"
|
||||
authors = ["cottongin"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -2,11 +2,89 @@ mod commands;
|
||||
mod models;
|
||||
mod services;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use commands::{dependencies, export, media_analysis, video};
|
||||
use tauri::Manager;
|
||||
|
||||
struct MediaServerPort(u16);
|
||||
|
||||
/// Prepend well-known macOS tool directories to PATH so that Homebrew-installed
|
||||
/// binaries (ffmpeg, yt-dlp, etc.) are discoverable when the app is launched as
|
||||
/// a standalone .app bundle (which inherits only a minimal system PATH).
|
||||
///
|
||||
/// Also sets PYENV_ROOT when pyenv is present so that pyenv shims can resolve
|
||||
/// to the correct Python (and its pip-installed yt-dlp plugins).
|
||||
fn fix_path_env() {
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
|
||||
let mut extra_dirs: Vec<PathBuf> = Vec::new();
|
||||
|
||||
if !home.is_empty() {
|
||||
let home_path = PathBuf::from(&home);
|
||||
|
||||
// pyenv shims — must come before /opt/homebrew/bin so the pyenv-managed
|
||||
// yt-dlp (which knows about pip-installed plugins) wins over Homebrew's.
|
||||
let pyenv_root = home_path.join(".pyenv");
|
||||
if pyenv_root.is_dir() {
|
||||
extra_dirs.push(pyenv_root.join("shims"));
|
||||
if std::env::var("PYENV_ROOT").is_err() {
|
||||
std::env::set_var("PYENV_ROOT", &pyenv_root);
|
||||
}
|
||||
}
|
||||
|
||||
// nvm — node/deno installed via nvm live under a versioned directory
|
||||
let nvm_dir = home_path.join(".nvm/versions/node");
|
||||
if nvm_dir.is_dir() {
|
||||
if let Ok(entries) = std::fs::read_dir(&nvm_dir) {
|
||||
// Pick the lexicographically last (highest) version
|
||||
let mut versions: Vec<PathBuf> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path().join("bin"))
|
||||
.filter(|p| p.is_dir())
|
||||
.collect();
|
||||
versions.sort();
|
||||
if let Some(latest) = versions.pop() {
|
||||
extra_dirs.push(latest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User-local installs
|
||||
for rel in [".local/bin", ".cargo/bin", ".deno/bin"] {
|
||||
extra_dirs.push(home_path.join(rel));
|
||||
}
|
||||
}
|
||||
|
||||
// System-wide Homebrew / MacPorts paths
|
||||
for dir in [
|
||||
"/opt/homebrew/bin",
|
||||
"/opt/homebrew/sbin",
|
||||
"/usr/local/bin",
|
||||
"/usr/local/sbin",
|
||||
] {
|
||||
extra_dirs.push(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
// Filter to directories that actually exist
|
||||
let extra_dirs: Vec<PathBuf> = extra_dirs.into_iter().filter(|p| p.is_dir()).collect();
|
||||
|
||||
let current = std::env::var("PATH").unwrap_or_default();
|
||||
let current_dirs: HashSet<&str> = current.split(':').collect();
|
||||
|
||||
let mut new_path = String::new();
|
||||
for dir in &extra_dirs {
|
||||
let s = dir.to_string_lossy();
|
||||
if !current_dirs.contains(s.as_ref()) {
|
||||
new_path.push_str(&s);
|
||||
new_path.push(':');
|
||||
}
|
||||
}
|
||||
new_path.push_str(¤t);
|
||||
std::env::set_var("PATH", &new_path);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_media_server_port(state: tauri::State<'_, MediaServerPort>) -> u16 {
|
||||
state.0
|
||||
@@ -20,6 +98,8 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.setup(|app| {
|
||||
fix_path_env();
|
||||
|
||||
// Bind synchronously (no async needed), then spawn on Tauri's runtime
|
||||
let (port, listener) = services::media_server::bind_media_server()
|
||||
.expect("Failed to bind media server");
|
||||
|
||||
@@ -54,17 +54,11 @@ pub fn check_js_runtime() -> Option<String> {
|
||||
check_tool_exists("deno").or_else(|| check_tool_exists("node"))
|
||||
}
|
||||
|
||||
pub fn check_pot_plugin(cookie_source: &CookieSource) -> bool {
|
||||
let mut command = Command::new("yt-dlp");
|
||||
command
|
||||
.arg("-v")
|
||||
.arg("--simulate")
|
||||
.arg("--remote-components")
|
||||
.arg("ejs:github")
|
||||
.args(cookie_source.to_ytdlp_args())
|
||||
.arg("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
||||
let output = command.output();
|
||||
|
||||
pub fn check_pot_plugin(_cookie_source: &CookieSource) -> bool {
|
||||
// Use `yt-dlp -v` (no URL) which lists loaded plugins without network access.
|
||||
// The old approach ran a full YouTube simulation (~9s, network-dependent) which
|
||||
// failed silently in the .app bundle context.
|
||||
let output = Command::new("yt-dlp").arg("-v").output();
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "GUI Video Clipper",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"identifier": "xyz.cottongin.gui-video-clipper",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -34,7 +34,10 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["app", "dmg"],
|
||||
"targets": [
|
||||
"app",
|
||||
"dmg"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
Reference in New Issue
Block a user