Files
gui-video-clipper/docs/superpowers/plans/2026-09-22-v0.1-release-polish.md

764 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v0.1.0 Release Polish 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:** Prepare GUI Video Clipper for its initial tagged v0.1.0 release — versioning, branding, About dialog, LICENSE, README, history scrub, and push to remote.
**Architecture:** All changes are config/metadata/UI polish — no backend feature work. A `VERSION` file is the single source of truth, synced to manifests via a bump script. An `AboutDialog.svelte` component exposes version/author/license info. `git-filter-repo` scrubs leftover personal paths from history before the first push.
**Tech Stack:** Svelte 5, Tauri v2, Rust, shell scripting, git-filter-repo
## Global Constraints
- Official title: "GUI Video Clipper"
- App identifier: `xyz.cottongin.gui-video-clipper`
- Author: cottongin
- License: MIT, copyright 2026
- Remote: `git@code.cottongin.xyz:cottongin/gui-video-clipper.git`
- Version: `0.1.0`
- No new npm or crate dependencies
- The string `REDACTED_USERNAME` must not appear anywhere in the final repo (files or git history)
---
### Task 1: VERSION file and bump script
**Files:**
- Create: `VERSION`
- Create: `scripts/bump-version.sh`
**Interfaces:**
- Consumes: Nothing
- Produces: `VERSION` file containing `0.1.0`. `scripts/bump-version.sh` that reads `VERSION` (or accepts a version argument) and patches `package.json`, `src-tauri/Cargo.toml`, and `src-tauri/tauri.conf.json`.
- [ ] **Step 1: Create the VERSION file**
Create `VERSION` at the project root with exactly this content (no trailing newline):
```
0.1.0
```
- [ ] **Step 2: Create the bump script**
Create `scripts/bump-version.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"
if [ $# -ge 1 ]; then
echo "$1" > "$VERSION_FILE"
fi
if [ ! -f "$VERSION_FILE" ]; then
echo "ERROR: VERSION file not found at $VERSION_FILE" >&2
exit 1
fi
VERSION=$(cat "$VERSION_FILE")
# Sanitize: strip leading v/V, trim whitespace
VERSION=$(echo "$VERSION" | sed 's/^[vV]//' | tr -d '[:space:]')
# Validate semver (X.Y.Z)
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 "Bumping to version $VERSION"
# 1. package.json
cd "$PROJECT_ROOT"
npm pkg set "version=$VERSION" --json 2>/dev/null || npm pkg set "version=$VERSION"
echo " ✓ package.json"
# 2. src-tauri/Cargo.toml — update [package] version (first version = line)
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" "$PROJECT_ROOT/src-tauri/Cargo.toml"
echo " ✓ src-tauri/Cargo.toml"
# 3. src-tauri/tauri.conf.json
node -e "
const fs = require('fs');
const path = '$PROJECT_ROOT/src-tauri/tauri.conf.json';
const conf = JSON.parse(fs.readFileSync(path, 'utf8'));
conf.version = '$VERSION';
fs.writeFileSync(path, JSON.stringify(conf, null, 2) + '\n');
"
echo " ✓ src-tauri/tauri.conf.json"
# 4. Sync Cargo.lock
cd "$PROJECT_ROOT/src-tauri"
cargo generate-lockfile 2>/dev/null
echo " ✓ src-tauri/Cargo.lock"
echo ""
echo "Done — all manifests set to $VERSION"
```
- [ ] **Step 3: Make the script executable**
Run: `chmod +x scripts/bump-version.sh`
- [ ] **Step 4: Test the bump script**
Run: `./scripts/bump-version.sh`
Expected output:
```
Bumping to version 0.1.0
✓ package.json
✓ src-tauri/Cargo.toml
✓ src-tauri/tauri.conf.json
✓ src-tauri/Cargo.lock
Done — all manifests set to 0.1.0
```
Verify the files were updated:
Run: `grep '"version"' package.json src-tauri/tauri.conf.json && grep '^version' src-tauri/Cargo.toml`
Expected: all three show `0.1.0`.
- [ ] **Step 5: Test the sanitization (leading v, trailing newline)**
Run: `echo "v0.1.0 " > VERSION && ./scripts/bump-version.sh`
Expected: script prints `Bumping to version 0.1.0` (strips the `v` and whitespace) and completes successfully.
Reset: `echo -n "0.1.0" > VERSION`
- [ ] **Step 6: Commit**
```bash
git add VERSION scripts/bump-version.sh
git commit -m "chore: add VERSION file and bump-version.sh script"
```
---
### Task 2: Metadata cleanup — config files and Rust crate rename
**Files:**
- Modify: `src-tauri/tauri.conf.json`
- Modify: `src-tauri/Cargo.toml`
- Modify: `src-tauri/src/main.rs`
- Modify: `index.html`
- Modify: `src/lib/components/SetupWizard.svelte`
**Interfaces:**
- Consumes: Nothing (standalone metadata changes)
- Produces: All placeholder values replaced with real project metadata. The Rust lib crate is renamed from `tauri_app_lib` to `gui_video_clipper_lib`.
- [ ] **Step 1: Update `src-tauri/tauri.conf.json`**
Replace the following values:
- `"productName": "Video Clipper"` → `"productName": "GUI Video Clipper"`
- `"identifier": "xyz.cottongin.gui-video-clipper"` → `"identifier": "xyz.cottongin.gui-video-clipper"`
- `"title": "Video Clipper"` → `"title": "GUI Video Clipper"`
- [ ] **Step 2: Update `src-tauri/Cargo.toml`**
Replace the following values in the `[package]` section:
- `name = "tauri-app"` → `name = "gui-video-clipper"`
- `description = "A Tauri App"` → `description = "A macOS GUI app for clipping online videos"`
- `authors = ["you"]` → `authors = ["cottongin"]`
In the `[lib]` section:
- `name = "tauri_app_lib"` → `name = "gui_video_clipper_lib"`
- [ ] **Step 3: Update `src-tauri/src/main.rs`**
Replace `tauri_app_lib::run()` with `gui_video_clipper_lib::run()`.
The full file should be:
```rust
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
gui_video_clipper_lib::run()
}
```
- [ ] **Step 4: Update `index.html`**
Replace `<title>Video Clipper</title>` with `<title>GUI Video Clipper</title>`.
- [ ] **Step 5: Update `src/lib/components/SetupWizard.svelte`**
Replace the string `Video Clipper requires` with `GUI Video Clipper requires` (line 82).
- [ ] **Step 6: Sync Cargo.lock after crate rename**
Run: `cd src-tauri && cargo generate-lockfile`
This updates the `Cargo.lock` entry from `tauri-app` to `gui-video-clipper`.
- [ ] **Step 7: Verify the build compiles**
Run: `cd src-tauri && cargo check`
Expected: compiles without errors. The lib name change is picked up via `Cargo.toml` and `main.rs`.
- [ ] **Step 8: Commit**
```bash
git add src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/src/main.rs index.html src/lib/components/SetupWizard.svelte
git commit -m "chore: replace placeholder metadata with real project values"
```
---
### Task 3: Path scrubbing in plan docs
**Files:**
- Modify: `docs/superpowers/plans/2026-09-21-video-clipper.md`
- Modify: `docs/superpowers/plans/2026-09-22-paged-teleprompter-subtitles.md`
- Modify: `docs/superpowers/plans/2026-09-22-extended-caption-styling.md`
**Interfaces:**
- Consumes: Nothing
- Produces: All absolute paths containing the personal username are replaced with relative equivalents.
- [ ] **Step 1: Replace absolute paths in all three plan files**
In each of the three files, replace all occurrences of `` (with trailing slash) with an empty string — this leaves just the command after `cd ... &&` becomes just the command.
Also replace any remaining occurrences of `` (without trailing slash) with `.` (current directory).
The specific patterns to replace in order (longer match first):
1. `` → `` (empty — removes the cd entirely, leaving just the command)
2. `cd ` → `cd .` (standalone cd references)
3. `` → `` (path prefixes before filenames)
4. `` → `.` (any remaining)
- [ ] **Step 2: Verify no personal paths remain**
Run: `grep -r "REDACTED_USERNAME" docs/superpowers/plans/`
Expected: no output (no matches).
- [ ] **Step 3: Commit**
```bash
git add docs/superpowers/plans/
git commit -m "chore: replace absolute paths with relative equivalents in plan docs"
```
---
### Task 4: LICENSE file
**Files:**
- Create: `LICENSE`
**Interfaces:**
- Consumes: Nothing
- Produces: MIT license file at project root.
- [ ] **Step 1: Create the LICENSE file**
Create `LICENSE` at the project root with this exact content:
```
MIT License
Copyright (c) 2026 cottongin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
- [ ] **Step 2: Commit**
```bash
git add LICENSE
git commit -m "chore: add MIT license"
```
---
### Task 5: README rewrite
**Files:**
- Modify: `README.md`
**Interfaces:**
- Consumes: Nothing
- Produces: Full README replacing the stock Tauri template.
- [ ] **Step 1: Rewrite README.md**
Replace the entire contents of `README.md` with:
```markdown
# GUI Video Clipper
A macOS desktop app for clipping segments from online videos. Paste a URL, scrub the timeline, mark your clips, and export — without manually downloading the video first.
## Features
- **URL Input** — Paste a YouTube (or other supported) URL and start working immediately
- **Video Preview** — Scrub, seek, and frame-step through the video with full transport controls
- **Mark In/Out** — Set clip boundaries with keyboard shortcuts (I/O) or transport buttons
- **Timeline** — Visual timeline with waveform display and thumbnail strip
- **Clip Export** — Export clips in lossless (stream copy) or precise (re-encode) modes
- **Captions** — Download subtitles, preview with karaoke-style highlighting, burn into exports
- **Setup Wizard** — Automatically detects and installs required dependencies (ffmpeg, yt-dlp)
## Prerequisites
- **macOS** (primary target platform)
- **Node.js** (v18+)
- **Rust** toolchain ([rustup.rs](https://rustup.rs))
ffmpeg and yt-dlp are detected (and can be installed) automatically by the built-in setup wizard on first launch.
## Build & Run
```bash
# Install frontend dependencies
npm install
# Development (hot-reload)
npm run tauri dev
# Production build
npm run tauri build
```
## Architecture
GUI Video Clipper is built with [Tauri v2](https://v2.tauri.app/) (Rust backend) and [Svelte 5](https://svelte.dev/) (TypeScript frontend).
### Backend (Rust)
- **Video Resolver** — Resolves URLs via yt-dlp, extracts metadata and stream URLs
- **Download Manager** — Manages preview and high-quality downloads with progress tracking
- **Media Server** — Local HTTP server for streaming video to the frontend player
- **Waveform Generator** — Extracts audio waveform data at multiple zoom tiers via ffmpeg
- **Thumbnail Extractor** — Generates timeline thumbnail strips from keyframes
- **Export Pipeline** — Handles clip extraction with ffmpeg (lossless and precise modes)
- **Subtitle Downloader** — Fetches and converts subtitles for preview and burn-in
### Frontend (Svelte 5 + TypeScript)
- **Timeline** — Canvas-based timeline with waveform, thumbnails, and clip region rendering
- **Transport Controls** — Play/pause, seek, frame-step, shuttle (J/K/L), keyboard shortcuts
- **Video Player** — HTML5 video element connected to the local media server
- **Export Dialog** — Configure and execute clip exports with progress tracking
- **Caption Settings** — Font, color, shadow, and positioning controls for subtitle burn-in
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feat/my-feature`)
3. Make your changes
4. Run checks: `npm run check`
5. Commit with a descriptive message
6. Open a pull request
## License
[MIT](LICENSE)
```
- [ ] **Step 2: Commit**
```bash
git add README.md
git commit -m "docs: rewrite README with full project documentation"
```
---
### Task 6: About dialog
**Files:**
- Create: `src/lib/components/AboutDialog.svelte`
- Modify: `src/App.svelte`
- Modify: `src/lib/components/StatusBar.svelte`
**Interfaces:**
- Consumes: `getVersion()` from `@tauri-apps/api/app`. Existing `showAboutDialog` state from `App.svelte` (added in this task). `onClose` callback prop (same pattern as `PreferencesPanel`).
- Produces: `AboutDialog.svelte` component with `{ onClose: () => void }` props. Clickable version label in `StatusBar` that calls `onOpenAbout` callback. Toolbar info button and `⌘/` shortcut in `App.svelte`.
- [ ] **Step 1: Create `AboutDialog.svelte`**
Create `src/lib/components/AboutDialog.svelte`:
```svelte
<script lang="ts">
import { getVersion } from '@tauri-apps/api/app';
let { onClose }: { onClose: () => void } = $props();
let version = $state('');
$effect(() => {
getVersion().then((v) => {
version = v;
});
});
</script>
<div class="overlay" role="presentation" onclick={onClose}>
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
<img class="app-icon" src="/favicon.svg" alt="GUI Video Clipper icon" />
<h2>GUI Video Clipper</h2>
{#if version}
<span class="version">v{version}</span>
{/if}
<p class="author">by cottongin</p>
<p class="license">MIT License</p>
<a
class="repo-link"
href="https://code.cottongin.xyz/cottongin/gui-video-clipper"
target="_blank"
rel="noopener noreferrer"
>
code.cottongin.xyz/cottongin/gui-video-clipper
</a>
<div class="actions">
<button class="primary" onclick={onClose}>Close</button>
</div>
</div>
</div>
<style>
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 900;
}
.dialog {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px;
max-width: 360px;
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.app-icon {
width: 64px;
height: 64px;
border-radius: 12px;
margin-bottom: 8px;
}
h2 {
margin: 0;
font-size: 18px;
}
.version {
color: var(--text-secondary);
font-size: 14px;
font-family: var(--font-mono);
}
.author {
color: var(--text-secondary);
font-size: 13px;
margin: 0;
}
.license {
color: var(--text-muted);
font-size: 12px;
margin: 0;
}
.repo-link {
color: var(--accent);
font-size: 12px;
text-decoration: none;
}
.repo-link:hover {
text-decoration: underline;
}
.actions {
margin-top: 12px;
}
.primary {
background: var(--accent);
color: var(--bg-primary);
font-weight: 600;
}
</style>
```
- [ ] **Step 2: Add `showAboutDialog` state and import to `App.svelte`**
In `src/App.svelte`, add the import at the top of the `<script>` block (after the other component imports):
```typescript
import AboutDialog from '$lib/components/AboutDialog.svelte';
```
Add a new state variable alongside the existing dialog states:
```typescript
let showAboutDialog = $state(false);
```
- [ ] **Step 3: Add the About dialog render block to `App.svelte`**
Add the following block after the existing `{#if showExportDialog}` block (before the ProcessingModal block):
```svelte
{#if showAboutDialog}
<AboutDialog onClose={() => (showAboutDialog = false)} />
{/if}
```
- [ ] **Step 4: Add the ⌘/ keyboard shortcut to `App.svelte`**
In the `handleGlobalKeydown` function's switch statement, add a new case before the `default:` case:
```typescript
case '/':
if (e.metaKey) {
e.preventDefault();
showAboutDialog = true;
}
break;
```
- [ ] **Step 5: Add the info button to the toolbar in `App.svelte`**
In the `<header class="toolbar">` section, add an info button before the existing prefs button:
```svelte
<button class="about-btn" onclick={() => (showAboutDialog = true)} title="About">ℹ</button>
<button class="prefs-btn" onclick={() => (showPreferences = true)} title="Preferences">⚙</button>
```
Add the corresponding style (in the `<style>` block, next to the existing `.prefs-btn` rule):
```css
.about-btn {
font-size: 16px;
padding: 4px 8px;
}
```
- [ ] **Step 6: Update `StatusBar.svelte` to accept and use `onOpenAbout` callback**
In `src/lib/components/StatusBar.svelte`, update the script to accept a new prop and import `getVersion`:
Add imports at the top:
```typescript
import { getVersion } from '@tauri-apps/api/app';
```
Add the prop and version state:
```typescript
let { onOpenAbout }: { onOpenAbout?: () => void } = $props();
let appVersion = $state('');
$effect(() => {
getVersion().then((v) => {
appVersion = v;
});
});
```
At the end of the `<div class="status-bar">` template (just before the closing `</div>`), add:
```svelte
{#if appVersion}
<button class="version-btn" onclick={() => onOpenAbout?.()} title="About GUI Video Clipper">
v{appVersion}
</button>
{/if}
```
Add the style:
```css
.version-btn {
margin-left: auto;
background: none;
border: none;
color: var(--text-muted);
font-size: 11px;
font-family: var(--font-mono);
padding: 2px 6px;
cursor: pointer;
border-radius: 3px;
}
.version-btn:hover {
background: var(--bg-secondary);
color: var(--text-secondary);
}
```
- [ ] **Step 7: Pass the callback from `App.svelte` to `StatusBar`**
In `src/App.svelte`, update the `<StatusBar />` usage to:
```svelte
<StatusBar onOpenAbout={() => (showAboutDialog = true)} />
```
- [ ] **Step 8: Verify the frontend compiles**
Run: `npm run check`
Expected: no errors.
- [ ] **Step 9: Commit**
```bash
git add src/lib/components/AboutDialog.svelte src/App.svelte src/lib/components/StatusBar.svelte
git commit -m "feat: add About dialog with version, author, and license info"
```
---
### Task 7: Stage all untracked files and commit the release
**Files:**
- Modify: `docs/superpowers/specs/2026-09-22-v0.1-release-polish-design.md` (already exists, just needs staging)
- All other untracked files from git status
**Interfaces:**
- Consumes: All changes from Tasks 1-6
- Produces: A clean commit history with all project files tracked.
- [ ] **Step 1: Verify no `REDACTED_USERNAME` remains in tracked files**
Run: `grep -r "REDACTED_USERNAME" --include="*.json" --include="*.toml" --include="*.rs" --include="*.svelte" --include="*.html" --include="*.ts" --include="*.md" .`
Expected: no matches in any source file. (If any remain, fix them before proceeding.)
- [ ] **Step 2: Stage and commit the spec and plan docs**
```bash
git add docs/superpowers/specs/2026-09-22-v0.1-release-polish-design.md
git add docs/superpowers/plans/2026-09-22-v0.1-release-polish.md
git commit -m "docs: add v0.1.0 release polish spec and implementation plan"
```
- [ ] **Step 3: Final build verification**
Run: `npm run check && cd src-tauri && cargo check`
Expected: both pass without errors.
---
### Task 8: Git history scrub and push to remote
**Files:**
- Create (temporary): `replacements.txt`
- No permanent file changes — this task rewrites git history.
**Interfaces:**
- Consumes: A fully committed repo with no dirty working tree.
- Produces: Clean git history with no `REDACTED_USERNAME` references. Tagged `v0.1.0` release pushed to remote.
- [ ] **Step 1: Create the replacements file**
Create `replacements.txt` in the project root (this file will NOT be committed):
```
xyz.cottongin.gui-video-clipper==>xyz.cottongin.gui-video-clipper
==>
==>
==>
```
Each line uses `==>` as the separator between old and new values. The path replacements have empty right-hand sides to delete the paths entirely. Longer matches come first to avoid partial replacements.
**Note:** This rewrite will also affect the spec and plan docs themselves (they contain the old values as documentation). The "before" columns in tables will be mangled. This is expected — a complete scrub is the goal.
- [ ] **Step 2: Run git-filter-repo**
Run: `git filter-repo --replace-text replacements.txt --force`
Expected: rewrites all commits. Output shows the number of commits processed.
- [ ] **Step 3: Verify the scrub was successful**
Run: `git log --all -p --no-color | grep -i "REDACTED_USERNAME" | head -20`
Expected: no output (no matches anywhere in the history).
Also verify the working tree is still clean:
Run: `git status`
Expected: clean working tree (filter-repo rewrites blobs in place).
- [ ] **Step 4: Clean up the replacements file**
Run: `rm replacements.txt`
- [ ] **Step 5: Add the remote**
Run: `git remote add origin git@code.cottongin.xyz:cottongin/gui-video-clipper.git`
(git-filter-repo removes remotes by default, so this is always needed.)
- [ ] **Step 6: Tag the release**
Run: `git tag -a v0.1.0 -m "Initial release"`
- [ ] **Step 7: Push to remote**
Run: `git push -u origin main && git push origin v0.1.0`
Expected: main branch and v0.1.0 tag pushed successfully.
- [ ] **Step 8: Verify the remote**
Run: `git log --oneline -5 && git tag -l && git remote -v`
Expected: shows recent commits with new hashes, `v0.1.0` tag, and the origin remote URL.