feature: splashscreen (#36)

* feat: move all pages to app folder, add splashscreen

* feat: use Dance as splashscreen

* feat: add zoom in for splashscreen logo

* refactor: move svelte files into app folder

* fix: url prefix with /app

* refactor: remove platform-specific tauri conf

Merge windows back to main tauri config. The reason I separated them was
because I need decoration: true on mac and false on windows and linux.
Now I use tauri rust API to set decorations to false for win and linux.
This commit is contained in:
Huakun Shen 2024-12-21 04:04:05 -05:00 committed by GitHub
parent 80ad705f7c
commit caa252b4dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 437 additions and 390 deletions

View File

@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main*"], "windows": ["main*", "splashscreen"],
"permissions": [ "permissions": [
{ {
"identifier": "http:default", "identifier": "http:default",

View File

@ -53,7 +53,19 @@ impl<R: Runtime> WindowExt for WebviewWindow<R> {
} }
pub fn setup_window<R: Runtime>(app: &AppHandle<R>) { pub fn setup_window<R: Runtime>(app: &AppHandle<R>) {
let window = app.get_webview_window("main").unwrap();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
window.set_transparent_titlebar(true, true); {
let main_win = app.get_webview_window("main").unwrap();
main_win.set_transparent_titlebar(true, true);
let splashscreen_win = app.get_webview_window("splashscreen").unwrap();
splashscreen_win.set_transparent_titlebar(true, true);
}
#[cfg(not(target_os = "macos"))]
{
// on linux or windows, set decorations to false
let main_win = app.get_webview_window("main").unwrap();
main_win
.set_decorations(false)
.expect("Failed to set decorations");
}
} }

View File

@ -13,7 +13,23 @@
"macOSPrivateApi": true, "macOSPrivateApi": true,
"security": { "security": {
"csp": null "csp": null
} },
"windows": [
{
"hiddenTitle": true,
"url": "/app",
"title": "Kunkun",
"width": 800,
"visible": false,
"height": 600,
"decorations": true
},
{
"url": "/splashscreen",
"visible": false,
"label": "splashscreen"
}
]
}, },
"bundle": { "bundle": {
"createUpdaterArtifacts": true, "createUpdaterArtifacts": true,

View File

@ -1,14 +0,0 @@
{
"identifier": "sh.kunkun.desktop",
"app": {
"windows": [
{
"hiddenTitle": true,
"title": "Kunkun",
"width": 800,
"height": 600,
"decorations": false
}
]
}
}

View File

@ -1,13 +0,0 @@
{
"identifier": "sh.kunkun.desktop",
"app": {
"windows": [
{
"hiddenTitle": true,
"title": "Kunkun",
"width": 800,
"height": 600
}
]
}
}

View File

@ -1,14 +0,0 @@
{
"identifier": "sh.kunkun.desktop",
"app": {
"windows": [
{
"hiddenTitle": true,
"title": "Kunkun",
"width": 800,
"height": 600,
"decorations": false
}
]
}
}

View File

@ -13,7 +13,6 @@ import { derived } from "svelte/store"
import * as clipboard from "tauri-plugin-clipboard-api" import * as clipboard from "tauri-plugin-clipboard-api"
import { open } from "tauri-plugin-shellx-api" import { open } from "tauri-plugin-shellx-api"
import { v4 as uuidv4 } from "uuid" import { v4 as uuidv4 } from "uuid"
import { hexColor } from "valibot"
export const rawBuiltinCmds: BuiltinCmd[] = [ export const rawBuiltinCmds: BuiltinCmd[] = [
{ {
@ -25,7 +24,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
description: "Go to Extension Store", description: "Go to Extension Store",
function: async () => { function: async () => {
appState.clearSearchTerm() appState.clearSearchTerm()
goto("/extension/store") goto("/app/extension/store")
} }
}, },
{ {
@ -36,7 +35,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}, },
description: "", description: "",
function: async () => { function: async () => {
goto("/auth") goto("/app/auth")
} }
}, },
{ {
@ -73,6 +72,23 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}, 2_000) }, 2_000)
} }
}, },
{
name: "Splashscreen (Dev)",
icon: {
type: IconEnum.Iconify,
value: "material-symbols:skeleton"
},
description: "",
flags: {
dev: true
},
function: async () => {
new WebviewWindow(`splashscreen`, {
url: "/splashscreen"
})
appState.clearSearchTerm()
}
},
{ {
name: "File Transfer", name: "File Transfer",
icon: { icon: {
@ -81,7 +97,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}, },
description: "", description: "",
function: async () => { function: async () => {
goto("/extension/file-transfer") goto("/app/extension/file-transfer")
appState.clearSearchTerm() appState.clearSearchTerm()
} }
}, },
@ -95,7 +111,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
description: "", description: "",
function: async () => { function: async () => {
appState.clearSearchTerm() appState.clearSearchTerm()
goto("/settings/add-dev-extension") goto("/app/settings/add-dev-extension")
} }
}, },
{ {
@ -120,7 +136,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
function: async () => { function: async () => {
// const appStateStore = useAppStateStore() // const appStateStore = useAppStateStore()
appState.clearSearchTerm() appState.clearSearchTerm()
goto("/settings/set-dev-ext-path") goto("/app/settings/set-dev-ext-path")
} }
}, },
{ {
@ -132,11 +148,11 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
description: "", description: "",
function: async () => { function: async () => {
appState.clearSearchTerm() appState.clearSearchTerm()
// goto("/window-troubleshooter") // goto("/app/window-troubleshooter")
const winLabel = `main:extension-window-troubleshooter-${uuidv4()}` const winLabel = `main:extension-window-troubleshooter-${uuidv4()}`
console.log(winLabel) console.log(winLabel)
new WebviewWindow(winLabel, { new WebviewWindow(winLabel, {
url: "/troubleshooters/extension-window", url: "/app/troubleshooters/extension-window",
title: "Extension Window Troubleshooter" title: "Extension Window Troubleshooter"
}) })
}, },
@ -151,7 +167,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
description: "", description: "",
function: async () => { function: async () => {
appState.clearSearchTerm() appState.clearSearchTerm()
goto("/extension/permission-inspector") goto("/app/extension/permission-inspector")
}, },
keywords: ["extension"] keywords: ["extension"]
}, },
@ -164,7 +180,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
description: "", description: "",
function: async () => { function: async () => {
appState.clearSearchTerm() appState.clearSearchTerm()
goto("/troubleshooters/extension-loading") goto("/app/troubleshooters/extension-loading")
}, },
keywords: ["extension", "troubleshooter"] keywords: ["extension", "troubleshooter"]
}, },
@ -177,7 +193,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
description: "Create a Quicklink", description: "Create a Quicklink",
function: async () => { function: async () => {
appState.clearSearchTerm() appState.clearSearchTerm()
goto("/extension/create-quick-link") goto("/app/extension/create-quick-link")
} }
}, },
{ {
@ -188,7 +204,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}, },
description: "Open Settings", description: "Open Settings",
function: async () => { function: async () => {
goto("/settings") goto("/app/settings")
appState.clearSearchTerm() appState.clearSearchTerm()
} }
}, },
@ -248,7 +264,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}, },
description: "Dance", description: "Dance",
function: async () => { function: async () => {
goto("/dance") goto("/app/dance")
} }
}, },
{ {
@ -289,7 +305,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
description: "Clipboard History", description: "Clipboard History",
function: async () => { function: async () => {
appState.clearSearchTerm() appState.clearSearchTerm()
goto("/extension/clipboard") goto("/app/extension/clipboard")
} }
}, },
{ {
@ -306,7 +322,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
return return
} }
const window = new WebviewWindow(`main:pinned-screenshot-${uuidv4()}`, { const window = new WebviewWindow(`main:pinned-screenshot-${uuidv4()}`, {
url: "/extension/pin-screenshot", url: "/app/extension/pin-screenshot",
title: "Pinned Screenshot", title: "Pinned Screenshot",
hiddenTitle: true, hiddenTitle: true,
titleBarStyle: "transparent", titleBarStyle: "transparent",
@ -326,7 +342,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}, },
description: "MDNS Debugger", description: "MDNS Debugger",
function: async () => { function: async () => {
goto("/troubleshooters/mdns-debugger") goto("/app/troubleshooters/mdns-debugger")
}, },
flags: { flags: {
developer: true developer: true

View File

@ -24,7 +24,7 @@ export async function onTemplateUiCmdSelect(
) { ) {
await createExtSupportDir(ext.extPath) await createExtSupportDir(ext.extPath)
// console.log("onTemplateUiCmdSelect", ext, cmd, isDev, hmr) // console.log("onTemplateUiCmdSelect", ext, cmd, isDev, hmr)
const url = `/extension/ui-worker?extPath=${encodeURIComponent(ext.extPath)}&cmdName=${encodeURIComponent(cmd.name)}` const url = `/app/extension/ui-worker?extPath=${encodeURIComponent(ext.extPath)}&cmdName=${encodeURIComponent(cmd.name)}`
if (cmd.window) { if (cmd.window) {
const winLabel = await winExtMap.registerExtensionWithWindow({ extPath: ext.extPath }) const winLabel = await winExtMap.registerExtensionWithWindow({ extPath: ext.extPath })
const window = launchNewExtWindow(winLabel, url, cmd.window) const window = launchNewExtWindow(winLabel, url, cmd.window)
@ -52,7 +52,7 @@ export async function onCustomUiCmdSelect(
} else { } else {
url = decodeURIComponent(convertFileSrc(`${trimSlash(cmd.main)}`, "ext")) url = decodeURIComponent(convertFileSrc(`${trimSlash(cmd.main)}`, "ext"))
} }
let url2 = `/extension/ui-iframe?url=${encodeURIComponent(url)}&extPath=${encodeURIComponent(ext.extPath)}` let url2 = `/app/extension/ui-iframe?url=${encodeURIComponent(url)}&extPath=${encodeURIComponent(ext.extPath)}`
if (cmd.window) { if (cmd.window) {
const winLabel = await winExtMap.registerExtensionWithWindow({ const winLabel = await winExtMap.registerExtensionWithWindow({
extPath: ext.extPath, extPath: ext.extPath,
@ -61,7 +61,7 @@ export async function onCustomUiCmdSelect(
if (platform() === "windows" && !useDevMain) { if (platform() === "windows" && !useDevMain) {
const addr = await spawnExtensionFileServer(winLabel) const addr = await spawnExtensionFileServer(winLabel)
const newUrl = `http://${addr}` const newUrl = `http://${addr}`
url2 = `/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}` url2 = `/app/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}`
} }
const window = launchNewExtWindow(winLabel, url2, cmd.window) const window = launchNewExtWindow(winLabel, url2, cmd.window)
window.onCloseRequested(async (event) => { window.onCloseRequested(async (event) => {
@ -78,7 +78,7 @@ export async function onCustomUiCmdSelect(
const addr = await spawnExtensionFileServer(winLabel) // addr has format "127.0.0.1:<port>" const addr = await spawnExtensionFileServer(winLabel) // addr has format "127.0.0.1:<port>"
console.log("Extension file server address: ", addr) console.log("Extension file server address: ", addr)
const newUrl = `http://${addr}` const newUrl = `http://${addr}`
url2 = `/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}` url2 = `/app/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}`
} }
goto(url2) goto(url2)
} }

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { cn } from "@/utils"
import { GridAnimation } from "@kksh/ui" import { GridAnimation } from "@kksh/ui"
import { decompressFrame, decompressString, deserializeFrame } from "@kksh/utils" import { decompressFrame, decompressString, deserializeFrame } from "@kksh/utils"
import compressedDance from "$lib/../data/dance.bin?raw" import compressedDance from "$lib/../data/dance.bin?raw"
@ -7,11 +8,14 @@
const { fps, frames: rawFrames }: { fps: number; frames: string[] } = rawData const { fps, frames: rawFrames }: { fps: number; frames: string[] } = rawData
const decodedFrames = rawFrames.map((frame) => deserializeFrame(decompressFrame(frame))) const decodedFrames = rawFrames.map((frame) => deserializeFrame(decompressFrame(frame)))
let { scale = 1 } = $props() let { scale = 1, class: className }: { scale?: number; class?: string } = $props()
</script> </script>
<GridAnimation <GridAnimation
class="pointer-events-none max-h-full max-w-full select-none invert dark:invert-0" class={cn(
"pointer-events-none max-h-full max-w-full select-none invert dark:invert-0",
className
)}
{fps} {fps}
frames={decodedFrames} frames={decodedFrames}
{scale} {scale}

View File

@ -87,7 +87,7 @@
async function pickExtFiles() { async function pickExtFiles() {
if (!$appConfig.devExtensionPath) { if (!$appConfig.devExtensionPath) {
toast.warning("Please set the dev extension path in the settings") toast.warning("Please set the dev extension path in the settings")
return goto("/settings/set-dev-ext-path") return goto("/app/settings/set-dev-ext-path")
} }
const selected = await openFileSelector({ const selected = await openFileSelector({
directory: false, directory: false,

View File

@ -17,7 +17,7 @@
toast.warning( toast.warning(
"Please set the dev extension path in the settings to install tarball extension" "Please set the dev extension path in the settings to install tarball extension"
) )
return goto("/settings/set-dev-ext-path") return goto("/app/settings/set-dev-ext-path")
} }
await extensions await extensions
.installFromNpmPackageName(data.name, $appConfig.devExtensionPath) .installFromNpmPackageName(data.name, $appConfig.devExtensionPath)

View File

@ -19,7 +19,7 @@
toast.warning( toast.warning(
"Please set the dev extension path in the settings to install tarball extension" "Please set the dev extension path in the settings to install tarball extension"
) )
return goto("/settings/set-dev-ext-path") return goto("/app/settings/set-dev-ext-path")
} }
await extensions await extensions
.installFromTarballUrl(data.url, $appConfig.devExtensionPath) .installFromTarballUrl(data.url, $appConfig.devExtensionPath)

View File

@ -59,7 +59,7 @@ export async function handleKunkunProtocol(parsedUrl: URL) {
if (parsed.identifier) { if (parsed.identifier) {
goto(`/extension/store/${parsed.identifier}`) goto(`/extension/store/${parsed.identifier}`)
} else { } else {
goto("/extension/store") goto("/app/extension/store")
} }
} else if (href.startsWith(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)) { } else if (href.startsWith(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)) {
emitRefreshDevExt() emitRefreshDevExt()

View File

@ -76,7 +76,7 @@ export async function globalKeyDownHandler(e: KeyboardEvent) {
if ((_platform === "macos" && e.metaKey) || (_platform === "windows" && e.ctrlKey)) { if ((_platform === "macos" && e.metaKey) || (_platform === "windows" && e.ctrlKey)) {
if (e.key === ",") { if (e.key === ",") {
e.preventDefault() e.preventDefault()
goto("/settings") goto("/app/settings")
} }
} }
// Toggle Devtools with control + shift + I // Toggle Devtools with control + shift + I

View File

@ -7,7 +7,7 @@ export function goBack() {
} }
export function goHome() { export function goHome() {
goto("/") goto("/app/")
} }
export function goHomeOrCloseDependingOnWindow() { export function goHomeOrCloseDependingOnWindow() {

View File

@ -1,117 +1,11 @@
<script lang="ts"> <script lang="ts">
import AppContext from "@/components/context/AppContext.svelte"
import "../app.css" import "../app.css"
import { appConfig, appState, extensions, quickLinks, winExtMap } from "@/stores" import { ModeWatcher, ThemeWrapper } from "@kksh/svelte5"
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 { listenToKillProcessEvent, listenToRecordExtensionProcessEvent } from "@kksh/api/events"
import {
Button,
ModeWatcher,
themeConfigStore,
ThemeWrapper,
updateTheme,
type ThemeConfig
} from "@kksh/svelte5"
import { Constants, ViewTransition } from "@kksh/ui"
import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { attachConsole, error, info } 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 { toast, Toaster } from "svelte-sonner"
import * as shellx from "tauri-plugin-shellx-api"
/* -------------------------------------------------------------------------- */
/* 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() let { children } = $props()
const unlisteners: UnlistenFn[] = []
onDestroy(() => {
unlisteners.forEach((unlistener) => unlistener())
})
onMount(async () => {
attachConsole().then((unlistener) => unlisteners.push(unlistener))
initDeeplink().then((unlistener) => unlisteners.push(unlistener))
shellx
.fixPathEnv()
.then(() => {
info("fixed path env")
})
.catch(error)
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()
unlisteners.push(
await listenToRecordExtensionProcessEvent(async (event) => {
console.log("record extension process event", event)
winExtMap.registerProcess(event.payload.windowLabel, event.payload.pid)
})
)
unlisteners.push(
await listenToKillProcessEvent((event) => {
console.log("kill process event", event)
winExtMap.unregisterProcess(event.payload.pid)
})
)
} else {
}
getCurrentWebviewWindow().show()
})
</script> </script>
<svelte:window on:keydown={globalKeyDownHandler} />
<ViewTransition />
<ModeWatcher /> <ModeWatcher />
<Toaster richColors /> <ThemeWrapper>
<AppContext {appConfig} {appState}> {@render children()}
<ThemeWrapper> </ThemeWrapper>
{@render children()}
</ThemeWrapper>
</AppContext>

View File

@ -1,13 +1,5 @@
import { getExtensionsFolder, IS_IN_TAURI } from "@/constants"
import { error } from "@tauri-apps/plugin-log"
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
// so we will use adapter-static to prerender the app (SSG) // so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true export const prerender = true
export const ssr = false export const ssr = false
export const load: LayoutLoad = async () => {
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "" }
}

View File

@ -1,176 +1 @@
<!-- This file renders the main command palette, a list of commands --> <h1 class="text-red-500">Root</h1>
<script lang="ts">
import { commandLaunchers } from "@/cmds"
import { builtinCmds } from "@/cmds/builtin"
import { systemCommands } from "@/cmds/system"
import { appConfig, appState, devStoreExts, installedStoreExts, quickLinks } from "@/stores"
import { cmdQueries } from "@/stores/cmdQuery"
import { isKeyboardEventFromInputElement } from "@/utils/dom"
import Icon from "@iconify/svelte"
import { toggleDevTools } from "@kksh/api/commands"
import { Button, Command, DropdownMenu } from "@kksh/svelte5"
import {
BuiltinCmds,
CustomCommandInput,
ExtCmdsGroup,
GlobalCommandPaletteFooter,
QuickLinks,
SystemCmds
} from "@kksh/ui/main"
import type { CmdValue } from "@kksh/ui/types"
import { cn, commandScore } from "@kksh/ui/utils"
import { convertFileSrc } from "@tauri-apps/api/core"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { exit } from "@tauri-apps/plugin-process"
import { ArrowBigUpIcon, CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte"
import { onMount } from "svelte"
import { hasCommand, whereIsCommand } from "tauri-plugin-shellx-api"
let inputEle: HTMLInputElement | null = null
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
;(event.target as HTMLInputElement).value = ""
$appState.searchTerm = ""
}
}
// let imgSrc = convertFileSrc("/?id=15", "cbimg")
</script>
<svelte:window
on:keydown={(e) => {
if (e.key === "/") {
if (isKeyboardEventFromInputElement(e)) {
e.preventDefault()
} else {
e.preventDefault()
inputEle?.focus()
}
}
}}
/>
<!-- <pre>{imgSrc}</pre>
<img class="border-2 border-red-500 w-64" src={imgSrc} alt="test" /> -->
<Command.Root
class={cn("h-screen rounded-lg border shadow-md")}
bind:value={$appState.highlightedCmd}
filter={(value, search, keywords) => {
return commandScore(
value.startsWith("{") ? (JSON.parse(value) as CmdValue).cmdName : value,
search,
keywords
)
}}
loop
>
<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
class={cn("absolute flex space-x-2")}
style={`left: ${$appState.searchTerm.length + 3}ch`}
>
{#each $cmdQueries as cmdQuery}
{@const queryWidth = Math.max(cmdQuery.name.length, cmdQuery.value.length) + 2}
<input
class="bg-muted rounded-md border border-gray-300 pl-2 font-mono focus:outline-none dark:border-gray-600"
type="text"
placeholder={cmdQuery.name}
style={`width: ${queryWidth}ch`}
onkeydown={(evt) => {
if (evt.key === "Enter") {
evt.preventDefault()
evt.stopPropagation()
commandLaunchers.onQuickLinkSelect(
JSON.parse($appState.highlightedCmd),
$cmdQueries
)
}
}}
bind:value={cmdQuery.value}
/>
{/each}
</span>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline" size="icon"><EllipsisVerticalIcon /></Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-80">
<DropdownMenu.Group>
<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
><span class="flex items-center">⌃+<ArrowBigUpIcon class="h-4 w-4" />+I</span
></DropdownMenu.Shortcut
>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => location.reload()}>
<RefreshCcwIcon class="mr-2 h-4 w-4 text-green-500" />
Reload Window
<DropdownMenu.Shortcut
><span class="flex items-center">⌃+<ArrowBigUpIcon class="h-4 w-4" />+R</span
></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", { "text-green-500": $appConfig.hmr })}
/>
Toggle Dev Extension HMR
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/snippet}
</CustomCommandInput>
<Command.List class="max-h-screen grow">
<Command.Empty data-tauri-drag-region>No results found.</Command.Empty>
{#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0}
<ExtCmdsGroup
extensions={$devStoreExts}
heading="Dev Extensions"
isDev={true}
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
hmr={$appConfig.hmr}
/>
{/if}
{#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0}
<ExtCmdsGroup
extensions={$installedStoreExts}
heading="Extensions"
isDev={false}
hmr={false}
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
/>
{/if}
<QuickLinks quickLinks={$quickLinks} />
<BuiltinCmds builtinCmds={$builtinCmds} />
<SystemCmds {systemCommands} />
</Command.List>
<GlobalCommandPaletteFooter />
</Command.Root>

View File

@ -0,0 +1,107 @@
<script lang="ts">
import AppContext from "@/components/context/AppContext.svelte"
import { appConfig, appState, extensions, quickLinks, winExtMap } 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 { listenToKillProcessEvent, listenToRecordExtensionProcessEvent } from "@kksh/api/events"
import { Constants, ViewTransition } from "@kksh/ui"
import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { attachConsole, error, info } 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 { toast, Toaster } from "svelte-sonner"
import * as shellx from "tauri-plugin-shellx-api"
/* -------------------------------------------------------------------------- */
/* 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()
const unlisteners: UnlistenFn[] = []
onDestroy(() => {
unlisteners.forEach((unlistener) => unlistener())
})
onMount(async () => {
console.log("root layout onMount")
attachConsole().then((unlistener) => unlisteners.push(unlistener))
initDeeplink().then((unlistener) => unlisteners.push(unlistener))
shellx
.fixPathEnv()
.then(() => {
info("fixed path env")
})
.catch(error)
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()
unlisteners.push(
await listenToRecordExtensionProcessEvent(async (event) => {
console.log("record extension process event", event)
winExtMap.registerProcess(event.payload.windowLabel, event.payload.pid)
})
)
unlisteners.push(
await listenToKillProcessEvent((event) => {
console.log("kill process event", event)
winExtMap.unregisterProcess(event.payload.pid)
})
)
} else {
}
getCurrentWebviewWindow().show()
})
</script>
<svelte:window on:keydown={globalKeyDownHandler} />
<ViewTransition />
<Toaster richColors />
<AppContext {appConfig} {appState}>
{@render children()}
</AppContext>

View File

@ -0,0 +1,7 @@
import { getExtensionsFolder, IS_IN_TAURI } from "@/constants"
import { error } from "@tauri-apps/plugin-log"
import type { LayoutLoad } from "./$types"
export const load: LayoutLoad = async () => {
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "" }
}

View File

@ -0,0 +1,183 @@
<!-- This file renders the main command palette, a list of commands -->
<script lang="ts">
import { commandLaunchers } from "@/cmds"
import { builtinCmds } from "@/cmds/builtin"
import { systemCommands } from "@/cmds/system"
import { appConfig, appState, devStoreExts, installedStoreExts, quickLinks } from "@/stores"
import { cmdQueries } from "@/stores/cmdQuery"
import { isKeyboardEventFromInputElement } from "@/utils/dom"
import Icon from "@iconify/svelte"
import { toggleDevTools } from "@kksh/api/commands"
import { Button, Command, DropdownMenu } from "@kksh/svelte5"
import {
BuiltinCmds,
CustomCommandInput,
ExtCmdsGroup,
GlobalCommandPaletteFooter,
QuickLinks,
SystemCmds
} from "@kksh/ui/main"
import type { CmdValue } from "@kksh/ui/types"
import { cn, commandScore } from "@kksh/ui/utils"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { getCurrentWindow, Window } from "@tauri-apps/api/window"
import { exit } from "@tauri-apps/plugin-process"
import { ArrowBigUpIcon, CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte"
import { onMount } from "svelte"
import { hasCommand, whereIsCommand } from "tauri-plugin-shellx-api"
let inputEle: HTMLInputElement | null = $state(null)
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
;(event.target as HTMLInputElement).value = ""
$appState.searchTerm = ""
}
}
onMount(() => {
Promise.all([Window.getByLabel("splashscreen"), getCurrentWindow()]).then(
([splashscreenWin, mainWin]) => {
mainWin.show()
if (splashscreenWin) {
splashscreenWin.close()
}
}
)
})
</script>
<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}
filter={(value, search, keywords) => {
return commandScore(
value.startsWith("{") ? (JSON.parse(value) as CmdValue).cmdName : value,
search,
keywords
)
}}
loop
>
<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
class={cn("absolute flex space-x-2")}
style={`left: ${$appState.searchTerm.length + 3}ch`}
>
{#each $cmdQueries as cmdQuery}
{@const queryWidth = Math.max(cmdQuery.name.length, cmdQuery.value.length) + 2}
<input
class="bg-muted rounded-md border border-gray-300 pl-2 font-mono focus:outline-none dark:border-gray-600"
type="text"
placeholder={cmdQuery.name}
style={`width: ${queryWidth}ch`}
onkeydown={(evt) => {
if (evt.key === "Enter") {
evt.preventDefault()
evt.stopPropagation()
commandLaunchers.onQuickLinkSelect(
JSON.parse($appState.highlightedCmd),
$cmdQueries
)
}
}}
bind:value={cmdQuery.value}
/>
{/each}
</span>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline" size="icon"><EllipsisVerticalIcon /></Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-80">
<DropdownMenu.Group>
<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
><span class="flex items-center">⌃+<ArrowBigUpIcon class="h-4 w-4" />+I</span
></DropdownMenu.Shortcut
>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => location.reload()}>
<RefreshCcwIcon class="mr-2 h-4 w-4 text-green-500" />
Reload Window
<DropdownMenu.Shortcut
><span class="flex items-center">⌃+<ArrowBigUpIcon class="h-4 w-4" />+R</span
></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", { "text-green-500": $appConfig.hmr })}
/>
Toggle Dev Extension HMR
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/snippet}
</CustomCommandInput>
<Command.List class="max-h-screen grow">
<Command.Empty data-tauri-drag-region>No results found.</Command.Empty>
{#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0}
<ExtCmdsGroup
extensions={$devStoreExts}
heading="Dev Extensions"
isDev={true}
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
hmr={$appConfig.hmr}
/>
{/if}
{#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0}
<ExtCmdsGroup
extensions={$installedStoreExts}
heading="Extensions"
isDev={false}
hmr={false}
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
/>
{/if}
<QuickLinks quickLinks={$quickLinks} />
<BuiltinCmds builtinCmds={$builtinCmds} />
<SystemCmds {systemCommands} />
</Command.List>
<GlobalCommandPaletteFooter />
</Command.Root>

View File

@ -44,7 +44,7 @@
function onSignOut() { function onSignOut() {
auth auth
.signOut() .signOut()
.then(() => goto("/auth")) .then(() => goto("/app/auth"))
.catch((err) => toast.error("Failed to sign out", { description: err.message })) .catch((err) => toast.error("Failed to sign out", { description: err.message }))
} }
</script> </script>
@ -56,7 +56,7 @@
size="icon" size="icon"
onclick={() => { onclick={() => {
console.log("go Home") console.log("go Home")
goto("/") goto("/app/")
}} }}
> >
<ArrowLeft class="size-4" /> <ArrowLeft class="size-4" />

View File

@ -5,7 +5,7 @@
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Enter") { if (event.key === "Enter") {
goto("/") goto("/app/")
} }
} }
</script> </script>
@ -17,7 +17,7 @@
title="Fail to Load Extension" title="Fail to Load Extension"
class="w-fit max-w-screen-sm border-2 border-red-500" class="w-fit max-w-screen-sm border-2 border-red-500"
message={$page.error?.message ?? "Unknown Error"} message={$page.error?.message ?? "Unknown Error"}
onGoBack={() => goto("/")} onGoBack={() => goto("/app/")}
rawJsonError={JSON.stringify($page, null, 2)} rawJsonError={JSON.stringify($page, null, 2)}
/> />
</Layouts.Center> </Layouts.Center>

View File

@ -136,7 +136,7 @@
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") { if (e.key === "Escape") {
if (!delayedImageDialogOpen) { if (!delayedImageDialogOpen) {
goto("/extension/store") goto("/app/extension/store")
} }
} }
} }
@ -148,7 +148,7 @@
size="icon" size="icon"
class={cn("fixed left-3 top-3", Constants.CLASSNAMES.BACK_BUTTON)} class={cn("fixed left-3 top-3", Constants.CLASSNAMES.BACK_BUTTON)}
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON} data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
onclick={() => goto("/extension/store")} onclick={() => goto("/app/extension/store")}
> >
<ArrowLeftIcon /> <ArrowLeftIcon />
</Button> </Button>

View File

@ -51,7 +51,7 @@
const iframeUiAPI: IUiIframeServer2 = { const iframeUiAPI: IUiIframeServer2 = {
goBack: async () => { goBack: async () => {
if (isInMainWindow()) { if (isInMainWindow()) {
goto("/") goto("/app/")
} else { } else {
appWin.close() appWin.close()
} }

View File

@ -24,7 +24,7 @@ export const load: PageLoad = async ({
if (!_extPath || !_extUrl) { if (!_extPath || !_extUrl) {
toast.error("Invalid extension path or url") toast.error("Invalid extension path or url")
error("Invalid extension path or url") error("Invalid extension path or url")
goto("/") goto("/app/")
} }
const extPath = z.string().parse(_extPath) const extPath = z.string().parse(_extPath)
const extUrl = z.string().parse(_extUrl) const extUrl = z.string().parse(_extUrl)
@ -36,7 +36,7 @@ export const load: PageLoad = async ({
toast.error("Error loading extension manifest", { toast.error("Error loading extension manifest", {
description: `${err}` description: `${err}`
}) })
goto("/") goto("/app/")
} }
const loadedExt = _loadedExt! const loadedExt = _loadedExt!
const extInfoInDB = await db.getUniqueExtensionByPath(loadedExt.extPath) const extInfoInDB = await db.getUniqueExtensionByPath(loadedExt.extPath)
@ -44,7 +44,7 @@ export const load: PageLoad = async ({
toast.error("Unexpected Error", { toast.error("Unexpected Error", {
description: `Extension ${loadedExt.kunkun.identifier} not found in database. Run Troubleshooter.` description: `Extension ${loadedExt.kunkun.identifier} not found in database. Run Troubleshooter.`
}) })
goto("/") goto("/app/")
} }
return { extPath, url: extUrl, loadedExt, extInfoInDB: extInfoInDB! } return { extPath, url: extUrl, loadedExt, extInfoInDB: extInfoInDB! }
} }

View File

@ -63,7 +63,7 @@
async function goBack() { async function goBack() {
if (isInMainWindow()) { if (isInMainWindow()) {
goto("/") goto("/app/")
} else { } else {
appWin.close() appWin.close()
} }

View File

@ -27,7 +27,7 @@ export const load: PageLoad = async ({ url }) => {
if (!extPath || !cmdName) { if (!extPath || !cmdName) {
toast.error("Invalid extension path or url") toast.error("Invalid extension path or url")
error("Invalid extension path or url") error("Invalid extension path or url")
goto("/") goto("/app/")
} }
let _loadedExt: ExtPackageJsonExtra | undefined let _loadedExt: ExtPackageJsonExtra | undefined
@ -38,7 +38,7 @@ export const load: PageLoad = async ({ url }) => {
toast.error("Error loading extension manifest", { toast.error("Error loading extension manifest", {
description: `${err}` description: `${err}`
}) })
goto("/") goto("/app/")
} }
const loadedExt = _loadedExt! const loadedExt = _loadedExt!
const extInfoInDB = await db.getUniqueExtensionByPath(loadedExt.extPath) const extInfoInDB = await db.getUniqueExtensionByPath(loadedExt.extPath)
@ -46,7 +46,7 @@ export const load: PageLoad = async ({ url }) => {
toast.error("Unexpected Error", { toast.error("Unexpected Error", {
description: `Extension ${loadedExt.kunkun.identifier} not found in database. Run Troubleshooter.` description: `Extension ${loadedExt.kunkun.identifier} not found in database. Run Troubleshooter.`
}) })
goto("/") goto("/app/")
} }
const pkgJsonPath = await join(extPath!, "package.json") const pkgJsonPath = await join(extPath!, "package.json")
if (!(await exists(extPath!))) { if (!(await exists(extPath!))) {

View File

@ -14,32 +14,32 @@
const items = [ const items = [
{ {
title: "General", title: "General",
url: "/settings", url: "/app/settings",
icon: Cog icon: Cog
}, },
{ {
title: "Developer", title: "Developer",
url: "/settings/developer", url: "/app/settings/developer",
icon: SquareTerminal icon: SquareTerminal
}, },
{ {
title: "Extensions", title: "Extensions",
url: "/settings/extensions", url: "/app/settings/extensions",
icon: Blocks icon: Blocks
}, },
{ {
title: "Set Dev Extension", title: "Set Dev Extension",
url: "/settings/set-dev-ext-path", url: "/app/settings/set-dev-ext-path",
icon: Route icon: Route
}, },
{ {
title: "Add Dev Extension", title: "Add Dev Extension",
url: "/settings/add-dev-extension", url: "/app/settings/add-dev-extension",
icon: FileCode2 icon: FileCode2
}, },
{ {
title: "About", title: "About",
url: "/settings/about", url: "/app/settings/about",
icon: Info icon: Info
} }
] ]

View File

@ -11,17 +11,17 @@
const items = [ const items = [
{ {
title: "Extension Loading", title: "Extension Loading",
url: "/troubleshooters/extension-loading", url: "/app/troubleshooters/extension-loading",
icon: Loader icon: Loader
}, },
{ {
title: "Extension Window", title: "Extension Window",
url: "/troubleshooters/extension-window", url: "/app/troubleshooters/extension-window",
icon: AppWindow icon: AppWindow
}, },
{ {
title: "MDNS Debugger", title: "MDNS Debugger",
url: "/troubleshooters/mdns-debugger", url: "/app/troubleshooters/mdns-debugger",
icon: Network icon: Network
} }
] ]

View File

@ -0,0 +1,32 @@
<script lang="ts">
import { Layouts } from "@kksh/ui"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { onMount } from "svelte"
onMount(async () => {
const mainWin = await getCurrentWindow()
mainWin.show()
})
</script>
<Layouts.Center class="h-screen w-screen">
<div class="animate-zoom-in flex flex-col items-center justify-center gap-2 pb-20">
<img src="/favicon.png" alt="Logo" />
<h2 class="font-mono text-2xl font-extrabold">Kunkun</h2>
</div>
</Layouts.Center>
<style scoped>
.animate-zoom-in {
animation: zoom-in 0.2s ease-in-out;
}
@keyframes zoom-in {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
</style>