feat: implement quicklinks

This commit is contained in:
ByteAtATime 2025-06-21 20:01:19 -07:00
parent a0521b7b4d
commit 1ae8a571cb
No known key found for this signature in database
8 changed files with 576 additions and 63 deletions

View file

@ -55,4 +55,13 @@ impl std::fmt::Display for AppError {
} }
} }
impl std::error::Error for AppError {} impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Io(err) => Some(err),
AppError::Rusqlite(err) => Some(err),
AppError::Keyring(err) => Some(err),
_ => None,
}
}
}

View file

@ -8,9 +8,11 @@ mod error;
mod extensions; mod extensions;
mod filesystem; mod filesystem;
mod oauth; mod oauth;
mod quicklinks;
use crate::{app::App, cache::AppCache}; use crate::{app::App, cache::AppCache};
use browser_extension::WsState; use browser_extension::WsState;
use quicklinks::QuicklinkManager;
use selection::get_text; use selection::get_text;
use std::process::Command; use std::process::Command;
use std::thread; use std::thread;
@ -144,7 +146,12 @@ pub fn run() {
clipboard_history::history_delete_item, clipboard_history::history_delete_item,
clipboard_history::history_toggle_pin, clipboard_history::history_toggle_pin,
clipboard_history::history_clear_all, clipboard_history::history_clear_all,
clipboard_history::history_item_was_copied clipboard_history::history_item_was_copied,
quicklinks::create_quicklink,
quicklinks::list_quicklinks,
quicklinks::update_quicklink,
quicklinks::delete_quicklink,
quicklinks::execute_quicklink
]) ])
.setup(|app| { .setup(|app| {
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
@ -153,6 +160,10 @@ pub fn run() {
let app_handle_for_history = app.handle().clone(); let app_handle_for_history = app.handle().clone();
clipboard_history::init(app_handle_for_history); clipboard_history::init(app_handle_for_history);
let quicklink_manager = QuicklinkManager::new(app.handle().clone())?;
quicklink_manager.init_db()?;
app.manage(quicklink_manager);
setup_background_refresh(); setup_background_refresh();
setup_global_shortcut(app)?; setup_global_shortcut(app)?;

168
src-tauri/src/quicklinks.rs Normal file
View file

@ -0,0 +1,168 @@
use crate::error::AppError;
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection, Result as RusqliteResult};
use serde::Serialize;
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
use tauri_plugin_opener::{open_path, open_url};
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Quicklink {
id: i64,
name: String,
link: String,
application: Option<String>,
icon: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
pub struct QuicklinkManager {
db: Mutex<Connection>,
}
impl QuicklinkManager {
pub fn new(app_handle: AppHandle) -> Result<Self, AppError> {
let data_dir = app_handle
.path()
.app_local_data_dir()
.map_err(|_| AppError::DirectoryNotFound)?;
let db_path = data_dir.join("quicklinks.sqlite");
let db = Connection::open(db_path)?;
Ok(Self {
db: Mutex::new(db),
})
}
pub fn init_db(&self) -> RusqliteResult<()> {
let db = self.db.lock().unwrap();
db.execute(
"CREATE TABLE IF NOT EXISTS quicklinks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
link TEXT NOT NULL,
application TEXT,
icon TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)",
[],
)?;
Ok(())
}
fn create_quicklink(
&self,
name: String,
link: String,
application: Option<String>,
icon: Option<String>,
) -> Result<i64, AppError> {
let db = self.db.lock().unwrap();
let now = Utc::now().timestamp();
db.execute(
"INSERT INTO quicklinks (name, link, application, icon, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)",
params![name, link, application, icon, now, now],
)?;
Ok(db.last_insert_rowid())
}
fn list_quicklinks(&self) -> Result<Vec<Quicklink>, AppError> {
let db = self.db.lock().unwrap();
let mut stmt = db.prepare("SELECT id, name, link, application, icon, created_at, updated_at FROM quicklinks ORDER BY name ASC")?;
let quicklinks_iter = stmt.query_map([], |row| {
let created_at_ts: i64 = row.get(5)?;
let updated_at_ts: i64 = row.get(6)?;
Ok(Quicklink {
id: row.get(0)?,
name: row.get(1)?,
link: row.get(2)?,
application: row.get(3)?,
icon: row.get(4)?,
created_at: DateTime::from_timestamp(created_at_ts, 0).unwrap_or_default(),
updated_at: DateTime::from_timestamp(updated_at_ts, 0).unwrap_or_default(),
})
})?;
quicklinks_iter.collect::<Result<Vec<_>, _>>().map_err(|e| e.into())
}
fn update_quicklink(
&self,
id: i64,
name: String,
link: String,
application: Option<String>,
icon: Option<String>,
) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let now = Utc::now().timestamp();
db.execute(
"UPDATE quicklinks SET name = ?, link = ?, application = ?, icon = ?, updated_at = ?
WHERE id = ?",
params![name, link, application, icon, now, id],
)?;
Ok(())
}
fn delete_quicklink(&self, id: i64) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
db.execute("DELETE FROM quicklinks WHERE id = ?", params![id])?;
Ok(())
}
}
#[tauri::command]
pub fn create_quicklink(
app: AppHandle,
name: String,
link: String,
application: Option<String>,
icon: Option<String>,
) -> Result<i64, String> {
app.state::<QuicklinkManager>()
.create_quicklink(name, link, application, icon)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn list_quicklinks(app: AppHandle) -> Result<Vec<Quicklink>, String> {
app.state::<QuicklinkManager>()
.list_quicklinks()
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn update_quicklink(
app: AppHandle,
id: i64,
name: String,
link: String,
application: Option<String>,
icon: Option<String>,
) -> Result<(), String> {
app.state::<QuicklinkManager>()
.update_quicklink(id, name, link, application, icon)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn delete_quicklink(app: AppHandle, id: i64) -> Result<(), String> {
app.state::<QuicklinkManager>()
.delete_quicklink(id)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn execute_quicklink(link: String, application: Option<String>) -> Result<(), String> {
if let Some(app_name) = application {
open_path(link, Some(app_name)).map_err(|e| e.to_string())
} else if link.starts_with("http://") || link.starts_with("https://") {
open_url(link, None::<String>).map_err(|e| e.to_string())
} else {
open_path(link, None::<String>).map_err(|e| e.to_string())
}
}

View file

@ -7,12 +7,18 @@
itemSnippet: Snippet<[{ item: T; isSelected: boolean; onclick: () => void }]>; itemSnippet: Snippet<[{ item: T; isSelected: boolean; onclick: () => void }]>;
autofocus?: boolean; autofocus?: boolean;
onenter: (item: T) => void; onenter: (item: T) => void;
selectedIndex?: number;
listElement?: HTMLElement | null;
}; };
let { items, itemSnippet, autofocus = false, onenter }: Props = $props(); let {
items,
let selectedIndex = $state(0); itemSnippet,
let listElement: HTMLElement | null = $state(null); autofocus = false,
onenter,
selectedIndex = $bindable(0),
listElement = $bindable()
}: Props = $props();
$effect(() => { $effect(() => {
if (selectedIndex >= items.length) { if (selectedIndex >= items.length) {

View file

@ -9,68 +9,57 @@
import path from 'path'; import path from 'path';
import { create, all } from 'mathjs'; import { create, all } from 'mathjs';
import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import type { Quicklink } from '$lib/quicklinks.svelte';
import { tick } from 'svelte';
type Props = { type Props = {
plugins: PluginInfo[]; plugins: PluginInfo[];
onRunPlugin: (plugin: PluginInfo) => void; onRunPlugin: (plugin: PluginInfo) => void;
installedApps?: any[]; installedApps?: any[];
quicklinks?: Quicklink[];
}; };
type UnifiedItem = type UnifiedItem =
| { type: 'calculator'; id: 'calculator'; value: string; result: string; resultType: string } | { type: 'calculator'; id: 'calculator'; value: string; result: string; resultType: string }
| { type: 'plugin'; id: string; data: PluginInfo } | { type: 'plugin'; id: string; data: PluginInfo }
| { type: 'app'; id: string; data: any }; | { type: 'app'; id: string; data: any }
| { type: 'quicklink'; id: number; data: Quicklink };
let { plugins, onRunPlugin, installedApps = [] }: Props = $props(); let { plugins, onRunPlugin, installedApps = [], quicklinks = [] }: Props = $props();
let searchText = $state(''); let searchText = $state('');
let quicklinkArgument = $state('');
let selectedIndex = $state(0);
let listElement: HTMLElement | null = $state(null);
let searchInputEl: HTMLInputElement | null = $state(null);
let argumentInputEl: HTMLInputElement | null = $state(null);
let selectedQuicklinkForArgument: Quicklink | null = $state(null);
const math = create(all); const math = create(all);
const calculatorResult = $derived.by(() => { const calculatorResult = $derived.by(() => {
if (!searchText.trim()) { if (!searchText.trim() || selectedQuicklinkForArgument) {
return null; return null;
} }
try { try {
const result = math.evaluate(searchText.trim()); const result = math.evaluate(searchText.trim());
if (typeof result === 'function' || typeof result === 'undefined') return null;
if (typeof result === 'function' || typeof result === 'undefined') {
return null;
}
let resultString = math.format(result, { precision: 14 }); let resultString = math.format(result, { precision: 14 });
if (resultString === searchText.trim()) return null;
if (resultString === searchText.trim()) { return { value: resultString, type: math.typeOf(result) };
return null;
}
return {
value: resultString,
type: math.typeOf(result)
};
} catch (error) { } catch (error) {
return null; return null;
} }
}); });
const pluginFuse = $derived( const pluginFuse = $derived(
new Fuse(plugins, { new Fuse(plugins, { keys: ['title', 'description', 'pluginName'], threshold: 0.4 })
keys: [
{ name: 'title', weight: 0.7 },
{ name: 'description', weight: 0.2 },
{ name: 'pluginName', weight: 0.1 }
],
threshold: 0.4
})
); );
const appFuse = $derived( const appFuse = $derived(
new Fuse(installedApps, { new Fuse(installedApps, { keys: ['name', 'comment', 'exec'], threshold: 0.4 })
keys: ['name', 'comment', 'exec'],
threshold: 0.4
})
); );
const quicklinkFuse = $derived(new Fuse(quicklinks, { keys: ['name', 'link'], threshold: 0.4 }));
const displayItems = $derived.by(() => { const displayItems = $derived.by(() => {
const items: UnifiedItem[] = []; const items: UnifiedItem[] = [];
@ -85,27 +74,54 @@
}); });
} }
const filteredPlugins = searchText const filterAndMap = (
? pluginFuse.search(searchText) data: any[],
: plugins.map((p) => ({ item: p })); fuse: Fuse<any>,
const uniquePlugins = [...new Map(filteredPlugins.map((p) => [p.item.pluginPath, p])).values()]; type: 'plugin' | 'app' | 'quicklink',
idKey: string
) => {
const filtered = searchText ? fuse.search(searchText) : data.map((item) => ({ item }));
const unique = [...new Map(filtered.map((res) => [res.item[idKey], res.item])).values()];
return unique.map((item) => ({ type, id: item[idKey], data: item }));
};
const filteredApps = searchText items.push(...filterAndMap(plugins, pluginFuse, 'plugin', 'pluginPath'));
? appFuse.search(searchText) items.push(...filterAndMap(installedApps, appFuse, 'app', 'exec'));
: installedApps.map((a) => ({ item: a })); items.push(...filterAndMap(quicklinks, quicklinkFuse, 'quicklink', 'id'));
const uniqueApps = [...new Map(filteredApps.map((a) => [a.item.exec, a])).values()];
items.push( return items as UnifiedItem[];
...uniquePlugins.map(
(p) => ({ type: 'plugin', id: p.item.pluginPath, data: p.item }) as const
)
);
items.push(...uniqueApps.map((a) => ({ type: 'app', id: a.item.exec, data: a.item }) as const));
return items;
}); });
function handleEnter(item: UnifiedItem) { $effect(() => {
const selectedItem = displayItems[selectedIndex];
if (selectedItem?.type === 'quicklink') {
selectedQuicklinkForArgument = selectedItem.data;
searchInputEl?.focus();
} else {
selectedQuicklinkForArgument = null;
}
});
function resetState() {
searchText = '';
quicklinkArgument = '';
selectedIndex = 0;
selectedQuicklinkForArgument = null;
tick().then(() => searchInputEl?.focus());
}
async function executeQuicklink(quicklink: Quicklink, argument?: string) {
const finalLink = argument
? quicklink.link.replace(/\{argument\}/g, encodeURIComponent(argument))
: quicklink.link.replace(/\{argument\}/g, '');
await invoke('execute_quicklink', {
link: finalLink,
application: quicklink.application
});
resetState();
}
async function handleEnter(item: UnifiedItem) {
switch (item.type) { switch (item.type) {
case 'calculator': case 'calculator':
writeText(item.result); writeText(item.result);
@ -118,21 +134,78 @@
invoke('launch_app', { exec: item.data.exec }).catch(console.error); invoke('launch_app', { exec: item.data.exec }).catch(console.error);
} }
break; break;
case 'quicklink':
if (item.data.link.includes('{argument}')) {
await tick();
argumentInputEl?.focus();
} else {
executeQuicklink(item.data);
}
break;
}
}
async function handleArgumentKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
if (selectedQuicklinkForArgument) {
await executeQuicklink(selectedQuicklinkForArgument, quicklinkArgument);
}
} else if (e.key === 'Escape' || (e.key === 'Backspace' && quicklinkArgument === '')) {
e.preventDefault();
quicklinkArgument = '';
await tick();
searchInputEl?.focus();
} }
} }
</script> </script>
<main class="bg-background text-foreground flex h-screen flex-col"> <main class="bg-background text-foreground flex h-screen flex-col">
<header class="flex h-12 shrink-0 items-center border-b px-2"> <header class="flex h-12 shrink-0 items-center border-b px-2">
<div class="relative flex w-full items-center">
<Input <Input
class="rounded-none border-none !bg-transparent pr-0" class="w-full rounded-none border-none !bg-transparent pr-0 text-base"
placeholder="Search for extensions and commands..." placeholder={selectedQuicklinkForArgument ? '' : 'Search for extensions and commands...'}
bind:value={searchText} bind:value={searchText}
bind:ref={searchInputEl}
autofocus autofocus
/> />
{#if selectedQuicklinkForArgument}
<div class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center">
<span class="whitespace-pre text-transparent">{searchText}</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="col-start-1 row-start-1 h-7 w-full"
placeholder="Query"
bind:value={quicklinkArgument}
bind:ref={argumentInputEl}
onkeydown={handleArgumentKeydown}
/>
</div>
</div>
</div>
{/if}
</div>
</header> </header>
<div class="grow overflow-y-auto"> <div class="grow overflow-y-auto">
<BaseList items={displayItems} onenter={handleEnter}> <BaseList
items={displayItems}
onenter={handleEnter}
bind:selectedIndex
bind:listElement
autofocus={!selectedQuicklinkForArgument}
>
{#snippet itemSnippet({ item, isSelected, onclick })} {#snippet itemSnippet({ item, isSelected, onclick })}
{#if item.type === 'calculator'} {#if item.type === 'calculator'}
<Calculator <Calculator
@ -170,6 +243,20 @@
</span> </span>
{/snippet} {/snippet}
</ListItemBase> </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} {/if}
{/snippet} {/snippet}
</BaseList> </BaseList>

View file

@ -0,0 +1,134 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import * as Select from '$lib/components/ui/select';
import Icon from '$lib/components/Icon.svelte';
import { ArrowLeft, Save } from '@lucide/svelte';
import { quicklinksStore, type Quicklink } from '$lib/quicklinks.svelte';
type AppInfo = {
name: string;
exec: string;
icon_path?: string;
};
type Props = {
quicklink?: Quicklink;
onBack: () => void;
onSave: () => void;
};
let { quicklink, onBack, onSave }: Props = $props();
let name = $state(quicklink?.name ?? '');
let link = $state(quicklink?.link ?? '');
let application = $state(quicklink?.application ?? 'Default');
let icon = $state(quicklink?.icon ?? 'link-16');
let applications = $state<AppInfo[]>([]);
let error = $state('');
onMount(async () => {
try {
applications = (await invoke('get_installed_apps')) as AppInfo[];
} catch (e) {
console.error('Failed to fetch installed apps:', e);
}
});
async function handleSave() {
if (!name.trim()) {
error = 'Name cannot be empty';
return;
}
error = '';
const data = {
name,
link,
application: application === 'Default' ? undefined : application,
icon: icon === 'link-16' ? undefined : icon
};
try {
if (quicklink) {
await quicklinksStore.update(quicklink.id, data);
} else {
await quicklinksStore.create(data);
}
onSave();
} catch (e) {
error = e instanceof Error ? e.message : String(e);
}
}
</script>
<main class="bg-background text-foreground flex h-screen flex-col">
<header class="flex h-12 shrink-0 items-center border-b px-2">
<Button variant="ghost" size="icon" onclick={onBack}>
<ArrowLeft class="size-5" />
</Button>
<div class="flex items-center gap-3 px-2">
<Icon icon="link-16" class="size-6" />
<h1 class="text-lg font-medium">{quicklink ? 'Edit Quicklink' : 'Create Quicklink'}</h1>
</div>
</header>
<div class="grow overflow-y-auto p-6">
<div class="mx-auto max-w-xl space-y-6">
<div class="grid grid-cols-[120px_1fr] items-center gap-4">
<label for="name" class="text-right text-sm text-gray-400">Name</label>
<Input id="name" placeholder="Quicklink name" bind:value={name} />
</div>
<div class="grid grid-cols-[120px_1fr] items-start gap-4">
<label for="link" class="pt-2 text-right text-sm text-gray-400">Link</label>
<div>
<Textarea
id="link"
placeholder="https://google.com/search?q={'{argument}'}"
bind:value={link}
/>
<p class="text-muted-foreground mt-1 text-xs">
Include <span class="text-foreground font-mono">{'{argument}'}</span> for context like the
selected or copied text in the link.
</p>
</div>
</div>
<div class="grid grid-cols-[120px_1fr] items-center gap-4">
<label for="open-with" class="text-right text-sm text-gray-400">Open With</label>
<Select.Root bind:value={application} type="single">
<Select.Trigger id="open-with" class="w-full">
{@const selectedApp = applications.find((a) => a.exec === application)}
{selectedApp?.name ?? 'Default'}
</Select.Trigger>
<Select.Content>
<Select.Item value="Default">Default</Select.Item>
{#each applications as app (app.exec)}
<Select.Item value={app.exec}>{app.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="grid grid-cols-[120px_1fr] items-center gap-4">
<label for="icon" class="text-right text-sm text-gray-400">Icon</label>
<Input id="icon" placeholder="link-16" bind:value={icon} />
</div>
{#if error}
<p class="text-center text-red-500">{error}</p>
{/if}
</div>
</div>
<footer class="bg-card flex h-12 shrink-0 items-center justify-between border-t px-4">
<div class="flex items-center gap-2">
<Icon icon="link-16" class="size-5" />
<span class="text-sm font-medium">Create Quicklink</span>
</div>
<Button onclick={handleSave}><Save class="mr-2 size-4" /> Save Quicklink</Button>
</footer>
</main>

View file

@ -0,0 +1,70 @@
import { invoke } from '@tauri-apps/api/core';
export type Quicklink = {
id: number;
name: string;
link: string;
application: string | null;
icon: string | null;
createdAt: string;
updatedAt: string;
};
class QuicklinksStore {
quicklinks = $state<Quicklink[]>([]);
isLoading = $state(true);
error = $state<string | null>(null);
constructor() {
this.fetchQuicklinks();
}
async fetchQuicklinks() {
this.isLoading = true;
this.error = null;
try {
const result = await invoke<Quicklink[]>('list_quicklinks');
this.quicklinks = result;
} catch (e) {
this.error = e instanceof Error ? e.message : String(e);
console.error('Failed to fetch quicklinks:', e);
} finally {
this.isLoading = false;
}
}
async create(data: { name: string; link: string; application?: string; icon?: string }) {
try {
await invoke('create_quicklink', data);
await this.fetchQuicklinks();
} catch (e) {
console.error('Failed to create quicklink:', e);
throw e;
}
}
async update(
id: number,
data: { name: string; link: string; application?: string; icon?: string }
) {
try {
await invoke('update_quicklink', { id, ...data });
await this.fetchQuicklinks();
} catch (e) {
console.error('Failed to update quicklink:', e);
throw e;
}
}
async delete(id: number) {
try {
await invoke('delete_quicklink', { id });
await this.fetchQuicklinks();
} catch (e) {
console.error('Failed to delete quicklink:', e);
throw e;
}
}
}
export const quicklinksStore = new QuicklinksStore();

View file

@ -13,13 +13,16 @@
import OAuthView from '$lib/components/OAuthView.svelte'; import OAuthView from '$lib/components/OAuthView.svelte';
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from '@tauri-apps/plugin-opener';
import ClipboardHistoryView from '$lib/components/ClipboardHistoryView.svelte'; import ClipboardHistoryView from '$lib/components/ClipboardHistoryView.svelte';
import QuicklinkForm from '$lib/components/QuicklinkForm.svelte';
import { quicklinksStore } from '$lib/quicklinks.svelte';
type ViewState = type ViewState =
| 'plugin-list' | 'plugin-list'
| 'plugin-running' | 'plugin-running'
| 'settings' | 'settings'
| 'extensions-store' | 'extensions-store'
| 'clipboard-history'; | 'clipboard-history'
| 'create-quicklink';
let viewState = $state<ViewState>('plugin-list'); let viewState = $state<ViewState>('plugin-list');
let installedApps = $state<any[]>([]); let installedApps = $state<any[]>([]);
@ -49,8 +52,26 @@
mode: 'view' mode: 'view'
}; };
const createQuicklinkPlugin: PluginInfo = {
title: 'Create Quicklink',
description: 'Create a new Quicklink',
pluginTitle: 'Quicklinks',
pluginName: 'Quicklinks',
commandName: 'create-quicklink',
pluginPath: 'builtin:create-quicklink',
icon: 'link-16',
preferences: [],
mode: 'view'
};
const { pluginList, currentPreferences } = $derived(uiStore); const { pluginList, currentPreferences } = $derived(uiStore);
const allPlugins = $derived([...pluginList, storePlugin, clipboardHistoryPlugin]); const { quicklinks } = $derived(quicklinksStore);
const allPlugins = $derived([
...pluginList,
storePlugin,
clipboardHistoryPlugin,
createQuicklinkPlugin
]);
$effect(() => { $effect(() => {
untrack(() => { untrack(() => {
@ -59,6 +80,7 @@
uiStore.setCurrentRunningPlugin(null); uiStore.setCurrentRunningPlugin(null);
}); });
sidecarService.start(); sidecarService.start();
quicklinksStore.fetchQuicklinks();
return () => sidecarService.stop(); return () => sidecarService.stop();
}); });
}); });
@ -148,6 +170,10 @@
viewState = 'clipboard-history'; viewState = 'clipboard-history';
return; return;
} }
if (plugin.pluginPath === 'builtin:create-quicklink') {
viewState = 'create-quicklink';
return;
}
uiStore.setCurrentRunningPlugin(plugin); uiStore.setCurrentRunningPlugin(plugin);
sidecarService.dispatchEvent('run-plugin', { sidecarService.dispatchEvent('run-plugin', {
@ -217,7 +243,7 @@
{/if} {/if}
{#if viewState === 'plugin-list'} {#if viewState === 'plugin-list'}
<CommandPalette plugins={allPlugins} onRunPlugin={handleRunPlugin} {installedApps} /> <CommandPalette plugins={allPlugins} onRunPlugin={handleRunPlugin} {installedApps} {quicklinks} />
{:else if viewState === 'settings'} {:else if viewState === 'settings'}
<SettingsView <SettingsView
plugins={pluginList} plugins={pluginList}
@ -237,4 +263,6 @@
/> />
{:else if viewState === 'clipboard-history'} {:else if viewState === 'clipboard-history'}
<ClipboardHistoryView onBack={() => (viewState = 'plugin-list')} /> <ClipboardHistoryView onBack={() => (viewState = 'plugin-list')} />
{:else if viewState === 'create-quicklink'}
<QuicklinkForm onBack={handleBackToPluginList} onSave={handleBackToPluginList} />
{/if} {/if}