mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 11:17:27 +00:00
refactor: migrate views to MainLayout
This commit is contained in:
parent
3a11a73133
commit
8c395f0a60
4 changed files with 465 additions and 427 deletions
|
@ -15,6 +15,7 @@
|
|||
import BaseList from './BaseList.svelte';
|
||||
import KeyboardShortcut from './KeyboardShortcut.svelte';
|
||||
import HeaderInput from './HeaderInput.svelte';
|
||||
import MainLayout from './layout/MainLayout.svelte';
|
||||
|
||||
type Props = {
|
||||
onBack: () => void;
|
||||
|
@ -225,150 +226,158 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||
<header class="flex h-15 shrink-0 items-center">
|
||||
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||
<ArrowLeft class="size-5" />
|
||||
</Button>
|
||||
<HeaderInput placeholder="Type to filter entries..." bind:value={searchText} autofocus />
|
||||
<Select.Root bind:value={filter} type="single">
|
||||
<Select.Trigger class="w-32">
|
||||
{filter === 'all' ? 'All Types' : filter.charAt(0).toUpperCase() + filter.slice(1) + 's'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="all">All Types</Select.Item>
|
||||
<Select.Item value="text">Text</Select.Item>
|
||||
<Select.Item value="image">Images</Select.Item>
|
||||
<Select.Item value="link">Links</Select.Item>
|
||||
<Select.Item value="color">Colors</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</header>
|
||||
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
|
||||
<div class="flex-grow overflow-y-auto border-r" bind:this={listContainerEl}>
|
||||
<BaseList
|
||||
items={displayedItems}
|
||||
bind:selectedIndex
|
||||
onenter={(item) => handleCopy(item.data as ClipboardItem)}
|
||||
isItemSelectable={(item) => item.itemType === 'item'}
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
|
||||
{#if item.itemType === 'header'}
|
||||
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
|
||||
{item.data as string}
|
||||
</h3>
|
||||
{:else if item.itemType === 'item'}
|
||||
{@const clipboardItem = item.data as ClipboardItem}
|
||||
<button class="w-full" onclick={itemOnClick}>
|
||||
<ListItemBase
|
||||
icon={iconMap.get(clipboardItem.contentType) ?? 'question-mark-circle-16'}
|
||||
title={clipboardItem.preview ?? clipboardItem.contentValue ?? ''}
|
||||
{isSelected}
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
{#if isFetching && allItems.length > 0}
|
||||
<div class="text-muted-foreground flex h-10 items-center justify-center">
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-hidden">
|
||||
{#if selectedItem}
|
||||
<div class="relative flex-grow overflow-y-auto p-4">
|
||||
{#if isContentLoading}
|
||||
<div
|
||||
class="bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-sm"
|
||||
>
|
||||
<Loader2 class="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedItemContent}
|
||||
{#if selectedItem.contentType === 'color'}
|
||||
<div class="flex flex-col items-center justify-center gap-4 py-8">
|
||||
<div
|
||||
class="size-24 rounded-full border"
|
||||
style:background-color={selectedItemContent}
|
||||
></div>
|
||||
<p class="font-mono text-lg">{selectedItemContent}</p>
|
||||
</div>
|
||||
{:else if selectedItem.contentType === 'image'}
|
||||
<img
|
||||
src={convertFileSrc(selectedItemContent)}
|
||||
alt="Clipboard content"
|
||||
class="mx-auto max-h-full max-w-full rounded-lg object-contain"
|
||||
/>
|
||||
{:else if virtualizedLines.length > 0}
|
||||
<div class="h-full font-mono text-sm">
|
||||
<VList data={virtualizedLines}>
|
||||
{#snippet children(item)}
|
||||
<div class="whitespace-pre">{item}</div>
|
||||
{/snippet}
|
||||
</VList>
|
||||
</div>
|
||||
{:else if selectedItem.contentType === 'text'}
|
||||
<div class="rounded bg-black/10 p-4 font-mono text-sm whitespace-pre-wrap">
|
||||
{selectedItemContent}
|
||||
<MainLayout>
|
||||
{#snippet header()}
|
||||
<header class="flex h-15 shrink-0 items-center">
|
||||
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||
<ArrowLeft class="size-5" />
|
||||
</Button>
|
||||
<HeaderInput placeholder="Type to filter entries..." bind:value={searchText} autofocus />
|
||||
<Select.Root bind:value={filter} type="single">
|
||||
<Select.Trigger class="w-32">
|
||||
{filter === 'all' ? 'All Types' : filter.charAt(0).toUpperCase() + filter.slice(1) + 's'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="all">All Types</Select.Item>
|
||||
<Select.Item value="text">Text</Select.Item>
|
||||
<Select.Item value="image">Images</Select.Item>
|
||||
<Select.Item value="link">Links</Select.Item>
|
||||
<Select.Item value="color">Colors</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</header>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
|
||||
<div class="flex-grow overflow-y-auto border-r" bind:this={listContainerEl}>
|
||||
<BaseList
|
||||
items={displayedItems}
|
||||
bind:selectedIndex
|
||||
onenter={(item) => handleCopy(item.data as ClipboardItem)}
|
||||
isItemSelectable={(item) => item.itemType === 'item'}
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
|
||||
{#if item.itemType === 'header'}
|
||||
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
|
||||
{item.data as string}
|
||||
</h3>
|
||||
{:else if item.itemType === 'item'}
|
||||
{@const clipboardItem = item.data as ClipboardItem}
|
||||
<button class="w-full" onclick={itemOnClick}>
|
||||
<ListItemBase
|
||||
icon={iconMap.get(clipboardItem.contentType) ?? 'question-mark-circle-16'}
|
||||
title={clipboardItem.preview ?? clipboardItem.contentValue ?? ''}
|
||||
{isSelected}
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
{#if isFetching && allItems.length > 0}
|
||||
<div class="text-muted-foreground flex h-10 items-center justify-center">
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-hidden">
|
||||
{#if selectedItem}
|
||||
<div class="relative flex-grow overflow-y-auto p-4">
|
||||
{#if isContentLoading}
|
||||
<div
|
||||
class="bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-sm"
|
||||
>
|
||||
<Loader2 class="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border-t p-4">
|
||||
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
|
||||
<div class="flex flex-col gap-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Application</span>
|
||||
<span>{selectedItem.sourceAppName ?? 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Content type</span>
|
||||
<span class="capitalize">{selectedItem.contentType}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Times copied</span>
|
||||
<span>{selectedItem.timesCopied}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Last copied</span>
|
||||
<span>{formatDateTime(selectedItem.lastCopiedAt)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">First copied</span>
|
||||
<span>{formatDateTime(selectedItem.firstCopiedAt)}</span>
|
||||
{#if selectedItemContent}
|
||||
{#if selectedItem.contentType === 'color'}
|
||||
<div class="flex flex-col items-center justify-center gap-4 py-8">
|
||||
<div
|
||||
class="size-24 rounded-full border"
|
||||
style:background-color={selectedItemContent}
|
||||
></div>
|
||||
<p class="font-mono text-lg">{selectedItemContent}</p>
|
||||
</div>
|
||||
{:else if selectedItem.contentType === 'image'}
|
||||
<img
|
||||
src={convertFileSrc(selectedItemContent)}
|
||||
alt="Clipboard content"
|
||||
class="mx-auto max-h-full max-w-full rounded-lg object-contain"
|
||||
/>
|
||||
{:else if virtualizedLines.length > 0}
|
||||
<div class="h-full font-mono text-sm">
|
||||
<VList data={virtualizedLines}>
|
||||
{#snippet children(item)}
|
||||
<div class="whitespace-pre">{item}</div>
|
||||
{/snippet}
|
||||
</VList>
|
||||
</div>
|
||||
{:else if selectedItem.contentType === 'text'}
|
||||
<div class="rounded bg-black/10 p-4 font-mono text-sm whitespace-pre-wrap">
|
||||
{selectedItemContent}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border-t p-4">
|
||||
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
|
||||
<div class="flex flex-col gap-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Application</span>
|
||||
<span>{selectedItem.sourceAppName ?? 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Content type</span>
|
||||
<span class="capitalize">{selectedItem.contentType}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Times copied</span>
|
||||
<span>{selectedItem.timesCopied}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Last copied</span>
|
||||
<span>{formatDateTime(selectedItem.lastCopiedAt)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">First copied</span>
|
||||
<span>{formatDateTime(selectedItem.firstCopiedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActionBar>
|
||||
{#snippet primaryAction({ props })}
|
||||
<Button {...props} onclick={() => handleCopy(selectedItem)}>
|
||||
Copy to Clipboard <Kbd>⏎</Kbd>
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<ActionMenu>
|
||||
<DropdownMenu.Item onclick={() => handlePin(selectedItem)}>
|
||||
<Pin class="mr-2 size-4" />
|
||||
<span>{selectedItem.isPinned ? 'Unpin' : 'Pin'}</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'P', modifiers: ['cmd', 'shift'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
|
||||
<Trash class="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</ActionMenu>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
{#if selectedItem}
|
||||
<ActionBar>
|
||||
{#snippet primaryAction({ props })}
|
||||
<Button {...props} onclick={() => handleCopy(selectedItem)}>
|
||||
Copy to Clipboard <Kbd>⏎</Kbd>
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<ActionMenu>
|
||||
<DropdownMenu.Item onclick={() => handlePin(selectedItem)}>
|
||||
<Pin class="mr-2 size-4" />
|
||||
<span>{selectedItem.isPinned ? 'Unpin' : 'Pin'}</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'P', modifiers: ['cmd', 'shift'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
|
||||
<Trash class="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</ActionMenu>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</MainLayout>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import KeyboardShortcut from './KeyboardShortcut.svelte';
|
||||
import { focusManager } from '$lib/focus.svelte';
|
||||
import HeaderInput from './HeaderInput.svelte';
|
||||
import MainLayout from './layout/MainLayout.svelte';
|
||||
|
||||
type Props = {
|
||||
onBack: () => void;
|
||||
|
@ -132,104 +133,112 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
|
||||
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||
<ArrowLeft class="size-5" />
|
||||
</Button>
|
||||
<HeaderInput
|
||||
placeholder="Search for files and folders..."
|
||||
bind:value={searchText}
|
||||
bind:ref={searchInputEl}
|
||||
autofocus
|
||||
/>
|
||||
</header>
|
||||
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
|
||||
<div class="flex-grow overflow-y-auto border-r">
|
||||
{#if isFetching && searchResults.length === 0}
|
||||
<div class="text-muted-foreground flex h-full items-center justify-center">
|
||||
<Loader2 class="size-6 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
<BaseList
|
||||
items={searchResults.map((item) => ({ ...item, id: item.path }))}
|
||||
bind:selectedIndex
|
||||
onenter={(item) => handleOpen(item)}
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick })}
|
||||
<button class="w-full text-left" {onclick}>
|
||||
<ListItemBase
|
||||
icon={item.fileType === 'directory' ? 'folder-16' : 'blank-document-16'}
|
||||
title={item.name}
|
||||
subtitle={item.parentPath}
|
||||
{isSelected}
|
||||
/>
|
||||
</button>
|
||||
<MainLayout>
|
||||
{#snippet header()}
|
||||
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
|
||||
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||
<ArrowLeft class="size-5" />
|
||||
</Button>
|
||||
<HeaderInput
|
||||
placeholder="Search for files and folders..."
|
||||
bind:value={searchText}
|
||||
bind:ref={searchInputEl}
|
||||
autofocus
|
||||
/>
|
||||
</header>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
|
||||
<div class="flex-grow overflow-y-auto border-r">
|
||||
{#if isFetching && searchResults.length === 0}
|
||||
<div class="text-muted-foreground flex h-full items-center justify-center">
|
||||
<Loader2 class="size-6 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
<BaseList
|
||||
items={searchResults.map((item) => ({ ...item, id: item.path }))}
|
||||
bind:selectedIndex
|
||||
onenter={(item) => handleOpen(item)}
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick })}
|
||||
<button class="w-full text-left" {onclick}>
|
||||
<ListItemBase
|
||||
icon={item.fileType === 'directory' ? 'folder-16' : 'blank-document-16'}
|
||||
title={item.name}
|
||||
subtitle={item.parentPath}
|
||||
{isSelected}
|
||||
/>
|
||||
</button>
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-hidden">
|
||||
{#if selectedItem}
|
||||
<div class="flex h-full flex-col items-center justify-center p-4">
|
||||
<div class="mb-4">
|
||||
{#if selectedItem.fileType === 'directory'}
|
||||
<Folder class="size-24 text-gray-500" />
|
||||
{:else}
|
||||
<File class="size-24 text-gray-500" />
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xl font-semibold">{selectedItem.name}</p>
|
||||
<p class="text-muted-foreground text-sm">{selectedItem.path}</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t p-4">
|
||||
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
|
||||
<div class="flex flex-col gap-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Type</span>
|
||||
<span class="capitalize">{selectedItem.fileType}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Last Modified</span>
|
||||
<span>{formatDateTime(selectedItem.lastModified)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
{#if selectedItem}
|
||||
<ActionBar>
|
||||
{#snippet primaryAction({ props })}
|
||||
<Button {...props} onclick={() => handleOpen(selectedItem)}>
|
||||
Open <KeyboardShortcut shortcut={{ key: 'enter', modifiers: [] }} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-hidden">
|
||||
{#if selectedItem}
|
||||
<div class="flex h-full flex-col items-center justify-center p-4">
|
||||
<div class="mb-4">
|
||||
{#if selectedItem.fileType === 'directory'}
|
||||
<Folder class="size-24 text-gray-500" />
|
||||
{:else}
|
||||
<File class="size-24 text-gray-500" />
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xl font-semibold">{selectedItem.name}</p>
|
||||
<p class="text-muted-foreground text-sm">{selectedItem.path}</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t p-4">
|
||||
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
|
||||
<div class="flex flex-col gap-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Type</span>
|
||||
<span class="capitalize">{selectedItem.fileType}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Last Modified</span>
|
||||
<span>{formatDateTime(selectedItem.lastModified)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActionBar>
|
||||
{#snippet primaryAction({ props })}
|
||||
<Button {...props} onclick={() => handleOpen(selectedItem)}>
|
||||
Open <KeyboardShortcut shortcut={{ key: 'enter', modifiers: [] }} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<ActionMenu>
|
||||
<DropdownMenu.Item onclick={() => handleShow(selectedItem)}>
|
||||
<Eye class="mr-2 size-4" />
|
||||
<span>Show in File Manager</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'Enter', modifiers: ['cmd'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => handleCopyPath(selectedItem)}>
|
||||
<Copy class="mr-2 size-4" />
|
||||
<span>Copy Path</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'c', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item class="text-red-500" onclick={() => handleDelete(selectedItem)}>
|
||||
<Trash class="mr-2 size-4" />
|
||||
<span>Move to Trash</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</ActionMenu>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{#snippet actions()}
|
||||
<ActionMenu>
|
||||
<DropdownMenu.Item onclick={() => handleShow(selectedItem)}>
|
||||
<Eye class="mr-2 size-4" />
|
||||
<span>Show in File Manager</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'Enter', modifiers: ['cmd'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => handleCopyPath(selectedItem)}>
|
||||
<Copy class="mr-2 size-4" />
|
||||
<span>Copy Path</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'c', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item class="text-red-500" onclick={() => handleDelete(selectedItem)}>
|
||||
<Trash class="mr-2 size-4" />
|
||||
<span>Move to Trash</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</ActionMenu>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</MainLayout>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import BaseList from './BaseList.svelte';
|
||||
import KeyboardShortcut from './KeyboardShortcut.svelte';
|
||||
import HeaderInput from './HeaderInput.svelte';
|
||||
import MainLayout from './layout/MainLayout.svelte';
|
||||
|
||||
type Props = {
|
||||
onBack: () => void;
|
||||
|
@ -160,93 +161,101 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
|
||||
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||
<ArrowLeft class="size-5" />
|
||||
</Button>
|
||||
<HeaderInput placeholder="Search snippets..." bind:value={searchText} autofocus />
|
||||
</header>
|
||||
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
|
||||
<div class="flex-grow overflow-y-auto border-r">
|
||||
{#if isFetching && snippets.length === 0}
|
||||
<div class="text-muted-foreground flex h-full items-center justify-center">
|
||||
<Loader2 class="size-6 animate-spin" />
|
||||
</div>
|
||||
{:else}
|
||||
<BaseList
|
||||
items={displayedItems}
|
||||
bind:selectedIndex
|
||||
onenter={(item) => handlePaste(item.data as Snippet)}
|
||||
isItemSelectable={(item) => item.itemType === 'item'}
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
|
||||
{#if item.itemType === 'header'}
|
||||
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
|
||||
{item.data as string}
|
||||
</h3>
|
||||
{:else if item.itemType === 'item'}
|
||||
{@const snippetItem = item.data as Snippet}
|
||||
<button class="w-full text-left" onclick={itemOnClick}>
|
||||
<ListItemBase
|
||||
icon="snippets-16"
|
||||
title={snippetItem.name}
|
||||
subtitle={snippetItem.keyword}
|
||||
{isSelected}
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-hidden">
|
||||
{#if selectedItem}
|
||||
<div class="relative flex-grow overflow-y-auto p-4">
|
||||
<div class="font-mono text-sm whitespace-pre-wrap">{selectedItem.content}</div>
|
||||
</div>
|
||||
<MainLayout>
|
||||
{#snippet header()}
|
||||
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
|
||||
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||
<ArrowLeft class="size-5" />
|
||||
</Button>
|
||||
<HeaderInput placeholder="Search snippets..." bind:value={searchText} autofocus />
|
||||
</header>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
|
||||
<div class="flex-grow overflow-y-auto border-r">
|
||||
{#if isFetching && snippets.length === 0}
|
||||
<div class="text-muted-foreground flex h-full items-center justify-center">
|
||||
<Loader2 class="size-6 animate-spin" />
|
||||
</div>
|
||||
{:else}
|
||||
<BaseList
|
||||
items={displayedItems}
|
||||
bind:selectedIndex
|
||||
onenter={(item) => handlePaste(item.data as Snippet)}
|
||||
isItemSelectable={(item) => item.itemType === 'item'}
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
|
||||
{#if item.itemType === 'header'}
|
||||
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
|
||||
{item.data as string}
|
||||
</h3>
|
||||
{:else if item.itemType === 'item'}
|
||||
{@const snippetItem = item.data as Snippet}
|
||||
<button class="w-full text-left" onclick={itemOnClick}>
|
||||
<ListItemBase
|
||||
icon="snippets-16"
|
||||
title={snippetItem.name}
|
||||
subtitle={snippetItem.keyword}
|
||||
{isSelected}
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-hidden">
|
||||
{#if selectedItem}
|
||||
<div class="relative flex-grow overflow-y-auto p-4">
|
||||
<div class="font-mono text-sm whitespace-pre-wrap">{selectedItem.content}</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t p-4">
|
||||
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
|
||||
<div class="flex flex-col gap-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Name</span>
|
||||
<span>{selectedItem.name}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Content type</span>
|
||||
<span class="capitalize">Text</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Times used</span>
|
||||
<span>{selectedItem.timesUsed}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Last used</span>
|
||||
<span>{formatDateTime(selectedItem.lastUsedAt)}</span>
|
||||
<div class="border-t p-4">
|
||||
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
|
||||
<div class="flex flex-col gap-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Name</span>
|
||||
<span>{selectedItem.name}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Content type</span>
|
||||
<span class="capitalize">Text</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Times used</span>
|
||||
<span>{selectedItem.timesUsed}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Last used</span>
|
||||
<span>{formatDateTime(selectedItem.lastUsedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActionBar>
|
||||
{#snippet primaryAction({ props })}
|
||||
<Button {...props} onclick={() => handlePaste(selectedItem)}>
|
||||
Paste <Kbd>⏎</Kbd>
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<ActionMenu>
|
||||
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
|
||||
<Trash class="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</ActionMenu>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
{#if selectedItem}
|
||||
<ActionBar>
|
||||
{#snippet primaryAction({ props })}
|
||||
<Button {...props} onclick={() => handlePaste(selectedItem)}>
|
||||
Paste <Kbd>⏎</Kbd>
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<ActionMenu>
|
||||
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
|
||||
<Trash class="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</ActionMenu>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</MainLayout>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import { focusManager } from '$lib/focus.svelte';
|
||||
import HeaderInput from '../HeaderInput.svelte';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import MainLayout from '../layout/MainLayout.svelte';
|
||||
|
||||
type Props = {
|
||||
plugins: PluginInfo[];
|
||||
|
@ -133,110 +134,120 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||
<header class="flex h-15 shrink-0 items-center border-b">
|
||||
<div class="relative flex w-full items-center">
|
||||
<HeaderInput
|
||||
placeholder={selectedQuicklinkForArgument
|
||||
? selectedQuicklinkForArgument.name
|
||||
: 'Search for apps and commands...'}
|
||||
bind:value={searchText}
|
||||
bind:ref={searchInputEl}
|
||||
onkeydown={handleKeyDown}
|
||||
autofocus
|
||||
/>
|
||||
<MainLayout>
|
||||
{#snippet header()}
|
||||
<header class="flex h-15 shrink-0 items-center border-b">
|
||||
<div class="relative flex w-full items-center">
|
||||
<HeaderInput
|
||||
placeholder={selectedQuicklinkForArgument
|
||||
? selectedQuicklinkForArgument.name
|
||||
: 'Search for apps and commands...'}
|
||||
bind:value={searchText}
|
||||
bind:ref={searchInputEl}
|
||||
onkeydown={handleKeyDown}
|
||||
autofocus
|
||||
/>
|
||||
|
||||
{#if selectedQuicklinkForArgument}
|
||||
<div class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center pl-4">
|
||||
<span class="text-lg whitespace-pre text-transparent"
|
||||
>{searchText || selectedQuicklinkForArgument.name}</span
|
||||
{#if selectedQuicklinkForArgument}
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center pl-4"
|
||||
>
|
||||
<span class="w-2"></span>
|
||||
<div class="pointer-events-auto">
|
||||
<div class="inline-grid items-center">
|
||||
<span
|
||||
class="invisible col-start-1 row-start-1 px-3 text-base whitespace-pre md:text-sm"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{quicklinkArgument || 'Query'}
|
||||
</span>
|
||||
<span class="text-lg whitespace-pre text-transparent"
|
||||
>{searchText || selectedQuicklinkForArgument.name}</span
|
||||
>
|
||||
<span class="w-2"></span>
|
||||
<div class="pointer-events-auto">
|
||||
<div class="inline-grid items-center">
|
||||
<span
|
||||
class="invisible col-start-1 row-start-1 px-3 text-base whitespace-pre md:text-sm"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{quicklinkArgument || 'Query'}
|
||||
</span>
|
||||
|
||||
<Input
|
||||
class="border-border col-start-1 row-start-1 h-7 w-full"
|
||||
placeholder="Query"
|
||||
bind:value={quicklinkArgument}
|
||||
bind:ref={argumentInputEl}
|
||||
onkeydown={handleArgumentKeydown}
|
||||
/>
|
||||
<Input
|
||||
class="border-border col-start-1 row-start-1 h-7 w-full"
|
||||
placeholder="Query"
|
||||
bind:value={quicklinkArgument}
|
||||
bind:ref={argumentInputEl}
|
||||
onkeydown={handleArgumentKeydown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grow overflow-y-auto">
|
||||
<BaseList
|
||||
items={displayItems.map((item) => ({ ...item, itemType: 'item' }))}
|
||||
onenter={actions.handleEnter}
|
||||
bind:selectedIndex
|
||||
bind:listElement
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick })}
|
||||
{#if item.type === 'calculator'}
|
||||
<Calculator
|
||||
searchText={item.data.value}
|
||||
mathResult={item.data.result}
|
||||
mathResultType={item.data.resultType}
|
||||
{isSelected}
|
||||
onSelect={onclick}
|
||||
/>
|
||||
{:else if item.type === 'plugin'}
|
||||
{@const assetsPath = path.dirname(item.data.pluginPath) + '/assets'}
|
||||
<ListItemBase
|
||||
title={item.data.title}
|
||||
subtitle={item.data.pluginTitle}
|
||||
icon={item.data.icon || 'app-window-16'}
|
||||
{assetsPath}
|
||||
{isSelected}
|
||||
{onclick}
|
||||
>
|
||||
{#snippet accessories()}
|
||||
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap"> Command </span>
|
||||
{/snippet}
|
||||
</ListItemBase>
|
||||
{:else if item.type === 'app'}
|
||||
<ListItemBase
|
||||
title={item.data.name}
|
||||
subtitle={item.data.comment}
|
||||
icon={item.data.icon_path ?? 'app-window-16'}
|
||||
{isSelected}
|
||||
{onclick}
|
||||
>
|
||||
{#snippet accessories()}
|
||||
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
|
||||
Application
|
||||
</span>
|
||||
{/snippet}
|
||||
</ListItemBase>
|
||||
{:else if item.type === 'quicklink'}
|
||||
<ListItemBase
|
||||
title={item.data.name}
|
||||
subtitle={item.data.link.replace(/\{argument\}/g, '...')}
|
||||
icon={item.data.icon ?? 'link-16'}
|
||||
{isSelected}
|
||||
{onclick}
|
||||
>
|
||||
{#snippet accessories()}
|
||||
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
|
||||
Quicklink
|
||||
</span>
|
||||
{/snippet}
|
||||
</ListItemBase>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{/snippet}
|
||||
|
||||
<CommandPaletteActionBar {selectedItem} {actions} {setSearchText} />
|
||||
</main>
|
||||
{#snippet content()}
|
||||
<div class="grow overflow-y-auto">
|
||||
<BaseList
|
||||
items={displayItems.map((item) => ({ ...item, itemType: 'item' }))}
|
||||
onenter={actions.handleEnter}
|
||||
bind:selectedIndex
|
||||
bind:listElement
|
||||
>
|
||||
{#snippet itemSnippet({ item, isSelected, onclick })}
|
||||
{#if item.type === 'calculator'}
|
||||
<Calculator
|
||||
searchText={item.data.value}
|
||||
mathResult={item.data.result}
|
||||
mathResultType={item.data.resultType}
|
||||
{isSelected}
|
||||
onSelect={onclick}
|
||||
/>
|
||||
{:else if item.type === 'plugin'}
|
||||
{@const assetsPath = path.dirname(item.data.pluginPath) + '/assets'}
|
||||
<ListItemBase
|
||||
title={item.data.title}
|
||||
subtitle={item.data.pluginTitle}
|
||||
icon={item.data.icon || 'app-window-16'}
|
||||
{assetsPath}
|
||||
{isSelected}
|
||||
{onclick}
|
||||
>
|
||||
{#snippet accessories()}
|
||||
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
|
||||
Command
|
||||
</span>
|
||||
{/snippet}
|
||||
</ListItemBase>
|
||||
{:else if item.type === 'app'}
|
||||
<ListItemBase
|
||||
title={item.data.name}
|
||||
subtitle={item.data.comment}
|
||||
icon={item.data.icon_path ?? 'app-window-16'}
|
||||
{isSelected}
|
||||
{onclick}
|
||||
>
|
||||
{#snippet accessories()}
|
||||
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
|
||||
Application
|
||||
</span>
|
||||
{/snippet}
|
||||
</ListItemBase>
|
||||
{:else if item.type === 'quicklink'}
|
||||
<ListItemBase
|
||||
title={item.data.name}
|
||||
subtitle={item.data.link.replace(/\{argument\}/g, '...')}
|
||||
icon={item.data.icon ?? 'link-16'}
|
||||
{isSelected}
|
||||
{onclick}
|
||||
>
|
||||
{#snippet accessories()}
|
||||
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
|
||||
Quicklink
|
||||
</span>
|
||||
{/snippet}
|
||||
</ListItemBase>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
<CommandPaletteActionBar {selectedItem} {actions} {setSearchText} />
|
||||
{/snippet}
|
||||
</MainLayout>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue