diff --git a/src/lib/components/AboutDialog.svelte b/src/lib/components/AboutDialog.svelte
index 572de81..e23044b 100644
--- a/src/lib/components/AboutDialog.svelte
+++ b/src/lib/components/AboutDialog.svelte
@@ -4,12 +4,43 @@
let { onClose }: { onClose: () => void } = $props();
let version = $state('');
+ let activeTab = $state<'about' | 'shortcuts'>('about');
$effect(() => {
getVersion().then((v) => {
version = v;
});
});
+
+ const globalShortcuts = [
+ { key: 'Space', action: 'Play / Pause' },
+ { key: 'K', action: 'Play / Pause' },
+ { key: 'J', action: 'Shuttle slower (−0.25×)' },
+ { key: 'L', action: 'Shuttle faster (+0.25×)' },
+ { key: '←', action: 'Seek back 5s' },
+ { key: '→', action: 'Seek forward 5s' },
+ { key: 'Shift+←', action: 'Seek back 1s' },
+ { key: 'Shift+→', action: 'Seek forward 1s' },
+ { key: ',', action: 'Previous frame' },
+ { key: '.', action: 'Next frame' },
+ { key: '< (Shift+,)', action: 'Previous keyframe' },
+ { key: '> (Shift+.)', action: 'Next keyframe' },
+ { key: 'C / P', action: 'Toggle mode' },
+ { key: '⌘/', action: 'About' },
+ ];
+
+ const playerShortcuts = [
+ { key: 'F', action: 'Toggle fullscreen' },
+ { key: 'X', action: 'Cut clip at playhead → Clipper' },
+ { key: 'Z', action: 'Cut clip 10s before playhead → Clipper' },
+ ];
+
+ const clipperShortcuts = [
+ { key: 'I', action: 'Set in-point' },
+ { key: 'O', action: 'Set out-point' },
+ { key: 'Delete / ⌫', action: 'Remove selected clip' },
+ { key: '⌘E', action: 'Export' },
+ ];
e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>
-
-
GUI Video Clipper
- {#if version}
-
v{version}
+
+
+
+
+
+
+ {#if activeTab === 'about'}
+
+
+
+
GUI Video Clipper
+ {#if version}
+
v{version}
+ {/if}
+
by cottongin
+
MIT License
+
+ code.cottongin.xyz/cottongin/gui-video-clipper
+
+
+ {:else}
+
+
+
+ Global
+
+
+ {#each globalShortcuts as { key, action }}
+
+ | {key} |
+ {action} |
+
+ {/each}
+
+
+
+
+
+ Player Mode
+
+
+ {#each playerShortcuts as { key, action }}
+
+ | {key} |
+ {action} |
+
+ {/each}
+
+
+
+
+
+ Clipper Mode
+
+
+ {#each clipperShortcuts as { key, action }}
+
+ | {key} |
+ {action} |
+
+ {/each}
+
+
+
+
{/if}
-
by cottongin
-
MIT License
-
- code.cottongin.xyz/cottongin/gui-video-clipper
-
+
@@ -103,7 +206,7 @@
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
- padding: 32px;
+ padding: 24px 32px 32px;
max-width: 360px;
width: 100%;
text-align: center;
@@ -111,6 +214,53 @@
flex-direction: column;
align-items: center;
gap: 8px;
+ transition: max-width 0.2s ease;
+ }
+
+ .dialog.wide {
+ max-width: 520px;
+ max-height: 70vh;
+ }
+
+ /* --- Tabs --- */
+ .tab-bar {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--border);
+ width: 100%;
+ margin-bottom: 12px;
+ }
+
+ .tab {
+ flex: 1;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--text-secondary);
+ font-size: 13px;
+ font-weight: 500;
+ padding: 8px 0;
+ cursor: pointer;
+ border-radius: 0;
+ }
+
+ .tab:hover {
+ color: var(--text-primary);
+ background: none;
+ }
+
+ .tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+ background: none;
+ }
+
+ /* --- About content --- */
+ .about-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
}
.app-icon {
@@ -153,6 +303,64 @@
text-decoration: underline;
}
+ /* --- Shortcuts content --- */
+ .shortcuts-content {
+ width: 100%;
+ text-align: left;
+ overflow-y: auto;
+ max-height: calc(70vh - 140px);
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .shortcuts-content h3 {
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-muted);
+ margin: 0 0 6px;
+ }
+
+ .shortcuts-content table {
+ width: 100%;
+ border-collapse: collapse;
+ }
+
+ .shortcuts-content tr {
+ border-bottom: 1px solid var(--border);
+ }
+
+ .shortcuts-content tr:last-child {
+ border-bottom: none;
+ }
+
+ .key-cell {
+ width: 40%;
+ padding: 5px 8px 5px 0;
+ white-space: nowrap;
+ }
+
+ .action-cell {
+ padding: 5px 0;
+ color: var(--text-secondary);
+ font-size: 13px;
+ }
+
+ kbd {
+ display: inline-block;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 1px 6px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--text-primary);
+ line-height: 1.4;
+ }
+
+ /* --- Actions --- */
.actions {
margin-top: 12px;
}
diff --git a/tests/lib/components/AboutDialog.test.ts b/tests/lib/components/AboutDialog.test.ts
new file mode 100644
index 0000000..cd35147
--- /dev/null
+++ b/tests/lib/components/AboutDialog.test.ts
@@ -0,0 +1,59 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent } from '@testing-library/svelte';
+import AboutDialog from '$lib/components/AboutDialog.svelte';
+
+vi.mock('@tauri-apps/api/app', () => ({
+ getVersion: vi.fn().mockResolvedValue('0.2.1'),
+}));
+
+describe('AboutDialog', () => {
+ it('renders the About tab by default', () => {
+ const { getByText } = render(AboutDialog, {
+ props: { onClose: vi.fn() },
+ });
+ expect(getByText('GUI Video Clipper')).toBeTruthy();
+ expect(getByText('by cottongin')).toBeTruthy();
+ });
+
+ it('shows two tabs: About and Shortcuts', () => {
+ const { getByRole } = render(AboutDialog, {
+ props: { onClose: vi.fn() },
+ });
+ const tablist = getByRole('tablist');
+ expect(tablist).toBeTruthy();
+ expect(tablist.querySelectorAll('[role="tab"]')).toHaveLength(2);
+ });
+
+ it('switches to Shortcuts tab on click', async () => {
+ const { getByText, getAllByText } = render(AboutDialog, {
+ props: { onClose: vi.fn() },
+ });
+ const shortcutsTab = getByText('Shortcuts');
+ await fireEvent.click(shortcutsTab);
+
+ expect(getAllByText('Play / Pause').length).toBeGreaterThan(0);
+ expect(getByText('Toggle mode')).toBeTruthy();
+ expect(getByText('Player Mode')).toBeTruthy();
+ expect(getByText('Clipper Mode')).toBeTruthy();
+ });
+
+ it('switches back to About tab', async () => {
+ const { getByText, getByRole } = render(AboutDialog, {
+ props: { onClose: vi.fn() },
+ });
+ await fireEvent.click(getByRole('tab', { name: 'Shortcuts' }));
+ await fireEvent.click(getByRole('tab', { name: 'About' }));
+
+ expect(getByText('GUI Video Clipper')).toBeTruthy();
+ expect(getByText('by cottongin')).toBeTruthy();
+ });
+
+ it('calls onClose when Close button is clicked', async () => {
+ const onClose = vi.fn();
+ const { getByText } = render(AboutDialog, {
+ props: { onClose },
+ });
+ await fireEvent.click(getByText('Close'));
+ expect(onClose).toHaveBeenCalled();
+ });
+});