chore: migrate all FA icons to Lucide (@lucide/svelte)
- Replaced all active FontAwesome <span class="fas fa-*"> icons with
Lucide components across 145 files (excluding /idaa/ which is intentional)
- Fixed merge script bug: consolidated lucide-svelte imports into @lucide/svelte
- Replaced dynamic toggle patterns (fa-toggle-on/off) with ToggleRight/ToggleLeft
- Replaced fa-eye/fa-eye-slash with Eye/EyeOff
- Replaced fa-bug/fa-bug-slash with Bug/BugOff
- Replaced fa-sync fa-spin with RefreshCw + animate-spin
- Replaced fa-microchip with Cpu
- Fixed {@const} placement in element_manage_event_file_li.svelte
- Removed obsolete CSS hover rules for .unlock_icon/.lock_icon
- svelte-check: 0 errors, 0 warnings
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
} from '$lib/stores/ae_stores';
|
||||
|
||||
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
|
||||
|
||||
import { Check, Download, LoaderCircle, MinusCircle, Scissors } from '@lucide/svelte';
|
||||
// Exports
|
||||
|
||||
// export let input_name = 'file_list';
|
||||
@@ -221,7 +221,7 @@
|
||||
class="btn btn-sm preset-tonal-warning hover:preset-filled-warning-500"
|
||||
title={`Remove this file from list of videos:\n${hosted_file_obj.filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}... Hosted ID: ${hosted_file_obj.hosted_file_id}`}
|
||||
>
|
||||
<span class="fas fa-minus-circle m-1"></span>
|
||||
<MinusCircle size="1em" class="m-1" />
|
||||
<span class="">Remove</span>
|
||||
</button>
|
||||
|
||||
@@ -387,15 +387,15 @@
|
||||
>
|
||||
<!-- {#await ae_promises[hosted_file_id]} -->
|
||||
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipping'}
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
<LoaderCircle size="1em" class="m-1 animate-spin" />
|
||||
<span class="highlight">Clipping...</span>
|
||||
{:else}
|
||||
<!-- {#if ae_promises[hosted_file_id]} -->
|
||||
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipped'}
|
||||
<span class="fas fa-check m-1"></span>
|
||||
<Check size="1em" class="m-1" />
|
||||
Clipped
|
||||
{:else}
|
||||
<span class="fas fa-cut m-1"></span>
|
||||
<Scissors size="1em" class="m-1" />
|
||||
Clip Video
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -405,13 +405,13 @@
|
||||
</form>
|
||||
|
||||
{#await ae_promises[hosted_file_id]}
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
<LoaderCircle size="1em" class="m-1 animate-spin" />
|
||||
<span class="highlight"
|
||||
>Processing... This may take a few minutes.</span
|
||||
>
|
||||
{:then}
|
||||
{#if ae_promises[hosted_file_id]}
|
||||
<span class="fas fa-download"></span> Ready to download below!
|
||||
<Download size="1em" /> Ready to download below!
|
||||
{:else}
|
||||
<!-- <p>Fill out the form and select the video file to clip.</p> -->
|
||||
{/if}
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
* Supports General, AI, Performance, and IDAA-specific configurations.
|
||||
*/
|
||||
import { Modal } from 'flowbite-svelte';
|
||||
import {
|
||||
Palette, Mail, Brain, Timer,
|
||||
ShieldCheck, CodeXml, Save,
|
||||
Plus, Minus, Globe, ExternalLink
|
||||
} from 'lucide-svelte';
|
||||
import { Brain, CodeXml, ExternalLink, Globe, Mail, Minus, Palette, Plus, Save, ShieldCheck, Timer } from '@lucide/svelte';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/AE_Comp_Editor_CodeMirror.svelte';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Settings } from '@lucide/svelte';
|
||||
|
||||
import { Code, Eraser, LockOpen, RefreshCw, Settings, ShieldCheck, ShieldUser, Trash2, UserRound, Users } from '@lucide/svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/stores/ae_stores';
|
||||
import E_app_url_builder from '$lib/app_components/e_app_url_builder.svelte';
|
||||
@@ -138,19 +137,19 @@
|
||||
</div>
|
||||
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
|
||||
{#if $ae_loc.access_type == 'super'}
|
||||
<span class="fas fa-secret mx-1"></span> Super Access
|
||||
<ShieldCheck size="1em" class="mx-1" /> Super Access
|
||||
{:else if $ae_loc.access_type == 'manager'}
|
||||
<span class="fas fa-user-shield mx-1"></span> Manager Access
|
||||
<ShieldUser size="1em" class="mx-1" /> Manager Access
|
||||
{:else if $ae_loc.access_type == 'administrator'}
|
||||
<span class="fas fa-user-ninja mx-1"></span> Administrator Access
|
||||
<UserRound size="1em" class="mx-1" /> Administrator Access
|
||||
{:else if $ae_loc.access_type == 'trusted'}
|
||||
<span class="fas fa-user-nurse mx-1"></span> Trusted Access
|
||||
<UserRound size="1em" class="mx-1" /> Trusted Access
|
||||
{:else if $ae_loc.access_type == 'authenticated'}
|
||||
<span class="fas fa-user-friends mx-1"></span> Authenticated Access
|
||||
<Users size="1em" class="mx-1" /> Authenticated Access
|
||||
{:else if $ae_loc.access_type == 'anonymous'}
|
||||
<span class="fas fa-users mx-1"></span> Anonymous Access
|
||||
<Users size="1em" class="mx-1" /> Anonymous Access
|
||||
{:else}
|
||||
<span class="fas fa-unlock mx-1"></span> Unknown Access
|
||||
<LockOpen size="1em" class="mx-1" /> Unknown Access
|
||||
{/if}
|
||||
|
||||
<!-- <button
|
||||
@@ -171,18 +170,18 @@
|
||||
<section class="space-y-2">
|
||||
<h2 class="strong">Utilities:</h2>
|
||||
<a class="btn btn-sm preset-tonal-secondary" href="/hosted_files">
|
||||
<span class="fas fa-code mx-1"></span>
|
||||
<Code size="1em" class="mx-1" />
|
||||
Util: Convert Videos
|
||||
</a>
|
||||
|
||||
{#if $ae_loc.iframe}
|
||||
<a class="btn btn-sm preset-tonal-secondary" href="/?iframe=false">
|
||||
<span class="fas fa-code mx-1"></span>
|
||||
<Code size="1em" class="mx-1" />
|
||||
Exit iframe Mode
|
||||
</a>
|
||||
{:else}
|
||||
<a class="btn btn-sm preset-tonal-secondary" href="/?iframe=true">
|
||||
<span class="fas fa-code mx-1"></span>
|
||||
<Code size="1em" class="mx-1" />
|
||||
Use iframe Mode
|
||||
</a>
|
||||
{/if}
|
||||
@@ -196,9 +195,9 @@
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<span class="fas fa-sync mx-1"></span>
|
||||
<RefreshCw size="1em" class="mx-1" />
|
||||
Reload &
|
||||
<span class="fas fa-trash mx-1"></span>
|
||||
<Trash2 size="1em" class="mx-1" />
|
||||
Clear Cache
|
||||
</button>
|
||||
<button
|
||||
@@ -225,7 +224,7 @@
|
||||
// alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.');
|
||||
}}
|
||||
>
|
||||
<span class="fas fa-eraser mx-1"></span>
|
||||
<Eraser size="1em" class="mx-1" />
|
||||
Clear Storage & DB
|
||||
</button>
|
||||
</div>
|
||||
@@ -244,19 +243,19 @@ class:justify-end={!expand} -->
|
||||
<span>
|
||||
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
|
||||
{#if $ae_loc.access_type == 'super'}
|
||||
<span class="fas fa-secret mx-1"></span> Super Access
|
||||
<ShieldCheck size="1em" class="mx-1" /> Super Access
|
||||
{:else if $ae_loc.access_type == 'manager'}
|
||||
<span class="fas fa-user-shield mx-1"></span> Manager Access
|
||||
<ShieldUser size="1em" class="mx-1" /> Manager Access
|
||||
{:else if $ae_loc.access_type == 'administrator'}
|
||||
<span class="fas fa-user-ninja mx-1"></span> Administrator Access
|
||||
<UserRound size="1em" class="mx-1" /> Administrator Access
|
||||
{:else if $ae_loc.access_type == 'trusted'}
|
||||
<span class="fas fa-user-nurse mx-1"></span> Trusted Access
|
||||
<UserRound size="1em" class="mx-1" /> Trusted Access
|
||||
{:else if $ae_loc.access_type == 'authenticated'}
|
||||
<span class="fas fa-user-friends mx-1"></span> Authenticated Access
|
||||
<Users size="1em" class="mx-1" /> Authenticated Access
|
||||
{:else if $ae_loc.access_type == 'anonymous'}
|
||||
<span class="fas fa-users mx-1"></span> Anonymous Access
|
||||
<Users size="1em" class="mx-1" /> Anonymous Access
|
||||
{:else}
|
||||
<span class="fas fa-unlock mx-1"></span> Unknown Access
|
||||
<LockOpen size="1em" class="mx-1" /> Unknown Access
|
||||
{/if}
|
||||
|
||||
<!-- <button
|
||||
|
||||
@@ -39,27 +39,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
// *** Import other supporting libraries
|
||||
import {
|
||||
// ArrowBigRight,
|
||||
BadgeQuestionMark,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
// CircleX,
|
||||
// Copy,
|
||||
// Eye, EyeOff,
|
||||
// Key,
|
||||
LifeBuoy,
|
||||
// LogIn, LogOut, LockKeyhole,
|
||||
// Mail, MailCheck,
|
||||
// Menu,
|
||||
RefreshCw,
|
||||
// RefreshCcw,
|
||||
// RefreshCcwDot,
|
||||
// ShieldEllipsis, ShieldMinus, ShieldPlus, ShieldUser,
|
||||
SquareX
|
||||
// User, UserCheck
|
||||
} from '@lucide/svelte';
|
||||
|
||||
import { BadgeQuestionMark, ChevronDown, ChevronRight, LifeBuoy, RefreshCw, SquareX } from '@lucide/svelte';
|
||||
// *** Import Aether specific variables and functions
|
||||
import {
|
||||
ae_snip,
|
||||
@@ -71,7 +51,6 @@
|
||||
slct_trigger,
|
||||
type key_val
|
||||
} from '$lib/stores/ae_stores';
|
||||
import { User } from 'lucide-svelte';
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
if (log_lvl) {
|
||||
|
||||
@@ -12,19 +12,7 @@
|
||||
* (access_type, sign_in_out). Simple controls (theme, cfg utilities)
|
||||
* are inlined here for clean panel layout without blue-bg conflicts.
|
||||
*/
|
||||
import {
|
||||
Bug,
|
||||
CircleX,
|
||||
LogOut,
|
||||
Menu,
|
||||
Moon,
|
||||
Sun,
|
||||
ShieldEllipsis,
|
||||
ShieldMinus,
|
||||
ShieldUser,
|
||||
User,
|
||||
} from '@lucide/svelte';
|
||||
|
||||
import { Bug, CircleX, Eraser, LogOut, Maximize2, Menu, Minimize2, Moon, Palette, RefreshCw, ShieldEllipsis, ShieldMinus, ShieldUser, Sun, ToggleLeft, ToggleRight, User } from '@lucide/svelte';
|
||||
import { ae_loc, ae_sess, ae_api } from '$lib/stores/ae_stores';
|
||||
|
||||
import Element_access_type from '$lib/app_components/e_app_access_type.svelte';
|
||||
@@ -341,7 +329,7 @@
|
||||
onclick={() => sec_appearance = !sec_appearance}
|
||||
>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="fas fa-palette opacity-60"></span>
|
||||
<Palette size="1em" class="opacity-60" />
|
||||
Appearance
|
||||
<span class="normal-case font-normal opacity-60 ml-1">
|
||||
({$ae_loc?.theme_name ?? '—'} · {$ae_loc?.theme_mode ?? '—'})
|
||||
@@ -425,12 +413,12 @@
|
||||
<!-- iframe mode toggle -->
|
||||
{#if $ae_loc.iframe}
|
||||
<a class="btn btn-sm preset-tonal-secondary w-full justify-end" href="/?iframe=false">
|
||||
<span class="fas fa-compress-arrows-alt opacity-60"></span>
|
||||
<Minimize2 size="1em" class="opacity-60" />
|
||||
Exit iframe Mode
|
||||
</a>
|
||||
{:else}
|
||||
<a class="btn btn-sm preset-tonal-secondary w-full justify-end" href="/?iframe=true">
|
||||
<span class="fas fa-expand-arrows-alt opacity-60"></span>
|
||||
<Maximize2 size="1em" class="opacity-60" />
|
||||
Enable iframe Mode
|
||||
</a>
|
||||
{/if}
|
||||
@@ -442,7 +430,7 @@
|
||||
onclick={() => window.location.reload()}
|
||||
title="Hard reload the page"
|
||||
>
|
||||
<span class="fas fa-sync opacity-60"></span>
|
||||
<RefreshCw size="1em" class="opacity-60" />
|
||||
Reload Page
|
||||
</button>
|
||||
|
||||
@@ -453,7 +441,7 @@
|
||||
onclick={handle_clear_storage_db}
|
||||
title="Clear localStorage, sessionStorage, and all IndexedDB tables — then reload"
|
||||
>
|
||||
<span class="fas fa-eraser opacity-60"></span>
|
||||
<Eraser size="1em" class="opacity-60" />
|
||||
Clear Storage & DB
|
||||
</button>
|
||||
|
||||
@@ -612,7 +600,7 @@
|
||||
onclick={() => { $ae_loc.edit_mode = false; }}
|
||||
title="Edit mode ON — click to turn off"
|
||||
>
|
||||
<span class="fas fa-toggle-on text-sm inline-block"></span>
|
||||
<ToggleRight size="1em" class="text-sm inline-block" />
|
||||
<span class="btn-label max-w-0 overflow-hidden opacity-0 group-hover/edit:max-w-20 group-hover/edit:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Edit</span>
|
||||
</button>
|
||||
{:else}
|
||||
@@ -622,7 +610,7 @@
|
||||
onclick={() => { $ae_loc.edit_mode = true; }}
|
||||
title="Edit mode OFF — click to turn on"
|
||||
>
|
||||
<span class="fas fa-toggle-off text-sm inline-block opacity-50"></span>
|
||||
<ToggleLeft size="1em" class="text-sm inline-block opacity-50" />
|
||||
<span class="btn-label max-w-0 overflow-hidden opacity-0 group-hover/edit:max-w-20 group-hover/edit:opacity-100 transition-all duration-300 ease-in-out text-xs pl-1">Edit</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
ShieldMinus,
|
||||
ShieldPlus,
|
||||
ShieldUser,
|
||||
// ToggleLeft, ToggleRight,
|
||||
ToggleLeft, ToggleRight,
|
||||
User,
|
||||
UserCheck,
|
||||
UserCog,
|
||||
Wand2,
|
||||
X
|
||||
} from '@lucide/svelte';
|
||||
|
||||
@@ -191,22 +193,22 @@ max-w-max -->
|
||||
<!-- <ShieldPlus class="inline-block" /> -->
|
||||
|
||||
{#if $ae_loc.access_type == 'super'}
|
||||
<span class="fas fa-hat-wizard m-1"></span>
|
||||
<Wand2 size="1em" class="m-1 inline-block" />
|
||||
<span class:hidden={!expand} class="hidden group-hover:inline-block"
|
||||
>Super</span
|
||||
>
|
||||
{:else if $ae_loc.access_type == 'manager'}
|
||||
<span class="fas fa-user-shield m-1"></span>
|
||||
<ShieldUser size="1em" class="m-1 inline-block" />
|
||||
<span class:hidden={!expand} class="hidden group-hover:inline-block"
|
||||
>Manager</span
|
||||
>
|
||||
{:else if $ae_loc.access_type == 'administrator'}
|
||||
<span class="fas fa-user-ninja m-1"></span>
|
||||
<UserCog size="1em" class="m-1 inline-block" />
|
||||
<span class:hidden={!expand} class="hidden group-hover:inline-block"
|
||||
>Administrator</span
|
||||
>
|
||||
{:else if $ae_loc.access_type == 'trusted'}
|
||||
<span class="fas fa-user-check m-1"></span>
|
||||
<UserCheck size="1em" class="m-1 inline-block" />
|
||||
<span class:hidden={!expand} class="hidden group-hover:inline-block"
|
||||
>Trusted Access</span
|
||||
>
|
||||
@@ -357,7 +359,7 @@ max-w-max -->
|
||||
"
|
||||
title="Click to turn off edit mode. Edit mode is currently on."
|
||||
>
|
||||
<span class="fas fa-toggle-on m-1 inline-block"></span>
|
||||
<ToggleRight size="1em" class="m-1 inline-block" />
|
||||
<span class="text-xs">Edit</span>
|
||||
<span class="hidden group-hover:inline-block group-hover:text-xs"> Off </span>
|
||||
</button>
|
||||
@@ -379,7 +381,7 @@ max-w-max -->
|
||||
"
|
||||
title="Click to torn on/enable edit mode. Edit mode is currently off/disabled."
|
||||
>
|
||||
<span class="fas fa-toggle-off m-1 inline-block"></span>
|
||||
<ToggleLeft size="1em" class="m-1 inline-block" />
|
||||
<span class="text-xs">Edit</span>
|
||||
<span class="hidden group-hover:inline-block group-hover:text-xs"> On? </span>
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Moon, Sun } from '@lucide/svelte';
|
||||
|
||||
import { Minimize2, Moon, Sun } from '@lucide/svelte';
|
||||
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/stores/ae_stores';
|
||||
|
||||
interface Props {
|
||||
@@ -131,7 +130,7 @@ if ($ae_loc.app_cfg.theme_mode == 'light') {
|
||||
title="Hide Theme Options"
|
||||
>
|
||||
<!-- <span class="fas fa-compress-alt"></span> -->
|
||||
<span class="fas fa-compress-arrows-alt m-1"></span>
|
||||
<Minimize2 size="1em" class="m-1" />
|
||||
<span
|
||||
class="
|
||||
hidden
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
Html5QrcodeSupportedFormats
|
||||
} from 'html5-qrcode';
|
||||
|
||||
// *** Import Lucide icons
|
||||
import { Camera, Crosshair, Keyboard, QrCode, Send } from '@lucide/svelte';
|
||||
|
||||
// *** Import Aether core variables and functions
|
||||
import { api } from '$lib/api/api';
|
||||
import { ae_api } from '$lib/stores/ae_stores';
|
||||
@@ -380,7 +383,7 @@
|
||||
}}
|
||||
class="ae_btn__allow_camera btn btn-sm preset-tonal-primary"
|
||||
>
|
||||
<span class="fas fa-camera mx-1"></span>
|
||||
<Camera size="1em" class="mx-1 inline-block" />
|
||||
Allow Camera Access
|
||||
</button>
|
||||
|
||||
@@ -393,7 +396,7 @@
|
||||
}}
|
||||
class="ae_btn__start btn btn-sm preset-tonal-primary"
|
||||
>
|
||||
<span class="fas fa-qrcode mx-1"></span>
|
||||
<QrCode size="1em" class="mx-1 inline-block" />
|
||||
Start Scanning
|
||||
</button>
|
||||
|
||||
@@ -411,7 +414,7 @@
|
||||
onclick={handle_stop_qr_scanning}
|
||||
class="ae_btn__stop btn btn-sm preset-tonal-secondary"
|
||||
>
|
||||
<span class="fas fa-crosshairs fa-spin opacity-50 m-1"></span>
|
||||
<Crosshair size="1em" class="animate-spin opacity-50 m-1" />
|
||||
<!-- <span class="fas fa-stop-circle m-1"></span> -->
|
||||
Stop
|
||||
</button>
|
||||
@@ -437,7 +440,7 @@
|
||||
bind:value={qr_entered_text}
|
||||
/>
|
||||
<button onclick={handle_qr_manual_entry} class="btn btn-md preset-tonal-warning"
|
||||
><span class="fas fa-paper-plane"></span> Submit Text</button
|
||||
><Send size="1em" class="inline-block" /> Submit Text</button
|
||||
>
|
||||
|
||||
<div class="search_by_text">
|
||||
@@ -457,7 +460,7 @@
|
||||
}}
|
||||
class="btn btn-md preset-tonal-warning m-1"
|
||||
>
|
||||
<span class="fas fa-keyboard mx-1"></span> Enter Text
|
||||
<Keyboard size="1em" class="mx-1 inline-block" /> Enter Text
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -488,7 +491,7 @@
|
||||
class:btn_default={disable_submit_badge_id_btn}
|
||||
class:btn_primary={!disable_submit_badge_id_btn}
|
||||
>
|
||||
<span class="fas fa-paper-plane mx-1"></span> Submit Badge ID
|
||||
<Send size="1em" class="mx-1 inline-block" /> Submit Badge ID
|
||||
</button>
|
||||
</form>
|
||||
{:else}
|
||||
@@ -499,7 +502,7 @@
|
||||
}}
|
||||
class="btn btn-md preset-tonal-secondary m-1"
|
||||
>
|
||||
<span class="fas fa-keyboard mx-1"></span> Enter Badge ID
|
||||
<Keyboard size="1em" class="mx-1 inline-block" /> Enter Badge ID
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
|
||||
// Icons (Standardized to Lucide where possible, or FontAwesome placeholders)
|
||||
import { Bold, Italic, List, Code, Maximize2 } from 'lucide-svelte';
|
||||
|
||||
import { Bold, Code, Italic, List, Maximize2 } from '@lucide/svelte';
|
||||
interface Props {
|
||||
content?: string;
|
||||
new_content?: string;
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
*/
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import {
|
||||
Bold, Italic, List, ListOrdered,
|
||||
RemoveFormatting, Type, Code, AlignLeft
|
||||
} from 'lucide-svelte';
|
||||
import { AlignLeft, Bold, Code, Italic, List, ListOrdered, RemoveFormatting, Type } from '@lucide/svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
// import { api } from '$lib/api';
|
||||
// import { update_ae_obj_id_crud } from '$lib/ae_core/core__crud_generic';
|
||||
import { update_ae_obj } from '$lib/ae_core/core__crud_generic';
|
||||
import { Check, LoaderCircle, Pencil, Save, X } from '@lucide/svelte';
|
||||
// import { ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger } from '$lib/ae_stores';
|
||||
|
||||
// *** Import Aether core components
|
||||
@@ -43,7 +44,7 @@
|
||||
hide_edit_btn?: boolean;
|
||||
outline_element?: boolean;
|
||||
show_crud?: boolean;
|
||||
btn_label?: any; // '<span class="fas fa-check mx-1"></span> Save'; // PATCH
|
||||
btn_label?: any; // '<Check size="1em" class="mx-1" /> Save'; // PATCH
|
||||
// export let remove_breaks = false;
|
||||
class_li?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
@@ -186,7 +187,7 @@
|
||||
}}
|
||||
title="Double click to edit property"
|
||||
>
|
||||
<span class="fas fa-edit"></span>
|
||||
<Pencil size="1em" />
|
||||
<span class="hidden">Edit</span>
|
||||
</button>
|
||||
</span>
|
||||
@@ -208,7 +209,7 @@
|
||||
}}
|
||||
title="Close field editing"
|
||||
>
|
||||
<span class="fas fa-window-close"></span>
|
||||
<X size="1em" />
|
||||
<span class="hidden sm:inline">Close</span>
|
||||
</button>
|
||||
<!-- </span> -->
|
||||
@@ -272,25 +273,25 @@
|
||||
{#if btn_label}
|
||||
{@html btn_label}
|
||||
{:else}
|
||||
<span class="fas fa-check mx-1"></span>
|
||||
<Check size="1em" class="mx-1" />
|
||||
<span>Save</span>
|
||||
{/if}
|
||||
<!-- <span>{patch_result}</span> -->
|
||||
{:else if btn_label}
|
||||
{@html btn_label}
|
||||
{:else}
|
||||
<span class="fas fa-save mx-1"></span>
|
||||
<Save size="1em" class="mx-1" />
|
||||
<span>Save</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="field_patch_result" class:show_crud>
|
||||
{#await ae_promises.api_update__ae_obj}
|
||||
<span class="fas fa-spinner fa-spin mx-1"></span>
|
||||
<LoaderCircle size="1em" class="mx-1 animate-spin" />
|
||||
<span>Processing...</span>
|
||||
{:then}
|
||||
{#if patch_result}
|
||||
<span class="fas fa-check mx-1"></span>
|
||||
<Check size="1em" class="mx-1" />
|
||||
<span>{patch_result}</span>
|
||||
{:else}
|
||||
<!-- <div>Nothing to show yet...</div> -->
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
outline_element?: boolean;
|
||||
|
||||
btn_label?: any; // '<span class="fas fa-check mx-1"></span> Save'; // PATCH
|
||||
btn_label?: any; // '<Check size="1em" class="mx-1" /> Save'; // PATCH
|
||||
// export let remove_breaks = false;
|
||||
class_li?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
// *** Import Aether core components
|
||||
import AE_Comp_Editor_TipTap from '$lib/elements/AE_Comp_Editor_TipTap.svelte';
|
||||
|
||||
import { Check, LoaderCircle, Pencil, Save, X } from '@lucide/svelte';
|
||||
// *** Import Aether module variables and functions
|
||||
// *** Import Aether module components
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
}}
|
||||
title="Double click to edit property"
|
||||
>
|
||||
<span class="fas fa-edit m-0 p-0"></span>
|
||||
<Pencil size="1em" class="m-0 p-0" />
|
||||
<span class="hidden">Edit</span>
|
||||
</button>
|
||||
</span>
|
||||
@@ -313,7 +313,7 @@
|
||||
}}
|
||||
title="Close field editing. This does not save any changes."
|
||||
>
|
||||
<span class="fas fa-window-close"></span>
|
||||
<X size="1em" />
|
||||
<span class="hidden sm:inline">Close</span>
|
||||
</button>
|
||||
</span>
|
||||
@@ -405,25 +405,25 @@
|
||||
{#if btn_label}
|
||||
{@html btn_label}
|
||||
{:else}
|
||||
<span class="fas fa-check mx-1"></span>
|
||||
<Check size="1em" class="mx-1" />
|
||||
<span>Save</span>
|
||||
{/if}
|
||||
<!-- <span>{patch_result}</span> -->
|
||||
{:else if btn_label}
|
||||
{@html btn_label}
|
||||
{:else}
|
||||
<span class="fas fa-save mx-1"></span>
|
||||
<Save size="1em" class="mx-1" />
|
||||
<span>Save</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="field_patch_result" class:hidden={!patch_status}>
|
||||
{#await ae_promises.api_update__ae_obj}
|
||||
<span class="fas fa-spinner fa-spin mx-1"></span>
|
||||
<LoaderCircle size="1em" class="mx-1 animate-spin" />
|
||||
<span>Processing...</span>
|
||||
{:then}
|
||||
{#if patch_status}
|
||||
<span class="fas fa-check mx-1"></span>
|
||||
<Check size="1em" class="mx-1" />
|
||||
<span>{patch_status}</span>
|
||||
{:else}
|
||||
<!-- <div>Nothing to show yet...</div> -->
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// import { browser } from '$app/environment';
|
||||
import { untrack } from 'svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { LoaderCircle, SquarePen, Save, X, Trash2, Check, CircleAlert } from 'lucide-svelte';
|
||||
import { Check, CircleAlert, LoaderCircle, Save, SquarePen, Trash2, X } from '@lucide/svelte';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import type { ae_DataStore } from '$lib/types/ae_types';
|
||||
|
||||
import { LoaderCircle, Pencil, Save, Trash2 } from '@lucide/svelte';
|
||||
interface Props {
|
||||
log_lvl?: number;
|
||||
expire_minutes?: number;
|
||||
@@ -379,13 +379,13 @@
|
||||
|
||||
<div class="flex justify-between items-center pt-4">
|
||||
<button type="button" class="btn variant-filled-error" onclick={handle_delete}>
|
||||
<span class="fas fa-trash mr-2"></span> Delete
|
||||
<Trash2 size="1em" class="mr-2" /> Delete
|
||||
</button>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="btn variant-soft" onclick={() => show_edit = false}>Cancel</button>
|
||||
<button type="submit" class="btn variant-filled-primary">
|
||||
<span class="fas fa-save mr-2"></span> Save
|
||||
<Save size="1em" class="mr-2" /> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -409,7 +409,7 @@
|
||||
ondblclick={() => { show_edit = true; show_view = false; }}
|
||||
title="Edit Data Store: {ds_code}"
|
||||
>
|
||||
<span class="fas fa-edit"></span>
|
||||
<Pencil size="1em" />
|
||||
</button>
|
||||
{/if}
|
||||
{:else if ds_loading_status === 'not found'}
|
||||
@@ -424,7 +424,7 @@
|
||||
|
||||
{#if ds_loading_status === 'loading'}
|
||||
<div class="absolute bottom-0 left-0 p-1 opacity-50">
|
||||
<span class="fas fa-spinner fa-spin text-xs"></span>
|
||||
<LoaderCircle size="1em" class="text-xs animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
slct,
|
||||
slct_trigger
|
||||
} from '$lib/stores/ae_stores';
|
||||
|
||||
import { LoaderCircle, Minus, Upload } from '@lucide/svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
interface Props {
|
||||
@@ -367,7 +367,7 @@
|
||||
>
|
||||
<label for={element_id} class="svelte_input_file_label text-center">
|
||||
<div>
|
||||
<span class="fas fa-upload"></span>
|
||||
<Upload size="1em" />
|
||||
<!-- Select files to upload -->
|
||||
<!-- <span class="fas fa-file-archive"></span> -->
|
||||
<strong>Upload your files</strong>
|
||||
@@ -394,7 +394,7 @@
|
||||
<div
|
||||
class="file_list_status ae_warning preset-tonal-warning border border-warning-500 p-1 m-1"
|
||||
>
|
||||
<span class="fas fa-spinner fa-spin m-1"></span> Processing selected file
|
||||
<LoaderCircle size="1em" class="m-1 animate-spin" /> Processing selected file
|
||||
list...
|
||||
</div>
|
||||
{/if}
|
||||
@@ -426,7 +426,7 @@
|
||||
class="btn btn-md preset-tonal-warning hover:preset-filled-secondary-500 m-1"
|
||||
title="Remove file from upload list"
|
||||
>
|
||||
<span class="fas fa-minus"></span>
|
||||
<Minus size="1em" />
|
||||
<span class="hidden">Remove</span>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// import { api } from '$lib/api';
|
||||
import { check_hosted_file_obj_w_hash } from '$lib/ae_core/core__check_hosted_file_obj_w_hash';
|
||||
import { ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger } from '$lib/stores/ae_stores';
|
||||
|
||||
import { LoaderCircle, Minus } from '@lucide/svelte';
|
||||
interface Props {
|
||||
// export let element_id = 'svelte_input_file_element';
|
||||
container_class_li?: string[];
|
||||
@@ -335,7 +335,7 @@
|
||||
<div
|
||||
class="file_list_status ae_warning preset-tonal-warning border border-warning-500 p-1 m-1"
|
||||
>
|
||||
<span class="fas fa-spinner fa-spin m-1"></span> Processing selected file list...
|
||||
<LoaderCircle size="1em" class="m-1 animate-spin" /> Processing selected file list...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
class="btn btn-md preset-tonal-warning hover:preset-filled-secondary-500 m-1"
|
||||
title="Remove file from upload list"
|
||||
>
|
||||
<span class="fas fa-minus"></span>
|
||||
<Minus size="1em" />
|
||||
<span class="hidden">Remove</span>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
} from '$lib/stores/ae_events_stores';
|
||||
import { events_func } from '$lib/ae_events_functions';
|
||||
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
|
||||
|
||||
import { Check, Clock, Download, Eye, EyeOff, FileImage, FolderOpen, Laptop, LoaderCircle, Monitor, Pencil, RefreshCw, Save, Trash2, TriangleAlert } from '@lucide/svelte';
|
||||
interface Props {
|
||||
log_lvl?: number;
|
||||
container_class_li?: string | Array<string>;
|
||||
@@ -156,7 +156,7 @@
|
||||
class:hidden={!$ae_loc.edit_mode || !$ae_loc.authenticated_access}
|
||||
title="Refresh the list of files"
|
||||
>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
<RefreshCw size="1em" class="m-1" />
|
||||
<div class="hidden">Files</div>
|
||||
</button>
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
class:hidden={!$ae_loc.edit_mode || !$ae_loc.trusted_access}
|
||||
title="Toggle direct download link and copy link button"
|
||||
>
|
||||
<span class="fas fa-download m-1"></span>
|
||||
<Download size="1em" class="m-1" />
|
||||
<div class="hidden">
|
||||
{ae_tmp.show__direct_download ? 'Alt On' : 'Alt Download Off'}
|
||||
</div>
|
||||
@@ -193,7 +193,7 @@
|
||||
'-- not set --'} (files: {$lq__event_file_obj_li?.length ??
|
||||
'None'})"
|
||||
>
|
||||
<span class="fas fa-folder-open mx-1"></span>
|
||||
<FolderOpen size="1em" class="mx-1" />
|
||||
{@html $lq__event_file_obj_li
|
||||
? `${$lq__event_file_obj_li.length}×`
|
||||
: '-- none --'}
|
||||
@@ -228,6 +228,7 @@
|
||||
|
||||
<tbody>
|
||||
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
|
||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(event_file_obj.extension)}
|
||||
<tr
|
||||
class="ae_obj obj_event_file border-t border-b border-surface-200-800 hover:bg-surface-100-900 hover:border-surface-300-700 transition-colors duration-200"
|
||||
class:dim={event_file_obj?.hide}
|
||||
@@ -254,17 +255,17 @@
|
||||
title="Convert this PDF to a high-res webp image for use in the Launcher poster display."
|
||||
onclick={() => handle_convert_pdf_to_image(event_file_obj)}
|
||||
>
|
||||
<span class="fas fa-file-image mx-1"></span>
|
||||
<FileImage size="1em" class="mx-1" />
|
||||
Convert PDF → Image
|
||||
</button>
|
||||
{:else if convert_status_kv[event_file_obj.event_file_id] === 'converting'}
|
||||
<span class="btn btn-sm preset-tonal-surface opacity-60 cursor-wait">
|
||||
<span class="fas fa-spinner fa-spin mx-1"></span>
|
||||
<LoaderCircle size="1em" class="mx-1 animate-spin" />
|
||||
Converting…
|
||||
</span>
|
||||
{:else if convert_status_kv[event_file_obj.event_file_id] === 'done'}
|
||||
<span class="btn btn-sm preset-tonal-success" title="Conversion complete. New webp hosted_file created: {convert_result_kv[event_file_obj.event_file_id]?.filename ?? ''}">
|
||||
<span class="fas fa-check mx-1"></span>
|
||||
<Check size="1em" class="mx-1" />
|
||||
Done — {convert_result_kv[event_file_obj.event_file_id]?.filename ?? 'image created'}
|
||||
</span>
|
||||
{:else if convert_status_kv[event_file_obj.event_file_id] === 'error'}
|
||||
@@ -276,7 +277,7 @@
|
||||
convert_status_kv[event_file_obj.event_file_id] = 'idle';
|
||||
}}
|
||||
>
|
||||
<span class="fas fa-exclamation-triangle mx-1"></span>
|
||||
<TriangleAlert size="1em" class="mx-1" />
|
||||
Failed — Retry?
|
||||
</button>
|
||||
{/if}
|
||||
@@ -391,16 +392,12 @@
|
||||
title="Save changes"
|
||||
>
|
||||
{#await ae_promises.update__event_file_obj}
|
||||
<span
|
||||
class="fas fa-spinner fa-spin mx-1"
|
||||
></span>
|
||||
<LoaderCircle size="1em" class="mx-1 animate-spin" />
|
||||
<span class=""
|
||||
>Saving {event_file_obj.extension}</span
|
||||
>
|
||||
{:then}
|
||||
<span
|
||||
class="fas fa-save mx-1"
|
||||
></span>
|
||||
<Save size="1em" class="mx-1" />
|
||||
Save {event_file_obj.extension}
|
||||
filename?
|
||||
{/await}
|
||||
@@ -444,8 +441,7 @@
|
||||
event_file_obj.event_file_id}
|
||||
title={`Rename this file? "${event_file_obj.filename}"}`}
|
||||
>
|
||||
<span class="fas fa-edit mx-1"
|
||||
></span>
|
||||
<Pencil size="1em" class="mx-1" />
|
||||
{#if $events_sess.pres_mgmt?.show_field_edit__filename == event_file_obj.event_file_id}
|
||||
Cancel?
|
||||
{:else}
|
||||
@@ -515,20 +511,15 @@
|
||||
-->
|
||||
|
||||
{#await ae_promises.update__event_file_obj}
|
||||
<span
|
||||
class="fas fa-spinner fa-spin mx-1"
|
||||
></span>
|
||||
<LoaderCircle size="1em" class="mx-1 animate-spin" />
|
||||
<span class=""
|
||||
>Saving {event_file_obj.extension}</span
|
||||
>
|
||||
{:then}
|
||||
{#if event_file_obj.hide}
|
||||
<span class="fas fa-eye m-1"
|
||||
></span> Unhide File
|
||||
<Eye size="1em" class="m-1" /> Unhide File
|
||||
{:else}
|
||||
<span
|
||||
class="fas fa-eye-slash m-1"
|
||||
></span> Hide
|
||||
<EyeOff size="1em" class="m-1" /> Hide
|
||||
{/if}
|
||||
{/await}
|
||||
</button>
|
||||
@@ -561,8 +552,7 @@
|
||||
class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-success"
|
||||
title="Delete this file"
|
||||
>
|
||||
<span class="fas fa-trash-alt mx-1"
|
||||
></span>
|
||||
<Trash2 size="1em" class="mx-1" />
|
||||
<!-- <span class="fas fa-minus mx-1"></span> -->
|
||||
Delete
|
||||
</button>
|
||||
@@ -581,13 +571,9 @@
|
||||
>
|
||||
<div class="">
|
||||
{#if event_file_obj.open_in_os == 'win'}
|
||||
MS Windows <span
|
||||
class="fab fa-windows"
|
||||
></span>
|
||||
MS Windows <Monitor size="1em" class="inline-block" />
|
||||
{:else if event_file_obj.open_in_os == 'mac'}
|
||||
Apple macOS <span
|
||||
class="fab fa-apple"
|
||||
></span>
|
||||
Apple macOS <Laptop size="1em" class="inline-block" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -698,11 +684,7 @@
|
||||
Type:
|
||||
<strong
|
||||
>{event_file_obj.extension}
|
||||
<span
|
||||
class="fas fa-{ae_util.file_extension_icon(
|
||||
event_file_obj.extension
|
||||
)}"
|
||||
></span>
|
||||
<ExtIcon size="1em" class="inline-block" />
|
||||
</strong>
|
||||
<!-- {#if event_file_obj.open_in_os == 'win'}
|
||||
<strong>
|
||||
@@ -794,8 +776,7 @@
|
||||
<!-- <span class="fas fa-cloud-upload-alt mx-1"></span> -->
|
||||
<!-- Uploaded: -->
|
||||
<!-- <span class="fas fa-calendar-day mx-1"></span> -->
|
||||
<span class="fas fa-clock mx-1"
|
||||
></span>
|
||||
<Clock size="1em" class="mx-1" />
|
||||
<strong>
|
||||
{ae_util.iso_datetime_formatter(
|
||||
event_file_obj.created_on,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger } from '$lib/stores/ae_stores';
|
||||
import { db_core } from '$lib/ae_core/db_core';
|
||||
import { core_func } from '$lib/ae_core/ae_core_functions';
|
||||
|
||||
import { CalendarDays, Download, FolderOpen, MinusCircle, Pencil, PlusCircle, RefreshCw, Trash2 } from '@lucide/svelte';
|
||||
// export let allow_basic: boolean = false;
|
||||
|
||||
interface Props {
|
||||
@@ -60,7 +60,7 @@
|
||||
title="Files for {link_to_type ?? '-- not set --'}: {link_to_id ??
|
||||
'-- not set --'} (files: {$lq__hosted_file_obj_li?.length ?? 'None'})"
|
||||
>
|
||||
<span class="fas fa-folder-open mx-1"></span>
|
||||
<FolderOpen size="1em" class="mx-1" />
|
||||
{@html $lq__hosted_file_obj_li
|
||||
? `${$lq__hosted_file_obj_li.length}×`
|
||||
: '-- none --'}
|
||||
@@ -101,7 +101,7 @@
|
||||
class:hidden={!$ae_loc.edit_mode || !$ae_loc.authenticated_access}
|
||||
title="Refresh the list of files"
|
||||
>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
<RefreshCw size="1em" class="m-1" />
|
||||
<div class="hidden">Files</div>
|
||||
</button>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
class:hidden={!$ae_loc.edit_mode || !$ae_loc.trusted_access}
|
||||
title="Toggle direct download link and copy link button"
|
||||
>
|
||||
<span class="fas fa-download m-1"></span>
|
||||
<Download size="1em" class="m-1" />
|
||||
<div class="hidden">
|
||||
{ae_tmp.show__direct_download ? 'Alt On' : 'Alt Download Off'}
|
||||
</div>
|
||||
@@ -163,10 +163,10 @@
|
||||
title="Add/Remove file to/from the locally stored uploaded file list. This is referenced by other AE components."
|
||||
>
|
||||
{#if $ae_loc.files.uploaded_file_kv[hosted_file_obj.hosted_file_id]}
|
||||
<span class="fas fa-minus-circle m-1"></span>
|
||||
<MinusCircle size="1em" class="m-1" />
|
||||
<span class="hidden">Remove</span>
|
||||
{:else}
|
||||
<span class="fas fa-plus-circle m-1"></span>
|
||||
<PlusCircle size="1em" class="m-1" />
|
||||
<span class="hidden">Add</span>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -198,7 +198,7 @@
|
||||
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500"
|
||||
title="Delete a file from the host server."
|
||||
>
|
||||
<span class="fas fa-trash-alt m-1"></span>
|
||||
<Trash2 size="1em" class="m-1" />
|
||||
<span class="hidden">Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -212,19 +212,19 @@
|
||||
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
|
||||
title="Edit file details"
|
||||
>
|
||||
<span class="fas fa-edit m-1"></span>
|
||||
<Pencil size="1em" class="m-1" />
|
||||
<span class="hidden">Edit</span>
|
||||
</button>
|
||||
|
||||
<span class="text-xs text-gray-500">
|
||||
<span class="fas fa-calendar-day mx-1"></span>
|
||||
<CalendarDays size="1em" class="mx-1" />
|
||||
<span class="hidden">Created on:</span>
|
||||
{ae_util.iso_datetime_formatter(
|
||||
hosted_file_obj.created_on,
|
||||
'datetime_medium_sec'
|
||||
)}
|
||||
{#if hosted_file_obj.updated_on}
|
||||
<span class="fas fa-sync-alt mx-1"></span>
|
||||
<RefreshCw size="1em" class="mx-1" />
|
||||
Updated on
|
||||
{ae_util.iso_datetime_formatter(
|
||||
hosted_file_obj.updated_on,
|
||||
|
||||
93
src/lib/elements/element_pwa_install_prompt.svelte
Normal file
93
src/lib/elements/element_pwa_install_prompt.svelte
Normal file
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* src/lib/elements/element_pwa_install_prompt.svelte
|
||||
*
|
||||
* Reusable PWA install nudge. Drop this anywhere in the app to surface
|
||||
* a platform-aware "Add to Home Screen" prompt.
|
||||
*
|
||||
* - Chrome / Android / Desktop Chrome: shows a button that triggers the
|
||||
* native browser install flow (via the captured beforeinstallprompt event).
|
||||
* - iOS Safari: shows manual step-by-step "Share → Add to Home Screen" instructions
|
||||
* since iOS does not support the beforeinstallprompt API.
|
||||
* - Already installed (standalone mode) or dismissed: renders nothing.
|
||||
*
|
||||
* The pwa_install singleton must be initialised first (done in root +layout.svelte).
|
||||
*/
|
||||
import { Download, Plus, Share2, Smartphone, X } from '@lucide/svelte';
|
||||
import { pwa_install } from '$lib/pwa/pwa_install.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
let { class: extra_class = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if pwa_install.should_show}
|
||||
<div
|
||||
class="relative w-full rounded-2xl overflow-hidden
|
||||
border border-primary-500/25
|
||||
bg-primary-500/8 dark:bg-primary-500/12
|
||||
{extra_class}"
|
||||
>
|
||||
<!-- Dismiss button -->
|
||||
<button
|
||||
class="absolute top-2 right-2 p-2 rounded-lg
|
||||
opacity-40 hover:opacity-90
|
||||
hover:bg-surface-500/10
|
||||
transition-opacity"
|
||||
onclick={() => pwa_install.dismiss()}
|
||||
aria-label="Dismiss install prompt"
|
||||
>
|
||||
<X size="1rem" />
|
||||
</button>
|
||||
|
||||
<div class="p-4 pr-10 space-y-3">
|
||||
<!-- Header row: icon + text -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="shrink-0 p-2.5 rounded-xl bg-primary-500/15 text-primary-500">
|
||||
<Smartphone size="1.35em" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-sm leading-tight">Install Aether Leads</p>
|
||||
<p class="text-xs opacity-55 leading-snug mt-0.5">
|
||||
Add to your home screen for the best mobile experience
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if pwa_install.can_prompt}
|
||||
<!-- Chrome / Android / Desktop Chrome — native one-tap install -->
|
||||
<button
|
||||
class="btn preset-filled-primary w-full"
|
||||
onclick={() => pwa_install.prompt()}
|
||||
>
|
||||
<Download size="1em" />
|
||||
<span>Add to Home Screen</span>
|
||||
</button>
|
||||
{:else}
|
||||
<!-- iOS Safari — manual instructions (no API available) -->
|
||||
<ol class="space-y-2 text-sm">
|
||||
<li class="flex items-center gap-2.5">
|
||||
<span class="shrink-0 flex items-center justify-center w-5 h-5 rounded-full
|
||||
bg-primary-500/20 text-primary-600 dark:text-primary-300
|
||||
text-[10px] font-bold leading-none">1</span>
|
||||
<span class="opacity-75">
|
||||
Tap the
|
||||
<Share2 size="0.85em" class="inline align-middle mx-0.5 text-primary-500" />
|
||||
<strong>Share</strong> button in Safari
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2.5">
|
||||
<span class="shrink-0 flex items-center justify-center w-5 h-5 rounded-full
|
||||
bg-primary-500/20 text-primary-600 dark:text-primary-300
|
||||
text-[10px] font-bold leading-none">2</span>
|
||||
<span class="opacity-75">
|
||||
Tap <strong>Add to Home Screen</strong>
|
||||
<Plus size="0.85em" class="inline align-middle mx-0.5 text-primary-500" />
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
142
src/lib/pwa/pwa_install.svelte.ts
Normal file
142
src/lib/pwa/pwa_install.svelte.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* src/lib/pwa/pwa_install.svelte.ts
|
||||
*
|
||||
* Global PWA install prompt state using Svelte 5 universal reactivity.
|
||||
*
|
||||
* Usage:
|
||||
* - Call `pwa_install.init()` once in the root +layout.svelte (browser $effect).
|
||||
* - Import `pwa_install` in any component to read state and trigger the prompt.
|
||||
*
|
||||
* Platform behaviour:
|
||||
* - Chrome / Android / Desktop Chrome: intercepts `beforeinstallprompt`,
|
||||
* lets us show a custom button that calls the native install flow.
|
||||
* - iOS Safari: `beforeinstallprompt` never fires; we detect iOS + not-standalone
|
||||
* and show manual "Share → Add to Home Screen" instructions instead.
|
||||
* - If already running in standalone mode (i.e. already installed) → hide everything.
|
||||
*
|
||||
* Dismiss: persists for DISMISS_DAYS days via localStorage key `ae_pwa_install_dismissed`.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// `BeforeInstallPromptEvent` is non-standard and not included in TypeScript's default lib.
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
|
||||
}
|
||||
|
||||
const DISMISS_KEY = 'ae_pwa_install_dismissed';
|
||||
const DISMISS_DAYS = 7;
|
||||
|
||||
// --- Module-level reactive state (Svelte 5 universal reactivity) ---
|
||||
let _deferred_prompt = $state<BeforeInstallPromptEvent | null>(null);
|
||||
let _is_installed = $state(false);
|
||||
let _is_dismissed = $state(false);
|
||||
let _initialized = false; // plain boolean — not reactive, guards against double-init
|
||||
|
||||
// --- Private helpers (SSR-safe) ---
|
||||
|
||||
function _in_standalone(): boolean {
|
||||
if (!browser) return false;
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
// iOS Safari sets navigator.standalone (non-standard)
|
||||
('standalone' in window.navigator && (window.navigator as any).standalone === true)
|
||||
);
|
||||
}
|
||||
|
||||
function _is_ios(): boolean {
|
||||
if (!browser) return false;
|
||||
// Cover iPhone/iPod/iPad (incl. iPadOS 13+ which reports MacIntel with touch points)
|
||||
return (
|
||||
/iphone|ipad|ipod/i.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
export const pwa_install = {
|
||||
/**
|
||||
* True if the browser has captured a deferred install prompt
|
||||
* (Chrome / Android / PWA-capable desktop browsers).
|
||||
*/
|
||||
get can_prompt(): boolean {
|
||||
return _deferred_prompt !== null && !_is_installed && !_is_dismissed;
|
||||
},
|
||||
|
||||
/**
|
||||
* True if on iOS Safari and NOT already installed.
|
||||
* We cannot trigger a native prompt on iOS so we show manual instructions instead.
|
||||
*/
|
||||
get is_ios_nudge(): boolean {
|
||||
return _is_ios() && !_in_standalone() && !_is_dismissed && !_is_installed;
|
||||
},
|
||||
|
||||
/** True if the install UI should be rendered at all. */
|
||||
get should_show(): boolean {
|
||||
return pwa_install.can_prompt || pwa_install.is_ios_nudge;
|
||||
},
|
||||
|
||||
/** True once the app is confirmed installed (standalone mode or appinstalled event). */
|
||||
get is_installed(): boolean {
|
||||
return _is_installed;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register browser event listeners. Call ONCE from root +layout.svelte.
|
||||
* Safe to call multiple times (idempotent).
|
||||
*/
|
||||
init(): void {
|
||||
if (!browser || _initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
// Already running as installed PWA — nothing to prompt.
|
||||
if (_in_standalone()) {
|
||||
_is_installed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore dismiss state from localStorage (with expiry check).
|
||||
const raw = localStorage.getItem(DISMISS_KEY);
|
||||
if (raw) {
|
||||
const days_since = (Date.now() - parseInt(raw, 10)) / 86_400_000;
|
||||
if (days_since < DISMISS_DAYS) {
|
||||
_is_dismissed = true;
|
||||
} else {
|
||||
// Expired — clear so the nudge can reappear.
|
||||
localStorage.removeItem(DISMISS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the deferred install prompt (Chrome/Android/Desktop Chrome).
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
_deferred_prompt = e as BeforeInstallPromptEvent;
|
||||
});
|
||||
|
||||
// Fired by the browser after the user accepts the install prompt.
|
||||
window.addEventListener('appinstalled', () => {
|
||||
_is_installed = true;
|
||||
_deferred_prompt = null;
|
||||
});
|
||||
},
|
||||
|
||||
/** Trigger the native install prompt. No-op if no deferred prompt is available. */
|
||||
async prompt(): Promise<void> {
|
||||
if (!_deferred_prompt) return;
|
||||
// Grab and clear to prevent double-fire.
|
||||
const evt = _deferred_prompt;
|
||||
_deferred_prompt = null;
|
||||
await evt.prompt();
|
||||
const { outcome } = await evt.userChoice;
|
||||
if (outcome === 'accepted') {
|
||||
_is_installed = true;
|
||||
}
|
||||
},
|
||||
|
||||
/** Dismiss the nudge; it will not re-appear for DISMISS_DAYS days. */
|
||||
dismiss(): void {
|
||||
_is_dismissed = true;
|
||||
if (browser) localStorage.setItem(DISMISS_KEY, Date.now().toString());
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user