feat(neode-ui): mobile app-launch UX — store-driven panel, loader, ElectrumX icon
- Mobile launches use the store-driven panel (no route push) so the background tab no longer changes and closing returns to where you launched from. - Tab-only apps open directly (in-app WebView on companion / new tab on PWA) — no "this app opens in a tab" interstitial. - Shared AppLoadingScreen (app icon + progress bar) on the app session and the legacy iframe overlay instead of a black screen. - Pin the dashboard to 100dvh on mobile so the mesh chat/tools panes stop sliding under the bottom tab bar in mobile browsers (no-op in the companion WebView). - ElectrumX/electrs/electrs-ui ids now resolve to the real ElectrumX icon in My Apps. - isMobile made reactive so overlay/footer/teleport decisions track the viewport. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
db7d424bff
commit
a7c7c44843
@@ -10,7 +10,14 @@ export type AppsTab = 'apps' | 'websites' | 'services'
|
||||
// Service container name patterns (backend/infra, not user-facing)
|
||||
export const SERVICE_NAMES = new Set([
|
||||
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
||||
// Headless backends with no user-facing UI: the Fedimint ecash client daemon,
|
||||
// the Nostr relay, and the Meshtastic LoRa daemon (its chat UI lives in the
|
||||
// built-in Mesh tab) belong in Services, not My Apps.
|
||||
'fedimint-clientd', 'nostr-rs-relay', 'meshtastic',
|
||||
'immich_postgres', 'immich_redis',
|
||||
// immich is now a manifest-driven stack (app_id-named, hyphen). The server is
|
||||
// the launcher app; postgres/redis are backends → Services.
|
||||
'immich-postgres', 'immich-redis',
|
||||
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
|
||||
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
|
||||
'bitcoin-ui', 'lnd-ui', 'electrs-ui',
|
||||
@@ -37,7 +44,10 @@ export function isServiceContainer(id: string): boolean {
|
||||
if (SERVICE_NAMES.has(id)) return true
|
||||
if (id.startsWith('indeedhub-build_')) return true
|
||||
if (id.startsWith('archy-')) return true
|
||||
if (id.endsWith('_db') || id.endsWith('-db')) return true
|
||||
// Backend naming patterns that never carry a user-facing UI: databases and
|
||||
// caches. Safe to classify by suffix (a database is never a launcher).
|
||||
if (/-(db|postgres|postgresql|redis|valkey|mariadb|mysql|cache)$/.test(id)) return true
|
||||
if (id.endsWith('_db')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -96,8 +106,11 @@ export function isWebsitePackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||
// Curated known apps stay in My Apps even if their manifest predates the UI
|
||||
// interface field.
|
||||
if (isKnownApp(id, pkg)) return false
|
||||
// Fallback: reachable on the LAN but declares no UI → treat as a website.
|
||||
return !!pkg && !!runtimeLanAddress(pkg)
|
||||
// Anything still here has no declared UI and isn't a known launcher app:
|
||||
// databases, APIs, backends, workers. They belong in Services (not My Apps),
|
||||
// whether or not they expose a LAN address. (#10 — "anything that isn't the
|
||||
// frontend UI launcher".)
|
||||
return !!pkg
|
||||
}
|
||||
|
||||
export function filterEntriesForTab(
|
||||
@@ -113,10 +126,33 @@ export function filterEntriesForTab(
|
||||
if (activeTab === 'apps' && selectedCategory !== 'all') {
|
||||
return getAppCategory(id, pkg) === selectedCategory
|
||||
}
|
||||
if (activeTab === 'services' && selectedCategory !== 'all') {
|
||||
return getServiceCategory(id, pkg) === selectedCategory
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Group a (non-launcher) service container by type for the Services tab sub-nav
|
||||
// (#12). Heuristic over the container id + manifest id.
|
||||
export function getServiceCategory(id: string, pkg?: PackageDataEntry): string {
|
||||
const s = `${id} ${pkg?.manifest?.id || ''}`.toLowerCase()
|
||||
if (/postgres|mariadb|mysql|(^|[-_])db([-_]|$)/.test(s)) return 'database'
|
||||
if (/redis|valkey|(^|[-_])cache([-_]|$)/.test(s)) return 'cache'
|
||||
if (/(^|[-_])api([-_]|$)/.test(s)) return 'api'
|
||||
return 'backend'
|
||||
}
|
||||
|
||||
export function buildServiceCategories(t: (key: string) => string): Array<{ id: string; name: string }> {
|
||||
return [
|
||||
{ id: 'all', name: t('marketplace.all') },
|
||||
{ id: 'database', name: 'Databases' },
|
||||
{ id: 'cache', name: 'Caches' },
|
||||
{ id: 'api', name: 'APIs' },
|
||||
{ id: 'backend', name: 'Backends' },
|
||||
]
|
||||
}
|
||||
|
||||
// Web-only app IDs and their URLs
|
||||
export const WEB_ONLY_APP_URLS: Record<string, string> = {
|
||||
'nwnn': 'https://nwnn.l484.com',
|
||||
@@ -178,8 +214,56 @@ export function opensInTab(id: string): boolean {
|
||||
return TAB_LAUNCH_APPS.has(id)
|
||||
}
|
||||
|
||||
// Backend services that ship no icon of their own reuse their PARENT app's icon
|
||||
// (#14) so they render the app's logo instead of a 404 → 📦 placeholder. Paths
|
||||
// are explicit because icon extensions vary (.png / .webp / .svg).
|
||||
const APP_ICON_FALLBACKS: Record<string, string> = {
|
||||
gitea: '/assets/img/app-icons/gitea.svg',
|
||||
'fedimint-gateway': '/assets/img/app-icons/fedimint.png',
|
||||
'fedimint-clientd': '/assets/img/app-icons/fedimint.png',
|
||||
// immich stack
|
||||
'immich-postgres': '/assets/img/app-icons/immich.png',
|
||||
'immich-redis': '/assets/img/app-icons/immich.png',
|
||||
'immich-server': '/assets/img/app-icons/immich.png',
|
||||
'immich_postgres': '/assets/img/app-icons/immich.png',
|
||||
'immich_redis': '/assets/img/app-icons/immich.png',
|
||||
// btcpay stack
|
||||
'archy-btcpay-db': '/assets/img/app-icons/btcpay-server.png',
|
||||
'archy-nbxplorer': '/assets/img/app-icons/btcpay-server.png',
|
||||
// mempool stack
|
||||
'archy-mempool-db': '/assets/img/app-icons/mempool.webp',
|
||||
'mempool-api': '/assets/img/app-icons/mempool.webp',
|
||||
'archy-mempool-web': '/assets/img/app-icons/mempool.webp',
|
||||
'mysql-mempool': '/assets/img/app-icons/mempool.webp',
|
||||
// bitcoin / lightning companion UIs
|
||||
'archy-bitcoin-ui': '/assets/img/app-icons/bitcoin-knots.webp',
|
||||
'archy-lnd-ui': '/assets/img/app-icons/lnd.svg',
|
||||
'archy-electrs-ui': '/assets/img/app-icons/electrumx.png',
|
||||
// ElectrumX ships under a few historical ids (the backend was renamed
|
||||
// electrs → electrumx). Without an explicit map, an `electrs`-keyed install
|
||||
// falls through to the default `/assets/img/app-icons/electrs.png`, which
|
||||
// doesn't exist → handleImageError swaps .png→.svg and lands on electrs.svg
|
||||
// (the "Electrs in Rust" logo) instead of the real ElectrumX icon. Pin the
|
||||
// whole family to the ElectrumX icon so My Apps shows the right logo no
|
||||
// matter which id the node has it installed under.
|
||||
'electrs': '/assets/img/app-icons/electrumx.png',
|
||||
'electrs-ui': '/assets/img/app-icons/electrumx.png',
|
||||
'electrumx': '/assets/img/app-icons/electrumx.png',
|
||||
}
|
||||
|
||||
// Parent-app icon by prefix, for stack members not listed explicitly above
|
||||
// (e.g. every indeedhub-* sub-container → indeedhub).
|
||||
const SERVICE_ICON_PREFIXES: Array<[string, string]> = [
|
||||
['indeedhub-', '/assets/img/app-icons/indeedhub.png'],
|
||||
['immich-', '/assets/img/app-icons/immich.png'],
|
||||
['immich_', '/assets/img/app-icons/immich.png'],
|
||||
]
|
||||
|
||||
function serviceParentIcon(id: string): string | undefined {
|
||||
for (const [prefix, icon] of SERVICE_ICON_PREFIXES) {
|
||||
if (id.startsWith(prefix)) return icon
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const DEFAULT_APP_ICON = '/assets/icon/favico-black-v2.svg'
|
||||
@@ -195,7 +279,12 @@ export function resolveAppIcon(id: string, pkg: PackageDataEntry, curatedIcon?:
|
||||
) {
|
||||
return icon
|
||||
}
|
||||
return curatedIcon || APP_ICON_FALLBACKS[id] || `/assets/img/app-icons/${id}.png`
|
||||
return (
|
||||
curatedIcon ||
|
||||
APP_ICON_FALLBACKS[id] ||
|
||||
serviceParentIcon(id) ||
|
||||
`/assets/img/app-icons/${id}.png`
|
||||
)
|
||||
}
|
||||
|
||||
export function canLaunch(pkg: PackageDataEntry): boolean {
|
||||
@@ -302,6 +391,21 @@ export function useCategoriesWithApps(
|
||||
})
|
||||
}
|
||||
|
||||
// Services-tab equivalent of useCategoriesWithApps: only show a service category
|
||||
// when at least one installed service belongs to it (#12).
|
||||
export function useServiceCategories(
|
||||
packages: Ref<Record<string, PackageDataEntry>>,
|
||||
serviceCategories: Ref<Array<{ id: string; name: string }>>,
|
||||
) {
|
||||
return computed(() => {
|
||||
const entries = Object.entries(packages.value).filter(([id, pkg]) => isWebsitePackage(id, pkg) && !isInternalToolingPackage(id, pkg))
|
||||
return serviceCategories.value.filter(cat => {
|
||||
if (cat.id === 'all') return true
|
||||
return entries.some(([id, pkg]) => getServiceCategory(id, pkg) === cat.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function handleImageError(e: Event) {
|
||||
const target = e.target as HTMLImageElement
|
||||
const currentSrc = target.src
|
||||
|
||||
Reference in New Issue
Block a user