Compare commits

..

6 Commits

Author SHA1 Message Date
Huakun Shen
3596f0b8a4
refactor: remove unused extension and command CRUD operations from db module 2025-04-01 05:36:12 -04:00
Huakun Shen
8fd3223b66
refactor: migrate db tauri commands to drizzle 2025-04-01 05:31:13 -04:00
Huakun Shen
92c69430ff
update pnpm version and lock 2025-04-01 03:45:53 -04:00
Huakun Shen
555a0594e3
Merge branch 'develop' into feature/drizzle 2025-04-01 03:45:03 -04:00
Huakun Shen
bf51fdadbc
Update version to 0.1.37-beta.1 and add loading animation translations for multiple languages 2025-03-28 08:54:18 -04:00
Huakun
9cf06b1835
Feature: custom transition animation (#266)
* Add loading animation to general settings

* Update dependencies and integrate @tauri-store/svelte

- Added `bon` and `bon-macros` packages to Cargo.lock.
- Upgraded `tauri-plugin-svelte`, `tauri-store`, and related packages to their latest versions.
- Updated `@tauri-store/svelte` integration in the desktop app, including changes to app configuration and layout handling.
- Adjusted pnpm-lock.yaml to reflect updated package versions and added new dependencies.
- Introduced a new app configuration file for development.

* Enhance loading animation handling in FullScreenLoading component

- Integrated conditional rendering for loading animations based on app configuration.
- Updated default loading animation to "kunkun-dancing" in app configuration.
- Adjusted general settings to ensure proper type handling for language labels.
- Modified ui-iframe component to manage full-screen loading state more effectively.

* remove a mis-placed config file

* Refactor window handling to ensure focus after showing

- Updated various components to use promise chaining with `show()` and `setFocus()` for better window management.
- Introduced `data.win` in multiple places to streamline access to the current webview window.
- Enhanced splashscreen and app layout handling to improve user experience by ensuring the window is focused after being shown.

* Refactor window handling to improve safety and consistency

- Introduced optional chaining for `data.win` to prevent potential runtime errors when accessing window methods.
- Updated various components to ensure proper handling of window focus and visibility.
- Enhanced the layout and extension pages to utilize the current webview window more effectively, improving overall user experience.
2025-03-28 07:45:25 -04:00
57 changed files with 833 additions and 1228 deletions

46
Cargo.lock generated
View File

@ -833,6 +833,31 @@ dependencies = [
"piper",
]
[[package]]
name = "bon"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65268237be94042665b92034f979c42d431d2fd998b49809543afe3e66abad1c"
dependencies = [
"bon-macros",
"rustversion",
]
[[package]]
name = "bon-macros"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "803c95b2ecf650eb10b5f87dda6b9f6a1b758cee53245e2b7b825c9b3803a443"
dependencies = [
"darling",
"ident_case",
"prettyplease",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.87",
]
[[package]]
name = "borsh"
version = "1.3.0"
@ -8360,9 +8385,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-svelte"
version = "1.2.1"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab0a4d739af1108c6572e6249113190135c66a45586d0f8f93b3ee532e6176f"
checksum = "17e96f88b3c614b98cea3afb5de6e2661d32f82c70423ae125c56a25d62017e6"
dependencies = [
"serde",
"tauri",
@ -8493,9 +8518,9 @@ dependencies = [
[[package]]
name = "tauri-store"
version = "0.8.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4e7c0776d9f8b54fd4788f4e471ce83b4c6cf62079799830a3735582a51fc4"
checksum = "5a33c8afdf92c1b177296c0299f6d20116cbce0fa1e2264819fea8c80fd31774"
dependencies = [
"dashmap",
"futures",
@ -8512,9 +8537,9 @@ dependencies = [
[[package]]
name = "tauri-store-macros"
version = "0.8.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabed02238bc887f75887b315c6a14d9571ab463c1a188cc27ec2f7e917b06c3"
checksum = "c8857e4240cf6dbabb15fc2d595e92abba404f0a5cce0f3abbfe9316cac4aa99"
dependencies = [
"proc-macro2",
"quote",
@ -8523,11 +8548,14 @@ dependencies = [
[[package]]
name = "tauri-store-utils"
version = "0.2.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b983a259b22d622ce74b957140efa161bd75c6bfd47b7bf621c98dd05b1a2474"
checksum = "14376c237a6632663991634d51a31f128b6b381b94d65e747db2419a513ae6d8"
dependencies = [
"futures",
"bon",
"semver",
"serde",
"serde_json",
"tauri",
"thiserror 2.0.3",
"tokio",

View File

@ -40,6 +40,7 @@
"settings_general_join_beta_updates": "Beta-Updates nutzen",
"settings_general_developer_mode": "Entwickler-Modus",
"settings_general_language": "Sprache",
"settings_general_loading_animation": "Ladeanimation",
"settings_app_search_paths_title": "Zusätzliche Verzeichnisse für die Programm-Suche",
"settings_app_search_paths_add_app_search_path": "Verzeichnis für Programm-Suche hinzufügen",

View File

@ -40,6 +40,7 @@
"settings_general_join_beta_updates": "Join Beta Updates",
"settings_general_developer_mode": "Developer Mode",
"settings_general_language": "Language",
"settings_general_loading_animation": "Loading Animation",
"settings_app_search_paths_title": "Extra App Search Paths",
"settings_app_search_paths_add_app_search_path": "Add App Search Path",

View File

@ -39,6 +39,7 @@
"settings_general_join_beta_updates": "Participar das Atualizações Beta",
"settings_general_developer_mode": "Modo Desenvolvedor",
"settings_general_language": "Idioma",
"settings_general_loading_animation": "Animação de Carregamento",
"settings_about_version": "Versão",
"settings_about_author": "Autor",

View File

@ -39,6 +39,7 @@
"settings_general_join_beta_updates": "Получать бета-обновления",
"settings_general_developer_mode": "Режим разработчика",
"settings_general_language": "Язык",
"settings_general_loading_animation": "Анимация загрузки",
"settings_about_version": "Версия",
"settings_about_author": "Автор",

View File

@ -39,6 +39,7 @@
"settings_general_join_beta_updates": "Cài đặt cập nhật thử nghiệm (beta)",
"settings_general_developer_mode": "Chế độ nhà phát triển",
"settings_general_language": "Ngôn ngữ",
"settings_general_loading_animation": "Hình ảnh tải",
"settings_about_version": "Phiên bản",
"settings_about_author": "Tác giả",

View File

@ -40,6 +40,7 @@
"settings_general_join_beta_updates": "加入 Beta 更新",
"settings_general_developer_mode": "开发者模式",
"settings_general_language": "语言",
"settings_general_loading_animation": "加载动画",
"settings_app_search_paths_title": "额外应用搜索路径",
"settings_app_search_paths_add_app_search_path": "添加应用搜索路径",

View File

@ -1,6 +1,6 @@
{
"name": "@kksh/desktop",
"version": "0.1.36",
"version": "0.1.37-beta.1",
"description": "",
"type": "module",
"scripts": {
@ -30,6 +30,7 @@
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-sql": "^2.2.0",
"@tauri-apps/plugin-stronghold": "^2.2.0",
"@tauri-store/svelte": "^2.1.1",
"dompurify": "^3.2.4",
"drizzle-orm": "^0.40.1",
"eslint": "^9.21.0",

View File

@ -72,4 +72,4 @@ tauri-plugin-cli = "2"
tauri-plugin-global-shortcut = "2.0.1"
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
tauri-plugin-updater = "2.0.2"
tauri-plugin-svelte = "1.2.1"
tauri-plugin-svelte = "2.1.1"

View File

@ -24,6 +24,7 @@
"core:event:default",
"core:window:default",
"core:window:allow-set-size",
"core:window:allow-set-enabled",
"core:window:allow-start-dragging",
"core:window:allow-set-focus",
"core:window:allow-toggle-maximize",

View File

@ -27,7 +27,7 @@ use utils::server::tauri_file_server;
pub fn run() {
let context = tauri::generate_context!();
let mut builder = tauri::Builder::default();
// let app_data_path = tauri::path::PathResolver::app_data_dir().unwrap();
// let db_key = if cfg!(debug_assertions) {
// None
// } else {

View File

@ -428,7 +428,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
visible: false
})
setTimeout(() => {
window.show()
window.show().then(() => window.setFocus())
}, 2_000)
}
},

View File

@ -7,10 +7,11 @@ import { decideKkrpcSerialization } from "@/utils/kkrpc"
import { sleep } from "@/utils/time"
import { trimSlash } from "@/utils/url"
import { constructExtensionSupportDir } from "@kksh/api"
import { db, spawnExtensionFileServer } from "@kksh/api/commands"
import { spawnExtensionFileServer } from "@kksh/api/commands"
import type { HeadlessCommand } from "@kksh/api/headless"
import { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { constructJarvisServerAPIWithPermissions, type IApp } from "@kksh/api/ui"
import { db } from "@kksh/drizzle"
import { launchNewExtWindow, loadExtensionManifestFromDisk } from "@kksh/extension"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import { convertFileSrc } from "@tauri-apps/api/core"

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { appState } from "@/stores"
import { appConfig, appState } from "@/stores"
import { cn } from "@/utils"
import { Button } from "@kksh/svelte5"
import { BorderBeam, Constants, Layouts, TauriLink } from "@kksh/ui"
@ -25,8 +25,13 @@
>
<ArrowLeftIcon class="size-4" />
</Button>
<Dance class="absolute z-50 h-screen opacity-20" />
<LoaderCircleIcon class="h-24 w-24 animate-spin" />
<span class="font-mono">Loading</span>
{#if $appConfig.loadingAnimation === "kunkun-dancing"}
<!-- <DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} /> -->
<Dance class="absolute z-50 h-screen opacity-20" />
{:else}
<!-- <LoadingAnimation delay={300} autoHide={false} show={!uiControl.iframeLoaded} /> -->
<LoaderCircleIcon class="h-24 w-24 animate-spin" />
<span class="font-mono">Loading</span>
{/if}
<BorderBeam size={150} duration={12} />
</Layouts.Center>

View File

@ -11,20 +11,24 @@
} from "@/paraglide/runtime"
import { appConfig } from "@/stores"
import { Select, Switch } from "@kksh/svelte5"
import type { LoadingAnimation } from "@kksh/types"
import * as autoStart from "@tauri-apps/plugin-autostart"
import { onMount } from "svelte"
import { toast } from "svelte-sonner"
const languages = availableLanguageTags.map((lang) => ({
value: lang,
label: LanguageMap[lang] ?? lang
label: LanguageMap[lang as keyof typeof LanguageMap] ?? lang
}))
let loadingAnimation = $state<LoadingAnimation>("spinning-circle")
const loadingAnimations = ["spinning-circle", "kunkun-dancing"] as const
let launchAtLogin = $state(false)
let language = $state(languageTag())
onMount(() => {
autoStart.isEnabled().then((enabled) => {
launchAtLogin = enabled
})
loadingAnimation = $appConfig.loadingAnimation
})
const triggerContent = $derived(languages.find((f) => f.value === language)?.label ?? "Language")
</script>
@ -101,6 +105,31 @@
</Select.Content>
</Select.Root>
</li>
<li>
<span>{m.settings_general_loading_animation()}</span>
<Select.Root type="single" name="loadingAnimation" bind:value={loadingAnimation}>
<Select.Trigger class="w-fit">
{loadingAnimation}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.GroupHeading>Loading Animation</Select.GroupHeading>
{#each loadingAnimations as anim}
<Select.Item
onclick={() => {
appConfig.setLoadingAnimation(anim)
}}
value={anim}
label={anim}
>
{anim}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</li>
</ul>
<style scoped>

View File

@ -1,13 +1,13 @@
import { getExtensionsFolder } from "@/constants"
import type { SearchPath } from "@kksh/api/models"
import { updateTheme, type ThemeConfig } from "@kksh/svelte5"
import { PersistedAppConfig, type AppConfigState } from "@kksh/types"
import { LoadingAnimation, PersistedAppConfig, type AppConfigState } from "@kksh/types"
import { debug, error, info } from "@tauri-apps/plugin-log"
import * as os from "@tauri-apps/plugin-os"
import { load } from "@tauri-apps/plugin-store"
import { Store } from "@tauri-store/svelte"
import { toast } from "svelte-sonner"
import { get, writable } from "svelte/store"
import { Store } from "tauri-plugin-svelte"
import * as v from "valibot"
export const defaultAppConfig: AppConfigState = {
@ -29,7 +29,8 @@ export const defaultAppConfig: AppConfigState = {
joinBetaProgram: false,
onBoarded: false,
developerMode: false,
appSearchPaths: []
appSearchPaths: [],
loadingAnimation: "kunkun-dancing"
}
export const appConfigLoaded = writable(false)
@ -99,74 +100,10 @@ class AppConfigStore extends Store<AppConfigState> implements AppConfigAPI {
appSearchPaths: config.appSearchPaths.filter((path) => path.path !== appSearchPath.path)
}))
}
setLoadingAnimation(loadingAnimation: LoadingAnimation) {
this.update((config) => ({ ...config, loadingAnimation }))
}
}
// function createAppConfig(): WithSyncStore<AppConfigState & { language: string }> & AppConfigAPI {
// const store = createTauriSyncStore("app-config", defaultAppConfig)
// async function init() {
// debug("Initializing app config")
// const persistStore = await load("kk-config.json", { autoSave: true })
// let loadedConfig = await persistStore.get("config")
// if (typeof loadedConfig === "object") {
// loadedConfig = { ...defaultAppConfig, ...loadedConfig }
// }
// const parseRes = v.safeParse(PersistedAppConfig, loadedConfig)
// if (parseRes.success) {
// console.log("Parse Persisted App Config Success", parseRes.output)
// const extensionsInstallDir = await getExtensionsFolder()
// store.update((config) => ({
// ...config,
// ...parseRes.output,
// isInitialized: true,
// extensionsInstallDir,
// platform: os.platform()
// }))
// } else {
// error("Failed to parse app config, going to remove it and reinitialize")
// console.error(v.flatten<typeof PersistedAppConfig>(parseRes.issues))
// await persistStore.clear()
// await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig))
// }
// store.subscribe(async (config) => {
// console.log("Saving app config", config)
// await persistStore.set("config", config)
// updateTheme(config.theme)
// })
// }
// return {
// ...store,
// get: () => get(store),
// setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })),
// setDevExtensionPath: (devExtensionPath: string | null) => {
// console.log("setDevExtensionPath", devExtensionPath)
// store.update((config) => ({ ...config, devExtensionPath }))
// },
// setTriggerHotkey: (triggerHotkey: string[]) => {
// store.update((config) => ({ ...config, triggerHotkey }))
// },
// setOnBoarded: (onBoarded: boolean) => {
// store.update((config) => ({ ...config, onBoarded }))
// },
// setLanguage: (language: string) => {
// store.update((config) => ({ ...config, language }))
// },
// addAppSearchPath: (appSearchPath: SearchPath) => {
// store.update((config) => ({
// ...config,
// appSearchPaths: [...config.appSearchPaths, appSearchPath]
// }))
// },
// removeAppSearchPath: (appSearchPath: SearchPath) => {
// store.update((config) => ({
// ...config,
// appSearchPaths: config.appSearchPaths.filter((path) => path.path !== appSearchPath.path)
// }))
// },
// init
// }
// }
// export const appConfig = createAppConfig()
export const appConfig = new AppConfigStore()

View File

@ -1,5 +1,5 @@
import { db } from "@kksh/api/commands"
import type { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import * as extAPI from "@kksh/extension"
import * as path from "@tauri-apps/api/path"
import Fuse from "fuse.js"

View File

@ -48,8 +48,7 @@ export async function registerAppHotkey(hotkeyStr: string) {
mainWin.setFocus()
}
} else {
mainWin.show()
mainWin.setFocus()
mainWin.show().then(() => mainWin.setFocus())
}
}
})

View File

@ -97,7 +97,7 @@ export async function globalKeyDownHandler(e: KeyboardEvent) {
await appWin.hide()
location.reload()
setTimeout(() => {
appWin.show()
appWin.show().then(() => appWin.setFocus())
}, 1_000)
}
}

View File

@ -12,6 +12,7 @@
<svelte:window on:keydown={handleKeyDown} />
<div class="fixed h-12 w-full" data-tauri-drag-region></div>
<Layouts.Center class="min-h-screen py-5">
<Error.RawErrorJSONPreset
title="Error"

View File

@ -1,5 +1,15 @@
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { browser } from "$app/environment"
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true
export const ssr = false
export const load = () => {
if (browser) {
const win = getCurrentWebviewWindow()
return { win }
}
}

View File

@ -46,7 +46,8 @@
})
})
let { children } = $props()
let { children, data } = $props()
const unlisteners: UnlistenFn[] = []
onDestroy(() => {
unlisteners.forEach((unlistener) => unlistener())
@ -100,7 +101,7 @@
})
)
}
getCurrentWebviewWindow().show()
data.win?.show().then(() => data.win?.setFocus())
})
</script>

View File

@ -1,7 +1,11 @@
import { getExtensionsFolder, IS_IN_TAURI } from "@/constants"
import * as path from "@tauri-apps/api/path"
import { error } from "@tauri-apps/plugin-log"
import { setStoreCollectionPath } from "@tauri-store/svelte"
import type { LayoutLoad } from "./$types"
export const load: LayoutLoad = async () => {
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "" }
const appDataPath = await path.appDataDir()
await setStoreCollectionPath(await path.join(appDataPath, "kk-config"))
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "", appDataPath }
}

View File

@ -5,8 +5,6 @@
import { systemCommands, systemCommandsFiltered } from "@/cmds/system"
import AppsCmds from "@/components/main/AppsCmds.svelte"
import { i18n } from "@/i18n"
import { getUniqueExtensionByIdentifier } from "@/orm/cmds"
import { db } from "@/orm/database"
import * as m from "@/paraglide/messages"
import {
appConfig,
@ -68,12 +66,16 @@
if (splashscreenWin) {
splashscreenWin.close()
}
win.show()
win.show().then(() => win.setFocus())
})
win.onFocusChanged(({ payload: focused }) => {
if (focused) {
win.show()
inputEle?.focus()
win
.show()
.then(() => win.setFocus())
.finally(() => {
inputEle?.focus()
})
}
})
inputEle?.focus()

View File

@ -3,8 +3,9 @@
import { goHome } from "@/utils/route"
import { listenToNewClipboardItem, listenToWindowFocus } from "@/utils/tauri-events"
import Icon from "@iconify/svelte"
import { ClipboardContentType, db } from "@kksh/api/commands"
import { ClipboardContentType } from "@kksh/api/commands"
import { SearchModeEnum, SQLSortOrderEnum, type ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Button, Command, Resizable } from "@kksh/svelte5"
import { Constants } from "@kksh/ui"
import { CustomCommandInput, GlobalCommandPaletteFooter } from "@kksh/ui/main"

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from "@/utils"
import { db } from "@kksh/api/commands"
import type { ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Resizable, Separator } from "@kksh/svelte5"
import { convertFileSrc } from "@tauri-apps/api/core"
import DOMPurify from "dompurify"

View File

@ -8,6 +8,8 @@
import * as userInput from "tauri-plugin-user-input-api"
import { type InputEvent } from "tauri-plugin-user-input-api"
let { data } = $props()
const SymbolMap = {
Alt: "⎇",
AltGr: "⌥",
@ -97,10 +99,7 @@
}
$effect(() => {
const win = getCurrentWebviewWindow()
if (win) {
win.show()
}
data.win?.show().then(() => data.win?.setFocus())
userInput.setEventTypes([userInput.EventTypeEnum.KeyPress, userInput.EventTypeEnum.KeyRelease])
userInput.startListening((evt: InputEvent) => {

View File

@ -8,23 +8,24 @@
import * as clipboard from "tauri-plugin-clipboard-api"
let image = $state<string | null>(null)
const appWin = getCurrentWebviewWindow()
let { data } = $props()
let originalSize = $state<{ width: number; height: number } | null>(null)
let originalScaleFactor = $state<number | null>(null)
let scale = $state<number>(1)
let currentSize = $derived(
originalSize ? { width: originalSize.width * scale, height: originalSize.height * scale } : null
)
const win = getCurrentWebviewWindow()
$effect(() => {
if (currentSize) {
appWin.setSize(new LogicalSize(currentSize.width, currentSize.height))
win.setSize(new LogicalSize(currentSize.width, currentSize.height))
}
})
async function getWindowSize() {
const size = await appWin.outerSize()
const scaleFactor = originalScaleFactor ?? (await appWin.scaleFactor())
const size = await win.outerSize()
const scaleFactor = originalScaleFactor ?? (await win.scaleFactor())
const logicalSize = size.toLogical(scaleFactor)
return { logicalSize, scaleFactor }
}
@ -36,7 +37,7 @@
image = b64
})
.finally(() => {
appWin.show()
data.win?.show().then(() => data.win?.setFocus())
})
const { logicalSize, scaleFactor } = await getWindowSize()
originalSize = { width: logicalSize.width, height: logicalSize.height }
@ -67,13 +68,13 @@
<svelte:window
on:keydown={(e) => {
if (e.key === "Escape") {
appWin.close()
win.close()
}
}}
/>
<Button size="icon" variant="ghost" class="fixed left-2 top-2" onclick={() => appWin.close()}
><CircleX /></Button
>
<Button size="icon" variant="ghost" class="fixed left-2 top-2" onclick={() => win.close()}>
<CircleX />
</Button>
<main class="z-50 h-screen w-screen overflow-hidden" data-tauri-drag-region>
{#if image}
<img

View File

@ -1,7 +1,7 @@
<script lang="ts">
import DanceTransition from "@/components/dance/dance-transition.svelte"
import { i18n } from "@/i18n"
import { appConfig, winExtMap } from "@/stores"
import { appConfig, appState, winExtMap } from "@/stores"
import { helperAPI } from "@/utils/helper"
import { paste } from "@/utils/hotkey"
import { goBackOnEscape } from "@/utils/key"
@ -10,7 +10,6 @@
import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style"
import { sleep } from "@/utils/time"
import { isInMainWindow } from "@/utils/window"
import { db } from "@kksh/api/commands"
import { CustomPosition, ThemeColor, type Position } from "@kksh/api/models"
import {
constructJarvisServerAPIWithPermissions,
@ -19,6 +18,7 @@
type IUiCustom
} from "@kksh/api/ui"
import { toast, type IUiCustomServer1, type IUiCustomServer2 } from "@kksh/api/ui/custom"
import { db } from "@kksh/drizzle"
import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/ui/utils"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
@ -38,7 +38,6 @@
let { data }: { data: PageData } = $props()
const { loadedExt, url, extPath, extInfoInDB } = data
let extSpawnedProcesses = $state<number[]>([])
const appWin = getCurrentWindow()
let iframeRef: HTMLIFrameElement
let uiControl = $state<{
iframeLoaded: boolean
@ -65,7 +64,7 @@
if (isInMainWindow()) {
goto(i18n.resolveRoute("/app/"))
} else {
appWin.close()
data.win?.close()
}
},
hideBackButton: async () => {
@ -131,7 +130,7 @@
},
getSpawnedProcesses: () => Promise.resolve(extSpawnedProcesses),
paste: async () => {
await appWin.hide()
await data.win?.hide()
await sleep(200)
return paste()
}
@ -155,7 +154,7 @@
if (isInMainWindow()) {
goHome()
} else {
appWin.close()
data.win?.close()
}
}
@ -163,12 +162,14 @@
setTimeout(() => {
iframeRef.focus()
uiControl.iframeLoaded = true
appState.setFullScreenLoading(false)
}, 300)
}
onMount(() => {
appState.setFullScreenLoading(true)
setTimeout(() => {
appWin.show()
data.win?.setFocus()
}, 200)
if (iframeRef?.contentWindow) {
const io = new IframeParentIO(iframeRef.contentWindow)
@ -194,7 +195,7 @@
})
onDestroy(() => {
winExtMap.unregisterExtensionFromWindow(appWin.label)
winExtMap.unregisterExtensionFromWindow(data.win?.label ?? "")
})
</script>
@ -207,7 +208,7 @@
onclick={onBackBtnClicked}
style={`${positionToCssStyleString(uiControl.backBtnPosition)}`}
>
{#if appWin.label === "main"}
{#if data.win?.label === "main"}
<ArrowLeftIcon class="w-4" />
{:else}
<XIcon class="w-4" />
@ -238,7 +239,6 @@
{/if}
<main class="h-screen">
<DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} />
<iframe
bind:this={iframeRef}
class={cn("h-full", {

View File

@ -1,7 +1,7 @@
import { KunkunIframeExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n"
import { db, unregisterExtensionWindow } from "@kksh/api/commands"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path"

View File

@ -13,7 +13,6 @@
} from "@/utils/tauri-events.js"
import { sleep } from "@/utils/time.js"
import { isInMainWindow } from "@/utils/window.js"
import { db } from "@kksh/api/commands"
import {
constructJarvisServerAPIWithPermissions,
type IApp,
@ -29,6 +28,7 @@
type IComponent,
type TemplateUiCommand
} from "@kksh/api/ui/template"
import { db } from "@kksh/drizzle"
import { Button, Form } from "@kksh/svelte5"
import { LoadingBar } from "@kksh/ui"
import { Templates } from "@kksh/ui/extension"
@ -310,7 +310,7 @@
onMount(async () => {
setTimeout(() => {
appState.setLoadingBar(true)
appWin.show()
appWin.show().then(() => appWin.setFocus())
}, 100)
unlistenRefreshWorkerExt = await listenToRefreshDevExt(() => {
debug("Refreshing Worker Extension")

View File

@ -1,7 +1,7 @@
import { KunkunTemplateExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n"
import { db, unregisterExtensionWindow } from "@kksh/api/commands"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models"
import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as sbError, error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path"

View File

@ -1,6 +1,6 @@
<script lang="ts">
import * as m from "@/paraglide/messages"
import { db } from "@kksh/api/commands"
import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { Button, Dialog, Table } from "@kksh/svelte5"
import { join } from "@tauri-apps/api/path"

View File

@ -7,13 +7,12 @@
getUniqueExtensionByPath,
searchExtensionData,
updateCmdByID
} from "@/orm/cmds"
} from "@kksh/drizzle/api"
import * as schema from "@kksh/drizzle/schema"
import { Button, Input } from "@kksh/svelte5"
import { CmdTypeEnum, Ext } from "@kunkunapi/src/models/extension"
import { SearchModeEnum, SQLSortOrderEnum } from "@kunkunapi/src/models/sql"
import { db } from "$lib/orm/database"
import * as orm from "drizzle-orm"
// import * as orm from "drizzle-orm"
import { Inspect } from "svelte-inspect-value"
import { toast } from "svelte-sonner"
import * as v from "valibot"

View File

@ -1,11 +1,11 @@
<script lang="ts">
import { Layouts } from "@kksh/ui"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { onMount } from "svelte"
onMount(async () => {
const mainWin = await getCurrentWindow()
mainWin.show()
let { data } = $props()
onMount(() => {
data.win?.show().then(() => data.win?.setFocus())
})
</script>

View File

@ -22,7 +22,7 @@
"typescript": "^5.0.0",
"verify-package-export": "^0.0.3"
},
"packageManager": "pnpm@9.15.2",
"packageManager": "pnpm@10.7.0",
"engines": {
"node": ">=22"
},

View File

@ -20,9 +20,10 @@ import type {
IPath as ITauriPath
} from "tauri-api-adapter"
import * as v from "valibot"
import { KV, type JarvisExtDB } from "../commands/db"
import type { fileSearch } from "../commands/fileSearch"
import { type AppInfo } from "../models/apps"
import { type ExtData } from "../models/extension"
import { ExtDataField, SearchMode, SQLSortOrder } from "../models/sql"
import type { LightMode, Position, Radius, ThemeColor } from "../models/styles"
import type { DenoSysOptions } from "../permissions/schema"
@ -154,23 +155,34 @@ export interface IUiCustom {
}
export interface IDb {
add: typeof JarvisExtDB.prototype.add
delete: typeof JarvisExtDB.prototype.delete
search: typeof JarvisExtDB.prototype.search
retrieveAll: typeof JarvisExtDB.prototype.retrieveAll
retrieveAllByType: typeof JarvisExtDB.prototype.retrieveAllByType
deleteAll: typeof JarvisExtDB.prototype.deleteAll
update: typeof JarvisExtDB.prototype.update
add: (data: { data: string; dataType?: string; searchText?: string }) => Promise<void>
delete: (dataId: number) => Promise<void>
search: (searchParams: {
dataId?: number
searchMode?: SearchMode
dataType?: string
searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}) => Promise<ExtData[]>
retrieveAll: (options: { fields?: ExtDataField[] }) => Promise<ExtData[]>
retrieveAllByType: (dataType: string) => Promise<ExtData[]>
deleteAll: () => Promise<void>
update: (data: { dataId: number; data: string; searchText?: string }) => Promise<void>
}
/**
* A key-value store built on top of the Database API (based on sqlite)
*/
export interface IKV {
get: typeof KV.prototype.get
set: typeof KV.prototype.set
exists: typeof KV.prototype.exists
delete: typeof KV.prototype.delete
get: <T = string>(key: string) => Promise<T | null | undefined>
set: (key: string, value: string) => Promise<void>
exists: (key: string) => Promise<boolean>
delete: (key: string) => Promise<void>
}
export interface IFs {

View File

@ -1,465 +0,0 @@
import { invoke } from "@tauri-apps/api/core"
import { array, literal, optional, parse, safeParse, union, type InferOutput } from "valibot"
import { KUNKUN_EXT_IDENTIFIER } from "../constants"
import { CmdType, Ext, ExtCmd, ExtData } from "../models/extension"
import { convertDateToSqliteString, SearchMode, SearchModeEnum, SQLSortOrder } from "../models/sql"
import { generateJarvisPluginCommand } from "./common"
export interface QueryResult {
/** The number of rows affected by the query. */
rowsAffected: number
/**
* The last inserted `id`.
*
* This value is not set for Postgres databases. If the
* last inserted id is required on Postgres, the `select` function
* must be used, with a `RETURNING` clause
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
*/
lastInsertId?: number
}
export function select(query: string, values: any[]) {
return invoke<any[]>(generateJarvisPluginCommand("select"), {
query,
values
})
}
export function execute(query: string, values: any[]) {
return invoke<QueryResult>(generateJarvisPluginCommand("execute"), {
query,
values
})
}
/* -------------------------------------------------------------------------- */
/* Extension CRUD */
/* -------------------------------------------------------------------------- */
export function createExtension(ext: {
identifier: string
version: string
enabled?: boolean
path?: string
data?: any
}) {
return invoke<void>(generateJarvisPluginCommand("create_extension"), ext)
}
export function getAllExtensions() {
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions"))
}
export function getUniqueExtensionByIdentifier(identifier: string) {
return invoke<Ext | undefined>(
generateJarvisPluginCommand("get_unique_extension_by_identifier"),
{
identifier
}
)
}
export function getUniqueExtensionByPath(path: string) {
return invoke<Ext | undefined>(generateJarvisPluginCommand("get_unique_extension_by_path"), {
path
})
}
export function getAllExtensionsByIdentifier(identifier: string) {
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions_by_identifier"), {
identifier
})
}
/**
* Use this function when you expect the extension to exist. Such as builtin extensions.
* @param identifier
* @returns
*/
export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> {
return getUniqueExtensionByIdentifier(identifier).then((ext) => {
if (!ext) {
throw new Error(`Unexpexted Error: Extension ${identifier} not found`)
}
return ext
})
}
// TODO: clean this up
// export function deleteExtensionByIdentifier(identifier: string) {
// return invoke<void>(generateJarvisPluginCommand("delete_extension_by_identifier"), { identifier })
// }
export function deleteExtensionByPath(path: string) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_path"), {
path
})
}
export function deleteExtensionByExtId(extId: string) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_ext_id"), { extId })
}
/* -------------------------------------------------------------------------- */
/* Extension Command CRUD */
/* -------------------------------------------------------------------------- */
export function createCommand(data: {
extId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled?: boolean
}) {
return invoke<void>(generateJarvisPluginCommand("create_command"), {
...data,
enabled: data.enabled ?? false
})
}
export function getCommandById(cmdId: number) {
return invoke<ExtCmd | undefined>(generateJarvisPluginCommand("get_command_by_id"), { cmdId })
}
export function getCommandsByExtId(extId: number) {
return invoke<ExtCmd[]>(generateJarvisPluginCommand("get_commands_by_ext_id"), { extId })
}
export function deleteCommandById(cmdId: number) {
return invoke<void>(generateJarvisPluginCommand("delete_command_by_id"), {
cmdId
})
}
export function updateCommandById(data: {
cmdId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled: boolean
}) {
return invoke<void>(generateJarvisPluginCommand("update_command_by_id"), data)
}
/* -------------------------------------------------------------------------- */
/* Extension Data CRUD */
/* -------------------------------------------------------------------------- */
export const ExtDataField = union([literal("data"), literal("search_text")])
export type ExtDataField = InferOutput<typeof ExtDataField>
function convertRawExtDataToExtData(rawData?: {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
}): ExtData | undefined {
if (!rawData) {
return rawData
}
const parsedRes = safeParse(ExtData, {
...rawData,
createdAt: new Date(rawData.createdAt),
updatedAt: new Date(rawData.updatedAt),
data: rawData.data ?? undefined,
searchText: rawData.searchText ?? undefined
})
if (parsedRes.success) {
return parsedRes.output
} else {
console.error("Extension Data Parse Failure", parsedRes.issues)
throw new Error("Fail to parse extension data")
}
}
export function createExtensionData(data: {
extId: number
dataType: string
data: string
searchText?: string
}) {
return invoke<void>(generateJarvisPluginCommand("create_extension_data"), data)
}
export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) {
return invoke<
| (ExtData & {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
})
| undefined
>(generateJarvisPluginCommand("get_extension_data_by_id"), {
dataId,
fields
}).then(convertRawExtDataToExtData)
}
/**
* Fields option can be used to select optional fields. By default, if left empty, data and searchText are not returned.
* This is because data and searchText can be large and we don't want to return them by default.
* If you just want to get data ids in order to delete them, retrieving all data is not necessary.
* @param searchParams
*/
export async function searchExtensionData(searchParams: {
extId: number
searchMode: SearchMode
dataId?: number
dataType?: string
searchText?: string
afterCreatedAt?: string
beforeCreatedAt?: string
limit?: number
offset?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const fields = parse(optional(array(ExtDataField), []), searchParams.fields)
let items = await invoke<
(ExtData & {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
})[]
>(generateJarvisPluginCommand("search_extension_data"), {
searchQuery: {
...searchParams,
fields
}
})
return items.map(convertRawExtDataToExtData).filter((item) => item) as ExtData[]
}
export function deleteExtensionDataById(dataId: number) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_data_by_id"), { dataId })
}
export function updateExtensionDataById(data: {
dataId: number
data: string
searchText?: string
}) {
return invoke<void>(generateJarvisPluginCommand("update_extension_data_by_id"), data)
}
/* -------------------------------------------------------------------------- */
/* Built-in Extensions */
/* -------------------------------------------------------------------------- */
export function getExtClipboard() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER)
}
export function getExtQuickLinks() {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER
)
}
export function getExtRemote() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER)
}
export function getExtScriptCmd() {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER
)
}
export function getExtDev() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER)
}
/**
* Database API for extensions.
* Extensions shouldn't have full access to the database, they can only access their own data.
* When an extension is loaded, the main thread will create an instance of this class and
* expose it to the extension.
*/
export class JarvisExtDB {
extId: number
constructor(extId: number) {
this.extId = extId
}
async add(data: { data: string; dataType?: string; searchText?: string }) {
return createExtensionData({
data: data.data,
dataType: data.dataType ?? "default",
searchText: data.searchText,
extId: this.extId
})
}
async delete(dataId: number): Promise<void> {
// Verify if this data belongs to this extension
const d = await getExtensionDataById(dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return await deleteExtensionDataById(dataId)
}
async search(searchParams: {
dataId?: number
searchMode?: SearchMode
dataType?: string
searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const beforeCreatedAt = searchParams.beforeCreatedAt
? convertDateToSqliteString(searchParams.beforeCreatedAt)
: undefined
const afterCreatedAt = searchParams.afterCreatedAt
? convertDateToSqliteString(searchParams.afterCreatedAt)
: undefined
return searchExtensionData({
...searchParams,
searchMode: searchParams.searchMode ?? SearchModeEnum.FTS,
extId: this.extId,
beforeCreatedAt,
afterCreatedAt
})
}
/**
* Retrieve all data of this extension.
* Use `search()` method for more advanced search.
* @param options optional fields to retrieve. By default, data and searchText are not returned.
* @returns
*/
retrieveAll(options: { fields?: ExtDataField[] }): Promise<ExtData[]> {
return this.search({ fields: options.fields })
}
/**
* Retrieve all data of this extension by type.
* Use `search()` method for more advanced search.
* @param dataType
* @returns
*/
retrieveAllByType(dataType: string): Promise<ExtData[]> {
return this.search({ dataType })
}
/**
* Delete all data of this extension.
*/
deleteAll(): Promise<void> {
return this.search({})
.then((items) => {
return Promise.all(items.map((item) => this.delete(item.dataId)))
})
.then(() => {})
}
/**
* Update data and searchText of this extension.
* @param dataId unique id of the data
* @param data
* @param searchText
* @returns
*/
async update(data: { dataId: number; data: string; searchText?: string }): Promise<void> {
const d = await getExtensionDataById(data.dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return updateExtensionDataById(data)
}
}
export class KV {
extId: number
db: JarvisExtDB
private DataType: string = "kunkun_kv"
constructor(extId: number) {
this.extId = extId
this.db = new JarvisExtDB(extId)
}
get<T = string>(key: string): Promise<T | null | undefined> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: ["search_text", "data"]
})
.then((items) => {
if (items.length === 0) {
return null
} else if (items.length > 1) {
throw new Error("Multiple KVs with the same key")
}
return items[0].data ? (JSON.parse(items[0].data).value as T) : null
})
.catch((err) => {
console.warn(err)
return null
})
}
set(key: string, value: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
if (items.length === 0) {
return this.db.add({
data: JSON.stringify({ value: value }),
dataType: this.DataType,
searchText: key
})
} else if (items.length === 1) {
return this.db.update({
dataId: items[0].dataId,
data: JSON.stringify({ value: value }),
searchText: key
})
} else {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
}
})
}
delete(key: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
})
}
exists(key: string): Promise<boolean> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: []
})
.then((items) => {
return items.length > 0
})
}
}

View File

@ -5,8 +5,7 @@ export * from "./tools"
export * from "./extension"
export * from "./system"
export * from "./store"
export * as db from "./db"
export { JarvisExtDB } from "./db"
export * as sql from "./sql"
export * from "./clipboard"
export * from "./fileSearch"
export * from "./utils"

View File

@ -0,0 +1,30 @@
import { invoke } from "@tauri-apps/api/core"
import { generateJarvisPluginCommand } from "./common"
export interface QueryResult {
/** The number of rows affected by the query. */
rowsAffected: number
/**
* The last inserted `id`.
*
* This value is not set for Postgres databases. If the
* last inserted id is required on Postgres, the `select` function
* must be used, with a `RETURNING` clause
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
*/
lastInsertId?: number
}
export function select(query: string, values: any[]) {
return invoke<any[]>(generateJarvisPluginCommand("select"), {
query,
values
})
}
export function execute(query: string, values: any[]) {
return invoke<QueryResult>(generateJarvisPluginCommand("execute"), {
query,
values
})
}

View File

@ -18,10 +18,7 @@ export const Ext = v.object({
extId: v.number(),
identifier: v.string(),
version: v.string(),
enabled: v.pipe(
v.number(),
v.transform((input) => Boolean(input))
),
enabled: v.boolean(),
installedAt: v.string(),
path: v.optional(v.nullable(v.string())),
data: v.optional(v.any())
@ -49,11 +46,9 @@ export const ExtCmd = v.object({
data: v.string(),
alias: v.optional(v.nullable(v.string())),
hotkey: v.optional(v.nullable(v.string())),
enabled: v.pipe(
v.number(),
v.transform((input) => Boolean(input))
)
enabled: v.boolean()
})
export type ExtCmd = v.InferOutput<typeof ExtCmd>
export const QuickLinkCmd = v.object({

View File

@ -1,12 +1,12 @@
import { enum_, type InferOutput } from "valibot"
import * as v from "valibot"
export enum SQLSortOrderEnum {
Asc = "ASC",
Desc = "DESC"
}
export const SQLSortOrder = enum_(SQLSortOrderEnum)
export type SQLSortOrder = InferOutput<typeof SQLSortOrder>
export const SQLSortOrder = v.enum_(SQLSortOrderEnum)
export type SQLSortOrder = v.InferOutput<typeof SQLSortOrder>
export enum SearchModeEnum {
ExactMatch = "exact_match",
@ -14,8 +14,8 @@ export enum SearchModeEnum {
FTS = "fts"
}
export const SearchMode = enum_(SearchModeEnum)
export type SearchMode = InferOutput<typeof SearchMode>
export const SearchMode = v.enum_(SearchModeEnum)
export type SearchMode = v.InferOutput<typeof SearchMode>
export function convertDateToSqliteString(date: Date) {
const pad = (num: number) => num.toString().padStart(2, "0")
@ -29,3 +29,6 @@ export function convertDateToSqliteString(date: Date) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export const ExtDataField = v.union([v.literal("data"), v.literal("search_text")])
export type ExtDataField = v.InferOutput<typeof ExtDataField>

View File

@ -17,7 +17,8 @@ export const extensions = sqliteTable("extensions", {
extId: integer("ext_id").primaryKey({ autoIncrement: true }),
identifier: text().notNull(),
version: text().notNull(),
enabled: numeric().default(sql`(TRUE)`),
// enabled: numeric().default(sql`(TRUE)`),
enabled: integer({ mode: "boolean" }),
path: text(),
data: numeric(),
installedAt: numeric("installed_at").default(sql`(CURRENT_TIMESTAMP)`)

View File

@ -1,2 +1,3 @@
export * as schema from "./drizzle/schema"
export * as relations from "./drizzle/relations"
export * as db from "./src/apis"

View File

@ -4,6 +4,7 @@
"private": true,
"exports": {
".": "./index.ts",
"./api": "./src/apis.ts",
"./schema": "./drizzle/schema.ts",
"./relations": "./drizzle/relations.ts"
},

View File

@ -1,53 +1,80 @@
import * as relations from "@kksh/drizzle/relations"
import * as schema from "../drizzle/schema"
import { KUNKUN_EXT_IDENTIFIER, type IDb, type IKV } from "@kksh/api"
import {
CmdType,
Ext,
ExtCmd,
ExtData,
SearchMode,
SearchModeEnum,
SQLSortOrder,
SQLSortOrderEnum
CmdType,
convertDateToSqliteString,
Ext,
ExtCmd,
ExtData,
SearchMode,
SearchModeEnum,
SQLSortOrder,
SQLSortOrderEnum
} from "@kksh/api/models"
import * as relations from "@kksh/drizzle/relations"
import * as orm from "drizzle-orm"
import type { SelectedFields } from "drizzle-orm/sqlite-core"
import * as v from "valibot"
import * as schema from "../drizzle/schema"
import { db } from "./proxy"
/* -------------------------------------------------------------------------- */
/* Built-in Extensions */
/* -------------------------------------------------------------------------- */
export function getExtClipboard() {
// return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER)
export function getExtClipboard(): Promise<Ext> {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER)
}
export function getExtQuickLinks() {
// return getExtensionByIdentifierExpectExists(
// KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER
// )
export function getExtQuickLinks(): Promise<Ext> {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER
)
}
export function getExtRemote() {
// return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER)
export function getExtRemote(): Promise<Ext> {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER)
}
export function getExtScriptCmd() {
// return getExtensionByIdentifierExpectExists(
// KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER
// )
export function getExtScriptCmd(): Promise<Ext> {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER
)
}
export function getExtDev() {
// return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER)
export function getExtDev(): Promise<Ext> {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER)
}
/* -------------------------------------------------------------------------- */
/* Extension CRUD */
/* -------------------------------------------------------------------------- */
export function createExtension(ext: {
identifier: string
version: string
enabled?: boolean
path?: string
data?: any
}) {
return db
.insert(schema.extensions)
.values({
identifier: ext.identifier,
version: ext.version,
enabled: ext.enabled,
path: ext.path,
data: ext.data
})
.run()
}
export async function getUniqueExtensionByIdentifier(identifier: string): Promise<Ext | undefined> {
const ext = await db
.select()
.from(schema.extensions)
.where(orm.eq(schema.extensions.identifier, identifier))
.get()
return v.parse(v.optional(Ext), ext)
const ext = await db
.select()
.from(schema.extensions)
.where(orm.eq(schema.extensions.identifier, identifier))
.get()
const result = v.safeParse(v.optional(Ext), ext)
if (!result.success) {
console.error("Failed to parse extension:", v.flatten(result.issues))
return undefined
}
const parsed = result.output
return parsed
}
/**
@ -56,17 +83,17 @@ export async function getUniqueExtensionByIdentifier(identifier: string): Promis
* @returns
*/
export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> {
return getUniqueExtensionByIdentifier(identifier).then((ext) => {
if (!ext) {
throw new Error(`Unexpexted Error: Extension ${identifier} not found`)
}
return ext
})
return getUniqueExtensionByIdentifier(identifier).then((ext) => {
if (!ext) {
throw new Error(`Unexpexted Error: Extension ${identifier} not found`)
}
return ext
})
}
export async function getAllExtensions(): Promise<Ext[]> {
const exts = await db.select().from(schema.extensions).all()
return v.parse(v.array(Ext), exts)
const exts = await db.select().from(schema.extensions).all()
return v.parse(v.array(Ext), exts)
}
/**
@ -75,112 +102,121 @@ export async function getAllExtensions(): Promise<Ext[]> {
* @param path
*/
export async function getUniqueExtensionByPath(path: string) {
const ext = await db
.select()
.from(schema.extensions)
.where(orm.eq(schema.extensions.path, path))
.get()
return v.parse(Ext, ext)
const ext = await db
.select()
.from(schema.extensions)
.where(orm.eq(schema.extensions.path, path))
.get()
return v.parse(Ext, ext)
}
export function getAllExtensionsByIdentifier(identifier: string): Promise<Ext[]> {
return db
.select()
.from(schema.extensions)
.where(orm.eq(schema.extensions.identifier, identifier))
.all()
.then((exts) => v.parse(v.array(Ext), exts))
return db
.select()
.from(schema.extensions)
.where(orm.eq(schema.extensions.identifier, identifier))
.all()
.then((exts) => v.parse(v.array(Ext), exts))
}
export function deleteExtensionByPath(path: string): Promise<void> {
return db
.delete(schema.extensions)
.where(orm.eq(schema.extensions.path, path))
.run()
.then(() => undefined)
return db
.delete(schema.extensions)
.where(orm.eq(schema.extensions.path, path))
.run()
.then(() => undefined)
}
export function deleteExtensionByExtId(extId: number): Promise<void> {
return db
.delete(schema.extensions)
.where(orm.eq(schema.extensions.extId, extId))
.run()
.then(() => undefined)
return db
.delete(schema.extensions)
.where(orm.eq(schema.extensions.extId, extId))
.run()
.then(() => undefined)
}
/* -------------------------------------------------------------------------- */
/* Extension Command CRUD */
/* -------------------------------------------------------------------------- */
// export async function getExtensionWithCmdsByIdentifier(identifier: string): Promise<ExtWithCmds> {
// const ext = await db
// .select({
// ...schema.extensions,
// commands: relations.commandsRelations
// })
// .from(schema.extensions)
// .leftJoin(schema.commands, orm.eq(schema.extensions.extId, schema.commands.extId))
// .where(orm.eq(schema.extensions.identifier, identifier))
// .get()
// // return v.parse(v.nullable(ExtWithCmds), ext);
// }
export function createCommand(data: {
extId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled?: boolean
}) {
return db
.insert(schema.commands)
.values({
extId: data.extId,
name: data.name,
type: data.cmdType,
data: data.data,
alias: data.alias,
hotkey: data.hotkey,
enabled: data.enabled ?? true
})
.run()
.then(() => undefined)
}
export async function getCmdById(cmdId: number): Promise<ExtCmd> {
const cmd = await db
.select()
.from(schema.commands)
.where(orm.eq(schema.commands.cmdId, cmdId))
.get()
return v.parse(ExtCmd, cmd)
const cmd = await db
.select()
.from(schema.commands)
.where(orm.eq(schema.commands.cmdId, cmdId))
.get()
return v.parse(ExtCmd, cmd)
}
export async function getAllCmds(): Promise<ExtCmd[]> {
const cmds = await db.select().from(schema.commands).all()
return v.parse(v.array(ExtCmd), cmds)
const cmds = await db.select().from(schema.commands).all()
return v.parse(v.array(ExtCmd), cmds)
}
export function getCommandsByExtId(extId: number) {
return db
.select()
.from(schema.commands)
.where(orm.eq(schema.commands.extId, extId))
.all()
.then((cmds) => v.parse(v.array(ExtCmd), cmds))
return db
.select()
.from(schema.commands)
.where(orm.eq(schema.commands.extId, extId))
.all()
.then((cmds) => v.parse(v.array(ExtCmd), cmds))
}
export function deleteCmdById(cmdId: number) {
return db
.delete(schema.commands)
.where(orm.eq(schema.commands.cmdId, cmdId))
.run()
.then(() => undefined)
return db
.delete(schema.commands)
.where(orm.eq(schema.commands.cmdId, cmdId))
.run()
.then(() => undefined)
}
export function updateCmdByID(data: {
cmdId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled: boolean
cmdId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled: boolean
}) {
return db
.update(schema.commands)
.set({
name: data.name,
type: data.cmdType,
data: data.data,
alias: data.alias, // optional
hotkey: data.hotkey, // optional
enabled: data.enabled
// in drizzle schema, use integer({ mode: 'boolean' }) for boolean sqlite
// enabled: data.enabled ? String(data.enabled) : undefined
})
.where(orm.eq(schema.commands.cmdId, data.cmdId))
.run()
.then(() => undefined)
return db
.update(schema.commands)
.set({
name: data.name,
type: data.cmdType,
data: data.data,
alias: data.alias, // optional
hotkey: data.hotkey, // optional
enabled: data.enabled
// in drizzle schema, use integer({ mode: 'boolean' }) for boolean sqlite
// enabled: data.enabled ? String(data.enabled) : undefined
})
.where(orm.eq(schema.commands.cmdId, data.cmdId))
.run()
.then(() => undefined)
}
/* -------------------------------------------------------------------------- */
@ -190,200 +226,391 @@ export const ExtDataField = v.union([v.literal("data"), v.literal("search_text")
export type ExtDataField = v.InferOutput<typeof ExtDataField>
function convertRawExtDataToExtData(rawData?: {
createdAt: string
updatedAt: string
data: null | string
searchText?: null | string
dataId: number
extId: number
dataType: string
createdAt: string
updatedAt: string
data: null | string
searchText?: null | string
dataId: number
extId: number
dataType: string
}): ExtData | undefined {
if (!rawData) {
return rawData
}
const parsedRes = v.safeParse(ExtData, {
...rawData,
createdAt: new Date(rawData.createdAt),
updatedAt: new Date(rawData.updatedAt),
data: rawData.data ?? undefined,
searchText: rawData.searchText ?? undefined
})
if (parsedRes.success) {
return parsedRes.output
} else {
console.error("Extension Data Parse Failure", parsedRes.issues)
throw new Error("Fail to parse extension data")
}
if (!rawData) {
return rawData
}
const parsedRes = v.safeParse(ExtData, {
...rawData,
createdAt: new Date(rawData.createdAt),
updatedAt: new Date(rawData.updatedAt),
data: rawData.data ?? undefined,
searchText: rawData.searchText ?? undefined
})
if (parsedRes.success) {
return parsedRes.output
} else {
console.error("Extension Data Parse Failure", parsedRes.issues)
throw new Error("Fail to parse extension data")
}
}
export function createExtensionData(data: {
extId: number
dataType: string
data: string
searchText?: string
extId: number
dataType: string
data: string
searchText?: string
}) {
return db.insert(schema.extensionData).values(data).run()
return db.insert(schema.extensionData).values(data).run()
}
export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) {
const _fields = fields ?? []
const selectQuery: SelectedFields = {
dataId: schema.extensionData.dataId,
extId: schema.extensionData.extId,
dataType: schema.extensionData.dataType,
metadata: schema.extensionData.metadata,
createdAt: schema.extensionData.createdAt,
updatedAt: schema.extensionData.updatedAt
// data: schema.extensionData.data,
// searchText: schema.extensionData.searchText
}
if (_fields.includes("data")) {
selectQuery["data"] = schema.extensionData.data
}
if (_fields.includes("search_text")) {
selectQuery["searchText"] = schema.extensionData.searchText
}
return db
.select(selectQuery)
.from(schema.extensionData)
.where(orm.eq(schema.extensionData.dataId, dataId))
.get()
.then((rawData) => {
console.log("Raw Data", rawData)
// @ts-expect-error - rawData is unknown, but will be safe parsed with valibot
return convertRawExtDataToExtData(rawData)
})
const _fields = fields ?? []
const selectQuery: SelectedFields = {
dataId: schema.extensionData.dataId,
extId: schema.extensionData.extId,
dataType: schema.extensionData.dataType,
metadata: schema.extensionData.metadata,
createdAt: schema.extensionData.createdAt,
updatedAt: schema.extensionData.updatedAt
// data: schema.extensionData.data,
// searchText: schema.extensionData.searchText
}
if (_fields.includes("data")) {
selectQuery["data"] = schema.extensionData.data
}
if (_fields.includes("search_text")) {
selectQuery["searchText"] = schema.extensionData.searchText
}
return db
.select(selectQuery)
.from(schema.extensionData)
.where(orm.eq(schema.extensionData.dataId, dataId))
.get()
.then((rawData) => {
console.log("Raw Data", rawData)
// @ts-expect-error - rawData is unknown, but will be safe parsed with valibot
return convertRawExtDataToExtData(rawData)
})
}
export async function searchExtensionData(searchParams: {
extId: number
searchMode: SearchMode
dataId?: number
dataType?: string
searchText?: string
afterCreatedAt?: string
beforeCreatedAt?: string
limit?: number
offset?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
extId: number
searchMode: SearchMode
dataId?: number
dataType?: string
searchText?: string
afterCreatedAt?: string
beforeCreatedAt?: string
limit?: number
offset?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const fields = v.parse(v.optional(v.array(ExtDataField), []), searchParams.fields)
const _fields = fields ?? []
const fields = v.parse(v.optional(v.array(ExtDataField), []), searchParams.fields)
const _fields = fields ?? []
// Build the select query based on fields
const selectQuery: SelectedFields = {
dataId: schema.extensionData.dataId,
extId: schema.extensionData.extId,
dataType: schema.extensionData.dataType,
createdAt: schema.extensionData.createdAt,
updatedAt: schema.extensionData.updatedAt
}
// Build the select query based on fields
const selectQuery: SelectedFields = {
dataId: schema.extensionData.dataId,
extId: schema.extensionData.extId,
dataType: schema.extensionData.dataType,
createdAt: schema.extensionData.createdAt,
updatedAt: schema.extensionData.updatedAt
}
if (_fields.includes("data")) {
selectQuery["data"] = schema.extensionData.data
}
if (_fields.includes("search_text")) {
selectQuery["searchText"] = schema.extensionData.searchText
}
if (_fields.includes("data")) {
selectQuery["data"] = schema.extensionData.data
}
if (_fields.includes("search_text")) {
selectQuery["searchText"] = schema.extensionData.searchText
}
// Build the query
let baseQuery = db.select(selectQuery).from(schema.extensionData)
// Build the query
let baseQuery = db.select(selectQuery).from(schema.extensionData)
// Add FTS join if needed
if (searchParams.searchMode === SearchModeEnum.FTS && searchParams.searchText) {
// @ts-expect-error - The join type is correct but TypeScript can't infer it properly
baseQuery = baseQuery.innerJoin(
schema.extensionDataFts,
orm.eq(schema.extensionData.dataId, schema.extensionDataFts.dataId)
)
}
// Add FTS join if needed
if (searchParams.searchMode === SearchModeEnum.FTS && searchParams.searchText) {
// @ts-expect-error - The join type is correct but TypeScript can't infer it properly
baseQuery = baseQuery.innerJoin(
schema.extensionDataFts,
orm.eq(schema.extensionData.dataId, schema.extensionDataFts.dataId)
)
}
// Add conditions
const conditions = [orm.eq(schema.extensionData.extId, searchParams.extId)]
// Add conditions
const conditions = [orm.eq(schema.extensionData.extId, searchParams.extId)]
if (searchParams.dataId) {
conditions.push(orm.eq(schema.extensionData.dataId, searchParams.dataId))
}
if (searchParams.dataId) {
conditions.push(orm.eq(schema.extensionData.dataId, searchParams.dataId))
}
if (searchParams.dataType) {
conditions.push(orm.eq(schema.extensionData.dataType, searchParams.dataType))
}
if (searchParams.dataType) {
conditions.push(orm.eq(schema.extensionData.dataType, searchParams.dataType))
}
if (searchParams.searchText) {
switch (searchParams.searchMode) {
case SearchModeEnum.ExactMatch:
conditions.push(orm.eq(schema.extensionData.searchText, searchParams.searchText))
break
case SearchModeEnum.Like:
conditions.push(orm.like(schema.extensionData.searchText, `%${searchParams.searchText}%`))
break
case SearchModeEnum.FTS:
conditions.push(
orm.sql`${schema.extensionDataFts.searchText} MATCH ${searchParams.searchText}`
)
break
}
}
if (searchParams.searchText) {
switch (searchParams.searchMode) {
case SearchModeEnum.ExactMatch:
conditions.push(orm.eq(schema.extensionData.searchText, searchParams.searchText))
break
case SearchModeEnum.Like:
conditions.push(orm.like(schema.extensionData.searchText, `%${searchParams.searchText}%`))
break
case SearchModeEnum.FTS:
conditions.push(
orm.sql`${schema.extensionDataFts.searchText} MATCH ${searchParams.searchText}`
)
break
}
}
if (searchParams.afterCreatedAt) {
conditions.push(orm.gt(schema.extensionData.createdAt, searchParams.afterCreatedAt))
}
if (searchParams.afterCreatedAt) {
conditions.push(orm.gt(schema.extensionData.createdAt, searchParams.afterCreatedAt))
}
if (searchParams.beforeCreatedAt) {
conditions.push(orm.lt(schema.extensionData.createdAt, searchParams.beforeCreatedAt))
}
if (searchParams.beforeCreatedAt) {
conditions.push(orm.lt(schema.extensionData.createdAt, searchParams.beforeCreatedAt))
}
// Build the final query with all conditions and modifiers
const query = baseQuery
.where(orm.and(...conditions))
.orderBy(
searchParams.orderByCreatedAt
? searchParams.orderByCreatedAt === SQLSortOrderEnum.Asc
? orm.asc(schema.extensionData.createdAt)
: orm.desc(schema.extensionData.createdAt)
: searchParams.orderByUpdatedAt
? searchParams.orderByUpdatedAt === SQLSortOrderEnum.Asc
? orm.asc(schema.extensionData.updatedAt)
: orm.desc(schema.extensionData.updatedAt)
: orm.asc(schema.extensionData.createdAt) // Default ordering
)
.limit(searchParams.limit ?? 100) // Default limit
.offset(searchParams.offset ?? 0) // Default offset
// Build the final query with all conditions and modifiers
const query = baseQuery
.where(orm.and(...conditions))
.orderBy(
searchParams.orderByCreatedAt
? searchParams.orderByCreatedAt === SQLSortOrderEnum.Asc
? orm.asc(schema.extensionData.createdAt)
: orm.desc(schema.extensionData.createdAt)
: searchParams.orderByUpdatedAt
? searchParams.orderByUpdatedAt === SQLSortOrderEnum.Asc
? orm.asc(schema.extensionData.updatedAt)
: orm.desc(schema.extensionData.updatedAt)
: orm.asc(schema.extensionData.createdAt) // Default ordering
)
.limit(searchParams.limit ?? 100) // Default limit
.offset(searchParams.offset ?? 0) // Default offset
// Execute query and convert results
const results = await query.all()
return results
.map((rawData) => {
// @ts-expect-error - rawData is unknown, but will be safe parsed with valibot
return convertRawExtDataToExtData(rawData)
})
.filter((item): item is ExtData => item !== undefined)
// Execute query and convert results
const results = await query.all()
return results
.map((rawData) => {
// @ts-expect-error - rawData is unknown, but will be safe parsed with valibot
return convertRawExtDataToExtData(rawData)
})
.filter((item): item is ExtData => item !== undefined)
}
export function deleteExtensionDataById(dataId: number) {
// return invoke<void>(generateJarvisPluginCommand("delete_extension_data_by_id"), { dataId })
return db
.delete(schema.extensionData)
.where(orm.eq(schema.extensionData.dataId, dataId))
.run()
.then(() => undefined)
}
export function updateExtensionDataById(data: {
dataId: number
data: string
searchText?: string
}) {
// return invoke<void>(generateJarvisPluginCommand("update_extension_data_by_id"), data)
}): Promise<void> {
return db
.update(schema.extensionData)
.set({
data: data.data,
searchText: data.searchText
})
.where(orm.eq(schema.extensionData.dataId, data.dataId))
.run()
.then(() => undefined)
}
/**
* Database API for extensions.
* Extensions shouldn't have full access to the database, they can only access their own data.
* When an extension is loaded, the main thread will create an instance of this class and
* expose it to the extension.
*/
export class JarvisExtDB implements IDb {
extId: number
// export async function getNCommands(n: number):
// export function createExtension(ext: {
// identifier: string
// version: string
// enabled?: boolean
// path?: string
// data?: any
// }) {
// return invoke<void>(generateJarvisPluginCommand("create_extension"), ext)
// }
constructor(extId: number) {
this.extId = extId
}
async add(data: { data: string; dataType?: string; searchText?: string }): Promise<void> {
return createExtensionData({
data: data.data,
dataType: data.dataType ?? "default",
searchText: data.searchText,
extId: this.extId
}).then(() => undefined)
}
async delete(dataId: number): Promise<void> {
// Verify if this data belongs to this extension
const d = await getExtensionDataById(dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return await deleteExtensionDataById(dataId)
}
async search(searchParams: {
dataId?: number
searchMode?: SearchMode
dataType?: string
searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const beforeCreatedAt = searchParams.beforeCreatedAt
? convertDateToSqliteString(searchParams.beforeCreatedAt)
: undefined
const afterCreatedAt = searchParams.afterCreatedAt
? convertDateToSqliteString(searchParams.afterCreatedAt)
: undefined
return searchExtensionData({
...searchParams,
searchMode: searchParams.searchMode ?? SearchModeEnum.FTS,
extId: this.extId,
beforeCreatedAt,
afterCreatedAt
})
}
/**
* Retrieve all data of this extension.
* Use `search()` method for more advanced search.
* @param options optional fields to retrieve. By default, data and searchText are not returned.
* @returns
*/
retrieveAll(options: { fields?: ExtDataField[] }): Promise<ExtData[]> {
return this.search({ fields: options.fields })
}
/**
* Retrieve all data of this extension by type.
* Use `search()` method for more advanced search.
* @param dataType
* @returns
*/
retrieveAllByType(dataType: string): Promise<ExtData[]> {
return this.search({ dataType })
}
/**
* Delete all data of this extension.
*/
deleteAll(): Promise<void> {
return this.search({})
.then((items) => {
return Promise.all(items.map((item) => this.delete(item.dataId)))
})
.then(() => {})
}
/**
* Update data and searchText of this extension.
* @param dataId unique id of the data
* @param data
* @param searchText
* @returns
*/
async update(data: { dataId: number; data: string; searchText?: string }): Promise<void> {
const d = await getExtensionDataById(data.dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return updateExtensionDataById(data)
}
}
export class KV implements IKV {
extId: number
db: JarvisExtDB
private DataType: string = "kunkun_kv"
constructor(extId: number) {
this.extId = extId
this.db = new JarvisExtDB(extId)
}
get<T = string>(key: string): Promise<T | null | undefined> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: ["search_text", "data"]
})
.then((items) => {
if (items.length === 0) {
return null
} else if (items.length > 1) {
throw new Error("Multiple KVs with the same key")
}
return items[0]?.data ? (JSON.parse(items[0].data).value as T) : null
})
.catch((err) => {
console.warn(err)
return null
})
}
set(key: string, value: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
if (items.length === 0) {
return this.db.add({
data: JSON.stringify({ value: value }),
dataType: this.DataType,
searchText: key
})
} else if (items.length === 1) {
return this.db.update({
dataId: items[0]!.dataId,
data: JSON.stringify({ value: value }),
searchText: key
})
} else {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
}
})
}
delete(key: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
})
}
exists(key: string): Promise<boolean> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: []
})
.then((items) => {
return items.length > 0
})
}
}

View File

@ -1,52 +1,47 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { db as dbCmd } from "@kksh/api/commands"
import * as schema from "../drizzle/schema"
import { sql } from "@kksh/api/commands"
import { error } from "@tauri-apps/plugin-log"
import { drizzle } from "drizzle-orm/sqlite-proxy"
/**
* Loads the sqlite database via the Tauri Proxy.
*/
// export const sqlite = await Database.load("sqlite:test.db");
import * as schema from "../drizzle/schema"
/**
* The drizzle database instance.
*/
export const db = drizzle<typeof schema>(
async (sql, params, method) => {
let rows: any = []
let results = []
console.log({
sql,
params,
method
})
console.log(sql)
// If the query is a SELECT, use the select method
if (isSelectQuery(sql)) {
rows = await dbCmd.select(sql, params).catch((e) => {
error("SQL Error:", e)
return []
})
} else {
// Otherwise, use the execute method
rows = await dbCmd.execute(sql, params).catch((e) => {
error("SQL Error:", e)
return []
})
return { rows: [] }
}
async (sqlQuery, params, method) => {
let rows: any = []
let results = []
console.log({
sql: sqlQuery,
params,
method
})
console.log(sqlQuery)
// If the query is a SELECT, use the select method
if (isSelectQuery(sqlQuery)) {
rows = await sql.select(sqlQuery, params).catch((e) => {
error("SQL Error:", e)
return []
})
} else {
// Otherwise, use the execute method
rows = await sql.execute(sqlQuery, params).catch((e) => {
error("SQL Error:", e)
return []
})
return { rows: [] }
}
rows = rows.map((row: any) => {
return Object.values(row)
})
rows = rows.map((row: any) => {
return Object.values(row)
})
// If the method is "all", return all rows
results = method === "all" ? rows : rows[0]
return { rows: results }
},
// Pass the schema to the drizzle instance
{ schema: schema, logger: true }
// If the method is "all", return all rows
results = method === "all" ? rows : rows[0]
return { rows: results }
},
// Pass the schema to the drizzle instance
{ schema: schema, logger: true }
)
/**
@ -55,6 +50,6 @@ export const db = drizzle<typeof schema>(
* @returns True if the query is a SELECT query, false otherwise.
*/
function isSelectQuery(sql: string): boolean {
const selectRegex = /^\s*SELECT\b/i
return selectRegex.test(sql)
const selectRegex = /^\s*SELECT\b/i
return selectRegex.test(sql)
}

View File

@ -16,6 +16,7 @@
},
"dependencies": {
"@kksh/api": "workspace:*",
"@kksh/drizzle": "workspace:*",
"@std/semver": "npm:@jsr/std__semver@^1.0.4",
"@tauri-apps/plugin-upload": "^2.2.1",
"semver": "^7.7.1",

View File

@ -1,14 +1,6 @@
import { db } from "@kksh/api/commands"
import {
CmdTypeEnum,
ExtCmd,
ExtPackageJson,
ExtPackageJsonExtra,
Icon,
QuickLinkCmd
} from "@kksh/api/models"
import { CmdTypeEnum, ExtPackageJson, Icon, QuickLinkCmd } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import * as v from "valibot"
import { isExtPathInDev } from "./utils"
export async function upsertExtension(extPkgJson: ExtPackageJson, extFullPath: string) {
const extInDb = await db.getUniqueExtensionByIdentifier(extPkgJson.kunkun.identifier)
@ -39,7 +31,7 @@ export async function createQuickLinkCommand(name: string, link: string, icon: I
export async function getAllQuickLinkCommands(): Promise<QuickLinkCmd[]> {
const extension = await db.getExtQuickLinks()
const cmds = await db.getCommandsByExtId(extension.extId)
return cmds
const parsedCmds = cmds
.map((cmd) => {
try {
cmd.data = JSON.parse(cmd.data)
@ -55,4 +47,5 @@ export async function getAllQuickLinkCommands(): Promise<QuickLinkCmd[]> {
}
})
.filter((cmd) => cmd !== null)
return parsedCmds
}

View File

@ -3,8 +3,9 @@
* including install, uninstall, upgrade, check app-extension compatibility, etc.
*/
import { isCompatible } from "@kksh/api"
import { copy_dir_all, db, decompressTarball } from "@kksh/api/commands"
import { copy_dir_all, decompressTarball } from "@kksh/api/commands"
import type { ExtensionStoreListItem, ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { greaterThan, parse as parseSemver } from "@std/semver"
import * as path from "@tauri-apps/api/path"
import * as dialog from "@tauri-apps/plugin-dialog"

View File

@ -1,5 +1,5 @@
import { db } from "@kksh/api/commands"
import { ExtPackageJson, ExtPackageJsonExtra, License } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { basename, dirname, join } from "@tauri-apps/api/path"
import { readDir, readTextFile } from "@tauri-apps/plugin-fs"
import { debug, error } from "@tauri-apps/plugin-log"
@ -77,7 +77,7 @@ export function loadAllExtensionsFromDisk(
* @returns loaded extensions
*/
export async function loadAllExtensionsFromDb(): Promise<ExtPackageJsonExtra[]> {
const allDbExts = await (await db.getAllExtensions()).filter((ext) => ext.path)
const allDbExts = (await db.getAllExtensions()).filter((ext) => ext.path)
const results: ExtPackageJsonExtra[] = []
for (const ext of allDbExts) {
if (!ext.path) continue

View File

@ -81,23 +81,23 @@ const COMMANDS: &[&str] = &[
// "ext_store_wrapper_save",
"get_server_port",
/* ----------------------------- sqlite database ---------------------------- */
"create_extension",
"get_all_extensions",
"get_unique_extension_by_identifier",
"get_unique_extension_by_path",
"get_all_extensions_by_identifier",
"delete_extension_by_path",
"delete_extension_by_ext_id",
"create_command",
"get_command_by_id",
"get_commands_by_ext_id",
"delete_command_by_id",
"update_command_by_id",
"create_extension_data",
"get_extension_data_by_id",
"search_extension_data",
"delete_extension_data_by_id",
"update_extension_data_by_id",
// "create_extension",
// "get_all_extensions",
// "get_unique_extension_by_identifier",
// "get_unique_extension_by_path",
// "get_all_extensions_by_identifier",
// "delete_extension_by_path",
// "delete_extension_by_ext_id",
// "create_command",
// "get_command_by_id",
// "get_commands_by_ext_id",
// "delete_command_by_id",
// "update_command_by_id",
// "create_extension_data",
// "get_extension_data_by_id",
// "search_extension_data",
// "delete_extension_data_by_id",
// "update_extension_data_by_id",
"select",
"execute",
/* -------------------------------- Clipboard ------------------------------- */

View File

@ -22,231 +22,6 @@ impl DBState {
}
}
/* -------------------------------------------------------------------------- */
/* Extension CRUD */
/* -------------------------------------------------------------------------- */
#[tauri::command]
pub async fn create_extension(
db: State<'_, DBState>,
identifier: &str,
version: &str,
enabled: Option<bool>,
path: Option<&str>,
data: Option<&str>,
) -> Result<(), String> {
db.db
.lock()
.unwrap()
.create_extension(identifier, version, enabled.unwrap_or(true), path, data)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn get_all_extensions(db: State<'_, DBState>) -> Result<Vec<Ext>, String> {
db.db
.lock()
.unwrap()
.get_all_extensions()
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn get_all_extensions_by_identifier(
identifier: &str,
db: State<'_, DBState>,
) -> Result<Vec<Ext>, String> {
db.db
.lock()
.unwrap()
.get_all_extensions_by_identifier(identifier)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn get_unique_extension_by_identifier(
identifier: &str,
db: State<'_, DBState>,
) -> Result<Option<Ext>, String> {
let ext = db
.db
.lock()
.unwrap()
.get_unique_extension_by_identifier(identifier)
.map_err(|err| err.to_string())?;
Ok(ext)
}
#[tauri::command]
pub async fn get_unique_extension_by_path(
path: &str,
db: State<'_, DBState>,
) -> Result<Option<Ext>, String> {
let ext = db
.db
.lock()
.unwrap()
.get_unique_extension_by_path(path)
.map_err(|err| err.to_string())?;
Ok(ext)
}
#[tauri::command]
pub async fn delete_extension_by_path(path: &str, db: State<'_, DBState>) -> Result<(), String> {
db.db
.lock()
.unwrap()
.delete_extension_by_path(path)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn delete_extension_by_ext_id(ext_id: i32, db: State<'_, DBState>) -> Result<(), String> {
db.db
.lock()
.unwrap()
.delete_extension_by_ext_id(ext_id)
.map_err(|err| err.to_string())
}
/* -------------------------------------------------------------------------- */
/* Extension Command CRUD */
/* -------------------------------------------------------------------------- */
#[tauri::command]
pub async fn create_command(
db: State<'_, DBState>,
ext_id: i32,
name: &str,
cmd_type: CmdType,
data: &str,
enabled: bool,
alias: Option<&str>,
hotkey: Option<&str>,
) -> Result<(), String> {
db.db
.lock()
.unwrap()
.create_command(ext_id, name, cmd_type, data, enabled, alias, hotkey)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn get_command_by_id(db: State<'_, DBState>, cmd_id: i32) -> Result<Option<Cmd>, String> {
db.db
.lock()
.unwrap()
.get_command_by_id(cmd_id)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn get_commands_by_ext_id(
db: State<'_, DBState>,
ext_id: i32,
) -> Result<Vec<Cmd>, String> {
db.db
.lock()
.unwrap()
.get_commands_by_ext_id(ext_id)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn delete_command_by_id(db: State<'_, DBState>, cmd_id: i32) -> Result<(), String> {
db.db
.lock()
.unwrap()
.delete_command_by_id(cmd_id)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn update_command_by_id(
db: State<'_, DBState>,
cmd_id: i32,
name: &str,
cmd_type: CmdType,
data: &str,
enabled: bool,
alias: Option<&str>,
hotkey: Option<&str>,
) -> Result<(), String> {
db.db
.lock()
.unwrap()
.update_command_by_id(cmd_id, name, cmd_type, data, enabled, alias, hotkey)
.map_err(|err| err.to_string())
}
/* -------------------------------------------------------------------------- */
/* Extension Data CRUD */
/* -------------------------------------------------------------------------- */
#[tauri::command]
pub async fn create_extension_data(
ext_id: i32,
data_type: &str,
data: &str,
search_text: Option<&str>,
db: State<'_, DBState>,
) -> Result<(), String> {
db.db
.lock()
.unwrap()
.create_extension_data(ext_id, data_type, data, search_text, None)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn get_extension_data_by_id(
data_id: i32,
fields: Option<Vec<ExtDataField>>,
db: State<'_, DBState>,
) -> Result<Option<ExtData>, String> {
db.db
.lock()
.unwrap()
.get_extension_data_by_id(data_id, fields)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn search_extension_data(
db: State<'_, DBState>,
search_query: ExtDataSearchQuery,
) -> Result<Vec<ExtData>, String> {
db.db
.lock()
.unwrap()
.search_extension_data(search_query)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn delete_extension_data_by_id(
data_id: i32,
db: State<'_, DBState>,
) -> Result<(), String> {
db.db
.lock()
.unwrap()
.delete_extension_data_by_id(data_id)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn update_extension_data_by_id(
data_id: i32,
data: &str,
search_text: Option<&str>,
db: State<'_, DBState>,
) -> Result<(), String> {
db.db
.lock()
.unwrap()
.update_extension_data_by_id(data_id, data, search_text)
.map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn select(
db: State<'_, DBState>,

View File

@ -142,24 +142,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
/* -------------------------------- database -------------------------------- */
commands::db::select,
commands::db::execute,
commands::db::create_extension,
commands::db::create_extension,
commands::db::get_all_extensions,
commands::db::get_unique_extension_by_identifier,
commands::db::get_unique_extension_by_path,
commands::db::get_all_extensions_by_identifier,
commands::db::delete_extension_by_path,
commands::db::delete_extension_by_ext_id,
commands::db::create_command,
commands::db::get_command_by_id,
commands::db::get_commands_by_ext_id,
commands::db::delete_command_by_id,
commands::db::update_command_by_id,
commands::db::create_extension_data,
commands::db::get_extension_data_by_id,
commands::db::search_extension_data,
commands::db::delete_extension_data_by_id,
commands::db::update_extension_data_by_id,
/* -------------------------------- Clipboard ------------------------------- */
commands::clipboard::get_history,
commands::clipboard::add_to_history,

View File

@ -2,6 +2,9 @@ import { LightMode, SearchPath } from "@kksh/api/models"
import type { Platform } from "@tauri-apps/plugin-os"
import * as v from "valibot"
export const LoadingAnimation = v.union([v.literal("spinning-circle"), v.literal("kunkun-dancing")])
export type LoadingAnimation = v.InferOutput<typeof LoadingAnimation>
export const PersistedAppConfig = v.object({
theme: v.object({
theme: v.string(),
@ -18,7 +21,8 @@ export const PersistedAppConfig = v.object({
joinBetaProgram: v.boolean(),
onBoarded: v.boolean(),
developerMode: v.boolean(),
appSearchPaths: v.array(SearchPath)
appSearchPaths: v.array(SearchPath),
loadingAnimation: LoadingAnimation
})
export type PersistedAppConfig = v.InferOutput<typeof PersistedAppConfig>

View File

@ -1,11 +1,9 @@
<script lang="ts">
import autoAnimate from "@formkit/auto-animate"
import Icon from "@iconify/svelte"
import { Button, ButtonModule, Collapsible, ScrollArea } from "@kksh/svelte5"
import { Error, Layouts, Shiki } from "@kksh/ui"
import { ChevronsUpDown } from "lucide-svelte"
import { type Snippet } from "svelte"
import { fade, slide } from "svelte/transition"
const {
title,

29
pnpm-lock.yaml generated
View File

@ -251,6 +251,9 @@ importers:
'@tauri-apps/plugin-stronghold':
specifier: ^2.2.0
version: 2.2.0
'@tauri-store/svelte':
specifier: ^2.1.1
version: 2.1.1
dompurify:
specifier: ^3.2.4
version: 3.2.4
@ -585,6 +588,9 @@ importers:
'@kksh/api':
specifier: workspace:*
version: link:../api
'@kksh/drizzle':
specifier: workspace:*
version: link:../drizzle
'@std/semver':
specifier: npm:@jsr/std__semver@^1.0.4
version: '@jsr/std__semver@1.0.3'
@ -5685,6 +5691,12 @@ packages:
'@tauri-store/shared@0.6.0':
resolution: {integrity: sha512-2KBezqqkw68HvvXHEtbbpxyQHDjymBUZl10YuAsNRI8DHFIA0n18WE7NRyQ93+H7IzDP1/B41m2/rcMDHBSiKw==}
'@tauri-store/shared@0.7.2':
resolution: {integrity: sha512-42nprNNeU+tjpCvYnaBsu3kYwAy8gP6KyXX9zeaIvpMys1tIhQdEiUAXo3KChHq/jCkSralXRLN4zfjCfBJFrw==}
'@tauri-store/svelte@2.1.1':
resolution: {integrity: sha512-exGvgEM6zcXZq6KRnG2b2JDXogyarRaJdjrblD27Q4IU1vhSTY8TxvDMCPGfD31kbOcf/aR4A6zT8OX0DFuxjg==}
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@ -8027,6 +8039,9 @@ packages:
resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
engines: {node: '>= 0.4'}
es-toolkit@1.34.1:
resolution: {integrity: sha512-OA6cd94fJV9bm8dWhIySkWq4xV+rAQnBZUr2dnpXam0QJ8c+hurLbKA8/QooL9Mx4WCAxvIDsiEkid5KPQ5xgQ==}
esbuild-register@3.6.0:
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
peerDependencies:
@ -9449,7 +9464,6 @@ packages:
libsql@0.5.3:
resolution: {integrity: sha512-S3WR8WNCJV1VXraBFUKjDA6+8LcNDJMLm+83qohm1O3YM1iVqV2+/XN3SXOxpxVjuL4g/rLrjO5kzygkPefCFQ==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
lilconfig@2.1.0:
@ -18251,6 +18265,17 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-store/shared@0.7.2':
dependencies:
'@tauri-apps/api': 2.3.0
es-toolkit: 1.34.1
'@tauri-store/svelte@2.1.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-store/shared': 0.7.2
svelte: 5.20.5
'@trysound/sax@0.2.0': {}
'@ts-graphviz/adapter@2.0.5':
@ -20945,6 +20970,8 @@ snapshots:
is-date-object: 1.0.5
is-symbol: 1.0.4
es-toolkit@1.34.1: {}
esbuild-register@3.6.0(esbuild@0.19.12):
dependencies:
debug: 4.4.0(supports-color@9.4.0)