From 383270c93aeb8272f5b0d2e5836b6f7e06efeaf6 Mon Sep 17 00:00:00 2001 From: Huakun Shen Date: Fri, 8 Nov 2024 15:34:37 -0500 Subject: [PATCH] [features] UI Worker Extension, Troubleshooters, Quick Link (#21) * perf: reduce desktop frontend bundle from 10 to 2MB Use shiki fine-grained bundle, avoid bundling all languages and themes * feat: add cross-page transition for ext store back button with gasp Flip * refactor: move StoreListing.svelte in @kksh/ui back to desktop I realized that StoreListing is a pure wrapper, all the interactions are done with props. Even if this component is later used in other projects, it either lacks flexibility or require more changes. So it's moved back to desktop as a regular +page.svelte * feat: Add a bunch of builtin commands for app internal control * feat: add system commands * feat: add extensionsInstallDir var to +layout.ts, exposed to all pages All pages won't need to get the path asynchronously, it's kind of like a global constant * [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 * Feature: Deep Link + Supabase OAuth + open extension in store with deep link (#16) * feat(auth): add deep link and supabase auth * fix(deep-link): fix some routing and reactive page rendering * feat: implement supabase auth with pkce auth flow * feat: add createTauriSyncStore factory function for creating sync svelte store * Feature: Quick Link (#17) * feat: add page for add quick link (not working yet) * upgrade @kksh/svelte5 * fix: infinite recursive footer * dep: add @kksh/svelte5 to ui package * dep: add supabase-js * dep: add @iconify/svelte * style: modify StoreExtDetail width control * fixed: UI for extension store detail * feat: add page to create quick link * feat: display quick links in cmd palette * snapshot * show queries in command input * feat: quick link fully implemented * refactor: format all with prettier * feat: add icon picker for quick link adder * fix: make invert for icon optional, caused many types to crash * [Feature] Implement UI template worker command (#20) * feat: add ui worker command loading code (not working yet) * feat: add unocss * feat: add-dev-extension page * feat: implemented list view template * feat: implement list view detail view width, add demo extension for dev * fix: resize listview, add metadata component * fix: metadata tag component background color * feat: implement boolean (checkbox), date fields for form template * feat: support default, optional, placeholder for form fields * feat: implemented form view Select Field * feat: markdown view * feat: fixed a markdown schema type error * fix: markdown styling * feat: implement action panel for UI worker template list view * format: format all * chore: bump desktop version * fix: fix search term bind in list view --- .gitattributes | 2 + apps/desktop/app.d.ts | 8 + apps/desktop/package.json | 10 +- apps/desktop/src-tauri/src/utils/server.rs | 33 - apps/desktop/src/lib/cmds/builtin.ts | 318 ++-- apps/desktop/src/lib/cmds/ext.ts | 32 +- apps/desktop/src/lib/cmds/index.ts | 3 +- apps/desktop/src/lib/cmds/quick-links.ts | 25 + apps/desktop/src/lib/cmds/system.ts | 4 + .../lib/components/common/DragNDrop.svelte | 38 + .../lib/components/main/CommandPalette.svelte | 72 - .../standalone/settings/AddDevExtForm.svelte | 149 ++ .../install-npm-package-name-form.svelte | 61 + .../settings/install-tarball-url-form.svelte | 62 + apps/desktop/src/lib/stores/appConfig.ts | 33 +- apps/desktop/src/lib/stores/appState.ts | 28 +- apps/desktop/src/lib/stores/auth.ts | 47 + apps/desktop/src/lib/stores/cmdQuery.ts | 23 + apps/desktop/src/lib/stores/extensions.ts | 52 +- apps/desktop/src/lib/stores/index.ts | 2 + apps/desktop/src/lib/stores/quick-links.ts | 39 + apps/desktop/src/lib/stores/winExtMap.ts | 21 +- apps/desktop/src/lib/utils/deeplink.ts | 115 ++ apps/desktop/src/lib/utils/dom.ts | 3 + apps/desktop/src/lib/utils/key.ts | 10 + apps/desktop/src/lib/utils/sync-store.ts | 44 + apps/desktop/src/lib/utils/tauri-events.ts | 57 + apps/desktop/src/lib/utils/updater.ts | 88 ++ apps/desktop/src/routes/+error.svelte | 9 +- apps/desktop/src/routes/+layout.svelte | 13 +- apps/desktop/src/routes/+layout.ts | 7 + apps/desktop/src/routes/+page.svelte | 138 +- apps/desktop/src/routes/auth/+page.svelte | 62 + .../src/routes/auth/confirm/+page.svelte | 84 ++ apps/desktop/src/routes/auth/confirm/+page.ts | 10 + .../extension/create-quick-link/+page.svelte | 108 ++ .../extension/create-quick-link/schema.ts | 7 + .../permission-inspector/+page.svelte | 100 ++ .../src/routes/extension/store/+page.svelte | 11 +- .../store/[identifier]/+error.svelte | 6 +- .../extension/store/[identifier]/+page.svelte | 27 +- .../extension/store/[identifier]/+page.ts | 6 + .../routes/extension/ui-worker/+page.svelte | 290 +++- .../src/routes/extension/ui-worker/+page.ts | 76 + .../settings/add-dev-extension/+page.svelte | 31 + .../settings/set-dev-ext-path/+page.svelte | 4 +- .../extension-loading/+page.svelte | 129 ++ .../extension-window/+page.svelte | 111 ++ apps/desktop/tailwind.config.ts | 5 +- apps/desktop/tsconfig.json | 35 +- apps/desktop/uno.config.ts | 5 + apps/desktop/vite.config.js | 4 +- package.json | 2 +- packages/api/jsr.json | 2 +- packages/api/package.json | 2 +- packages/api/src/commands/extension.ts | 1 - packages/api/src/commands/system.ts | 2 +- packages/api/src/constants.ts | 2 +- packages/api/src/dev/index.ts | 2 +- packages/api/src/models/extension.ts | 12 +- packages/api/src/models/icon.ts | 14 +- packages/api/src/ui/client.ts | 3 +- .../api/src/ui/worker/components/form-view.ts | 3 + packages/api/src/ui/worker/schema/form.ts | 27 +- packages/api/src/version.ts | 2 +- packages/extension/src/db.ts | 45 +- packages/extension/src/install.ts | 5 + .../demo-worker-template-ext/.gitignore | 176 +++ .../demo-worker-template-ext/CHANGELOG.md | 8 + .../demo-worker-template-ext/README.md | 15 + .../demo-worker-template-ext/buffer.ts | 3 + .../demo-worker-template-ext/build.ts | 31 + .../deno-src/deno.json | 5 + .../deno-src/deno.lock | 23 + .../demo-worker-template-ext/deno-src/rpc.ts | 13 + .../demo-worker-template-ext/package.json | 114 ++ .../demo-worker-template-ext/rollup.config.js | 20 + .../demo-worker-template-ext/src/index.ts | 181 +++ .../demo-worker-template-ext/tsconfig.json | 27 + packages/extensions/form-view/.gitignore | 177 +++ packages/extensions/form-view/README.md | 125 ++ packages/extensions/form-view/build.ts | 30 + packages/extensions/form-view/package.json | 47 + packages/extensions/form-view/src/i18n/en.ts | 5 + .../extensions/form-view/src/i18n/index.ts | 20 + packages/extensions/form-view/src/i18n/zh.ts | 5 + packages/extensions/form-view/src/index.ts | 95 ++ packages/extensions/form-view/tsconfig.json | 27 + packages/schema/package.json | 4 +- .../scripts/upload-schema-to-supabase.ts | 5 +- packages/supabase/package.json | 3 +- packages/supabase/src/index.ts | 6 +- packages/types/src/appConfig.ts | 2 +- packages/types/src/appState.ts | 5 + packages/ui/components.json | 4 +- packages/ui/package.json | 13 +- .../components/common/IconMultiplexer.svelte | 32 +- .../src/components/common/IconSelector.svelte | 56 + .../common/IconSelectorDialog.svelte | 75 + .../components/common/IconSelectorForm.svelte | 0 packages/ui/src/components/common/Kbd.svelte | 15 + .../src/components/common/LoadingBar.svelte | 46 + .../components/common/StrikeSeparator.svelte | 22 + .../ui/src/components/common/TauriLink.svelte | 30 + .../components/common/date/DatePicker.svelte | 37 + .../common/date/DatePickerWithPreset.svelte | 65 + .../ui/src/components/common/date/index.ts | 2 + packages/ui/src/components/common/index.ts | 4 + .../error/raw-error-json-preset.svelte | 35 +- .../extension/PermissionInspector.svelte | 1 - .../extension/StoreExtDetail.svelte | 10 +- packages/ui/src/components/extension/index.ts | 1 + .../extension/templates/Markdown.svelte | 10 + .../extension/templates/MarkdownView.svelte | 28 + .../extension/templates/form-view.svelte | 19 + .../extension/templates/form.svelte | 136 ++ .../components/extension/templates/index.ts | 3 + .../extension/templates/list-detail.svelte | 19 + .../extension/templates/list-item.svelte | 27 + .../extension/templates/list-view.svelte | 156 ++ .../templates/metadata/Metadata.svelte | 43 + .../extension/templates/metadata/label.svelte | 32 + .../extension/templates/metadata/link.svelte | 22 + .../extension/templates/metadata/tag.svelte | 19 + .../extension/templates/metadata/tags.svelte | 21 + .../ui/src/components/main/ActionPanel.svelte | 69 + .../ui/src/components/main/BuiltinCmds.svelte | 10 +- .../components/main/CustomCommandInput.svelte | 3 + .../src/components/main/ExtCmdsGroup.svelte | 11 +- .../main/GlobalCommandPaletteFooter.svelte | 38 +- .../ui/src/components/main/QuickLinks.svelte | 31 + .../ui/src/components/main/SystemCmds.svelte | 31 + packages/ui/src/components/main/index.ts | 2 + packages/ui/src/components/main/types.ts | 23 +- .../src/components/theme/mode-toggle.svelte | 16 + .../src/components/ui/form/form-button.svelte | 7 + .../ui/form/form-description.svelte | 17 + .../ui/form/form-element-field.svelte | 31 + .../ui/form/form-field-errors.svelte | 30 + .../src/components/ui/form/form-field.svelte | 31 + .../components/ui/form/form-fieldset.svelte | 22 + .../src/components/ui/form/form-label.svelte | 21 + .../src/components/ui/form/form-legend.svelte | 17 + packages/ui/src/components/ui/form/index.ts | 33 + packages/ui/src/components/ui/label/index.ts | 7 + .../ui/src/components/ui/label/label.svelte | 15 + packages/ui/src/index.ts | 5 +- packages/ui/src/utils/command-score.ts | 175 +++ packages/ui/src/utils/form.ts | 47 + packages/ui/src/utils/index.ts | 2 + packages/ui/tsconfig.json | 6 +- pnpm-lock.yaml | 1287 ++++++++++++++++- 152 files changed, 6447 insertions(+), 476 deletions(-) create mode 100644 .gitattributes create mode 100644 apps/desktop/app.d.ts create mode 100644 apps/desktop/src/lib/cmds/quick-links.ts create mode 100644 apps/desktop/src/lib/cmds/system.ts create mode 100644 apps/desktop/src/lib/components/common/DragNDrop.svelte delete mode 100644 apps/desktop/src/lib/components/main/CommandPalette.svelte create mode 100644 apps/desktop/src/lib/components/standalone/settings/AddDevExtForm.svelte create mode 100644 apps/desktop/src/lib/components/standalone/settings/install-npm-package-name-form.svelte create mode 100644 apps/desktop/src/lib/components/standalone/settings/install-tarball-url-form.svelte create mode 100644 apps/desktop/src/lib/stores/auth.ts create mode 100644 apps/desktop/src/lib/stores/cmdQuery.ts create mode 100644 apps/desktop/src/lib/stores/quick-links.ts create mode 100644 apps/desktop/src/lib/utils/deeplink.ts create mode 100644 apps/desktop/src/lib/utils/dom.ts create mode 100644 apps/desktop/src/lib/utils/sync-store.ts create mode 100644 apps/desktop/src/lib/utils/tauri-events.ts create mode 100644 apps/desktop/src/lib/utils/updater.ts create mode 100644 apps/desktop/src/routes/auth/+page.svelte create mode 100644 apps/desktop/src/routes/auth/confirm/+page.svelte create mode 100644 apps/desktop/src/routes/auth/confirm/+page.ts create mode 100644 apps/desktop/src/routes/extension/create-quick-link/+page.svelte create mode 100644 apps/desktop/src/routes/extension/create-quick-link/schema.ts create mode 100644 apps/desktop/src/routes/extension/permission-inspector/+page.svelte create mode 100644 apps/desktop/src/routes/extension/ui-worker/+page.ts create mode 100644 apps/desktop/src/routes/settings/add-dev-extension/+page.svelte create mode 100644 apps/desktop/src/routes/troubleshooters/extension-loading/+page.svelte create mode 100644 apps/desktop/src/routes/troubleshooters/extension-window/+page.svelte create mode 100644 apps/desktop/uno.config.ts create mode 100644 packages/extensions/demo-worker-template-ext/.gitignore create mode 100644 packages/extensions/demo-worker-template-ext/CHANGELOG.md create mode 100644 packages/extensions/demo-worker-template-ext/README.md create mode 100644 packages/extensions/demo-worker-template-ext/buffer.ts create mode 100644 packages/extensions/demo-worker-template-ext/build.ts create mode 100644 packages/extensions/demo-worker-template-ext/deno-src/deno.json create mode 100644 packages/extensions/demo-worker-template-ext/deno-src/deno.lock create mode 100644 packages/extensions/demo-worker-template-ext/deno-src/rpc.ts create mode 100644 packages/extensions/demo-worker-template-ext/package.json create mode 100644 packages/extensions/demo-worker-template-ext/rollup.config.js create mode 100644 packages/extensions/demo-worker-template-ext/src/index.ts create mode 100644 packages/extensions/demo-worker-template-ext/tsconfig.json create mode 100644 packages/extensions/form-view/.gitignore create mode 100644 packages/extensions/form-view/README.md create mode 100644 packages/extensions/form-view/build.ts create mode 100644 packages/extensions/form-view/package.json create mode 100644 packages/extensions/form-view/src/i18n/en.ts create mode 100644 packages/extensions/form-view/src/i18n/index.ts create mode 100644 packages/extensions/form-view/src/i18n/zh.ts create mode 100644 packages/extensions/form-view/src/index.ts create mode 100644 packages/extensions/form-view/tsconfig.json create mode 100644 packages/ui/src/components/common/IconSelector.svelte create mode 100644 packages/ui/src/components/common/IconSelectorDialog.svelte create mode 100644 packages/ui/src/components/common/IconSelectorForm.svelte create mode 100644 packages/ui/src/components/common/Kbd.svelte create mode 100644 packages/ui/src/components/common/LoadingBar.svelte create mode 100644 packages/ui/src/components/common/StrikeSeparator.svelte create mode 100644 packages/ui/src/components/common/TauriLink.svelte create mode 100644 packages/ui/src/components/common/date/DatePicker.svelte create mode 100644 packages/ui/src/components/common/date/DatePickerWithPreset.svelte create mode 100644 packages/ui/src/components/common/date/index.ts create mode 100644 packages/ui/src/components/extension/templates/Markdown.svelte create mode 100644 packages/ui/src/components/extension/templates/MarkdownView.svelte create mode 100644 packages/ui/src/components/extension/templates/form-view.svelte create mode 100644 packages/ui/src/components/extension/templates/form.svelte create mode 100644 packages/ui/src/components/extension/templates/index.ts create mode 100644 packages/ui/src/components/extension/templates/list-detail.svelte create mode 100644 packages/ui/src/components/extension/templates/list-item.svelte create mode 100644 packages/ui/src/components/extension/templates/list-view.svelte create mode 100644 packages/ui/src/components/extension/templates/metadata/Metadata.svelte create mode 100644 packages/ui/src/components/extension/templates/metadata/label.svelte create mode 100644 packages/ui/src/components/extension/templates/metadata/link.svelte create mode 100644 packages/ui/src/components/extension/templates/metadata/tag.svelte create mode 100644 packages/ui/src/components/extension/templates/metadata/tags.svelte create mode 100644 packages/ui/src/components/main/ActionPanel.svelte create mode 100644 packages/ui/src/components/main/QuickLinks.svelte create mode 100644 packages/ui/src/components/main/SystemCmds.svelte create mode 100644 packages/ui/src/components/theme/mode-toggle.svelte create mode 100644 packages/ui/src/components/ui/form/form-button.svelte create mode 100644 packages/ui/src/components/ui/form/form-description.svelte create mode 100644 packages/ui/src/components/ui/form/form-element-field.svelte create mode 100644 packages/ui/src/components/ui/form/form-field-errors.svelte create mode 100644 packages/ui/src/components/ui/form/form-field.svelte create mode 100644 packages/ui/src/components/ui/form/form-fieldset.svelte create mode 100644 packages/ui/src/components/ui/form/form-label.svelte create mode 100644 packages/ui/src/components/ui/form/form-legend.svelte create mode 100644 packages/ui/src/components/ui/form/index.ts create mode 100644 packages/ui/src/components/ui/label/index.ts create mode 100644 packages/ui/src/components/ui/label/label.svelte create mode 100644 packages/ui/src/utils/command-score.ts create mode 100644 packages/ui/src/utils/form.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..423830b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +pnpm-lock.yaml linguist-generated=true +packages/tauri-plugins/jarvis/permissions/autogenerated linguist-generated=true diff --git a/apps/desktop/app.d.ts b/apps/desktop/app.d.ts new file mode 100644 index 0000000..9959563 --- /dev/null +++ b/apps/desktop/app.d.ts @@ -0,0 +1,8 @@ +import type { AttributifyAttributes } from "@unocss/preset-attributify" + +declare module "svelte/elements" { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars + interface HTMLAttributes extends AttributifyAttributes {} +} + +export {} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 717ed17..b909e8b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@kksh/desktop", - "version": "0.1.9-beta.8", + "version": "0.1.10", "description": "", "type": "module", "scripts": { @@ -15,6 +15,7 @@ "license": "MIT", "dependencies": { "@formkit/auto-animate": "^0.8.2", + "@huakunshen/comlink": "^4.4.1", "@kksh/extension": "workspace:*", "@kksh/supabase": "workspace:*", "@kksh/ui": "workspace:*", @@ -27,9 +28,11 @@ "lucide-svelte": "^0.454.0", "lz-string": "^1.5.0", "mode-watcher": "^0.4.1", + "semver": "^7.6.3", "svelte-radix": "^2.0.1", "svelte-sonner": "^0.3.28", - "sveltekit-superforms": "^2.20.0" + "sveltekit-superforms": "^2.20.0", + "uuid": "^11.0.2" }, "devDependencies": { "@kksh/types": "workspace:*", @@ -42,6 +45,8 @@ "@tailwindcss/typography": "^0.5.15", "@tauri-apps/cli": "^2.0.4", "@types/bun": "latest", + "@types/semver": "^7.5.8", + "@unocss/preset-attributify": "^0.64.0", "autoprefixer": "^10.4.20", "clsx": "^2.1.1", "embla-carousel-svelte": "^8.3.1", @@ -52,6 +57,7 @@ "tailwindcss-animate": "^1.0.7", "tslib": "^2.8.1", "typescript": "^5.6.3", + "unocss": "^0.64.0", "vaul-svelte": "^0.3.2", "vite": "^5.4.10" } diff --git a/apps/desktop/src-tauri/src/utils/server.rs b/apps/desktop/src-tauri/src/utils/server.rs index 53b161c..c6377f0 100644 --- a/apps/desktop/src-tauri/src/utils/server.rs +++ b/apps/desktop/src-tauri/src/utils/server.rs @@ -8,47 +8,14 @@ pub fn tauri_file_server( extension_folder_path: PathBuf, dist: Option, ) -> tauri::http::Response> { - // let host = request.uri().host().unwrap(); - // let host_parts: Vec<&str> = host.split(".").collect(); - // if host_parts.len() != 3 { - // return tauri::http::Response::builder() - // .status(tauri::http::StatusCode::NOT_FOUND) - // .header("Access-Control-Allow-Origin", "*") - // .body("Invalid Host".as_bytes().to_vec()) - // .unwrap(); - // } - // expect 3 parts, ext_identifier, dist and ext_type - // let ext_identifier = host_parts[0]; - // let dist = host_parts[1]; - // let ext_type = host_parts[2]; // ext or dev-ext - // let app_state = app.state::(); - // let app_state: tauri:State = app.state(); - // let extension_folder_path: Option = match ext_type { - // "ext" => Some(app_state.extension_path.lock().unwrap().clone()), - // "dev-ext" => app_state.dev_extension_path.lock().unwrap().clone(), - // _ => None, - // }; - // let extension_folder_path = match extension_folder_path { - // Some(path) => path, - // None => { - // return tauri::http::Response::builder() - // .status(tauri::http::StatusCode::NOT_FOUND) - // .header("Access-Control-Allow-Origin", "*") - // .body("Extension Folder Not Found".as_bytes().to_vec()) - // .unwrap() - // } - // }; - println!("dist: {:?}", dist); let path = &request.uri().path()[1..]; // skip the first / let path = urlencoding::decode(path).unwrap().to_string(); let mut url_file_path = extension_folder_path; - // .join(ext_identifier) match dist { Some(dist) => url_file_path = url_file_path.join(dist), None => {} } url_file_path = url_file_path.join(path); - println!("url_file_path: {:?}", url_file_path); // check if it's file or directory, if file and exist, return file, if directory, return index.html, if neither, check .html if url_file_path.is_file() { // println!("1st case url_file_path: {:?}", url_file_path); diff --git a/apps/desktop/src/lib/cmds/builtin.ts b/apps/desktop/src/lib/cmds/builtin.ts index e6ee94e..f6b3a01 100644 --- a/apps/desktop/src/lib/cmds/builtin.ts +++ b/apps/desktop/src/lib/cmds/builtin.ts @@ -1,7 +1,12 @@ -import { appState } from "@/stores" +import { appConfig, appState, auth } from "@/stores" +import { checkUpdateAndInstall } from "@/utils/updater" import type { BuiltinCmd } from "@kksh/ui/types" -import { dev } from "$app/environment" +import { getVersion } from "@tauri-apps/api/app" +import { WebviewWindow } from "@tauri-apps/api/webviewWindow" +import { exit } from "@tauri-apps/plugin-process" import { goto } from "$app/navigation" +import { toast } from "svelte-sonner" +import { v4 as uuidv4 } from "uuid" export const builtinCmds: BuiltinCmd[] = [ { @@ -13,58 +18,59 @@ export const builtinCmds: BuiltinCmd[] = [ goto("/extension/store") } }, - // { - // name: "Sign In", - // iconifyIcon: "mdi:login-variant", - // description: "", - // function: async () => { - // goto("/auth") - // } - // }, - // { - // name: "Sign Out", - // iconifyIcon: "mdi:logout-variant", - // description: "", - // function: async () => { - // const supabase = useSupabaseClient() - // supabase.auth.signOut() - // } - // }, - // { - // name: "Show Draggable Area", - // iconifyIcon: "mingcute:move-fill", - // description: "", - // function: async () => { - // // select all html elements with attribute data-tauri-drag-region - // const elements = document.querySelectorAll("[data-tauri-drag-region]") - // elements.forEach((el) => { - // el.classList.add("bg-red-500/30") - // }) - // setTimeout(() => { - // elements.forEach((el) => { - // el.classList.remove("bg-red-500/30") - // }) - // }, 2_000) - // } - // }, - // { - // name: "Add Dev Extension", - // iconifyIcon: "lineicons:dev", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/add-dev-ext") - // } - // }, - // { - // name: "Kunkun Version", - // iconifyIcon: "stash:version-solid", - // description: "", - // function: async () => { - // toast.success(`Kunkun Version: ${await getVersion()}`) - // } - // }, + { + name: "Sign In", + iconifyIcon: "mdi:login-variant", + description: "", + function: async () => { + goto("/auth") + } + }, + { + name: "Sign Out", + iconifyIcon: "mdi:logout-variant", + description: "", + function: async () => { + auth + .signOut() + .then(() => toast.success("Signed out")) + .catch((err) => toast.error("Failed to sign out: ", { description: err.message })) + } + }, + { + name: "Show Draggable Area", + iconifyIcon: "mingcute:move-fill", + description: "", + function: async () => { + // select all html elements with attribute data-tauri-drag-region + const elements = document.querySelectorAll("[data-tauri-drag-region]") + elements.forEach((el) => { + el.classList.add("bg-red-500/30") + }) + setTimeout(() => { + elements.forEach((el) => { + el.classList.remove("bg-red-500/30") + }) + }, 2_000) + } + }, + { + name: "Add Dev Extension", + iconifyIcon: "lineicons:dev", + description: "", + function: async () => { + appState.clearSearchTerm() + goto("/settings/add-dev-extension") + } + }, + { + name: "Kunkun Version", + iconifyIcon: "stash:version-solid", + description: "", + function: async () => { + toast.success(`Kunkun Version: ${await getVersion()}`) + } + }, { name: "Set Dev Extension Path", iconifyIcon: "lineicons:dev", @@ -75,52 +81,48 @@ export const builtinCmds: BuiltinCmd[] = [ goto("/settings/set-dev-ext-path") } }, - // { - // name: "Extension Window Troubleshooter", - // iconifyIcon: "material-symbols:window-outline", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // // goto("/window-troubleshooter") - // const winLabel = `main:window-troubleshooter-${uuidv4()}` - // console.log(winLabel) - // new WebviewWindow(winLabel, { - // url: "/window-troubleshooter", - // title: "Window Troubleshooter" - // }) - // } - // }, - // { - // name: "Extension Permission Inspector", - // iconifyIcon: "hugeicons:inspect-code", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/ext-permission-inspector") - // } - // }, - // { - // name: "Extension Loading Troubleshooter", - // iconifyIcon: "material-symbols:troubleshoot", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/extension-load-troubleshooter") - // } - // }, - // { - // name: "Create Quicklink", - // iconifyIcon: "material-symbols:link", - // description: "Create a Quicklink", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/create-quicklink") - // } - // }, + { + name: "Extension Window Troubleshooter", + iconifyIcon: "material-symbols:window-outline", + description: "", + function: async () => { + appState.clearSearchTerm() + // goto("/window-troubleshooter") + const winLabel = `main:extension-window-troubleshooter-${uuidv4()}` + console.log(winLabel) + new WebviewWindow(winLabel, { + url: "/troubleshooters/extension-window", + title: "Extension Window Troubleshooter" + }) + } + }, + { + name: "Extension Permission Inspector", + iconifyIcon: "hugeicons:inspect-code", + description: "", + function: async () => { + appState.clearSearchTerm() + goto("/extension/permission-inspector") + } + }, + { + name: "Extension Loading Troubleshooter", + iconifyIcon: "material-symbols:troubleshoot", + description: "", + function: async () => { + appState.clearSearchTerm() + goto("/troubleshooters/extension-loading") + } + }, + { + name: "Create Quicklink", + iconifyIcon: "material-symbols:link", + description: "Create a Quicklink", + function: async () => { + appState.clearSearchTerm() + goto("/extension/create-quick-link") + } + }, // { // name: "Settings", // iconifyIcon: "solar:settings-linear", @@ -143,30 +145,32 @@ export const builtinCmds: BuiltinCmd[] = [ // appStateStore.setSearchTermSync("") // } // }, - // { - // name: "Check Update", - // iconifyIcon: "material-symbols:update", - // description: "Check for updates", - // function: async () => { - // checkUpdateAndInstall() - // } - // }, - // { - // name: "Check Beta Update", - // iconifyIcon: "material-symbols:update", - // description: "Check for Beta updates", - // function: async () => { - // checkUpdateAndInstall(true) - // } - // }, - // { - // name: "Reload", - // iconifyIcon: "tabler:reload", - // description: "Reload this page", - // function: async () => { - // location.reload() - // } - // }, + { + name: "Check Update", + iconifyIcon: "material-symbols:update", + description: "Check for updates", + function: async () => { + checkUpdateAndInstall() + appState.clearSearchTerm() + } + }, + { + name: "Check Beta Update", + iconifyIcon: "material-symbols:update", + description: "Check for Beta updates", + function: async () => { + checkUpdateAndInstall({ beta: true }) + appState.clearSearchTerm() + } + }, + { + name: "Reload", + iconifyIcon: "tabler:reload", + description: "Reload this page", + function: async () => { + location.reload() + } + }, { name: "Dance", iconifyIcon: "mdi:dance-pole", @@ -174,33 +178,43 @@ export const builtinCmds: BuiltinCmd[] = [ function: async () => { goto("/dance") } + }, + { + name: "Quit Kunkun", + iconifyIcon: "emojione:cross-mark-button", + description: "Quit Kunkun", + function: async () => { + exit(0) + } + }, + { + name: "Toggle Dev Extension HMR", + iconifyIcon: "ri:toggle-line", + description: "Load dev extensions from their dev server URLs", + function: async () => { + appConfig.update((config) => { + toast.success(`Dev Extension HMR toggled to: ${!config.hmr}`) + return { + ...config, + hmr: !config.hmr + } + }) + appState.clearSearchTerm() + } + }, + { + name: "Toggle Hide On Blur", + iconifyIcon: "ri:toggle-line", + description: "Toggle Hide On Blur", + function: async () => { + appConfig.update((config) => { + toast.success(`"Hide on Blur" toggled to: ${!config.hideOnBlur}`) + return { + ...config, + hideOnBlur: !config.hideOnBlur + } + }) + appState.clearSearchTerm() + } } - // { - // name: "Quit Kunkun", - // iconifyIcon: "emojione:cross-mark-button", - // description: "Quit Kunkun", - // function: async () => { - // exit(0) - // } - // }, - // { - // name: "Toggle Dev Extension Live Load Mode", - // iconifyIcon: "ri:toggle-line", - // description: "Load dev extensions from their dev server URLs", - // function: async () => { - // toggleDevExtensionLiveLoadMode() - // } - // }, - // { - // name: "Toggle Hide On Blur", - // iconifyIcon: "ri:toggle-line", - // description: "Toggle Hide On Blur", - // function: async () => { - // const appConfig = useAppConfigStore() - // appConfig.setHideOnBlur(!appConfig.hideOnBlur) - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // toast.success(`"Hide on Blur" toggled to: ${appConfig.hideOnBlur}`) - // } - // } ] diff --git a/apps/desktop/src/lib/cmds/ext.ts b/apps/desktop/src/lib/cmds/ext.ts index 68fb4b6..1aede5c 100644 --- a/apps/desktop/src/lib/cmds/ext.ts +++ b/apps/desktop/src/lib/cmds/ext.ts @@ -1,14 +1,12 @@ +import { appState } from "@/stores" import { winExtMap } from "@/stores/winExtMap" import { trimSlash } from "@/utils/url" import { constructExtensionSupportDir } from "@kksh/api" -import { CmdTypeEnum, CustomUiCmd, ExtPackageJsonExtra, TemplateUiCmd } from "@kksh/api/models" +import { CustomUiCmd, ExtPackageJsonExtra, TemplateUiCmd } from "@kksh/api/models" import { launchNewExtWindow } from "@kksh/extension" import { convertFileSrc } from "@tauri-apps/api/core" -import { WebviewWindow } from "@tauri-apps/api/webviewWindow" import * as fs from "@tauri-apps/plugin-fs" -import { debug } from "@tauri-apps/plugin-log" import { goto } from "$app/navigation" -import * as v from "valibot" export async function createExtSupportDir(extPath: string) { const extSupportDir = await constructExtensionSupportDir(extPath) @@ -23,7 +21,19 @@ export async function onTemplateUiCmdSelect( { isDev, hmr }: { isDev: boolean; hmr: boolean } ) { await createExtSupportDir(ext.extPath) - console.log("onTemplateUiCmdSelect", ext, cmd, isDev, hmr) + // console.log("onTemplateUiCmdSelect", ext, cmd, isDev, hmr) + const url = `/extension/ui-worker?extPath=${encodeURIComponent(ext.extPath)}&cmdName=${encodeURIComponent(cmd.name)}` + if (cmd.window) { + const winLabel = await winExtMap.registerExtensionWithWindow({ extPath: ext.extPath }) + const window = launchNewExtWindow(winLabel, url, cmd.window) + window.onCloseRequested(async (event) => { + await winExtMap.unregisterExtensionFromWindow(winLabel) + }) + } else { + return winExtMap + .registerExtensionWithWindow({ windowLabel: "main", extPath: ext.extPath }) + .then(() => goto(url)) + } } export async function onCustomUiCmdSelect( @@ -31,7 +41,7 @@ export async function onCustomUiCmdSelect( cmd: CustomUiCmd, { isDev, hmr }: { isDev: boolean; hmr: boolean } ) { - console.log("onCustomUiCmdSelect", ext, cmd, isDev, hmr) + // console.log("onCustomUiCmdSelect", ext, cmd, isDev, hmr) await createExtSupportDir(ext.extPath) let url = cmd.main @@ -48,14 +58,14 @@ export async function onCustomUiCmdSelect( }) console.log("Launch new window, ", winLabel) const window = launchNewExtWindow(winLabel, url2, cmd.window) + window.onCloseRequested(async (event) => { + await winExtMap.unregisterExtensionFromWindow(winLabel) + }) } else { console.log("Launch main window") return winExtMap - .registerExtensionWithWindow({ - windowLabel: "main", - extPath: ext.extPath, - dist: cmd.dist - }) + .registerExtensionWithWindow({ windowLabel: "main", extPath: ext.extPath, dist: cmd.dist }) .then(() => goto(url2)) } + appState.clearSearchTerm() } diff --git a/apps/desktop/src/lib/cmds/index.ts b/apps/desktop/src/lib/cmds/index.ts index 3efc68d..a845ef1 100644 --- a/apps/desktop/src/lib/cmds/index.ts +++ b/apps/desktop/src/lib/cmds/index.ts @@ -2,6 +2,7 @@ import { CmdTypeEnum, CustomUiCmd, ExtPackageJsonExtra, TemplateUiCmd } from "@k import type { CommandLaunchers, OnExtCmdSelect } from "@kksh/ui/types" import * as v from "valibot" import { onCustomUiCmdSelect, onTemplateUiCmdSelect } from "./ext" +import { onQuickLinkSelect } from "./quick-links" const onExtCmdSelect: OnExtCmdSelect = ( ext: ExtPackageJsonExtra, @@ -20,4 +21,4 @@ const onExtCmdSelect: OnExtCmdSelect = ( } } -export const commandLaunchers = { onExtCmdSelect } satisfies CommandLaunchers +export const commandLaunchers = { onExtCmdSelect, onQuickLinkSelect } satisfies CommandLaunchers diff --git a/apps/desktop/src/lib/cmds/quick-links.ts b/apps/desktop/src/lib/cmds/quick-links.ts new file mode 100644 index 0000000..43662a4 --- /dev/null +++ b/apps/desktop/src/lib/cmds/quick-links.ts @@ -0,0 +1,25 @@ +import { appState } from "@/stores" +import type { CmdQuery, CmdValue } from "@kksh/ui/types" +import { open } from "tauri-plugin-shellx-api" + +/** + * Given some link like https://google.com/search?q={argument}&query={query} + * Find {argument} and {query} + */ +export function findAllArgsInLink(link: string): string[] { + const regex = /\{([^}]+)\}/g + const matches = [...link.matchAll(regex)] + return matches.map((match) => match[1]) +} + +export function onQuickLinkSelect(quickLink: CmdValue, queries: CmdQuery[]) { + console.log(quickLink, queries) + let qlink = quickLink.data + for (const arg of queries) { + console.log(`replace all {${arg.name}} with ${arg.value}`) + qlink = qlink.replaceAll(`{${arg.name}}`, arg.value) + } + appState.clearSearchTerm() + console.log(qlink) + open(qlink) +} diff --git a/apps/desktop/src/lib/cmds/system.ts b/apps/desktop/src/lib/cmds/system.ts new file mode 100644 index 0000000..cd7221f --- /dev/null +++ b/apps/desktop/src/lib/cmds/system.ts @@ -0,0 +1,4 @@ +import { getSystemCommands } from "@kksh/api/commands" +import type { SysCommand } from "@kksh/api/models" + +export const systemCommands: SysCommand[] = getSystemCommands() diff --git a/apps/desktop/src/lib/components/common/DragNDrop.svelte b/apps/desktop/src/lib/components/common/DragNDrop.svelte new file mode 100644 index 0000000..c74dbce --- /dev/null +++ b/apps/desktop/src/lib/components/common/DragNDrop.svelte @@ -0,0 +1,38 @@ + + + + {@render children()} + diff --git a/apps/desktop/src/lib/components/main/CommandPalette.svelte b/apps/desktop/src/lib/components/main/CommandPalette.svelte deleted file mode 100644 index 8e117e4..0000000 --- a/apps/desktop/src/lib/components/main/CommandPalette.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - No results found. - - {#if $appConfig.extensionPath && $devStoreExts.length > 0} - - {/if} - {#if $appConfig.extensionPath && $installedStoreExts.length > 0} - - {/if} - - - - diff --git a/apps/desktop/src/lib/components/standalone/settings/AddDevExtForm.svelte b/apps/desktop/src/lib/components/standalone/settings/AddDevExtForm.svelte new file mode 100644 index 0000000..53f66d3 --- /dev/null +++ b/apps/desktop/src/lib/components/standalone/settings/AddDevExtForm.svelte @@ -0,0 +1,149 @@ + + +
+ + +
+ + +

Drag and Drop

+
+ + + { + handleDragNDropInstall(e.payload.paths) + }} + onEnter={() => (dragging = true)} + onCancelled={() => (dragging = false)} + > + +
+
+ + Drag and Drop + Extension Folder or Tarball +
+
+
+
+
+ +

Install Tarball From URL

+
+ + diff --git a/apps/desktop/src/lib/components/standalone/settings/install-npm-package-name-form.svelte b/apps/desktop/src/lib/components/standalone/settings/install-npm-package-name-form.svelte new file mode 100644 index 0000000..4cb78b5 --- /dev/null +++ b/apps/desktop/src/lib/components/standalone/settings/install-npm-package-name-form.svelte @@ -0,0 +1,61 @@ + + +
+ + + {#snippet children({ props })} + + + Install + + {/snippet} + + + +
diff --git a/apps/desktop/src/lib/components/standalone/settings/install-tarball-url-form.svelte b/apps/desktop/src/lib/components/standalone/settings/install-tarball-url-form.svelte new file mode 100644 index 0000000..a950a64 --- /dev/null +++ b/apps/desktop/src/lib/components/standalone/settings/install-tarball-url-form.svelte @@ -0,0 +1,62 @@ + + +
+ + + {#snippet children({ props })} + + + Install + + {/snippet} + + + +
diff --git a/apps/desktop/src/lib/stores/appConfig.ts b/apps/desktop/src/lib/stores/appConfig.ts index aa50558..78ebcd0 100644 --- a/apps/desktop/src/lib/stores/appConfig.ts +++ b/apps/desktop/src/lib/stores/appConfig.ts @@ -1,12 +1,10 @@ import { getExtensionsFolder } from "@/constants" -import { themeConfigStore, updateTheme, type ThemeConfig } from "@kksh/svelte5" +import { createTauriSyncStore, type WithSyncStore } from "@/utils/sync-store" +import { updateTheme, type ThemeConfig } from "@kksh/svelte5" import { PersistedAppConfig, type AppConfig } from "@kksh/types" -import * as path from "@tauri-apps/api/path" -import { remove } from "@tauri-apps/plugin-fs" import { debug, error } from "@tauri-apps/plugin-log" import * as os from "@tauri-apps/plugin-os" import { load } from "@tauri-apps/plugin-store" -import { get, writable, type Writable } from "svelte/store" import * as v from "valibot" export const defaultAppConfig: AppConfig = { @@ -21,7 +19,7 @@ export const defaultAppConfig: AppConfig = { launchAtLogin: true, showInTray: true, devExtensionPath: null, - extensionPath: undefined, + extensionsInstallDir: undefined, hmr: false, hideOnBlur: true, extensionAutoUpgrade: true, @@ -35,25 +33,22 @@ interface AppConfigAPI { setDevExtensionPath: (devExtensionPath: string | null) => void } -function createAppConfig(): Writable & AppConfigAPI { - const { subscribe, update, set } = writable(defaultAppConfig) +function createAppConfig(): WithSyncStore & AppConfigAPI { + const store = createTauriSyncStore("app-config", defaultAppConfig) async function init() { debug("Initializing app config") - const appDataDir = await path.appDataDir() - // const appConfigPath = await path.join(appDataDir, "appConfig.json") - // debug(`appConfigPath: ${appConfigPath}`) const persistStore = await load("kk-config.json", { autoSave: true }) const loadedConfig = await persistStore.get("config") const parseRes = v.safeParse(PersistedAppConfig, loadedConfig) if (parseRes.success) { console.log("Parse Persisted App Config Success", parseRes.output) - const extensionPath = await path.join(appDataDir, "extensions") - update((config) => ({ + const extensionsInstallDir = await getExtensionsFolder() + store.update((config) => ({ ...config, ...parseRes.output, isInitialized: true, - extensionPath, + extensionsInstallDir, platform: os.platform() })) } else { @@ -63,7 +58,7 @@ function createAppConfig(): Writable & AppConfigAPI { await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig)) } - subscribe(async (config) => { + store.subscribe(async (config) => { console.log("Saving app config", config) await persistStore.set("config", config) updateTheme(config.theme) @@ -71,15 +66,13 @@ function createAppConfig(): Writable & AppConfigAPI { } return { - setTheme: (theme: ThemeConfig) => update((config) => ({ ...config, theme })), + ...store, + setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })), setDevExtensionPath: (devExtensionPath: string | null) => { console.log("setDevExtensionPath", devExtensionPath) - update((config) => ({ ...config, devExtensionPath })) + store.update((config) => ({ ...config, devExtensionPath })) }, - init, - subscribe, - update, - set + init } } diff --git a/apps/desktop/src/lib/stores/appState.ts b/apps/desktop/src/lib/stores/appState.ts index aaaf45d..d45c87a 100644 --- a/apps/desktop/src/lib/stores/appState.ts +++ b/apps/desktop/src/lib/stores/appState.ts @@ -1,26 +1,42 @@ -import type { AppState } from "@/types" -import { get, writable, type Writable } from "svelte/store" +import { findAllArgsInLink } from "@/cmds/quick-links" +import { Action as ActionSchema, CmdTypeEnum } from "@kksh/api/models" +import type { AppState } from "@kksh/types" +import type { CmdValue } from "@kksh/ui/types" +import { derived, get, writable, type Writable } from "svelte/store" export const defaultAppState: AppState = { searchTerm: "", - highlightedCmd: "" + highlightedCmd: "", + loadingBar: false, + defaultAction: "", + actionPanel: undefined } interface AppStateAPI { clearSearchTerm: () => void get: () => AppState + setLoadingBar: (loadingBar: boolean) => void + setDefaultAction: (defaultAction: string) => void + setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void } function createAppState(): Writable & AppStateAPI { const store = writable(defaultAppState) return { - subscribe: store.subscribe, - update: store.update, - set: store.set, + ...store, get: () => get(store), clearSearchTerm: () => { store.update((state) => ({ ...state, searchTerm: "" })) + }, + setLoadingBar: (loadingBar: boolean) => { + store.update((state) => ({ ...state, loadingBar })) + }, + setDefaultAction: (defaultAction: string) => { + store.update((state) => ({ ...state, defaultAction })) + }, + setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => { + store.update((state) => ({ ...state, actionPanel })) } } } diff --git a/apps/desktop/src/lib/stores/auth.ts b/apps/desktop/src/lib/stores/auth.ts new file mode 100644 index 0000000..c88e3bd --- /dev/null +++ b/apps/desktop/src/lib/stores/auth.ts @@ -0,0 +1,47 @@ +import { supabase } from "@/supabase" +import type { AuthError, Session, User } from "@supabase/supabase-js" +import { get, writable, type Writable } from "svelte/store" + +type State = { session: Session | null; user: User | null } + +interface AuthAPI { + get: () => State + refresh: () => Promise + signOut: () => Promise<{ error: AuthError | null }> + signInExchange: (code: string) => Promise<{ error: AuthError | null }> +} + +function createAuth(): Writable & AuthAPI { + const store = writable({ session: null, user: null }) + async function refresh() { + const { + data: { session }, + error + } = await supabase.auth.getSession() + const { + data: { user } + } = await supabase.auth.getUser() + store.update((state) => ({ ...state, session, user })) + } + async function signOut() { + return supabase.auth.signOut().then((res) => { + refresh() + return res + }) + } + async function signInExchange(code: string) { + return supabase.auth.exchangeCodeForSession(code).then((res) => { + refresh() + return res + }) + } + return { + ...store, + get: () => get(store), + refresh, + signOut, + signInExchange + } +} + +export const auth = createAuth() diff --git a/apps/desktop/src/lib/stores/cmdQuery.ts b/apps/desktop/src/lib/stores/cmdQuery.ts new file mode 100644 index 0000000..18bcb01 --- /dev/null +++ b/apps/desktop/src/lib/stores/cmdQuery.ts @@ -0,0 +1,23 @@ +import { findAllArgsInLink } from "@/cmds/quick-links" +import { CmdTypeEnum } from "@kksh/api/models" +import type { CmdQuery, CmdValue } from "@kksh/ui/main" +import { derived, get, writable, type Writable } from "svelte/store" +import { appState } from "./appState" + +function createCmdQueryStore(): Writable { + const store = writable([]) + appState.subscribe(($appState) => { + if ($appState.highlightedCmd.startsWith("{")) { + const parsedCmd = JSON.parse($appState.highlightedCmd) as CmdValue + if (parsedCmd.cmdType === CmdTypeEnum.QuickLink && parsedCmd.data) { + return store.set(findAllArgsInLink(parsedCmd.data).map((arg) => ({ name: arg, value: "" }))) + } + } + store.set([]) + }) + return { + ...store + } +} + +export const cmdQueries = createCmdQueryStore() diff --git a/apps/desktop/src/lib/stores/extensions.ts b/apps/desktop/src/lib/stores/extensions.ts index 0bcb1a4..770ed2a 100644 --- a/apps/desktop/src/lib/stores/extensions.ts +++ b/apps/desktop/src/lib/stores/extensions.ts @@ -10,22 +10,25 @@ import { appConfig } from "./appConfig" function createExtensionsStore(): Writable & { init: () => Promise getExtensionsFromStore: () => ExtPackageJsonExtra[] + installTarball: (tarballPath: string, extsDir: string) => Promise + installDevExtensionDir: (dirPath: string) => Promise installFromTarballUrl: (tarballUrl: string, installDir: string) => Promise + installFromNpmPackageName: (name: string, installDir: string) => Promise findStoreExtensionByIdentifier: (identifier: string) => ExtPackageJsonExtra | undefined registerNewExtensionByPath: (extPath: string) => Promise uninstallStoreExtensionByIdentifier: (identifier: string) => Promise upgradeStoreExtension: (identifier: string, tarballUrl: string) => Promise } { - const { subscribe, update, set } = writable([]) + const store = writable([]) function init() { return extAPI.loadAllExtensionsFromDb().then((exts) => { - set(exts) + store.set(exts) }) } function getExtensionsFromStore() { - const extContainerPath = get(appConfig).extensionPath + const extContainerPath = get(appConfig).extensionsInstallDir if (!extContainerPath) return [] return get(extensions).filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath)) } @@ -43,7 +46,7 @@ function createExtensionsStore(): Writable & { return extAPI .loadExtensionManifestFromDisk(await path.join(extPath, "package.json")) .then((ext) => { - update((exts) => { + store.update((exts) => { const existingExt = exts.find((e) => e.extPath === ext.extPath) if (existingExt) return exts return [...exts, ext] @@ -56,12 +59,36 @@ function createExtensionsStore(): Writable & { }) } + /** + * 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`) @@ -69,7 +96,7 @@ function createExtensionsStore(): Writable & { return extAPI .uninstallExtensionByPath(targetPath) - .then(() => update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath))) + .then(() => store.update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath))) .then(() => targetExt) } @@ -83,7 +110,7 @@ function createExtensionsStore(): Writable & { identifier: string, tarballUrl: string ): Promise { - const extsDir = get(appConfig).extensionPath + const extsDir = get(appConfig).extensionsInstallDir if (!extsDir) throw new Error("Extension path not set") return uninstallStoreExtensionByIdentifier(identifier).then(() => installFromTarballUrl(tarballUrl, extsDir) @@ -91,16 +118,17 @@ function createExtensionsStore(): Writable & { } return { + ...store, init, getExtensionsFromStore, findStoreExtensionByIdentifier, registerNewExtensionByPath, + installTarball, + installDevExtensionDir, installFromTarballUrl, + installFromNpmPackageName, uninstallStoreExtensionByIdentifier, - upgradeStoreExtension, - subscribe, - update, - set + upgradeStoreExtension } } @@ -109,7 +137,7 @@ export const extensions = createExtensionsStore() export const installedStoreExts: Readable = derived( extensions, ($extensionsStore) => { - const extContainerPath = get(appConfig).extensionPath + const extContainerPath = get(appConfig).extensionsInstallDir if (!extContainerPath) return [] return $extensionsStore.filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath)) } @@ -117,7 +145,7 @@ export const installedStoreExts: Readable = derived( export const devStoreExts: Readable = derived( extensions, ($extensionsStore) => { - const extContainerPath = get(appConfig).extensionPath + const extContainerPath = get(appConfig).extensionsInstallDir if (!extContainerPath) return [] return $extensionsStore.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath)) } diff --git a/apps/desktop/src/lib/stores/index.ts b/apps/desktop/src/lib/stores/index.ts index daaa348..141acbc 100644 --- a/apps/desktop/src/lib/stores/index.ts +++ b/apps/desktop/src/lib/stores/index.ts @@ -2,3 +2,5 @@ export * from "./appConfig" export * from "./appState" export * from "./winExtMap" export * from "./extensions" +export * from "./auth" +export * from "./quick-links" diff --git a/apps/desktop/src/lib/stores/quick-links.ts b/apps/desktop/src/lib/stores/quick-links.ts new file mode 100644 index 0000000..e265ca4 --- /dev/null +++ b/apps/desktop/src/lib/stores/quick-links.ts @@ -0,0 +1,39 @@ +import type { Icon } from "@kksh/api/models" +import { createQuickLinkCommand, getAllQuickLinkCommands } from "@kksh/extension/db" +import type { CmdQuery, QuickLink } from "@kksh/ui/types" +import { get, writable, type Writable } from "svelte/store" + +export interface QuickLinkAPI { + get: () => QuickLink[] + init: () => Promise + refresh: () => Promise + createQuickLink: (name: string, link: string, icon: Icon) => Promise +} + +function createQuickLinksStore(): Writable & QuickLinkAPI { + const store = writable([]) + + async function init() { + refresh() + } + + async function refresh() { + const cmds = await getAllQuickLinkCommands() + store.set(cmds.map((cmd) => ({ link: cmd.data.link, name: cmd.name, icon: cmd.data.icon }))) + } + + async function createQuickLink(name: string, link: string, icon: Icon) { + await createQuickLinkCommand(name, link, icon) + await refresh() + } + + return { + ...store, + get: () => get(store), + init, + refresh, + createQuickLink + } +} + +export const quickLinks = createQuickLinksStore() diff --git a/apps/desktop/src/lib/stores/winExtMap.ts b/apps/desktop/src/lib/stores/winExtMap.ts index 7b5cd5f..2bea26a 100644 --- a/apps/desktop/src/lib/stores/winExtMap.ts +++ b/apps/desktop/src/lib/stores/winExtMap.ts @@ -40,6 +40,7 @@ function createWinExtMapStore(): Writable & API { async function init() {} return { + ...store, init, registerExtensionWithWindow: async ({ extPath, @@ -58,11 +59,11 @@ function createWinExtMapStore(): Writable & API { await killProcesses(winExtMap[windowLabel].pids) delete winExtMap[windowLabel] } else { - winExtMap[windowLabel] = { - windowLabel, - extPath, - pids: [] - } + // winExtMap[windowLabel] = { + // windowLabel, + // extPath, + // pids: [] + // } } } const returnedWinLabel = await registerExtensionWindow({ @@ -70,6 +71,11 @@ function createWinExtMapStore(): Writable & API { windowLabel, dist }) + winExtMap[returnedWinLabel] = { + windowLabel: returnedWinLabel, + extPath, + pids: [] + } store.set(winExtMap) return returnedWinLabel }, @@ -109,10 +115,7 @@ function createWinExtMapStore(): Writable & API { return unregisterExtensionSpawnedProcess(windowLabel, pid).then(() => { ext.pids = ext.pids.filter((p) => p !== pid) }) - }, - subscribe: store.subscribe, - update: store.update, - set: store.set + } } } diff --git a/apps/desktop/src/lib/utils/deeplink.ts b/apps/desktop/src/lib/utils/deeplink.ts new file mode 100644 index 0000000..11bcc7f --- /dev/null +++ b/apps/desktop/src/lib/utils/deeplink.ts @@ -0,0 +1,115 @@ +import { emitRefreshDevExt } from "@/utils/tauri-events" +import { + DEEP_LINK_PATH_AUTH_CONFIRM, + DEEP_LINK_PATH_OPEN, + DEEP_LINK_PATH_REFRESH_DEV_EXTENSION, + DEEP_LINK_PATH_STORE +} from "@kksh/api" +import type { UnlistenFn } from "@tauri-apps/api/event" +import { extname } from "@tauri-apps/api/path" +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" +import * as deepLink from "@tauri-apps/plugin-deep-link" +import { error } from "@tauri-apps/plugin-log" +import { goto } from "$app/navigation" +import { toast } from "svelte-sonner" +import * as v from "valibot" +import { isInMainWindow } from "./window" + +const StorePathSearchParams = v.object({ + identifier: v.optional(v.string()) +}) + +export function initDeeplink(): Promise { + console.log("init deeplink") + if (!isInMainWindow()) { + return Promise.resolve(() => {}) + } + // deepLink.getCurrent() + return deepLink.onOpenUrl((urls) => { + console.log("deep link:", urls) + urls.forEach(handleDeepLink) + }) +} + +/** + * Show and focus on the main window + */ +function openMainWindow() { + const appWindow = getCurrentWebviewWindow() + return appWindow + .show() + .then(() => appWindow.setFocus()) + .catch((err) => { + console.error(err) + error(`Failed to show window upon deep link: ${err.message}`) + toast.error("Failed to show window upon deep link", { + description: err.message + }) + }) +} + +export async function handleKunkunProtocol(parsedUrl: URL) { + const params = Object.fromEntries(parsedUrl.searchParams) + const { host, pathname, href } = parsedUrl + if (href.startsWith(DEEP_LINK_PATH_OPEN)) { + openMainWindow() + } else if (href.startsWith(DEEP_LINK_PATH_STORE)) { + const parsed = v.parse(StorePathSearchParams, params) + openMainWindow() + if (parsed.identifier) { + goto(`/extension/store/${parsed.identifier}`) + } else { + goto("/extension/store") + } + } else if (href.startsWith(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)) { + emitRefreshDevExt() + } else if (href.startsWith(DEEP_LINK_PATH_AUTH_CONFIRM)) { + openMainWindow() + goto(`/auth/confirm?${parsedUrl.searchParams.toString()}`) + } else { + console.error("Invalid path:", pathname) + toast.error("Invalid path", { + description: parsedUrl.href + }) + } +} + +export async function handleFileProtocol(parsedUrl: URL) { + console.log("File protocol:", parsedUrl) + const filePath = parsedUrl.pathname // Remove the leading '//' kunkun://open?identifier=qrcode gives "open" + console.log("File path:", filePath) + // from file absolute path, get file extension + const fileExt = await extname(filePath) + console.log("File extension:", fileExt) + switch (fileExt) { + case "kunkun": + // TODO: Handle file protocol, install extension from file (essentially a .tgz file) + break + default: + console.error("Unknown file extension:", fileExt) + toast.error("Unknown file extension", { + description: fileExt + }) + break + } +} + +/** + * + * @param url Deep Link URl, e.g. kunkun://open + */ +export async function handleDeepLink(url: string) { + const parsedUrl = new URL(url) + switch (parsedUrl.protocol) { + case "kunkun:": + return handleKunkunProtocol(parsedUrl) + case "file:": + return handleFileProtocol(parsedUrl) + default: + console.error("Invalid Protocol:", parsedUrl.protocol) + toast.error("Invalid Protocol", { + description: parsedUrl.protocol + }) + break + } +} diff --git a/apps/desktop/src/lib/utils/dom.ts b/apps/desktop/src/lib/utils/dom.ts new file mode 100644 index 0000000..36bcfa6 --- /dev/null +++ b/apps/desktop/src/lib/utils/dom.ts @@ -0,0 +1,3 @@ +export function getActiveElementNodeName(): string | undefined { + return document.activeElement?.nodeName +} diff --git a/apps/desktop/src/lib/utils/key.ts b/apps/desktop/src/lib/utils/key.ts index 62f16f1..61e9527 100644 --- a/apps/desktop/src/lib/utils/key.ts +++ b/apps/desktop/src/lib/utils/key.ts @@ -23,3 +23,13 @@ export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) { } } } + +export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) { + if (e.key === "Escape") { + if (appState.get().searchTerm) { + appState.clearSearchTerm() + } else { + goHome() + } + } +} diff --git a/apps/desktop/src/lib/utils/sync-store.ts b/apps/desktop/src/lib/utils/sync-store.ts new file mode 100644 index 0000000..667f6a5 --- /dev/null +++ b/apps/desktop/src/lib/utils/sync-store.ts @@ -0,0 +1,44 @@ +import * as evt from "@tauri-apps/api/event" +import { writable, type Writable } from "svelte/store" + +export function buildEventName(storeName: string) { + return `app://sync-store-${storeName}` +} + +export type WithSyncStore = Writable & { + listen: () => void + unlisten: evt.UnlistenFn | undefined +} + +export function createTauriSyncStore(storeName: string, initialValue: T): WithSyncStore { + const store = writable(initialValue) + let unlisten: evt.UnlistenFn | undefined + + async function listen() { + console.log("[listen] start", storeName) + if (unlisten) { + console.log("[listen] already listening, skip") + return + } + const _unlisten = await evt.listen<{ value: T }>(buildEventName(storeName), (evt) => { + console.log(`[listen] update from tauri event`, storeName, evt.payload.value) + store.set(evt.payload.value) + }) + const unsubscribe = store.subscribe((value) => { + console.log("[subscribe] got update, emit data", storeName, value) + evt.emit(buildEventName(storeName), { value }) + }) + unlisten = () => { + _unlisten() + unsubscribe() + unlisten = undefined + } + return unlisten + } + + return { + ...store, + listen, + unlisten + } +} diff --git a/apps/desktop/src/lib/utils/tauri-events.ts b/apps/desktop/src/lib/utils/tauri-events.ts new file mode 100644 index 0000000..1102342 --- /dev/null +++ b/apps/desktop/src/lib/utils/tauri-events.ts @@ -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) { + return listen(TauriEvent.WINDOW_BLUR, cb) +} + +export function listenToWindowFocus(cb: EventCallback) { + return listen(TauriEvent.WINDOW_FOCUS, cb) +} + +export function listenToNewClipboardItem(cb: EventCallback) { + return listen(NewClipboardItemAddedEvent, cb) +} + +export function emitRefreshConfig() { + return emit(RefreshConfigEvent) +} + +export function listenToRefreshConfig(cb: EventCallback) { + return listen(RefreshConfigEvent, cb) +} + +export function emitRefreshExt() { + return emitTo("main", RefreshExtEvent) +} + +export function listenToRefreshExt(cb: EventCallback) { + return listen(RefreshExtEvent, cb) +} + +export function emitRefreshDevExt() { + return emit(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION) +} + +export function listenToRefreshDevExt(cb: EventCallback) { + return listen(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION, cb) +} diff --git a/apps/desktop/src/lib/utils/updater.ts b/apps/desktop/src/lib/utils/updater.ts new file mode 100644 index 0000000..17705b0 --- /dev/null +++ b/apps/desktop/src/lib/utils/updater.ts @@ -0,0 +1,88 @@ +import { extensions } from "@/stores" +import { supabaseAPI } from "@/supabase" +import { isCompatible } from "@kksh/api" +import type { ExtPackageJsonExtra } from "@kksh/api/models" +import { greaterThan } from "@std/semver" +import { relaunch } from "@tauri-apps/plugin-process" +import { check } from "@tauri-apps/plugin-updater" +import { gt } from "semver" +import { toast } from "svelte-sonner" +import { get } from "svelte/store" + +export async function checkUpdateAndInstall({ beta }: { beta?: boolean } = {}) { + const update = await check({ + headers: { + "kk-updater-mode": beta ? "beta" : "stable" + } + }) + if (update?.available) { + const confirmUpdate = await confirm( + `A new version ${update.version} is available. Do you want to install and relaunch?` + ) + if (confirmUpdate) { + await update.downloadAndInstall() + await relaunch() + } + } else { + toast.info("You are on the latest version") + } +} + +export async function checkSingleExtensionUpdate( + installedExt: ExtPackageJsonExtra, + autoupgrade: boolean +) { + const { data: sbExt, error } = await supabaseAPI.getLatestExtPublish( + installedExt.kunkun.identifier + ) + if (error) { + return toast.error(`Failed to check update for ${installedExt.kunkun.identifier}: ${error}`) + } + + if (!sbExt) { + return null + } + + if ( + gt(sbExt.version, installedExt.version) && + (sbExt.api_version ? isCompatible(sbExt.api_version) : true) + ) { + if (autoupgrade) { + await extensions + .upgradeStoreExtension( + sbExt.identifier, + supabaseAPI.translateExtensionFilePathToUrl(sbExt.tarball_path) + ) + .then(() => { + toast.success(`${sbExt.name} upgraded`, { + description: `From ${installedExt.version} to ${sbExt.version}` + }) + }) + .catch((err) => { + toast.error(`Failed to upgrade ${sbExt.name}`, { description: err }) + }) + return true + } else { + console.log(`new version available ${installedExt.kunkun.identifier} ${sbExt.version}`) + toast.info( + `Extension ${installedExt.kunkun.identifier} has a new version ${sbExt.version}, you can upgrade in Store.`, + { duration: 10_000 } + ) + } + } + return false +} + +export async function checkExtensionUpdate(autoupgrade: boolean = false) { + let upgradedCount = 0 + for (const ext of get(extensions)) { + const upgraded = await checkSingleExtensionUpdate(ext, autoupgrade) + if (upgraded) { + upgradedCount++ + } + } + + if (upgradedCount > 0) { + toast.info(`${upgradedCount} extensions have been upgraded`) + } +} diff --git a/apps/desktop/src/routes/+error.svelte b/apps/desktop/src/routes/+error.svelte index 91a5da3..a4ae1af 100644 --- a/apps/desktop/src/routes/+error.svelte +++ b/apps/desktop/src/routes/+error.svelte @@ -1,4 +1,5 @@ - + + { + return commandScore( + value.startsWith("{") ? (JSON.parse(value) as CmdValue).cmdName : value, + search, + keywords + ) + }} + loop +> + + {#snippet rightSlot()} + + {#each $cmdQueries as cmdQuery} + {@const queryWidth = Math.max(cmdQuery.name.length, cmdQuery.value.length) + 2} + { + if (evt.key === "Enter") { + evt.preventDefault() + evt.stopPropagation() + commandLaunchers.onQuickLinkSelect( + JSON.parse($appState.highlightedCmd), + $cmdQueries + ) + } + }} + bind:value={cmdQuery.value} + /> + {/each} + + + + + + + + + Settings + + exit()}>Quit + openDevTools()}>Open Dev Tools + getCurrentWebviewWindow().hide()} + >Close Window + + + + {/snippet} + + + No results found. + {#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0} + + {/if} + {#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0} + + {/if} + + + + + + diff --git a/apps/desktop/src/routes/auth/+page.svelte b/apps/desktop/src/routes/auth/+page.svelte new file mode 100644 index 0000000..4a42802 --- /dev/null +++ b/apps/desktop/src/routes/auth/+page.svelte @@ -0,0 +1,62 @@ + + + + +
+ + + + Kunkun + Sign In + + + + + + + diff --git a/apps/desktop/src/routes/auth/confirm/+page.svelte b/apps/desktop/src/routes/auth/confirm/+page.svelte new file mode 100644 index 0000000..78e2035 --- /dev/null +++ b/apps/desktop/src/routes/auth/confirm/+page.svelte @@ -0,0 +1,84 @@ + + + + +
+
+
+
+ {#if $auth.session} + Welcome, You are Logged In + {:else} + You Are Not Logged In + {/if} + + {#if $auth.session} + + + {avatarFallback} + + {/if} + + +
+
+
diff --git a/apps/desktop/src/routes/auth/confirm/+page.ts b/apps/desktop/src/routes/auth/confirm/+page.ts new file mode 100644 index 0000000..3418ef7 --- /dev/null +++ b/apps/desktop/src/routes/auth/confirm/+page.ts @@ -0,0 +1,10 @@ +import { error } from "@sveltejs/kit" +import type { PageLoad } from "./$types" + +export const load: PageLoad = async ({ params, url }) => { + const code = url.searchParams.get("code") + if (!code) { + throw error(400, "Auth Exchange Code is Required") + } + return { params, code } +} diff --git a/apps/desktop/src/routes/extension/create-quick-link/+page.svelte b/apps/desktop/src/routes/extension/create-quick-link/+page.svelte new file mode 100644 index 0000000..4e782f4 --- /dev/null +++ b/apps/desktop/src/routes/extension/create-quick-link/+page.svelte @@ -0,0 +1,108 @@ + + + + +
+
+

Create Quick Link

+
+ + + {#snippet children({ props })} + Name + + {/snippet} + + Quick Link Display Name + + + + + {#snippet children({ props })} + Link + + {/snippet} + + Quick Link URL + + + + + + + Submit + +
+{#if dev} +
+ +
+{/if} diff --git a/apps/desktop/src/routes/extension/create-quick-link/schema.ts b/apps/desktop/src/routes/extension/create-quick-link/schema.ts new file mode 100644 index 0000000..c4de730 --- /dev/null +++ b/apps/desktop/src/routes/extension/create-quick-link/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +export const formSchema = z.object({ + username: z.string().min(2).max(50) +}) + +export type FormSchema = typeof formSchema diff --git a/apps/desktop/src/routes/extension/permission-inspector/+page.svelte b/apps/desktop/src/routes/extension/permission-inspector/+page.svelte new file mode 100644 index 0000000..2294407 --- /dev/null +++ b/apps/desktop/src/routes/extension/permission-inspector/+page.svelte @@ -0,0 +1,100 @@ + + + + +
+ +

Extension Permission Inspector

+ +
+ {#each pkgJsons as pkgJson} + + + {pkgJson.kunkun.name} + {pkgJson.kunkun.shortDescription} + + + + + +

+ Identifier: {pkgJson.kunkun.identifier} +

+

+ Extension Path: {pkgJson.extPath} +

+
+
+ {/each} +
+
+ + diff --git a/apps/desktop/src/routes/extension/store/+page.svelte b/apps/desktop/src/routes/extension/store/+page.svelte index 7f53ef9..d8d7a1f 100644 --- a/apps/desktop/src/routes/extension/store/+page.svelte +++ b/apps/desktop/src/routes/extension/store/+page.svelte @@ -2,8 +2,8 @@ import { getExtensionsFolder } from "@/constants" import { appState, extensions } from "@/stores" import { supabaseAPI } from "@/supabase" - import { goBackOnEscapeClearSearchTerm } from "@/utils/key" - import { goBack } from "@/utils/route" + import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key" + import { goBack, goHome } from "@/utils/route" import { SBExt } from "@kksh/api/supabase" import { isUpgradable } from "@kksh/extension" import { Button, Command } from "@kksh/svelte5" @@ -64,20 +64,19 @@ } - - + {#snippet leftSlot()} {/snippet} - + - + goto("/")} + onGoBack={() => goto("/")} rawJsonError={JSON.stringify($page, null, 2)} /> diff --git a/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte b/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte index 67ce854..55a7f97 100644 --- a/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte +++ b/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte @@ -9,13 +9,15 @@ import { StoreExtDetail } from "@kksh/ui/extension" import { greaterThan, parse as parseSemver } from "@std/semver" import { error } from "@tauri-apps/plugin-log" + import { goto } from "$app/navigation" import { ArrowLeftIcon } from "lucide-svelte" import { onMount } from "svelte" import { toast } from "svelte-sonner" import { get, derived as storeDerived } from "svelte/store" const { data } = $props() - let { ext, manifest } = data + const ext = $derived(data.ext) + const manifest = $derived(data.manifest) const installedExt = storeDerived(installedStoreExts, ($e) => { return $e.find((e) => e.kunkun.identifier === ext.identifier) }) @@ -36,9 +38,9 @@ onMount(() => { showBtn = { - install: !installedExt, + install: !$installedExt, upgrade: isUpgradable, - uninstall: !!installedExt + uninstall: !!$installedExt } }) @@ -114,43 +116,44 @@ .uninstallStoreExtensionByIdentifier(ext.identifier) .then((uninstalledExt) => { toast.success(`${uninstalledExt.name} Uninstalled`) + loading.uninstall = false + showBtn.uninstall = false + showBtn.install = true }) .catch((err) => { toast.error("Fail to uninstall extension", { description: err }) error(`Fail to uninstall store extension (${ext.identifier}): ${err}`) }) - .finally(() => { - loading.uninstall = false - showBtn.uninstall = false - showBtn.install = true - }) + .finally(() => {}) } function onEnterPressed() { - return onInstallSelected() + if (showBtn.install) { + return onInstallSelected() + } } function handleKeydown(e: KeyboardEvent) { if (e.key === "Escape") { if (!delayedImageDialogOpen) { - goBack() + goto("/extension/store") } } } - manifest: KunkunExtManifest + params: { + identifier: string + } }> => { + console.log("store[identifier] params", params) + const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier) if (dbError) { return error(400, { @@ -30,6 +35,7 @@ export const load: PageLoad = async ({ return { ext, + params, manifest: parseManifest.output } } diff --git a/apps/desktop/src/routes/extension/ui-worker/+page.svelte b/apps/desktop/src/routes/extension/ui-worker/+page.svelte index 5bf03f4..2dffffc 100644 --- a/apps/desktop/src/routes/extension/ui-worker/+page.svelte +++ b/apps/desktop/src/routes/extension/ui-worker/+page.svelte @@ -1 +1,289 @@ - + + +{#if loadingBar} + +{/if} +{#if loaded && listViewContent !== undefined} + { + workerAPI?.onListScrolledToBottom() + }} + onEnterKeyPressed={() => { + workerAPI?.onEnterPressedOnSearchBar() + }} + onListItemSelected={(value: string) => { + workerAPI?.onListItemSelected(value) + }} + onSearchTermChange={(searchTerm) => { + workerAPI?.onSearchTermChange(searchTerm) + }} + onHighlightedItemChanged={(value) => { + workerAPI?.onHighlightedListItemChanged(value) + if (listViewContent?.defaultAction) { + appState.setDefaultAction(listViewContent.defaultAction) + } + if (listViewContent?.actions) { + appState.setActionPanel(listViewContent.actions) + } + }} + > + {#snippet footer()} + { + workerAPI?.onEnterPressedOnSearchBar() + }} + onActionSelected={(value) => { + workerAPI?.onActionSelected(value) + }} + /> + {/snippet} + +{:else if loaded && formViewContent !== undefined} + +{:else if loaded && markdownViewContent !== undefined} + +{/if} diff --git a/apps/desktop/src/routes/extension/ui-worker/+page.ts b/apps/desktop/src/routes/extension/ui-worker/+page.ts new file mode 100644 index 0000000..711ac4a --- /dev/null +++ b/apps/desktop/src/routes/extension/ui-worker/+page.ts @@ -0,0 +1,76 @@ +import { db, unregisterExtensionWindow } from "@kksh/api/commands" +import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models" +import { loadExtensionManifestFromDisk } from "@kksh/extension" +import { error as sbError } from "@sveltejs/kit" +import { join } from "@tauri-apps/api/path" +import { exists, readTextFile } from "@tauri-apps/plugin-fs" +import { error } from "@tauri-apps/plugin-log" +import { goto } from "$app/navigation" +import { toast } from "svelte-sonner" +import type { PageLoad } from "./$types" + +// : Promise<{ +// extPath: string +// scriptPath: string +// // workerScript: string +// pkgJsonPath: string +// cmdName: string +// loadedExt: ExtPackageJsonExtra +// extInfoInDB: ExtInfoInDB +// }> + +export const load: PageLoad = async ({ url }) => { + // both query parameter must exist + const extPath = url.searchParams.get("extPath") + const cmdName = url.searchParams.get("cmdName") + if (!extPath || !cmdName) { + toast.error("Invalid extension path or url") + error("Invalid extension path or url") + goto("/") + } + + let _loadedExt: ExtPackageJsonExtra | undefined + try { + _loadedExt = await loadExtensionManifestFromDisk(await join(extPath!, "package.json")) + } catch (err) { + error(`Error loading extension manifest: ${err}`) + toast.error("Error loading extension manifest", { + description: `${err}` + }) + goto("/") + } + const loadedExt = _loadedExt! + const extInfoInDB = await db.getUniqueExtensionByPath(loadedExt.extPath) + if (!extInfoInDB) { + toast.error("Unexpected Error", { + description: `Extension ${loadedExt.kunkun.identifier} not found in database. Run Troubleshooter.` + }) + goto("/") + } + const pkgJsonPath = await join(extPath!, "package.json") + if (!(await exists(extPath!))) { + sbError(404, `Extension not found at ${extPath}`) + } + if (!(await exists(pkgJsonPath))) { + sbError(404, `Extension package.json not found at ${pkgJsonPath}`) + } + + const cmd = loadedExt.kunkun.templateUiCmds.find((cmd) => cmd.name === cmdName) + if (!cmd) { + sbError(404, `Command ${cmdName} not found in extension ${loadedExt.kunkun.identifier}`) + } + const scriptPath = await join(loadedExt.extPath, cmd.main) + if (!(await exists(scriptPath))) { + sbError(404, `Command script not found at ${scriptPath}`) + } + // const workerScript = await readTextFile(scriptPath) + return { + extPath: extPath!, + pkgJsonPath, + scriptPath, + // workerScript, + cmdName: cmdName!, + loadedExt, + extInfoInDB: extInfoInDB! + } +} diff --git a/apps/desktop/src/routes/settings/add-dev-extension/+page.svelte b/apps/desktop/src/routes/settings/add-dev-extension/+page.svelte new file mode 100644 index 0000000..e8d9d1b --- /dev/null +++ b/apps/desktop/src/routes/settings/add-dev-extension/+page.svelte @@ -0,0 +1,31 @@ + + + + +
+
+

Add Dev Extension

+ + There are 4 options to install an extension in developer mode. Either load it from your local + tarball file, a tarball remote URL, npm package name or load from a remote URL. + + +
diff --git a/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte b/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte index 867100e..5d5d0ee 100644 --- a/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte +++ b/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte @@ -6,8 +6,8 @@ import { ArrowLeftIcon } from "lucide-svelte" - -
diff --git a/apps/desktop/src/routes/troubleshooters/extension-loading/+page.svelte b/apps/desktop/src/routes/troubleshooters/extension-loading/+page.svelte new file mode 100644 index 0000000..df4901d --- /dev/null +++ b/apps/desktop/src/routes/troubleshooters/extension-loading/+page.svelte @@ -0,0 +1,129 @@ + + + + +
+
+

Extension Loading Troubleshooter

+ + + + + Error Details + + {errorMsg} + + + + A list of your extensions. + + + Identifier + Path + Error + + + + {#each sortedResults as row} + +
{row.identifier}
+ + + + + + +
+ {/each} +
+
+
+ + diff --git a/apps/desktop/src/routes/troubleshooters/extension-window/+page.svelte b/apps/desktop/src/routes/troubleshooters/extension-window/+page.svelte new file mode 100644 index 0000000..08c0044 --- /dev/null +++ b/apps/desktop/src/routes/troubleshooters/extension-window/+page.svelte @@ -0,0 +1,111 @@ + + + + +
+
+
+ + +
+ + + Refreshed {refreshCount} times + +
+ + {#each Object.entries(winLabelMap) as [label, content]} +
  • + + Label: +
    {label}
    +
    +
      + {#each Object.entries(content) as [key, value]} +
    • + + {key}: +
      {value}
      +
      +
    • + {/each} +
    + +
  • + {/each} +
    +
    diff --git a/apps/desktop/tailwind.config.ts b/apps/desktop/tailwind.config.ts index fbc57f7..95c4e88 100644 --- a/apps/desktop/tailwind.config.ts +++ b/apps/desktop/tailwind.config.ts @@ -1,3 +1,4 @@ +import typography from "@tailwindcss/typography" import type { Config } from "tailwindcss" import tailwindcssAnimate from "tailwindcss-animate" import { fontFamily } from "tailwindcss/defaultTheme" @@ -9,7 +10,7 @@ const config: Config = { "./node_modules/@kksh/ui/src/**/*.{html,js,svelte,ts}", "../../node_modules/@kksh/svelte5/src/**/*.{html,js,svelte,ts}" ], - safelist: ["dark"], + safelist: ["dark", "bg-red-500/30"], theme: { container: { center: true, @@ -94,7 +95,7 @@ const config: Config = { } } }, - plugins: [tailwindcssAnimate] + plugins: [tailwindcssAnimate, typography] } export default config diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 593dc19..a2288c7 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -1,19 +1,20 @@ { - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias - // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + }, + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in + "include": ["src/**/*"] } diff --git a/apps/desktop/uno.config.ts b/apps/desktop/uno.config.ts new file mode 100644 index 0000000..7ca4602 --- /dev/null +++ b/apps/desktop/uno.config.ts @@ -0,0 +1,5 @@ +import { defineConfig, presetAttributify, presetTagify, presetUno } from "unocss" + +export default defineConfig({ + presets: [presetUno(), presetAttributify(), presetTagify()] +}) diff --git a/apps/desktop/vite.config.js b/apps/desktop/vite.config.js index 7aa392c..09ea201 100644 --- a/apps/desktop/vite.config.js +++ b/apps/desktop/vite.config.js @@ -1,4 +1,5 @@ import { sveltekit } from "@sveltejs/kit/vite" +import UnoCSS from "unocss/vite" import { defineConfig } from "vite" // @ts-expect-error process is a nodejs global @@ -6,8 +7,7 @@ const host = process.env.TAURI_DEV_HOST // https://vitejs.dev/config/ export default defineConfig(async () => ({ - plugins: [sveltekit()], - + plugins: [UnoCSS(), sveltekit()], // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent vite from obscuring rust errors diff --git a/package.json b/package.json index c980dcc..382d7fb 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@kksh/api": "workspace:*", - "@kksh/svelte5": "0.1.2-beta.4", + "@kksh/svelte5": "0.1.2-beta.8", "prettier": "^3.2.5", "prettier-plugin-svelte": "^3.2.7", "prettier-plugin-tailwindcss": "^0.6.8", diff --git a/packages/api/jsr.json b/packages/api/jsr.json index 334de1a..37cc4c3 100644 --- a/packages/api/jsr.json +++ b/packages/api/jsr.json @@ -1,7 +1,7 @@ { "$schema": "https://jsr.io/schema/config-file.v1.json", "name": "@kunkun/api", - "version": "0.0.27", + "version": "0.0.28", "license": "MIT", "exports": { ".": "./src/index.ts", diff --git a/packages/api/package.json b/packages/api/package.json index f171610..96ea468 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@kksh/api", - "version": "0.0.27", + "version": "0.0.28", "type": "module", "exports": { ".": "./src/index.ts", diff --git a/packages/api/src/commands/extension.ts b/packages/api/src/commands/extension.ts index d7b02d0..66481bb 100644 --- a/packages/api/src/commands/extension.ts +++ b/packages/api/src/commands/extension.ts @@ -26,7 +26,6 @@ export function registerExtensionWindow(options: { } export function unregisterExtensionWindow(label: string): Promise { - console.log("unregisterExtensionWindow", label) return invoke(generateJarvisPluginCommand("unregister_extension_window"), { label }) diff --git a/packages/api/src/commands/system.ts b/packages/api/src/commands/system.ts index 953f7fb..40746be 100644 --- a/packages/api/src/commands/system.ts +++ b/packages/api/src/commands/system.ts @@ -295,7 +295,7 @@ export const rawSystemCommands = [ } ] -export async function getSystemCommands(): Promise { +export function getSystemCommands(): SysCommand[] { return rawSystemCommands .filter(async (cmd) => cmd.platforms.includes(platform())) // Filter out system commands that are not supported on the current platform .map((cmd) => ({ diff --git a/packages/api/src/constants.ts b/packages/api/src/constants.ts index a3b8789..83fda7a 100644 --- a/packages/api/src/constants.ts +++ b/packages/api/src/constants.ts @@ -7,7 +7,7 @@ export enum KUNKUN_EXT_IDENTIFIER { } export const KUNKUN_DESKTOP_APP_SERVER_PORTS = [1566, 1567, 1568, 9559, 9560, 9561] -export const DESKTOP_SERVICE_NAME = "Kunkun" +export const DESKTOP_SERVICE_NAME = "kunkun" /* -------------------------------------------------------------------------- */ /* Deep Link */ diff --git a/packages/api/src/dev/index.ts b/packages/api/src/dev/index.ts index 196fc7b..dd04977 100644 --- a/packages/api/src/dev/index.ts +++ b/packages/api/src/dev/index.ts @@ -15,7 +15,7 @@ export function checkLocalKunkunService(port: number): Promise { return res.json() }) .then((data) => { - return data["service_name"] === DESKTOP_SERVICE_NAME + return data["service_name"].toLowerCase() === DESKTOP_SERVICE_NAME.toLowerCase() }) .catch((err) => { // fetch fail, i.e. server not on this port diff --git a/packages/api/src/models/extension.ts b/packages/api/src/models/extension.ts index 0b1a150..4c7f2e6 100644 --- a/packages/api/src/models/extension.ts +++ b/packages/api/src/models/extension.ts @@ -41,6 +41,8 @@ export type Ext = InferOutput export enum CmdTypeEnum { HeadlessWorker = "headless_worker", + Builtin = "builtin", + System = "system", UiWorker = "ui_worker", UiIframe = "ui_iframe", QuickLink = "quick_link", @@ -55,12 +57,18 @@ export const ExtCmd = object({ name: string(), type: CmdType, data: string(), - alias: optional(string()), - hotkey: optional(string()), + alias: nullable(optional(string())), + hotkey: nullable(optional(string())), enabled: boolean() }) export type ExtCmd = InferOutput +export const QuickLinkCmd = object({ + ...ExtCmd.entries, + data: object({ link: string(), icon: Icon }) +}) +export type QuickLinkCmd = InferOutput + export const ExtData = object({ dataId: number(), extId: number(), diff --git a/packages/api/src/models/icon.ts b/packages/api/src/models/icon.ts index 6639ba5..26872e9 100644 --- a/packages/api/src/models/icon.ts +++ b/packages/api/src/models/icon.ts @@ -1,4 +1,13 @@ -import { enum_, literal, object, string, type InferOutput } from "valibot" +import { + boolean, + enum_, + literal, + nullable, + object, + optional, + string, + type InferOutput +} from "valibot" import { NodeName, NodeNameEnum } from "./constants" /* -------------------------------------------------------------------------- */ @@ -16,7 +25,8 @@ export type IconType = InferOutput export const Icon = object({ type: IconType, - value: string() + value: string(), + invert: optional(boolean()) }) export type Icon = InferOutput export const IconNode = object({ diff --git a/packages/api/src/ui/client.ts b/packages/api/src/ui/client.ts index c4aff94..d2a0d44 100644 --- a/packages/api/src/ui/client.ts +++ b/packages/api/src/ui/client.ts @@ -33,6 +33,7 @@ import type { fileSearch } from "../commands/fileSearch" import { type AppInfo } from "../models/apps" import type { LightMode, Position, Radius, ThemeColor } from "../models/styles" import type { DenoSysOptions } from "../permissions/schema" +import type { MarkdownSchema } from "./worker" import { type IComponent } from "./worker/components/interfaces" import type { Markdown } from "./worker/components/markdown" import * as FormSchema from "./worker/schema/form" @@ -116,7 +117,7 @@ export interface IToast { } export interface IUiWorker { - render: (view: IComponent) => Promise + render: (view: IComponent) => Promise goBack: () => Promise showLoadingBar: (loading: boolean) => Promise setScrollLoading: (loading: boolean) => Promise diff --git a/packages/api/src/ui/worker/components/form-view.ts b/packages/api/src/ui/worker/components/form-view.ts index 5f25a7c..7cb661c 100644 --- a/packages/api/src/ui/worker/components/form-view.ts +++ b/packages/api/src/ui/worker/components/form-view.ts @@ -145,6 +145,9 @@ export class Form implements IComponent { constructor(model: OmitNodeName) { this.fields = model.fields this.key = model.key + this.title = model.title + this.description = model.description + this.submitBtnText = model.submitBtnText } toModel(): FormSchema.Form { diff --git a/packages/api/src/ui/worker/schema/form.ts b/packages/api/src/ui/worker/schema/form.ts index 711dc82..7b1c125 100644 --- a/packages/api/src/ui/worker/schema/form.ts +++ b/packages/api/src/ui/worker/schema/form.ts @@ -65,7 +65,8 @@ export type BaseField = InferOutput export const InputField = object({ ...BaseField.entries, type: optional(InputTypes), - component: optional(union([literal("textarea"), literal("default")])) + component: optional(union([literal("textarea"), literal("default")])), + default: optional(string()) }) export type InputField = InferOutput @@ -74,7 +75,8 @@ export type InputField = InferOutput /* -------------------------------------------------------------------------- */ export const NumberField = object({ ...BaseField.entries, - nodeName: FormNodeName + nodeName: FormNodeName, + default: optional(number()) }) export type NumberField = InferOutput @@ -84,7 +86,8 @@ export type NumberField = InferOutput // with zod enum export const SelectField = object({ ...BaseField.entries, - options: array(string()) + options: array(string()), + default: optional(string()) }) export type SelectField = InferOutput @@ -101,7 +104,8 @@ export type BooleanField = InferOutput /* Date */ /* -------------------------------------------------------------------------- */ export const DateField = object({ - ...BaseField.entries + ...BaseField.entries, + default: optional(string()) }) export type DateField = InferOutput @@ -121,14 +125,22 @@ export type ArrayField = InferOutput /* -------------------------------------------------------------------------- */ export const FormField = union([ ArrayField, // this must be placed first, otherwise its content field won't be parsed + SelectField, InputField, NumberField, - SelectField, BooleanField, DateField ]) export type FormField = InferOutput // export type Form = InferOutput +export const Form: GenericSchema
    = object({ + nodeName: FormNodeName, + key: string(), + fields: array(union([lazy(() => Form), FormField])), + title: optional(string()), + description: optional(string()), + submitBtnText: optional(string()) +}) export type Form = { nodeName: FormNodeName title?: string @@ -137,8 +149,3 @@ export type Form = { key: string fields: (FormField | Form)[] } -export const Form: GenericSchema = object({ - nodeName: FormNodeName, - key: string(), - fields: array(union([lazy(() => Form), FormField])) -}) diff --git a/packages/api/src/version.ts b/packages/api/src/version.ts index ec7bba0..e056254 100644 --- a/packages/api/src/version.ts +++ b/packages/api/src/version.ts @@ -13,7 +13,7 @@ export const breakingChangesVersionCheckpoints = [ const checkpointVersions = breakingChangesVersionCheckpoints.map((c) => c.version) const sortedCheckpointVersions = sort(checkpointVersions) -export const version = "0.0.27" +export const version = "0.0.28" export function isVersionBetween(v: string, start: string, end: string) { const vCleaned = clean(v) diff --git a/packages/extension/src/db.ts b/packages/extension/src/db.ts index a6246ba..b76d7e4 100644 --- a/packages/extension/src/db.ts +++ b/packages/extension/src/db.ts @@ -1,5 +1,13 @@ import { db } from "@kksh/api/commands" -import { ExtPackageJson, ExtPackageJsonExtra } from "@kksh/api/models" +import { + CmdTypeEnum, + ExtCmd, + ExtPackageJson, + ExtPackageJsonExtra, + Icon, + QuickLinkCmd +} from "@kksh/api/models" +import * as v from "valibot" export async function upsertExtension(extPkgJson: ExtPackageJson, extFullPath: string) { const extInDb = await db.getUniqueExtensionByIdentifier(extPkgJson.kunkun.identifier) @@ -12,3 +20,38 @@ export async function upsertExtension(extPkgJson: ExtPackageJson, extFullPath: s }) } } + +export async function createQuickLinkCommand(name: string, link: string, icon: Icon) { + const extension = await db.getExtQuickLinks() + return db.createCommand({ + extId: extension.extId, + name, + cmdType: CmdTypeEnum.QuickLink, + data: JSON.stringify({ + link, + icon + }), + enabled: true + }) +} + +export async function getAllQuickLinkCommands(): Promise { + const extension = await db.getExtQuickLinks() + const cmds = await db.getCommandsByExtId(extension.extId) + return cmds + .map((cmd) => { + try { + cmd.data = JSON.parse(cmd.data) + const parsedData = v.safeParse(QuickLinkCmd, cmd) + if (!parsedData.success) { + console.warn("Fail to parse quick link command", cmd) + console.error(v.flatten(parsedData.issues)) + return null + } + return parsedData.output + } catch (error) { + return null + } + }) + .filter((cmd) => cmd !== null) +} diff --git a/packages/extension/src/install.ts b/packages/extension/src/install.ts index 0704f26..4a65756 100644 --- a/packages/extension/src/install.ts +++ b/packages/extension/src/install.ts @@ -88,6 +88,11 @@ export async function installTarballUrl(tarballUrl: string, extsDir: string): Pr } } +/** + * Install dev extension from a local directory + * @param extPath Path to the extension directory + * @returns + */ export async function installDevExtensionDir(extPath: string): Promise { const manifestPath = await path.join(extPath, "package.json") if (!(await fs.exists(manifestPath))) { diff --git a/packages/extensions/demo-worker-template-ext/.gitignore b/packages/extensions/demo-worker-template-ext/.gitignore new file mode 100644 index 0000000..dc364a9 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/.gitignore @@ -0,0 +1,176 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +extensions_support/ diff --git a/packages/extensions/demo-worker-template-ext/CHANGELOG.md b/packages/extensions/demo-worker-template-ext/CHANGELOG.md new file mode 100644 index 0000000..f8fbdad --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/CHANGELOG.md @@ -0,0 +1,8 @@ +# demo-template-extension + +## 0.0.3 + +### Patch Changes + +- Updated dependencies + - @kksh/api@0.0.9 diff --git a/packages/extensions/demo-worker-template-ext/README.md b/packages/extensions/demo-worker-template-ext/README.md new file mode 100644 index 0000000..eb93d75 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/README.md @@ -0,0 +1,15 @@ +# tempalte-ext-worker + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.1.20. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/extensions/demo-worker-template-ext/buffer.ts b/packages/extensions/demo-worker-template-ext/buffer.ts new file mode 100644 index 0000000..4f1715a --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/buffer.ts @@ -0,0 +1,3 @@ +import Buffer from "node:buffer" + +console.log(Buffer) diff --git a/packages/extensions/demo-worker-template-ext/build.ts b/packages/extensions/demo-worker-template-ext/build.ts new file mode 100644 index 0000000..d85f263 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/build.ts @@ -0,0 +1,31 @@ +import { watch } from "fs" +import { join } from "path" +import { refreshTemplateWorkerExtension } from "@kksh/api/dev" +import { $ } from "bun" + +async function build() { + try { + // await $`bun build --minify --target=browser --outdir=./dist ./src/index.ts` + const output = await Bun.build({ + entrypoints: ["./src/index.ts"], + outdir: "./dist", + minify: true, + target: "browser" + }) + console.log(output) + await refreshTemplateWorkerExtension() + } catch (error) { + console.error(error) + } +} + +const srcDir = join(import.meta.dir, "src") + +await build() + +if (Bun.argv.includes("dev")) { + console.log(`Watching ${srcDir} for changes...`) + watch(srcDir, { recursive: true }, async (event, filename) => { + await build() + }) +} diff --git a/packages/extensions/demo-worker-template-ext/deno-src/deno.json b/packages/extensions/demo-worker-template-ext/deno-src/deno.json new file mode 100644 index 0000000..5861f34 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/deno-src/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@kunkun/api": "jsr:@kunkun/api@^0.0.14" + } +} diff --git a/packages/extensions/demo-worker-template-ext/deno-src/deno.lock b/packages/extensions/demo-worker-template-ext/deno-src/deno.lock new file mode 100644 index 0000000..be78ed3 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/deno-src/deno.lock @@ -0,0 +1,23 @@ +{ + "version": "4", + "specifiers": { + "jsr:@hk/comlink-stdio@~0.1.5": "0.1.5", + "jsr:@kunkun/api@^0.0.14": "0.0.14" + }, + "jsr": { + "@hk/comlink-stdio@0.1.5": { + "integrity": "1fd67d5d53ab4571e745584d66b480b5be402f6ca6b2c9e591230fa1d23f85ee" + }, + "@kunkun/api@0.0.14": { + "integrity": "a21a255748164992ca93fc292451677261dffca336922a6bed7eb8703c6e880b", + "dependencies": [ + "jsr:@hk/comlink-stdio" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@kunkun/api@^0.0.14" + ] + } +} diff --git a/packages/extensions/demo-worker-template-ext/deno-src/rpc.ts b/packages/extensions/demo-worker-template-ext/deno-src/rpc.ts new file mode 100644 index 0000000..0e09884 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/deno-src/rpc.ts @@ -0,0 +1,13 @@ +import { expose } from "@kunkun/api/runtime/deno" + +export interface API { + add(a: number, b: number): Promise + subtract(a: number, b: number): Promise +} + +// Define your API methods +export const apiMethods: API = { + add: async (a: number, b: number) => a + b, + subtract: async (a: number, b: number) => a - b +} +expose(apiMethods) diff --git a/packages/extensions/demo-worker-template-ext/package.json b/packages/extensions/demo-worker-template-ext/package.json new file mode 100644 index 0000000..68de2ad --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/package.json @@ -0,0 +1,114 @@ +{ + "$schema": "../../schema/manifest-json-schema.json", + "name": "demo-template-extension", + "version": "0.0.3", + "type": "module", + "kunkun": { + "name": "Demo Template Extension", + "shortDescription": "Demo Template Extension", + "longDescription": "Demo Template Extension", + "identifier": "demo-worker-template-ext", + "permissions": [ + "fetch:all", + "shell:kill", + "security:mac:all", + { + "permission": "shell:deno:execute", + "allow": [ + { + "path": "$EXTENSION/deno-src/deno-script.ts", + "env": [ + "npm_package_config_libvips", + "CWD" + ], + "ffi": "*", + "read": [ + "$DESKTOP" + ] + }, + { + "path": "$EXTENSION/deno-src/rpc.ts", + "ffi": "*" + } + ] + }, + { + "permission": "open:file", + "allow": [ + { + "path": "$EXTENSION/src/deno-script.ts" + } + ] + }, + "shell:stdin-write", + { + "permission": "shell:execute", + "allow": [ + { + "cmd": { + "program": "ls", + "args": [ + "-l" + ] + } + }, + { + "cmd": { + "program": "bash", + "args": [ + "-c", + ".+" + ] + } + }, + { + "cmd": { + "program": "deno", + "args": [ + "-A", + ".+", + ".+" + ] + } + } + ] + } + ], + "demoImages": [], + "icon": { + "type": "iconify", + "value": "carbon:demo" + }, + "customUiCmds": [], + "templateUiCmds": [ + { + "name": "Demo Worker Template", + "main": "dist/index.js", + "cmds": [] + } + ] + }, + "scripts": { + "dev": "bun build.ts dev", + "build": "bun build.ts" + }, + "dependencies": { + "@hk/comlink-stdio": "npm:@jsr/hk__comlink-stdio@^0.1.6", + "@kksh/api": "workspace:*", + "@kunkun/api": "npm:@jsr/kunkun__api@^0.0.13" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6", + "@types/bun": "latest", + "rollup-plugin-visualizer": "^5.12.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "files": [ + "./dist", + ".gitignore" + ] +} diff --git a/packages/extensions/demo-worker-template-ext/rollup.config.js b/packages/extensions/demo-worker-template-ext/rollup.config.js new file mode 100644 index 0000000..640b20b --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/rollup.config.js @@ -0,0 +1,20 @@ +import { visualizer } from "rollup-plugin-visualizer"; +import resolve from '@rollup/plugin-node-resolve'; +import typescript from "@rollup/plugin-typescript"; +import commonjs from '@rollup/plugin-commonjs'; + + +export default { + input: "src/index.ts", + output: { + dir: "dist", + format: "esm", + }, + plugins: [ + typescript(), + resolve(), + commonjs(), + // put it the last one + visualizer(), + ], +}; diff --git a/packages/extensions/demo-worker-template-ext/src/index.ts b/packages/extensions/demo-worker-template-ext/src/index.ts new file mode 100644 index 0000000..60105e1 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/src/index.ts @@ -0,0 +1,181 @@ +import type { RPCChannel } from "@hk/comlink-stdio/browser" +import { + Action, + app, + Child, + expose, + Form, + fs, + Icon, + IconEnum, + List, + Markdown, + open, + path, + security, + shell, + toast, + ui, + WorkerExtension +} from "@kksh/api/ui/worker" +import { IconType } from "@kunkun/api/models" + +const nums = Array.from({ length: 20 }, (_, i) => i + 1) +const categories = ["Suggestion", "Advice", "Idea"] +const itemsTitle = nums.map((n) => categories.map((c) => `${c} ${n}`)).flat() +const allItems: List.Item[] = itemsTitle.map( + (title) => + new List.Item({ + title, + value: title, + defaultAction: "Item Default Action" + }) +) + +class ExtensionTemplate extends WorkerExtension { + async onBeforeGoBack() { + console.log("onBeforeGoBack") + // console.log(`Try killing pid: ${this.apiProcess?.pid}`) + // await this.apiProcess?.kill() + // console.log("apiProcess killed") + } + async onFormSubmit(value: Record): Promise { + console.log("Form submitted", value) + } + + async onEnterPressedOnSearchBar(): Promise { + console.log("Enter pressed on search bar") + } + + async load() { + // console.log("Check screen capture permission:", await security.mac.checkScreenCapturePermission()) + // await security.mac.revealSecurityPane("AllFiles") + // console.log(await security.mac.verifyFingerprint()) + ui.setSearchBarPlaceholder("Search for items") + ui.showLoadingBar(true) + setTimeout(() => { + ui.showLoadingBar(false) + }, 2000) + const { rpcChannel, process } = await shell.createDenoRpcChannel< + {}, + { + add(a: number, b: number): Promise + subtract(a: number, b: number): Promise + } + >("$EXTENSION/deno-src/rpc.ts", [], {}, {}) + const api = rpcChannel.getApi() + await api.add(1, 2).then(console.log) + await api.subtract(1, 2).then(console.log) + await process.kill() + const extPath = await path.extensionDir() + // console.log("Extension path:", extPath) + const tagList = new List.ItemDetailMetadataTagList({ + title: "Tag List Title", + tags: [ + new List.ItemDetailMetadataTagListItem({ + text: "red", + color: "#ff0000" + }), + new List.ItemDetailMetadataTagListItem({ + text: "yellow", + color: "#ffff00" + }) + ] + }) + const list = new List.List({ + items: allItems, + defaultAction: "Top Default Action", + detail: new List.ItemDetail({ + children: [ + new List.ItemDetailMetadata([ + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }), + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text", + icon: new Icon({ + type: IconType.enum.Iconify, + value: "mingcute:appstore-fill" + }) + }), + new List.ItemDetailMetadataSeparator(), + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }), + new List.ItemDetailMetadataLink({ + title: "Link Title", + text: "Link Text", + url: "https://github.com/huakunshen" + }), + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }), + tagList + ]), + new Markdown(` +# Hello World + + + + `) + ], + width: 50 + }), + actions: new Action.ActionPanel({ + items: [ + new Action.Action({ + title: "Action 1", + value: "action 1", + icon: new Icon({ type: IconType.enum.Iconify, value: "material-symbols:add-reaction" }) + }), + new Action.Action({ title: "Action 2", value: "action 2" }), + new Action.Action({ title: "Action 3", value: "action 3" }), + new Action.Action({ title: "Action 4", value: "action 4" }) + ] + }) + }) + + return ui.render(list) + } + + async onSearchTermChange(term: string): Promise { + return ui.render( + new List.List({ + // items: allItems.filter((item) => item.title.toLowerCase().includes(term.toLowerCase())), + inherits: ["items", "sections"], + defaultAction: "Top Default Action", + detail: new List.ItemDetail({ + children: [ + new List.ItemDetailMetadata([ + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }) + ]) + // new Markdown(` + // ## Search results for "${term}" + // + // + // + // `) + ], + width: term.length > 3 ? 70 : 30 + }) + }) + ) + } + + async onListItemSelected(value: string): Promise { + console.log("Item selected:", value) + } + + async onActionSelected(value: string): Promise { + console.log("Action selected:", value) + } +} + +expose(new ExtensionTemplate()) diff --git a/packages/extensions/demo-worker-template-ext/tsconfig.json b/packages/extensions/demo-worker-template-ext/tsconfig.json new file mode 100644 index 0000000..49ca356 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": [ + "ESNext", + "DOM" + ], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} \ No newline at end of file diff --git a/packages/extensions/form-view/.gitignore b/packages/extensions/form-view/.gitignore new file mode 100644 index 0000000..8c67a06 --- /dev/null +++ b/packages/extensions/form-view/.gitignore @@ -0,0 +1,177 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +extensions_support/ + diff --git a/packages/extensions/form-view/README.md b/packages/extensions/form-view/README.md new file mode 100644 index 0000000..e1c7cc7 --- /dev/null +++ b/packages/extensions/form-view/README.md @@ -0,0 +1,125 @@ +# Kunkun Template UI Extension + +This is a template for a template UI extension. (UI follows pre-defined template) + +[./src/index.ts](./src/index.ts) is the default entrypoint for the extension. You can import any other files in this file, but the build process will bundle them into a single file. + +## Pros and Cons + +This type of extension is suitable for simple use cases, such as a list or form. All components are pre-defined, so there is not much room for customization. If you want more flexibility on the UI, consider using [Custom UI Extension](https://docs.kunkun.sh/extensions/custom-ui-ext/), which requires some frontend knowledge but gives you full control over the UI. + +Read documentation at https://docs.kunkun.sh/extensions/worker-template/ + +Make sure you understand what this type of extension is capable of. + +### Pros + +- Simple to develop, no need for any frontend knowledge. +- Small bundle size (~40KB) + - [Custom UI Extension](https://docs.kunkun.sh/extensions/custom-ui-ext/) are usually larger than 300KB. + +### Cons + +- Limited UI customization. Not suitable for complex use cases. + +Consider [Custom UI Extension](https://docs.kunkun.sh/extensions/custom-ui-ext/) if you need more complex UI. + +## Development + +```bash +pnpm install +``` + +Start extension in development mode. Every save will trigger a hot reload in Kunkun. + +```bash +pnpm dev +``` + +- During development, right click in Kunkun to open the developer tools. + - Error messages will be shown in the console. + - If you got any permission error while calling Kunknu's APIs, make sure you've declared the permission in `package.json`. Then go back to home page and enter the extension again to re-apply the permission. +- To develop and preview the extension in Kunkun, you need to run the `Add Dev Extension` command in Kunkun, and register this extension's path. + +Build the extension. Your extension source code can contain many files, but the build process will bundle them into a single file. + +```bash +pnpm build +# Due to Bun's bug, if you are on windows, and install dependencies with pnpm, you may get error during build. +# Try install dependencies with bun or npm instead. +``` + +## i18n + +[./src/i18n](./src/i18n/) contains optional internationalization support starter code. + +If you want to support i18n, you can use the `t` function to translate the strings in the extension. + +User's language setting is available via `app.language()`. + +```ts +import { app } from "@kksh/api/ui/worker" +import { setupI18n, t } from "./src/i18n" + +setupI18n("zh") +console.log(t("welcome")) + +setupI18n(await app.language()) +console.log(t("welcome")) +``` + +## Add More Commands + +If you want to add more template worker extension commands, simply modify the `entrypoints` array in [./build.ts](./build.ts). + +Then in `package.json`, register the new command. + +## Verify Build and Publish + +```bash +pnpm build # make sure the build npm script works +npx kksh@latest verify # Verify some basic settings +npx kksh@latest verify --publish # Verify some basic settings before publishing +``` + +It is recommended to build the extension with the same environment our CI uses. + +The docker image used by our CI is `huakunshen/kunkun-ext-builder:latest`. + +You can use the following command to build the extension with the same environment our CI uses. +This requires you to have docker installed, and the shell you are using has access to it via `docker` command. + +```bash +npx kksh@latest build # Build the extension with +``` + +`pnpm` is used to install dependencies and build the extension. + +The docker image environment also has `node`, `pnpm`, `npm`, `bun`, `deno` installed. +If your build failed, try debug with `huakunshen/kunkun-ext-builder:latest` image in interative mode and bind your extension volume to `/workspace`. + +After build successfully, you should find a tarball file ends with `.tgz` in the root of your extension. +The tarball is packaged with `npm pack` command. You can uncompress it to see if it contains all the necessary files. + +This tarball is the final product that will be published and installed in Kunkun. You can further verify your extension by installing this tarball directly in Kunkun. + +After verifying the tarball, it's ready to be published. + +Fork [KunkunExtensions](https://github.com/kunkunsh/KunkunExtensions) repo, add your extension to the `extensions` directory, and create a PR. + +Once CI passed and PR merged, you can use your extension in Kunkun. + +## Potential Error + +Our CI uses `pnpm` to install dependencies. If you are on Windows, you may get error during build. + +See issue https://github.com/kunkunsh/kunkun/issues/78 + +`bun` had problem building the extension when `pnpm` is used to install dependencies. + +### Options + +1. Install an older version of `bun` (1.1.27 should work) +2. Install dependencies with `bun` or `npm` instead of `pnpm` + +Our CI always builds the extension with on Linux and shouldn't have this problem. diff --git a/packages/extensions/form-view/build.ts b/packages/extensions/form-view/build.ts new file mode 100644 index 0000000..a99deb5 --- /dev/null +++ b/packages/extensions/form-view/build.ts @@ -0,0 +1,30 @@ +import { watch } from "fs" +import { join } from "path" +import { refreshTemplateWorkerExtension } from "@kksh/api/dev" +import { $ } from "bun" + +const entrypoints = ["./src/index.ts"] + +async function build() { + try { + for (const entrypoint of entrypoints) { + await $`bun build --minify --target=browser --outdir=./dist ${entrypoint}` + } + if (Bun.argv.includes("dev")) { + await refreshTemplateWorkerExtension() + } + } catch (error) { + console.error(error) + } +} + +const srcDir = join(import.meta.dir, "src") + +await build() + +if (Bun.argv.includes("dev")) { + console.log(`Watching ${srcDir} for changes...`) + watch(srcDir, { recursive: true }, async (event, filename) => { + await build() + }) +} diff --git a/packages/extensions/form-view/package.json b/packages/extensions/form-view/package.json new file mode 100644 index 0000000..d31bc05 --- /dev/null +++ b/packages/extensions/form-view/package.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://schema.kunkun.sh", + "name": "form-view", + "version": "0.0.2", + "type": "module", + "kunkun": { + "name": "Form View", + "shortDescription": "A Worker Extension Template", + "longDescription": "A Worker Extension Template", + "identifier": "form-view", + "permissions": [ + "fetch:all", + "clipboard:read-all" + ], + "demoImages": [], + "icon": { + "type": "iconify", + "value": "fluent:form-multiple-28-filled" + }, + "customUiCmds": [], + "templateUiCmds": [ + { + "name": "Dev Form View", + "main": "dist/index.js", + "cmds": [] + } + ] + }, + "scripts": { + "dev": "bun build.ts dev", + "build": "bun build.ts" + }, + "dependencies": { + "@kksh/api": "workspace:*", + "i18next": "^23.15.1" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "files": [ + "./dist", + ".gitignore" + ] +} diff --git a/packages/extensions/form-view/src/i18n/en.ts b/packages/extensions/form-view/src/i18n/en.ts new file mode 100644 index 0000000..331e9f1 --- /dev/null +++ b/packages/extensions/form-view/src/i18n/en.ts @@ -0,0 +1,5 @@ +const en = { + welcome: "Welcome to Kunkun" +} +export default en +export type Translation = typeof en diff --git a/packages/extensions/form-view/src/i18n/index.ts b/packages/extensions/form-view/src/i18n/index.ts new file mode 100644 index 0000000..2f0e1c8 --- /dev/null +++ b/packages/extensions/form-view/src/i18n/index.ts @@ -0,0 +1,20 @@ +import i18next from "i18next" +import en, { type Translation } from "./en" +import zh from "./zh" + +export function setupI18n(language: "en" | "zh" = "en") { + i18next.init({ + resources: { + en: { + translation: en + }, + zh: { + translation: zh + } + }, + lng: language, // default language + fallbackLng: "en" + }) +} + +export const t = (key: keyof Translation, options?: any) => i18next.t(key, options) diff --git a/packages/extensions/form-view/src/i18n/zh.ts b/packages/extensions/form-view/src/i18n/zh.ts new file mode 100644 index 0000000..dc3ab95 --- /dev/null +++ b/packages/extensions/form-view/src/i18n/zh.ts @@ -0,0 +1,5 @@ +import type { Translation } from "./en" + +export default { + welcome: "欢迎来到Kunkun" +} satisfies Translation diff --git a/packages/extensions/form-view/src/index.ts b/packages/extensions/form-view/src/index.ts new file mode 100644 index 0000000..f6b4d2c --- /dev/null +++ b/packages/extensions/form-view/src/index.ts @@ -0,0 +1,95 @@ +import { + Action, + app, + expose, + Form, + fs, + Icon, + IconEnum, + List, + Markdown, + path, + shell, + toast, + ui, + WorkerExtension +} from "@kksh/api/ui/worker" + +class ExtensionTemplate extends WorkerExtension { + async onFormSubmit(value: Record): Promise { + console.log("Form submitted", value) + toast.success(`Form submitted: ${JSON.stringify(value)}`) + } + async load() { + const markdown = new Markdown(`# Hello World +`) + // markdown.toModel + return ui.render(markdown) + const form = new Form.Form({ + title: "Form 1", + key: "form1", + submitBtnText: "Download", + fields: [ + new Form.DateField({ + key: "birthday", + label: "Date of Birth", + hideLabel: false, + description: "Enter your date of birth" + }), + new Form.NumberField({ + key: "age", + label: "Age", + default: 18, + placeholder: "Enter your age", + optional: true, + description: "Enter your age" + }), + new Form.InputField({ + key: "name", + label: "Name", + default: "Huakun" + }), + new Form.InputField({ + key: "name2", + label: "Name 2" + }), + new Form.BooleanField({ + key: "isActive", + label: "Is Active", + description: "Is the user active?" + }), + new Form.SelectField({ + key: "gender", + label: "Gender", + options: ["Male", "Female", "Other"], + description: "Select your gender" + }) + ] + }) + console.log(form) + console.log(form.toModel()) + return ui.render(form) + } + + async onActionSelected(actionValue: string): Promise { + switch (actionValue) { + case "open": + break + + default: + break + } + } + + onSearchTermChange(term: string): Promise { + console.log("Search term changed to:", term) + return Promise.resolve() + } + + onListItemSelected(value: string): Promise { + console.log("Item selected:", value) + return Promise.resolve() + } +} + +expose(new ExtensionTemplate()) diff --git a/packages/extensions/form-view/tsconfig.json b/packages/extensions/form-view/tsconfig.json new file mode 100644 index 0000000..49ca356 --- /dev/null +++ b/packages/extensions/form-view/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": [ + "ESNext", + "DOM" + ], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} \ No newline at end of file diff --git a/packages/schema/package.json b/packages/schema/package.json index e3070c8..0b33136 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -15,8 +15,7 @@ }, "devDependencies": { "@gcornut/valibot-json-schema": "^0.42.0", - "@types/bun": "latest", - "supabase": ">=1.8.1" + "@types/bun": "latest" }, "peerDependencies": { "@kksh/supabase": "workspace:*", @@ -25,7 +24,6 @@ "dependencies": { "@aws-sdk/client-s3": "^3.583.0", "@kksh/api": "workspace:*", - "@supabase/supabase-js": "^2.43.4", "valibot": "^0.40.0" } } diff --git a/packages/schema/scripts/upload-schema-to-supabase.ts b/packages/schema/scripts/upload-schema-to-supabase.ts index f4bf48a..eddd837 100644 --- a/packages/schema/scripts/upload-schema-to-supabase.ts +++ b/packages/schema/scripts/upload-schema-to-supabase.ts @@ -1,10 +1,9 @@ import { ExtPackageJson } from "@kksh/api/models" -import { type Database } from "@kksh/supabase" -import { createClient } from "@supabase/supabase-js" +import { createSB } from "@kksh/supabase" import { parse, string } from "valibot" import { getJsonSchema } from "../src" -const supabase = createClient( +const supabase = createSB( parse(string(), process.env.SUPABASE_URL), parse(string(), process.env.SUPABASE_SERVICE_ROLE_KEY) ) diff --git a/packages/supabase/package.json b/packages/supabase/package.json index 4248fdd..d4cf8bf 100644 --- a/packages/supabase/package.json +++ b/packages/supabase/package.json @@ -8,7 +8,8 @@ ".": "./src/index.ts" }, "dependencies": { - "@kksh/api": "workspace:*" + "@kksh/api": "workspace:*", + "@supabase/supabase-js": "^2.46.1" }, "devDependencies": { "@types/bun": "latest" diff --git a/packages/supabase/src/index.ts b/packages/supabase/src/index.ts index 9285d65..09aae5e 100644 --- a/packages/supabase/src/index.ts +++ b/packages/supabase/src/index.ts @@ -2,7 +2,11 @@ import type { Database } from "@kksh/api/supabase/types" import { createClient } from "@supabase/supabase-js" export function createSB(supabaseUrl: string, supabaseAnonKey: string) { - return createClient(supabaseUrl, supabaseAnonKey) + return createClient(supabaseUrl, supabaseAnonKey, { + auth: { + flowType: "pkce" + } + }) } export { SupabaseAPI } from "./api" diff --git a/packages/types/src/appConfig.ts b/packages/types/src/appConfig.ts index 31a2dec..289adc0 100644 --- a/packages/types/src/appConfig.ts +++ b/packages/types/src/appConfig.ts @@ -23,6 +23,6 @@ export type PersistedAppConfig = v.InferOutput export type AppConfig = PersistedAppConfig & { isInitialized: boolean - extensionPath?: string + extensionsInstallDir?: string platform: Platform } diff --git a/packages/types/src/appState.ts b/packages/types/src/appState.ts index dd9c9f4..4a0539c 100644 --- a/packages/types/src/appState.ts +++ b/packages/types/src/appState.ts @@ -1,4 +1,9 @@ +import { Action as ActionSchema } from "@kksh/api/models" + export interface AppState { searchTerm: string highlightedCmd: string + loadingBar: boolean + defaultAction: string + actionPanel?: ActionSchema.ActionPanel } diff --git a/packages/ui/components.json b/packages/ui/components.json index f10b563..33c48e0 100644 --- a/packages/ui/components.json +++ b/packages/ui/components.json @@ -8,10 +8,10 @@ }, "aliases": { "components": "@kksh/ui/src/components", - "utils": "@kksh/ui/src/utils", + "utils": "@kksh/ui/utils", "ui": "@kksh/ui/src/components/ui", "hooks": "@kksh/ui/src/hooks" }, "typescript": true, "registry": "https://next.shadcn-svelte.com/registry" -} +} \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index 55cca6d..2a6149b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -34,24 +34,31 @@ "lint": "eslint ." }, "devDependencies": { + "@iconify/svelte": "^4.0.2", "@kksh/api": "workspace:*", + "@kksh/svelte5": "^0.1.2-beta.8", "@types/bun": "latest", - "bits-ui": "1.0.0-next.36", + "bits-ui": "1.0.0-next.45", "clsx": "^2.1.1", + "formsnap": "2.0.0-next.1", "lucide-svelte": "^0.454.0", "mode-watcher": "^0.4.1", "paneforge": "1.0.0-next.1", "shiki": "^1.22.2", "svelte-radix": "^2.0.1", "svelte-sonner": "^0.3.28", + "sveltekit-superforms": "^2.20.0", "tailwind-merge": "^2.5.4", "tailwind-variants": "^0.2.1", "tailwindcss": "^3.4.14", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "dependencies": { "@formkit/auto-animate": "^0.8.2", + "@internationalized/date": "^3.5.6", "@std/semver": "npm:@jsr/std__semver@^1.0.3", - "gsap": "^3.12.5" + "gsap": "^3.12.5", + "svelte-markdown": "^0.4.1" } } diff --git a/packages/ui/src/components/common/IconMultiplexer.svelte b/packages/ui/src/components/common/IconMultiplexer.svelte index e76760e..74996ee 100644 --- a/packages/ui/src/components/common/IconMultiplexer.svelte +++ b/packages/ui/src/components/common/IconMultiplexer.svelte @@ -12,17 +12,37 @@ {#if icon.type === IconEnum.RemoteUrl} - + {:else if icon.type === IconEnum.Iconify} - + {:else if icon.type === IconEnum.Base64PNG} - + {:else if icon.type === IconEnum.Text} - {:else if icon.type === IconEnum.Svg} - {@html icon.value} + {@html icon.value} {:else} - + {/if} diff --git a/packages/ui/src/components/common/IconSelector.svelte b/packages/ui/src/components/common/IconSelector.svelte new file mode 100644 index 0000000..70576ab --- /dev/null +++ b/packages/ui/src/components/common/IconSelector.svelte @@ -0,0 +1,56 @@ + + +
    + + + {triggerContent} + + + + Icon Type + {#each iconOptionsArray as [label, value]} + {label} + {/each} + + + +