mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-09-12 08:56:23 +00:00
feat: implement quicklinks
This commit is contained in:
parent
a0521b7b4d
commit
1ae8a571cb
8 changed files with 576 additions and 63 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,9 +8,11 @@ mod error;
|
|||
mod extensions;
|
||||
mod filesystem;
|
||||
mod oauth;
|
||||
mod quicklinks;
|
||||
|
||||
use crate::{app::App, cache::AppCache};
|
||||
use browser_extension::WsState;
|
||||
use quicklinks::QuicklinkManager;
|
||||
use selection::get_text;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
|
@ -144,7 +146,12 @@ pub fn run() {
|
|||
clipboard_history::history_delete_item,
|
||||
clipboard_history::history_toggle_pin,
|
||||
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| {
|
||||
let app_handle = app.handle().clone();
|
||||
|
@ -153,6 +160,10 @@ pub fn run() {
|
|||
let app_handle_for_history = app.handle().clone();
|
||||
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_global_shortcut(app)?;
|
||||
|
||||
|
|
168
src-tauri/src/quicklinks.rs
Normal file
168
src-tauri/src/quicklinks.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -7,12 +7,18 @@
|
|||
itemSnippet: Snippet<[{ item: T; isSelected: boolean; onclick: () => void }]>;
|
||||
autofocus?: boolean;
|
||||
onenter: (item: T) => void;
|
||||
selectedIndex?: number;
|
||||
listElement?: HTMLElement | null;
|
||||
};
|
||||
|
||||
let { items, itemSnippet, autofocus = false, onenter }: Props = $props();
|
||||
|
||||
let selectedIndex = $state(0);
|
||||
let listElement: HTMLElement | null = $state(null);
|
||||
let {
|
||||
items,
|
||||
itemSnippet,
|
||||
autofocus = false,
|
||||
onenter,
|
||||
selectedIndex = $bindable(0),
|
||||
listElement = $bindable()
|
||||
}: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (selectedIndex >= items.length) {
|
||||
|
|
|
@ -9,68 +9,57 @@
|
|||
import path from 'path';
|
||||
import { create, all } from 'mathjs';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import type { Quicklink } from '$lib/quicklinks.svelte';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
plugins: PluginInfo[];
|
||||
onRunPlugin: (plugin: PluginInfo) => void;
|
||||
installedApps?: any[];
|
||||
quicklinks?: Quicklink[];
|
||||
};
|
||||
|
||||
type UnifiedItem =
|
||||
| { type: 'calculator'; id: 'calculator'; value: string; result: string; resultType: string }
|
||||
| { 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 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 calculatorResult = $derived.by(() => {
|
||||
if (!searchText.trim()) {
|
||||
if (!searchText.trim() || selectedQuicklinkForArgument) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
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 });
|
||||
|
||||
if (resultString === searchText.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
value: resultString,
|
||||
type: math.typeOf(result)
|
||||
};
|
||||
if (resultString === searchText.trim()) return null;
|
||||
return { value: resultString, type: math.typeOf(result) };
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const pluginFuse = $derived(
|
||||
new Fuse(plugins, {
|
||||
keys: [
|
||||
{ name: 'title', weight: 0.7 },
|
||||
{ name: 'description', weight: 0.2 },
|
||||
{ name: 'pluginName', weight: 0.1 }
|
||||
],
|
||||
threshold: 0.4
|
||||
})
|
||||
new Fuse(plugins, { keys: ['title', 'description', 'pluginName'], threshold: 0.4 })
|
||||
);
|
||||
|
||||
const appFuse = $derived(
|
||||
new Fuse(installedApps, {
|
||||
keys: ['name', 'comment', 'exec'],
|
||||
threshold: 0.4
|
||||
})
|
||||
new Fuse(installedApps, { keys: ['name', 'comment', 'exec'], threshold: 0.4 })
|
||||
);
|
||||
const quicklinkFuse = $derived(new Fuse(quicklinks, { keys: ['name', 'link'], threshold: 0.4 }));
|
||||
|
||||
const displayItems = $derived.by(() => {
|
||||
const items: UnifiedItem[] = [];
|
||||
|
@ -85,27 +74,54 @@
|
|||
});
|
||||
}
|
||||
|
||||
const filteredPlugins = searchText
|
||||
? pluginFuse.search(searchText)
|
||||
: plugins.map((p) => ({ item: p }));
|
||||
const uniquePlugins = [...new Map(filteredPlugins.map((p) => [p.item.pluginPath, p])).values()];
|
||||
const filterAndMap = (
|
||||
data: any[],
|
||||
fuse: Fuse<any>,
|
||||
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
|
||||
? appFuse.search(searchText)
|
||||
: installedApps.map((a) => ({ item: a }));
|
||||
const uniqueApps = [...new Map(filteredApps.map((a) => [a.item.exec, a])).values()];
|
||||
items.push(...filterAndMap(plugins, pluginFuse, 'plugin', 'pluginPath'));
|
||||
items.push(...filterAndMap(installedApps, appFuse, 'app', 'exec'));
|
||||
items.push(...filterAndMap(quicklinks, quicklinkFuse, 'quicklink', 'id'));
|
||||
|
||||
items.push(
|
||||
...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;
|
||||
return items as UnifiedItem[];
|
||||
});
|
||||
|
||||
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) {
|
||||
case 'calculator':
|
||||
writeText(item.result);
|
||||
|
@ -118,21 +134,78 @@
|
|||
invoke('launch_app', { exec: item.data.exec }).catch(console.error);
|
||||
}
|
||||
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>
|
||||
|
||||
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||
<header class="flex h-12 shrink-0 items-center border-b px-2">
|
||||
<div class="relative flex w-full items-center">
|
||||
<Input
|
||||
class="rounded-none border-none !bg-transparent pr-0"
|
||||
placeholder="Search for extensions and commands..."
|
||||
class="w-full rounded-none border-none !bg-transparent pr-0 text-base"
|
||||
placeholder={selectedQuicklinkForArgument ? '' : 'Search for extensions and commands...'}
|
||||
bind:value={searchText}
|
||||
bind:ref={searchInputEl}
|
||||
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>
|
||||
|
||||
<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 })}
|
||||
{#if item.type === 'calculator'}
|
||||
<Calculator
|
||||
|
@ -170,6 +243,20 @@
|
|||
</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>
|
||||
|
|
134
src/lib/components/QuicklinkForm.svelte
Normal file
134
src/lib/components/QuicklinkForm.svelte
Normal 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>
|
70
src/lib/quicklinks.svelte.ts
Normal file
70
src/lib/quicklinks.svelte.ts
Normal 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();
|
|
@ -13,13 +13,16 @@
|
|||
import OAuthView from '$lib/components/OAuthView.svelte';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import ClipboardHistoryView from '$lib/components/ClipboardHistoryView.svelte';
|
||||
import QuicklinkForm from '$lib/components/QuicklinkForm.svelte';
|
||||
import { quicklinksStore } from '$lib/quicklinks.svelte';
|
||||
|
||||
type ViewState =
|
||||
| 'plugin-list'
|
||||
| 'plugin-running'
|
||||
| 'settings'
|
||||
| 'extensions-store'
|
||||
| 'clipboard-history';
|
||||
| 'clipboard-history'
|
||||
| 'create-quicklink';
|
||||
|
||||
let viewState = $state<ViewState>('plugin-list');
|
||||
let installedApps = $state<any[]>([]);
|
||||
|
@ -49,8 +52,26 @@
|
|||
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 allPlugins = $derived([...pluginList, storePlugin, clipboardHistoryPlugin]);
|
||||
const { quicklinks } = $derived(quicklinksStore);
|
||||
const allPlugins = $derived([
|
||||
...pluginList,
|
||||
storePlugin,
|
||||
clipboardHistoryPlugin,
|
||||
createQuicklinkPlugin
|
||||
]);
|
||||
|
||||
$effect(() => {
|
||||
untrack(() => {
|
||||
|
@ -59,6 +80,7 @@
|
|||
uiStore.setCurrentRunningPlugin(null);
|
||||
});
|
||||
sidecarService.start();
|
||||
quicklinksStore.fetchQuicklinks();
|
||||
return () => sidecarService.stop();
|
||||
});
|
||||
});
|
||||
|
@ -148,6 +170,10 @@
|
|||
viewState = 'clipboard-history';
|
||||
return;
|
||||
}
|
||||
if (plugin.pluginPath === 'builtin:create-quicklink') {
|
||||
viewState = 'create-quicklink';
|
||||
return;
|
||||
}
|
||||
|
||||
uiStore.setCurrentRunningPlugin(plugin);
|
||||
sidecarService.dispatchEvent('run-plugin', {
|
||||
|
@ -217,7 +243,7 @@
|
|||
{/if}
|
||||
|
||||
{#if viewState === 'plugin-list'}
|
||||
<CommandPalette plugins={allPlugins} onRunPlugin={handleRunPlugin} {installedApps} />
|
||||
<CommandPalette plugins={allPlugins} onRunPlugin={handleRunPlugin} {installedApps} {quicklinks} />
|
||||
{:else if viewState === 'settings'}
|
||||
<SettingsView
|
||||
plugins={pluginList}
|
||||
|
@ -237,4 +263,6 @@
|
|||
/>
|
||||
{:else if viewState === 'clipboard-history'}
|
||||
<ClipboardHistoryView onBack={() => (viewState = 'plugin-list')} />
|
||||
{:else if viewState === 'create-quicklink'}
|
||||
<QuicklinkForm onBack={handleBackToPluginList} onSave={handleBackToPluginList} />
|
||||
{/if}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue