mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-04-14 18:54:34 +00:00
[feat] troubleshooters (#15)
* feat: add extension loading troubleshooter * feat: add extension permission inspector * feat: add extension window map troubleshooter (WIP) * fix: unregister extension when window is closed
This commit is contained in:
parent
f64e562034
commit
2c99f231f7
apps/desktop
packages
api/src/commands
ui/src/components/extension
@ -30,7 +30,8 @@
|
|||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"svelte-radix": "^2.0.1",
|
"svelte-radix": "^2.0.1",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"sveltekit-superforms": "^2.20.0"
|
"sveltekit-superforms": "^2.20.0",
|
||||||
|
"uuid": "^11.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kksh/types": "workspace:*",
|
"@kksh/types": "workspace:*",
|
||||||
|
@ -2,10 +2,12 @@ import { appConfig, appState } from "@/stores"
|
|||||||
import { checkUpdateAndInstall } from "@/utils/updater"
|
import { checkUpdateAndInstall } from "@/utils/updater"
|
||||||
import type { BuiltinCmd } from "@kksh/ui/types"
|
import type { BuiltinCmd } from "@kksh/ui/types"
|
||||||
import { getVersion } from "@tauri-apps/api/app"
|
import { getVersion } from "@tauri-apps/api/app"
|
||||||
|
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
import { exit } from "@tauri-apps/plugin-process"
|
import { exit } from "@tauri-apps/plugin-process"
|
||||||
import { dev } from "$app/environment"
|
import { dev } from "$app/environment"
|
||||||
import { goto } from "$app/navigation"
|
import { goto } from "$app/navigation"
|
||||||
import { toast } from "svelte-sonner"
|
import { toast } from "svelte-sonner"
|
||||||
|
import { v4 as uuidv4 } from "uuid"
|
||||||
|
|
||||||
export const builtinCmds: BuiltinCmd[] = [
|
export const builtinCmds: BuiltinCmd[] = [
|
||||||
{
|
{
|
||||||
@ -79,42 +81,39 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
goto("/settings/set-dev-ext-path")
|
goto("/settings/set-dev-ext-path")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: "Extension Window Troubleshooter",
|
name: "Extension Window Troubleshooter",
|
||||||
// iconifyIcon: "material-symbols:window-outline",
|
iconifyIcon: "material-symbols:window-outline",
|
||||||
// description: "",
|
description: "",
|
||||||
// function: async () => {
|
function: async () => {
|
||||||
// const appStateStore = useAppStateStore()
|
appState.clearSearchTerm()
|
||||||
// appStateStore.setSearchTermSync("")
|
// goto("/window-troubleshooter")
|
||||||
// // goto("/window-troubleshooter")
|
const winLabel = `main:extension-window-troubleshooter-${uuidv4()}`
|
||||||
// const winLabel = `main:window-troubleshooter-${uuidv4()}`
|
console.log(winLabel)
|
||||||
// console.log(winLabel)
|
new WebviewWindow(winLabel, {
|
||||||
// new WebviewWindow(winLabel, {
|
url: "/troubleshooters/extension-window",
|
||||||
// url: "/window-troubleshooter",
|
title: "Extension Window Troubleshooter"
|
||||||
// title: "Window Troubleshooter"
|
})
|
||||||
// })
|
}
|
||||||
// }
|
},
|
||||||
// },
|
{
|
||||||
// {
|
name: "Extension Permission Inspector",
|
||||||
// name: "Extension Permission Inspector",
|
iconifyIcon: "hugeicons:inspect-code",
|
||||||
// iconifyIcon: "hugeicons:inspect-code",
|
description: "",
|
||||||
// description: "",
|
function: async () => {
|
||||||
// function: async () => {
|
appState.clearSearchTerm()
|
||||||
// const appStateStore = useAppStateStore()
|
goto("/extension/permission-inspector")
|
||||||
// appStateStore.setSearchTermSync("")
|
}
|
||||||
// goto("/ext-permission-inspector")
|
},
|
||||||
// }
|
{
|
||||||
// },
|
name: "Extension Loading Troubleshooter",
|
||||||
// {
|
iconifyIcon: "material-symbols:troubleshoot",
|
||||||
// name: "Extension Loading Troubleshooter",
|
description: "",
|
||||||
// iconifyIcon: "material-symbols:troubleshoot",
|
function: async () => {
|
||||||
// description: "",
|
appState.clearSearchTerm()
|
||||||
// function: async () => {
|
goto("/troubleshooters/extension-loading")
|
||||||
// const appStateStore = useAppStateStore()
|
}
|
||||||
// appStateStore.setSearchTermSync("")
|
},
|
||||||
// goto("/extension-load-troubleshooter")
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
// {
|
||||||
// name: "Create Quicklink",
|
// name: "Create Quicklink",
|
||||||
// iconifyIcon: "material-symbols:link",
|
// iconifyIcon: "material-symbols:link",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { appState } from "@/stores"
|
||||||
import { winExtMap } from "@/stores/winExtMap"
|
import { winExtMap } from "@/stores/winExtMap"
|
||||||
import { trimSlash } from "@/utils/url"
|
import { trimSlash } from "@/utils/url"
|
||||||
import { constructExtensionSupportDir } from "@kksh/api"
|
import { constructExtensionSupportDir } from "@kksh/api"
|
||||||
@ -48,6 +49,9 @@ export async function onCustomUiCmdSelect(
|
|||||||
})
|
})
|
||||||
console.log("Launch new window, ", winLabel)
|
console.log("Launch new window, ", winLabel)
|
||||||
const window = launchNewExtWindow(winLabel, url2, cmd.window)
|
const window = launchNewExtWindow(winLabel, url2, cmd.window)
|
||||||
|
window.onCloseRequested(async (event) => {
|
||||||
|
await winExtMap.unregisterExtensionFromWindow(winLabel)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log("Launch main window")
|
console.log("Launch main window")
|
||||||
return winExtMap
|
return winExtMap
|
||||||
@ -58,4 +62,5 @@ export async function onCustomUiCmdSelect(
|
|||||||
})
|
})
|
||||||
.then(() => goto(url2))
|
.then(() => goto(url2))
|
||||||
}
|
}
|
||||||
|
appState.clearSearchTerm()
|
||||||
}
|
}
|
||||||
|
@ -63,8 +63,6 @@ passing everything through props will be very complicated and hard to maintain.
|
|||||||
/>
|
/>
|
||||||
<Command.List class="max-h-screen grow">
|
<Command.List class="max-h-screen grow">
|
||||||
<Command.Empty data-tauri-drag-region>No results found.</Command.Empty>
|
<Command.Empty data-tauri-drag-region>No results found.</Command.Empty>
|
||||||
<BuiltinCmds {builtinCmds} />
|
|
||||||
<SystemCmds {systemCommands} />
|
|
||||||
{#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0}
|
{#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0}
|
||||||
<ExtCmdsGroup
|
<ExtCmdsGroup
|
||||||
extensions={$devStoreExts}
|
extensions={$devStoreExts}
|
||||||
@ -83,6 +81,8 @@ passing everything through props will be very complicated and hard to maintain.
|
|||||||
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
|
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
<BuiltinCmds {builtinCmds} />
|
||||||
|
<SystemCmds {systemCommands} />
|
||||||
<Command.Separator />
|
<Command.Separator />
|
||||||
</Command.List>
|
</Command.List>
|
||||||
<GlobalCommandPaletteFooter />
|
<GlobalCommandPaletteFooter />
|
||||||
|
@ -58,11 +58,11 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
|
|||||||
await killProcesses(winExtMap[windowLabel].pids)
|
await killProcesses(winExtMap[windowLabel].pids)
|
||||||
delete winExtMap[windowLabel]
|
delete winExtMap[windowLabel]
|
||||||
} else {
|
} else {
|
||||||
winExtMap[windowLabel] = {
|
// winExtMap[windowLabel] = {
|
||||||
windowLabel,
|
// windowLabel,
|
||||||
extPath,
|
// extPath,
|
||||||
pids: []
|
// pids: []
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const returnedWinLabel = await registerExtensionWindow({
|
const returnedWinLabel = await registerExtensionWindow({
|
||||||
@ -70,6 +70,11 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
|
|||||||
windowLabel,
|
windowLabel,
|
||||||
dist
|
dist
|
||||||
})
|
})
|
||||||
|
winExtMap[returnedWinLabel] = {
|
||||||
|
windowLabel: returnedWinLabel,
|
||||||
|
extPath,
|
||||||
|
pids: []
|
||||||
|
}
|
||||||
store.set(winExtMap)
|
store.set(winExtMap)
|
||||||
return returnedWinLabel
|
return returnedWinLabel
|
||||||
},
|
},
|
||||||
|
57
apps/desktop/src/lib/utils/tauri-events.ts
Normal file
57
apps/desktop/src/lib/utils/tauri-events.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { DEEP_LINK_PATH_REFRESH_DEV_EXTENSION } from "@kksh/api"
|
||||||
|
import {
|
||||||
|
emit,
|
||||||
|
emitTo,
|
||||||
|
listen,
|
||||||
|
TauriEvent,
|
||||||
|
type Event,
|
||||||
|
type EventCallback,
|
||||||
|
type UnlistenFn
|
||||||
|
} from "@tauri-apps/api/event"
|
||||||
|
|
||||||
|
export const FileDragDrop = "tauri://drag-drop"
|
||||||
|
export const FileDragEnter = "tauri://drag-enter"
|
||||||
|
export const FileDragLeave = "tauri://drag-leave"
|
||||||
|
export const FileDragOver = "tauri://drag-over"
|
||||||
|
export const NewClipboardItemAddedEvent = "new_clipboard_item_added"
|
||||||
|
export const RefreshConfigEvent = "kunkun://refresh-config"
|
||||||
|
export const RefreshExtEvent = "kunkun://refresh-extensions"
|
||||||
|
export function listenToFileDrop(cb: EventCallback<{ paths: string[] }>) {
|
||||||
|
return listen<{ paths: string[] }>(FileDragDrop, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenToWindowBlur(cb: EventCallback<null>) {
|
||||||
|
return listen(TauriEvent.WINDOW_BLUR, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenToWindowFocus(cb: EventCallback<null>) {
|
||||||
|
return listen(TauriEvent.WINDOW_FOCUS, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenToNewClipboardItem(cb: EventCallback<null>) {
|
||||||
|
return listen(NewClipboardItemAddedEvent, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitRefreshConfig() {
|
||||||
|
return emit(RefreshConfigEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenToRefreshConfig(cb: EventCallback<null>) {
|
||||||
|
return listen(RefreshConfigEvent, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitRefreshExt() {
|
||||||
|
return emitTo("main", RefreshExtEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenToRefreshExt(cb: EventCallback<null>) {
|
||||||
|
return listen(RefreshExtEvent, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitRefreshDevExt() {
|
||||||
|
return emit(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenToRefreshDevExt(cb: EventCallback<null>) {
|
||||||
|
return listen(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION, cb)
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goBackOnEscape } from "@/utils/key"
|
||||||
|
import { goBack } from "@/utils/route"
|
||||||
|
import { listenToFileDrop } from "@/utils/tauri-events"
|
||||||
|
import type { ExtPackageJsonExtra } from "@kksh/api/models"
|
||||||
|
import { loadExtensionManifestFromDisk } from "@kksh/extension"
|
||||||
|
import { Button, Card } from "@kksh/svelte5"
|
||||||
|
import { PermissionInspector } from "@kksh/ui/extension"
|
||||||
|
import type { UnlistenFn } from "@tauri-apps/api/event"
|
||||||
|
import { join } from "@tauri-apps/api/path"
|
||||||
|
import { getCurrentWebview } from "@tauri-apps/api/webview"
|
||||||
|
import { open as openDialog } from "@tauri-apps/plugin-dialog"
|
||||||
|
import { exists } from "@tauri-apps/plugin-fs"
|
||||||
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
import { toast } from "svelte-sonner"
|
||||||
|
|
||||||
|
let pkgJsons = $state<ExtPackageJsonExtra[]>([])
|
||||||
|
let unlistenDropEvt: UnlistenFn
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
unlistenDropEvt = await getCurrentWebview().onDragDropEvent((event) => {
|
||||||
|
if (event.payload.type === "drop") {
|
||||||
|
inspectPaths(event.payload.paths)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unlistenDropEvt?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function inspectPaths(paths: string[]) {
|
||||||
|
for (const path of paths) {
|
||||||
|
if (!(await exists(path))) {
|
||||||
|
toast.error("Selected path does not exist", { description: path })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const manifestPath = await join(path, "package.json")
|
||||||
|
if (!(await exists(manifestPath))) {
|
||||||
|
toast.error("Selected path is not an extension", { description: path })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
pkgJsons.push(await loadExtensionManifestFromDisk(manifestPath))
|
||||||
|
toast.success("Extension manifest loaded", { description: path })
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Failed to load extension manifest: ${err}`, { description: path })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onPick() {
|
||||||
|
const paths = await openDialog({
|
||||||
|
directory: true,
|
||||||
|
multiple: true
|
||||||
|
})
|
||||||
|
if (!paths) {
|
||||||
|
return toast.error("No folder selected")
|
||||||
|
}
|
||||||
|
inspectPaths(paths)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={goBackOnEscape} />
|
||||||
|
|
||||||
|
<main class="container w-screen pt-10">
|
||||||
|
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
|
||||||
|
<ArrowLeftIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<h1 class="text-2xl font-bold">Extension Permission Inspector</h1>
|
||||||
|
<Button class="my-5" onclick={onPick}>Pick Extension Folder to Inspect</Button>
|
||||||
|
<div class="mb-5 flex flex-col gap-4">
|
||||||
|
{#each pkgJsons as pkgJson}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{pkgJson.kunkun.name}</Card.Title>
|
||||||
|
<Card.Description>{pkgJson.kunkun.shortDescription}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<PermissionInspector manifest={pkgJson.kunkun} />
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Footer class="block">
|
||||||
|
<p class="text-sm">
|
||||||
|
<strong>Identifier:</strong> <code>{pkgJson.kunkun.identifier}</code>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
<strong>Extension Path:</strong> <code>{pkgJson.extPath}</code>
|
||||||
|
</p>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
@ -6,7 +6,7 @@
|
|||||||
import { ArrowLeftIcon } from "lucide-svelte"
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown|preventDefault={goBackOnEscape} />
|
<svelte:window on:keydown={goBackOnEscape} />
|
||||||
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
|
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
|
||||||
<ArrowLeftIcon class="h-4 w-4" />
|
<ArrowLeftIcon class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goBackOnEscape } from "@/utils/key"
|
||||||
|
import { goBack } from "@/utils/route"
|
||||||
|
import { db } from "@kksh/api/commands"
|
||||||
|
import { loadExtensionManifestFromDisk } from "@kksh/extension"
|
||||||
|
import { Button, Dialog, ScrollArea, Table } from "@kksh/svelte5"
|
||||||
|
import { join } from "@tauri-apps/api/path"
|
||||||
|
import { exists } from "@tauri-apps/plugin-fs"
|
||||||
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { toast } from "svelte-sonner"
|
||||||
|
import { open } from "tauri-plugin-shellx-api"
|
||||||
|
|
||||||
|
type Result = {
|
||||||
|
identifier: string
|
||||||
|
path: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = $state<Result[]>([])
|
||||||
|
let isDialogOpen = $state(false)
|
||||||
|
let errorMsg = $state<string | undefined>()
|
||||||
|
|
||||||
|
const sortedResults = $derived.by(() =>
|
||||||
|
results.slice().sort((a, b) => {
|
||||||
|
return a.error ? -1 : 1
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
async function check() {
|
||||||
|
results = []
|
||||||
|
const tmpResults = []
|
||||||
|
const extensions = await db.getAllExtensions()
|
||||||
|
for (const ext of extensions) {
|
||||||
|
if (!ext.path) continue
|
||||||
|
const _exists = await exists(ext.path)
|
||||||
|
let error: string | undefined = undefined
|
||||||
|
if (!_exists) {
|
||||||
|
error = `Extension path (${ext.path}) does not exist`
|
||||||
|
}
|
||||||
|
const pkgJsonPath = await join(ext.path, "package.json")
|
||||||
|
const _pkgJsonExists = await exists(pkgJsonPath)
|
||||||
|
if (!_pkgJsonExists) {
|
||||||
|
error = `Extension package.json (${pkgJsonPath}) does not exist`
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const manifest = await loadExtensionManifestFromDisk(pkgJsonPath)
|
||||||
|
} catch (err: any) {
|
||||||
|
error = `Failed to load manifest from ${pkgJsonPath}: ${err.message}`
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpResults.push({
|
||||||
|
identifier: ext.identifier,
|
||||||
|
path: ext.path,
|
||||||
|
error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
results = tmpResults
|
||||||
|
const numErrors = results.filter((r) => r.error).length
|
||||||
|
const toastFn = numErrors > 0 ? toast.error : toast.info
|
||||||
|
toastFn(`${numErrors} errors found`, {
|
||||||
|
description: numErrors > 0 ? "Click on an error to see more details" : undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onErrorClick(errMsg?: string) {
|
||||||
|
if (errMsg) {
|
||||||
|
isDialogOpen = true
|
||||||
|
errorMsg = errMsg
|
||||||
|
} else {
|
||||||
|
toast.info("No error message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
check()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={goBackOnEscape} />
|
||||||
|
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
|
||||||
|
<ArrowLeftIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div class="absolute left-0 top-0 h-10 w-screen" data-tauri-drag-region></div>
|
||||||
|
<div class="container pt-10">
|
||||||
|
<h1 class="text-2xl font-bold">Extension Loading Troubleshooter</h1>
|
||||||
|
<Button class="my-2" onclick={check}>Check</Button>
|
||||||
|
<Dialog.Root bind:open={isDialogOpen}>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Error Details</Dialog.Title>
|
||||||
|
</Dialog.Header>
|
||||||
|
{errorMsg}
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Caption>A list of your extensions.</Table.Caption>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Head class="">Identifier</Table.Head>
|
||||||
|
<Table.Head>Path</Table.Head>
|
||||||
|
<Table.Head>Error</Table.Head>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{#each sortedResults as row}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="font-medium"><pre>{row.identifier}</pre></Table.Cell>
|
||||||
|
<Table.Cell class="">
|
||||||
|
<button onclick={() => open(row.path)} class="text-left">
|
||||||
|
<pre class="cursor-pointer text-wrap">{row.path}</pre>
|
||||||
|
</button>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-right">
|
||||||
|
<button onclick={() => onErrorClick(row.error)}>
|
||||||
|
{row.error ? "⚠️" : "✅"}
|
||||||
|
</button>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { winExtMap } from "@/stores"
|
||||||
|
import { goBackOnEscape, goBackOnEscapeClearSearchTerm } from "@/utils/key"
|
||||||
|
import { goBack, goHome } from "@/utils/route"
|
||||||
|
import { getExtLabelMap, unregisterExtensionWindow } from "@kksh/api/commands"
|
||||||
|
import type { ExtensionLabelMap } from "@kksh/api/models"
|
||||||
|
import { Button, Checkbox, ScrollArea } from "@kksh/svelte5"
|
||||||
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
|
import { ArrowLeftIcon, TrashIcon } from "lucide-svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { toast } from "svelte-sonner"
|
||||||
|
|
||||||
|
const appWin = getCurrentWebviewWindow()
|
||||||
|
let winLabelMap = $state<ExtensionLabelMap>({})
|
||||||
|
let refreshEverySecond = $state(true)
|
||||||
|
let refreshCount = $state(0)
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const extLabelMap = await getExtLabelMap()
|
||||||
|
winLabelMap = extLabelMap
|
||||||
|
refreshCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshWinLabelMapRecursively() {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await refresh()
|
||||||
|
if (refreshEverySecond) {
|
||||||
|
refreshWinLabelMapRecursively()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const extLabelMap = await getExtLabelMap()
|
||||||
|
winLabelMap = extLabelMap
|
||||||
|
refreshCount = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (refreshEverySecond) {
|
||||||
|
refreshWinLabelMapRecursively()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function unregisterWindow(label: string) {
|
||||||
|
// winExtMap
|
||||||
|
// .unregisterExtensionFromWindow(label)
|
||||||
|
unregisterExtensionWindow(label)
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Unregistered window")
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error("Failed to unregister window", { description: err.message })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (appWin.label === "main") {
|
||||||
|
goHome()
|
||||||
|
} else {
|
||||||
|
appWin.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeyDown} />
|
||||||
|
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
|
||||||
|
<ArrowLeftIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<main class="container h-screen w-screen pt-10">
|
||||||
|
<div class="flex items-center justify-between space-x-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Checkbox id="refreshEverySecond" bind:checked={refreshEverySecond} />
|
||||||
|
<label
|
||||||
|
for="refreshEverySecond"
|
||||||
|
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Refresh Every Second
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<Button size="sm" onclick={refresh}>Refresh</Button>
|
||||||
|
<span>Refreshed {refreshCount} times</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ScrollArea class="py-5" orientation="both">
|
||||||
|
{#each Object.entries(winLabelMap) as [label, content]}
|
||||||
|
<li>
|
||||||
|
<span class="flex gap-2">
|
||||||
|
<strong>Label:</strong>
|
||||||
|
<pre class="text-lime">{label}</pre>
|
||||||
|
</span>
|
||||||
|
<ul class="pl-5">
|
||||||
|
{#each Object.entries(content) as [key, value]}
|
||||||
|
<li>
|
||||||
|
<span class="flex gap-2">
|
||||||
|
<strong>{key}:</strong>
|
||||||
|
<pre class="text-lime">{value}</pre>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<Button variant="destructive" size="icon" onclick={() => unregisterWindow(label)}>
|
||||||
|
<TrashIcon />
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ScrollArea>
|
||||||
|
</main>
|
@ -26,7 +26,6 @@ export function registerExtensionWindow(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function unregisterExtensionWindow(label: string): Promise<void> {
|
export function unregisterExtensionWindow(label: string): Promise<void> {
|
||||||
console.log("unregisterExtensionWindow", label)
|
|
||||||
return invoke(generateJarvisPluginCommand("unregister_extension_window"), {
|
return invoke(generateJarvisPluginCommand("unregister_extension_window"), {
|
||||||
label
|
label
|
||||||
})
|
})
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
<HoverCard.Root>
|
<HoverCard.Root>
|
||||||
<HoverCard.Trigger class="flex items-center">
|
<HoverCard.Trigger class="flex items-center">
|
||||||
<IconMultiplexer
|
<IconMultiplexer
|
||||||
class="border"
|
|
||||||
icon={{
|
icon={{
|
||||||
type: IconEnum.Iconify,
|
type: IconEnum.Iconify,
|
||||||
value: "material-symbols:info-outline"
|
value: "material-symbols:info-outline"
|
||||||
|
@ -126,7 +126,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<span class="flex items-center w-full" use:autoAnimate>
|
<span class="flex w-full items-center" use:autoAnimate>
|
||||||
<strong class="ext-name text-xl">{manifest?.name}</strong>
|
<strong class="ext-name text-xl">{manifest?.name}</strong>
|
||||||
{#if isInstalled}
|
{#if isInstalled}
|
||||||
<CircleCheckBigIcon class="ml-2 inline text-green-400" />
|
<CircleCheckBigIcon class="ml-2 inline text-green-400" />
|
||||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -165,6 +165,9 @@ importers:
|
|||||||
sveltekit-superforms:
|
sveltekit-superforms:
|
||||||
specifier: ^2.20.0
|
specifier: ^2.20.0
|
||||||
version: 2.20.0(@sveltejs/kit@2.7.4(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.9)(vite@5.4.10(@types/node@22.8.7)(terser@5.36.0)))(svelte@5.1.9)(vite@5.4.10(@types/node@22.8.7)(terser@5.36.0)))(@types/json-schema@7.0.15)(svelte@5.1.9)(typescript@5.6.3)
|
version: 2.20.0(@sveltejs/kit@2.7.4(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.9)(vite@5.4.10(@types/node@22.8.7)(terser@5.36.0)))(svelte@5.1.9)(vite@5.4.10(@types/node@22.8.7)(terser@5.36.0)))(@types/json-schema@7.0.15)(svelte@5.1.9)(typescript@5.6.3)
|
||||||
|
uuid:
|
||||||
|
specifier: ^11.0.2
|
||||||
|
version: 11.0.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@kksh/types':
|
'@kksh/types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user