mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-04-11 17:29:44 +00:00
Feature: Settings (#23)
* feat: add pin screenshot builtin command * feat: pin screenshot command nows zoom and scroll * chore: upgrade @kksh/svelte5 * feat: add mdns built-in command * feat: add sidebar for settings page * fix: builtin command command listing problem with key id in "each" loop * feat: add settings * style: modify settings sidebar style * feat: add sidebar to troubleshooter pages * fix: some styling bug * feat: add menu item highlight for sidebar * feat: improve some keyboard interaction logic * fix: improve troubleshooter flex box * feat: add uuid for mdns * fix mdns host removing caused by dead lock * feat: settings page implemented, hotkey, hide on blur implemented * style: update styles in settings * feat: improve search bar dropdown menu items
This commit is contained in:
parent
0600eca59a
commit
7865d18580
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3215,6 +3215,7 @@ dependencies = [
|
|||||||
"tauri-plugin-upload",
|
"tauri-plugin-upload",
|
||||||
"tokio",
|
"tokio",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"uuid",
|
||||||
"zip 2.2.0",
|
"zip 2.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
17
apps/desktop/components.json
Normal file
17
apps/desktop/components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "neutral"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://next.shadcn-svelte.com/registry"
|
||||||
|
}
|
@ -21,44 +21,43 @@
|
|||||||
"@kksh/ui": "workspace:*",
|
"@kksh/ui": "workspace:*",
|
||||||
"@kksh/utils": "workspace:*",
|
"@kksh/utils": "workspace:*",
|
||||||
"@std/semver": "npm:@jsr/std__semver@^1.0.3",
|
"@std/semver": "npm:@jsr/std__semver@^1.0.3",
|
||||||
"@tauri-apps/api": "^2",
|
"@tanstack/table-core": "^8.20.5",
|
||||||
|
"@tauri-apps/api": "^2.1.0",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"bits-ui": "1.0.0-next.36",
|
|
||||||
"gsap": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"lucide-svelte": "^0.454.0",
|
|
||||||
"lz-string": "^1.5.0",
|
"lz-string": "^1.5.0",
|
||||||
"mode-watcher": "^0.4.1",
|
"mode-watcher": "^0.4.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"svelte-radix": "^2.0.1",
|
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"sveltekit-superforms": "^2.20.0",
|
"sveltekit-superforms": "^2.20.0",
|
||||||
"tauri-plugin-clipboard-api": "^2.1.11",
|
"tauri-plugin-clipboard-api": "^2.1.11",
|
||||||
"uuid": "^11.0.2"
|
"uuid": "^11.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kksh/types": "workspace:*",
|
"@kksh/types": "workspace:*",
|
||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
"@sveltejs/kit": "^2.7.4",
|
"@sveltejs/kit": "^2.8.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tauri-apps/cli": "^2.0.4",
|
"@tauri-apps/cli": "^2.1.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@unocss/preset-attributify": "^0.64.0",
|
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bits-ui": "1.0.0-next.49",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"embla-carousel-svelte": "^8.3.1",
|
"embla-carousel-svelte": "^8.3.1",
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
|
"lucide-svelte": "^0.456.0",
|
||||||
|
"svelte-radix": "^2.0.1",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwind-variants": "^0.2.1",
|
"tailwind-variants": "^0.2.1",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"unocss": "^0.64.0",
|
|
||||||
"vaul-svelte": "^0.3.2",
|
"vaul-svelte": "^0.3.2",
|
||||||
"vite": "^5.4.10"
|
"vite": "^5.4.10"
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ tauri-plugin-store = "2.1.0"
|
|||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
tauri-plugin-log = { version = "2.0.1", features = ["colored"] }
|
tauri-plugin-log = { version = "2.0.1", features = ["colored"] }
|
||||||
zip = "2.1.3"
|
zip = "2.1.3"
|
||||||
|
uuid = "1.11.0"
|
||||||
# tauri-plugin-devtools = "2.0.0"
|
# tauri-plugin-devtools = "2.0.0"
|
||||||
|
|
||||||
[target."cfg(target_os = \"macos\")".dependencies]
|
[target."cfg(target_os = \"macos\")".dependencies]
|
||||||
|
@ -3,73 +3,75 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 0 0% 3.9%;
|
--foreground: 0 0% 3.9%;
|
||||||
--muted: 0 0% 96.1%;
|
--muted: 0 0% 96.1%;
|
||||||
--muted-foreground: 0 0% 45.1%;
|
--muted-foreground: 0 0% 45.1%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 0 0% 3.9%;
|
--popover-foreground: 0 0% 3.9%;
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 0 0% 3.9%;
|
--card-foreground: 0 0% 3.9%;
|
||||||
--border: 0 0% 89.8%;
|
--border: 0 0% 89.8%;
|
||||||
--input: 0 0% 89.8%;
|
--input: 0 0% 89.8%;
|
||||||
--primary: 0 0% 9%;
|
--primary: 0 0% 9%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 96.1%;
|
--secondary: 0 0% 96.1%;
|
||||||
--secondary-foreground: 0 0% 9%;
|
--secondary-foreground: 0 0% 9%;
|
||||||
--accent: 0 0% 96.1%;
|
--accent: 0 0% 96.1%;
|
||||||
--accent-foreground: 0 0% 9%;
|
--accent-foreground: 0 0% 9%;
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 72.2% 50.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--ring: 0 0% 3.9%;
|
--ring: 0 0% 3.9%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--sidebar-background: 0 0% 98%;
|
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
||||||
--sidebar-border: 220 13% 91%;
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
--sidebar-background: 0 0% 98%;
|
||||||
--background: 0 0% 3.9%;
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
--foreground: 0 0% 98%;
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
--muted: 0 0% 14.9%;
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
--muted-foreground: 0 0% 63.9%;
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
--popover: 0 0% 3.9%;
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--sidebar-border: 220 13% 91%;
|
||||||
--card: 0 0% 3.9%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
--card-foreground: 0 0% 98%;
|
}
|
||||||
--border: 0 0% 14.9%;
|
|
||||||
--input: 0 0% 14.9%;
|
.dark {
|
||||||
--primary: 0 0% 98%;
|
--background: 0 0% 3.9%;
|
||||||
--primary-foreground: 0 0% 9%;
|
--foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 14.9%;
|
--muted: 0 0% 14.9%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--muted-foreground: 0 0% 63.9%;
|
||||||
--accent: 0 0% 14.9%;
|
--popover: 0 0% 3.9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--card: 0 0% 3.9%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--ring: 0 0% 83.1%;
|
--border: 0 0% 14.9%;
|
||||||
--sidebar-background: 240 5.9% 10%;
|
--input: 0 0% 14.9%;
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--primary: 0 0% 98%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--primary-foreground: 0 0% 9%;
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--secondary: 0 0% 14.9%;
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--accent: 0 0% 14.9%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
}
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--ring: 0 0% 83.1%;
|
||||||
|
|
||||||
|
--sidebar-background: 240 5.9% 10%;
|
||||||
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,14 @@ import type { BuiltinCmd } from "@kksh/ui/types"
|
|||||||
import { getVersion } from "@tauri-apps/api/app"
|
import { getVersion } from "@tauri-apps/api/app"
|
||||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
|
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
import { exit } from "@tauri-apps/plugin-process"
|
import { exit } from "@tauri-apps/plugin-process"
|
||||||
|
import { dev } from "$app/environment"
|
||||||
import { goto } from "$app/navigation"
|
import { goto } from "$app/navigation"
|
||||||
import { toast } from "svelte-sonner"
|
import { toast } from "svelte-sonner"
|
||||||
|
import { derived } from "svelte/store"
|
||||||
import * as clipboard from "tauri-plugin-clipboard-api"
|
import * as clipboard from "tauri-plugin-clipboard-api"
|
||||||
import { v4 as uuidv4 } from "uuid"
|
import { v4 as uuidv4 } from "uuid"
|
||||||
|
|
||||||
export const builtinCmds: BuiltinCmd[] = [
|
export const rawBuiltinCmds: BuiltinCmd[] = [
|
||||||
{
|
{
|
||||||
name: "Store",
|
name: "Store",
|
||||||
iconifyIcon: "streamline:store-2-solid",
|
iconifyIcon: "streamline:store-2-solid",
|
||||||
@ -95,7 +97,8 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
url: "/troubleshooters/extension-window",
|
url: "/troubleshooters/extension-window",
|
||||||
title: "Extension Window Troubleshooter"
|
title: "Extension Window Troubleshooter"
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
keywords: ["extension", "window", "troubleshooter"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Extension Permission Inspector",
|
name: "Extension Permission Inspector",
|
||||||
@ -104,7 +107,8 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
function: async () => {
|
function: async () => {
|
||||||
appState.clearSearchTerm()
|
appState.clearSearchTerm()
|
||||||
goto("/extension/permission-inspector")
|
goto("/extension/permission-inspector")
|
||||||
}
|
},
|
||||||
|
keywords: ["extension"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Extension Loading Troubleshooter",
|
name: "Extension Loading Troubleshooter",
|
||||||
@ -113,7 +117,8 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
function: async () => {
|
function: async () => {
|
||||||
appState.clearSearchTerm()
|
appState.clearSearchTerm()
|
||||||
goto("/troubleshooters/extension-loading")
|
goto("/troubleshooters/extension-loading")
|
||||||
}
|
},
|
||||||
|
keywords: ["extension", "troubleshooter"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Create Quicklink",
|
name: "Create Quicklink",
|
||||||
@ -124,28 +129,15 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
goto("/extension/create-quick-link")
|
goto("/extension/create-quick-link")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: "Settings",
|
name: "Settings",
|
||||||
// iconifyIcon: "solar:settings-linear",
|
iconifyIcon: "solar:settings-linear",
|
||||||
// description: "Open Settings",
|
description: "Open Settings",
|
||||||
// function: async () => {
|
function: async () => {
|
||||||
// const windows = await getAllWebviewWindows()
|
goto("/settings")
|
||||||
// const found = windows.find((w) => w.label === SettingsWindowLabel)
|
appState.clearSearchTerm()
|
||||||
// if (found) {
|
}
|
||||||
// ElNotification.error("Settings Page is already open")
|
},
|
||||||
// } else {
|
|
||||||
// const win = await newSettingsPage()
|
|
||||||
// setTimeout(() => {
|
|
||||||
// // this is a backup, if window is not properly loaded,
|
|
||||||
// // the show() will not be called within setting page, we call it here with a larger delay,
|
|
||||||
// // at least the window will be shown
|
|
||||||
// win.show()
|
|
||||||
// }, 800)
|
|
||||||
// }
|
|
||||||
// const appStateStore = useAppStateStore()
|
|
||||||
// appStateStore.setSearchTermSync("")
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
name: "Check Update",
|
name: "Check Update",
|
||||||
iconifyIcon: "material-symbols:update",
|
iconifyIcon: "material-symbols:update",
|
||||||
@ -226,6 +218,18 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
}, 2_000)
|
}, 2_000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "MDNS Debugger",
|
||||||
|
iconifyIcon: "material-symbols:wifi-find",
|
||||||
|
description: "MDNS Debugger",
|
||||||
|
function: async () => {
|
||||||
|
goto("/troubleshooters/mdns-debugger")
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
developer: true
|
||||||
|
},
|
||||||
|
keywords: ["mdns", "debugger", "troubleshooter"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Toggle Hide On Blur",
|
name: "Toggle Hide On Blur",
|
||||||
iconifyIcon: "ri:toggle-line",
|
iconifyIcon: "ri:toggle-line",
|
||||||
@ -240,5 +244,24 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
})
|
})
|
||||||
appState.clearSearchTerm()
|
appState.clearSearchTerm()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Toggle Developer Mode",
|
||||||
|
iconifyIcon: "hugeicons:developer",
|
||||||
|
description: "Toggle Developer Mode",
|
||||||
|
function: async () => {
|
||||||
|
appConfig.update((config) => {
|
||||||
|
toast.success(`Developer Mode toggled to: ${!config.developerMode}`)
|
||||||
|
return { ...config, developerMode: !config.developerMode }
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
].map((cmd) => ({ ...cmd, id: uuidv4() }))
|
||||||
|
|
||||||
|
export const builtinCmds = derived(appConfig, ($appConfig) => {
|
||||||
|
return rawBuiltinCmds.filter((cmd) => {
|
||||||
|
const passDeveloper = cmd.flags?.developer ? $appConfig.developerMode : true
|
||||||
|
const passDev = cmd.flags?.dev ? dev : true
|
||||||
|
return passDeveloper && passDev
|
||||||
|
})
|
||||||
|
})
|
||||||
|
81
apps/desktop/src/lib/components/common/HotkeyInput.svelte
Normal file
81
apps/desktop/src/lib/components/common/HotkeyInput.svelte
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { keyCodeToKey, keyCombToDisplay } from "@/utils/js"
|
||||||
|
import { isShortcut } from "@/utils/key"
|
||||||
|
import { Button, Input } from "@kksh/svelte5"
|
||||||
|
import { cn } from "@kksh/svelte5/utils"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let {
|
||||||
|
// keys = $bindable([]),
|
||||||
|
class: className,
|
||||||
|
// ref = $bindable(null),
|
||||||
|
// recording = $bindable(false),
|
||||||
|
onSubmit
|
||||||
|
}: {
|
||||||
|
// keys?: string[] | null
|
||||||
|
class?: string
|
||||||
|
// recording?: boolean
|
||||||
|
// ref?: HTMLInputElement | null
|
||||||
|
onSubmit?: (keys: string[]) => void
|
||||||
|
} = $props()
|
||||||
|
let keys = $state<string[]>([])
|
||||||
|
let recording = $state(false)
|
||||||
|
let savedShortcut = $state<Set<string> | null>(null)
|
||||||
|
let keyCombination = $derived(
|
||||||
|
savedShortcut !== null
|
||||||
|
? keyCombToDisplay(Array.from(savedShortcut))
|
||||||
|
: keyCombToDisplay(keys ?? [])
|
||||||
|
)
|
||||||
|
let inputRef = $state<HTMLInputElement | null>(null)
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (recording) {
|
||||||
|
e.preventDefault()
|
||||||
|
const newKeys = [...keys, keyCodeToKey(e.code)]
|
||||||
|
keys = newKeys
|
||||||
|
if (isShortcut(newKeys)) {
|
||||||
|
// console.log("shortcut detected", newKeys)
|
||||||
|
savedShortcut = new Set(newKeys)
|
||||||
|
recording = false // stop recording
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(e: KeyboardEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (recording) {
|
||||||
|
keys = keys.filter((k) => k !== keyCodeToKey(e.code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log(inputRef)
|
||||||
|
inputRef?.focus()
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef?.focus()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-col gap-1"
|
||||||
|
onsubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit?.(Array.from(keys))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<!-- <pre>recording: {recording}</pre> -->
|
||||||
|
<Input
|
||||||
|
value={keyCombination}
|
||||||
|
class={cn("w-full text-center", className)}
|
||||||
|
onkeydown={onKeyDown}
|
||||||
|
autofocus
|
||||||
|
bind:ref={inputRef}
|
||||||
|
onkeyup={onKeyUp}
|
||||||
|
onfocus={() => {
|
||||||
|
recording = true
|
||||||
|
keys = []
|
||||||
|
}}
|
||||||
|
onblur={() => (recording = false)}
|
||||||
|
/>
|
||||||
|
<Button size="sm" type="submit" variant="outline">Submit</Button>
|
||||||
|
</form>
|
@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "@/utils"
|
||||||
|
import { keyCombToDisplay } from "@/utils/js"
|
||||||
|
import { ButtonModule, Input, Label, Popover } from "@kksh/svelte5"
|
||||||
|
import HotkeyInput from "./HotkeyInput.svelte"
|
||||||
|
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
savedHotkey,
|
||||||
|
onSubmit
|
||||||
|
}: { class?: string; savedHotkey: string[]; onSubmit: (keys: string[]) => void } = $props()
|
||||||
|
let recording = $state(false)
|
||||||
|
let keys = $state<string[]>([])
|
||||||
|
function onRecordClicked() {
|
||||||
|
keys = []
|
||||||
|
recording = true
|
||||||
|
}
|
||||||
|
let open = $state(false)
|
||||||
|
let inputRef = $state<HTMLInputElement | null>(null)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover.Root bind:open>
|
||||||
|
<Popover.Trigger
|
||||||
|
onclick={onRecordClicked}
|
||||||
|
class={cn(ButtonModule.buttonVariants({ variant: "outline", size: "sm" }), className)}
|
||||||
|
>
|
||||||
|
<!-- <button>recording: {recording}</button> -->
|
||||||
|
{#if savedHotkey.length === 0}
|
||||||
|
<span>Record Hotkey</span>
|
||||||
|
{:else}
|
||||||
|
<span>{keyCombToDisplay(savedHotkey)}</span>
|
||||||
|
{/if}
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content
|
||||||
|
class="w-60"
|
||||||
|
onOpenAutoFocus={(e: FocusEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
console.log("inputRef", inputRef)
|
||||||
|
// inputRef?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HotkeyInput
|
||||||
|
onSubmit={(keys) => {
|
||||||
|
open = false
|
||||||
|
onSubmit(keys)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goHome } from "@/utils/route"
|
||||||
|
import { Button, SideBar } from "@kksh/svelte5"
|
||||||
|
import { Constants } from "@kksh/ui"
|
||||||
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
|
|
||||||
|
const { useSidebar } = SideBar
|
||||||
|
const sidebar = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="fixed flex h-10 w-full items-center gap-2 pl-1 pt-1" data-tauri-drag-region>
|
||||||
|
<SideBar.Trigger class="z-50" />
|
||||||
|
{#if sidebar.state === "collapsed"}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="z-50 {Constants.CLASSNAMES.BACK_BUTTON}"
|
||||||
|
onclick={goHome}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="h-10"></div>
|
@ -109,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-center gap-3">
|
<div class="flex justify-center gap-3 my-3">
|
||||||
<Button size="sm" onclick={pickExtFolders}>Install from Extension Folders</Button>
|
<Button size="sm" onclick={pickExtFolders}>Install from Extension Folders</Button>
|
||||||
<Button size="sm" onclick={pickExtFiles}>Install from Extension Tarball File</Button>
|
<Button size="sm" onclick={pickExtFiles}>Install from Extension Tarball File</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import HotkeyInput from "@/components/common/HotkeyInput.svelte"
|
||||||
|
import HotkeyInputPopover from "@/components/common/HotkeyInputPopover.svelte"
|
||||||
|
import { appConfig } from "@/stores"
|
||||||
|
import { updateAppHotkey } from "@/utils/hotkey"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let savedHotkey = $state<string[]>([])
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
savedHotkey = $appConfig.triggerHotkey ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateHotkey(keys: string[]) {
|
||||||
|
savedHotkey = keys
|
||||||
|
updateAppHotkey(keys, $appConfig.triggerHotkey).then(() => {
|
||||||
|
appConfig.setTriggerHotkey(keys)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<HotkeyInputPopover {savedHotkey} onSubmit={updateHotkey} />
|
@ -50,10 +50,10 @@
|
|||||||
<Form.Field {form} name="name">
|
<Form.Field {form} name="name">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<flex items-center gap-2>
|
<div class="flex items-center gap-2">
|
||||||
<Input {...props} bind:value={$formData.name} placeholder="NPM Package Name" />
|
<Input {...props} bind:value={$formData.name} placeholder="NPM Package Name" />
|
||||||
<Form.Button class="my-1">Install</Form.Button>
|
<Form.Button class="my-1">Install</Form.Button>
|
||||||
</flex>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
|
@ -51,10 +51,10 @@
|
|||||||
<Form.Field {form} name="url">
|
<Form.Field {form} name="url">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<flex items-center gap-2>
|
<div class="flex items-center gap-2">
|
||||||
<Input {...props} bind:value={$formData.url} placeholder="Tarball URL" />
|
<Input {...props} bind:value={$formData.url} placeholder="Tarball URL" />
|
||||||
<Form.Button class="my-1">Install</Form.Button>
|
<Form.Button class="my-1">Install</Form.Button>
|
||||||
</flex>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
|
@ -5,6 +5,7 @@ import { PersistedAppConfig, type AppConfig } from "@kksh/types"
|
|||||||
import { debug, error } from "@tauri-apps/plugin-log"
|
import { debug, error } from "@tauri-apps/plugin-log"
|
||||||
import * as os from "@tauri-apps/plugin-os"
|
import * as os from "@tauri-apps/plugin-os"
|
||||||
import { load } from "@tauri-apps/plugin-store"
|
import { load } from "@tauri-apps/plugin-store"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import * as v from "valibot"
|
import * as v from "valibot"
|
||||||
|
|
||||||
export const defaultAppConfig: AppConfig = {
|
export const defaultAppConfig: AppConfig = {
|
||||||
@ -24,13 +25,16 @@ export const defaultAppConfig: AppConfig = {
|
|||||||
hideOnBlur: true,
|
hideOnBlur: true,
|
||||||
extensionAutoUpgrade: true,
|
extensionAutoUpgrade: true,
|
||||||
joinBetaProgram: false,
|
joinBetaProgram: false,
|
||||||
onBoarded: false
|
onBoarded: false,
|
||||||
|
developerMode: false
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppConfigAPI {
|
interface AppConfigAPI {
|
||||||
init: () => Promise<void>
|
init: () => Promise<void>
|
||||||
|
get: () => AppConfig
|
||||||
setTheme: (theme: ThemeConfig) => void
|
setTheme: (theme: ThemeConfig) => void
|
||||||
setDevExtensionPath: (devExtensionPath: string | null) => void
|
setDevExtensionPath: (devExtensionPath: string | null) => void
|
||||||
|
setTriggerHotkey: (triggerHotkey: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAppConfig(): WithSyncStore<AppConfig> & AppConfigAPI {
|
function createAppConfig(): WithSyncStore<AppConfig> & AppConfigAPI {
|
||||||
@ -67,11 +71,15 @@ function createAppConfig(): WithSyncStore<AppConfig> & AppConfigAPI {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
|
get: () => get(store),
|
||||||
setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })),
|
setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })),
|
||||||
setDevExtensionPath: (devExtensionPath: string | null) => {
|
setDevExtensionPath: (devExtensionPath: string | null) => {
|
||||||
console.log("setDevExtensionPath", devExtensionPath)
|
console.log("setDevExtensionPath", devExtensionPath)
|
||||||
store.update((config) => ({ ...config, devExtensionPath }))
|
store.update((config) => ({ ...config, devExtensionPath }))
|
||||||
},
|
},
|
||||||
|
setTriggerHotkey: (triggerHotkey: string[]) => {
|
||||||
|
store.update((config) => ({ ...config, triggerHotkey }))
|
||||||
|
},
|
||||||
init
|
init
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
apps/desktop/src/lib/utils.ts
Normal file
6
apps/desktop/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
@ -1,3 +1,11 @@
|
|||||||
export function getActiveElementNodeName(): string | undefined {
|
export function getActiveElementNodeName(): string | undefined {
|
||||||
return document.activeElement?.nodeName
|
return document.activeElement?.nodeName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isInputElement(element: HTMLElement): boolean {
|
||||||
|
return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKeyboardEventFromInputElement(e: KeyboardEvent): boolean {
|
||||||
|
return isInputElement(e.target as HTMLElement)
|
||||||
|
}
|
||||||
|
58
apps/desktop/src/lib/utils/hotkey.ts
Normal file
58
apps/desktop/src/lib/utils/hotkey.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { getAllWindows } from "@tauri-apps/api/window"
|
||||||
|
import { isRegistered, register, unregister } from "@tauri-apps/plugin-global-shortcut"
|
||||||
|
import { debug, info } from "@tauri-apps/plugin-log"
|
||||||
|
import { sendNotificationWithPermission } from "./notification"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tauri global shortcut doesn't accept 'Meta' Key. This function maps browser detected keys to Tauri-accepted keys.
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
export function mapKeyToTauriKey(key: string): string {
|
||||||
|
if (key === "Meta") {
|
||||||
|
return "Command"
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerAppHotkey(hotkeyStr: string) {
|
||||||
|
if (await isRegistered(hotkeyStr)) {
|
||||||
|
debug(`Hotkey (${hotkeyStr}) already registered`)
|
||||||
|
await unregister(hotkeyStr)
|
||||||
|
}
|
||||||
|
info(`Registering hotkey: ${hotkeyStr}`)
|
||||||
|
return register(hotkeyStr, async (e) => {
|
||||||
|
if (e.state === "Released") {
|
||||||
|
const wins = await getAllWindows()
|
||||||
|
const mainWin = wins.find((w) => w.label === "main")
|
||||||
|
if (!mainWin) {
|
||||||
|
return sendNotificationWithPermission(
|
||||||
|
"No main window found",
|
||||||
|
"Please open main window first"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const isVisible = await mainWin.isVisible()
|
||||||
|
const isFocused = await mainWin.isFocused()
|
||||||
|
if (isVisible) {
|
||||||
|
if (isFocused) {
|
||||||
|
mainWin.hide()
|
||||||
|
} else {
|
||||||
|
mainWin.setFocus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mainWin.show()
|
||||||
|
mainWin.setFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAppHotkey(newHotkey: string[], oldHotkey?: string[] | null) {
|
||||||
|
if (oldHotkey) {
|
||||||
|
const hotkeyStr = oldHotkey.map(mapKeyToTauriKey).join("+")
|
||||||
|
if (await isRegistered(hotkeyStr)) {
|
||||||
|
await unregister(hotkeyStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hotkeyStr = newHotkey.map(mapKeyToTauriKey).join("+")
|
||||||
|
return registerAppHotkey(hotkeyStr)
|
||||||
|
}
|
52
apps/desktop/src/lib/utils/js.ts
Normal file
52
apps/desktop/src/lib/utils/js.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export function setsEqual<T>(set1: Set<T>, set2: Set<T>) {
|
||||||
|
if (set1.size !== set2.size) return false
|
||||||
|
for (let item of set1) {
|
||||||
|
if (!set2.has(item)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const keyToDisplayMap: Record<string, string> = {
|
||||||
|
" ": "Space",
|
||||||
|
Enter: "↵",
|
||||||
|
ArrowUp: "↑",
|
||||||
|
ArrowDown: "↓",
|
||||||
|
ArrowLeft: "←",
|
||||||
|
ArrowRight: "→",
|
||||||
|
Escape: "Esc",
|
||||||
|
Meta: "⌘",
|
||||||
|
Control: "Ctrl",
|
||||||
|
Alt: "⌥",
|
||||||
|
Shift: "⇧"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyCodeToKey(keyCode: string): string {
|
||||||
|
if (keyCode.startsWith("Key")) {
|
||||||
|
return keyCode.slice(3)
|
||||||
|
}
|
||||||
|
if (keyCode.endsWith("Left")) {
|
||||||
|
return keyCode.slice(0, -4)
|
||||||
|
}
|
||||||
|
if (keyCode.startsWith("Digit")) {
|
||||||
|
return keyCode.slice(5)
|
||||||
|
}
|
||||||
|
if (keyCode.endsWith("Right")) {
|
||||||
|
return keyCode.slice(0, -5)
|
||||||
|
}
|
||||||
|
return keyCode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyToDisplay(keyCode: string): string {
|
||||||
|
const mappedChar = keyToDisplayMap[keyCodeToKey(keyCode)]
|
||||||
|
if (mappedChar) {
|
||||||
|
return mappedChar
|
||||||
|
} else {
|
||||||
|
return keyCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyCombToDisplay(keyComb: string[]): string {
|
||||||
|
return keyComb.map(keyToDisplay).join("+")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modifierKeySet = new Set(["Meta", "Shift", "Alt", "Control"])
|
@ -1,20 +1,30 @@
|
|||||||
import { appState } from "@/stores"
|
import { appState } from "@/stores"
|
||||||
|
import { toggleDevTools } from "@kksh/api/commands"
|
||||||
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||||
|
import { platform } from "@tauri-apps/plugin-os"
|
||||||
import { goto } from "$app/navigation"
|
import { goto } from "$app/navigation"
|
||||||
import { goBack, goHome } from "./route"
|
import { isKeyboardEventFromInputElement } from "./dom"
|
||||||
|
import { modifierKeySet } from "./js"
|
||||||
|
import { goBack, goHome, goHomeOrCloseDependingOnWindow } from "./route"
|
||||||
|
import { isInMainWindow } from "./window"
|
||||||
|
|
||||||
export function goHomeOnEscape(e: KeyboardEvent) {
|
export function goHomeOnEscape(e: KeyboardEvent) {
|
||||||
|
console.log("goHomeOnEscape", e.key)
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
goHome()
|
goHome()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function goBackOnEscape(e: KeyboardEvent) {
|
export function goBackOnEscape(e: KeyboardEvent) {
|
||||||
|
console.log("goBackOnEscape", e.key)
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) {
|
export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) {
|
||||||
|
console.log("goBackOnEscapeClearSearchTerm", e.key)
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
if (appState.get().searchTerm) {
|
if (appState.get().searchTerm) {
|
||||||
appState.clearSearchTerm()
|
appState.clearSearchTerm()
|
||||||
@ -25,6 +35,7 @@ export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) {
|
export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) {
|
||||||
|
console.log("goHomeOnEscapeClearSearchTerm", e.key)
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
if (appState.get().searchTerm) {
|
if (appState.get().searchTerm) {
|
||||||
appState.clearSearchTerm()
|
appState.clearSearchTerm()
|
||||||
@ -33,3 +44,76 @@ export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function goBackOrCloseOnEscape(e: KeyboardEvent) {
|
||||||
|
console.log("goBackOrCloseOnEscape", e.key)
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (isInMainWindow()) {
|
||||||
|
goBack()
|
||||||
|
} else {
|
||||||
|
getCurrentWindow().close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goHomeOrCloseOnEscapeWithInput(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (isKeyboardEventFromInputElement(e)) {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
if (target.value === "") {
|
||||||
|
goHomeOrCloseDependingOnWindow()
|
||||||
|
} else {
|
||||||
|
target.value = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
goHomeOrCloseDependingOnWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function globalKeyDownHandler(e: KeyboardEvent) {
|
||||||
|
const _platform = platform()
|
||||||
|
if ((_platform === "macos" && e.metaKey) || (_platform === "windows" && e.ctrlKey)) {
|
||||||
|
if (e.key === ",") {
|
||||||
|
e.preventDefault()
|
||||||
|
goto("/settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Toggle Devtools with control + shift + I
|
||||||
|
if (e.ctrlKey && e.shiftKey && e.key === "I") {
|
||||||
|
e.preventDefault()
|
||||||
|
toggleDevTools()
|
||||||
|
}
|
||||||
|
// Reload window with control + shift + R
|
||||||
|
if (e.ctrlKey && e.shiftKey && e.key === "R") {
|
||||||
|
e.preventDefault()
|
||||||
|
const appWin = getCurrentWebviewWindow()
|
||||||
|
await appWin.hide()
|
||||||
|
location.reload()
|
||||||
|
setTimeout(() => {
|
||||||
|
appWin.show()
|
||||||
|
}, 1_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLetter(letter: string): boolean {
|
||||||
|
if (letter.length != 1) return false
|
||||||
|
return letter.match(/[a-zA-Z]/) ? true : false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isShortcut(letters: string[]): boolean {
|
||||||
|
// letters contain at least one modifier key and one non-modifier key
|
||||||
|
|
||||||
|
let hasModifier = false
|
||||||
|
let hasNonModifier = false
|
||||||
|
|
||||||
|
for (let letter of letters) {
|
||||||
|
if (modifierKeySet.has(letter)) {
|
||||||
|
hasModifier = true
|
||||||
|
} else {
|
||||||
|
hasNonModifier = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasModifier && hasNonModifier
|
||||||
|
}
|
||||||
|
20
apps/desktop/src/lib/utils/notification.ts
Normal file
20
apps/desktop/src/lib/utils/notification.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// import { notification } from "@kksh/api/ui"
|
||||||
|
import * as notification from "@tauri-apps/plugin-notification"
|
||||||
|
|
||||||
|
export async function getNotificationPermission() {
|
||||||
|
let permissionGranted = await notification.isPermissionGranted()
|
||||||
|
|
||||||
|
// If not we need to request it
|
||||||
|
if (!permissionGranted) {
|
||||||
|
const permission = await notification.requestPermission()
|
||||||
|
permissionGranted = permission === "granted"
|
||||||
|
}
|
||||||
|
return permissionGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendNotificationWithPermission(title: string, body: string) {
|
||||||
|
const notificationGranted = await getNotificationPermission()
|
||||||
|
if (notificationGranted) {
|
||||||
|
notification.sendNotification({ title, body })
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||||
import { goto } from "$app/navigation"
|
import { goto } from "$app/navigation"
|
||||||
|
import { isInMainWindow } from "./window"
|
||||||
|
|
||||||
export function goBack() {
|
export function goBack() {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
@ -7,3 +9,11 @@ export function goBack() {
|
|||||||
export function goHome() {
|
export function goHome() {
|
||||||
goto("/")
|
goto("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function goHomeOrCloseDependingOnWindow() {
|
||||||
|
if (isInMainWindow()) {
|
||||||
|
goHome()
|
||||||
|
} else {
|
||||||
|
getCurrentWindow().close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
import "../app.css"
|
import "../app.css"
|
||||||
import { appConfig, appState, extensions, quickLinks } from "@/stores"
|
import { appConfig, appState, extensions, quickLinks } from "@/stores"
|
||||||
import { initDeeplink } from "@/utils/deeplink"
|
import { initDeeplink } from "@/utils/deeplink"
|
||||||
|
import { updateAppHotkey } from "@/utils/hotkey"
|
||||||
|
import { globalKeyDownHandler, goBackOrCloseOnEscape } from "@/utils/key"
|
||||||
|
import { listenToWindowBlur } from "@/utils/tauri-events"
|
||||||
import { isInMainWindow } from "@/utils/window"
|
import { isInMainWindow } from "@/utils/window"
|
||||||
import {
|
import {
|
||||||
ModeWatcher,
|
ModeWatcher,
|
||||||
@ -12,15 +15,39 @@
|
|||||||
updateTheme,
|
updateTheme,
|
||||||
type ThemeConfig
|
type ThemeConfig
|
||||||
} from "@kksh/svelte5"
|
} from "@kksh/svelte5"
|
||||||
import { ViewTransition } from "@kksh/ui"
|
import { Constants, ViewTransition } from "@kksh/ui"
|
||||||
import type { UnlistenFn } from "@tauri-apps/api/event"
|
import type { UnlistenFn } from "@tauri-apps/api/event"
|
||||||
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
import { attachConsole } from "@tauri-apps/plugin-log"
|
import { attachConsole } from "@tauri-apps/plugin-log"
|
||||||
|
import { afterNavigate, beforeNavigate } from "$app/navigation"
|
||||||
|
import { gsap } from "gsap"
|
||||||
|
import { Flip } from "gsap/Flip"
|
||||||
import { onDestroy, onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
|
||||||
onMount(() => {
|
/* -------------------------------------------------------------------------- */
|
||||||
setTimeout(() => {
|
/* Gsap Flip Animation */
|
||||||
import("virtual:uno.css")
|
/* -------------------------------------------------------------------------- */
|
||||||
}, 1000)
|
gsap.registerPlugin(Flip)
|
||||||
|
let flipState: Flip.FlipState
|
||||||
|
|
||||||
|
beforeNavigate(() => {
|
||||||
|
flipState = Flip.getState(
|
||||||
|
`.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterNavigate(() => {
|
||||||
|
if (!flipState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Flip.from(flipState, {
|
||||||
|
targets: `.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`,
|
||||||
|
duration: 0.5,
|
||||||
|
absolute: true,
|
||||||
|
scale: true,
|
||||||
|
ease: "ease-out"
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let { children } = $props()
|
let { children } = $props()
|
||||||
@ -29,12 +56,31 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
attachConsole().then((unlistener) => unlisteners.push(unlistener))
|
attachConsole().then((unlistener) => unlisteners.push(unlistener))
|
||||||
initDeeplink().then((unlistener) => unlisteners.push(unlistener))
|
initDeeplink().then((unlistener) => unlisteners.push(unlistener))
|
||||||
|
|
||||||
quickLinks.init()
|
quickLinks.init()
|
||||||
appConfig.init()
|
appConfig.init()
|
||||||
if (isInMainWindow()) {
|
if (isInMainWindow()) {
|
||||||
|
if ($appConfig.triggerHotkey) {
|
||||||
|
updateAppHotkey($appConfig.triggerHotkey)
|
||||||
|
}
|
||||||
|
unlisteners.push(
|
||||||
|
await listenToWindowBlur(() => {
|
||||||
|
const win = getCurrentWebviewWindow()
|
||||||
|
win.isFocused().then((isFocused) => {
|
||||||
|
// this extra is focused check may be needed because blur event got triggered somehow when window show()
|
||||||
|
// for edge case: when settings page is opened and focused, switch to main window, the blur event is triggered for main window
|
||||||
|
if (!isFocused) {
|
||||||
|
if ($appConfig.hideOnBlur) {
|
||||||
|
win.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
extensions.init()
|
extensions.init()
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
|
getCurrentWebviewWindow().show()
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@ -42,6 +88,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={globalKeyDownHandler} />
|
||||||
<ViewTransition />
|
<ViewTransition />
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getExtensionsFolder } from "@/constants"
|
import { getExtensionsFolder, IS_IN_TAURI } from "@/constants"
|
||||||
import type { LayoutLoad } from "./$types"
|
import type { LayoutLoad } from "./$types"
|
||||||
|
|
||||||
// Tauri doesn't have a Node.js server to do proper SSR
|
// Tauri doesn't have a Node.js server to do proper SSR
|
||||||
@ -8,5 +8,5 @@ export const prerender = true
|
|||||||
export const ssr = false
|
export const ssr = false
|
||||||
|
|
||||||
export const load: LayoutLoad = async () => {
|
export const load: LayoutLoad = async () => {
|
||||||
return { extsInstallDir: await getExtensionsFolder() }
|
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "" }
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
import { systemCommands } from "@/cmds/system"
|
import { systemCommands } from "@/cmds/system"
|
||||||
import { appConfig, appState, devStoreExts, installedStoreExts, quickLinks } from "@/stores"
|
import { appConfig, appState, devStoreExts, installedStoreExts, quickLinks } from "@/stores"
|
||||||
import { cmdQueries } from "@/stores/cmdQuery"
|
import { cmdQueries } from "@/stores/cmdQuery"
|
||||||
import { getActiveElementNodeName } from "@/utils/dom"
|
import { getActiveElementNodeName, isKeyboardEventFromInputElement } from "@/utils/dom"
|
||||||
import { openDevTools } from "@kksh/api/commands"
|
import Icon from "@iconify/svelte"
|
||||||
|
import { openDevTools, toggleDevTools } from "@kksh/api/commands"
|
||||||
import type { ExtPackageJsonExtra } from "@kksh/api/models"
|
import type { ExtPackageJsonExtra } from "@kksh/api/models"
|
||||||
import { isExtPathInDev } from "@kksh/extension/utils"
|
import { isExtPathInDev } from "@kksh/extension/utils"
|
||||||
import { Button, Command, DropdownMenu } from "@kksh/svelte5"
|
import { Button, Command, DropdownMenu } from "@kksh/svelte5"
|
||||||
@ -23,22 +24,29 @@
|
|||||||
import { cn, commandScore } from "@kksh/ui/utils"
|
import { cn, commandScore } from "@kksh/ui/utils"
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
import { exit } from "@tauri-apps/plugin-process"
|
import { exit } from "@tauri-apps/plugin-process"
|
||||||
import { EllipsisVerticalIcon } from "lucide-svelte"
|
import { CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte"
|
||||||
import type { Writable } from "svelte/store"
|
|
||||||
|
|
||||||
|
let inputEle: HTMLInputElement | null = null
|
||||||
function onKeyDown(event: KeyboardEvent) {
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
if (getActiveElementNodeName() === "INPUT") {
|
;(event.target as HTMLInputElement).value = ""
|
||||||
;(event.target as HTMLInputElement).value = ""
|
$appState.searchTerm = ""
|
||||||
if ((event.target as HTMLInputElement | undefined)?.id === "main-command-input") {
|
|
||||||
$appState.searchTerm = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={onKeyDown} />
|
<svelte:window
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === "/") {
|
||||||
|
if (isKeyboardEventFromInputElement(e)) {
|
||||||
|
e.preventDefault()
|
||||||
|
} else {
|
||||||
|
e.preventDefault()
|
||||||
|
inputEle?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Command.Root
|
<Command.Root
|
||||||
class={cn("h-screen rounded-lg border shadow-md")}
|
class={cn("h-screen rounded-lg border shadow-md")}
|
||||||
bind:value={$appState.highlightedCmd}
|
bind:value={$appState.highlightedCmd}
|
||||||
@ -53,9 +61,11 @@
|
|||||||
>
|
>
|
||||||
<CustomCommandInput
|
<CustomCommandInput
|
||||||
autofocus
|
autofocus
|
||||||
|
bind:ref={inputEle}
|
||||||
id="main-command-input"
|
id="main-command-input"
|
||||||
placeholder={$cmdQueries.length === 0 ? "Type a command or search..." : undefined}
|
placeholder={$cmdQueries.length === 0 ? "Type a command or search..." : undefined}
|
||||||
bind:value={$appState.searchTerm}
|
bind:value={$appState.searchTerm}
|
||||||
|
onkeydown={onKeyDown}
|
||||||
>
|
>
|
||||||
{#snippet rightSlot()}
|
{#snippet rightSlot()}
|
||||||
<span
|
<span
|
||||||
@ -88,15 +98,42 @@
|
|||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<Button variant="outline" size="icon"><EllipsisVerticalIcon /></Button>
|
<Button variant="outline" size="icon"><EllipsisVerticalIcon /></Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content class="w-80">
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
<DropdownMenu.GroupHeading>Settings</DropdownMenu.GroupHeading>
|
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item onclick={() => exit()}>Quit</DropdownMenu.Item>
|
<DropdownMenu.Item onclick={() => exit()}>
|
||||||
<DropdownMenu.Item onclick={() => openDevTools()}>Open Dev Tools</DropdownMenu.Item>
|
<CircleXIcon class="h-4 w-4 text-red-500" />
|
||||||
<DropdownMenu.Item onclick={() => getCurrentWebviewWindow().hide()}
|
Quit
|
||||||
>Close Window</DropdownMenu.Item
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => getCurrentWebviewWindow().hide()}>
|
||||||
|
<CircleXIcon class="h-4 w-4 text-red-500" />
|
||||||
|
Close Window
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.GroupHeading data-tauri-drag-region>Developer</DropdownMenu.GroupHeading>
|
||||||
|
<DropdownMenu.Item onclick={toggleDevTools}>
|
||||||
|
<Icon icon="mingcute:code-fill" class="mr-2 h-5 w-5 text-green-500" />
|
||||||
|
Toggle Devtools
|
||||||
|
<DropdownMenu.Shortcut>⌃+Shift+I</DropdownMenu.Shortcut>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => location.reload()}>
|
||||||
|
<RefreshCcwIcon class="mr-2 h-4 w-4 text-green-500" />
|
||||||
|
Reload Window
|
||||||
|
<DropdownMenu.Shortcut>⌃+Shift+R</DropdownMenu.Shortcut>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={() => {
|
||||||
|
appConfig.update((config) => ({ ...config, hmr: !config.hmr }))
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Icon
|
||||||
|
icon={$appConfig.hmr ? "fontisto:toggle-on" : "fontisto:toggle-off"}
|
||||||
|
class={cn("mr-1 h-5 w-5", $appConfig.hmr ? "text-green-500" : "")}
|
||||||
|
/>
|
||||||
|
Toggle Dev Extension HMR
|
||||||
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
@ -123,7 +160,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<QuickLinks quickLinks={$quickLinks} />
|
<QuickLinks quickLinks={$quickLinks} />
|
||||||
<BuiltinCmds {builtinCmds} />
|
<BuiltinCmds builtinCmds={$builtinCmds} />
|
||||||
<SystemCmds {systemCommands} />
|
<SystemCmds {systemCommands} />
|
||||||
</Command.List>
|
</Command.List>
|
||||||
<GlobalCommandPaletteFooter />
|
<GlobalCommandPaletteFooter />
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Constants } from "@kksh/ui"
|
// import { Constants } from "@kksh/ui"
|
||||||
import { afterNavigate, beforeNavigate } from "$app/navigation"
|
// import { afterNavigate, beforeNavigate } from "$app/navigation"
|
||||||
import { gsap } from "gsap"
|
// import { gsap } from "gsap"
|
||||||
import { Flip } from "gsap/Flip"
|
// import { Flip } from "gsap/Flip"
|
||||||
|
|
||||||
gsap.registerPlugin(Flip)
|
// gsap.registerPlugin(Flip)
|
||||||
let flipState: Flip.FlipState
|
// let flipState: Flip.FlipState
|
||||||
|
|
||||||
beforeNavigate(() => {
|
// beforeNavigate(() => {
|
||||||
flipState = Flip.getState(
|
// flipState = Flip.getState(
|
||||||
`.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`
|
// `.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`
|
||||||
)
|
// )
|
||||||
})
|
// })
|
||||||
|
|
||||||
afterNavigate(() => {
|
// afterNavigate(() => {
|
||||||
if (!flipState) {
|
// if (!flipState) {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
Flip.from(flipState, {
|
// Flip.from(flipState, {
|
||||||
targets: `.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`,
|
// targets: `.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`,
|
||||||
duration: 0.5,
|
// duration: 0.5,
|
||||||
absolute: true,
|
// absolute: true,
|
||||||
scale: true,
|
// scale: true,
|
||||||
ease: "ease-out"
|
// ease: "ease-out"
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
|
|
||||||
const { children } = $props()
|
const { children } = $props()
|
||||||
</script>
|
</script>
|
||||||
|
@ -56,11 +56,14 @@
|
|||||||
let loaded = $state(false)
|
let loaded = $state(false)
|
||||||
|
|
||||||
async function goBack() {
|
async function goBack() {
|
||||||
|
console.log("goBack")
|
||||||
if (isInMainWindow()) {
|
if (isInMainWindow()) {
|
||||||
|
console.log("goBack in main window")
|
||||||
// if in main window, then winExtMap store must contain this
|
// if in main window, then winExtMap store must contain this
|
||||||
winExtMap.unregisterExtensionFromWindow(appWin.label)
|
// winExtMap.unregisterExtensionFromWindow(appWin.label)
|
||||||
goto("/")
|
goto("/")
|
||||||
} else {
|
} else {
|
||||||
|
console.log("goBack in webview window")
|
||||||
appWin.close()
|
appWin.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,6 +182,7 @@
|
|||||||
searchBarPlaceholder = placeholder
|
searchBarPlaceholder = placeholder
|
||||||
},
|
},
|
||||||
async goBack() {
|
async goBack() {
|
||||||
|
console.log("goBack in ui-worker")
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
apps/desktop/src/routes/settings/+layout.svelte
Normal file
18
apps/desktop/src/routes/settings/+layout.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goHomeOrCloseOnEscapeWithInput } from "@/utils/key"
|
||||||
|
import { SideBar } from "@kksh/svelte5"
|
||||||
|
import SidebarTrigger from "$lib/components/common/sidebar-trigger.svelte"
|
||||||
|
import SettingsSidebar from "./sidebar.svelte"
|
||||||
|
|
||||||
|
let { children } = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={goHomeOrCloseOnEscapeWithInput} />
|
||||||
|
|
||||||
|
<SideBar.Provider style="--sidebar-width: 13rem;">
|
||||||
|
<SettingsSidebar />
|
||||||
|
<main class="grow overflow-x-clip">
|
||||||
|
<SidebarTrigger />
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
</SideBar.Provider>
|
65
apps/desktop/src/routes/settings/+page.svelte
Normal file
65
apps/desktop/src/routes/settings/+page.svelte
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import HotkeyInputPopover from "@/components/common/HotkeyInputPopover.svelte"
|
||||||
|
import HotkeyPick from "@/components/standalone/settings/hotkey-pick.svelte"
|
||||||
|
import { appConfig } from "@/stores"
|
||||||
|
import { Button, Switch } from "@kksh/svelte5"
|
||||||
|
import { Shiki } from "@kksh/ui"
|
||||||
|
import { dev } from "$app/environment"
|
||||||
|
import { onMount, type Snippet } from "svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="container flex flex-col space-y-2">
|
||||||
|
<!-- <h3 class="text-mg mb-2 ml-3 font-bold">App Updates</h3> -->
|
||||||
|
<ul class="rounded-lg border">
|
||||||
|
<li>
|
||||||
|
<span>Launch at Login</span>
|
||||||
|
<Switch bind:checked={$appConfig.launchAtLogin} />
|
||||||
|
</li>
|
||||||
|
<li class="">
|
||||||
|
<span>Hotkey</span>
|
||||||
|
<!-- <HotkeyInput bind:keys={$appConfig.triggerHotkey} /> -->
|
||||||
|
<!-- <HotkeyInputPopover class="" /> -->
|
||||||
|
<HotkeyPick />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Menu Bar Icon</span>
|
||||||
|
<Switch bind:checked={$appConfig.showInTray} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Hide On Blur</span>
|
||||||
|
<Switch bind:checked={$appConfig.hideOnBlur} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Extension Auto Upgrade</span>
|
||||||
|
<Switch bind:checked={$appConfig.extensionAutoUpgrade} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Dev Extension HMR</span>
|
||||||
|
<Switch bind:checked={$appConfig.hmr} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Join Beta Updates</span>
|
||||||
|
<Switch bind:checked={$appConfig.joinBetaProgram} />
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- <li>
|
||||||
|
<span>Language</span>
|
||||||
|
<Switch bind:checked={$appConfig} />
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
{#if dev}
|
||||||
|
<Shiki class="w-full overflow-x-auto" lang="json" code={JSON.stringify($appConfig, null, 2)} />
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
li {
|
||||||
|
@apply flex items-center justify-between border-b px-3 py-3;
|
||||||
|
}
|
||||||
|
ul li:last-child {
|
||||||
|
@apply border-b-0;
|
||||||
|
}
|
||||||
|
li > span {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
</style>
|
59
apps/desktop/src/routes/settings/about/+page.svelte
Normal file
59
apps/desktop/src/routes/settings/about/+page.svelte
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goHome } from "@/utils/route"
|
||||||
|
import { checkUpdateAndInstall } from "@/utils/updater"
|
||||||
|
import Icon from "@iconify/svelte"
|
||||||
|
import { Button, Card, SideBar } from "@kksh/svelte5"
|
||||||
|
import { Layouts, TauriLink } from "@kksh/ui"
|
||||||
|
import { getVersion } from "@tauri-apps/api/app"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let appVersion = ""
|
||||||
|
onMount(async () => {
|
||||||
|
appVersion = await getVersion()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layouts.Center class="absolute left-0 top-0 h-full w-full overflow-hidden border">
|
||||||
|
<div>
|
||||||
|
<div class="flex w-full items-center space-x-5">
|
||||||
|
<img src="/favicon.png" class="w-44" alt="Logo" />
|
||||||
|
<div class="flex flex-col space-y-1">
|
||||||
|
<p class="text-3xl font-bold">KunKun Shell</p>
|
||||||
|
<p class="text-xs">Version: {appVersion}</p>
|
||||||
|
<p>
|
||||||
|
<strong class="font-bold">Author: </strong>
|
||||||
|
<a
|
||||||
|
href="https://github.com/HuakunShen"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="flex items-center gap-2 font-mono text-sm hover:text-blue-600 hover:underline hover:dark:text-blue-500"
|
||||||
|
>
|
||||||
|
@HuakunShen
|
||||||
|
<Icon icon="mdi:github" class="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://github.com/kunkunsh/kunkun"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="flex items-center gap-2 font-mono text-sm hover:text-blue-600 hover:underline hover:dark:text-blue-500"
|
||||||
|
>
|
||||||
|
Source Code
|
||||||
|
<Icon icon="mdi:github" class="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/kunkunsh/kunkunExtensions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="flex items-center gap-2 font-mono text-sm hover:text-blue-600 hover:underline hover:dark:text-blue-500"
|
||||||
|
>
|
||||||
|
Extensions Source Code
|
||||||
|
<Icon icon="mdi:github" class="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<Button onclick={checkUpdateAndInstall} size="sm" variant="secondary">
|
||||||
|
Check for Updates
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layouts.Center>
|
@ -3,10 +3,10 @@
|
|||||||
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
|
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
|
||||||
import { appConfig, extensions } from "@/stores"
|
import { appConfig, extensions } from "@/stores"
|
||||||
import { goBackOnEscape } from "@/utils/key"
|
import { goBackOnEscape } from "@/utils/key"
|
||||||
import { goBack } from "@/utils/route"
|
import { goBack, goHome } from "@/utils/route"
|
||||||
import * as extAPI from "@kksh/extension"
|
import * as extAPI from "@kksh/extension"
|
||||||
import { installFromNpmPackageName } from "@kksh/extension"
|
import { installFromNpmPackageName } from "@kksh/extension"
|
||||||
import { Button, Separator } from "@kksh/svelte5"
|
import { Button, Separator, SideBar } from "@kksh/svelte5"
|
||||||
import { StrikeSeparator } from "@kksh/ui"
|
import { StrikeSeparator } from "@kksh/ui"
|
||||||
import { open as openFileSelector } from "@tauri-apps/plugin-dialog"
|
import { open as openFileSelector } from "@tauri-apps/plugin-dialog"
|
||||||
import * as fs from "@tauri-apps/plugin-fs"
|
import * as fs from "@tauri-apps/plugin-fs"
|
||||||
@ -14,18 +14,16 @@
|
|||||||
import { ArrowLeftIcon } from "lucide-svelte"
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
import { toast } from "svelte-sonner"
|
import { toast } from "svelte-sonner"
|
||||||
import * as v from "valibot"
|
import * as v from "valibot"
|
||||||
|
|
||||||
|
const { useSidebar } = SideBar
|
||||||
|
const sidebar = useSidebar()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={goBackOnEscape} />
|
<main class="container">
|
||||||
<Button variant="outline" size="icon" class="fixed left-2 top-2 z-50" onclick={goBack}>
|
|
||||||
<ArrowLeftIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div class="absolute left-0 top-0 h-10 w-screen" data-tauri-drag-region></div>
|
|
||||||
<main class="container pt-10">
|
|
||||||
<h2 class="text-2xl font-bold">Add Dev Extension</h2>
|
<h2 class="text-2xl font-bold">Add Dev Extension</h2>
|
||||||
<small>
|
<small>
|
||||||
There are 4 options to install an extension in developer mode. Either load it from your local
|
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.
|
tarball file, a tarball remote URL, npm package name or load from a remote URL.
|
||||||
</small>
|
</small>
|
||||||
<AddDevExtForm />
|
<AddDevExtForm />
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
|
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
|
||||||
import { goBackOnEscape } from "@/utils/key"
|
|
||||||
import { goBack } from "@/utils/route"
|
|
||||||
import { Button } from "@kksh/svelte5"
|
|
||||||
import { ArrowLeftIcon } from "lucide-svelte"
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={goBackOnEscape} />
|
<main class="container">
|
||||||
<Button variant="outline" size="icon" class="fixed left-2 top-2 z-50" onclick={goBack}>
|
|
||||||
<ArrowLeftIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div class="absolute left-0 top-0 h-10 w-screen" data-tauri-drag-region></div>
|
|
||||||
<main class="container pt-10">
|
|
||||||
<h2 class="text-2xl font-bold">Set Dev Extension Path</h2>
|
<h2 class="text-2xl font-bold">Set Dev Extension Path</h2>
|
||||||
<p>This is where your extensions will be installed.</p>
|
<p>This is where your extensions will be installed.</p>
|
||||||
<DevExtPathForm />
|
<DevExtPathForm />
|
||||||
|
90
apps/desktop/src/routes/settings/sidebar.svelte
Normal file
90
apps/desktop/src/routes/settings/sidebar.svelte
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goHome } from "@/utils/route"
|
||||||
|
import { Button, SideBar } from "@kksh/svelte5"
|
||||||
|
import { Constants } from "@kksh/ui"
|
||||||
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
|
import Blocks from "lucide-svelte/icons/blocks"
|
||||||
|
import Cog from "lucide-svelte/icons/cog"
|
||||||
|
import FileCode2 from "lucide-svelte/icons/file-code-2"
|
||||||
|
import Info from "lucide-svelte/icons/info"
|
||||||
|
import Route from "lucide-svelte/icons/route"
|
||||||
|
import SquareTerminal from "lucide-svelte/icons/square-terminal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: "General",
|
||||||
|
url: "/settings",
|
||||||
|
icon: Cog
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Developer",
|
||||||
|
url: "/settings/developer",
|
||||||
|
icon: SquareTerminal
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Extensions",
|
||||||
|
url: "/settings/extensions",
|
||||||
|
icon: Blocks
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Set Dev Extension",
|
||||||
|
url: "/settings/set-dev-ext-path",
|
||||||
|
icon: Route
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Add Dev Extension",
|
||||||
|
url: "/settings/add-dev-extension",
|
||||||
|
icon: FileCode2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "About",
|
||||||
|
url: "/settings/about",
|
||||||
|
icon: Info
|
||||||
|
}
|
||||||
|
]
|
||||||
|
let currentItem = $state(items.find((item) => window.location.pathname === item.url))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SideBar.Root>
|
||||||
|
<SideBar.Header class="h-12">
|
||||||
|
<SideBar.Menu>
|
||||||
|
<SideBar.MenuItem data-tauri-drag-region>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class={Constants.CLASSNAMES.BACK_BUTTON}
|
||||||
|
onclick={goHome}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</SideBar.MenuItem>
|
||||||
|
</SideBar.Menu>
|
||||||
|
</SideBar.Header>
|
||||||
|
<SideBar.Content>
|
||||||
|
<SideBar.Group>
|
||||||
|
<SideBar.GroupLabel data-tauri-drag-region>Settings</SideBar.GroupLabel>
|
||||||
|
<SideBar.GroupContent>
|
||||||
|
<SideBar.Menu>
|
||||||
|
{#each items as item (item.title)}
|
||||||
|
<SideBar.MenuItem>
|
||||||
|
<SideBar.MenuButton
|
||||||
|
isActive={currentItem?.url === item.url}
|
||||||
|
onclick={() => {
|
||||||
|
currentItem = item
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href={item.url} {...props}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</SideBar.MenuButton>
|
||||||
|
</SideBar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</SideBar.Menu>
|
||||||
|
</SideBar.GroupContent>
|
||||||
|
</SideBar.Group>
|
||||||
|
</SideBar.Content>
|
||||||
|
</SideBar.Root>
|
18
apps/desktop/src/routes/troubleshooters/+layout.svelte
Normal file
18
apps/desktop/src/routes/troubleshooters/+layout.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SidebarTrigger from "@/components/common/sidebar-trigger.svelte"
|
||||||
|
import { goHomeOrCloseOnEscapeWithInput } from "@/utils/key"
|
||||||
|
import { SideBar } from "@kksh/svelte5"
|
||||||
|
import TroubleshootersSidebar from "./sidebar.svelte"
|
||||||
|
|
||||||
|
let { children } = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={goHomeOrCloseOnEscapeWithInput} />
|
||||||
|
|
||||||
|
<SideBar.Provider style="--sidebar-width: 12rem;">
|
||||||
|
<TroubleshootersSidebar class="flex-none" />
|
||||||
|
<main class="grow overflow-x-clip">
|
||||||
|
<SidebarTrigger />
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
</SideBar.Provider>
|
@ -77,12 +77,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={goBackOnEscape} />
|
<div class="container">
|
||||||
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
|
|
||||||
<ArrowLeftIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div class="absolute left-0 top-0 h-10 w-screen" data-tauri-drag-region></div>
|
|
||||||
<div class="container pt-10">
|
|
||||||
<h1 class="text-2xl font-bold">Extension Loading Troubleshooter</h1>
|
<h1 class="text-2xl font-bold">Extension Loading Troubleshooter</h1>
|
||||||
<Button class="my-2" onclick={check}>Check</Button>
|
<Button class="my-2" onclick={check}>Check</Button>
|
||||||
<Dialog.Root bind:open={isDialogOpen}>
|
<Dialog.Root bind:open={isDialogOpen}>
|
||||||
|
@ -65,11 +65,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={onKeyDown} />
|
<main class="container">
|
||||||
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
|
|
||||||
<ArrowLeftIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<main class="container h-screen w-screen pt-10">
|
|
||||||
<div class="flex items-center justify-between space-x-2">
|
<div class="flex items-center justify-between space-x-2">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Checkbox id="refreshEverySecond" bind:checked={refreshEverySecond} />
|
<Checkbox id="refreshEverySecond" bind:checked={refreshEverySecond} />
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goBackOnEscape } from "@/utils/key.js"
|
||||||
|
import { goBack } from "@/utils/route"
|
||||||
|
import { getPeers } from "@kksh/api/commands"
|
||||||
|
import type { MdnsPeers } from "@kksh/api/models"
|
||||||
|
import { Button } from "@kksh/svelte5"
|
||||||
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let peers: MdnsPeers = $state({})
|
||||||
|
|
||||||
|
async function refreshPeers() {
|
||||||
|
console.log("refreshPeers")
|
||||||
|
peers = await getPeers()
|
||||||
|
console.log("peers", peers)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await refreshPeers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-10" data-tauri-drag-region></div>
|
||||||
|
<main class="container">
|
||||||
|
<Button onclick={refreshPeers}>Refresh mDNS Peers</Button>
|
||||||
|
<pre>{JSON.stringify(peers, null, 2)}</pre>
|
||||||
|
</main>
|
72
apps/desktop/src/routes/troubleshooters/sidebar.svelte
Normal file
72
apps/desktop/src/routes/troubleshooters/sidebar.svelte
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goHome } from "@/utils/route"
|
||||||
|
import { Button, SideBar } from "@kksh/svelte5"
|
||||||
|
import { Constants } from "@kksh/ui"
|
||||||
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
|
import AppWindow from "lucide-svelte/icons/app-window"
|
||||||
|
import Loader from "lucide-svelte/icons/loader"
|
||||||
|
import Network from "lucide-svelte/icons/network"
|
||||||
|
|
||||||
|
let { class: className }: { class?: string } = $props()
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: "Extension Loading",
|
||||||
|
url: "/troubleshooters/extension-loading",
|
||||||
|
icon: Loader
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Extension Window",
|
||||||
|
url: "/troubleshooters/extension-window",
|
||||||
|
icon: AppWindow
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "MDNS Debugger",
|
||||||
|
url: "/troubleshooters/mdns-debugger",
|
||||||
|
icon: Network
|
||||||
|
}
|
||||||
|
]
|
||||||
|
let currentItem = $state(items.find((item) => window.location.pathname === item.url))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SideBar.Root class={className}>
|
||||||
|
<SideBar.Header class="h-12">
|
||||||
|
<SideBar.Menu>
|
||||||
|
<SideBar.MenuItem data-tauri-drag-region>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="z-50 {Constants.CLASSNAMES.BACK_BUTTON}"
|
||||||
|
onclick={goHome}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</SideBar.MenuItem>
|
||||||
|
</SideBar.Menu>
|
||||||
|
</SideBar.Header>
|
||||||
|
<SideBar.Content>
|
||||||
|
<SideBar.Group>
|
||||||
|
<SideBar.GroupLabel data-tauri-drag-region>Settings</SideBar.GroupLabel>
|
||||||
|
<SideBar.GroupContent>
|
||||||
|
<SideBar.Menu>
|
||||||
|
{#each items as item (item.title)}
|
||||||
|
<SideBar.MenuItem>
|
||||||
|
<SideBar.MenuButton
|
||||||
|
isActive={currentItem?.url === item.url}
|
||||||
|
onclick={() => {
|
||||||
|
currentItem = item
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href={item.url} {...props}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</SideBar.MenuButton>
|
||||||
|
</SideBar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</SideBar.Menu>
|
||||||
|
</SideBar.GroupContent>
|
||||||
|
</SideBar.Group>
|
||||||
|
</SideBar.Content>
|
||||||
|
</SideBar.Root>
|
@ -15,7 +15,7 @@ const config = {
|
|||||||
alias: {
|
alias: {
|
||||||
"@/*": "./src/lib/*",
|
"@/*": "./src/lib/*",
|
||||||
// "@kksh/ui/*": "../../packages/ui/*",
|
// "@kksh/ui/*": "../../packages/ui/*",
|
||||||
"@kksh/svelte5/*": "../../node_modules/@kksh/svelte5/src/lib/*"
|
// "@kksh/svelte5/*": "../../node_modules/@kksh/svelte5/src/lib/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ const config: Config = {
|
|||||||
content: [
|
content: [
|
||||||
"./src/**/*.{html,js,svelte,ts}",
|
"./src/**/*.{html,js,svelte,ts}",
|
||||||
"./node_modules/@kksh/ui/src/**/*.{html,js,svelte,ts}",
|
"./node_modules/@kksh/ui/src/**/*.{html,js,svelte,ts}",
|
||||||
"../../node_modules/@kksh/svelte5/src/**/*.{html,js,svelte,ts}"
|
"../../node_modules/@kksh/svelte5/dist/**/*.{html,js,svelte,ts}"
|
||||||
],
|
],
|
||||||
safelist: ["dark", "bg-red-500/30"],
|
safelist: ["dark", "bg-red-500/30"],
|
||||||
theme: {
|
theme: {
|
||||||
@ -77,10 +77,10 @@ const config: Config = {
|
|||||||
keyframes: {
|
keyframes: {
|
||||||
"accordion-down": {
|
"accordion-down": {
|
||||||
from: { height: "0" },
|
from: { height: "0" },
|
||||||
to: { height: "var(--radix-accordion-content-height)" }
|
to: { height: "var(--bits-accordion-content-height)" }
|
||||||
},
|
},
|
||||||
"accordion-up": {
|
"accordion-up": {
|
||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: "var(--bits-accordion-content-height)" },
|
||||||
to: { height: "0" }
|
to: { height: "0" }
|
||||||
},
|
},
|
||||||
"caret-blink": {
|
"caret-blink": {
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import { defineConfig, presetAttributify, presetTagify, presetUno } from "unocss"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
presets: [presetUno(), presetAttributify(), presetTagify()]
|
|
||||||
})
|
|
@ -1,5 +1,4 @@
|
|||||||
import { sveltekit } from "@sveltejs/kit/vite"
|
import { sveltekit } from "@sveltejs/kit/vite"
|
||||||
import UnoCSS from "unocss/vite"
|
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
@ -7,7 +6,7 @@ const host = process.env.TAURI_DEV_HOST
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [UnoCSS(), sveltekit()],
|
plugins: [sveltekit()],
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent vite from obscuring rust errors
|
// 1. prevent vite from obscuring rust errors
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||||
"@kksh/api": "workspace:*",
|
"@kksh/api": "workspace:*",
|
||||||
"@kksh/svelte5": "0.1.2-beta.8",
|
"@kksh/svelte5": "0.1.9",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-svelte": "^3.2.7",
|
"prettier-plugin-svelte": "^3.2.7",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||||
|
@ -13,13 +13,15 @@ impl Peers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_peer(&self, service_type: String, fullname: String) {
|
pub fn remove_peer(&self, service_type: String, fullname: String) {
|
||||||
let peers = self.peers.lock().unwrap();
|
let peer = self
|
||||||
// find the peer by service_type and fullname
|
.peers
|
||||||
let peer = peers
|
.lock()
|
||||||
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, peer)| peer.fullname == fullname && peer.service_type == service_type);
|
.find(|(_, peer)| peer.fullname == fullname && peer.service_type == service_type)
|
||||||
if let Some((hostname, _)) = peer {
|
.map(|(hostname, _)| hostname.clone());
|
||||||
self.peers.lock().unwrap().remove(hostname);
|
if let Some(hostname) = peer {
|
||||||
|
self.peers.lock().unwrap().remove(&hostname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,5 +41,6 @@ pub async fn get_peers(
|
|||||||
state: tauri::State<'_, Peers>,
|
state: tauri::State<'_, Peers>,
|
||||||
) -> Result<HashMap<String, ServiceInfoMod>, String> {
|
) -> Result<HashMap<String, ServiceInfoMod>, String> {
|
||||||
let _peers = state.peers.lock().unwrap();
|
let _peers = state.peers.lock().unwrap();
|
||||||
|
println!("get_peers: {:?}", _peers);
|
||||||
Ok(_peers.to_owned())
|
Ok(_peers.to_owned())
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@ use crate::commands::discovery::Peers;
|
|||||||
use mdns_sd::ServiceEvent;
|
use mdns_sd::ServiceEvent;
|
||||||
use tauri::{AppHandle, Manager, Runtime};
|
use tauri::{AppHandle, Manager, Runtime};
|
||||||
use tauri_plugin_network::network::mdns::MdnsService;
|
use tauri_plugin_network::network::mdns::MdnsService;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn setup_mdns(my_port: u16) -> anyhow::Result<MdnsService> {
|
pub fn setup_mdns(my_port: u16) -> anyhow::Result<MdnsService> {
|
||||||
let mdns = MdnsService::new("tauri")?;
|
let mdns = MdnsService::new("kunkun")?;
|
||||||
|
let id = Uuid::new_v4();
|
||||||
mdns.register(
|
mdns.register(
|
||||||
"tauridesktop",
|
&format!("desktop-{}", id),
|
||||||
&MdnsService::get_default_ips_str(),
|
&MdnsService::get_default_ips_str(),
|
||||||
my_port,
|
my_port,
|
||||||
None,
|
None,
|
||||||
@ -23,13 +25,22 @@ pub fn handle_mdns_service_evt<R: Runtime>(
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
while let Ok(event) = rx.recv() {
|
while let Ok(event) = rx.recv() {
|
||||||
match event {
|
match event {
|
||||||
|
// ServiceEvent::ServiceResolved(info) => {
|
||||||
|
// log::info!("Service Resolved: {:?}", info);
|
||||||
|
// },
|
||||||
ServiceEvent::ServiceResolved(info) => {
|
ServiceEvent::ServiceResolved(info) => {
|
||||||
|
log::info!("Service Resolved: {:?}", info);
|
||||||
app_handle.state::<Peers>().add_peer(info.into());
|
app_handle.state::<Peers>().add_peer(info.into());
|
||||||
|
let peers = app_handle.state::<Peers>().peers.lock().unwrap().clone();
|
||||||
|
log::info!("Peers: {:?}", peers);
|
||||||
}
|
}
|
||||||
ServiceEvent::ServiceRemoved(service_type, fullname) => {
|
ServiceEvent::ServiceRemoved(service_type, fullname) => {
|
||||||
|
log::info!("Service Removed: {:?} {:?}", service_type, fullname);
|
||||||
app_handle
|
app_handle
|
||||||
.state::<Peers>()
|
.state::<Peers>()
|
||||||
.remove_peer(service_type, fullname);
|
.remove_peer(service_type, fullname);
|
||||||
|
let peers = app_handle.state::<Peers>().peers.lock().unwrap().clone();
|
||||||
|
log::info!("Peers: {:?}", peers);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@ export const PersistedAppConfig = v.object({
|
|||||||
hideOnBlur: v.boolean(),
|
hideOnBlur: v.boolean(),
|
||||||
extensionAutoUpgrade: v.boolean(),
|
extensionAutoUpgrade: v.boolean(),
|
||||||
joinBetaProgram: v.boolean(),
|
joinBetaProgram: v.boolean(),
|
||||||
onBoarded: v.boolean()
|
onBoarded: v.boolean(),
|
||||||
|
developerMode: v.boolean()
|
||||||
})
|
})
|
||||||
|
|
||||||
export type PersistedAppConfig = v.InferOutput<typeof PersistedAppConfig>
|
export type PersistedAppConfig = v.InferOutput<typeof PersistedAppConfig>
|
||||||
|
@ -34,12 +34,11 @@
|
|||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tauri-plugin-shellx-api": "^2.0.11",
|
|
||||||
"@iconify/svelte": "^4.0.2",
|
"@iconify/svelte": "^4.0.2",
|
||||||
"@kksh/api": "workspace:*",
|
"@kksh/api": "workspace:*",
|
||||||
"@kksh/svelte5": "^0.1.2-beta.8",
|
"@kksh/svelte5": "^0.1.9",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"bits-ui": "1.0.0-next.45",
|
"bits-ui": "1.0.0-next.49",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"formsnap": "2.0.0-next.1",
|
"formsnap": "2.0.0-next.1",
|
||||||
"lucide-svelte": "^0.454.0",
|
"lucide-svelte": "^0.454.0",
|
||||||
@ -53,6 +52,7 @@
|
|||||||
"tailwind-variants": "^0.2.1",
|
"tailwind-variants": "^0.2.1",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tauri-plugin-shellx-api": "^2.0.11",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from "@kksh/ui/utils"
|
import { cn } from "@kksh/ui/utils"
|
||||||
import { mode } from "mode-watcher"
|
import { mode } from "mode-watcher"
|
||||||
import { createHighlighterCore } from "shiki/core"
|
import { createHighlighterCore, type HighlighterCore } from "shiki/core"
|
||||||
import { createOnigurumaEngine } from "shiki/engine/oniguruma"
|
import { createOnigurumaEngine } from "shiki/engine/oniguruma"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
@ -19,17 +19,29 @@
|
|||||||
class?: string
|
class?: string
|
||||||
} = $props()
|
} = $props()
|
||||||
let html = $state("")
|
let html = $state("")
|
||||||
|
let highlighter: HighlighterCore
|
||||||
|
|
||||||
onMount(async () => {
|
function refresh() {
|
||||||
const highlighter = await createHighlighterCore({
|
|
||||||
themes: [import("shiki/themes/vitesse-dark.mjs"), import("shiki/themes/vitesse-light.mjs")],
|
|
||||||
langs: [import("shiki/langs/json.mjs"), import("shiki/langs/typescript.mjs")],
|
|
||||||
engine: createOnigurumaEngine(import("shiki/wasm"))
|
|
||||||
})
|
|
||||||
html = highlighter.codeToHtml(code, {
|
html = highlighter.codeToHtml(code, {
|
||||||
lang,
|
lang,
|
||||||
theme: theme ?? ($mode === "dark" ? "vitesse-dark" : "vitesse-light")
|
theme: theme ?? ($mode === "dark" ? "vitesse-dark" : "vitesse-light")
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
onMount(async () => {
|
||||||
|
highlighter = await createHighlighterCore({
|
||||||
|
themes: [import("shiki/themes/vitesse-dark.mjs"), import("shiki/themes/vitesse-light.mjs")],
|
||||||
|
langs: [import("shiki/langs/json.mjs"), import("shiki/langs/typescript.mjs")],
|
||||||
|
engine: createOnigurumaEngine(import("shiki/wasm"))
|
||||||
|
})
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
code // keep this here to watch for code changes
|
||||||
|
highlighter
|
||||||
|
if (highlighter) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,4 +2,5 @@ export { default as IconMultiplexer } from "./IconMultiplexer.svelte"
|
|||||||
export { default as IconSelector } from "./IconSelector.svelte"
|
export { default as IconSelector } from "./IconSelector.svelte"
|
||||||
export { default as StrikeSeparator } from "./StrikeSeparator.svelte"
|
export { default as StrikeSeparator } from "./StrikeSeparator.svelte"
|
||||||
export { default as LoadingBar } from "./LoadingBar.svelte"
|
export { default as LoadingBar } from "./LoadingBar.svelte"
|
||||||
|
export { default as TauriLink } from "./TauriLink.svelte"
|
||||||
export * from "./date"
|
export * from "./date"
|
||||||
|
@ -23,6 +23,6 @@
|
|||||||
<Button class="fixed left-2 top-2" onclick={onGoBack} variant="outline" size="icon">
|
<Button class="fixed left-2 top-2" onclick={onGoBack} variant="outline" size="icon">
|
||||||
<ArrowLeftIcon />
|
<ArrowLeftIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<main class="container my-5">
|
<main class="container my-5 px-12">
|
||||||
<Markdown markdown={markdownViewContent.content} />
|
<Markdown markdown={markdownViewContent.content} />
|
||||||
</main>
|
</main>
|
||||||
|
@ -16,4 +16,3 @@
|
|||||||
<h1 class="text-2xl font-bold">{formViewContent.title}</h1>
|
<h1 class="text-2xl font-bold">{formViewContent.title}</h1>
|
||||||
<Form {formViewContent} />
|
<Form {formViewContent} />
|
||||||
</main>
|
</main>
|
||||||
<!-- <pre>{JSON.stringify(formViewContent, null, 2)}</pre> -->
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DraggableCommandGroup heading="Builtin Commands">
|
<DraggableCommandGroup heading="Builtin Commands">
|
||||||
{#each builtinCmds as cmd}
|
{#each builtinCmds as cmd (cmd.id)}
|
||||||
<Command.Item
|
<Command.Item
|
||||||
class="flex justify-between"
|
class="flex justify-between"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@ -19,6 +19,7 @@
|
|||||||
cmdName: cmd.name,
|
cmdName: cmd.name,
|
||||||
cmdType: CmdTypeEnum.Builtin
|
cmdType: CmdTypeEnum.Builtin
|
||||||
} satisfies CmdValue)}
|
} satisfies CmdValue)}
|
||||||
|
keywords={cmd.keywords}
|
||||||
>
|
>
|
||||||
<span class="flex gap-2">
|
<span class="flex gap-2">
|
||||||
<IconMultiplexer
|
<IconMultiplexer
|
||||||
@ -26,6 +27,10 @@
|
|||||||
class="!h-5 !w-5 shrink-0"
|
class="!h-5 !w-5 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span>{cmd.name}</span>
|
<span>{cmd.name}</span>
|
||||||
|
<!-- <pre>{JSON.stringify({
|
||||||
|
cmdName: cmd.name,
|
||||||
|
cmdType: CmdTypeEnum.Builtin
|
||||||
|
})}</pre> -->
|
||||||
</span>
|
</span>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -25,8 +25,12 @@
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
class={cn("h-12 select-none items-center justify-between gap-4 border-t px-2", className)}
|
class={cn("h-12 select-none items-center justify-between gap-4 border-t px-2", className)}
|
||||||
>
|
>
|
||||||
<Avatar.Root class="p-2">
|
<Avatar.Root class="p-1.5">
|
||||||
<Avatar.Image src="/favicon.png" alt="Kunkun Logo" class="select-none invert dark:invert-0" />
|
<Avatar.Image
|
||||||
|
src="/favicon.png"
|
||||||
|
alt="Kunkun Logo"
|
||||||
|
class="h-full select-none invert dark:invert-0"
|
||||||
|
/>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<flex class="items-center gap-1">
|
<flex class="items-center gap-1">
|
||||||
{#if defaultAction}
|
{#if defaultAction}
|
||||||
|
@ -8,10 +8,16 @@ import {
|
|||||||
import * as v from "valibot"
|
import * as v from "valibot"
|
||||||
|
|
||||||
export type BuiltinCmd = {
|
export type BuiltinCmd = {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
iconifyIcon: string
|
iconifyIcon: string
|
||||||
|
keywords?: string[]
|
||||||
function: () => Promise<void>
|
function: () => Promise<void>
|
||||||
|
flags?: {
|
||||||
|
dev?: boolean // commands only available in dev mode
|
||||||
|
developer?: boolean // commands only available in developer mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OnExtCmdSelect = (
|
export type OnExtCmdSelect = (
|
||||||
|
1132
pnpm-lock.yaml
generated
1132
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user