ENH - app store

This commit is contained in:
Eichenholz
2026-08-07 10:56:43 +02:00
parent ec47ce20c4
commit cff9bfe9e2
4 changed files with 93 additions and 17 deletions
+25
View File
@@ -72,6 +72,31 @@ describe('app store', () => {
expect(mocks.saveDeviceNamespace).toHaveBeenCalledTimes(1)
})
it('reinstalls core and claimed apps removed from the Home Screen', () => {
vi.useFakeTimers()
const apps = useAppStoreStore()
apps.hydrate({ claimedApps: ['memory'] })
apps.removeHomeApp('mail')
apps.removeHomeApp('memory')
mocks.saveDeviceNamespace.mockClear()
apps.installApp('mail')
apps.installApp('memory')
expect(apps.installingApps).toEqual({ mail: true, memory: true })
expect(apps.homeLayout.hidden).toEqual(['mail', 'memory'])
vi.advanceTimersByTime(3000)
expect(apps.installingApps).toEqual({})
expect(apps.homeLayout.hidden).toEqual([])
expect(apps.homeLayout.grid).toContain('mail')
expect(apps.homeLayout.grid).toContain('memory')
expect(apps.claimedApps).toEqual(['memory'])
expect(mocks.saveDeviceNamespace).toHaveBeenCalledTimes(2)
})
it('persists home reordering and removal independently from installation', () => {
const apps = useAppStoreStore()
apps.hydrate(null)
+13 -2
View File
@@ -43,11 +43,22 @@ export const useAppStoreStore = defineStore('app-store', {
}
},
installApp(id: LaunchablePhoneAppId): void {
if (this.claimedApps.includes(id) || this.installingApps[id]) return
const installed =
CORE_APP_IDS.includes(id) || this.claimedApps.includes(id)
if (
this.installingApps[id] ||
(installed && !this.homeLayout.hidden.includes(id))
) {
return
}
this.installingApps[id] = true
globalThis.setTimeout(() => {
this.claimApp(id)
if (CORE_APP_IDS.includes(id) || this.claimedApps.includes(id)) {
this.restoreHomeApp(id)
} else {
this.claimApp(id)
}
delete this.installingApps[id]
}, INSTALL_DURATION_MS)
},
+54 -14
View File
@@ -9,16 +9,22 @@ import {
} from 'konsta/vue'
import { Gamepad2, Grid2X2, Search } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { PHONE_APPS } from '@/config/apps'
import { useAppStoreStore } from '@/stores/app-store'
import { usePhoneStore } from '@/stores/phone'
import type { LaunchablePhoneAppDefinition } from '@/types/apps'
import type {
LaunchablePhoneAppDefinition,
LaunchablePhoneAppId,
} from '@/types/apps'
const phone = usePhoneStore()
const appStore = useAppStoreStore()
const router = useRouter()
const tab = ref<'apps' | 'games' | 'search'>('apps')
const query = ref('')
const installedDuringVisit = ref<LaunchablePhoneAppId[]>([])
const tabs = [
{ id: 'apps', icon: Grid2X2 },
{ id: 'games', icon: Gamepad2 },
@@ -28,13 +34,23 @@ const tabBarColors = {
strongHighlightBgIos: 'bg-[#e5e5ea] dark:bg-[#2c2c2e]',
}
const catalog = computed(() =>
PHONE_APPS.filter(
(app): app is LaunchablePhoneAppDefinition =>
app.component !== null &&
app.route !== null &&
app.category === 'games' &&
!appStore.claimedApps.includes(app.id),
).sort((a, b) => a.gridOrder - b.gridOrder),
PHONE_APPS.filter((app): app is LaunchablePhoneAppDefinition => {
if (
app.component === null ||
app.route === null ||
app.id === 'app-store'
) {
return false
}
const installed =
app.category !== 'games' || appStore.claimedApps.includes(app.id)
return (
!installed ||
appStore.homeLayout.hidden.includes(app.id) ||
installedDuringVisit.value.includes(app.id)
)
}).sort((a, b) => a.gridOrder - b.gridOrder),
)
const shownApps = computed(() => {
if (tab.value === 'games') {
@@ -55,7 +71,33 @@ function updateSearch(event: Event): void {
query.value = (event.target as HTMLInputElement).value
}
function installApp(app: LaunchablePhoneAppDefinition): void {
function appAction(
app: LaunchablePhoneAppDefinition,
): 'get' | 'installing' | 'open' {
if (appStore.installingApps[app.id]) return 'installing'
const installed =
app.category !== 'games' || appStore.claimedApps.includes(app.id)
if (
installed &&
!appStore.homeLayout.hidden.includes(app.id) &&
installedDuringVisit.value.includes(app.id)
) {
return 'open'
}
return 'get'
}
function handleApp(app: LaunchablePhoneAppDefinition): void {
if (appAction(app) === 'open') {
void router.push(app.route)
return
}
if (!installedDuringVisit.value.includes(app.id)) {
installedDuringVisit.value.push(app.id)
}
appStore.installApp(app.id)
}
</script>
@@ -95,18 +137,16 @@ function installApp(app: LaunchablePhoneAppDefinition): void {
type="button"
:disabled="appStore.installingApps[app.id]"
:aria-label="`${phone.t(app.labelKey)} ${phone.t(
appStore.installingApps[app.id]
? 'Apps.appStore.installing'
: 'Apps.appStore.get',
`Apps.appStore.${appAction(app)}`,
)}`"
@click="installApp(app)"
@click="handleApp(app)"
>
<k-preloader
v-if="appStore.installingApps[app.id]"
class="store-installing"
/>
<template v-else>
{{ phone.t('Apps.appStore.get') }}
{{ phone.t(`Apps.appStore.${appAction(app)}`) }}
</template>
</button>
</article>
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sky Phone</title>
<script type="module" crossorigin src="./assets/sky-index-DJ4L_lNE.js"></script>
<script type="module" crossorigin src="./assets/sky-index-B2mWosa3.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-HW5wL36V.css">
</head>
<body>