diff --git a/.github/workflows/desktop-check.yml b/.github/workflows/desktop-check.yml index 5b835a6..539bdfb 100644 --- a/.github/workflows/desktop-check.yml +++ b/.github/workflows/desktop-check.yml @@ -69,6 +69,16 @@ jobs: mkdir -p dist echo 'stub' > dist/index.html + # externalBin ("bin/ffmpeg") is validated at compile time by tauri-build; + # an empty file is enough for `cargo check` — we never execute it here. + - name: Stub ffmpeg sidecar + shell: bash + run: | + mkdir -p src-tauri/bin + TRIPLE=$(rustc -vV | sed -n 's/host: //p') + EXT=""; [ "${{ runner.os }}" = "Windows" ] && EXT=".exe" + : > "src-tauri/bin/ffmpeg-${TRIPLE}${EXT}" + - name: cargo check (all targets) working-directory: src-tauri run: cargo check --all-targets diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c55ff2..38c6905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to GoodWebTools are documented here. +## [1.0.0-beta.3] — 2026-07-27 + +### Added +- Desktop app now **bundles FFmpeg**, so screen recording with audio works without a separate system FFmpeg install. + +### Fixed +- **Unix Timestamp Converter** now detects and converts microsecond (16-digit) and nanosecond (19-digit) values instead of failing with a parse error. + +### Changed +- Internal: resolved all source-code lint warnings (no behavior change). + ## [1.0.0-beta.2] — 2026-07-27 ### Fixed diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 111b04a..81a852a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1956,7 +1956,7 @@ dependencies = [ [[package]] name = "goodwebtools" -version = "1.0.0-beta.1" +version = "1.0.0-beta.3" dependencies = [ "chrono", "cocoa", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3449c66..ca8b0e6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goodwebtools" -version = "1.0.0-beta.1" +version = "1.0.0-beta.3" description = "Privacy-first client-side tools" authors = ["Kresna "] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 519a362..57a448c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,10 +1,11 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "GoodWebTools", - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "identifier": "com.goodwebtools.app", "build": { - "beforeBuildCommand": "npm run build", + "beforeDevCommand": "npm run download:ffmpeg", + "beforeBuildCommand": "npm run download:ffmpeg && npm run build", "frontendDist": "../dist", "devUrl": "http://localhost:4321" }, @@ -78,6 +79,7 @@ "bundle": { "active": true, "createUpdaterArtifacts": true, + "externalBin": ["bin/ffmpeg"], "targets": ["nsis", "app", "dmg", "deb"], "resources": [], "category": "Utility", diff --git a/src/islands/ToolHost.tsx b/src/islands/ToolHost.tsx index 7fba5a2..33f0208 100644 --- a/src/islands/ToolHost.tsx +++ b/src/islands/ToolHost.tsx @@ -48,10 +48,12 @@ class ToolErrorBoundary extends Component { export default function ToolHost({ toolId }: ToolHostProps) { const tool = getToolById(toolId); + // `tool` is a stable registry reference derived from `toolId`, so keying on + // both is equivalent to keying on `toolId` alone — and satisfies the linter. const LazyTool = useMemo(() => { if (!tool) return null; return lazy(tool.load); - }, [toolId]); + }, [toolId, tool]); if (!tool || !LazyTool) { return
Tool not found.
; diff --git a/src/islands/dev/HotkeyTest.tsx b/src/islands/dev/HotkeyTest.tsx index a8b93f9..4f5941f 100644 --- a/src/islands/dev/HotkeyTest.tsx +++ b/src/islands/dev/HotkeyTest.tsx @@ -29,7 +29,7 @@ export default function HotkeyTest() { const registerTestHotkey = async (keys: string, description: string) => { setError(null); try { - const id = await hotkeyService.register( + await hotkeyService.register( keys, () => { setLastTriggered(`${description} (${keys})`); diff --git a/src/islands/dev/JsonCompare.tsx b/src/islands/dev/JsonCompare.tsx index 8b0171d..723066f 100644 --- a/src/islands/dev/JsonCompare.tsx +++ b/src/islands/dev/JsonCompare.tsx @@ -195,11 +195,13 @@ export default function JsonCompare() { parseAndCompare(leftJson, value); }; - // Re-compare when ignoreArrayOrder changes + // Re-compare only when ignoreArrayOrder toggles; edits to left/right already + // trigger a compare via their change handlers, so they're intentionally omitted. useEffect(() => { if (leftJson.trim() && rightJson.trim()) { parseAndCompare(leftJson, rightJson); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ignoreArrayOrder]); return ( diff --git a/src/islands/dev/PasswordGen.tsx b/src/islands/dev/PasswordGen.tsx index 0736410..dd6974c 100644 --- a/src/islands/dev/PasswordGen.tsx +++ b/src/islands/dev/PasswordGen.tsx @@ -37,7 +37,6 @@ export default function PasswordGen() { // Regenerate whenever any option changes. useEffect(() => { setPassword(generatePassword({ length, enabled, avoidAmbiguous, minNumbers, minSpecial })); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [length, enabled, avoidAmbiguous, minNumbers, minSpecial]); const clampMin = (value: number) => setMinLength(Math.min(Math.max(value || 1, 1), maxLength)); diff --git a/src/islands/dev/Timestamp.tsx b/src/islands/dev/Timestamp.tsx index c2ea975..48a6bcd 100644 --- a/src/islands/dev/Timestamp.tsx +++ b/src/islands/dev/Timestamp.tsx @@ -6,6 +6,7 @@ import { Alert } from '@/components/ui/Alert'; import { describeDate, parseTimestamp, + detectNumericUnit, formatInTimeZone, listTimeZones, getLocalTimeZone, @@ -19,6 +20,7 @@ export default function Timestamp() { const [date, setDate] = useState(null); const [timeZone, setTimeZone] = useState(() => getLocalTimeZone()); const [error, setError] = useState(''); + const [detectedUnit, setDetectedUnit] = useState(''); const [pickerValue, setPickerValue] = useState(''); const [pickerZone, setPickerZone] = useState<'local' | 'utc'>('local'); @@ -27,6 +29,7 @@ export default function Timestamp() { setPickerValue(value); setPickerZone(zone); setError(''); + setDetectedUnit(''); if (!value) return; const parsed = parseDateTimeLocal(value, zone); if (parsed) setDate(parsed); @@ -35,6 +38,7 @@ export default function Timestamp() { const convert = () => { setError(''); setDate(null); + setDetectedUnit(''); const trimmed = input.trim(); if (!trimmed) return; const parsed = parseTimestamp(trimmed); @@ -43,10 +47,13 @@ export default function Timestamp() { return; } setDate(parsed); + // Surface how an all-digits value was interpreted (seconds/ms/µs/ns). + if (/^\d+$/.test(trimmed)) setDetectedUnit(detectNumericUnit(trimmed)); }; const now = () => { setError(''); + setDetectedUnit(''); setDate(new Date()); }; @@ -69,7 +76,7 @@ export default function Timestamp() { label="Unix timestamp or date string" value={input} onChange={e => setInput(e.target.value)} - placeholder="1720000000 · 2026-07-12 · Jul 12 2026 10:00" + placeholder="1720000000 (s · ms · µs · ns) · 2026-07-12 · Jul 12 2026 10:00" rows={2} /> @@ -113,13 +120,20 @@ export default function Timestamp() { - {error && {error}} + {detectedUnit && ( +

+ Detected numeric input as{' '} + Unix {detectedUnit}. +

+ )} + {date && ( <>