734 lines
21 KiB
Markdown
734 lines
21 KiB
Markdown
|
|
# 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
|
|||
|
|
```
|