From 234f245a9ca45b395cf3ddeec37cc25d537d63a3 Mon Sep 17 00:00:00 2001 From: Huakun Date: Fri, 7 Mar 2025 13:00:15 -0500 Subject: [PATCH] Improve: add global loading screen (#237) * refactor(desktop): move ext loading code in store from +page.ts to +page.svelte try to solve blank screen on slow network * Revert "refactor(desktop): move ext loading code in store from +page.ts to +page.svelte" This reverts commit 4a0a695ce615cee695849c64746ba569680ff8c4. * feat(desktop): add full-screen loading state and border beam animation - Implement full-screen loading component with BorderBeam animation - Add fullScreenLoading flag to appState store - Update extension store pages to use full-screen loading - Add border beam animation to Tailwind config - Enhance page loading experience with visual feedback * feat(desktop): add dance animation to loading screen and update imports - Add Dance component to FullScreenLoading with subtle background effect - Remove unused fade transition import from layout - Update lz-string import in utils to use default import - Clean up compress test imports * feat(desktop): add back button to full-screen loading component - Import ArrowLeftIcon and Constants from @kksh/ui - Add back button with absolute positioning - Remove "Go Home" text button - Enhance loading screen with improved navigation * refactor(desktop): update BorderBeam component to use Svelte 5 runes --- .../components/animations/BorderBeam.svelte | 44 +++++++++++++ .../common/FullScreenLoading.svelte | 33 ++++++++++ apps/desktop/src/lib/stores/appState.ts | 7 +- apps/desktop/src/routes/+layout.svelte | 5 ++ .../routes/app/extension/store/+page.svelte | 37 ++++++++++- .../src/routes/app/extension/store/+page.ts | 64 ++++++++++-------- .../app/extension/store/[identifier]/+page.ts | 66 ++++++++++--------- apps/desktop/tailwind.config.ts | 8 ++- packages/types/src/appState.ts | 1 + .../ui/src/components/layouts/center.svelte | 3 +- packages/utils/__tests__/compress.test.ts | 1 - packages/utils/src/serde.ts | 4 +- 12 files changed, 208 insertions(+), 65 deletions(-) create mode 100644 apps/desktop/src/lib/components/animations/BorderBeam.svelte create mode 100644 apps/desktop/src/lib/components/common/FullScreenLoading.svelte diff --git a/apps/desktop/src/lib/components/animations/BorderBeam.svelte b/apps/desktop/src/lib/components/animations/BorderBeam.svelte new file mode 100644 index 0000000..62b4abb --- /dev/null +++ b/apps/desktop/src/lib/components/animations/BorderBeam.svelte @@ -0,0 +1,44 @@ + + +
diff --git a/apps/desktop/src/lib/components/common/FullScreenLoading.svelte b/apps/desktop/src/lib/components/common/FullScreenLoading.svelte new file mode 100644 index 0000000..0d1119d --- /dev/null +++ b/apps/desktop/src/lib/components/common/FullScreenLoading.svelte @@ -0,0 +1,33 @@ + + + + + + + Loading + + diff --git a/apps/desktop/src/lib/stores/appState.ts b/apps/desktop/src/lib/stores/appState.ts index 8e0e56e..4b0b437 100644 --- a/apps/desktop/src/lib/stores/appState.ts +++ b/apps/desktop/src/lib/stores/appState.ts @@ -8,7 +8,8 @@ export const defaultAppState: AppState = { loadingBar: false, defaultAction: "", actionPanel: undefined, - lockHideOnBlur: false // when dialog is open, we don't hide the app, we lock the hide on blur and unlock when dialog is closed + lockHideOnBlur: false, // when dialog is open, we don't hide the app, we lock the hide on blur and unlock when dialog is closed + fullScreenLoading: false } interface AppStateAPI { @@ -18,6 +19,7 @@ interface AppStateAPI { setDefaultAction: (defaultAction: string | null) => void setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void setLockHideOnBlur: (lockHideOnBlur: boolean) => void + setFullScreenLoading: (fullScreenLoading: boolean) => void } function createAppState(): Writable & AppStateAPI { @@ -40,6 +42,9 @@ function createAppState(): Writable & AppStateAPI { }, setLockHideOnBlur: (lockHideOnBlur: boolean) => { store.update((state) => ({ ...state, lockHideOnBlur })) + }, + setFullScreenLoading: (fullScreenLoading: boolean) => { + store.update((state) => ({ ...state, fullScreenLoading })) } } } diff --git a/apps/desktop/src/routes/+layout.svelte b/apps/desktop/src/routes/+layout.svelte index 98d1042..cd866f5 100644 --- a/apps/desktop/src/routes/+layout.svelte +++ b/apps/desktop/src/routes/+layout.svelte @@ -2,6 +2,8 @@ import { ParaglideJS } from "@inlang/paraglide-sveltekit" import { i18n } from "$lib/i18n" import "../app.css" + import FullScreenLoading from "@/components/common/FullScreenLoading.svelte" + import { appState } from "@/stores/appState" import { ModeWatcher, ThemeWrapper } from "@kksh/svelte5" import { Toaster } from "svelte-sonner" @@ -12,6 +14,9 @@ + {#if $appState.fullScreenLoading} + + {/if} {@render children()} diff --git a/apps/desktop/src/routes/app/extension/store/+page.svelte b/apps/desktop/src/routes/app/extension/store/+page.svelte index 4805a94..115d426 100644 --- a/apps/desktop/src/routes/app/extension/store/+page.svelte +++ b/apps/desktop/src/routes/app/extension/store/+page.svelte @@ -17,8 +17,9 @@ import { platform } from "@tauri-apps/plugin-os" import { goto } from "$app/navigation" import { ArrowLeft } from "lucide-svelte" - import type { Snippet } from "svelte" + import { onMount, type Snippet } from "svelte" import { toast } from "svelte-sonner" + import type { Action as SvelteAction } from "svelte/action" import { getInstallExtras } from "./[identifier]/helper.js" let { data } = $props() @@ -38,6 +39,38 @@ // ) // } + onMount(() => { + // setTimeout(() => { + // console.log("focus", listviewInputRef) + // listviewInputRef?.focus() + // }, 3_000) + }) + + const inputAction: SvelteAction = (node) => { + // the node has been mounted in the DOM + + $effect(() => { + // setup goes here + console.log("inputAction", node) + listviewInputRef?.focus() + return () => { + // teardown goes here + } + }) + } + + $effect(() => { + function docKeyDown(e: KeyboardEvent) { + if (e.key === "/") { + listviewInputRef?.focus() + } + } + document.addEventListener("keydown", docKeyDown) + return () => { + document.removeEventListener("keydown", docKeyDown) + } + }) + function onExtItemSelected(ext: SBExt) { goto(`./store/${ext.identifier}`) } @@ -142,7 +175,7 @@ { diff --git a/apps/desktop/src/routes/app/extension/store/+page.ts b/apps/desktop/src/routes/app/extension/store/+page.ts index c34e63f..7a8e681 100644 --- a/apps/desktop/src/routes/app/extension/store/+page.ts +++ b/apps/desktop/src/routes/app/extension/store/+page.ts @@ -1,43 +1,51 @@ -import { appConfig, extensions, installedStoreExts } from "@/stores" +import { appConfig, appState, extensions, installedStoreExts } from "@/stores" import { supabaseAPI } from "@/supabase" import type { ExtPackageJsonExtra } from "@kksh/api/models" import { isExtPathInDev, isUpgradable } from "@kksh/extension" import { SBExt } from "@kksh/supabase/models" +import { sleep } from "@kksh/utils" import { error } from "@sveltejs/kit" import { derived, get, type Readable } from "svelte/store" import type { PageLoad } from "./$types" -export const load: PageLoad = async (): Promise<{ +export const load: PageLoad = (): Promise<{ storeExtList: SBExt[] installedStoreExts: Readable installedExtsMap: Readable> upgradableExpsMap: Readable> }> => { - const storeExtList = await supabaseAPI.getExtList() - // map identifier to extItem - const storeExtsMap = Object.fromEntries(storeExtList.map((ext) => [ext.identifier, ext])) - const _appConfig = get(appConfig) - // const installedStoreExts = derived(extensions, ($extensions) => { - // if (!_appConfig.extensionPath) return [] - // return $extensions.filter((ext) => !isExtPathInDev(_appConfig.extensionPath!, ext.extPath)) - // }) - // map installed extension identifier to version - const installedExtsMap = derived(installedStoreExts, ($exts) => - Object.fromEntries($exts.map((ext) => [ext.kunkun.identifier, ext.version])) - ) - const upgradableExpsMap = derived(installedStoreExts, ($exts) => - Object.fromEntries( - $exts.map((ext) => { - const dbExt: SBExt | undefined = storeExtsMap[ext.kunkun.identifier] - return [ext.kunkun.identifier, dbExt ? isUpgradable(dbExt, ext.version) : false] - }) - ) - ) + appState.setFullScreenLoading(true) + return supabaseAPI + .getExtList() + .then(async (storeExtList) => { + // map identifier to extItem + const storeExtsMap = Object.fromEntries(storeExtList.map((ext) => [ext.identifier, ext])) + // const _appConfig = get(appConfig) + // const installedStoreExts = derived(extensions, ($extensions) => { + // if (!_appConfig.extensionPath) return [] + // return $extensions.filter((ext) => !isExtPathInDev(_appConfig.extensionPath!, ext.extPath)) + // }) + // map installed extension identifier to version + const installedExtsMap = derived(installedStoreExts, ($exts) => + Object.fromEntries($exts.map((ext) => [ext.kunkun.identifier, ext.version])) + ) + const upgradableExpsMap = derived(installedStoreExts, ($exts) => + Object.fromEntries( + $exts.map((ext) => { + const dbExt: SBExt | undefined = storeExtsMap[ext.kunkun.identifier] + return [ext.kunkun.identifier, dbExt ? isUpgradable(dbExt, ext.version) : false] + }) + ) + ) - return { - storeExtList, - installedStoreExts, - installedExtsMap, - upgradableExpsMap - } + return { + storeExtList, + installedStoreExts, + installedExtsMap, + upgradableExpsMap + } + }) + .finally(() => { + appState.setFullScreenLoading(false) + }) } diff --git a/apps/desktop/src/routes/app/extension/store/[identifier]/+page.ts b/apps/desktop/src/routes/app/extension/store/[identifier]/+page.ts index 9314102..9994a21 100644 --- a/apps/desktop/src/routes/app/extension/store/[identifier]/+page.ts +++ b/apps/desktop/src/routes/app/extension/store/[identifier]/+page.ts @@ -1,14 +1,15 @@ -import { extensions } from "@/stores" +import { appState, extensions } from "@/stores" import { supabaseAPI } from "@/supabase" import { KunkunExtManifest, type ExtPackageJsonExtra } from "@kksh/api/models" import { ExtPublishMetadata } from "@kksh/supabase/models" import type { Tables } from "@kksh/supabase/types" +import { sleep } from "@kksh/utils" import { error } from "@sveltejs/kit" import { toast } from "svelte-sonner" import * as v from "valibot" import type { PageLoad } from "./$types" -export const load: PageLoad = async ({ +export const load: PageLoad = ({ params }): Promise<{ extPublish: Tables<"ext_publish"> & { metadata: ExtPublishMetadata } @@ -18,36 +19,41 @@ export const load: PageLoad = async ({ identifier: string } }> => { - const { error: dbError, data: extPublish } = await supabaseAPI.getLatestExtPublish( - params.identifier - ) - const metadataParse = v.safeParse(ExtPublishMetadata, extPublish?.metadata ?? {}) - if (dbError) { - return error(400, { - message: dbError.message - }) - } - const metadata = metadataParse.success ? metadataParse.output : {} - const parseManifest = v.safeParse(KunkunExtManifest, extPublish.manifest) - if (!parseManifest.success) { - const errMsg = "Invalid extension manifest, you may need to upgrade your app." - toast.error(errMsg) - throw error(400, errMsg) - } + appState.setFullScreenLoading(true) + return supabaseAPI + .getLatestExtPublish(params.identifier) + .then(async ({ error: dbError, data: extPublish }) => { + const metadataParse = v.safeParse(ExtPublishMetadata, extPublish?.metadata ?? {}) + if (dbError) { + return error(400, { + message: dbError.message + }) + } + const metadata = metadataParse.success ? metadataParse.output : {} + const parseManifest = v.safeParse(KunkunExtManifest, extPublish.manifest) + if (!parseManifest.success) { + const errMsg = "Invalid extension manifest, you may need to upgrade your app." + toast.error(errMsg) + throw error(400, errMsg) + } - const { data: ext, error: extError } = await supabaseAPI.getExtension(params.identifier) - if (extError) { - return error(400, { - message: extError.message - }) - } + const { data: ext, error: extError } = await supabaseAPI.getExtension(params.identifier) + if (extError) { + return error(400, { + message: extError.message + }) + } - return { - extPublish: { ...extPublish, metadata }, - ext, - params, - manifest: parseManifest.output - } + return { + extPublish: { ...extPublish, metadata }, + ext, + params, + manifest: parseManifest.output + } + }) + .finally(() => { + appState.setFullScreenLoading(false) + }) } export const csr = true diff --git a/apps/desktop/tailwind.config.ts b/apps/desktop/tailwind.config.ts index b290bd7..1ac6cb6 100644 --- a/apps/desktop/tailwind.config.ts +++ b/apps/desktop/tailwind.config.ts @@ -86,12 +86,18 @@ const config: Config = { "caret-blink": { "0%,70%,100%": { opacity: "1" }, "20%,50%": { opacity: "0" } + }, + "border-beam": { + "100%": { + "offset-distance": "100%" + } } }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", - "caret-blink": "caret-blink 1.25s ease-out infinite" + "caret-blink": "caret-blink 1.25s ease-out infinite", + "border-beam": "border-beam calc(var(--duration)*1s) infinite linear" } } }, diff --git a/packages/types/src/appState.ts b/packages/types/src/appState.ts index e6e04f6..969249a 100644 --- a/packages/types/src/appState.ts +++ b/packages/types/src/appState.ts @@ -7,4 +7,5 @@ export interface AppState { defaultAction: string | null actionPanel?: ActionSchema.ActionPanel lockHideOnBlur: boolean + fullScreenLoading: boolean } diff --git a/packages/ui/src/components/layouts/center.svelte b/packages/ui/src/components/layouts/center.svelte index 8cd1456..ca540a4 100644 --- a/packages/ui/src/components/layouts/center.svelte +++ b/packages/ui/src/components/layouts/center.svelte @@ -1,6 +1,7 @@ -
+
{@render children?.()}
diff --git a/packages/utils/__tests__/compress.test.ts b/packages/utils/__tests__/compress.test.ts index c0c3017..d9095c6 100644 --- a/packages/utils/__tests__/compress.test.ts +++ b/packages/utils/__tests__/compress.test.ts @@ -1,5 +1,4 @@ import { expect, test } from "bun:test" -import { compress, decompress } from "lz-string" import { compressString, decompressString } from "../src" test("decompressString", async () => { diff --git a/packages/utils/src/serde.ts b/packages/utils/src/serde.ts index bd0b9fb..42fdd4d 100644 --- a/packages/utils/src/serde.ts +++ b/packages/utils/src/serde.ts @@ -1,4 +1,6 @@ -import { compressToBase64, decompressFromBase64 } from "lz-string" +import lzString from "lz-string" + +const { compressToBase64, decompressFromBase64 } = lzString /** * This file contains the deserialization and compression functions I designed for the grid animation.