mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 03:26:36 +03:00
introdue app-wide event bus
This commit is contained in:
parent
e94f23d6ce
commit
25b44d7df6
@ -63,6 +63,7 @@
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"inter-ui": "^3.19.3",
|
||||
"nanoevents": "^7.0.1",
|
||||
"nanoid": "^4.0.1",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
|
@ -49,6 +49,7 @@ specifiers:
|
||||
eslint-config-prettier: ^8.5.0
|
||||
eslint-plugin-svelte3: ^4.0.0
|
||||
inter-ui: ^3.19.3
|
||||
nanoevents: ^7.0.1
|
||||
nanoid: ^4.0.1
|
||||
postcss: ^8.4.14
|
||||
postcss-load-config: ^4.0.1
|
||||
@ -127,6 +128,7 @@ devDependencies:
|
||||
eslint-config-prettier: 8.6.0_eslint@8.34.0
|
||||
eslint-plugin-svelte3: 4.0.0_dbthnr4b2bdkhyiebwn7su3hnq
|
||||
inter-ui: 3.19.3
|
||||
nanoevents: 7.0.1
|
||||
nanoid: 4.0.1
|
||||
postcss: 8.4.21
|
||||
postcss-load-config: 4.0.1_postcss@8.4.21
|
||||
@ -143,8 +145,8 @@ devDependencies:
|
||||
svelte-french-toast: 1.0.3_svelte@3.55.1
|
||||
svelte-resize-observer: 2.0.0
|
||||
tailwindcss: 3.2.4_postcss@8.4.21
|
||||
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/36100c269e33ac91246b0a4bfadd18c1caee5296
|
||||
tauri-plugin-websocket-api: github.com/tauri-apps/tauri-plugin-websocket/9b1883b40e6f53cfc77e029000408da8239fba7a
|
||||
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/782a97aeb30cf0f54355d04e6026842fa420bd83
|
||||
tauri-plugin-websocket-api: github.com/tauri-apps/tauri-plugin-websocket/100b5e9eb3f28c88d59d4fed52586fdaa0149ea1
|
||||
tinykeys: 1.4.0
|
||||
tslib: 2.5.0
|
||||
typescript: 4.9.5
|
||||
@ -6847,6 +6849,11 @@ packages:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/nanoevents/7.0.1:
|
||||
resolution: {integrity: sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q==}
|
||||
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
|
||||
dev: true
|
||||
|
||||
/nanoid/3.3.4:
|
||||
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@ -8985,16 +8992,16 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
github.com/tauri-apps/tauri-plugin-log/36100c269e33ac91246b0a4bfadd18c1caee5296:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/36100c269e33ac91246b0a4bfadd18c1caee5296}
|
||||
github.com/tauri-apps/tauri-plugin-log/782a97aeb30cf0f54355d04e6026842fa420bd83:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/782a97aeb30cf0f54355d04e6026842fa420bd83}
|
||||
name: tauri-plugin-log-api
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@tauri-apps/api': 1.2.0
|
||||
dev: true
|
||||
|
||||
github.com/tauri-apps/tauri-plugin-websocket/9b1883b40e6f53cfc77e029000408da8239fba7a:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-websocket/tar.gz/9b1883b40e6f53cfc77e029000408da8239fba7a}
|
||||
github.com/tauri-apps/tauri-plugin-websocket/100b5e9eb3f28c88d59d4fed52586fdaa0149ea1:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-websocket/tar.gz/100b5e9eb3f28c88d59d4fed52586fdaa0149ea1}
|
||||
name: tauri-plugin-websocket-api
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -8,10 +8,12 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { IconExternalLink } from '../icons';
|
||||
import type Events from '$lib/events';
|
||||
|
||||
export let projects: Readable<Project[]>;
|
||||
export let addProject: (params: { path: string }) => Promise<Project>;
|
||||
export let project = readable<Project | undefined>(undefined);
|
||||
export let events: ReturnType<typeof Events>;
|
||||
|
||||
const input = writable('');
|
||||
const scopeToProject = writable(!!$project);
|
||||
@ -27,7 +29,8 @@
|
||||
addProject,
|
||||
projects,
|
||||
project: scopeToProject ? project : undefined,
|
||||
input
|
||||
input,
|
||||
events
|
||||
})
|
||||
);
|
||||
|
||||
@ -128,20 +131,45 @@
|
||||
|
||||
onMount(() =>
|
||||
tinykeys(window, {
|
||||
Backspace: () => {
|
||||
Backspace: (events) => {
|
||||
if (!modal?.isOpen()) return;
|
||||
|
||||
events.preventDefault();
|
||||
events.stopPropagation();
|
||||
if ($selectedGroup) {
|
||||
selectedGroup.set(undefined);
|
||||
} else if ($input.length === 0) {
|
||||
scopeToProject.set(false);
|
||||
}
|
||||
},
|
||||
ArrowDown: selectNextCommand,
|
||||
ArrowUp: selectPreviousCommand,
|
||||
'Control+n': selectNextCommand,
|
||||
'Control+p': selectPreviousCommand,
|
||||
Enter: () => {
|
||||
ArrowDown: (event) => {
|
||||
if (!modal?.isOpen()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectNextCommand();
|
||||
},
|
||||
ArrowUp: (event) => {
|
||||
if (!modal?.isOpen()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectPreviousCommand();
|
||||
},
|
||||
'Control+n': (event) => {
|
||||
if (!modal?.isOpen()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectNextCommand;
|
||||
},
|
||||
'Control+p': (event) => {
|
||||
if (!modal?.isOpen()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectPreviousCommand();
|
||||
},
|
||||
Enter: (event) => {
|
||||
if (!modal?.isOpen()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
Promise.resolve($commandGroups[$selection[0]]).then((group) =>
|
||||
trigger(group.commands[$selection[1]].action)
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { type Project, git } from '$lib/api';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import type Events from '$lib/events';
|
||||
import { toasts } from '$lib';
|
||||
import {
|
||||
IconGitCommit,
|
||||
@ -99,11 +100,25 @@ const projectsGroup = ({
|
||||
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
|
||||
});
|
||||
|
||||
const commandsGroup = ({ project, input }: { project?: Project; input: string }): Group => ({
|
||||
const commandsGroup = ({
|
||||
project,
|
||||
input,
|
||||
events
|
||||
}: {
|
||||
project?: Project;
|
||||
input: string;
|
||||
events: ReturnType<typeof Events>;
|
||||
}): Group => ({
|
||||
title: 'Commands',
|
||||
commands: [
|
||||
...(project
|
||||
? [
|
||||
{
|
||||
title: 'Quick commits...',
|
||||
hotkey: 'C',
|
||||
action: () => events.openQuickCommitModal(),
|
||||
icon: IconGitCommit
|
||||
},
|
||||
{
|
||||
title: 'Replay',
|
||||
hotkey: 'Meta+R',
|
||||
@ -224,11 +239,12 @@ export default (params: {
|
||||
projects: Project[];
|
||||
project?: Project;
|
||||
input: string;
|
||||
events: ReturnType<typeof Events>;
|
||||
}) => {
|
||||
const { addProject, projects, input, project } = params;
|
||||
const { addProject, projects, input, project, events } = params;
|
||||
const groups = [];
|
||||
|
||||
groups.push(commandsGroup({ project, input }));
|
||||
groups.push(commandsGroup({ project, input, events }));
|
||||
groups.push(navigateGroup({ project, input }));
|
||||
!project && groups.push(projectsGroup({ addProject, projects, input }));
|
||||
project && groups.push(fileGroup({ project, input }));
|
||||
|
@ -3,7 +3,6 @@
|
||||
import { Status, type Project, git } from '$lib/api';
|
||||
import type { CloudApi, User } from '$lib/api';
|
||||
import { Button, Modal, Link } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
import { IconGitBranch } from './icons';
|
||||
|
||||
export const show = () => modal.show();
|
||||
@ -109,7 +108,7 @@
|
||||
<Button
|
||||
role="purple"
|
||||
height="small"
|
||||
disabled={isCommitting}
|
||||
disabled={isCommitting || !project.api?.sync}
|
||||
loading={isAutowriting}
|
||||
on:click={onAutowrite}
|
||||
>
|
||||
|
22
src/lib/events.ts
Normal file
22
src/lib/events.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { createNanoEvents } from 'nanoevents';
|
||||
|
||||
interface Events {
|
||||
goto: (path: string) => void;
|
||||
openCommandPalette: () => void;
|
||||
closeCommandPalette: () => void;
|
||||
openNewProjectModal: () => void;
|
||||
openQuickCommitModal: () => void;
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const emitter = createNanoEvents<Events>();
|
||||
return {
|
||||
on: (...args: Parameters<(typeof emitter)['on']>) => emitter.on(...args),
|
||||
|
||||
goto: (path: string) => emitter.emit('goto', path),
|
||||
openCommandPalette: () => emitter.emit('openCommandPalette'),
|
||||
openNewProjectModal: () => emitter.emit('openNewProjectModal'),
|
||||
openQuickCommitModal: () => emitter.emit('openQuickCommitModal'),
|
||||
closeCommandPalette: () => emitter.emit('closeCommandPalette')
|
||||
};
|
||||
};
|
23
src/lib/hotkeys.ts
Normal file
23
src/lib/hotkeys.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { building } from '$app/environment';
|
||||
import tinykeys from 'tinykeys';
|
||||
import type Events from '$lib/events';
|
||||
|
||||
export default (events: ReturnType<typeof Events>) => ({
|
||||
on: (combo: string, callback: (event: KeyboardEvent) => void) => {
|
||||
if (building) return () => {};
|
||||
const comboContainsControlKeys =
|
||||
combo.includes('Meta') || combo.includes('Alt') || combo.includes('Ctrl');
|
||||
return tinykeys(window, {
|
||||
[combo]: (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
|
||||
if (isInput && !comboContainsControlKeys) return;
|
||||
events.closeCommandPalette();
|
||||
callback(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -7,3 +7,8 @@ export const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number
|
||||
};
|
||||
|
||||
export const clone = <T>(obj: T): T => structuredClone(obj);
|
||||
|
||||
export const unsubscribe =
|
||||
(...unsubscribers: (() => void)[]) =>
|
||||
() =>
|
||||
unsubscribers.forEach((unsubscriber) => unsubscriber());
|
||||
|
@ -3,17 +3,16 @@
|
||||
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { toasts, Toaster } from '$lib';
|
||||
import tinykeys, { type KeyBindingMap } from 'tinykeys';
|
||||
import { format } from 'date-fns';
|
||||
import type { LayoutData } from './$types';
|
||||
import { BackForwardButtons, Link, CommandPalette, Breadcrumbs } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { unsubscribe } from '$lib/utils';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { user, posthog, projects, sentry } = data;
|
||||
const { user, posthog, projects, sentry, events, hotkeys } = data;
|
||||
|
||||
const project = derived([page, projects], ([page, projects]) =>
|
||||
projects.find((project) => project.id === page.params.projectId)
|
||||
@ -21,12 +20,9 @@
|
||||
|
||||
export let commandPalette: CommandPalette;
|
||||
|
||||
onMount(() => {
|
||||
const keybindings: KeyBindingMap = {
|
||||
// global
|
||||
'Meta+k': () => commandPalette.show(),
|
||||
'Meta+,': () => goto('/users/'),
|
||||
'Meta+Shift+N': async () => {
|
||||
onMount(() =>
|
||||
unsubscribe(
|
||||
events.on('openNewProjectModal', async () => {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
recursive: true
|
||||
@ -40,40 +36,16 @@
|
||||
} catch (e: any) {
|
||||
toasts.error(e.message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
events.on('openCommandPalette', () => commandPalette?.show()),
|
||||
events.on('closeCommandPalette', () => commandPalette?.close()),
|
||||
events.on('goto', (path: string) => goto(path)),
|
||||
|
||||
// project specific
|
||||
'Meta+Shift+C': () => $project && goto(`/projects/${$project.id}/commit/`),
|
||||
'Meta+T': () => $project && goto(`/projects/${$project.id}/terminal/`),
|
||||
'Meta+P': () => $project && goto(`/projects/${$project.id}/`),
|
||||
'Meta+Shift+,': () => $project && goto(`/projects/${$project.id}/settings/`),
|
||||
'Meta+R': () =>
|
||||
$project && goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`),
|
||||
'a i p': () => $project && goto(`/projects/${$project.id}/aiplayground/`)
|
||||
};
|
||||
|
||||
return tinykeys(
|
||||
window,
|
||||
Object.fromEntries(
|
||||
Object.entries(keybindings).map(([combo, handler]) => {
|
||||
const comboContainsControlKeys =
|
||||
combo.includes('Meta') || combo.includes('Alt') || combo.includes('Ctrl');
|
||||
return [
|
||||
combo,
|
||||
(e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
|
||||
if (isInput && !comboContainsControlKeys) return;
|
||||
|
||||
commandPalette?.close();
|
||||
|
||||
handler(e);
|
||||
}
|
||||
];
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
hotkeys.on('Meta+k', () => events.openCommandPalette()),
|
||||
hotkeys.on('Meta+,', () => events.goto('/users/')),
|
||||
hotkeys.on('Meta+Shift+N', () => events.openNewProjectModal())
|
||||
)
|
||||
);
|
||||
|
||||
user.subscribe(posthog.identify);
|
||||
user.subscribe(sentry.identify);
|
||||
@ -109,5 +81,11 @@
|
||||
<slot />
|
||||
</div>
|
||||
<Toaster />
|
||||
<CommandPalette bind:this={commandPalette} {projects} {project} addProject={projects.add} />
|
||||
<CommandPalette
|
||||
bind:this={commandPalette}
|
||||
{projects}
|
||||
{project}
|
||||
addProject={projects.add}
|
||||
{events}
|
||||
/>
|
||||
</div>
|
||||
|
@ -7,6 +7,8 @@ import Posthog from '$lib/posthog';
|
||||
import Sentry from '$lib/sentry';
|
||||
import { setup as setupLogger } from '$lib/log';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
import Events from '$lib/events';
|
||||
import Hotkeys from '$lib/hotkeys';
|
||||
|
||||
export const ssr = false;
|
||||
export const prerender = true;
|
||||
@ -36,11 +38,14 @@ export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
|
||||
}
|
||||
: await (await import('$lib/api')).users.CurrentUser();
|
||||
setupLogger();
|
||||
const events = Events();
|
||||
return {
|
||||
projects,
|
||||
user,
|
||||
api: Api({ fetch }),
|
||||
posthog: Posthog(),
|
||||
sentry: Sentry()
|
||||
sentry: Sentry(),
|
||||
events,
|
||||
hotkeys: Hotkeys(events)
|
||||
};
|
||||
});
|
||||
|
@ -6,9 +6,12 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { IconSearch, IconSettings, IconTerminal } from '$lib/components/icons';
|
||||
import QuickCommitModal from '$lib/components/QuickCommitModal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { unsubscribe } from '$lib/utils';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { user, api, project, head, statuses, diffs } = data;
|
||||
const { hotkeys, events, user, api, project, head, statuses, diffs } = data;
|
||||
|
||||
let query: string;
|
||||
|
||||
@ -25,6 +28,23 @@
|
||||
}
|
||||
|
||||
$: selection = $page?.route?.id?.split('/')?.[3];
|
||||
|
||||
let quickCommitModal: QuickCommitModal;
|
||||
onMount(() =>
|
||||
unsubscribe(
|
||||
events.on('openQuickCommitModal', () => quickCommitModal.show()),
|
||||
|
||||
hotkeys.on('C', () => events.openQuickCommitModal()),
|
||||
hotkeys.on('Meta+Shift+C', () => goto(`/projects/${$project.id}/commit/`)),
|
||||
hotkeys.on('Meta+T', () => goto(`/projects/${$project.id}/terminal/`)),
|
||||
hotkeys.on('Meta+P', () => goto(`/projects/${$project.id}/`)),
|
||||
hotkeys.on('Meta+Shift+,', () => goto(`/projects/${$project.id}/settings/`)),
|
||||
hotkeys.on('Meta+R', () =>
|
||||
goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`)
|
||||
),
|
||||
hotkeys.on('a i p', () => goto(`/projects/${$project.id}/aiplayground/`))
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col">
|
||||
@ -122,8 +142,9 @@
|
||||
|
||||
{#if $user}
|
||||
<QuickCommitModal
|
||||
bind:this={quickCommitModal}
|
||||
user={$user}
|
||||
api={api}
|
||||
{api}
|
||||
project={$project}
|
||||
head={$head}
|
||||
diffs={$diffs}
|
||||
|
Loading…
Reference in New Issue
Block a user