frontend: polish app launch and release experience
This commit is contained in:
@@ -18,15 +18,21 @@
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
:aria-label="getTitle(id, pkg)"
|
||||
@pointerdown="startLongPress(id)"
|
||||
@pointerup="clearLongPress"
|
||||
@pointercancel="clearLongPress"
|
||||
@pointerleave="clearLongPress"
|
||||
@contextmenu.prevent="openAppOptions(id)"
|
||||
@click="handleTap(id, pkg)"
|
||||
@keydown.enter="handleTap(id, pkg)"
|
||||
@keydown.space.prevent="openAppOptions(id)"
|
||||
>
|
||||
<!-- Icon with status indicator -->
|
||||
<div class="app-icon-frame">
|
||||
<img
|
||||
:src="getIcon(id, pkg)"
|
||||
:alt="getTitle(id, pkg)"
|
||||
class="app-icon-img"
|
||||
class="app-icon-img archy-app-icon"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<!-- Status dot -->
|
||||
@@ -44,7 +50,7 @@
|
||||
></span>
|
||||
<!-- Installing overlay -->
|
||||
<div
|
||||
v-if="serverStore.isInstalling(id)"
|
||||
v-if="serverStore.isInstalling(id) || serverStore.uninstallingApps.has(id)"
|
||||
class="app-icon-installing"
|
||||
>
|
||||
<svg class="animate-spin h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
||||
@@ -55,6 +61,13 @@
|
||||
</div>
|
||||
<!-- Label -->
|
||||
<span class="app-icon-label">{{ getTitle(id, pkg) }}</span>
|
||||
<span
|
||||
v-if="serverStore.isInstalling(id) || serverStore.uninstallingApps.has(id)"
|
||||
class="app-icon-progress-label"
|
||||
:title="progressLabel(id, pkg)"
|
||||
>
|
||||
{{ progressLabel(id, pkg) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +85,7 @@
|
||||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div v-if="credentialModal.show" class="fixed inset-0 z-[2700] flex items-end justify-center bg-black/60 backdrop-blur-md p-0 md:items-center md:p-6" @click.self="closeCredentialModal">
|
||||
<div v-if="credentialModal.show" class="credential-modal-overlay fixed inset-0 z-[2700] flex items-center justify-center bg-black/60 backdrop-blur-md p-4 md:p-6" @click.self="closeCredentialModal">
|
||||
<div class="sideload-modal credential-modal">
|
||||
<div class="flex items-start justify-between gap-4 mb-5">
|
||||
<div>
|
||||
@@ -107,6 +120,7 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import type { AppCredential, AppCredentialsResponse, PackageDataEntry } from '@/types/api'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
||||
import { resolveAppCredentials } from './appCredentials'
|
||||
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl, WEB_ONLY_APP_URLS } from './appsConfig'
|
||||
import { getCuratedAppList } from '../discover/curatedApps'
|
||||
|
||||
@@ -135,6 +149,8 @@ const emit = defineEmits<{
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const activePage = ref(0)
|
||||
const longPressTriggered = ref(false)
|
||||
let longPressTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const pages = computed(() => {
|
||||
const result: [string, PackageDataEntry][][] = []
|
||||
@@ -154,7 +170,22 @@ function getIcon(id: string, pkg: PackageDataEntry): string {
|
||||
return resolveAppIcon(id, pkg, curatedMap.get(id)?.icon)
|
||||
}
|
||||
|
||||
function progressLabel(id: string, pkg: PackageDataEntry): string {
|
||||
const install = serverStore.installingApps.get(id)
|
||||
if (install) {
|
||||
return `${install.message || 'Installing...'} ${Math.round(install.progress || 0)}%`
|
||||
}
|
||||
if (serverStore.uninstallingApps.has(id)) {
|
||||
return pkg['uninstall-stage'] || ((pkg as unknown as Record<string, unknown>).uninstall_stage as string | undefined) || 'Removing...'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function handleTap(id: string, pkg: PackageDataEntry) {
|
||||
if (longPressTriggered.value) {
|
||||
longPressTriggered.value = false
|
||||
return
|
||||
}
|
||||
if (canLaunch(pkg)) {
|
||||
const shown = await maybeShowCredentialsBeforeLaunch(id, pkg)
|
||||
if (shown) return
|
||||
@@ -164,6 +195,26 @@ async function handleTap(id: string, pkg: PackageDataEntry) {
|
||||
}
|
||||
}
|
||||
|
||||
function startLongPress(id: string) {
|
||||
clearLongPress()
|
||||
longPressTriggered.value = false
|
||||
longPressTimer = setTimeout(() => {
|
||||
longPressTriggered.value = true
|
||||
openAppOptions(id)
|
||||
}, 550)
|
||||
}
|
||||
|
||||
function clearLongPress() {
|
||||
if (!longPressTimer) return
|
||||
clearTimeout(longPressTimer)
|
||||
longPressTimer = null
|
||||
}
|
||||
|
||||
function openAppOptions(id: string) {
|
||||
clearLongPress()
|
||||
emit('goToApp', id)
|
||||
}
|
||||
|
||||
function launchNow(id: string, pkg: PackageDataEntry) {
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
||||
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
||||
@@ -191,18 +242,29 @@ function launchNow(id: string, pkg: PackageDataEntry) {
|
||||
async function maybeShowCredentialsBeforeLaunch(id: string, pkg: PackageDataEntry): Promise<boolean> {
|
||||
try {
|
||||
const result = await rpcClient.call<AppCredentialsResponse>({ method: 'package.credentials', params: { app_id: id }, timeout: 5000 })
|
||||
if (!result.credentials?.length) return false
|
||||
const credentials = resolveAppCredentials(id, result)
|
||||
if (!credentials) return false
|
||||
credentialModal.value = {
|
||||
show: true,
|
||||
appId: id,
|
||||
title: result.title || `${getTitle(id, pkg)} credentials`,
|
||||
description: result.description || 'Use these credentials when the app asks you to sign in.',
|
||||
credentials: result.credentials,
|
||||
title: credentials.title || `${getTitle(id, pkg)} credentials`,
|
||||
description: credentials.description || 'Use these credentials when the app asks you to sign in.',
|
||||
credentials: credentials.credentials,
|
||||
copied: '',
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
const credentials = resolveAppCredentials(id, null)
|
||||
if (!credentials) return false
|
||||
credentialModal.value = {
|
||||
show: true,
|
||||
appId: id,
|
||||
title: credentials.title || `${getTitle(id, pkg)} credentials`,
|
||||
description: credentials.description || 'Use these credentials when the app asks you to sign in.',
|
||||
credentials: credentials.credentials,
|
||||
copied: '',
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +332,12 @@ function scrollToPage(index: number) {
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.credential-modal {
|
||||
max-height: calc(100dvh - var(--safe-area-top, env(safe-area-inset-top, 0px)) - var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) - 2rem);
|
||||
border-radius: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.credential-modal-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user