mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
ENH - refine App Store experience
This commit is contained in:
@@ -715,8 +715,9 @@ export function getPhoneAppLabel(
|
|||||||
|
|
||||||
export function isPhoneAppRemovable(app: PhoneAppDefinition): boolean {
|
export function isPhoneAppRemovable(app: PhoneAppDefinition): boolean {
|
||||||
return app.kind === 'external'
|
return app.kind === 'external'
|
||||||
? app.removable
|
? app.removable && !app.defaultInstalled
|
||||||
: !NON_REMOVABLE_PHONE_APP_IDS.has(app.id)
|
: !DEFAULT_INSTALLED_PHONE_APP_IDS.has(app.id) &&
|
||||||
|
!NON_REMOVABLE_PHONE_APP_IDS.has(app.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLaunchablePhoneApp(
|
export function isLaunchablePhoneApp(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_INSTALLED_PHONE_APP_IDS,
|
DEFAULT_INSTALLED_PHONE_APP_IDS,
|
||||||
NON_REMOVABLE_PHONE_APP_IDS,
|
|
||||||
} from '@/config/apps'
|
} from '@/config/apps'
|
||||||
import { useAppStoreStore } from '@/stores/app-store'
|
import { useAppStoreStore } from '@/stores/app-store'
|
||||||
import {
|
import {
|
||||||
@@ -184,26 +183,26 @@ describe('app store', () => {
|
|||||||
it('fully uninstalls removable apps and allows downloading them again', () => {
|
it('fully uninstalls removable apps and allows downloading them again', () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const apps = useAppStoreStore()
|
const apps = useAppStoreStore()
|
||||||
apps.hydrate(null)
|
apps.hydrate({ claimedApps: ['snake'] })
|
||||||
mocks.phone.saveDeviceNamespace.mockClear()
|
mocks.phone.saveDeviceNamespace.mockClear()
|
||||||
|
|
||||||
expect(apps.uninstallApp('calculator')).toBe(true)
|
expect(apps.uninstallApp('snake')).toBe(true)
|
||||||
expect(apps.isInstalled('calculator')).toBe(false)
|
expect(apps.isInstalled('snake')).toBe(false)
|
||||||
expect(apps.uninstalledApps).toEqual(['calculator'])
|
expect(apps.uninstalledApps).toEqual(['snake'])
|
||||||
expect(apps.homeLayout.hidden).toContain('calculator')
|
expect(apps.homeLayout.hidden).toContain('snake')
|
||||||
expect(mocks.phone.saveDeviceNamespace).toHaveBeenLastCalledWith('apps', {
|
expect(mocks.phone.saveDeviceNamespace).toHaveBeenLastCalledWith('apps', {
|
||||||
claimedApps: [],
|
claimedApps: [],
|
||||||
homeLayout: apps.homeLayout,
|
homeLayout: apps.homeLayout,
|
||||||
launchCounts: {},
|
launchCounts: {},
|
||||||
uninstalledApps: ['calculator'],
|
uninstalledApps: ['snake'],
|
||||||
})
|
})
|
||||||
|
|
||||||
apps.installApp('calculator')
|
apps.installApp('snake')
|
||||||
vi.advanceTimersByTime(3000)
|
vi.advanceTimersByTime(3000)
|
||||||
|
|
||||||
expect(apps.isInstalled('calculator')).toBe(true)
|
expect(apps.isInstalled('snake')).toBe(true)
|
||||||
expect(apps.uninstalledApps).toEqual([])
|
expect(apps.uninstalledApps).toEqual([])
|
||||||
expect(apps.homeLayout.hidden).not.toContain('calculator')
|
expect(apps.homeLayout.hidden).not.toContain('snake')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('hydrates persisted removals while rejecting protected and invalid ids', () => {
|
it('hydrates persisted removals while rejecting protected and invalid ids', () => {
|
||||||
@@ -211,49 +210,25 @@ describe('app store', () => {
|
|||||||
|
|
||||||
apps.hydrate({
|
apps.hydrate({
|
||||||
uninstalledApps: ['calculator', 'phone', 'not-an-app'],
|
uninstalledApps: ['calculator', 'phone', 'not-an-app'],
|
||||||
updatedAppReleases: {
|
|
||||||
calculator: '2026-08-14',
|
|
||||||
phone: '2026-08-14',
|
|
||||||
snake: 'x'.repeat(64),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(apps.uninstalledApps).toEqual(['calculator'])
|
expect(apps.uninstalledApps).toEqual([])
|
||||||
expect(apps.isInstalled('calculator')).toBe(false)
|
expect(apps.isInstalled('calculator')).toBe(true)
|
||||||
expect(apps.isInstalled('phone')).toBe(true)
|
expect(apps.isInstalled('phone')).toBe(true)
|
||||||
expect(apps.updatedAppReleases).toEqual({ phone: '2026-08-14' })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('protects system apps from full uninstallation', () => {
|
it('protects every default app from full uninstallation', () => {
|
||||||
const apps = useAppStoreStore()
|
const apps = useAppStoreStore()
|
||||||
apps.hydrate(null)
|
apps.hydrate(null)
|
||||||
mocks.phone.saveDeviceNamespace.mockClear()
|
mocks.phone.saveDeviceNamespace.mockClear()
|
||||||
|
|
||||||
expect(apps.uninstallApp('phone')).toBe(false)
|
for (const appId of DEFAULT_INSTALLED_PHONE_APP_IDS) {
|
||||||
expect(apps.isInstalled('phone')).toBe(true)
|
expect(apps.uninstallApp(appId)).toBe(false)
|
||||||
|
expect(apps.isInstalled(appId)).toBe(true)
|
||||||
|
}
|
||||||
expect(mocks.phone.saveDeviceNamespace).not.toHaveBeenCalled()
|
expect(mocks.phone.saveDeviceNamespace).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates installed apps and persists the current release', () => {
|
|
||||||
vi.useFakeTimers()
|
|
||||||
const apps = useAppStoreStore()
|
|
||||||
apps.hydrate(null)
|
|
||||||
|
|
||||||
apps.updateApp('phone', '2026-08-14')
|
|
||||||
expect(apps.updatingApps.phone).toBe(true)
|
|
||||||
|
|
||||||
vi.advanceTimersByTime(1800)
|
|
||||||
|
|
||||||
expect(apps.updatingApps.phone).toBeUndefined()
|
|
||||||
expect(apps.updatedAppReleases.phone).toBe('2026-08-14')
|
|
||||||
expect(mocks.phone.saveDeviceNamespace).toHaveBeenLastCalledWith('apps', {
|
|
||||||
claimedApps: [],
|
|
||||||
homeLayout: apps.homeLayout,
|
|
||||||
launchCounts: {},
|
|
||||||
updatedAppReleases: { phone: '2026-08-14' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('ignores duplicate installation requests and invalid persisted ids', () => {
|
it('ignores duplicate installation requests and invalid persisted ids', () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const apps = useAppStoreStore()
|
const apps = useAppStoreStore()
|
||||||
@@ -267,7 +242,7 @@ describe('app store', () => {
|
|||||||
expect(mocks.phone.saveDeviceNamespace).toHaveBeenCalledTimes(1)
|
expect(mocks.phone.saveDeviceNamespace).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reinstalls core and claimed apps removed from the Home Screen', () => {
|
it('reinstalls claimed apps removed from the Home Screen', () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const apps = useAppStoreStore()
|
const apps = useAppStoreStore()
|
||||||
|
|
||||||
@@ -279,8 +254,8 @@ describe('app store', () => {
|
|||||||
apps.installApp('notes')
|
apps.installApp('notes')
|
||||||
apps.installApp('memory')
|
apps.installApp('memory')
|
||||||
|
|
||||||
expect(apps.installingApps).toEqual({ notes: true, memory: true })
|
expect(apps.installingApps).toEqual({ memory: true })
|
||||||
expect(apps.homeLayout.hidden).toEqual(['notes', 'memory'])
|
expect(apps.homeLayout.hidden).toEqual(['memory'])
|
||||||
|
|
||||||
vi.advanceTimersByTime(3000)
|
vi.advanceTimersByTime(3000)
|
||||||
|
|
||||||
@@ -289,25 +264,15 @@ describe('app store', () => {
|
|||||||
expect(apps.homeLayout.grid).toContain('notes')
|
expect(apps.homeLayout.grid).toContain('notes')
|
||||||
expect(apps.homeLayout.grid).toContain('memory')
|
expect(apps.homeLayout.grid).toContain('memory')
|
||||||
expect(apps.claimedApps).toEqual(['memory'])
|
expect(apps.claimedApps).toEqual(['memory'])
|
||||||
expect(mocks.phone.saveDeviceNamespace).toHaveBeenCalledTimes(2)
|
expect(mocks.phone.saveDeviceNamespace).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prevents protected apps from being removed from the Home Screen', () => {
|
it('prevents every default app from being removed from the Home Screen', () => {
|
||||||
const apps = useAppStoreStore()
|
const apps = useAppStoreStore()
|
||||||
apps.hydrate(null)
|
apps.hydrate(null)
|
||||||
mocks.phone.saveDeviceNamespace.mockClear()
|
mocks.phone.saveDeviceNamespace.mockClear()
|
||||||
|
|
||||||
expect([...NON_REMOVABLE_PHONE_APP_IDS]).toEqual([
|
for (const appId of DEFAULT_INSTALLED_PHONE_APP_IDS) {
|
||||||
'app-store',
|
|
||||||
'settings',
|
|
||||||
'camera',
|
|
||||||
'photos',
|
|
||||||
'phone',
|
|
||||||
'messages',
|
|
||||||
'mail',
|
|
||||||
'health',
|
|
||||||
])
|
|
||||||
for (const appId of NON_REMOVABLE_PHONE_APP_IDS) {
|
|
||||||
apps.removeHomeApp(appId)
|
apps.removeHomeApp(appId)
|
||||||
expect(apps.homeLayout.hidden).not.toContain(appId)
|
expect(apps.homeLayout.hidden).not.toContain(appId)
|
||||||
}
|
}
|
||||||
@@ -332,17 +297,18 @@ describe('app store', () => {
|
|||||||
|
|
||||||
it('persists home reordering and removal independently from installation', () => {
|
it('persists home reordering and removal independently from installation', () => {
|
||||||
const apps = useAppStoreStore()
|
const apps = useAppStoreStore()
|
||||||
apps.hydrate(null)
|
apps.hydrate({ claimedApps: ['memory'] })
|
||||||
|
mocks.phone.saveDeviceNamespace.mockClear()
|
||||||
|
|
||||||
const notesIndex = apps.homeLayout.grid.indexOf('notes')
|
const memoryIndex = apps.homeLayout.grid.indexOf('memory')
|
||||||
apps.moveHomeApp('grid', notesIndex, 'grid', 0)
|
apps.moveHomeApp('grid', memoryIndex, 'grid', 0)
|
||||||
expect(apps.homeLayout.grid[0]).toBe('notes')
|
expect(apps.homeLayout.grid[0]).toBe('memory')
|
||||||
|
|
||||||
apps.removeHomeApp('notes')
|
apps.removeHomeApp('memory')
|
||||||
expect(apps.homeLayout.grid).not.toContain('notes')
|
expect(apps.homeLayout.grid).not.toContain('memory')
|
||||||
expect(apps.homeLayout.hidden).toContain('notes')
|
expect(apps.homeLayout.hidden).toContain('memory')
|
||||||
expect(mocks.phone.saveDeviceNamespace).toHaveBeenLastCalledWith('apps', {
|
expect(mocks.phone.saveDeviceNamespace).toHaveBeenLastCalledWith('apps', {
|
||||||
claimedApps: [],
|
claimedApps: ['memory'],
|
||||||
homeLayout: apps.homeLayout,
|
homeLayout: apps.homeLayout,
|
||||||
launchCounts: {},
|
launchCounts: {},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
import { nuiCall } from '@/utils/nui'
|
import { nuiCall } from '@/utils/nui'
|
||||||
|
|
||||||
const INSTALL_DURATION_MS = 3000
|
const INSTALL_DURATION_MS = 3000
|
||||||
const UPDATE_DURATION_MS = 1800
|
|
||||||
|
|
||||||
type PendingInstallation = {
|
type PendingInstallation = {
|
||||||
deviceImei: string
|
deviceImei: string
|
||||||
@@ -39,19 +38,10 @@ type PendingInstallation = {
|
|||||||
token: symbol
|
token: symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
type PendingUpdate = {
|
|
||||||
deviceImei: string
|
|
||||||
timer: ReturnType<typeof globalThis.setTimeout>
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingInstallations = new WeakMap<
|
const pendingInstallations = new WeakMap<
|
||||||
object,
|
object,
|
||||||
Map<LaunchablePhoneAppId, PendingInstallation>
|
Map<LaunchablePhoneAppId, PendingInstallation>
|
||||||
>()
|
>()
|
||||||
const pendingUpdates = new WeakMap<
|
|
||||||
object,
|
|
||||||
Map<LaunchablePhoneAppId, PendingUpdate>
|
|
||||||
>()
|
|
||||||
|
|
||||||
function getDefaultGridIds(): LaunchablePhoneAppId[] {
|
function getDefaultGridIds(): LaunchablePhoneAppId[] {
|
||||||
return [...PHONE_APPS]
|
return [...PHONE_APPS]
|
||||||
@@ -112,7 +102,6 @@ export const useAppStoreStore = defineStore('app-store', {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
claimedApps: [] as LaunchablePhoneAppId[],
|
claimedApps: [] as LaunchablePhoneAppId[],
|
||||||
uninstalledApps: [] as LaunchablePhoneAppId[],
|
uninstalledApps: [] as LaunchablePhoneAppId[],
|
||||||
updatedAppReleases: {} as Partial<Record<LaunchablePhoneAppId, string>>,
|
|
||||||
homeLayout: createDefaultHomeLayout(
|
homeLayout: createDefaultHomeLayout(
|
||||||
getDefaultInstalledIds(),
|
getDefaultInstalledIds(),
|
||||||
getDefaultGridIds(),
|
getDefaultGridIds(),
|
||||||
@@ -120,7 +109,6 @@ export const useAppStoreStore = defineStore('app-store', {
|
|||||||
),
|
),
|
||||||
hydrated: false,
|
hydrated: false,
|
||||||
installingApps: {} as Partial<Record<LaunchablePhoneAppId, boolean>>,
|
installingApps: {} as Partial<Record<LaunchablePhoneAppId, boolean>>,
|
||||||
updatingApps: {} as Partial<Record<LaunchablePhoneAppId, boolean>>,
|
|
||||||
launchCounts: {} as Partial<Record<LaunchablePhoneAppId, number>>,
|
launchCounts: {} as Partial<Record<LaunchablePhoneAppId, number>>,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
@@ -157,14 +145,6 @@ export const useAppStoreStore = defineStore('app-store', {
|
|||||||
pendingInstallations.delete(this)
|
pendingInstallations.delete(this)
|
||||||
}
|
}
|
||||||
this.installingApps = {}
|
this.installingApps = {}
|
||||||
const updates = pendingUpdates.get(this)
|
|
||||||
if (updates) {
|
|
||||||
for (const update of updates.values()) {
|
|
||||||
globalThis.clearTimeout(update.timer)
|
|
||||||
}
|
|
||||||
pendingUpdates.delete(this)
|
|
||||||
}
|
|
||||||
this.updatingApps = {}
|
|
||||||
},
|
},
|
||||||
installApp(id: LaunchablePhoneAppId): void {
|
installApp(id: LaunchablePhoneAppId): void {
|
||||||
const installed = this.isInstalled(id)
|
const installed = this.isInstalled(id)
|
||||||
@@ -244,7 +224,6 @@ export const useAppStoreStore = defineStore('app-store', {
|
|||||||
homeLayout?: unknown
|
homeLayout?: unknown
|
||||||
launchCounts?: unknown
|
launchCounts?: unknown
|
||||||
uninstalledApps?: unknown
|
uninstalledApps?: unknown
|
||||||
updatedAppReleases?: unknown
|
|
||||||
} | null
|
} | null
|
||||||
const layoutVersion =
|
const layoutVersion =
|
||||||
data?.homeLayout && typeof data.homeLayout === 'object'
|
data?.homeLayout && typeof data.homeLayout === 'object'
|
||||||
@@ -268,29 +247,9 @@ export const useAppStoreStore = defineStore('app-store', {
|
|||||||
return !!app && isPhoneAppRemovable(app)
|
return !!app && isPhoneAppRemovable(app)
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
this.updatedAppReleases = {}
|
|
||||||
if (
|
|
||||||
data?.updatedAppReleases &&
|
|
||||||
typeof data.updatedAppReleases === 'object'
|
|
||||||
) {
|
|
||||||
for (const [id, release] of Object.entries(data.updatedAppReleases)) {
|
|
||||||
if (
|
|
||||||
isPhoneAppId(id) &&
|
|
||||||
typeof release === 'string' &&
|
|
||||||
release.length <= 32
|
|
||||||
) {
|
|
||||||
this.updatedAppReleases[id] = release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const installedIds = [
|
const installedIds = [
|
||||||
...new Set([...getDefaultInstalledIds(), ...this.claimedApps]),
|
...new Set([...getDefaultInstalledIds(), ...this.claimedApps]),
|
||||||
].filter((id) => !this.uninstalledApps.includes(id))
|
].filter((id) => !this.uninstalledApps.includes(id))
|
||||||
for (const appId of Object.keys(this.updatedAppReleases)) {
|
|
||||||
if (!isPhoneAppId(appId) || !installedIds.includes(appId)) {
|
|
||||||
delete this.updatedAppReleases[appId as LaunchablePhoneAppId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const removedLegacyDefaults = hasUninstalledBuiltinApp(
|
const removedLegacyDefaults = hasUninstalledBuiltinApp(
|
||||||
data?.homeLayout,
|
data?.homeLayout,
|
||||||
installedIds,
|
installedIds,
|
||||||
@@ -512,52 +471,11 @@ export const useAppStoreStore = defineStore('app-store', {
|
|||||||
if (!this.uninstalledApps.includes(appId)) {
|
if (!this.uninstalledApps.includes(appId)) {
|
||||||
this.uninstalledApps.push(appId)
|
this.uninstalledApps.push(appId)
|
||||||
}
|
}
|
||||||
delete this.updatedAppReleases[appId]
|
|
||||||
this.homeLayout = removeHomeApp(this.homeLayout, appId)
|
this.homeLayout = removeHomeApp(this.homeLayout, appId)
|
||||||
this.persist()
|
this.persist()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
updateApp(appId: LaunchablePhoneAppId, release: string): void {
|
|
||||||
if (
|
|
||||||
!this.isInstalled(appId) ||
|
|
||||||
this.updatingApps[appId] ||
|
|
||||||
!release.trim()
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const phone = usePhoneStore()
|
|
||||||
const deviceImei = phone.device?.imei
|
|
||||||
if (!phone.isOpen || !deviceImei) {
|
|
||||||
console.error(
|
|
||||||
`[App store] Update cancelled because no phone device is open for ${appId}.`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const updates =
|
|
||||||
pendingUpdates.get(this) ??
|
|
||||||
new Map<LaunchablePhoneAppId, PendingUpdate>()
|
|
||||||
this.updatingApps[appId] = true
|
|
||||||
const timer = globalThis.setTimeout(() => {
|
|
||||||
updates.delete(appId)
|
|
||||||
if (!updates.size) pendingUpdates.delete(this)
|
|
||||||
delete this.updatingApps[appId]
|
|
||||||
const activePhone = usePhoneStore()
|
|
||||||
if (!activePhone.isOpen || activePhone.device?.imei !== deviceImei) {
|
|
||||||
console.error(
|
|
||||||
`[App store] Update cancelled because the active phone changed for ${appId}.`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!this.isInstalled(appId)) return
|
|
||||||
this.updatedAppReleases[appId] = release
|
|
||||||
this.persist()
|
|
||||||
}, UPDATE_DURATION_MS)
|
|
||||||
updates.set(appId, { deviceImei, timer })
|
|
||||||
pendingUpdates.set(this, updates)
|
|
||||||
},
|
|
||||||
persist(): void {
|
persist(): void {
|
||||||
usePhoneStore().saveDeviceNamespace('apps', {
|
usePhoneStore().saveDeviceNamespace('apps', {
|
||||||
claimedApps: this.claimedApps,
|
claimedApps: this.claimedApps,
|
||||||
@@ -566,9 +484,6 @@ export const useAppStoreStore = defineStore('app-store', {
|
|||||||
...(this.uninstalledApps.length
|
...(this.uninstalledApps.length
|
||||||
? { uninstalledApps: this.uninstalledApps }
|
? { uninstalledApps: this.uninstalledApps }
|
||||||
: {}),
|
: {}),
|
||||||
...(Object.keys(this.updatedAppReleases).length
|
|
||||||
? { updatedAppReleases: this.updatedAppReleases }
|
|
||||||
: {}),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1861,8 +1861,6 @@ const defaultLocales: LocaleTree = {
|
|||||||
search: 'Search',
|
search: 'Search',
|
||||||
},
|
},
|
||||||
today: {
|
today: {
|
||||||
freshDaily: 'A fresh selection every day',
|
|
||||||
dailyEdition: 'Daily Edition',
|
|
||||||
featured: 'Featured today',
|
featured: 'Featured today',
|
||||||
gameHighlight: 'Game highlight',
|
gameHighlight: 'Game highlight',
|
||||||
appHighlight: 'App highlight',
|
appHighlight: 'App highlight',
|
||||||
@@ -1896,11 +1894,52 @@ const defaultLocales: LocaleTree = {
|
|||||||
essentialApps: 'Essential Apps',
|
essentialApps: 'Essential Apps',
|
||||||
essentialGames: 'Essential Games',
|
essentialGames: 'Essential Games',
|
||||||
},
|
},
|
||||||
|
taglines: {
|
||||||
|
health: 'Health, activity and medical information',
|
||||||
|
'weazel-news': 'Local news and city stories',
|
||||||
|
companies: 'Businesses, jobs and services',
|
||||||
|
music: 'Songs, playlists and audio',
|
||||||
|
picstagram: 'Photo sharing and social feed',
|
||||||
|
feather: 'Short posts and city conversations',
|
||||||
|
fliptok: 'Short videos and trends',
|
||||||
|
flare: 'Social posts and live moments',
|
||||||
|
calendar: 'Events, schedules and reminders',
|
||||||
|
radio: 'Live radio and team communication',
|
||||||
|
'local-pages': 'Local businesses and community pages',
|
||||||
|
crewlink: 'Crews, members and coordination',
|
||||||
|
phone: 'Calls, contacts and voicemail',
|
||||||
|
messages: 'Private texts and media',
|
||||||
|
darkchat: 'Private messaging and secure groups',
|
||||||
|
garage: 'Vehicles, parking and valet',
|
||||||
|
house: 'Homes, access and management',
|
||||||
|
map: 'Navigation and nearby places',
|
||||||
|
skyride: 'City rides and driver booking',
|
||||||
|
banking: 'Accounts, transfers and payments',
|
||||||
|
billing: 'Invoices and payment requests',
|
||||||
|
mail: 'Email and attachments',
|
||||||
|
notes: 'Notes, lists and ideas',
|
||||||
|
memos: 'Voice recordings and memos',
|
||||||
|
calculator: 'Everyday and scientific calculations',
|
||||||
|
camera: 'Photos and video capture',
|
||||||
|
clock: 'Time, alarms and timers',
|
||||||
|
weather: 'Forecasts and current conditions',
|
||||||
|
photos: 'Photo and video library',
|
||||||
|
settings: 'Phone preferences and security',
|
||||||
|
snake: 'Classic arcade snake',
|
||||||
|
memory: 'Card matching challenge',
|
||||||
|
'number-merge': 'Number puzzle and strategy',
|
||||||
|
minesweeper: 'Classic logic puzzle',
|
||||||
|
'tower-stack': 'Precision tower-building game',
|
||||||
|
'sky-flappy': 'Fast arcade flying challenge',
|
||||||
|
citymarkt: 'Local marketplace and listings',
|
||||||
|
'neon-drop': 'Neon block-dropping puzzle',
|
||||||
|
},
|
||||||
search: {
|
search: {
|
||||||
recommended: 'Recommended',
|
recommended: 'Recommended',
|
||||||
discover: 'Discover',
|
discover: 'Discover',
|
||||||
results: 'Search Results',
|
results: 'Search Results',
|
||||||
noResults: 'No matching apps or games found.',
|
noResults: 'No Results',
|
||||||
|
noResultsBody: 'Try searching for a different app or game.',
|
||||||
ad: 'Ad',
|
ad: 'Ad',
|
||||||
topApps: 'Top Apps',
|
topApps: 'Top Apps',
|
||||||
topGames: 'Top Games',
|
topGames: 'Top Games',
|
||||||
@@ -1917,7 +1956,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
games: 'Games',
|
games: 'Games',
|
||||||
library: 'Your Library',
|
library: 'Your Library',
|
||||||
myApps: 'My Apps',
|
myApps: 'My Apps',
|
||||||
update: 'UPDATE',
|
downloadedOn: 'Downloaded {date}',
|
||||||
uninstall: 'Uninstall',
|
uninstall: 'Uninstall',
|
||||||
uninstallApp: 'Uninstall {app}',
|
uninstallApp: 'Uninstall {app}',
|
||||||
uninstallTitle: 'Uninstall this app?',
|
uninstallTitle: 'Uninstall this app?',
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ const source = readFileSync(
|
|||||||
)
|
)
|
||||||
|
|
||||||
describe('AppStoreAction contract', () => {
|
describe('AppStoreAction contract', () => {
|
||||||
it('uses an Apple-style cloud download icon instead of GET text', () => {
|
it('uses localized text for the download button instead of an icon', () => {
|
||||||
expect(source).toContain("import { CloudDownload } from 'lucide-vue-next'")
|
|
||||||
expect(source).toContain('v-if="action === \'get\'"')
|
expect(source).toContain('v-if="action === \'get\'"')
|
||||||
expect(source).not.toContain("phone.t('Apps.appStore.get')")
|
expect(source).toContain("phone.t('Apps.appStore.get')")
|
||||||
|
expect(source).not.toContain('CloudDownload')
|
||||||
|
expect(source).toContain(
|
||||||
|
":class=\"{ 'app-store-action--icon': action === 'installing' }\"",
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows a timed circular installation progress with a center stop mark', () => {
|
it('shows a timed circular installation progress with a center stop mark', () => {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CloudDownload } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -13,15 +11,12 @@ const phone = usePhoneStore()
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
class="app-store-action"
|
class="app-store-action"
|
||||||
:class="{ 'app-store-action--icon': action !== 'open' }"
|
:class="{ 'app-store-action--icon': action === 'installing' }"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<CloudDownload
|
<template v-if="action === 'get'">
|
||||||
v-if="action === 'get'"
|
{{ phone.t('Apps.appStore.get') }}
|
||||||
class="app-store-action__download"
|
</template>
|
||||||
:size="24"
|
|
||||||
:stroke-width="1.9"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
v-else-if="action === 'installing'"
|
v-else-if="action === 'installing'"
|
||||||
class="app-store-action__progress"
|
class="app-store-action__progress"
|
||||||
@@ -49,10 +44,6 @@ const phone = usePhoneStore()
|
|||||||
color: var(--sky-app-accent);
|
color: var(--sky-app-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-store-action__download {
|
|
||||||
filter: drop-shadow(0 2px 5px var(--sky-app-accent-soft));
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-store-action__progress {
|
.app-store-action__progress {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ describe('AppStoreApp Sky navigation contract', () => {
|
|||||||
expect(source).toContain('<SkyNavbar')
|
expect(source).toContain('<SkyNavbar')
|
||||||
expect(source).toContain('<SkyPillNavigation')
|
expect(source).toContain('<SkyPillNavigation')
|
||||||
expect(source).toContain('<SkySearchbar')
|
expect(source).toContain('<SkySearchbar')
|
||||||
expect(source).toContain('<SkySpinner')
|
|
||||||
expect(source.match(/<SkyScrollArea/g)).toHaveLength(1)
|
expect(source.match(/<SkyScrollArea/g)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -46,7 +45,19 @@ describe('AppStoreApp Sky navigation contract', () => {
|
|||||||
expect(source).toContain('const searchRecommendations = computed')
|
expect(source).toContain('const searchRecommendations = computed')
|
||||||
expect(source).toContain('const searchDiscoverCards = computed')
|
expect(source).toContain('const searchDiscoverCards = computed')
|
||||||
expect(source).toContain('v-if="!hasSearchQuery"')
|
expect(source).toContain('v-if="!hasSearchQuery"')
|
||||||
|
expect(source).toContain(
|
||||||
|
":class=\"{ 'app-store-navbar--search': tab === 'search' }\"",
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.app-store-navbar--search\s*\{[^}]*margin-bottom:\s*-24px;/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.app-store-navbar--search :deep\(\.sky-navbar__subnavbar\)\s*\{[^}]*- 24px/s,
|
||||||
|
)
|
||||||
expect(source).toContain('class="store-search__recommendations"')
|
expect(source).toContain('class="store-search__recommendations"')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-search__recommendations article\.store-search__recommendation--promoted\s*\{[^}]*margin:\s*0 calc\(0px - var\(--sky-space-2\)\);[^}]*border-radius:\s*var\(--sky-radius-control\);[^}]*padding:\s*var\(--sky-space-3\) var\(--sky-space-2\);[^}]*rgba\(10, 132, 255, 0\.18\)/s,
|
||||||
|
)
|
||||||
expect(source).toContain('class="store-search__discover-grid"')
|
expect(source).toContain('class="store-search__discover-grid"')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'class="store-list store-list--browse store-list--search-results"',
|
'class="store-list store-list--browse store-list--search-results"',
|
||||||
@@ -58,16 +69,34 @@ describe('AppStoreApp Sky navigation contract', () => {
|
|||||||
expect(source).toMatch(
|
expect(source).toMatch(
|
||||||
/\.store-search__discover-grid > button\s*\{[^}]*min-height:\s*124px/s,
|
/\.store-search__discover-grid > button\s*\{[^}]*min-height:\s*124px/s,
|
||||||
)
|
)
|
||||||
|
expect(source).toContain('<SkyEmptyState')
|
||||||
|
expect(source).toContain("phone.t('Apps.appStore.search.noResultsBody')")
|
||||||
|
expect(source).toContain('<Search :size="38"')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-search__empty\s*\{[^}]*border:\s*0;[^}]*background:\s*transparent;/s,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens profile app management with update and uninstall actions', () => {
|
it('opens profile app management with open and uninstall actions', () => {
|
||||||
expect(source).toContain('@click="profileOpened = true"')
|
expect(source).toContain('@click="profileOpened = true"')
|
||||||
expect(source).toContain('const installedApps = computed')
|
expect(source).toContain('const installedApps = computed')
|
||||||
expect(source).toContain('return !appStore.isInstalled(app.id)')
|
expect(source).toContain('return !appStore.isInstalled(app.id)')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'class="store-account__apps phone-effect--expensive-shadow"',
|
'class="store-account__apps phone-effect--expensive-shadow"',
|
||||||
)
|
)
|
||||||
expect(source).toContain('appStore.updateApp(app.id, currentRelease)')
|
expect(source).toContain('@click="handleManagedApp(app)"')
|
||||||
|
expect(source).toContain("phone.t('Apps.appStore.open')")
|
||||||
|
expect(source).toContain("phone.t('Apps.appStore.account.downloadedOn'")
|
||||||
|
expect(source).toContain('<small>{{ downloadDateDescription }}</small>')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-account__primary-action\s*\{[^}]*min-width:\s*60px;[^}]*height:\s*30px;/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-account__summary > div\s*\{[^}]*border:\s*1px solid var\(--sky-hairline\);[^}]*border-radius:\s*var\(--sky-radius-card\);[^}]*background:\s*var\(--sky-surface-muted\);/s,
|
||||||
|
)
|
||||||
|
expect(source).not.toContain('appStore.updateApp(')
|
||||||
|
expect(source).not.toContain('appStore.updatedAppReleases')
|
||||||
|
expect(source).not.toContain('appStore.updatingApps')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'appStore.uninstallApp(uninstallCandidate.value.id)',
|
'appStore.uninstallApp(uninstallCandidate.value.id)',
|
||||||
)
|
)
|
||||||
@@ -136,10 +165,16 @@ describe('AppStoreApp Sky navigation contract', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gives interactive download, update and remove buttons subtle pointer feedback', () => {
|
it('gives App Store actions calm pointer feedback without moving app rows', () => {
|
||||||
expect(source).toContain('@media (hover: hover) and (pointer: fine)')
|
expect(source).toContain('@media (hover: hover) and (pointer: fine)')
|
||||||
expect(source).toMatch(
|
expect(source).toMatch(
|
||||||
/button:not\(\.store-ranking__detail-link\):not\(:disabled\):hover\)\s*\{[^}]*brightness\(1\.08\)[^}]*translateY\(-1px\)/s,
|
/button:not\(\.store-ranking__detail-link\):not\([\s\S]*?\.store-list__detail-link[\s\S]*?:hover[\s\S]*?brightness\(1\.08\)[\s\S]*?translateY\(-1px\)/,
|
||||||
|
)
|
||||||
|
expect(source).toContain(
|
||||||
|
'.store-list--browse article:hover',
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/button\.store-action-button--get:not\(\.store-list__detail-link\):hover\s*\{[^}]*background:\s*var\(--sky-app-accent-soft\);[^}]*filter:\s*none;[^}]*transform:\s*none;/s,
|
||||||
)
|
)
|
||||||
expect(source).toContain('.app-store-page .store-action-button--icon:hover')
|
expect(source).toContain('.app-store-page .store-action-button--icon:hover')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
@@ -162,7 +197,10 @@ describe('AppStoreApp Sky navigation contract', () => {
|
|||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'class="store-highlight store-highlight--compact phone-effect--expensive-shadow"',
|
'class="store-highlight store-highlight--compact phone-effect--expensive-shadow"',
|
||||||
)
|
)
|
||||||
expect(source).toContain('class="store-today__edition"')
|
expect(source).not.toContain('store-today__edition')
|
||||||
|
expect(source).not.toContain('todayDate')
|
||||||
|
expect(source).not.toContain("phone.t('Apps.appStore.today.freshDaily')")
|
||||||
|
expect(source).not.toContain('store-highlight__story-number')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'class="store-ranking phone-effect--expensive-shadow"',
|
'class="store-ranking phone-effect--expensive-shadow"',
|
||||||
)
|
)
|
||||||
@@ -184,38 +222,95 @@ describe('AppStoreApp Sky navigation contract', () => {
|
|||||||
expect(source).toContain('@click.stop="handleApp(dailyHighlights[0])"')
|
expect(source).toContain('@click.stop="handleApp(dailyHighlights[0])"')
|
||||||
expect(source).toContain('@click.stop="handleApp(finalHighlight)"')
|
expect(source).toContain('@click.stop="handleApp(finalHighlight)"')
|
||||||
expect(source).toContain('.store-highlight:hover')
|
expect(source).toContain('.store-highlight:hover')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-highlight__orbit\s*\{[^}]*border:\s*0;[^}]*radial-gradient[^}]*box-shadow:\s*none;[^}]*filter:\s*blur\(14px\) saturate\(125%\);/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-highlight__orbit::before\s*\{[^}]*radial-gradient[^}]*filter:\s*blur\(12px\);/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-browse-feature__art > span\s*\{[^}]*radial-gradient[^}]*box-shadow:\s*none;[^}]*filter:\s*blur\(11px\) saturate\(125%\);/s,
|
||||||
|
)
|
||||||
|
expect(source).not.toContain('0 0 0 28px')
|
||||||
|
expect(source).not.toContain('0 0 0 22px')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-highlight__footer\s*\{[^}]*border-radius:\s*0 0 var\(--sky-radius-card\) var\(--sky-radius-card\);[^}]*linear-gradient/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-highlight__footer::before\s*\{[^}]*top:\s*-28px;[^}]*linear-gradient/s,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens Top Today apps while keeping their direct app actions separate', () => {
|
it('opens Top Today apps while keeping their direct app actions separate', () => {
|
||||||
expect(source).toContain('class="store-ranking__detail-link"')
|
expect(source).toContain('class="store-ranking__detail-link"')
|
||||||
|
expect(source).toContain('v-for="app in topToday"')
|
||||||
|
expect(source).not.toContain('store-ranking__position')
|
||||||
expect(source).toContain('@click="openAppDetail(app)"')
|
expect(source).toContain('@click="openAppDetail(app)"')
|
||||||
expect(source).toContain('@click.stop="handleApp(app)"')
|
expect(source).toContain('@click.stop="handleApp(app)"')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'.store-ranking li > button:not(.store-ranking__detail-link)',
|
'.store-ranking li > button:not(.store-ranking__detail-link)',
|
||||||
)
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-ranking li > button:not\(\.store-ranking__detail-link\),[\s\S]*?min-width:\s*68px;/,
|
||||||
|
)
|
||||||
expect(source).not.toContain('.store-ranking__detail-link:hover')
|
expect(source).not.toContain('.store-ranking__detail-link:hover')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'button:not(.store-ranking__detail-link):not(:disabled):hover',
|
'button:not(.store-ranking__detail-link):not(',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps App Store actions compatible with the FiveM CEF target', () => {
|
it('keeps App Store actions compatible with the FiveM CEF target', () => {
|
||||||
expect(source.match(/class="store-action-button"/g)).toHaveLength(8)
|
expect(source.match(/class="store-action-button"/g)).toHaveLength(8)
|
||||||
|
expect(source).not.toContain("appAction(app) !== 'open'")
|
||||||
|
expect(source).toContain("appAction(app) === 'installing'")
|
||||||
|
expect(source).toContain("'store-action-button--get': appAction(app) === 'get'")
|
||||||
|
expect(source).toContain(
|
||||||
|
'> button.store-action-button--get:not(.store-browse-feature__details),',
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-ranking li > button\.store-action-button--get:not\(\.store-ranking__detail-link\),[\s\S]*?\.store-list article > button\.store-action-button--get:not\(\.store-list__detail-link\)\s*\{[^}]*width:\s*56px;[^}]*min-width:\s*56px;[^}]*min-height:\s*32px;[^}]*height:\s*32px;[^}]*font-size:\s*11px;[^}]*font-weight:\s*850;/s,
|
||||||
|
)
|
||||||
expect(source).not.toContain(':has(')
|
expect(source).not.toContain(':has(')
|
||||||
expect(source).not.toContain('color-mix(')
|
expect(source).not.toContain('color-mix(')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('builds clean Apps and Games pages with rotating features', () => {
|
it('builds shorter horizontally snapping Apps and Games features', () => {
|
||||||
expect(source).not.toContain('class="store-browse__filters"')
|
expect(source).not.toContain('class="store-browse__filters"')
|
||||||
expect(source).not.toContain('browseFilter')
|
expect(source).not.toContain('browseFilter')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'class="store-browse-feature phone-effect--expensive-shadow"',
|
'class="store-browse-feature phone-effect--expensive-shadow"',
|
||||||
)
|
)
|
||||||
expect(source).toContain('class="store-list store-list--browse"')
|
expect(source).toContain('class="store-list store-list--browse"')
|
||||||
|
expect(source).not.toContain('class="store-list__tags"')
|
||||||
|
expect(source).toContain('class="store-list__tagline"')
|
||||||
|
expect(source).toContain('<small>{{ appStoreTagline(app) }}</small>')
|
||||||
|
expect(source).toContain('function appStoreTagline(')
|
||||||
|
expect(source).toContain('app.description.trim() || app.developer')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-list__tagline\s*\{[^}]*color:\s*var\(--sky-muted\);[^}]*font-size:\s*11px;[^}]*font-weight:\s*500;/s,
|
||||||
|
)
|
||||||
expect(source).toContain('featuredCandidates')
|
expect(source).toContain('featuredCandidates')
|
||||||
expect(source).toContain('globalThis.setInterval')
|
expect(source).toContain('ref="featuredScroller"')
|
||||||
expect(source).toContain('}, 6500)')
|
expect(source).toContain('@scroll.passive="updateFeaturedSlide"')
|
||||||
expect(source).toContain('globalThis.clearInterval')
|
expect(source).toContain('@wheel="handleFeaturedWheel"')
|
||||||
|
expect(source).toContain('function handleFeaturedWheel(event: WheelEvent)')
|
||||||
|
expect(source).toContain('event.preventDefault()')
|
||||||
|
expect(source).toContain('v-for="(app, index) in featuredCandidates"')
|
||||||
|
expect(source).toContain(":class=\"{ 'is-active': featuredSlide === index }\"")
|
||||||
|
expect(source).toContain('@click="scrollToFeatured(index)"')
|
||||||
|
expect(source).not.toContain('globalThis.setInterval')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-browse__featured-scroll\s*\{[^}]*overflow-x:\s*auto;[^}]*scroll-behavior:\s*smooth;[^}]*scroll-snap-type:\s*x mandatory;/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-browse-feature\s*\{[^}]*min-height:\s*246px;[^}]*opacity:\s*0\.72;[^}]*scroll-snap-align:\s*start;[^}]*scroll-snap-stop:\s*always;[^}]*transform:\s*scale\(0\.97\);[^}]*transition:/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-browse-feature\.is-active\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*scale\(1\);/s,
|
||||||
|
)
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-browse-feature footer::before\s*\{[^}]*top:\s*-28px;[^}]*linear-gradient\(180deg, transparent, rgba\(5, 8, 16, 0\.3\)\);[^}]*backdrop-filter:\s*blur\(7px\) saturate\(110%\);/s,
|
||||||
|
)
|
||||||
expect(source).toMatch(
|
expect(source).toMatch(
|
||||||
/\.store-browse__pages button\s*\{[^}]*width:\s*var\(--sky-touch-target\)[^}]*height:\s*var\(--sky-touch-target\)/s,
|
/\.store-browse__pages button\s*\{[^}]*width:\s*var\(--sky-touch-target\)[^}]*height:\s*var\(--sky-touch-target\)/s,
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,11 @@ describe('AppStoreDetail contract', () => {
|
|||||||
expect(source).toContain("emit('share')")
|
expect(source).toContain("emit('share')")
|
||||||
expect(source).toContain('<AppStoreAction :action="action" />')
|
expect(source).toContain('<AppStoreAction :action="action" />')
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
":class=\"{ 'store-detail__action--icon': action !== 'open' }\"",
|
"'store-detail__action--icon': action === 'installing'",
|
||||||
|
)
|
||||||
|
expect(source).toContain("'store-detail__action--get': action === 'get'")
|
||||||
|
expect(source).toMatch(
|
||||||
|
/\.store-detail__hero \.store-detail__action--get\s*\{[^}]*min-width:\s*54px;[^}]*min-height:\s*30px;[^}]*height:\s*30px;/s,
|
||||||
)
|
)
|
||||||
expect(source).not.toContain(':has(')
|
expect(source).not.toContain(':has(')
|
||||||
expect(source).toMatch(
|
expect(source).toMatch(
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ function updateActivePreview(): void {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="store-detail__action"
|
class="store-detail__action"
|
||||||
:class="{ 'store-detail__action--icon': action !== 'open' }"
|
:class="{
|
||||||
|
'store-detail__action--icon': action === 'installing',
|
||||||
|
'store-detail__action--get': action === 'get',
|
||||||
|
}"
|
||||||
:disabled="action === 'installing'"
|
:disabled="action === 'installing'"
|
||||||
@click="emit('action')"
|
@click="emit('action')"
|
||||||
>
|
>
|
||||||
@@ -389,6 +392,14 @@ function updateActivePreview(): void {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-detail__hero .store-detail__action--get {
|
||||||
|
min-width: 54px;
|
||||||
|
min-height: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.store-detail__facts {
|
.store-detail__facts {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -1783,7 +1783,6 @@ Locales["en"] = {
|
|||||||
selected = "Apps available for your Sky Phone",
|
selected = "Apps available for your Sky Phone",
|
||||||
tabs = { today = "Today", apps = "Apps", games = "Games", search = "Search" },
|
tabs = { today = "Today", apps = "Apps", games = "Games", search = "Search" },
|
||||||
today = {
|
today = {
|
||||||
freshDaily = "A fresh selection every day", dailyEdition = "Daily Edition",
|
|
||||||
featured = "Featured today",
|
featured = "Featured today",
|
||||||
gameHighlight = "Game highlight", appHighlight = "App highlight",
|
gameHighlight = "Game highlight", appHighlight = "App highlight",
|
||||||
curatedForYou = "Curated for you", editorsChoice = "Editor's Choice",
|
curatedForYou = "Curated for you", editorsChoice = "Editor's Choice",
|
||||||
@@ -1804,16 +1803,38 @@ Locales["en"] = {
|
|||||||
handPicked = "Hand-picked for you", essentialApps = "Essential Apps",
|
handPicked = "Hand-picked for you", essentialApps = "Essential Apps",
|
||||||
essentialGames = "Essential Games",
|
essentialGames = "Essential Games",
|
||||||
},
|
},
|
||||||
|
taglines = {
|
||||||
|
health = "Health, activity and medical information",
|
||||||
|
["weazel-news"] = "Local news and city stories",
|
||||||
|
companies = "Businesses, jobs and services", music = "Songs, playlists and audio",
|
||||||
|
picstagram = "Photo sharing and social feed", feather = "Short posts and city conversations",
|
||||||
|
fliptok = "Short videos and trends", flare = "Social posts and live moments",
|
||||||
|
calendar = "Events, schedules and reminders", radio = "Live radio and team communication",
|
||||||
|
["local-pages"] = "Local businesses and community pages", crewlink = "Crews, members and coordination",
|
||||||
|
phone = "Calls, contacts and voicemail", messages = "Private texts and media",
|
||||||
|
darkchat = "Private messaging and secure groups", garage = "Vehicles, parking and valet",
|
||||||
|
house = "Homes, access and management", map = "Navigation and nearby places",
|
||||||
|
skyride = "City rides and driver booking", banking = "Accounts, transfers and payments",
|
||||||
|
billing = "Invoices and payment requests", mail = "Email and attachments",
|
||||||
|
notes = "Notes, lists and ideas", memos = "Voice recordings and memos",
|
||||||
|
calculator = "Everyday and scientific calculations", camera = "Photos and video capture",
|
||||||
|
clock = "Time, alarms and timers", weather = "Forecasts and current conditions",
|
||||||
|
photos = "Photo and video library", settings = "Phone preferences and security",
|
||||||
|
snake = "Classic arcade snake", memory = "Card matching challenge",
|
||||||
|
["number-merge"] = "Number puzzle and strategy", minesweeper = "Classic logic puzzle",
|
||||||
|
["tower-stack"] = "Precision tower-building game", ["sky-flappy"] = "Fast arcade flying challenge",
|
||||||
|
citymarkt = "Local marketplace and listings", ["neon-drop"] = "Neon block-dropping puzzle",
|
||||||
|
},
|
||||||
search = {
|
search = {
|
||||||
recommended = "Recommended", discover = "Discover", results = "Search Results",
|
recommended = "Recommended", discover = "Discover", results = "Search Results",
|
||||||
noResults = "No matching apps or games found.", ad = "Ad",
|
noResults = "No Results", noResultsBody = "Try searching for a different app or game.", ad = "Ad",
|
||||||
topApps = "Top Apps", topGames = "Top Games", bestApps = "Bestselling Apps",
|
topApps = "Top Apps", topGames = "Top Games", bestApps = "Bestselling Apps",
|
||||||
bestGames = "Bestselling Games", productivity = "Productivity", photoVideo = "Photo & Video",
|
bestGames = "Bestselling Games", productivity = "Productivity", photoVideo = "Photo & Video",
|
||||||
},
|
},
|
||||||
account = {
|
account = {
|
||||||
account = "Account", title = "App Management", skyAccount = "Sky Phone Account",
|
account = "Account", title = "App Management", skyAccount = "Sky Phone Account",
|
||||||
apps = "Installed Apps", games = "Games", library = "Your Library", myApps = "My Apps",
|
apps = "Installed Apps", games = "Games", library = "Your Library", myApps = "My Apps",
|
||||||
update = "UPDATE", uninstall = "Uninstall", uninstallApp = "Uninstall {app}",
|
downloadedOn = "Downloaded {date}", uninstall = "Uninstall", uninstallApp = "Uninstall {app}",
|
||||||
uninstallTitle = "Uninstall this app?",
|
uninstallTitle = "Uninstall this app?",
|
||||||
uninstallBody = "{app} will be removed from this phone. You can download it again from the App Store.",
|
uninstallBody = "{app} will be removed from this phone. You can download it again from the App Store.",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user