Some checks failed
Deploy to Coolify / Code Quality (pull_request) Has been cancelled
Deploy to Coolify / Run Tests (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Development (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Production (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Test (pull_request) Has been cancelled
Pull Request Checks / Validate PR (pull_request) Has been cancelled
- Create usePWAInstall composable for install management - Add InstallPrompt banner component with auto-show after 3s - Add App Settings tab in settings page - Show install button with loading state - Display installation status and instructions - Handle dismissal with 7-day cooldown - Add iOS/Android installation guides - Show PWA features list - Display storage usage with visual progress - Auto-hide prompt after successful install Features: - Automatic install prompt after 3 seconds - Manual install from settings - Platform-specific instructions - Smart dismissal tracking - Storage info visualization Closes #35
191 lines
7.0 KiB
Vue
191 lines
7.0 KiB
Vue
<template>
|
|
<UCard class="mt-4">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="text-lg font-semibold mb-4">App Installation</h3>
|
|
|
|
<!-- Already installed -->
|
|
<div v-if="isInstalled" class="flex items-start gap-3 p-4 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg">
|
|
<UIcon name="i-heroicons-check-circle" class="w-6 h-6 text-emerald-600 dark:text-emerald-400 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p class="font-medium text-emerald-900 dark:text-emerald-100">
|
|
App is installed
|
|
</p>
|
|
<p class="text-sm text-emerald-700 dark:text-emerald-300 mt-1">
|
|
You're using Pantry as an installed app with offline support.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Can install -->
|
|
<div v-else-if="canInstall" class="space-y-3">
|
|
<p class="text-gray-600 dark:text-gray-400">
|
|
Install Pantry as an app for quick access and offline use.
|
|
</p>
|
|
<UButton
|
|
color="emerald"
|
|
size="lg"
|
|
@click="install"
|
|
:loading="installing"
|
|
block
|
|
>
|
|
<template #leading>
|
|
<UIcon name="i-heroicons-arrow-down-tray" />
|
|
</template>
|
|
Install App
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- Not supported or already running -->
|
|
<div v-else class="space-y-4">
|
|
<div v-if="isStandalone" class="flex items-start gap-3 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
<UIcon name="i-heroicons-information-circle" class="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p class="font-medium text-blue-900 dark:text-blue-100">
|
|
Running as standalone app
|
|
</p>
|
|
<p class="text-sm text-blue-700 dark:text-blue-300 mt-1">
|
|
You're already using Pantry in standalone mode.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
Installation is not available yet. Try one of these options:
|
|
</p>
|
|
|
|
<!-- iOS Instructions -->
|
|
<UCard class="mb-4">
|
|
<div class="flex items-start gap-3">
|
|
<UIcon name="i-heroicons-device-phone-mobile" class="w-6 h-6 text-gray-400 flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<h4 class="font-semibold mb-2">iOS (Safari)</h4>
|
|
<ol class="text-sm text-gray-600 dark:text-gray-400 space-y-1 list-decimal list-inside">
|
|
<li>Tap the Share button</li>
|
|
<li>Scroll down and tap "Add to Home Screen"</li>
|
|
<li>Tap "Add" to confirm</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Android Instructions -->
|
|
<UCard>
|
|
<div class="flex items-start gap-3">
|
|
<UIcon name="i-heroicons-device-phone-mobile" class="w-6 h-6 text-gray-400 flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<h4 class="font-semibold mb-2">Android (Chrome)</h4>
|
|
<ol class="text-sm text-gray-600 dark:text-gray-400 space-y-1 list-decimal list-inside">
|
|
<li>Tap the menu (⋮) button</li>
|
|
<li>Tap "Add to Home screen" or "Install app"</li>
|
|
<li>Tap "Install" to confirm</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PWA Features -->
|
|
<div>
|
|
<h3 class="text-lg font-semibold mb-3">App Features</h3>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center gap-2 text-gray-700 dark:text-gray-300">
|
|
<UIcon name="i-heroicons-check" class="w-5 h-5 text-emerald-500" />
|
|
<span>Works offline with cached data</span>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-gray-700 dark:text-gray-300">
|
|
<UIcon name="i-heroicons-check" class="w-5 h-5 text-emerald-500" />
|
|
<span>Quick access from home screen</span>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-gray-700 dark:text-gray-300">
|
|
<UIcon name="i-heroicons-check" class="w-5 h-5 text-emerald-500" />
|
|
<span>Full-screen experience</span>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-gray-700 dark:text-gray-300">
|
|
<UIcon name="i-heroicons-check" class="w-5 h-5 text-emerald-500" />
|
|
<span>Automatic updates</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Storage Info -->
|
|
<div v-if="storageInfo">
|
|
<h3 class="text-lg font-semibold mb-3">Storage Usage</h3>
|
|
<div class="space-y-2">
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-600 dark:text-gray-400">Used</span>
|
|
<span class="font-medium">{{ formatBytes(storageInfo.usage) }}</span>
|
|
</div>
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-600 dark:text-gray-400">Available</span>
|
|
<span class="font-medium">{{ formatBytes(storageInfo.quota) }}</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div
|
|
class="bg-emerald-500 h-2 rounded-full transition-all"
|
|
:style="{ width: storagePercent + '%' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { canInstall, isInstalled, promptInstall } = usePWAInstall()
|
|
const installing = ref(false)
|
|
const storageInfo = ref<{ usage: number; quota: number } | null>(null)
|
|
|
|
const isStandalone = computed(() => {
|
|
if (process.client) {
|
|
return window.matchMedia('(display-mode: standalone)').matches
|
|
}
|
|
return false
|
|
})
|
|
|
|
const storagePercent = computed(() => {
|
|
if (!storageInfo.value) return 0
|
|
return Math.round((storageInfo.value.usage / storageInfo.value.quota) * 100)
|
|
})
|
|
|
|
async function install() {
|
|
installing.value = true
|
|
try {
|
|
await promptInstall()
|
|
} catch (error) {
|
|
console.error('Install failed:', error)
|
|
} finally {
|
|
installing.value = false
|
|
}
|
|
}
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (bytes === 0) return '0 Bytes'
|
|
const k = 1024
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// Get storage info
|
|
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
|
try {
|
|
const estimate = await navigator.storage.estimate()
|
|
if (estimate.usage !== undefined && estimate.quota !== undefined) {
|
|
storageInfo.value = {
|
|
usage: estimate.usage,
|
|
quota: estimate.quota
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to get storage estimate:', error)
|
|
}
|
|
}
|
|
})
|
|
</script>
|