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:
Huakun Shen 2024-11-11 17:15:42 -05:00 committed by GitHub
parent 0600eca59a
commit 7865d18580
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1353 additions and 1145 deletions

1
Cargo.lock generated
View File

@ -3215,6 +3215,7 @@ dependencies = [
"tauri-plugin-upload",
"tokio",
"urlencoding",
"uuid",
"zip 2.2.0",
]

View 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"
}

View File

@ -21,44 +21,43 @@
"@kksh/ui": "workspace:*",
"@kksh/utils": "workspace:*",
"@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",
"bits-ui": "1.0.0-next.36",
"gsap": "^3.12.5",
"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",
"tauri-plugin-clipboard-api": "^2.1.11",
"uuid": "^11.0.2"
"uuid": "^11.0.3"
},
"devDependencies": {
"@kksh/types": "workspace:*",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.7.4",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tauri-apps/cli": "^2.0.4",
"@tauri-apps/cli": "^2.1.0",
"@types/bun": "latest",
"@types/semver": "^7.5.8",
"@unocss/preset-attributify": "^0.64.0",
"autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.49",
"clsx": "^2.1.1",
"embla-carousel-svelte": "^8.3.1",
"formsnap": "^1.0.1",
"lucide-svelte": "^0.456.0",
"svelte-radix": "^2.0.1",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"tailwindcss": "^3.4.14",
"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"
}

View File

@ -48,6 +48,7 @@ tauri-plugin-store = "2.1.0"
tauri-plugin-deep-link = "2"
tauri-plugin-log = { version = "2.0.1", features = ["colored"] }
zip = "2.1.3"
uuid = "1.11.0"
# tauri-plugin-devtools = "2.0.0"
[target."cfg(target_os = \"macos\")".dependencies]

View File

@ -3,73 +3,75 @@
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--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%;
}
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--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%;
}
--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 {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--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 {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -4,12 +4,14 @@ import type { BuiltinCmd } from "@kksh/ui/types"
import { getVersion } from "@tauri-apps/api/app"
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
import { exit } from "@tauri-apps/plugin-process"
import { dev } from "$app/environment"
import { goto } from "$app/navigation"
import { toast } from "svelte-sonner"
import { derived } from "svelte/store"
import * as clipboard from "tauri-plugin-clipboard-api"
import { v4 as uuidv4 } from "uuid"
export const builtinCmds: BuiltinCmd[] = [
export const rawBuiltinCmds: BuiltinCmd[] = [
{
name: "Store",
iconifyIcon: "streamline:store-2-solid",
@ -95,7 +97,8 @@ export const builtinCmds: BuiltinCmd[] = [
url: "/troubleshooters/extension-window",
title: "Extension Window Troubleshooter"
})
}
},
keywords: ["extension", "window", "troubleshooter"]
},
{
name: "Extension Permission Inspector",
@ -104,7 +107,8 @@ export const builtinCmds: BuiltinCmd[] = [
function: async () => {
appState.clearSearchTerm()
goto("/extension/permission-inspector")
}
},
keywords: ["extension"]
},
{
name: "Extension Loading Troubleshooter",
@ -113,7 +117,8 @@ export const builtinCmds: BuiltinCmd[] = [
function: async () => {
appState.clearSearchTerm()
goto("/troubleshooters/extension-loading")
}
},
keywords: ["extension", "troubleshooter"]
},
{
name: "Create Quicklink",
@ -124,28 +129,15 @@ export const builtinCmds: BuiltinCmd[] = [
goto("/extension/create-quick-link")
}
},
// {
// name: "Settings",
// iconifyIcon: "solar:settings-linear",
// description: "Open Settings",
// function: async () => {
// const windows = await getAllWebviewWindows()
// const found = windows.find((w) => w.label === SettingsWindowLabel)
// 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: "Settings",
iconifyIcon: "solar:settings-linear",
description: "Open Settings",
function: async () => {
goto("/settings")
appState.clearSearchTerm()
}
},
{
name: "Check Update",
iconifyIcon: "material-symbols:update",
@ -226,6 +218,18 @@ export const builtinCmds: BuiltinCmd[] = [
}, 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",
iconifyIcon: "ri:toggle-line",
@ -240,5 +244,24 @@ export const builtinCmds: BuiltinCmd[] = [
})
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
})
})

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -109,7 +109,7 @@
}
</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={pickExtFiles}>Install from Extension Tarball File</Button>
</div>

View File

@ -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} />

View File

@ -50,10 +50,10 @@
<Form.Field {form} name="name">
<Form.Control>
{#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" />
<Form.Button class="my-1">Install</Form.Button>
</flex>
</div>
{/snippet}
</Form.Control>
<Form.FieldErrors />

View File

@ -51,10 +51,10 @@
<Form.Field {form} name="url">
<Form.Control>
{#snippet children({ props })}
<flex items-center gap-2>
<div class="flex items-center gap-2">
<Input {...props} bind:value={$formData.url} placeholder="Tarball URL" />
<Form.Button class="my-1">Install</Form.Button>
</flex>
</div>
{/snippet}
</Form.Control>
<Form.FieldErrors />

View File

@ -5,6 +5,7 @@ import { PersistedAppConfig, type AppConfig } from "@kksh/types"
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 } from "svelte/store"
import * as v from "valibot"
export const defaultAppConfig: AppConfig = {
@ -24,13 +25,16 @@ export const defaultAppConfig: AppConfig = {
hideOnBlur: true,
extensionAutoUpgrade: true,
joinBetaProgram: false,
onBoarded: false
onBoarded: false,
developerMode: false
}
interface AppConfigAPI {
init: () => Promise<void>
get: () => AppConfig
setTheme: (theme: ThemeConfig) => void
setDevExtensionPath: (devExtensionPath: string | null) => void
setTriggerHotkey: (triggerHotkey: string[]) => void
}
function createAppConfig(): WithSyncStore<AppConfig> & AppConfigAPI {
@ -67,11 +71,15 @@ function createAppConfig(): WithSyncStore<AppConfig> & AppConfigAPI {
return {
...store,
get: () => get(store),
setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })),
setDevExtensionPath: (devExtensionPath: string | null) => {
console.log("setDevExtensionPath", devExtensionPath)
store.update((config) => ({ ...config, devExtensionPath }))
},
setTriggerHotkey: (triggerHotkey: string[]) => {
store.update((config) => ({ ...config, triggerHotkey }))
},
init
}
}

View 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));
}

View File

@ -1,3 +1,11 @@
export function getActiveElementNodeName(): string | undefined {
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)
}

View 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)
}

View 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"])

View File

@ -1,20 +1,30 @@
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 { 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) {
console.log("goHomeOnEscape", e.key)
if (e.key === "Escape") {
goHome()
}
}
export function goBackOnEscape(e: KeyboardEvent) {
console.log("goBackOnEscape", e.key)
if (e.key === "Escape") {
goBack()
}
}
export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) {
console.log("goBackOnEscapeClearSearchTerm", e.key)
if (e.key === "Escape") {
if (appState.get().searchTerm) {
appState.clearSearchTerm()
@ -25,6 +35,7 @@ export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) {
}
export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) {
console.log("goHomeOnEscapeClearSearchTerm", e.key)
if (e.key === "Escape") {
if (appState.get().searchTerm) {
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
}

View 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 })
}
}

View File

@ -1,4 +1,6 @@
import { getCurrentWindow } from "@tauri-apps/api/window"
import { goto } from "$app/navigation"
import { isInMainWindow } from "./window"
export function goBack() {
window.history.back()
@ -7,3 +9,11 @@ export function goBack() {
export function goHome() {
goto("/")
}
export function goHomeOrCloseDependingOnWindow() {
if (isInMainWindow()) {
goHome()
} else {
getCurrentWindow().close()
}
}

View File

@ -3,6 +3,9 @@
import "../app.css"
import { appConfig, appState, extensions, quickLinks } from "@/stores"
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 {
ModeWatcher,
@ -12,15 +15,39 @@
updateTheme,
type ThemeConfig
} from "@kksh/svelte5"
import { ViewTransition } from "@kksh/ui"
import { Constants, ViewTransition } from "@kksh/ui"
import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
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"
onMount(() => {
setTimeout(() => {
import("virtual:uno.css")
}, 1000)
/* -------------------------------------------------------------------------- */
/* Gsap Flip Animation */
/* -------------------------------------------------------------------------- */
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()
@ -29,12 +56,31 @@
onMount(async () => {
attachConsole().then((unlistener) => unlisteners.push(unlistener))
initDeeplink().then((unlistener) => unlisteners.push(unlistener))
quickLinks.init()
appConfig.init()
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()
} else {
}
getCurrentWebviewWindow().show()
})
onDestroy(() => {
@ -42,6 +88,7 @@
})
</script>
<svelte:window on:keydown={globalKeyDownHandler} />
<ViewTransition />
<ModeWatcher />
<Toaster richColors />

View File

@ -1,4 +1,4 @@
import { getExtensionsFolder } from "@/constants"
import { getExtensionsFolder, IS_IN_TAURI } from "@/constants"
import type { LayoutLoad } from "./$types"
// 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 load: LayoutLoad = async () => {
return { extsInstallDir: await getExtensionsFolder() }
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "" }
}

View File

@ -5,8 +5,9 @@
import { systemCommands } from "@/cmds/system"
import { appConfig, appState, devStoreExts, installedStoreExts, quickLinks } from "@/stores"
import { cmdQueries } from "@/stores/cmdQuery"
import { getActiveElementNodeName } from "@/utils/dom"
import { openDevTools } from "@kksh/api/commands"
import { getActiveElementNodeName, isKeyboardEventFromInputElement } from "@/utils/dom"
import Icon from "@iconify/svelte"
import { openDevTools, toggleDevTools } from "@kksh/api/commands"
import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { isExtPathInDev } from "@kksh/extension/utils"
import { Button, Command, DropdownMenu } from "@kksh/svelte5"
@ -23,22 +24,29 @@
import { cn, commandScore } from "@kksh/ui/utils"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { exit } from "@tauri-apps/plugin-process"
import { EllipsisVerticalIcon } from "lucide-svelte"
import type { Writable } from "svelte/store"
import { CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte"
let inputEle: HTMLInputElement | null = null
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
if (getActiveElementNodeName() === "INPUT") {
;(event.target as HTMLInputElement).value = ""
if ((event.target as HTMLInputElement | undefined)?.id === "main-command-input") {
$appState.searchTerm = ""
}
}
;(event.target as HTMLInputElement).value = ""
$appState.searchTerm = ""
}
}
</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
class={cn("h-screen rounded-lg border shadow-md")}
bind:value={$appState.highlightedCmd}
@ -53,9 +61,11 @@
>
<CustomCommandInput
autofocus
bind:ref={inputEle}
id="main-command-input"
placeholder={$cmdQueries.length === 0 ? "Type a command or search..." : undefined}
bind:value={$appState.searchTerm}
onkeydown={onKeyDown}
>
{#snippet rightSlot()}
<span
@ -88,15 +98,42 @@
<DropdownMenu.Trigger>
<Button variant="outline" size="icon"><EllipsisVerticalIcon /></Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Content class="w-80">
<DropdownMenu.Group>
<DropdownMenu.GroupHeading>Settings</DropdownMenu.GroupHeading>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={() => exit()}>Quit</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => openDevTools()}>Open Dev Tools</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => getCurrentWebviewWindow().hide()}
>Close Window</DropdownMenu.Item
<DropdownMenu.Item onclick={() => exit()}>
<CircleXIcon class="h-4 w-4 text-red-500" />
Quit
</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.Content>
</DropdownMenu.Root>
@ -123,7 +160,7 @@
/>
{/if}
<QuickLinks quickLinks={$quickLinks} />
<BuiltinCmds {builtinCmds} />
<BuiltinCmds builtinCmds={$builtinCmds} />
<SystemCmds {systemCommands} />
</Command.List>
<GlobalCommandPaletteFooter />

View File

@ -1,31 +1,31 @@
<script lang="ts">
import { Constants } from "@kksh/ui"
import { afterNavigate, beforeNavigate } from "$app/navigation"
import { gsap } from "gsap"
import { Flip } from "gsap/Flip"
// import { Constants } from "@kksh/ui"
// import { afterNavigate, beforeNavigate } from "$app/navigation"
// import { gsap } from "gsap"
// import { Flip } from "gsap/Flip"
gsap.registerPlugin(Flip)
let flipState: Flip.FlipState
// gsap.registerPlugin(Flip)
// let flipState: Flip.FlipState
beforeNavigate(() => {
flipState = Flip.getState(
`.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`
)
})
// beforeNavigate(() => {
// flipState = Flip.getState(
// `.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`
// )
// })
afterNavigate(() => {
if (!flipState) {
return
}
// 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"
})
})
// Flip.from(flipState, {
// targets: `.${Constants.CLASSNAMES.EXT_LOGO}, .${Constants.CLASSNAMES.BACK_BUTTON}`,
// duration: 0.5,
// absolute: true,
// scale: true,
// ease: "ease-out"
// })
// })
const { children } = $props()
</script>

View File

@ -56,11 +56,14 @@
let loaded = $state(false)
async function goBack() {
console.log("goBack")
if (isInMainWindow()) {
console.log("goBack in main window")
// if in main window, then winExtMap store must contain this
winExtMap.unregisterExtensionFromWindow(appWin.label)
// winExtMap.unregisterExtensionFromWindow(appWin.label)
goto("/")
} else {
console.log("goBack in webview window")
appWin.close()
}
}
@ -179,6 +182,7 @@
searchBarPlaceholder = placeholder
},
async goBack() {
console.log("goBack in ui-worker")
goBack()
}
}

View 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>

View 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>

View 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>

View File

@ -3,10 +3,10 @@
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
import { appConfig, extensions } from "@/stores"
import { goBackOnEscape } from "@/utils/key"
import { goBack } from "@/utils/route"
import { goBack, goHome } from "@/utils/route"
import * as extAPI 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 { open as openFileSelector } from "@tauri-apps/plugin-dialog"
import * as fs from "@tauri-apps/plugin-fs"
@ -14,18 +14,16 @@
import { ArrowLeftIcon } from "lucide-svelte"
import { toast } from "svelte-sonner"
import * as v from "valibot"
const { useSidebar } = SideBar
const sidebar = useSidebar()
</script>
<svelte:window on:keydown={goBackOnEscape} />
<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">
<main class="container">
<h2 class="text-2xl font-bold">Add Dev Extension</h2>
<small>
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.
</small>
<AddDevExtForm />
<AddDevExtForm />
</main>

View File

@ -1,17 +1,8 @@
<script lang="ts">
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>
<svelte:window on:keydown={goBackOnEscape} />
<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">
<main class="container">
<h2 class="text-2xl font-bold">Set Dev Extension Path</h2>
<p>This is where your extensions will be installed.</p>
<DevExtPathForm />

View 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>

View 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>

View File

@ -77,12 +77,7 @@
})
</script>
<svelte:window on:keydown={goBackOnEscape} />
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
<ArrowLeftIcon class="h-4 w-4" />
</Button>
<div class="absolute left-0 top-0 h-10 w-screen" data-tauri-drag-region></div>
<div class="container pt-10">
<div class="container">
<h1 class="text-2xl font-bold">Extension Loading Troubleshooter</h1>
<Button class="my-2" onclick={check}>Check</Button>
<Dialog.Root bind:open={isDialogOpen}>

View File

@ -65,11 +65,7 @@
}
</script>
<svelte:window on:keydown={onKeyDown} />
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" onclick={goBack}>
<ArrowLeftIcon class="h-4 w-4" />
</Button>
<main class="container h-screen w-screen pt-10">
<main class="container">
<div class="flex items-center justify-between space-x-2">
<div class="flex items-center space-x-2">
<Checkbox id="refreshEverySecond" bind:checked={refreshEverySecond} />

View File

@ -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>

View 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>

View File

@ -15,7 +15,7 @@ const config = {
alias: {
"@/*": "./src/lib/*",
// "@kksh/ui/*": "../../packages/ui/*",
"@kksh/svelte5/*": "../../node_modules/@kksh/svelte5/src/lib/*"
// "@kksh/svelte5/*": "../../node_modules/@kksh/svelte5/src/lib/*"
}
}
}

View File

@ -8,7 +8,7 @@ const config: Config = {
content: [
"./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"],
theme: {
@ -77,10 +77,10 @@ const config: Config = {
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" }
to: { height: "var(--bits-accordion-content-height)" }
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" }
},
"caret-blink": {

View File

@ -1,5 +0,0 @@
import { defineConfig, presetAttributify, presetTagify, presetUno } from "unocss"
export default defineConfig({
presets: [presetUno(), presetAttributify(), presetTagify()]
})

View File

@ -1,5 +1,4 @@
import { sveltekit } from "@sveltejs/kit/vite"
import UnoCSS from "unocss/vite"
import { defineConfig } from "vite"
// @ts-expect-error process is a nodejs global
@ -7,7 +6,7 @@ const host = process.env.TAURI_DEV_HOST
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [UnoCSS(), sveltekit()],
plugins: [sveltekit()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors

View File

@ -12,7 +12,7 @@
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@kksh/api": "workspace:*",
"@kksh/svelte5": "0.1.2-beta.8",
"@kksh/svelte5": "0.1.9",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.7",
"prettier-plugin-tailwindcss": "^0.6.8",

View File

@ -13,13 +13,15 @@ impl Peers {
}
pub fn remove_peer(&self, service_type: String, fullname: String) {
let peers = self.peers.lock().unwrap();
// find the peer by service_type and fullname
let peer = peers
let peer = self
.peers
.lock()
.unwrap()
.iter()
.find(|(_, peer)| peer.fullname == fullname && peer.service_type == service_type);
if let Some((hostname, _)) = peer {
self.peers.lock().unwrap().remove(hostname);
.find(|(_, peer)| peer.fullname == fullname && peer.service_type == service_type)
.map(|(hostname, _)| hostname.clone());
if let Some(hostname) = peer {
self.peers.lock().unwrap().remove(&hostname);
}
}
@ -39,5 +41,6 @@ pub async fn get_peers(
state: tauri::State<'_, Peers>,
) -> Result<HashMap<String, ServiceInfoMod>, String> {
let _peers = state.peers.lock().unwrap();
println!("get_peers: {:?}", _peers);
Ok(_peers.to_owned())
}

View File

@ -2,11 +2,13 @@ use crate::commands::discovery::Peers;
use mdns_sd::ServiceEvent;
use tauri::{AppHandle, Manager, Runtime};
use tauri_plugin_network::network::mdns::MdnsService;
use uuid::Uuid;
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(
"tauridesktop",
&format!("desktop-{}", id),
&MdnsService::get_default_ips_str(),
my_port,
None,
@ -23,13 +25,22 @@ pub fn handle_mdns_service_evt<R: Runtime>(
tauri::async_runtime::spawn(async move {
while let Ok(event) = rx.recv() {
match event {
// ServiceEvent::ServiceResolved(info) => {
// log::info!("Service Resolved: {:?}", info);
// },
ServiceEvent::ServiceResolved(info) => {
log::info!("Service Resolved: {:?}", info);
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) => {
log::info!("Service Removed: {:?} {:?}", service_type, fullname);
app_handle
.state::<Peers>()
.remove_peer(service_type, fullname);
let peers = app_handle.state::<Peers>().peers.lock().unwrap().clone();
log::info!("Peers: {:?}", peers);
}
_ => {}
}

View File

@ -16,7 +16,8 @@ export const PersistedAppConfig = v.object({
hideOnBlur: v.boolean(),
extensionAutoUpgrade: v.boolean(),
joinBetaProgram: v.boolean(),
onBoarded: v.boolean()
onBoarded: v.boolean(),
developerMode: v.boolean()
})
export type PersistedAppConfig = v.InferOutput<typeof PersistedAppConfig>

View File

@ -34,12 +34,11 @@
"lint": "eslint ."
},
"devDependencies": {
"tauri-plugin-shellx-api": "^2.0.11",
"@iconify/svelte": "^4.0.2",
"@kksh/api": "workspace:*",
"@kksh/svelte5": "^0.1.2-beta.8",
"@kksh/svelte5": "^0.1.9",
"@types/bun": "latest",
"bits-ui": "1.0.0-next.45",
"bits-ui": "1.0.0-next.49",
"clsx": "^2.1.1",
"formsnap": "2.0.0-next.1",
"lucide-svelte": "^0.454.0",
@ -53,6 +52,7 @@
"tailwind-variants": "^0.2.1",
"tailwindcss": "^3.4.14",
"tailwindcss-animate": "^1.0.7",
"tauri-plugin-shellx-api": "^2.0.11",
"zod": "^3.23.8"
},
"dependencies": {

View File

@ -3,7 +3,7 @@
<script lang="ts">
import { cn } from "@kksh/ui/utils"
import { mode } from "mode-watcher"
import { createHighlighterCore } from "shiki/core"
import { createHighlighterCore, type HighlighterCore } from "shiki/core"
import { createOnigurumaEngine } from "shiki/engine/oniguruma"
import { onMount } from "svelte"
@ -19,17 +19,29 @@
class?: string
} = $props()
let html = $state("")
let highlighter: HighlighterCore
onMount(async () => {
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"))
})
function refresh() {
html = highlighter.codeToHtml(code, {
lang,
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>

View File

@ -2,4 +2,5 @@ export { default as IconMultiplexer } from "./IconMultiplexer.svelte"
export { default as IconSelector } from "./IconSelector.svelte"
export { default as StrikeSeparator } from "./StrikeSeparator.svelte"
export { default as LoadingBar } from "./LoadingBar.svelte"
export { default as TauriLink } from "./TauriLink.svelte"
export * from "./date"

View File

@ -23,6 +23,6 @@
<Button class="fixed left-2 top-2" onclick={onGoBack} variant="outline" size="icon">
<ArrowLeftIcon />
</Button>
<main class="container my-5">
<main class="container my-5 px-12">
<Markdown markdown={markdownViewContent.content} />
</main>

View File

@ -16,4 +16,3 @@
<h1 class="text-2xl font-bold">{formViewContent.title}</h1>
<Form {formViewContent} />
</main>
<!-- <pre>{JSON.stringify(formViewContent, null, 2)}</pre> -->

View File

@ -9,7 +9,7 @@
</script>
<DraggableCommandGroup heading="Builtin Commands">
{#each builtinCmds as cmd}
{#each builtinCmds as cmd (cmd.id)}
<Command.Item
class="flex justify-between"
onSelect={() => {
@ -19,6 +19,7 @@
cmdName: cmd.name,
cmdType: CmdTypeEnum.Builtin
} satisfies CmdValue)}
keywords={cmd.keywords}
>
<span class="flex gap-2">
<IconMultiplexer
@ -26,6 +27,10 @@
class="!h-5 !w-5 shrink-0"
/>
<span>{cmd.name}</span>
<!-- <pre>{JSON.stringify({
cmdName: cmd.name,
cmdType: CmdTypeEnum.Builtin
})}</pre> -->
</span>
</Command.Item>
{/each}

View File

@ -25,8 +25,12 @@
data-tauri-drag-region
class={cn("h-12 select-none items-center justify-between gap-4 border-t px-2", className)}
>
<Avatar.Root class="p-2">
<Avatar.Image src="/favicon.png" alt="Kunkun Logo" class="select-none invert dark:invert-0" />
<Avatar.Root class="p-1.5">
<Avatar.Image
src="/favicon.png"
alt="Kunkun Logo"
class="h-full select-none invert dark:invert-0"
/>
</Avatar.Root>
<flex class="items-center gap-1">
{#if defaultAction}

View File

@ -8,10 +8,16 @@ import {
import * as v from "valibot"
export type BuiltinCmd = {
id: string
name: string
description: string
iconifyIcon: string
keywords?: string[]
function: () => Promise<void>
flags?: {
dev?: boolean // commands only available in dev mode
developer?: boolean // commands only available in developer mode
}
}
export type OnExtCmdSelect = (

1132
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff