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() {
-