From cff9bfe9e2fd60a96953b65ec7cc95a7da656183 Mon Sep 17 00:00:00 2001 From: Eichenholz Date: Fri, 7 Aug 2026 10:56:43 +0200 Subject: [PATCH] ENH - app store --- frontend/src/stores/app-store.test.ts | 25 +++++++++ frontend/src/stores/app-store.ts | 15 +++++- frontend/src/views/apps/AppStoreApp.vue | 68 ++++++++++++++++++++----- sky_phone/source/html/index.html | 2 +- 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/frontend/src/stores/app-store.test.ts b/frontend/src/stores/app-store.test.ts index 680d4a1..fb0e569 100644 --- a/frontend/src/stores/app-store.test.ts +++ b/frontend/src/stores/app-store.test.ts @@ -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) diff --git a/frontend/src/stores/app-store.ts b/frontend/src/stores/app-store.ts index b0e83ec..d64cc7d 100644 --- a/frontend/src/stores/app-store.ts +++ b/frontend/src/stores/app-store.ts @@ -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) }, diff --git a/frontend/src/views/apps/AppStoreApp.vue b/frontend/src/views/apps/AppStoreApp.vue index 17ed2e9..ecd399c 100644 --- a/frontend/src/views/apps/AppStoreApp.vue +++ b/frontend/src/views/apps/AppStoreApp.vue @@ -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([]) 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) } @@ -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)" > diff --git a/sky_phone/source/html/index.html b/sky_phone/source/html/index.html index cf3b768..6a245af 100644 --- a/sky_phone/source/html/index.html +++ b/sky_phone/source/html/index.html @@ -4,7 +4,7 @@ Sky Phone - +