You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
595 lines
16 KiB
595 lines
16 KiB
<script lang="ts"> |
|
import { prewarmCaches, type PrewarmProgress } from '../../services/cache/cache-prewarmer.js'; |
|
import { markVersionUpdated, getAppVersion } from '../../services/version-manager.js'; |
|
import { getDB } from '../../services/cache/indexeddb-store.js'; |
|
import { getNewestChangelog } from '../../services/changelog.js'; |
|
import Icon from '../ui/Icon.svelte'; |
|
import { onMount } from 'svelte'; |
|
import { goto } from '$app/navigation'; |
|
|
|
interface Props { |
|
onComplete: () => void; |
|
} |
|
|
|
let { onComplete }: Props = $props(); |
|
|
|
let updating = $state(false); |
|
let updateComplete = $state(false); |
|
let showPWAUpdatePrompt = $state(false); |
|
let appVersion = $state('0.2.0'); |
|
let isPWAInstalled = $state(false); |
|
let newestChangelog = $state<string[]>([]); |
|
let progress = $state<PrewarmProgress>({ |
|
step: 'Preparing update...', |
|
progress: 0, |
|
total: 7, |
|
current: 0 |
|
}); |
|
|
|
onMount(async () => { |
|
appVersion = await getAppVersion(); |
|
newestChangelog = await getNewestChangelog(); |
|
|
|
// Check if PWA is installed |
|
if (typeof window !== 'undefined') { |
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches; |
|
const isIOSStandalone = (window.navigator as any).standalone === true; |
|
isPWAInstalled = isStandalone || isIOSStandalone; |
|
} |
|
}); |
|
|
|
async function handleUpdate() { |
|
updating = true; |
|
progress = { |
|
step: 'Starting update...', |
|
progress: 0, |
|
total: 7, |
|
current: 0 |
|
}; |
|
|
|
try { |
|
// Step 1: Ensure database is up to date (with timeout) |
|
progress = { |
|
step: 'Updating database structure...', |
|
progress: 5, |
|
total: 8, |
|
current: 1 |
|
}; |
|
|
|
// Add timeout to prevent hanging |
|
await Promise.race([ |
|
getDB(), |
|
new Promise((_, reject) => |
|
setTimeout(() => reject(new Error('Database update timed out after 10 seconds')), 10000) |
|
) |
|
]).catch(async (error) => { |
|
// If timeout, try to continue anyway (database might still work) |
|
console.warn('Database update timeout, continuing:', error); |
|
try { |
|
// Try to get DB one more time without timeout |
|
await getDB(); |
|
} catch (e) { |
|
console.warn('Failed to get database after timeout:', e); |
|
// Continue anyway - the app might still work |
|
} |
|
}); |
|
|
|
// Step 2: Clean up service workers and caches |
|
progress = { |
|
step: 'Cleaning up service workers...', |
|
progress: 10, |
|
total: 8, |
|
current: 2 |
|
}; |
|
await cleanupServiceWorkers(); |
|
|
|
// Step 3: Prewarm caches |
|
await prewarmCaches((prog) => { |
|
progress = { |
|
...prog, |
|
progress: 15 + Math.round((prog.progress * 75) / 100) // Map 0-100 to 15-90 |
|
}; |
|
}); |
|
|
|
// Step 4: Mark version as updated |
|
progress = { |
|
step: 'Finalizing update...', |
|
progress: 95, |
|
total: 8, |
|
current: 8 |
|
}; |
|
await markVersionUpdated(); |
|
|
|
// Step 5: Complete |
|
progress = { |
|
step: 'Update complete!', |
|
progress: 100, |
|
total: 8, |
|
current: 8 |
|
}; |
|
|
|
// Small delay to show completion |
|
await new Promise(resolve => setTimeout(resolve, 500)); |
|
|
|
updateComplete = true; |
|
|
|
// If PWA is installed, ask if they want to update it |
|
if (isPWAInstalled) { |
|
showPWAUpdatePrompt = true; |
|
} else { |
|
// Not a PWA, route to about page and complete |
|
onComplete(); |
|
goto('/about'); |
|
} |
|
} catch (error) { |
|
progress = { |
|
step: `Update error: ${error instanceof Error ? error.message : 'Unknown error'}`, |
|
progress: 0, |
|
total: 7, |
|
current: 0 |
|
}; |
|
console.error('Update failed:', error); |
|
} |
|
} |
|
|
|
/** |
|
* Clean up service workers and caches to prevent corrupted content errors |
|
*/ |
|
async function cleanupServiceWorkers(): Promise<void> { |
|
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { |
|
return; |
|
} |
|
|
|
try { |
|
// Get all service worker registrations |
|
const registrations = await navigator.serviceWorker.getRegistrations(); |
|
|
|
// Unregister all service workers |
|
await Promise.all( |
|
registrations.map(async (registration) => { |
|
try { |
|
// Clear all caches for this registration |
|
if (registration.active) { |
|
const cacheNames = await caches.keys(); |
|
await Promise.all( |
|
cacheNames.map(cacheName => caches.delete(cacheName)) |
|
); |
|
} |
|
|
|
// Unregister the service worker |
|
const unregistered = await registration.unregister(); |
|
if (unregistered) { |
|
console.log('Service worker unregistered successfully'); |
|
} |
|
} catch (error) { |
|
console.warn('Failed to unregister service worker:', error); |
|
// Continue with other registrations even if one fails |
|
} |
|
}) |
|
); |
|
|
|
// Also try to clear all caches (in case some weren't associated with registrations) |
|
try { |
|
const cacheNames = await caches.keys(); |
|
await Promise.all( |
|
cacheNames.map(cacheName => caches.delete(cacheName)) |
|
); |
|
if (cacheNames.length > 0) { |
|
console.log(`Cleared ${cacheNames.length} cache(s)`); |
|
} |
|
} catch (error) { |
|
console.warn('Failed to clear some caches:', error); |
|
// Non-critical, continue |
|
} |
|
} catch (error) { |
|
console.warn('Service worker cleanup encountered errors (non-critical):', error); |
|
// Non-critical - continue with update even if cleanup fails |
|
} |
|
} |
|
|
|
async function handlePWAUpdate() { |
|
try { |
|
// Check for service worker update |
|
if ('serviceWorker' in navigator) { |
|
const registration = await navigator.serviceWorker.getRegistration(); |
|
if (registration) { |
|
// Check for updates |
|
await registration.update(); |
|
|
|
// If there's a waiting service worker, skip waiting and reload |
|
if (registration.waiting) { |
|
registration.waiting.postMessage({ type: 'SKIP_WAITING' }); |
|
} |
|
|
|
// Store route to about page before reload |
|
sessionStorage.setItem('postUpdateRedirect', '/about'); |
|
|
|
// Reload to apply update |
|
window.location.reload(); |
|
return; |
|
} |
|
} |
|
|
|
// If no service worker or update mechanism, route to about and reload |
|
sessionStorage.setItem('postUpdateRedirect', '/about'); |
|
window.location.reload(); |
|
} catch (error) { |
|
console.error('PWA update failed:', error); |
|
// Still reload to ensure update is applied |
|
sessionStorage.setItem('postUpdateRedirect', '/about'); |
|
window.location.reload(); |
|
} |
|
} |
|
</script> |
|
|
|
<div |
|
class="update-modal-overlay" |
|
role="dialog" |
|
aria-modal="true" |
|
aria-labelledby="update-modal-title" |
|
onclick={(e) => e.target === e.currentTarget && !updating && onComplete()} |
|
onkeydown={(e) => { |
|
if (e.key === 'Escape' && !updating) { |
|
onComplete(); |
|
} |
|
}} |
|
tabindex="-1" |
|
> |
|
<div class="update-modal"> |
|
<div class="update-modal-header"> |
|
<h2 id="update-modal-title" class="update-modal-title">New Version Available</h2> |
|
<p class="update-modal-subtitle"> |
|
Version {appVersion} is now available. Update to get the latest features and improvements. |
|
</p> |
|
</div> |
|
|
|
{#if !updating} |
|
<div class="update-modal-content"> |
|
{#if newestChangelog.length > 0} |
|
<div class="whats-new-section"> |
|
<h3 class="whats-new-title">What's New in Version {appVersion}</h3> |
|
<ul class="whats-new-list"> |
|
{#each newestChangelog as change} |
|
<li>{change}</li> |
|
{/each} |
|
</ul> |
|
</div> |
|
{/if} |
|
|
|
<div class="update-process-section"> |
|
<p class="update-modal-description"> |
|
This update will: |
|
</p> |
|
<ul class="update-modal-list"> |
|
<li>Update the database structure</li> |
|
<li>Preload common data for faster performance</li> |
|
<li>Prepare caches for quick access</li> |
|
</ul> |
|
</div> |
|
</div> |
|
|
|
<div class="update-modal-actions"> |
|
<button |
|
onclick={handleUpdate} |
|
class="update-button" |
|
aria-label="Update to new version" |
|
> |
|
<Icon name="download" size={16} /> |
|
<span>Update Now</span> |
|
</button> |
|
<button |
|
onclick={() => { |
|
onComplete(); |
|
goto('/about'); |
|
}} |
|
class="update-button-secondary" |
|
aria-label="Update later" |
|
> |
|
Update Later |
|
</button> |
|
</div> |
|
{:else if !updateComplete} |
|
<div class="update-modal-content"> |
|
<div class="update-progress"> |
|
<div class="update-progress-bar-container"> |
|
<div |
|
class="update-progress-bar" |
|
style="width: {progress.progress}%" |
|
></div> |
|
</div> |
|
<p class="update-progress-text"> |
|
{progress.step} ({progress.progress}%) |
|
</p> |
|
<p class="update-progress-step"> |
|
Step {progress.current} of {progress.total} |
|
</p> |
|
</div> |
|
</div> |
|
{:else if showPWAUpdatePrompt} |
|
<div class="update-modal-content"> |
|
<p class="update-modal-description"> |
|
The app update is complete! Since you have aitherboard installed as a PWA, would you like to update it now? |
|
</p> |
|
<p class="update-modal-note"> |
|
This will reload the app to apply the PWA update. |
|
</p> |
|
</div> |
|
<div class="update-modal-actions"> |
|
<button |
|
onclick={handlePWAUpdate} |
|
class="update-button" |
|
aria-label="Update PWA now" |
|
> |
|
<Icon name="download" size={16} /> |
|
<span>Update PWA Now</span> |
|
</button> |
|
<button |
|
onclick={() => { |
|
onComplete(); |
|
goto('/about'); |
|
}} |
|
class="update-button-secondary" |
|
aria-label="Update PWA later" |
|
> |
|
Update Later |
|
</button> |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
|
|
<style> |
|
.update-modal-overlay { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: rgba(0, 0, 0, 0.5); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
z-index: 10000; |
|
padding: 1rem; |
|
} |
|
|
|
.update-modal { |
|
background: var(--fog-post, #ffffff); |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.5rem; |
|
max-width: 500px; |
|
width: 100%; |
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
:global(.dark) .update-modal { |
|
background: var(--fog-dark-post, #334155); |
|
border-color: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.update-modal-header { |
|
padding: 1.5rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .update-modal-header { |
|
border-bottom-color: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.update-modal-title { |
|
margin: 0 0 0.5rem 0; |
|
font-size: 1.25rem; |
|
font-weight: 600; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .update-modal-title { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.update-modal-subtitle { |
|
margin: 0; |
|
font-size: 0.875rem; |
|
color: var(--fog-text-light, #52667a); |
|
} |
|
|
|
:global(.dark) .update-modal-subtitle { |
|
color: var(--fog-dark-text-light, #a8b8d0); |
|
} |
|
|
|
.update-modal-content { |
|
padding: 1.5rem; |
|
} |
|
|
|
.update-modal-description { |
|
margin: 0 0 1rem 0; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .update-modal-description { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.update-modal-list { |
|
margin: 0; |
|
padding-left: 1.5rem; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .update-modal-list { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.update-modal-list li { |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.whats-new-section { |
|
margin-bottom: 1.5rem; |
|
padding-bottom: 1.5rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .whats-new-section { |
|
border-bottom-color: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.whats-new-title { |
|
margin: 0 0 0.75rem 0; |
|
font-size: 1rem; |
|
font-weight: 600; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .whats-new-title { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.whats-new-list { |
|
margin: 0; |
|
padding-left: 1.5rem; |
|
color: var(--fog-text, #1f2937); |
|
list-style-type: disc; |
|
} |
|
|
|
:global(.dark) .whats-new-list { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.whats-new-list li { |
|
margin-bottom: 0.5rem; |
|
line-height: 1.5; |
|
} |
|
|
|
.update-process-section { |
|
margin-top: 1rem; |
|
} |
|
|
|
.update-modal-note { |
|
margin: 1rem 0 0 0; |
|
font-size: 0.875rem; |
|
color: var(--fog-text-light, #52667a); |
|
font-style: italic; |
|
} |
|
|
|
:global(.dark) .update-modal-note { |
|
color: var(--fog-dark-text-light, #a8b8d0); |
|
} |
|
|
|
.update-modal-actions { |
|
padding: 1.5rem; |
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
|
display: flex; |
|
gap: 0.75rem; |
|
justify-content: flex-end; |
|
} |
|
|
|
:global(.dark) .update-modal-actions { |
|
border-top-color: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.update-button { |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
padding: 0.75rem 1.5rem; |
|
background: var(--fog-accent, #64748b); |
|
color: white; |
|
border: none; |
|
border-radius: 0.375rem; |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
font-weight: 500; |
|
transition: opacity 0.2s; |
|
} |
|
|
|
.update-button:hover { |
|
opacity: 0.9; |
|
} |
|
|
|
:global(.dark) .update-button { |
|
background: var(--fog-dark-accent, #94a3b8); |
|
} |
|
|
|
.update-button-secondary { |
|
padding: 0.75rem 1.5rem; |
|
background: var(--fog-highlight, #f3f4f6); |
|
color: var(--fog-text, #1f2937); |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.375rem; |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
font-weight: 500; |
|
transition: all 0.2s; |
|
} |
|
|
|
.update-button-secondary:hover { |
|
background: var(--fog-accent, #64748b); |
|
color: white; |
|
border-color: var(--fog-accent, #64748b); |
|
} |
|
|
|
:global(.dark) .update-button-secondary { |
|
background: var(--fog-dark-highlight, #475569); |
|
color: var(--fog-dark-text, #f9fafb); |
|
border-color: var(--fog-dark-border, #475569); |
|
} |
|
|
|
:global(.dark) .update-button-secondary:hover { |
|
background: var(--fog-dark-accent, #94a3b8); |
|
color: white; |
|
border-color: var(--fog-dark-accent, #94a3b8); |
|
} |
|
|
|
.update-progress { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 1rem; |
|
} |
|
|
|
.update-progress-bar-container { |
|
width: 100%; |
|
height: 8px; |
|
background: var(--fog-highlight, #f3f4f6); |
|
border-radius: 4px; |
|
overflow: hidden; |
|
} |
|
|
|
:global(.dark) .update-progress-bar-container { |
|
background: var(--fog-dark-highlight, #475569); |
|
} |
|
|
|
.update-progress-bar { |
|
height: 100%; |
|
background: var(--fog-accent, #64748b); |
|
border-radius: 4px; |
|
transition: width 0.3s ease; |
|
} |
|
|
|
:global(.dark) .update-progress-bar { |
|
background: var(--fog-dark-accent, #94a3b8); |
|
} |
|
|
|
.update-progress-text { |
|
margin: 0; |
|
font-size: 0.875rem; |
|
font-weight: 500; |
|
color: var(--fog-text, #1f2937); |
|
text-align: center; |
|
} |
|
|
|
:global(.dark) .update-progress-text { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.update-progress-step { |
|
margin: 0; |
|
font-size: 0.75rem; |
|
color: var(--fog-text-light, #52667a); |
|
text-align: center; |
|
} |
|
|
|
:global(.dark) .update-progress-step { |
|
color: var(--fog-dark-text-light, #a8b8d0); |
|
} |
|
</style>
|
|
|