Skip to content

Commit

Permalink
feat: add-dev-extension page
Browse files Browse the repository at this point in the history
  • Loading branch information
HuakunShen committed Nov 7, 2024
1 parent 83c277e commit 2e51aed
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 11 deletions.
19 changes: 9 additions & 10 deletions apps/desktop/src/lib/cmds/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,15 @@ export const builtinCmds: BuiltinCmd[] = [
}, 2_000)
}
},
// {
// name: "Add Dev Extension",
// iconifyIcon: "lineicons:dev",
// description: "",
// function: async () => {
// const appStateStore = useAppStateStore()
// appStateStore.setSearchTermSync("")
// goto("/add-dev-ext")
// }
// },
{
name: "Add Dev Extension",
iconifyIcon: "lineicons:dev",
description: "",
function: async () => {
appState.clearSearchTerm()
goto("/settings/add-dev-extension")
}
},
{
name: "Kunkun Version",
iconifyIcon: "stash:version-solid",
Expand Down
38 changes: 38 additions & 0 deletions apps/desktop/src/lib/components/common/DragNDrop.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
import { listen, TauriEvent, type UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { onDestroy, onMount, type Snippet } from "svelte"
let unlisteners: UnlistenFn[] = []
const {
children,
onEnter,
onDrop,
onCancelled,
onOver
}: {
children: Snippet
onEnter?: (event: any) => void
onDrop?: (event: any) => void
onCancelled?: (event: any) => void
onOver?: (event: any) => void
} = $props()
const appWin = getCurrentWebviewWindow()
onMount(async () => {
if (onEnter) await appWin.listen(TauriEvent.DRAG_ENTER, onEnter)
if (onDrop) await appWin.listen(TauriEvent.DRAG_DROP, onDrop)
if (onCancelled) await appWin.listen(TauriEvent.DRAG_LEAVE, onCancelled)
if (onOver) await appWin.listen(TauriEvent.DRAG_OVER, onOver)
})
onDestroy(() => {
for (const unlisten of unlisteners) {
unlisten()
}
})
</script>

<span>
{@render children()}
</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<script lang="ts">
import DragNDrop from "@/components/common/DragNDrop.svelte"
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
import { appConfig, extensions } from "@/stores"
import { goBackOnEscape } from "@/utils/key"
import { goBack } from "@/utils/route"
import { IconEnum } from "@kksh/api/models"
import * as extAPI from "@kksh/extension"
import { installFromNpmPackageName } from "@kksh/extension"
import { Button, Card } from "@kksh/svelte5"
import { cn } from "@kksh/svelte5/utils"
import { IconMultiplexer, Layouts, StrikeSeparator } from "@kksh/ui"
import { open as openFileSelector } from "@tauri-apps/plugin-dialog"
import * as fs from "@tauri-apps/plugin-fs"
import { enhance } from "$app/forms"
import { goto } from "$app/navigation"
import { ArrowLeftIcon } from "lucide-svelte"
import { toast } from "svelte-sonner"
import * as v from "valibot"
import InstallNpmPackageNameForm from "./install-npm-package-name-form.svelte"
import InstallTarballUrlForm from "./install-tarball-url-form.svelte"
let dragging = $state(false)
async function handleDragNDropInstall(paths: string[]) {
dragging = false
console.log(paths)
for (const path of paths) {
const stat = await fs.stat(path)
if (await stat.isDirectory) {
await extensions
.installDevExtensionDir(path)
.then((ext) => {
toast.success("Success", {
description: `Extension from ${ext.extPath} installed successfully`
})
})
.catch((err) => {
toast.warning("Failed to install extension", { description: err })
})
} else if (await stat.isFile) {
if (!$appConfig.devExtensionPath) {
toast.warning(
"Please set the dev extension path in the settings to install tarball extension"
)
continue
}
await extensions
.installTarball(path, $appConfig.devExtensionPath)
.then((ext) => {
toast.success("Success", {
description: `Extension from ${path} installed successfully`
})
})
.catch((err) => {
toast.warning("Failed to install extension", { description: err })
})
} else {
toast.warning(`Unsupported file type: ${path}`)
}
// await installDevExtensionDir(path)
}
}
async function pickExtFolders() {
const selected = await openFileSelector({
directory: true,
multiple: true // allow install multiple extensions at once
})
if (!selected) {
return toast.warning("No File Selected")
}
for (const dir of selected) {
await extensions
.installDevExtensionDir(dir)
.then((ext) => {
toast.success("Success", {
description: `Extension from ${ext.extPath} installed successfully`
})
})
.catch((err) => {
toast.warning("Failed to install extension", { description: err })
})
}
}
async function pickExtFiles() {
if (!$appConfig.devExtensionPath) {
toast.warning("Please set the dev extension path in the settings")
return goto("/settings/set-dev-ext-path")
}
const selected = await openFileSelector({
directory: false,
multiple: true, // allow install multiple extensions at once
filters: [
{
name: "tarball file",
extensions: ["tgz", "gz", "kunkun"]
}
]
})
if (!selected) {
return toast.warning("No File Selected")
}
console.log(selected)
for (const tarballPath of selected) {
await extensions.installTarball(tarballPath, $appConfig.devExtensionPath)
}
}
</script>

<div class="flex justify-center gap-3">
<Button size="sm" onclick={pickExtFolders}>Install from Extension Folders</Button>
<Button size="sm" onclick={pickExtFiles}>Install from Extension Tarball File</Button>
</div>

<StrikeSeparator class="my-1">
<h3 class="text-muted-foreground font-mono text-sm">Drag and Drop</h3>
</StrikeSeparator>

<Layouts.Center>
<DragNDrop
onDrop={handleDragNDropInstall}
onEnter={() => (dragging = true)}
onCancelled={() => (dragging = false)}
>
<Card.Root
class={cn("h-36 w-96", dragging ? "border-lime-400/30" : "text-white hover:text-blue-200")}
>
<div class="flex h-full cursor-pointer items-center justify-center">
<div class={cn("flex flex-col items-center", dragging ? "text-lime-400/70" : "")}>
<IconMultiplexer
icon={{ value: "mdi:folder-cog-outline", type: IconEnum.Iconify }}
class="h-10 w-10"
/>
<small class="select-none font-mono text-xs">Drag and Drop</small>
<small class="select-none font-mono text-xs">Extension Folder or Tarball</small>
</div>
</div>
</Card.Root>
</DragNDrop>
</Layouts.Center>
<StrikeSeparator class="my-1">
<h3 class="text-muted-foreground font-mono text-sm">Install Tarball From URL</h3>
</StrikeSeparator>
<InstallTarballUrlForm />
<InstallNpmPackageNameForm />
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script lang="ts">
import { appConfig, extensions } from "@/stores"
import { Input } from "@kksh/svelte5"
import { Form } from "@kksh/ui"
import { goto } from "$app/navigation"
import { toast } from "svelte-sonner"
import SuperDebug, { defaults, superForm } from "sveltekit-superforms"
import { valibot, valibotClient } from "sveltekit-superforms/adapters"
import * as v from "valibot"
const npmPackageNameFormSchema = v.object({
name: v.pipe(v.string(), v.minLength(1))
})
async function onNpmPackageNameSubmit(data: v.InferOutput<typeof npmPackageNameFormSchema>) {
if (!$appConfig.devExtensionPath) {
toast.warning(
"Please set the dev extension path in the settings to install tarball extension"
)
return goto("/settings/set-dev-ext-path")
}
await extensions
.installFromNpmPackageName(data.name, $appConfig.devExtensionPath)
.then(() => {
toast.success("Success", { description: "Extension installed successfully" })
})
.catch((err) => {
toast.warning("Failed to install extension", { description: err })
})
}
const form = superForm(defaults(valibot(npmPackageNameFormSchema)), {
validators: valibotClient(npmPackageNameFormSchema),
SPA: true,
onUpdate({ form, cancel }) {
if (!form.valid) {
console.log("invalid")
return
}
console.log(form.data)
onNpmPackageNameSubmit(form.data)
cancel()
}
})
const { form: formData, enhance, errors } = form
</script>

<form method="POST" use:enhance>
<Form.Field {form} name="name">
<Form.Control>
{#snippet children({ props })}
<flex items-center gap-2>
<Input {...props} bind:value={$formData.name} placeholder="NPM Package Name" />
<Form.Button class="my-1">Install</Form.Button>
</flex>
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script lang="ts">
import { appConfig, extensions } from "@/stores"
import { Input } from "@kksh/svelte5"
import { Form } from "@kksh/ui"
import { goto } from "$app/navigation"
import { toast } from "svelte-sonner"
import SuperDebug, { defaults, superForm } from "sveltekit-superforms"
import { valibot, valibotClient } from "sveltekit-superforms/adapters"
import * as v from "valibot"
const urlFormSchema = v.object({
url: v.pipe(v.string(), v.url())
})
async function onUrlSubmit(data: v.InferOutput<typeof urlFormSchema>) {
// data.url
// https://storage.huakun.tech/vscode-0.0.6.tgz
if (!$appConfig.devExtensionPath) {
toast.warning(
"Please set the dev extension path in the settings to install tarball extension"
)
return goto("/settings/set-dev-ext-path")
}
await extensions
.installFromTarballUrl(data.url, $appConfig.devExtensionPath)
.then(() => {
toast.success("Sucecss", { description: "Extension installed successfully" })
})
.catch((err) => {
toast.warning("Failed to install extension", { description: err })
})
}
const form = superForm(defaults(valibot(urlFormSchema)), {
validators: valibotClient(urlFormSchema),
SPA: true,
onUpdate({ form, cancel }) {
if (!form.valid) {
console.log("invalid")
return
}
console.log(form.data)
onUrlSubmit(form.data)
cancel()
}
})
const { form: formData, enhance, errors } = form
</script>

<form method="POST" use:enhance>
<Form.Field {form} name="url">
<Form.Control>
{#snippet children({ props })}
<flex items-center gap-2>
<Input {...props} bind:value={$formData.url} placeholder="Tarball URL" />
<Form.Button class="my-1">Install</Form.Button>
</flex>
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</form>
30 changes: 30 additions & 0 deletions apps/desktop/src/lib/stores/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { appConfig } from "./appConfig"
function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
init: () => Promise<void>
getExtensionsFromStore: () => ExtPackageJsonExtra[]
installTarball: (tarballPath: string, extsDir: string) => Promise<ExtPackageJsonExtra>
installDevExtensionDir: (dirPath: string) => Promise<ExtPackageJsonExtra>
installFromTarballUrl: (tarballUrl: string, installDir: string) => Promise<ExtPackageJsonExtra>
installFromNpmPackageName: (name: string, installDir: string) => Promise<ExtPackageJsonExtra>
findStoreExtensionByIdentifier: (identifier: string) => ExtPackageJsonExtra | undefined
registerNewExtensionByPath: (extPath: string) => Promise<ExtPackageJsonExtra>
uninstallStoreExtensionByIdentifier: (identifier: string) => Promise<ExtPackageJsonExtra>
Expand Down Expand Up @@ -56,12 +59,36 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
})
}

/**
* Install extension from tarball file
* @param tarballPath absolute path to the tarball file
* @param extsDir absolute path to the extensions directory
* @returns loaded extension
*/
async function installTarball(tarballPath: string, extsDir: string) {
return extAPI.installTarballUrl(tarballPath, extsDir).then((extInstallPath) => {
return registerNewExtensionByPath(extInstallPath)
})
}

async function installDevExtensionDir(dirPath: string) {
return extAPI.installDevExtensionDir(dirPath).then((ext) => {
return registerNewExtensionByPath(ext.extPath)
})
}

async function installFromTarballUrl(tarballUrl: string, extsDir: string) {
return extAPI.installTarballUrl(tarballUrl, extsDir).then((extInstallPath) => {
return registerNewExtensionByPath(extInstallPath)
})
}

async function installFromNpmPackageName(name: string, extsDir: string) {
return extAPI.installFromNpmPackageName(name, extsDir).then((extInstallPath) => {
return registerNewExtensionByPath(extInstallPath)
})
}

async function uninstallExtensionByPath(targetPath: string) {
const targetExt = get(extensions).find((ext) => ext.extPath === targetPath)
if (!targetExt) throw new Error(`Extension ${targetPath} not registered in DB`)
Expand Down Expand Up @@ -96,7 +123,10 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
getExtensionsFromStore,
findStoreExtensionByIdentifier,
registerNewExtensionByPath,
installTarball,
installDevExtensionDir,
installFromTarballUrl,
installFromNpmPackageName,
uninstallStoreExtensionByIdentifier,
upgradeStoreExtension
}
Expand Down
Loading

0 comments on commit 2e51aed

Please # to comment.