web


git clone https://radroots.dev/git/web.git
Log | Files | Refs | Submodules | README | LICENSE

commit 89560e49edaf408f5bf70411396536ff0471721a
parent b9699d53fbd6e95c91801de347718155665d2e30
Author: triesap <triesap@radroots.dev>
Date:   Sat, 27 Dec 2025 15:06:43 +0000

app: harden startup flows and expand PWA caching

- Guard db/geocoder startup with idempotent promise tracking
- Add cache-first service worker with app-shell fallback and range support
- Refactor setup carousel/view stack state and tighten error handling
- Wire new workspace packages and env example files into tooling

Diffstat:
M.gitignore | 2++
Mapp/package.json | 1+
Mapp/src/app.css | 2+-
Mapp/src/lib/utils/app/cipher.ts | 8++++----
Mapp/src/lib/utils/app/handlers.ts | 4++--
Mapp/src/lib/utils/app/index.ts | 30++++++++++++++++++++++++++++++
Mapp/src/lib/utils/backup/export.ts | 4++--
Mapp/src/lib/utils/backup/import.ts | 6+++---
Mapp/src/routes/(app)/+layout.svelte | 4++--
Mapp/src/routes/(app)/farms/+page.svelte | 4++--
Mapp/src/routes/(app)/profile/+page.svelte | 6+++---
Mapp/src/routes/(app)/profile/edit/+page.svelte | 2+-
Mapp/src/routes/(cfg)/setup/+page.svelte | 1353++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mapp/src/routes/+layout.svelte | 33++++++++++++++++++++++++---------
Mapp/src/service-worker.js | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpackage.json | 4++--
Mpnpm-lock.yaml | 61++++++++++++++++++++++++++++++++++++++++++++++---------------
17 files changed, 963 insertions(+), 653 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -16,6 +16,8 @@ logs/ .env .env.* !.env.example +!.env.development.example +!.env.production.example !.env.test .local* justfile diff --git a/app/package.json b/app/package.json @@ -41,6 +41,7 @@ "@radroots/http": "workspace:*", "@radroots/geocoder": "workspace:*", "@radroots/locales": "workspace:*", + "@radroots/nfc": "workspace:*", "@radroots/nostr": "workspace:*", "@radroots/tangle-schema-bindings": "workspace:*", "@radroots/themes": "workspace:*", diff --git a/app/src/app.css b/app/src/app.css @@ -20,7 +20,7 @@ } @source "./**/*.{svelte,ts}"; -@source "../../packages/lib-app/src/**/*.{svelte,ts}"; +@source "../../packages/apps-lib/src/**/*.{svelte,ts}"; @source "../../packages/apps-lib-pwa/src/**/*.{svelte,ts}"; @theme { diff --git a/app/src/lib/utils/app/cipher.ts b/app/src/lib/utils/app/cipher.ts @@ -1,4 +1,4 @@ -import { WebAesGcmCipher, type WebAesGcmCipherConfig } from "@radroots/client/keystore"; +import { WebAesGcmCipher, type WebAesGcmCipherConfig } from "@radroots/client/cipher"; import { cfg_data } from "../config"; const sql_cipher_config = (store_key: string): WebAesGcmCipherConfig => ({ @@ -8,5 +8,6 @@ const sql_cipher_config = (store_key: string): WebAesGcmCipherConfig => ({ export const reset_sql_cipher = async (store_key: string): Promise<void> => { const cipher = new WebAesGcmCipher(sql_cipher_config(store_key)); - await cipher.reset(); -}; -\ No newline at end of file + const res = await cipher.reset(); + if (res && "err" in res) throw new Error(res.err); +}; diff --git a/app/src/lib/utils/app/handlers.ts b/app/src/lib/utils/app/handlers.ts @@ -1,7 +1,7 @@ import { theme_mode, type LocalCallbackColorMode, type LocalCallbackGeocode, type LocalCallbackGeocodeCurrent, type LocalCallbackGuiAlert, type LocalCallbackGuiConfirm, type LocalCallbackImgBin, type LocalCallbackPhotosAddMultiple, type LocalCallbackPhotosUpload } from "@radroots/apps-lib"; import { parse_theme_mode } from "@radroots/themes"; import { throw_err } from "@radroots/utils"; -import { fs, geoc, geol, http, notif } from "."; +import { fs, geoc, geoc_init, geol, http, notif } from "."; type PhotoUploadResponse = { res_base: string; @@ -30,7 +30,7 @@ export const lc_gui_confirm: LocalCallbackGuiConfirm = async (opts) => { }; export const lc_geocode: LocalCallbackGeocode = async (geoc_p) => { - await geoc.connect(); + await geoc_init(); const geoc_res = await geoc.reverse(geoc_p); if ("err" in geoc_res) throw_err(geoc_res); return geoc_res.results[0] || undefined; diff --git a/app/src/lib/utils/app/index.ts b/app/src/lib/utils/app/index.ts @@ -52,6 +52,36 @@ export const db = new WebTangleDatabase({ }); let db_i: Promise<WebTangleDatabase> | null = null; +let db_init_promise: Promise<void> | null = null; +let geoc_init_promise: Promise<void> | null = null; + +export const db_init = async (): Promise<void> => { + if (!db_init_promise) { + db_init_promise = (async () => { + await db.init(); + })(); + } + try { + await db_init_promise; + } catch (e) { + db_init_promise = null; + throw e; + } +}; + +export const geoc_init = async (): Promise<void> => { + if (!geoc_init_promise) { + geoc_init_promise = (async () => { + await geoc.connect(); + })(); + } + try { + await geoc_init_promise; + } catch (e) { + geoc_init_promise = null; + throw e; + } +}; export const create_db = async (): Promise<WebTangleDatabase> => { if (!db_i) { diff --git a/app/src/lib/utils/backup/export.ts b/app/src/lib/utils/backup/export.ts @@ -1,4 +1,4 @@ -import { datastore, db, nostr_keys, notif } from "$lib/utils/app"; +import { datastore, db, db_init, nostr_keys, notif } from "$lib/utils/app"; import { ls } from "$lib/utils/i18n"; import { download_json, get_store, handle_err } from "@radroots/apps-lib"; import type { ExportedAppState } from "@radroots/apps-lib-pwa/types/app"; @@ -53,7 +53,7 @@ const export_nostr_keystore_state = async (): Promise<ExportedAppState["nostr_ke }; const export_tangle_db_state = async (): Promise<ExportedAppState["database"]> => { - await db.init(); + await db_init(); const store_key = db.get_store_key(); const backup = await db.export_backup(); if ("err" in backup) throw_err(backup); diff --git a/app/src/lib/utils/backup/import.ts b/app/src/lib/utils/backup/import.ts @@ -35,9 +35,9 @@ const assert_config_match = ( }; export const validate_import_file = async (file: File | null): Promise<ImportableAppState> => { - const parsed: any = await parse_file_json(file) - if (!parsed) throw_err(ls_val(`error.configuration.import.invalid_file_contents`)) - return await validate_import_state(parsed); + const parsed_res = await parse_file_json(file); + if (!parsed_res.ok) throw_err(ls_val(`error.configuration.import.invalid_file_contents`)); + return await validate_import_state(parsed_res.value); }; export const validate_import_state = async (state: any): Promise<ImportableAppState> => { diff --git a/app/src/routes/(app)/+layout.svelte b/app/src/routes/(app)/+layout.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { db, nostr_keys } from "$lib/utils/app"; + import { db_init, nostr_keys } from "$lib/utils/app"; import { nostr_login_nip01 } from "@radroots/apps-nostr"; import { nostr_context_default, nostr_relays_clear, nostr_relays_open } from "@radroots/nostr"; import { handle_err, throw_err } from "@radroots/utils"; @@ -21,7 +21,7 @@ }); const init = async (): Promise<void> => { - await db.init(); + await db_init(); }; const nostr_init = async (): Promise<void> => { diff --git a/app/src/routes/(app)/farms/+page.svelte b/app/src/routes/(app)/farms/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { db, route } from "$lib/utils/app"; + import { db, db_init, route } from "$lib/utils/app"; import { handle_err } from "@radroots/apps-lib"; import { Farms } from "@radroots/apps-lib-pwa"; import type { @@ -13,7 +13,7 @@ let data: LoadData = $state(undefined); onMount(async () => { - await db.init(); + await db_init(); data = await load_data(); }); diff --git a/app/src/routes/(app)/profile/+page.svelte b/app/src/routes/(app)/profile/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { db, fs, nostr_keys, notif, radroots, route } from "$lib/utils/app"; + import { db, db_init, fs, nostr_keys, notif, radroots, route } from "$lib/utils/app"; import { ls } from "$lib/utils/i18n"; import { parse_file_path } from "@radroots/apps-lib"; import { nostr_pubkey } from "@radroots/apps-nostr"; @@ -16,7 +16,7 @@ onMount(async () => { try { // await init(); - await db.init(); + await db_init(); data = await load_data(); } catch (e) { handle_err(e, `on_mount`); @@ -25,7 +25,7 @@ }); const init = async (): Promise<void> => { - await db.init(); + await db_init(); }; const load_data = async (): Promise<IViewProfileData | undefined> => { diff --git a/app/src/routes/(app)/profile/edit/+page.svelte b/app/src/routes/(app)/profile/edit/+page.svelte @@ -4,7 +4,7 @@ import { ProfileEdit } from "@radroots/apps-lib-pwa"; import { qp_field, qp_keynostr } from "@radroots/apps-lib-pwa/stores/app"; import type { IViewProfileEditData } from "@radroots/apps-lib-pwa/types/views/profile"; - import { parse_view_profile_field_key } from "@radroots/apps-lib-pwa/utils/profile/lib"; + import { parse_view_profile_field_key } from "@radroots/apps-lib-pwa/utils/profile"; import { handle_err, throw_err } from "@radroots/utils"; import { onMount } from "svelte"; diff --git a/app/src/routes/(cfg)/setup/+page.svelte b/app/src/routes/(cfg)/setup/+page.svelte @@ -17,16 +17,17 @@ import { ls } from "$lib/utils/i18n"; import { get_default_nostr_relays } from "$lib/utils/nostr/lib"; import { + carousel_create, carousel_dec, carousel_inc, - casl_i, - casl_imax, + carousel_init, el_id, Fade, fmt_id, Glyph, sleep, - view_effect, + ViewPane, + ViewStack, } from "@radroots/apps-lib"; import { ButtonLayoutPair, @@ -49,6 +50,8 @@ } from "@radroots/utils"; import { onMount } from "svelte"; + type View = "cfg_key" | "cfg_profile" | "eula"; + const page_carousel: Record<View, { max_index: number }> = { cfg_key: { max_index: 2, @@ -61,16 +64,60 @@ }, }; - type View = "cfg_key" | "cfg_profile" | "eula"; - let view: View = $state("cfg_key"); - $effect(() => { - view_effect<View>(view); + const carousel_cfg_key = carousel_create({ + view: "cfg_key", + max_index: page_carousel.cfg_key.max_index, }); + const carousel_cfg_profile = carousel_create({ + view: "cfg_profile", + max_index: page_carousel.cfg_profile.max_index, + }); + + const carousel_eula = carousel_create({ + view: "eula", + max_index: page_carousel.eula.max_index, + }); + + const view_carousel = { + cfg_key: carousel_cfg_key, + cfg_profile: carousel_cfg_profile, + eula: carousel_eula, + }; + + const carousel_cfg_profile_index = carousel_cfg_profile.index; + + let view: View = $state("cfg_key"); + let cfg_role: AppConfigRole | undefined = $state(undefined); type CfgKeyOpt = "nostr_key_gen" | "nostr_key_add"; let cgf_key_opt: CfgKeyOpt | undefined = $state(undefined); + type CfgKeyStep = "intro" | "choice" | "add_existing"; + let cfg_key_step: CfgKeyStep = $state("intro"); + + const cfg_key_step_index = (step: CfgKeyStep): number => { + switch (step) { + case "intro": + return 0; + case "choice": + return 1; + case "add_existing": + return 2; + } + }; + + $effect(() => { + console.log(`view `, view); + console.log(`cfg_key_step `, cfg_key_step); + }); + + const cfg_key_step_for_index = (index: number): CfgKeyStep => { + if (index <= 0) return "intro"; + if (index === 1) return "choice"; + return "add_existing"; + }; + let nostr_key_add_val = $state(``); let profile_name_val = $state(``); @@ -81,6 +128,33 @@ let is_eula_scrolled = $state(false); let is_loading_s = $state(false); + const reset_local_state = (): void => { + cfg_role = undefined; + cgf_key_opt = undefined; + cfg_key_step = "intro"; + nostr_key_add_val = ``; + profile_name_val = ``; + profile_name_valid = false; + profile_name_nip05 = true; + profile_name_loading = false; + is_eula_scrolled = false; + is_loading_s = false; + }; + + const sync_carousel = (new_view: View, index: number): void => { + const carousel = view_carousel[new_view]; + carousel_init(carousel, { + index, + max_index: page_carousel[new_view].max_index, + }); + }; + + const set_cfg_key_step = (step: CfgKeyStep): void => { + cfg_key_step = step; + // @todo confirm why this was breaking the correct... if (step === "choice" && !cgf_key_opt) cgf_key_opt = "nostr_key_gen"; + sync_carousel("cfg_key", cfg_key_step_index(step)); + }; + onMount(async () => { try { await page_init(); @@ -90,6 +164,7 @@ }); const page_init = async (): Promise<void> => { + reset_local_state(); const nostr_keys_all = await nostr_keys.keys(); if ("results" in nostr_keys_all) { const confirm = await notif.confirm({ @@ -106,8 +181,9 @@ return; } await page_reset(); + return; } - handle_view(view); + handle_view(`cfg_key`, { index: 0 }); }; const page_reset = async ( @@ -117,7 +193,8 @@ try { console.log(`[config] page reset`); app_loading.set(!prevent_loading); - handle_view(`cfg_key`); + reset_local_state(); + handle_view(`cfg_key`, { index: 0 }); if (alert_message) await notif.alert(alert_message); await sleep(cfg_delay.load); await nostr_keys.reset(); @@ -142,17 +219,26 @@ if (scroll_top + client_h >= scroll_h) is_eula_scrolled = true; }; - const handle_view = (new_view: View): void => { - if (new_view === "cfg_key" && view === "cfg_profile") { - const offset = cgf_key_opt === "nostr_key_gen" ? 1 : 0; - casl_i.set(page_carousel[new_view].max_index - offset); - } else { - casl_i.set(0); - casl_imax.set(page_carousel[new_view].max_index); + const handle_view = (new_view: View, opts?: { index?: number }): void => { + let next_index = opts?.index ?? 0; + if (new_view === "cfg_key") { + if (opts?.index !== undefined) + cfg_key_step = cfg_key_step_for_index(opts.index); + else if (view === "cfg_profile") + cfg_key_step = + cgf_key_opt === "nostr_key_add" ? "add_existing" : "choice"; + if (cfg_key_step === "choice" && !cgf_key_opt) + cgf_key_opt = "nostr_key_gen"; + next_index = cfg_key_step_index(cfg_key_step); } view = new_view; + sync_carousel(new_view, next_index); }; + $effect(() => { + console.log(`cgf_key_opt `, cgf_key_opt); + }); + const handle_config_err = async ( err?: IError<string> | string, ): Promise<void> => { @@ -166,16 +252,16 @@ const handle_continue = async (): Promise<void> => { switch (view) { case `cfg_key`: - switch ($casl_i) { - case 0: - return await carousel_inc(view); - case 1: + switch (cfg_key_step) { + case "intro": + return set_cfg_key_step("choice"); + case "choice": return handle_new_key_or_add(); - case 2: + case "add_existing": return handle_key_add_existing(); } case `cfg_profile`: - switch ($casl_i) { + switch ($carousel_cfg_profile_index) { case 0: return handle_setup_profile(); case 1: @@ -187,29 +273,45 @@ const handle_new_key_or_add = async (): Promise<void> => { try { if (cgf_key_opt === `nostr_key_add`) - return void (await carousel_inc(view)); - await create_nostr_key(); + return set_cfg_key_step("add_existing"); + const key_created = await create_nostr_key(); + if (!key_created) return; handle_view(`cfg_profile`); } catch (e) { handle_err(e, `handle_new_key_or_add`); } }; - const create_nostr_key = async (): Promise<void> => { + const create_nostr_key = async (): Promise<boolean> => { const keys_nostr_gen = await nostr_keys.generate(); - if ("err" in keys_nostr_gen) return handle_config_err(); - await datastore.update_obj<ConfigData>("cfg_data", { + if ("err" in keys_nostr_gen) { + await handle_config_err(keys_nostr_gen); + return false; + } + const cfg_update = await datastore.update_obj<ConfigData>("cfg_data", { nostr_public_key: keys_nostr_gen.public_key, }); + if ("err" in cfg_update) { + await handle_config_err(cfg_update); + return false; + } + return true; }; - const add_nostr_key = async (secret_key: string): Promise<void> => { + const add_nostr_key = async (secret_key: string): Promise<boolean> => { const keys_nostr_add = await nostr_keys.add(secret_key); - if ("err" in keys_nostr_add) - return void (await notif.alert(`${$ls(`common.invalid_key`)}`)); - await datastore.update_obj<ConfigData>("cfg_data", { + if ("err" in keys_nostr_add) { + await notif.alert(`${$ls(`common.invalid_key`)}`); + return false; + } + const cfg_update = await datastore.update_obj<ConfigData>("cfg_data", { nostr_public_key: keys_nostr_add.public_key, }); + if ("err" in cfg_update) { + await handle_config_err(cfg_update); + return false; + } + return true; }; const handle_key_add_existing = async (): Promise<void> => { @@ -227,7 +329,8 @@ value: `${$ls(`common.nostr_key`)}`.toLowerCase(), })}`, )); - await add_nostr_key(secret_key); + const key_added = await add_nostr_key(secret_key); + if (!key_added) return; nostr_key_add_val = ``; handle_view(`cfg_profile`); } catch (e) { @@ -243,13 +346,15 @@ const handle_setup_profile = async (): Promise<void> => { try { if (profile_name_loading) return; - + //@ const ds_cfg_data = await datastore.get_obj<ConfigData>("cfg_data"); + console.log(`ds_cfg_data `, ds_cfg_data); if ("err" in ds_cfg_data) return handle_config_err(); const ks_nostr_key = await nostr_keys.read( ds_cfg_data.result.nostr_public_key, ); + console.log(`ks_nostr_key `, ks_nostr_key); if ("err" in ks_nostr_key) return handle_config_err(); if (profile_name_nip05) { @@ -307,7 +412,8 @@ } if (!profile_name_val) { - const confirm = handle_add_profile_without_name_confirmation(); + const confirm = + await handle_add_profile_without_name_confirmation(); if (!confirm) return void el_id(fmt_id(`nostr:profile`))?.focus(); } @@ -318,7 +424,7 @@ }); } - await carousel_inc(view); + await carousel_inc(view_carousel[view]); } catch (e) { handle_err(e, `handle_setup_profile`); } finally { @@ -346,18 +452,18 @@ const handle_back = async (): Promise<void> => { switch (view) { case `cfg_key`: - switch ($casl_i) { - case 1: { + switch (cfg_key_step) { + case "choice": { cgf_key_opt = undefined; - return await carousel_dec(view); + return set_cfg_key_step("intro"); } - case 2: { + case "add_existing": { nostr_key_add_val = ``; - return await carousel_dec(view); + return set_cfg_key_step("choice"); } } case `cfg_profile`: - switch ($casl_i) { + switch ($carousel_cfg_profile_index) { case 0: { if (!profile_name_val) { const confirm = @@ -366,12 +472,12 @@ return void el_id( fmt_id(`nostr:profile`), )?.focus(); - return void carousel_inc(view); + return void carousel_inc(view_carousel[view]); } return handle_view(`cfg_key`); } case 1: - return carousel_dec(view); + return carousel_dec(view_carousel[view]); } } }; @@ -458,9 +564,13 @@ await datastore.del_obj("cfg_data"); return { pass: true }; }; + + $effect(() => { + console.log(`view `, view); + }); </script> -{#if view === "cfg_key" && $casl_i > 0} +{#if view === "cfg_key" && cfg_key_step !== "intro"} <Fade basis={{ classes: `z-10 absolute top-8 right-6` }}> <SelectMenu basis={{ @@ -496,629 +606,674 @@ </Fade> {/if} -<div - data-view={`cfg_key`} - class={`flex flex-col h-full w-full justify-start items-center`} +<ViewStack + basis={{ + active_view: view, + }} > - <CarouselContainer - basis={{ - view: `cfg_key`, - }} - > - <CarouselItem - basis={{ - view: `cfg_key`, - classes: `justify-center items-center`, - }} - > - <div - class={`relative flex flex-col h-full w-full justify-center items-center`} + <ViewPane basis={{ view: "cfg_key" }}> + <div class={`flex flex-col h-full w-full justify-start items-center`}> + <CarouselContainer + basis={{ + carousel: carousel_cfg_key, + }} > - <div - class={`flex flex-row w-full justify-start items-center -translate-y-16`} - > - <button - class={`flex flex-row w-full justify-center items-center`} - onclick={async () => { - await goto(`/`); - }} - > - <LogoCircle /> - </button> - </div> - <div - class={`absolute bottom-0 left-0 flex flex-col h-[20rem] w-full px-10 gap-2 justify-start items-center`} + <CarouselItem + basis={{ + classes: `justify-center items-center`, + }} > <div - class={`flex flex-row w-full justify-start items-center`} + class={`relative flex flex-col h-full w-full justify-center items-center`} > - <p - class={`font-sans font-[400] text-sm text-ly0-gl-label uppercase`} + <div + class={`flex flex-row w-full justify-start items-center -translate-y-16`} > - {`${$ls(`common.configure`)}`} - </p> + <button + class={`flex flex-row w-full justify-center items-center`} + onclick={async () => { + await goto(`/`); + }} + > + <LogoCircle /> + </button> + </div> + <div + class={`absolute bottom-0 left-0 flex flex-col h-[20rem] w-full px-10 gap-2 justify-start items-center`} + > + <div + class={`flex flex-row w-full justify-start items-center`} + > + <p + class={`font-sans font-[400] text-sm text-ly0-gl-label uppercase`} + > + {`${$ls(`common.configure`)}`} + </p> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-center`} + > + <div + class={`flex flex-row w-full justify-start items-center`} + > + <p + class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`} + > + {`${$ls(`notification.init.greeting_header`)}`} + </p> + </div> + <div + class={`flex flex-row w-full justify-start items-center`} + > + <p + class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`} + > + {`${$ls( + `notification.init.greeting_subheader`, + )}.`} + </p> + </div> + </div> + </div> </div> + </CarouselItem> + <CarouselItem + basis={{ + classes: `justify-center items-center`, + }} + > <div - class={`flex flex-col w-full gap-2 justify-start items-center`} + class={`flex flex-col h-[16rem] gap-8 w-full justify-start items-center`} > <div - class={`flex flex-row w-full justify-start items-center`} + class={`flex flex-row w-full justify-center items-center`} > <p - class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`} + class={`font-sans font-[600] text-ly0-gl text-3xl`} > - {`${$ls(`notification.init.greeting_header`)}`} + {`${$ls(`icu.configure_*`, { + value: `${$ls(`common.device`)}`, + })}`} </p> </div> <div - class={`flex flex-row w-full justify-start items-center`} + class={`flex flex-col w-full gap-6 justify-center items-center`} > - <p - class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`} + <button + class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ + cgf_key_opt === `nostr_key_gen` + ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` + : `bg-ly1` + } el-re`} + onclick={async (ev) => { + ev.stopPropagation(); + cgf_key_opt = `nostr_key_gen`; + }} > - {`${$ls( - `notification.init.greeting_subheader`, - )}.`} - </p> + <p + class={`font-sans font-[600] text-ly0-gl text-xl`} + > + {`${$ls(`icu.create_new_*`, { + value: `${$ls( + `common.keypair`, + )}`.toLowerCase(), + })}`} + </p> + </button> + <button + class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ + cgf_key_opt === `nostr_key_add` + ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` + : `bg-ly1` + } el-re`} + onclick={async (ev) => { + ev.stopPropagation(); + cgf_key_opt = `nostr_key_add`; + }} + > + <p + class={`font-sans font-[600] text-ly0-gl text-xl`} + > + {`${$ls(`icu.use_existing_*`, { + value: `${$ls( + `common.keypair`, + )}`.toLowerCase(), + })}`} + </p> + </button> </div> </div> - </div> - </div> - </CarouselItem> - <CarouselItem - basis={{ - view: `cfg_key`, - classes: `justify-center items-center`, - role: `button`, - tabindex: 0, - callback_click: async () => { - cgf_key_opt = undefined; - }, - }} - > - <div - class={`flex flex-col h-[16rem] gap-8 w-full justify-start items-center`} - > - <div class={`flex flex-row w-full justify-center items-center`}> - <p class={`font-sans font-[600] text-ly0-gl text-3xl`}> - {`${$ls(`icu.configure_*`, { - value: `${$ls(`common.device`)}`, - })}`} - </p> - </div> - <div - class={`flex flex-col w-full gap-6 justify-center items-center`} + </CarouselItem> + <CarouselItem + basis={{ + classes: `justify-center items-center`, + }} > - <button - class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ - cgf_key_opt === `nostr_key_gen` - ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` - : `bg-ly1` - } el-re`} - onclick={async (ev) => { - ev.stopPropagation(); - cgf_key_opt = `nostr_key_gen`; - }} - > - <p class={`font-sans font-[600] text-ly0-gl text-xl`}> - {`${$ls(`icu.create_new_*`, { - value: `${$ls(`common.keypair`)}`.toLowerCase(), - })}`} - </p> - </button> - <button - class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ - cgf_key_opt === `nostr_key_add` - ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` - : `bg-ly1` - } el-re`} - onclick={async (ev) => { - ev.stopPropagation(); - cgf_key_opt = `nostr_key_add`; - }} + <div + class={`flex flex-col w-full gap-8 justify-start items-center`} > - <p class={`font-sans font-[600] text-ly0-gl text-xl`}> - {`${$ls(`icu.use_existing_*`, { - value: `${$ls(`common.keypair`)}`.toLowerCase(), - })}`} - </p> - </button> - </div> - </div> - </CarouselItem> - <CarouselItem - basis={{ - view: `cfg_key`, - classes: `justify-center items-center`, - }} - > - <div - class={`flex flex-col w-full gap-8 justify-start items-center`} - > + <div + class={`flex flex-col w-full gap-6 justify-center items-center`} + > + <p + class={`font-sans font-[600] text-ly0-gl text-3xl capitalize`} + > + {`${$ls(`icu.add_existing_*`, { + value: `${$ls(`common.key`)}`.toLowerCase(), + })}`} + </p> + <EntryLine + bind:value={nostr_key_add_val} + basis={{ + wrap: { + layer: 1, + classes: `w-lo_${$app_lo}`, + style: `guide`, + }, + el: { + classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`, + layer: 1, + placeholder: `${$ls(`icu.enter_*`, { + value: `${$ls( + `common.nostr_nsec_hex`, + )}`, + })}`, + callback_keydown: async ({ + key_s, + el, + }) => { + if (key_s) { + el.blur(); + handle_continue(); + } + }, + }, + }} + /> + </div> + </div> + </CarouselItem> <div - class={`flex flex-col w-full gap-6 justify-center items-center`} + class={`z-10 absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`} > - <p - class={`font-sans font-[600] text-ly0-gl text-3xl capitalize`} - > - {`${$ls(`icu.add_existing_*`, { - value: `${$ls(`common.key`)}`.toLowerCase(), - })}`} - </p> - <EntryLine - bind:value={nostr_key_add_val} + <ButtonLayoutPair basis={{ - wrap: { - layer: 1, - classes: `w-lo_${$app_lo}`, - style: `guide`, + continue: { + label: `${$ls(`common.continue`)}`, + disabled: + cfg_key_step === "choice" && !cgf_key_opt, + callback: async () => handle_continue(), }, - el: { - classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`, - layer: 1, - placeholder: `${$ls(`icu.enter_*`, { - value: `${$ls(`common.nostr_nsec_hex`)}`, - })}`, - callback_keydown: async ({ key_s, el }) => { - if (key_s) { - el.blur(); - handle_continue(); - } - }, + back: { + label: `${$ls(`common.back`)}`, + visible: cfg_key_step !== "intro", + callback: async () => handle_back(), }, }} /> </div> - </div> - </CarouselItem> - <div - class={`z-10 absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`} - > - <ButtonLayoutPair - basis={{ - continue: { - label: `${$ls(`common.continue`)}`, - disabled: $casl_i === 1 && !cgf_key_opt, - callback: async () => handle_continue(), - }, - back: { - label: `${$ls(`common.back`)}`, - visible: $casl_i > 0, - callback: async () => handle_back(), - }, - }} - /> + </CarouselContainer> </div> - </CarouselContainer> -</div> + </ViewPane> -<div - data-view={`cfg_profile`} - class={`hidden flex flex-col h-full w-full justify-start items-center`} -> - <CarouselContainer - basis={{ - view: `cfg_profile`, - }} - > - <CarouselItem - basis={{ - view: `cfg_profile`, - classes: `justify-center items-center`, - }} - > - <div - class={`flex flex-col h-[16rem] w-full px-4 gap-6 justify-start items-center`} + <ViewPane basis={{ view: "cfg_profile" }}> + <div class={`flex flex-col h-full w-full justify-start items-center`}> + <CarouselContainer + basis={{ + carousel: carousel_cfg_profile, + }} > - <p class={`font-sans font-[600] text-ly0-gl text-3xl`}> - {`${$ls(`icu.add_*`, { - value: `${$ls(`common.profile`)}`, - })}`} - </p> - <div - class={`flex flex-col w-full gap-4 justify-center items-center`} + <CarouselItem + basis={{ + classes: `justify-center items-center`, + }} > - <EntryLine - bind:value={profile_name_val} - basis={{ - loading: profile_name_loading, - wrap: { - layer: 1, - classes: `w-lo_${$app_lo}`, - style: `guide`, - }, - el: { - classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`, - id: fmt_id(`nostr:profile`), - layer: 1, - placeholder: `${$ls(`icu.enter_*`, { - value: `${$ls( - `common.profile_name`, - )}`.toLowerCase(), - })}`, - field: form_fields.profile_name, - callback: async ({ pass }) => { - profile_name_valid = pass; - }, - callback_keydown: async ({ key_s, el }) => { - if (key_s) { - el.blur(); - handle_continue(); - } - }, - }, - }} - /> <div - class={`flex flex-row w-full gap-2 justify-center items-center`} + class={`flex flex-col h-[16rem] w-full px-4 gap-6 justify-start items-center`} > - <input - type="checkbox" - bind:checked={profile_name_nip05} - /> - <button - class={`flex flex-row justify-center items-center`} - onclick={async () => { - profile_name_nip05 = !profile_name_nip05; - }} + <p class={`font-sans font-[600] text-ly0-gl text-3xl`}> + {`${$ls(`icu.add_*`, { + value: `${$ls(`common.profile`)}`, + })}`} + </p> + <div + class={`flex flex-col w-full gap-4 justify-center items-center`} > - <p - class={`font-sans font-[400] text-ly0-gl text-[14px] tracking-wide`} + <EntryLine + bind:value={profile_name_val} + basis={{ + loading: profile_name_loading, + wrap: { + layer: 1, + classes: `w-lo_${$app_lo}`, + style: `guide`, + }, + el: { + classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`, + id: fmt_id(`nostr:profile`), + layer: 1, + placeholder: `${$ls(`icu.enter_*`, { + value: `${$ls( + `common.profile_name`, + )}`.toLowerCase(), + })}`, + field: form_fields.profile_name, + callback: async ({ pass }) => { + profile_name_valid = pass; + }, + callback_keydown: async ({ + key_s, + el, + }) => { + if (key_s) { + el.blur(); + handle_continue(); + } + }, + }, + }} + /> + <div + class={`flex flex-row w-full gap-2 justify-center items-center`} > - {`${$ls(`common.create`)}`} - <span - class={`font-mono font-[500] tracking-tight px-[3px]`} + <input + type="checkbox" + bind:checked={profile_name_nip05} + /> + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + profile_name_nip05 = + !profile_name_nip05; + }} > - {`@radroots`} - </span> - {`${$ls(`common.nip05_address`)}`} - </p> - </button> + <p + class={`font-sans font-[400] text-ly0-gl text-[14px] tracking-wide`} + > + {`${$ls(`common.create`)}`} + <span + class={`font-mono font-[500] tracking-tight px-[3px]`} + > + {`@radroots`} + </span> + {`${$ls(`common.nip05_address`)}`} + </p> + </button> + </div> + </div> </div> - </div> - </div> - </CarouselItem> - <CarouselItem - basis={{ - view: `cfg_profile`, - classes: `justify-center items-center`, - role: `button`, - tabindex: 0, - callback_click: async () => { - cfg_role = undefined; - }, - }} - > - <div - class={`flex flex-col h-[16rem] w-full gap-10 justify-start items-center`} - > - <div class={`flex flex-row w-full justify-center items-center`}> - <p class={`font-sans font-[600] text-ly0-gl text-3xl`}> - {`${$ls(`common.setup_for_farmer`)}`} - </p> - </div> - <div - class={`flex flex-col w-full gap-5 justify-center items-center`} - > - <button - class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ - cfg_role === `farmer` - ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` - : `bg-ly1` - } el-re`} - onclick={async (ev) => { - ev.stopPropagation(); - cfg_role = `farmer`; - }} - > - <p class={`font-sans font-[600] text-ly0-gl text-xl`}> - {`${$ls(`common.yes`)}`} - </p> - </button> - <button - class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ - cfg_role === `personal` - ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` - : `bg-ly1` - } el-re`} - onclick={async (ev) => { - ev.stopPropagation(); - cfg_role = `personal`; - }} - > - <p class={`font-sans font-[600] text-ly0-gl text-xl`}> - {`${$ls(`common.no`)}`} - </p> - </button> - </div> - </div> - </CarouselItem> - </CarouselContainer> - <div - class={`absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`} - > - <ButtonLayoutPair - basis={{ - continue: { - label: `${$ls(`common.continue`)}`, - disabled: $casl_i === 1 && !cfg_role, - callback: async () => handle_continue(), - }, - back: { - visible: true, - label: - view === "cfg_profile" && - $casl_i === 0 && - !profile_name_val - ? `${$ls(`common.skip`)}` - : `${$ls(`common.back`)}`, - callback: handle_back, - }, - }} - /> - </div> -</div> - -<div - data-view={`eula`} - class={`hidden flex flex-col h-full w-full ios0:pt-12 pt-24 justify-start items-center`} -> - <CarouselContainer - basis={{ - view: `eula`, - classes: `rounded-2xl scroll-hide`, - }} - > - <CarouselItem - basis={{ - view: `eula`, - classes: `justify-start items-center`, - }} - > - <div - class={`flex flex-col h-full w-full px-4 justify-start items-center ${ - view === `eula` ? `fade-in-long` : `` - }`} - > - <div - class={`flex flex-col w-full px-4 gap-4 justify-start items-center`} + </CarouselItem> + <CarouselItem + basis={{ + classes: `justify-center items-center`, + role: `button`, + tabindex: 0, + callback_click: async () => { + cfg_role = undefined; + }, + }} > <div - class={`flex flex-row w-full ios0:pt-8 justify-center items-center`} - > - <p class={`font-mono font-[600] text-ly0-gl text-2xl`}> - {`${$ls(`eula.title`)}`} - </p> - </div> - <div - class={`flex flex-col ios0:h-[26rem] ios1:h-[38rem] w-full gap-6 justify-start items-center overflow-y-scroll scroll-hide`} - onscroll={on_scroll_eula} + class={`flex flex-col h-[16rem] w-full gap-10 justify-start items-center`} > <div - class={`flex flex-col w-full gap-2 justify-start items-start`} + class={`flex flex-row w-full justify-center items-center`} > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls(`eula.introduction.title`)}**`} - </p> <p - class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + class={`font-sans font-[600] text-ly0-gl text-3xl`} > - {`${$ls(`eula.introduction.body`)}`} + {`${$ls(`common.setup_for_farmer`)}`} </p> </div> <div - class={`flex flex-col w-full gap-2 justify-start items-start`} + class={`flex flex-col w-full gap-5 justify-center items-center`} > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls(`eula.prohibited_content.title`)}**`} - </p> - <p - class={`font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + <button + class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ + cfg_role === `farmer` + ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` + : `bg-ly1` + } el-re`} + onclick={async (ev) => { + ev.stopPropagation(); + cfg_role = `farmer`; + }} > - {`${$ls( - `eula.prohibited_content.body_0_title`, - )}`} - </p> - <div - class={`flex flex-col w-full justify-start items-start`} + <p + class={`font-sans font-[600] text-ly0-gl text-xl`} + > + {`${$ls(`common.yes`)}`} + </p> + </button> + <button + class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${ + cfg_role === `personal` + ? `ly1-apply-active ly1-raise-apply ly1-ring-apply` + : `bg-ly1` + } el-re`} + onclick={async (ev) => { + ev.stopPropagation(); + cfg_role = `personal`; + }} > - {#each [0, 1, 2, 3, 4, 5] as li} - <div - class={`flex flex-row w-full justify-start items-center`} - > - <div - class={`flex flex-row h-full w-8 justify-start items-start`} - > - <p - class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} - > - {`*`} - </p> - </div> - <div - class={`flex flex-row h-full w-full justify-start items-start`} - > - <p - class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} - > - {`${$ls( - `eula.prohibited_content.body_li_0_${li}`, - )}`} - </p> - </div> - </div> - {/each} - </div> + <p + class={`font-sans font-[600] text-ly0-gl text-xl`} + > + {`${$ls(`common.no`)}`} + </p> + </button> </div> + </div> + </CarouselItem> + </CarouselContainer> + <div + class={`absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`} + > + <ButtonLayoutPair + basis={{ + continue: { + label: `${$ls(`common.continue`)}`, + disabled: + $carousel_cfg_profile_index === 1 && !cfg_role, + callback: async () => handle_continue(), + }, + back: { + visible: true, + label: + view === "cfg_profile" && + $carousel_cfg_profile_index === 0 && + !profile_name_val + ? `${$ls(`common.skip`)}` + : `${$ls(`common.back`)}`, + callback: handle_back, + }, + }} + /> + </div> + </div> + </ViewPane> + + <ViewPane basis={{ view: "eula" }}> + <div + class={`flex flex-col h-full w-full ios0:pt-12 pt-24 justify-start items-center`} + > + <CarouselContainer + basis={{ + carousel: carousel_eula, + classes: `rounded-2xl scroll-hide`, + }} + > + <CarouselItem + basis={{ + classes: `justify-start items-center`, + }} + > + <div + class={`flex flex-col h-full w-full px-4 justify-start items-center ${ + view === `eula` ? `fade-in-long` : `` + }`} + > <div - class={`flex flex-col w-full gap-2 justify-start items-start`} + class={`flex flex-col w-full px-4 gap-4 justify-start items-center`} > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls(`eula.prohibited_conduct.title`)}**`} - </p> <div - class={`flex flex-col w-full justify-start items-start`} + class={`flex flex-row w-full ios0:pt-8 justify-center items-center`} + > + <p + class={`font-mono font-[600] text-ly0-gl text-2xl`} + > + {`${$ls(`eula.title`)}`} + </p> + </div> + <div + class={`flex flex-col ios0:h-[26rem] ios1:h-[38rem] w-full gap-6 justify-start items-center overflow-y-scroll scroll-hide`} + onscroll={on_scroll_eula} > - {#each [0, 1, 2, 3] as li} + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls(`eula.introduction.title`)}**`} + </p> + <p + class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + > + {`${$ls(`eula.introduction.body`)}`} + </p> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls(`eula.prohibited_content.title`)}**`} + </p> + <p + class={`font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + > + {`${$ls( + `eula.prohibited_content.body_0_title`, + )}`} + </p> <div - class={`flex flex-row w-full justify-start items-center`} + class={`flex flex-col w-full justify-start items-start`} > - <div - class={`flex flex-row h-full w-8 justify-start items-start`} - > - <p - class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + {#each [0, 1, 2, 3, 4, 5] as li} + <div + class={`flex flex-row w-full justify-start items-center`} > - {`*`} - </p> - </div> - <div - class={`flex flex-row h-full w-full justify-start items-start`} - > - <p - class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + <div + class={`flex flex-row h-full w-8 justify-start items-start`} + > + <p + class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + > + {`*`} + </p> + </div> + <div + class={`flex flex-row h-full w-full justify-start items-start`} + > + <p + class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + > + {`${$ls( + `eula.prohibited_content.body_li_0_${li}`, + )}`} + </p> + </div> + </div> + {/each} + </div> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls(`eula.prohibited_conduct.title`)}**`} + </p> + <div + class={`flex flex-col w-full justify-start items-start`} + > + {#each [0, 1, 2, 3] as li} + <div + class={`flex flex-row w-full justify-start items-center`} > - {`${$ls( - `eula.prohibited_conduct.body_li_0_${li}`, - )}`} - </p> - </div> + <div + class={`flex flex-row h-full w-8 justify-start items-start`} + > + <p + class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + > + {`*`} + </p> + </div> + <div + class={`flex flex-row h-full w-full justify-start items-start`} + > + <p + class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`} + > + {`${$ls( + `eula.prohibited_conduct.body_li_0_${li}`, + )}`} + </p> + </div> + </div> + {/each} </div> - {/each} + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls( + `eula.consequences_of_violation.title`, + )}**`} + </p> + <p + class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + > + {`${$ls( + `eula.consequences_of_violation.body`, + )}`} + </p> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls(`eula.disclaimer.title`)}**`} + </p> + <p + class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + > + {`${$ls(`eula.disclaimer.body`)}`} + </p> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls(`eula.changes.title`)}**`} + </p> + <p + class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + > + {`${$ls(`eula.changes.body`)}`} + </p> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls(`eula.contact.title`)}**`} + </p> + <p + class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + > + {`${$ls(`eula.contact.body`)}`} + </p> + </div> + <div + class={`flex flex-col w-full gap-2 justify-start items-start`} + > + <p + class={`font-mono font-[600] text-ly0-gl`} + > + {`**${$ls(`eula.acceptance_of_terms.title`)}**`} + </p> + <p + class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + > + {`${$ls(`eula.acceptance_of_terms.body`)}`} + </p> + </div> </div> </div> <div - class={`flex flex-col w-full gap-2 justify-start items-start`} - > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls( - `eula.consequences_of_violation.title`, - )}**`} - </p> - <p - class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} - > - {`${$ls( - `eula.consequences_of_violation.body`, - )}`} - </p> - </div> - <div - class={`flex flex-col w-full gap-2 justify-start items-start`} + class={`flex flex-row w-full ios0:pt-8 pt-6 justify-center items-center`} > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls(`eula.disclaimer.title`)}**`} - </p> - <p - class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} - > - {`${$ls(`eula.disclaimer.body`)}`} - </p> - </div> - <div - class={`flex flex-col w-full gap-2 justify-start items-start`} - > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls(`eula.changes.title`)}**`} - </p> - <p - class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} - > - {`${$ls(`eula.changes.body`)}`} - </p> - </div> - <div - class={`flex flex-col w-full gap-2 justify-start items-start`} - > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls(`eula.contact.title`)}**`} - </p> - <p - class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + <button + class={`group flex flex-row basis-1/2 gap-4 justify-center items-center ${ + is_eula_scrolled ? `` : `opacity-80` + }`} + onclick={async () => { + const confirm = await notif.confirm({ + message: `${$ls( + `eula.error.required`, + )}`, + cancel: `${$ls(`common.quit`)}`, + }); + + if (confirm === false) + await page_reset(undefined, true); + }} > - {`${$ls(`eula.contact.body`)}`} - </p> - </div> - <div - class={`flex flex-col w-full gap-2 justify-start items-start`} - > - <p class={`font-mono font-[600] text-ly0-gl`}> - {`**${$ls(`eula.acceptance_of_terms.title`)}**`} - </p> - <p - class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`} + <p + class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`} + > + {`-`} + </p> + <p + class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`} + > + {`${`${$ls(`common.disagree`)}`}`} + </p> + <p + class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`} + > + {`-`} + </p> + </button> + <button + class={`relative group flex flex-row basis-1/2 gap-4 justify-center items-center el-re ${ + is_eula_scrolled ? `` : `opacity-40` + }`} + onclick={async () => { + if (is_eula_scrolled) await submit(); + }} > - {`${$ls(`eula.acceptance_of_terms.body`)}`} - </p> + <p + class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`} + > + {`-`} + </p> + <p + class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`} + > + {`${`${$ls(`common.agree`)}`}`} + </p> + <p + class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`} + > + {`- `} + </p> + {#if is_loading_s} + <div + class={`absolute right-3 flex flex-row justify-start items-center`} + > + <LoadSymbol basis={{ dim: `xs` }} /> + </div> + {/if} + </button> </div> </div> - </div> - <div - class={`flex flex-row w-full ios0:pt-8 pt-6 justify-center items-center`} - > - <button - class={`group flex flex-row basis-1/2 gap-4 justify-center items-center ${ - is_eula_scrolled ? `` : `opacity-80` - }`} - onclick={async () => { - const confirm = await notif.confirm({ - message: `${$ls(`eula.error.required`)}`, - cancel: `${$ls(`common.quit`)}`, - }); - - if (confirm === false) - await page_reset(undefined, true); - }} - > - <p - class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`} - > - {`-`} - </p> - <p - class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`} - > - {`${`${$ls(`common.disagree`)}`}`} - </p> - <p - class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`} - > - {`-`} - </p> - </button> - <button - class={`relative group flex flex-row basis-1/2 gap-4 justify-center items-center el-re ${ - is_eula_scrolled ? `` : `opacity-40` - }`} - onclick={async () => { - if (is_eula_scrolled) await submit(); - }} - > - <p - class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`} - > - {`-`} - </p> - <p - class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`} - > - {`${`${$ls(`common.agree`)}`}`} - </p> - <p - class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`} - > - {`- `} - </p> - {#if is_loading_s} - <div - class={`absolute right-3 flex flex-row justify-start items-center`} - > - <LoadSymbol basis={{ dim: `xs` }} /> - </div> - {/if} - </button> - </div> - </div> - </CarouselItem> - </CarouselContainer> -</div> + </CarouselItem> + </CarouselContainer> + </div> + </ViewPane> +</ViewStack> diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import { dev, version as kit_version } from "$app/environment"; import { page } from "$app/state"; - import { db, geoc } from "$lib/utils/app"; + import { db_init, geoc_init } from "$lib/utils/app"; import { app_cfg } from "$lib/utils/app/config"; import { lc_color_mode, @@ -23,7 +23,8 @@ } from "@radroots/apps-lib"; import { Css, LayoutWindow } from "@radroots/apps-lib-pwa"; import { app_lo } from "@radroots/apps-lib-pwa/stores/app"; - import { cfg_app } from "@radroots/apps-lib-pwa/utils/app"; + import type { LibContext } from "@radroots/apps-lib-pwa/types/context"; + import { CFG_APP } from "@radroots/apps-lib-pwa/utils/app"; import { parse_theme_key, parse_theme_mode } from "@radroots/themes"; import { str_cap_words } from "@radroots/utils"; import "css-paint-polyfill"; @@ -36,7 +37,7 @@ content: string; }; - const head_meta_tags: MetaTag[] = [ + const HEAD_META_TAGS: MetaTag[] = [ { name: "app_version", content: app_cfg.version, @@ -57,7 +58,7 @@ let { children }: LayoutProps = $props(); - set_context("lib", { + const LIB_CONTEXT: LibContext = { ls, locale, lc_color_mode, @@ -68,7 +69,9 @@ lc_img_bin, lc_photos_add, lc_photos_upload, - }); + }; + + set_context("lib", LIB_CONTEXT); theme_mode.subscribe((_theme_mode) => theme_set(parse_theme_key($theme_key), parse_theme_mode(_theme_mode)), @@ -79,16 +82,28 @@ ); win_h.subscribe((_win_h) => { - if (_win_h > cfg_app.layout.ios1.h) { + if (_win_h > CFG_APP.layout.ios1.h) { app_lo.set("ios1"); } else { app_lo.set("ios0"); } }); + const register_service_worker = async (): Promise<void> => { + if (dev) return; + if (!("serviceWorker" in navigator)) return; + try { + await navigator.serviceWorker.register("/service-worker.js"); + await navigator.serviceWorker.ready; + } catch { + return; + } + }; + onMount(async () => { - await db.init(); - await geoc.connect(); + await db_init(); + await geoc_init(); + await register_service_worker(); }); const format_title = (title: string): string => { @@ -100,7 +115,7 @@ <svelte:head> <title>{`${head_title || "Home"} | Rad Roots`}</title> - {#each head_meta_tags as meta_tag (meta_tag.name)} + {#each HEAD_META_TAGS as meta_tag (meta_tag.name)} <meta name={meta_tag.name} content={meta_tag.content} /> {/each} </svelte:head> diff --git a/app/src/service-worker.js b/app/src/service-worker.js @@ -1,9 +1,86 @@ -import { build, files, prerendered, version } from '$service-worker' -//import { precacheAndRoute } from 'workbox-precaching' +import { build, files, prerendered, version } from "$service-worker"; -const precache_list = [...build, ...files, ...prerendered].map((s) => ({ - url: s, - revision: version, -})) +const APP_SHELL_URL = "/index.html"; +const PRECACHE_URLS = [...new Set([...build, ...files, ...prerendered, APP_SHELL_URL])].filter( + (url) => !url.includes("/.") +); +const PRECACHE_LIST = PRECACHE_URLS.map((url) => ({ + url, + revision: version +})); +const APP_CACHE = `cache-app-shell-v${version}`; +const APP_CACHE_PREFIX = "cache-app-shell-v"; -//precacheAndRoute(precache_list) -\ No newline at end of file +const precache = async () => { + const cache = await caches.open(APP_CACHE); + await cache.addAll(PRECACHE_LIST.map((entry) => entry.url)); +}; + +const cleanup_caches = async () => { + const keys = await caches.keys(); + for (const key of keys) { + if (!key.startsWith(APP_CACHE_PREFIX)) continue; + if (key === APP_CACHE) continue; + await caches.delete(key); + } +}; + +const range_response = async (request, response) => { + const range = request.headers.get("range"); + if (!range || !response) return response; + const bytes = /bytes=(\d+)-(\d+)?/u.exec(range); + if (!bytes) return response; + const start = Number(bytes[1]); + const end_raw = bytes[2]; + const buffer = await response.arrayBuffer(); + const end = end_raw ? Number(end_raw) : buffer.byteLength - 1; + if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return response; + const sliced = buffer.slice(start, end + 1); + const headers = new Headers(response.headers); + headers.set("Content-Range", `bytes ${start}-${end}/${buffer.byteLength}`); + headers.set("Content-Length", `${sliced.byteLength}`); + return new Response(sliced, { status: 206, statusText: "Partial Content", headers }); +}; + +const cache_first = async (request) => { + const cache = await caches.open(APP_CACHE); + const cached = await cache.match(request); + if (cached) return await range_response(request, cached); + const response = await fetch(request); + if (response.ok) await cache.put(request, response.clone()); + return response; +}; + +const network_first = async (request) => { + const cache = await caches.open(APP_CACHE); + try { + const response = await fetch(request); + if (response.ok) await cache.put(request, response.clone()); + return response; + } catch { + const cached = await cache.match(request); + if (cached) return await range_response(request, cached); + const fallback = await cache.match(APP_SHELL_URL); + if (fallback) return fallback; + return new Response("offline", { status: 503 }); + } +}; + +self.addEventListener("install", (event) => { + event.waitUntil(precache().then(() => self.skipWaiting())); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil(cleanup_caches().then(() => self.clients.claim())); +}); + +self.addEventListener("fetch", (event) => { + const request = event.request; + if (request.method !== "GET") return; + const url = new URL(request.url); + if (request.mode === "navigate") { + event.respondWith(network_first(request)); + return; + } + if (url.origin === self.location.origin) event.respondWith(cache_first(request)); +}); diff --git a/package.json b/package.json @@ -6,8 +6,8 @@ "build": "turbo build", "build:app": "turbo build --filter=app", "build:packages": "turbo run build --filter=./packages/*", - "dev:app": "turbo dev --filter=app", - "dev:packages": "turbo dev --filter=./packages/* --concurrency 20" + "dev:app": "turbo dev --filter=app --filter=./packages/* --concurrency 25", + "dev:packages": "turbo dev --filter=./packages/* --concurrency 25" }, "devDependencies": { "turbo": "2.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -447,6 +447,9 @@ importers: '@radroots/locales': specifier: workspace:* version: link:../packages/locales + '@radroots/nfc': + specifier: workspace:* + version: link:../packages/nfc '@radroots/nostr': specifier: workspace:* version: link:../packages/nostr @@ -765,6 +768,22 @@ importers: specifier: 5.8.3 version: 5.8.3 + packages/ble: + dependencies: + '@radroots/utils': + specifier: workspace:* + version: link:../utils + devDependencies: + '@radroots/tsconfig': + specifier: workspace:* + version: link:../tsconfig + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: 5.8.3 + version: 5.8.3 + packages/client: dependencies: '@radroots/geo': @@ -916,6 +935,22 @@ importers: specifier: 5.8.3 version: 5.8.3 + packages/nfc: + dependencies: + '@radroots/utils': + specifier: workspace:* + version: link:../utils + devDependencies: + '@radroots/tsconfig': + specifier: workspace:* + version: link:../tsconfig + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: 5.8.3 + version: 5.8.3 + packages/nostr: dependencies: '@noble/curves': @@ -5318,7 +5353,7 @@ snapshots: '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5370,7 +5405,7 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.11 transitivePeerDependencies: @@ -5942,7 +5977,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -6804,7 +6839,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) svelte: 5.46.0 vite: 7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: @@ -6813,7 +6848,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) svelte: 5.46.0 vite: 7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: @@ -6822,7 +6857,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) svelte: 5.46.0 vite: 7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0) transitivePeerDependencies: @@ -6831,7 +6866,7 @@ snapshots: '@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 @@ -6845,7 +6880,7 @@ snapshots: '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) deepmerge: 4.3.1 magic-string: 0.30.21 svelte: 5.46.0 @@ -6857,7 +6892,7 @@ snapshots: '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) deepmerge: 4.3.1 magic-string: 0.30.21 svelte: 5.46.0 @@ -7431,10 +7466,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@4.4.3: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -9249,7 +9280,7 @@ snapshots: bundle-require: 4.2.1(esbuild@0.17.19) cac: 6.7.14 chokidar: 3.6.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) esbuild: 0.17.19 execa: 5.1.1 globby: 11.1.0 @@ -9395,7 +9426,7 @@ snapshots: vite-plugin-pwa@1.2.0(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))(workbox-build@7.4.0)(workbox-window@7.4.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) pretty-bytes: 6.1.1 tinyglobby: 0.2.15 vite: 7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)