mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 03:07:23 +00:00
feat(snippets): implement Search Snippets command
This commit implements the "Search Snippets" feature, providing a dedicated user interface accessible from the main command palette. The new view allows users to search their snippets by name, keyword, or content and see details like usage statistics. This change adds the necessary backend logic for searching and tracking usage, and a new frontend component.
This commit is contained in:
parent
59174dddca
commit
e32cc6223d
8 changed files with 439 additions and 63 deletions
|
@ -13,7 +13,7 @@ mod quicklinks;
|
|||
mod snippets;
|
||||
mod system;
|
||||
|
||||
use crate::snippets::input_manager::EvdevInputManager;
|
||||
use crate::snippets::input_manager::{EvdevInputManager, InputManager};
|
||||
use crate::{app::App, cache::AppCache};
|
||||
use browser_extension::WsState;
|
||||
use frecency::FrecencyManager;
|
||||
|
@ -162,7 +162,8 @@ fn setup_input_listener(app: &tauri::AppHandle) {
|
|||
let snippet_manager_arc = Arc::new(snippet_manager);
|
||||
|
||||
let input_manager = EvdevInputManager::new().unwrap();
|
||||
let input_manager_arc = Arc::new(input_manager);
|
||||
let input_manager_arc: Arc<dyn InputManager> = Arc::new(input_manager);
|
||||
app.manage(input_manager_arc.clone());
|
||||
|
||||
let engine = ExpansionEngine::new(snippet_manager_arc, input_manager_arc);
|
||||
thread::spawn(move || {
|
||||
|
@ -242,7 +243,9 @@ pub fn run() {
|
|||
snippets::list_snippets,
|
||||
snippets::update_snippet,
|
||||
snippets::delete_snippet,
|
||||
snippets::import_snippets
|
||||
snippets::import_snippets,
|
||||
snippets::paste_snippet_content,
|
||||
snippets::snippet_was_used
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
|
@ -270,4 +273,4 @@ pub fn run() {
|
|||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
}
|
|
@ -8,9 +8,9 @@ use std::thread;
|
|||
|
||||
const BUFFER_SIZE: usize = 30;
|
||||
|
||||
struct ResolvedSnippet {
|
||||
content: String,
|
||||
cursor_pos: Option<usize>,
|
||||
pub struct ResolvedSnippet {
|
||||
pub content: String,
|
||||
pub cursor_pos: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct ExpansionEngine {
|
||||
|
@ -64,7 +64,7 @@ impl ExpansionEngine {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok(snippets) = self.snippet_manager.list_snippets() {
|
||||
if let Ok(snippets) = self.snippet_manager.list_snippets(None) {
|
||||
for snippet in snippets {
|
||||
if buffer.ends_with(&snippet.keyword) {
|
||||
let (keyword, content) = (snippet.keyword.clone(), snippet.content.clone());
|
||||
|
@ -76,56 +76,13 @@ impl ExpansionEngine {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_and_resolve_placeholders(&self, raw_content: &str) -> ResolvedSnippet {
|
||||
let mut resolved_content = String::with_capacity(raw_content.len());
|
||||
let mut cursor_pos: Option<usize> = None;
|
||||
let mut last_end = 0;
|
||||
|
||||
for (start, _) in raw_content.match_indices('{') {
|
||||
if start < last_end {
|
||||
continue;
|
||||
}
|
||||
if let Some(end) = raw_content[start..].find('}') {
|
||||
let placeholder = &raw_content[start + 1..start + end];
|
||||
|
||||
resolved_content.push_str(&raw_content[last_end..start]);
|
||||
|
||||
let replacement = match placeholder {
|
||||
"cursor" => {
|
||||
if cursor_pos.is_none() {
|
||||
cursor_pos = Some(resolved_content.chars().count());
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
"clipboard" => Clipboard::new()
|
||||
.ok()
|
||||
.and_then(|mut c| c.get_text().ok())
|
||||
.unwrap_or_default(),
|
||||
"date" => Local::now().format("%d %b %Y").to_string(),
|
||||
"time" => Local::now().format("%H:%M").to_string(),
|
||||
"datetime" => Local::now().format("%d %b %Y at %H:%M").to_string(),
|
||||
"day" => Local::now().format("%A").to_string(),
|
||||
_ => raw_content[start..start + end + 1].to_string(),
|
||||
};
|
||||
resolved_content.push_str(&replacement);
|
||||
last_end = start + end + 1;
|
||||
}
|
||||
}
|
||||
resolved_content.push_str(&raw_content[last_end..]);
|
||||
|
||||
ResolvedSnippet {
|
||||
content: resolved_content,
|
||||
cursor_pos,
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_snippet(&self, keyword: &str, content: &str) {
|
||||
let mut backspaces = String::new();
|
||||
for _ in 0..keyword.len() {
|
||||
backspaces.push('\u{8}');
|
||||
}
|
||||
|
||||
let resolved = self.parse_and_resolve_placeholders(content);
|
||||
let resolved = parse_and_resolve_placeholders(content);
|
||||
let content_to_paste = resolved.content;
|
||||
|
||||
let chars_to_move_left = if let Some(pos) = resolved.cursor_pos {
|
||||
|
@ -159,3 +116,46 @@ impl ExpansionEngine {
|
|||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_and_resolve_placeholders(raw_content: &str) -> ResolvedSnippet {
|
||||
let mut resolved_content = String::with_capacity(raw_content.len());
|
||||
let mut cursor_pos: Option<usize> = None;
|
||||
let mut last_end = 0;
|
||||
|
||||
for (start, _) in raw_content.match_indices('{') {
|
||||
if start < last_end {
|
||||
continue;
|
||||
}
|
||||
if let Some(end) = raw_content[start..].find('}') {
|
||||
let placeholder = &raw_content[start + 1..start + end];
|
||||
|
||||
resolved_content.push_str(&raw_content[last_end..start]);
|
||||
|
||||
let replacement = match placeholder {
|
||||
"cursor" => {
|
||||
if cursor_pos.is_none() {
|
||||
cursor_pos = Some(resolved_content.chars().count());
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
"clipboard" => Clipboard::new()
|
||||
.ok()
|
||||
.and_then(|mut c| c.get_text().ok())
|
||||
.unwrap_or_default(),
|
||||
"date" => Local::now().format("%d %b %Y").to_string(),
|
||||
"time" => Local::now().format("%H:%M").to_string(),
|
||||
"datetime" => Local::now().format("%d %b %Y at %H:%M").to_string(),
|
||||
"day" => Local::now().format("%A").to_string(),
|
||||
_ => raw_content[start..start + end + 1].to_string(),
|
||||
};
|
||||
resolved_content.push_str(&replacement);
|
||||
last_end = start + end + 1;
|
||||
}
|
||||
}
|
||||
resolved_content.push_str(&raw_content[last_end..]);
|
||||
|
||||
ResolvedSnippet {
|
||||
content: resolved_content,
|
||||
cursor_pos,
|
||||
}
|
||||
}
|
|
@ -36,6 +36,25 @@ impl SnippetManager {
|
|||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
let mut stmt = db.prepare("PRAGMA table_info(snippets)")?;
|
||||
let columns: Vec<String> = stmt
|
||||
.query_map([], |row| row.get(1))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if !columns.contains(&"times_used".to_string()) {
|
||||
db.execute(
|
||||
"ALTER TABLE snippets ADD COLUMN times_used INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
if !columns.contains(&"last_used_at".to_string()) {
|
||||
db.execute(
|
||||
"ALTER TABLE snippets ADD COLUMN last_used_at INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -48,19 +67,34 @@ impl SnippetManager {
|
|||
let db = self.db.lock().unwrap();
|
||||
let now = Utc::now().timestamp();
|
||||
db.execute(
|
||||
"INSERT INTO snippets (name, keyword, content, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?4)",
|
||||
"INSERT INTO snippets (name, keyword, content, created_at, updated_at, times_used, last_used_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?4, 0, 0)",
|
||||
params![name, keyword, content, now],
|
||||
)?;
|
||||
Ok(db.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn list_snippets(&self) -> Result<Vec<Snippet>, AppError> {
|
||||
pub fn list_snippets(&self, search_term: Option<String>) -> Result<Vec<Snippet>, AppError> {
|
||||
let db = self.db.lock().unwrap();
|
||||
let mut stmt = db.prepare("SELECT id, name, keyword, content, created_at, updated_at FROM snippets ORDER BY name ASC")?;
|
||||
let snippets_iter = stmt.query_map([], |row| {
|
||||
let mut query = "SELECT id, name, keyword, content, created_at, updated_at, times_used, last_used_at FROM snippets".to_string();
|
||||
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = vec![];
|
||||
|
||||
if let Some(term) = search_term {
|
||||
if !term.is_empty() {
|
||||
query.push_str(" WHERE name LIKE ?1 OR keyword LIKE ?1 OR content LIKE ?1");
|
||||
params_vec.push(Box::new(format!("%{}%", term)));
|
||||
}
|
||||
}
|
||||
|
||||
query.push_str(" ORDER BY updated_at DESC");
|
||||
|
||||
let params_ref: Vec<&dyn rusqlite::ToSql> = params_vec.iter().map(|b| b.as_ref()).collect();
|
||||
|
||||
let mut stmt = db.prepare(&query)?;
|
||||
let snippets_iter = stmt.query_map(¶ms_ref[..], |row| {
|
||||
let created_at_ts: i64 = row.get(4)?;
|
||||
let updated_at_ts: i64 = row.get(5)?;
|
||||
let last_used_at_ts: i64 = row.get(7)?;
|
||||
Ok(Snippet {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
|
@ -68,6 +102,8 @@ impl SnippetManager {
|
|||
content: row.get(3)?,
|
||||
created_at: DateTime::from_timestamp(created_at_ts, 0).unwrap_or_default(),
|
||||
updated_at: DateTime::from_timestamp(updated_at_ts, 0).unwrap_or_default(),
|
||||
times_used: row.get(6)?,
|
||||
last_used_at: DateTime::from_timestamp(last_used_at_ts, 0).unwrap_or_default(),
|
||||
})
|
||||
})?;
|
||||
|
||||
|
@ -98,12 +134,23 @@ impl SnippetManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn snippet_was_used(&self, id: i64) -> Result<(), AppError> {
|
||||
let db = self.db.lock().unwrap();
|
||||
let now = Utc::now().timestamp();
|
||||
db.execute(
|
||||
"UPDATE snippets SET times_used = times_used + 1, last_used_at = ?1 WHERE id = ?2",
|
||||
params![now, id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_snippet_by_keyword(&self, keyword: &str) -> Result<Option<Snippet>, AppError> {
|
||||
let db = self.db.lock().unwrap();
|
||||
let mut stmt = db.prepare("SELECT id, name, keyword, content, created_at, updated_at FROM snippets WHERE keyword = ?1")?;
|
||||
let mut stmt = db.prepare("SELECT id, name, keyword, content, created_at, updated_at, times_used, last_used_at FROM snippets WHERE keyword = ?1")?;
|
||||
let mut rows = stmt.query_map(params![keyword], |row| {
|
||||
let created_at_ts: i64 = row.get(4)?;
|
||||
let updated_at_ts: i64 = row.get(5)?;
|
||||
let last_used_at_ts: i64 = row.get(7)?;
|
||||
Ok(Snippet {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
|
@ -111,6 +158,8 @@ impl SnippetManager {
|
|||
content: row.get(3)?,
|
||||
created_at: DateTime::from_timestamp(created_at_ts, 0).unwrap_or_default(),
|
||||
updated_at: DateTime::from_timestamp(updated_at_ts, 0).unwrap_or_default(),
|
||||
times_used: row.get(6)?,
|
||||
last_used_at: DateTime::from_timestamp(last_used_at_ts, 0).unwrap_or_default(),
|
||||
})
|
||||
})?;
|
||||
|
||||
|
@ -120,4 +169,4 @@ impl SnippetManager {
|
|||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ pub mod types;
|
|||
|
||||
use crate::error::AppError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use types::Snippet;
|
||||
|
||||
|
@ -36,9 +37,9 @@ pub fn create_snippet(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_snippets(app: AppHandle) -> Result<Vec<Snippet>, String> {
|
||||
pub fn list_snippets(app: AppHandle, search_term: Option<String>) -> Result<Vec<Snippet>, String> {
|
||||
app.state::<manager::SnippetManager>()
|
||||
.list_snippets()
|
||||
.list_snippets(search_term)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
|
@ -62,6 +63,46 @@ pub fn delete_snippet(app: AppHandle, id: i64) -> Result<(), String> {
|
|||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn snippet_was_used(app: AppHandle, id: i64) -> Result<(), String> {
|
||||
app.state::<manager::SnippetManager>()
|
||||
.snippet_was_used(id)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn paste_snippet_content(app: AppHandle, content: String) -> Result<(), String> {
|
||||
let input_manager = app
|
||||
.state::<Arc<dyn input_manager::InputManager>>()
|
||||
.inner()
|
||||
.clone();
|
||||
|
||||
let resolved = engine::parse_and_resolve_placeholders(&content);
|
||||
let content_to_paste = resolved.content;
|
||||
|
||||
let chars_to_move_left = if let Some(pos) = resolved.cursor_pos {
|
||||
content_to_paste.chars().count() - pos
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = input_manager.inject_text(&content_to_paste) {
|
||||
eprintln!("Failed to inject snippet content: {}", e);
|
||||
}
|
||||
|
||||
if chars_to_move_left > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
if let Err(e) =
|
||||
input_manager.inject_key_clicks(enigo::Key::LeftArrow, chars_to_move_left)
|
||||
{
|
||||
eprintln!("Failed to inject cursor movement: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn import_snippets(app: AppHandle, json_content: String) -> Result<ImportResult, String> {
|
||||
let snippets: Vec<ImportSnippet> =
|
||||
|
@ -90,4 +131,4 @@ pub fn import_snippets(app: AppHandle, json_content: String) -> Result<ImportRes
|
|||
snippets_added,
|
||||
duplicates_skipped,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -10,4 +10,6 @@ pub struct Snippet {
|
|||
pub content: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
pub times_used: i32,
|
||||
pub last_used_at: DateTime<Utc>,
|
||||
}
|
257
src/lib/components/SearchSnippets.svelte
Normal file
257
src/lib/components/SearchSnippets.svelte
Normal file
|
@ -0,0 +1,257 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onMount, tick, untrack } from 'svelte';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ArrowLeft, Trash, Loader2 } from '@lucide/svelte';
|
||||
import ListItemBase from './nodes/shared/ListItemBase.svelte';
|
||||
import { Kbd } from './ui/kbd';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { shortcutToText } from '$lib/renderKey';
|
||||
import ActionBar from './nodes/shared/ActionBar.svelte';
|
||||
import ActionMenu from './nodes/shared/ActionMenu.svelte';
|
||||
import BaseList from './BaseList.svelte';
|
||||
|
||||
type Props = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
type Snippet = {
|
||||
id: number;
|
||||
name: string;
|
||||
keyword: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
timesUsed: number;
|
||||
lastUsedAt: string;
|
||||
};
|
||||
|
||||
type DisplayItem = {
|
||||
id: number | string;
|
||||
itemType: 'item' | 'header';
|
||||
data: Snippet | string;
|
||||
};
|
||||
|
||||
let { onBack }: Props = $props();
|
||||
|
||||
let snippets = $state<Snippet[]>([]);
|
||||
let selectedIndex = $state(0);
|
||||
let searchText = $state('');
|
||||
let isFetching = $state(false);
|
||||
|
||||
const displayedItems = $derived.by(() => {
|
||||
const items: DisplayItem[] = [];
|
||||
let lastHeader = '';
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
for (const snippet of snippets) {
|
||||
const snippetDate = new Date(snippet.updatedAt);
|
||||
|
||||
let header = '';
|
||||
const snippetDay = new Date(snippetDate);
|
||||
snippetDay.setHours(0, 0, 0, 0);
|
||||
|
||||
if (snippetDay.getTime() === today.getTime()) {
|
||||
header = 'Today';
|
||||
} else if (snippetDay.getTime() === yesterday.getTime()) {
|
||||
header = 'Yesterday';
|
||||
} else {
|
||||
header = snippetDay.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
});
|
||||
}
|
||||
|
||||
if (header !== lastHeader) {
|
||||
items.push({ id: `header-${header}`, itemType: 'header', data: header });
|
||||
lastHeader = header;
|
||||
}
|
||||
items.push({ id: snippet.id, itemType: 'item', data: snippet });
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
const selectedItem = $derived(
|
||||
displayedItems[selectedIndex]?.itemType === 'item'
|
||||
? (displayedItems[selectedIndex].data as Snippet)
|
||||
: null
|
||||
);
|
||||
|
||||
const fetchSnippets = async () => {
|
||||
if (isFetching) return;
|
||||
isFetching = true;
|
||||
try {
|
||||
const newItems = await invoke<Snippet[]>('list_snippets', {
|
||||
searchTerm: searchText || null
|
||||
});
|
||||
snippets = newItems;
|
||||
if (selectedIndex >= newItems.length) {
|
||||
selectedIndex = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch snippets:', e);
|
||||
} finally {
|
||||
isFetching = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
if (date.getFullYear() < 1971) return 'Never';
|
||||
return `Today at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`;
|
||||
};
|
||||
|
||||
const handlePaste = async (item: Snippet) => {
|
||||
await invoke('paste_snippet_content', { content: item.content });
|
||||
await invoke('snippet_was_used', { id: item.id });
|
||||
const updatedItems = snippets.map((i) =>
|
||||
i.id === item.id
|
||||
? {
|
||||
...i,
|
||||
timesUsed: i.timesUsed + 1,
|
||||
lastUsedAt: new Date().toISOString()
|
||||
}
|
||||
: i
|
||||
);
|
||||
snippets = updatedItems;
|
||||
onBack();
|
||||
};
|
||||
|
||||
const handleDelete = async (item: Snippet) => {
|
||||
await invoke('delete_snippet', { id: item.id });
|
||||
fetchSnippets();
|
||||
};
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
if (!selectedItem) return;
|
||||
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'x') {
|
||||
e.preventDefault();
|
||||
handleDelete(selectedItem);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
fetchSnippets();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const term = searchText;
|
||||
untrack(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (term === searchText) {
|
||||
fetchSnippets();
|
||||
}
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<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>
|
||||
<Input
|
||||
class="rounded-none border-none !bg-transparent pr-0"
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
{shortcutToText({ key: 'x', modifiers: ['ctrl'] })}
|
||||
</DropdownMenu.Shortcut>
|
||||
</DropdownMenu.Item>
|
||||
</ActionMenu>
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
|
@ -9,6 +9,7 @@ export type ViewState =
|
|||
| 'settings'
|
||||
| 'extensions-store'
|
||||
| 'clipboard-history'
|
||||
| 'search-snippets'
|
||||
| 'quicklink-form'
|
||||
| 'create-snippet-form'
|
||||
| 'import-snippets';
|
||||
|
@ -46,6 +47,10 @@ class ViewManager {
|
|||
this.currentView = 'clipboard-history';
|
||||
};
|
||||
|
||||
showSearchSnippets = () => {
|
||||
this.currentView = 'search-snippets';
|
||||
};
|
||||
|
||||
showQuicklinkForm = (quicklink?: Quicklink) => {
|
||||
this.quicklinkToEdit = quicklink;
|
||||
this.currentView = 'quicklink-form';
|
||||
|
@ -68,6 +73,9 @@ class ViewManager {
|
|||
case 'builtin:history':
|
||||
this.showClipboardHistory();
|
||||
return;
|
||||
case 'builtin:search-snippets':
|
||||
this.showSearchSnippets();
|
||||
return;
|
||||
case 'builtin:create-quicklink':
|
||||
this.showQuicklinkForm();
|
||||
return;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import { viewManager } from '$lib/viewManager.svelte';
|
||||
import SnippetForm from '$lib/components/SnippetForm.svelte';
|
||||
import ImportSnippets from '$lib/components/ImportSnippets.svelte';
|
||||
import SearchSnippets from '$lib/components/SearchSnippets.svelte';
|
||||
|
||||
const storePlugin: PluginInfo = {
|
||||
title: 'Discover Extensions',
|
||||
|
@ -40,6 +41,18 @@
|
|||
mode: 'view'
|
||||
};
|
||||
|
||||
const searchSnippetsPlugin: PluginInfo = {
|
||||
title: 'Search Snippets',
|
||||
description: 'Search and manage your snippets',
|
||||
pluginTitle: 'Snippets',
|
||||
pluginName: 'Snippets',
|
||||
commandName: 'search-snippets',
|
||||
pluginPath: 'builtin:search-snippets',
|
||||
icon: 'snippets-16',
|
||||
preferences: [],
|
||||
mode: 'view'
|
||||
};
|
||||
|
||||
const createQuicklinkPlugin: PluginInfo = {
|
||||
title: 'Create Quicklink',
|
||||
description: 'Create a new Quicklink',
|
||||
|
@ -81,6 +94,7 @@
|
|||
...pluginList,
|
||||
storePlugin,
|
||||
clipboardHistoryPlugin,
|
||||
searchSnippetsPlugin,
|
||||
createQuicklinkPlugin,
|
||||
createSnippetPlugin,
|
||||
importSnippetsPlugin
|
||||
|
@ -187,6 +201,8 @@
|
|||
{/key}
|
||||
{:else if currentView === 'clipboard-history'}
|
||||
<ClipboardHistoryView onBack={viewManager.showCommandPalette} />
|
||||
{:else if currentView === 'search-snippets'}
|
||||
<SearchSnippets onBack={viewManager.showCommandPalette} />
|
||||
{:else if currentView === 'quicklink-form'}
|
||||
<QuicklinkForm
|
||||
quicklink={quicklinkToEdit}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue