mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
FIX - Inline App Store bundle; CEF-compatible CSS
Make the App Store part of the phone shell (remove preload helper/loadAppStoreComponent and App.vue preload) by importing AppStoreApp in PhoneAppWindow and selecting it when app.id === 'app-store'. Update the apps registry to use an inline import for the async component. Replace unsupported CSS features (:has, color-mix) with CEF-safe alternatives using semantic tokens (--sky-app-accent-soft, --sky-danger-soft) and add explicit .store-action-button/.store-action-button--icon and .store-detail__action classes. Add contract tests to assert the App Store is bundled with the shell and that styles remain compatible with the FiveM CEF target.
This commit is contained in:
@@ -47,7 +47,7 @@ import { useMarketplaceStore } from '@/stores/marketplace'
|
||||
import { useAppCatalogStore } from '@/stores/app-catalog'
|
||||
import { useAppStoreStore } from '@/stores/app-store'
|
||||
import { useWidgetsStore } from '@/stores/widgets'
|
||||
import { isPhoneAppId, loadAppStoreComponent } from '@/config/apps'
|
||||
import { isPhoneAppId } from '@/config/apps'
|
||||
import { useNotesStore } from '@/stores/notes'
|
||||
import { useMemosStore } from '@/stores/memos'
|
||||
import { useWeatherStore } from '@/stores/weather'
|
||||
@@ -1223,9 +1223,6 @@ onMounted(() => {
|
||||
window.addEventListener('resize', updateViewportScale)
|
||||
systemColorScheme.addEventListener('change', onSystemColorSchemeChange)
|
||||
phone.setSystemDarkMode(systemColorScheme.matches)
|
||||
void loadAppStoreComponent().catch((error: unknown) => {
|
||||
console.error('[App Store] Could not preload the phone app.', error)
|
||||
})
|
||||
void nuiCall('ui:ready', { protocolVersion: 1 })
|
||||
clockTicker = setInterval(() => {
|
||||
const now = Date.now()
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Newspaper } from 'lucide-vue-next'
|
||||
|
||||
import { isPhoneAppId, loadAppStoreComponent, PHONE_APPS } from './apps'
|
||||
import { isPhoneAppId, PHONE_APPS } from './apps'
|
||||
describe('app registry', () => {
|
||||
it('reuses the App Store module loaded by the phone shell', async () => {
|
||||
const firstLoad = loadAppStoreComponent()
|
||||
const secondLoad = loadAppStoreComponent()
|
||||
|
||||
expect(secondLoad).toBe(firstLoad)
|
||||
expect((await firstLoad).default).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has unique ids and routes with the reference dock order', () => {
|
||||
expect(new Set(PHONE_APPS.map((app) => app.id)).size).toBe(
|
||||
PHONE_APPS.length,
|
||||
|
||||
@@ -86,19 +86,6 @@ import type {
|
||||
PhoneAppDefinition,
|
||||
} from '@/types/apps'
|
||||
|
||||
let appStoreComponentPromise:
|
||||
| ReturnType<typeof importAppStoreComponent>
|
||||
| null = null
|
||||
|
||||
function importAppStoreComponent() {
|
||||
return import('@/views/apps/AppStoreApp.vue')
|
||||
}
|
||||
|
||||
export function loadAppStoreComponent() {
|
||||
appStoreComponentPromise ??= importAppStoreComponent()
|
||||
return appStoreComponentPromise
|
||||
}
|
||||
|
||||
export const PHONE_APPS = shallowReactive<PhoneAppDefinition[]>([
|
||||
{
|
||||
category: 'social',
|
||||
@@ -494,7 +481,9 @@ export const PHONE_APPS = shallowReactive<PhoneAppDefinition[]>([
|
||||
},
|
||||
{
|
||||
category: 'utilities',
|
||||
component: markRaw(defineAsyncComponent(loadAppStoreComponent)),
|
||||
component: markRaw(
|
||||
defineAsyncComponent(() => import('@/views/apps/AppStoreApp.vue')),
|
||||
),
|
||||
dockOrder: null,
|
||||
gridOrder: 9,
|
||||
icon: markRaw(ShoppingBag),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('./PhoneAppWindow.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('PhoneAppWindow bundle contract', () => {
|
||||
it('loads the App Store from the phone shell instead of a delayed chunk', () => {
|
||||
expect(source).toContain(
|
||||
"import AppStoreApp from '@/views/apps/AppStoreApp.vue'",
|
||||
)
|
||||
expect(source).toContain("app.value?.id === 'app-store'")
|
||||
expect(source).toContain('? AppStoreApp : app.value?.component')
|
||||
expect(source).toContain('<component :is="builtinAppComponent" />')
|
||||
})
|
||||
})
|
||||
@@ -6,10 +6,14 @@ import CustomAppFrame from '@/components/CustomAppFrame.vue'
|
||||
import { getPhoneApp, isExternalPhoneApp } from '@/config/apps'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { getCustomAppFrameKey } from '@/utils/customAppLifecycle'
|
||||
import AppStoreApp from '@/views/apps/AppStoreApp.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const phone = usePhoneStore()
|
||||
const app = computed(() => getPhoneApp(route.params.appId))
|
||||
const builtinAppComponent = computed(() =>
|
||||
app.value?.id === 'app-store' ? AppStoreApp : app.value?.component,
|
||||
)
|
||||
const launchStyle = computed(() => {
|
||||
const origin = phone.launchOrigin
|
||||
return {
|
||||
@@ -30,7 +34,7 @@ const launchStyle = computed(() => {
|
||||
:app="app"
|
||||
/>
|
||||
<Suspense v-else>
|
||||
<component :is="app.component" />
|
||||
<component :is="builtinAppComponent" />
|
||||
<template #fallback>
|
||||
<div class="app-loading">{{ phone.t('Common.loading') }}</div>
|
||||
</template>
|
||||
|
||||
@@ -25,5 +25,6 @@ describe('AppStoreAction contract', () => {
|
||||
expect(source).toMatch(
|
||||
/\.app-store-action__progress i\s*\{[^}]*width:\s*7px/s,
|
||||
)
|
||||
expect(source).not.toContain('color-mix(')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,9 +50,7 @@ const phone = usePhoneStore()
|
||||
}
|
||||
|
||||
.app-store-action__download {
|
||||
filter: drop-shadow(
|
||||
0 2px 5px color-mix(in srgb, var(--sky-app-accent) 30%, transparent)
|
||||
);
|
||||
filter: drop-shadow(0 2px 5px var(--sky-app-accent-soft));
|
||||
}
|
||||
|
||||
.app-store-action__progress {
|
||||
@@ -74,7 +72,7 @@ const phone = usePhoneStore()
|
||||
}
|
||||
|
||||
.app-store-action__track {
|
||||
stroke: color-mix(in srgb, var(--sky-app-accent) 24%, transparent);
|
||||
stroke: var(--sky-app-accent-soft);
|
||||
}
|
||||
|
||||
.app-store-action__value {
|
||||
|
||||
@@ -139,9 +139,7 @@ describe('AppStoreApp Sky navigation contract', () => {
|
||||
expect(source).toMatch(
|
||||
/button:not\(\.store-ranking__detail-link\):not\(:disabled\):hover\)\s*\{[^}]*brightness\(1\.08\)[^}]*translateY\(-1px\)/s,
|
||||
)
|
||||
expect(source).toContain(
|
||||
'.app-store-page :deep(button:has(.app-store-action--icon):hover)',
|
||||
)
|
||||
expect(source).toContain('.app-store-page .store-action-button--icon:hover')
|
||||
expect(source).toContain(
|
||||
'.store-account__primary-action:not(:disabled):hover',
|
||||
)
|
||||
@@ -175,9 +173,7 @@ describe('AppStoreApp Sky navigation contract', () => {
|
||||
expect(source).toContain('@click="openAppDetail(finalHighlight)"')
|
||||
expect(source).toContain('@click.stop="handleApp(dailyHighlights[0])"')
|
||||
expect(source).toContain('@click.stop="handleApp(finalHighlight)"')
|
||||
expect(source).toContain(
|
||||
'.store-highlight:has(.store-highlight__detail-link:hover)',
|
||||
)
|
||||
expect(source).toContain('.store-highlight:hover')
|
||||
})
|
||||
|
||||
it('opens Top Today apps while keeping their direct app actions separate', () => {
|
||||
@@ -193,6 +189,12 @@ describe('AppStoreApp Sky navigation contract', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps App Store actions compatible with the FiveM CEF target', () => {
|
||||
expect(source.match(/class="store-action-button"/g)).toHaveLength(8)
|
||||
expect(source).not.toContain(':has(')
|
||||
expect(source).not.toContain('color-mix(')
|
||||
})
|
||||
|
||||
it('builds clean Apps and Games pages with rotating features', () => {
|
||||
expect(source).not.toContain('class="store-browse__filters"')
|
||||
expect(source).not.toContain('browseFilter')
|
||||
|
||||
@@ -484,6 +484,11 @@ watch(
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon':
|
||||
appAction(dailyHighlights[0]) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[dailyHighlights[0].id]"
|
||||
:aria-label="`${getPhoneAppLabel(dailyHighlights[0], phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(dailyHighlights[0])}`,
|
||||
@@ -548,6 +553,10 @@ watch(
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon': appAction(app) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[app.id]"
|
||||
:aria-label="`${getPhoneAppLabel(app, phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(app)}`,
|
||||
@@ -590,6 +599,10 @@ watch(
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon': appAction(app) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[app.id]"
|
||||
:aria-label="`${getPhoneAppLabel(app, phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(app)}`,
|
||||
@@ -626,6 +639,10 @@ watch(
|
||||
<img :src="finalHighlight.iconImage" alt="" draggable="false" />
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon': appAction(finalHighlight) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[finalHighlight.id]"
|
||||
:aria-label="`${getPhoneAppLabel(finalHighlight, phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(finalHighlight)}`,
|
||||
@@ -681,6 +698,11 @@ watch(
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon':
|
||||
appAction(featuredApp) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[featuredApp.id]"
|
||||
:aria-label="`${getPhoneAppLabel(featuredApp, phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(featuredApp)}`,
|
||||
@@ -746,6 +768,10 @@ watch(
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon': appAction(app) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[app.id]"
|
||||
:aria-label="`${getPhoneAppLabel(app, phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(app)}`,
|
||||
@@ -792,6 +818,10 @@ watch(
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon': appAction(app) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[app.id]"
|
||||
:aria-label="`${getPhoneAppLabel(app, phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(app)}`,
|
||||
@@ -857,6 +887,10 @@ watch(
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="store-action-button"
|
||||
:class="{
|
||||
'store-action-button--icon': appAction(app) !== 'open',
|
||||
}"
|
||||
:disabled="appStore.installingApps[app.id]"
|
||||
:aria-label="`${getPhoneAppLabel(app, phone.t)} ${phone.t(
|
||||
`Apps.appStore.${appAction(app)}`,
|
||||
@@ -1985,8 +2019,8 @@ watch(
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.store-highlight:has(.store-highlight__detail-link:hover),
|
||||
.store-final-pick:has(.store-highlight__detail-link:hover) {
|
||||
.store-highlight:hover,
|
||||
.store-final-pick:hover {
|
||||
box-shadow: 0 17px 32px rgba(0, 0, 0, 0.24);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
@@ -2599,16 +2633,12 @@ watch(
|
||||
background: var(--sky-surface-muted);
|
||||
}
|
||||
|
||||
.store-highlight__footer :deep(button:has(.app-store-action--icon)),
|
||||
.store-ranking li > :deep(button:has(.app-store-action--icon)),
|
||||
.store-final-pick > :deep(button:has(.app-store-action--icon)),
|
||||
.store-browse-feature footer > :deep(button:has(.app-store-action--icon)),
|
||||
.store-search__recommendations
|
||||
article
|
||||
> :deep(button:has(.app-store-action--icon)),
|
||||
.store-list
|
||||
article
|
||||
> :deep(button:not(.store-list__detail-link):has(.app-store-action--icon)) {
|
||||
.store-highlight__footer .store-action-button--icon,
|
||||
.store-ranking li > .store-action-button--icon,
|
||||
.store-final-pick > .store-action-button--icon,
|
||||
.store-browse-feature footer > .store-action-button--icon,
|
||||
.store-search__recommendations article > .store-action-button--icon,
|
||||
.store-list article > .store-action-button--icon {
|
||||
width: var(--sky-touch-target);
|
||||
min-width: var(--sky-touch-target);
|
||||
padding: 0;
|
||||
@@ -2643,11 +2673,8 @@ watch(
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.app-store-page :deep(button:has(.app-store-action--icon):hover) {
|
||||
filter: brightness(1.16)
|
||||
drop-shadow(
|
||||
0 3px 7px color-mix(in srgb, var(--sky-app-accent) 28%, transparent)
|
||||
);
|
||||
.app-store-page .store-action-button--icon:hover {
|
||||
filter: brightness(1.16) drop-shadow(0 3px 7px var(--sky-app-accent-soft));
|
||||
}
|
||||
|
||||
.store-account__primary-action:not(:disabled):hover {
|
||||
@@ -2655,7 +2682,7 @@ watch(
|
||||
}
|
||||
|
||||
.store-account__remove:not(:disabled):hover {
|
||||
background: color-mix(in srgb, var(--sky-danger) 22%, transparent);
|
||||
background: var(--sky-danger-soft);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ describe('AppStoreDetail contract', () => {
|
||||
expect(source).toContain("emit('back')")
|
||||
expect(source).toContain("emit('share')")
|
||||
expect(source).toContain('<AppStoreAction :action="action" />')
|
||||
expect(source).toContain("'store-detail__action--icon': action !== 'open'")
|
||||
expect(source).not.toContain(':has(')
|
||||
expect(source).toMatch(
|
||||
/\.store-detail\s*\{[^}]*padding:\s*var\(--sky-space-3\) 0 var\(--sky-space-6\)/s,
|
||||
)
|
||||
|
||||
@@ -126,6 +126,8 @@ function updateActivePreview(): void {
|
||||
<p>{{ developer }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="store-detail__action"
|
||||
:class="{ 'store-detail__action--icon': action !== 'open' }"
|
||||
:disabled="action === 'installing'"
|
||||
@click="emit('action')"
|
||||
>
|
||||
@@ -368,7 +370,7 @@ function updateActivePreview(): void {
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.store-detail__hero button:has(.app-store-action--icon) {
|
||||
.store-detail__hero .store-detail__action--icon {
|
||||
width: var(--sky-touch-target);
|
||||
min-width: var(--sky-touch-target);
|
||||
padding: 0;
|
||||
|
||||
Reference in New Issue
Block a user