diff --git a/frontend/src/assets/img/app-previews/banking.jpg b/frontend/src/assets/img/app-previews/banking.jpg new file mode 100644 index 0000000..886efff Binary files /dev/null and b/frontend/src/assets/img/app-previews/banking.jpg differ diff --git a/frontend/src/assets/img/app-previews/billing.jpg b/frontend/src/assets/img/app-previews/billing.jpg new file mode 100644 index 0000000..906b512 Binary files /dev/null and b/frontend/src/assets/img/app-previews/billing.jpg differ diff --git a/frontend/src/assets/img/app-previews/calculator.jpg b/frontend/src/assets/img/app-previews/calculator.jpg new file mode 100644 index 0000000..f22821e Binary files /dev/null and b/frontend/src/assets/img/app-previews/calculator.jpg differ diff --git a/frontend/src/assets/img/app-previews/calendar.jpg b/frontend/src/assets/img/app-previews/calendar.jpg new file mode 100644 index 0000000..852d011 Binary files /dev/null and b/frontend/src/assets/img/app-previews/calendar.jpg differ diff --git a/frontend/src/assets/img/app-previews/camera.jpg b/frontend/src/assets/img/app-previews/camera.jpg new file mode 100644 index 0000000..70bc357 Binary files /dev/null and b/frontend/src/assets/img/app-previews/camera.jpg differ diff --git a/frontend/src/assets/img/app-previews/citymarkt.jpg b/frontend/src/assets/img/app-previews/citymarkt.jpg new file mode 100644 index 0000000..33da788 Binary files /dev/null and b/frontend/src/assets/img/app-previews/citymarkt.jpg differ diff --git a/frontend/src/assets/img/app-previews/citywarn.jpg b/frontend/src/assets/img/app-previews/citywarn.jpg new file mode 100644 index 0000000..637bebe Binary files /dev/null and b/frontend/src/assets/img/app-previews/citywarn.jpg differ diff --git a/frontend/src/assets/img/app-previews/clock.jpg b/frontend/src/assets/img/app-previews/clock.jpg new file mode 100644 index 0000000..7e13376 Binary files /dev/null and b/frontend/src/assets/img/app-previews/clock.jpg differ diff --git a/frontend/src/assets/img/app-previews/companies.jpg b/frontend/src/assets/img/app-previews/companies.jpg new file mode 100644 index 0000000..45b3c74 Binary files /dev/null and b/frontend/src/assets/img/app-previews/companies.jpg differ diff --git a/frontend/src/assets/img/app-previews/crewlink.jpg b/frontend/src/assets/img/app-previews/crewlink.jpg new file mode 100644 index 0000000..1071115 Binary files /dev/null and b/frontend/src/assets/img/app-previews/crewlink.jpg differ diff --git a/frontend/src/assets/img/app-previews/crypto.jpg b/frontend/src/assets/img/app-previews/crypto.jpg new file mode 100644 index 0000000..66c5d92 Binary files /dev/null and b/frontend/src/assets/img/app-previews/crypto.jpg differ diff --git a/frontend/src/assets/img/app-previews/darkchat.jpg b/frontend/src/assets/img/app-previews/darkchat.jpg new file mode 100644 index 0000000..ae7dfb3 Binary files /dev/null and b/frontend/src/assets/img/app-previews/darkchat.jpg differ diff --git a/frontend/src/assets/img/app-previews/feather.jpg b/frontend/src/assets/img/app-previews/feather.jpg new file mode 100644 index 0000000..3351119 Binary files /dev/null and b/frontend/src/assets/img/app-previews/feather.jpg differ diff --git a/frontend/src/assets/img/app-previews/flare.jpg b/frontend/src/assets/img/app-previews/flare.jpg new file mode 100644 index 0000000..5a67f7f Binary files /dev/null and b/frontend/src/assets/img/app-previews/flare.jpg differ diff --git a/frontend/src/assets/img/app-previews/fliptok.jpg b/frontend/src/assets/img/app-previews/fliptok.jpg new file mode 100644 index 0000000..d186d37 Binary files /dev/null and b/frontend/src/assets/img/app-previews/fliptok.jpg differ diff --git a/frontend/src/assets/img/app-previews/garage.jpg b/frontend/src/assets/img/app-previews/garage.jpg new file mode 100644 index 0000000..4b49ccd Binary files /dev/null and b/frontend/src/assets/img/app-previews/garage.jpg differ diff --git a/frontend/src/assets/img/app-previews/health.jpg b/frontend/src/assets/img/app-previews/health.jpg new file mode 100644 index 0000000..79df273 Binary files /dev/null and b/frontend/src/assets/img/app-previews/health.jpg differ diff --git a/frontend/src/assets/img/app-previews/house.jpg b/frontend/src/assets/img/app-previews/house.jpg new file mode 100644 index 0000000..e640aef Binary files /dev/null and b/frontend/src/assets/img/app-previews/house.jpg differ diff --git a/frontend/src/assets/img/app-previews/local-pages.jpg b/frontend/src/assets/img/app-previews/local-pages.jpg new file mode 100644 index 0000000..99ea4cc Binary files /dev/null and b/frontend/src/assets/img/app-previews/local-pages.jpg differ diff --git a/frontend/src/assets/img/app-previews/mail.jpg b/frontend/src/assets/img/app-previews/mail.jpg new file mode 100644 index 0000000..6c7bb7e Binary files /dev/null and b/frontend/src/assets/img/app-previews/mail.jpg differ diff --git a/frontend/src/assets/img/app-previews/map.jpg b/frontend/src/assets/img/app-previews/map.jpg new file mode 100644 index 0000000..ad40e7f Binary files /dev/null and b/frontend/src/assets/img/app-previews/map.jpg differ diff --git a/frontend/src/assets/img/app-previews/memory.jpg b/frontend/src/assets/img/app-previews/memory.jpg new file mode 100644 index 0000000..a1d9ede Binary files /dev/null and b/frontend/src/assets/img/app-previews/memory.jpg differ diff --git a/frontend/src/assets/img/app-previews/memos.jpg b/frontend/src/assets/img/app-previews/memos.jpg new file mode 100644 index 0000000..707c2c9 Binary files /dev/null and b/frontend/src/assets/img/app-previews/memos.jpg differ diff --git a/frontend/src/assets/img/app-previews/messages.jpg b/frontend/src/assets/img/app-previews/messages.jpg new file mode 100644 index 0000000..6346b0a Binary files /dev/null and b/frontend/src/assets/img/app-previews/messages.jpg differ diff --git a/frontend/src/assets/img/app-previews/minesweeper.jpg b/frontend/src/assets/img/app-previews/minesweeper.jpg new file mode 100644 index 0000000..3f6f8e5 Binary files /dev/null and b/frontend/src/assets/img/app-previews/minesweeper.jpg differ diff --git a/frontend/src/assets/img/app-previews/music.jpg b/frontend/src/assets/img/app-previews/music.jpg new file mode 100644 index 0000000..c3fc042 Binary files /dev/null and b/frontend/src/assets/img/app-previews/music.jpg differ diff --git a/frontend/src/assets/img/app-previews/neon-drop.jpg b/frontend/src/assets/img/app-previews/neon-drop.jpg new file mode 100644 index 0000000..666ab72 Binary files /dev/null and b/frontend/src/assets/img/app-previews/neon-drop.jpg differ diff --git a/frontend/src/assets/img/app-previews/notes.jpg b/frontend/src/assets/img/app-previews/notes.jpg new file mode 100644 index 0000000..7b21f91 Binary files /dev/null and b/frontend/src/assets/img/app-previews/notes.jpg differ diff --git a/frontend/src/assets/img/app-previews/number-merge.jpg b/frontend/src/assets/img/app-previews/number-merge.jpg new file mode 100644 index 0000000..dbb9b62 Binary files /dev/null and b/frontend/src/assets/img/app-previews/number-merge.jpg differ diff --git a/frontend/src/assets/img/app-previews/phone.jpg b/frontend/src/assets/img/app-previews/phone.jpg new file mode 100644 index 0000000..1517ee3 Binary files /dev/null and b/frontend/src/assets/img/app-previews/phone.jpg differ diff --git a/frontend/src/assets/img/app-previews/photos.jpg b/frontend/src/assets/img/app-previews/photos.jpg new file mode 100644 index 0000000..a6b15d8 Binary files /dev/null and b/frontend/src/assets/img/app-previews/photos.jpg differ diff --git a/frontend/src/assets/img/app-previews/picstagram.jpg b/frontend/src/assets/img/app-previews/picstagram.jpg new file mode 100644 index 0000000..34404a2 Binary files /dev/null and b/frontend/src/assets/img/app-previews/picstagram.jpg differ diff --git a/frontend/src/assets/img/app-previews/radio.jpg b/frontend/src/assets/img/app-previews/radio.jpg new file mode 100644 index 0000000..ca5d602 Binary files /dev/null and b/frontend/src/assets/img/app-previews/radio.jpg differ diff --git a/frontend/src/assets/img/app-previews/settings.jpg b/frontend/src/assets/img/app-previews/settings.jpg new file mode 100644 index 0000000..3ed9986 Binary files /dev/null and b/frontend/src/assets/img/app-previews/settings.jpg differ diff --git a/frontend/src/assets/img/app-previews/sky-flappy.jpg b/frontend/src/assets/img/app-previews/sky-flappy.jpg new file mode 100644 index 0000000..58a3860 Binary files /dev/null and b/frontend/src/assets/img/app-previews/sky-flappy.jpg differ diff --git a/frontend/src/assets/img/app-previews/skyride.jpg b/frontend/src/assets/img/app-previews/skyride.jpg new file mode 100644 index 0000000..967a426 Binary files /dev/null and b/frontend/src/assets/img/app-previews/skyride.jpg differ diff --git a/frontend/src/assets/img/app-previews/snake.jpg b/frontend/src/assets/img/app-previews/snake.jpg new file mode 100644 index 0000000..78db698 Binary files /dev/null and b/frontend/src/assets/img/app-previews/snake.jpg differ diff --git a/frontend/src/assets/img/app-previews/tower-stack.jpg b/frontend/src/assets/img/app-previews/tower-stack.jpg new file mode 100644 index 0000000..3416e4f Binary files /dev/null and b/frontend/src/assets/img/app-previews/tower-stack.jpg differ diff --git a/frontend/src/assets/img/app-previews/weather.jpg b/frontend/src/assets/img/app-previews/weather.jpg new file mode 100644 index 0000000..7471449 Binary files /dev/null and b/frontend/src/assets/img/app-previews/weather.jpg differ diff --git a/frontend/src/assets/img/app-previews/weazel-news.jpg b/frontend/src/assets/img/app-previews/weazel-news.jpg new file mode 100644 index 0000000..6056c92 Binary files /dev/null and b/frontend/src/assets/img/app-previews/weazel-news.jpg differ diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 62dc5ad..1338a20 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -2390,6 +2390,8 @@ const defaultLocales: LocaleTree = { essentialGames: 'Essential Games', }, taglines: { + citywarn: 'Live city alerts and public safety updates', + crypto: 'Synthetic markets, portfolio and wallet transfers', health: 'Health, activity and medical information', 'weazel-news': 'Local news and city stories', companies: 'Businesses, jobs and services', @@ -2429,6 +2431,48 @@ const defaultLocales: LocaleTree = { citymarkt: 'Local marketplace and listings', 'neon-drop': 'Neon block-dropping puzzle', }, + previews: { + citywarn: { first: 'Live alerts', second: 'Safety zones', third: 'Incident updates' }, + crypto: { first: 'Synthetic markets', second: 'Portfolio', third: 'Wallet transfers' }, + health: { first: 'Activity rings', second: 'Medical ID', third: 'Health records' }, + 'weazel-news': { first: 'Top stories', second: 'Local reports', third: 'Breaking news' }, + companies: { first: 'Business directory', second: 'Job requests', third: 'Services' }, + music: { first: 'Now playing', second: 'Playlists', third: 'Music library' }, + picstagram: { first: 'Photo feed', second: 'Stories', third: 'Profiles' }, + feather: { first: 'Short posts', second: 'Following feed', third: 'Conversations' }, + fliptok: { first: 'Video feed', second: 'Creator tools', third: 'Trends' }, + flare: { first: 'Discover people', second: 'Matches', third: 'Live moments' }, + calendar: { first: 'Upcoming events', second: 'Day planner', third: 'Reminders' }, + radio: { first: 'Live channels', second: 'Team radio', third: 'Favorites' }, + 'local-pages': { first: 'Local pages', second: 'Reviews', third: 'City discovery' }, + crewlink: { first: 'Crew roster', second: 'Shared locations', third: 'Coordination' }, + phone: { first: 'Recent calls', second: 'Contacts', third: 'Voicemail' }, + messages: { first: 'Conversations', second: 'Media sharing', third: 'Quick replies' }, + darkchat: { first: 'Private chats', second: 'Secure groups', third: 'Invitations' }, + garage: { first: 'Vehicle list', second: 'Parking locations', third: 'Valet' }, + house: { first: 'Property access', second: 'Residents', third: 'Management' }, + map: { first: 'Live navigation', second: 'Nearby places', third: 'Route guidance' }, + skyride: { first: 'Ride booking', second: 'Driver tracking', third: 'Trip history' }, + banking: { first: 'Account balance', second: 'Transfers', third: 'Transactions' }, + billing: { first: 'Open invoices', second: 'Payment requests', third: 'Payment history' }, + mail: { first: 'Inbox', second: 'Attachments', third: 'Mailboxes' }, + notes: { first: 'Notes', second: 'Checklists', third: 'Pinned ideas' }, + memos: { first: 'Voice recordings', second: 'Playback', third: 'Favorites' }, + calculator: { first: 'Basic calculation', second: 'Scientific tools', third: 'History' }, + camera: { first: 'Photo mode', second: 'Video capture', third: 'Zoom controls' }, + clock: { first: 'World clock', second: 'Alarms', third: 'Timers' }, + weather: { first: 'Current weather', second: 'Hourly forecast', third: 'Seven-day outlook' }, + photos: { first: 'Media library', second: 'Albums', third: 'Shared media' }, + settings: { first: 'Device controls', second: 'Privacy', third: 'Personalization' }, + snake: { first: 'High score', second: 'Speed', third: 'Classic grid' }, + memory: { first: 'Matched pairs', second: 'Best time', third: 'Card themes' }, + 'number-merge': { first: 'Highest tile', second: 'Score', third: 'Strategy grid' }, + minesweeper: { first: 'Mine counter', second: 'Best time', third: 'Difficulty' }, + 'tower-stack': { first: 'Tower height', second: 'Perfect drops', third: 'High score' }, + 'sky-flappy': { first: 'Flight score', second: 'Best run', third: 'Obstacles' }, + citymarkt: { first: 'Listings', second: 'Categories', third: 'Saved offers' }, + 'neon-drop': { first: 'Lines cleared', second: 'Level', third: 'Neon pieces' }, + }, search: { recommended: 'Recommended', discover: 'Discover', @@ -2491,6 +2535,23 @@ const defaultLocales: LocaleTree = { about: 'About this App', description: '{app} is a detailed {category} experience made for Sky Phone. Explore its features, connect with the city and keep everything close at hand.', + crypto: { + releaseNotes: + 'VaultX now offers clearer portfolio performance, live synthetic market updates and improved wallet transfers with server-validated quotes.', + previewOverview: 'Portfolio, balance and performance at a glance', + previewCommunity: 'Markets', + markets: 'Markets', + serverPrices: 'Server prices', + communityTitle: 'Trade synthetic markets', + communityBody: + 'Follow fictional assets, review short-lived quotes and trade against available exchange liquidity.', + featuredMoment: 'Live market movers', + previewInsights: 'Portfolio', + weeklyActivity: 'Simulated performance', + fromLastWeek: 'fictional weekly portfolio change', + description: + "VaultX is Sky Phone's fictional crypto exchange and wallet. Track synthetic markets, manage your character-bound portfolio, deposit or withdraw funds, trade with short-lived server quotes and transfer assets using public VaultX keys. All prices are fictional and have no real-world value.", + }, }, }, phone: { diff --git a/frontend/src/utils/appStorePreviewImages.ts b/frontend/src/utils/appStorePreviewImages.ts new file mode 100644 index 0000000..4a6f599 --- /dev/null +++ b/frontend/src/utils/appStorePreviewImages.ts @@ -0,0 +1,28 @@ +import type { BuiltinPhoneAppId } from '@/types/apps' + +type PreviewableBuiltinAppId = Exclude + +const previewModules = import.meta.glob( + '../assets/img/app-previews/*.jpg', + { + eager: true, + import: 'default', + query: '?url', + }, +) + +const APP_STORE_PREVIEW_IMAGES = Object.fromEntries( + Object.entries(previewModules).map(([path, imageUrl]) => { + const appId = path.split('/').at(-1)?.replace(/\.jpg$/, '') + if (!appId) throw new Error(`Invalid App Store preview path: ${path}`) + return [appId, imageUrl] + }), +) as Partial> + +export function getAppStorePreviewImage(appId: string): string | null { + return APP_STORE_PREVIEW_IMAGES[appId as PreviewableBuiltinAppId] ?? null +} + +export const APP_STORE_PREVIEW_IMAGE_IDS = Object.freeze( + Object.keys(APP_STORE_PREVIEW_IMAGES) as PreviewableBuiltinAppId[], +) diff --git a/frontend/src/utils/appStorePreviews.test.ts b/frontend/src/utils/appStorePreviews.test.ts new file mode 100644 index 0000000..03d8721 --- /dev/null +++ b/frontend/src/utils/appStorePreviews.test.ts @@ -0,0 +1,68 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +import { describe, expect, it } from 'vitest' + +import { PHONE_APPS } from '@/config/apps' +import { APP_STORE_PREVIEW_IMAGE_IDS } from '@/utils/appStorePreviewImages' +import { + getAppStorePreviewVisual, + PREVIEWABLE_BUILTIN_APP_IDS, +} from '@/utils/appStorePreviews' + +describe('App Store preview catalog', () => { + it('contains a real captured screenshot for every built-in store app', () => { + const storeAppIds = PHONE_APPS.filter( + (app) => app.kind !== 'external' && app.id !== 'app-store', + ).map((app) => app.id) + + expect([...APP_STORE_PREVIEW_IMAGE_IDS].sort()).toEqual(storeAppIds.sort()) + }) + + it('provides specialized preview data for every built-in store app', () => { + const storeAppIds = PHONE_APPS.filter( + (app) => app.kind !== 'external' && app.id !== 'app-store', + ).map((app) => app.id) + + expect([...PREVIEWABLE_BUILTIN_APP_IDS].sort()).toEqual( + storeAppIds.sort(), + ) + const visualSignatures = new Set() + for (const appId of storeAppIds) { + const preview = getAppStorePreviewVisual(appId) + visualSignatures.add(`${preview.accent}:${preview.surface}`) + } + expect(visualSignatures.size).toBe(storeAppIds.length) + }) + + it('localizes every specialized preview in the fallback, English and German locales', () => { + const localeSources = [ + readFileSync( + fileURLToPath(new URL('../stores/phone.ts', import.meta.url)), + 'utf8', + ), + readFileSync( + fileURLToPath( + new URL('../../../sky_phone/config/locales/en.lua', import.meta.url), + ), + 'utf8', + ), + readFileSync( + fileURLToPath( + new URL('../../../sky_phone/config/locales/de.lua', import.meta.url), + ), + 'utf8', + ), + ] + + for (const appId of PREVIEWABLE_BUILTIN_APP_IDS) { + for (const source of localeSources) { + expect(source).toMatch( + new RegExp( + `(?:["']${appId}["']\\]?|${appId.replace(/-/g, '\\-')})\\s*[:=]\\s*\\{\\s*first\\s*[:=]`, + ), + ) + } + } + }) +}) diff --git a/frontend/src/utils/appStorePreviews.ts b/frontend/src/utils/appStorePreviews.ts new file mode 100644 index 0000000..c393593 --- /dev/null +++ b/frontend/src/utils/appStorePreviews.ts @@ -0,0 +1,68 @@ +import type { BuiltinPhoneAppId } from '@/types/apps' + +export type AppStorePreviewVisual = { + accent: string + surface: string +} + +type PreviewableBuiltinAppId = Exclude + +const APP_STORE_PREVIEW_STYLES = { + citywarn: { accent: '#ff4f5e', surface: '#250e17' }, + crypto: { accent: '#855cff', surface: '#120d26' }, + health: { accent: '#ff3b6b', surface: '#250d18' }, + 'weazel-news': { accent: '#f4bf32', surface: '#201b0c' }, + companies: { accent: '#4a92ff', surface: '#0c1728' }, + music: { accent: '#fa3d71', surface: '#240b19' }, + picstagram: { accent: '#e54cff', surface: '#210b27' }, + feather: { accent: '#3c9cff', surface: '#091c2d' }, + fliptok: { accent: '#24f0d2', surface: '#071d1b' }, + flare: { accent: '#ff567f', surface: '#260d18' }, + calendar: { accent: '#ff554d', surface: '#24100d' }, + radio: { accent: '#47d9ff', surface: '#071c25' }, + 'local-pages': { accent: '#47c98a', surface: '#092118' }, + crewlink: { accent: '#f3a73a', surface: '#221507' }, + phone: { accent: '#35d36f', surface: '#082016' }, + messages: { accent: '#42d66d', surface: '#082117' }, + darkchat: { accent: '#845eff', surface: '#120c27' }, + garage: { accent: '#4d9dff', surface: '#0a1828' }, + house: { accent: '#d4a25c', surface: '#21170c' }, + map: { accent: '#46c97c', surface: '#091f16' }, + skyride: { accent: '#f6c43d', surface: '#211a08' }, + banking: { accent: '#3dcc8b', surface: '#092019' }, + billing: { accent: '#4aa2ff', surface: '#09192a' }, + mail: { accent: '#52a8ff', surface: '#091a2b' }, + notes: { accent: '#ffd34e', surface: '#211b08' }, + memos: { accent: '#ff4f63', surface: '#250c13' }, + calculator: { accent: '#ff9e36', surface: '#231407' }, + camera: { accent: '#b7c0cd', surface: '#11151b' }, + clock: { accent: '#ff9d32', surface: '#251507' }, + weather: { accent: '#54baff', surface: '#091b29' }, + photos: { accent: '#ff5f75', surface: '#251015' }, + settings: { accent: '#8f99a8', surface: '#141820' }, + snake: { accent: '#59e16d', surface: '#071f0c' }, + memory: { accent: '#9c6cff', surface: '#160d29' }, + 'number-merge': { accent: '#f0a451', surface: '#241508' }, + minesweeper: { accent: '#7a9dba', surface: '#101a23' }, + 'tower-stack': { accent: '#ff6f67', surface: '#260e0c' }, + 'sky-flappy': { accent: '#55cfff', surface: '#081d26' }, + citymarkt: { accent: '#ff9a45', surface: '#241407' }, + 'neon-drop': { accent: '#f244ff', surface: '#240828' }, +} satisfies Record + +const EXTERNAL_APP_PREVIEW: AppStorePreviewVisual = { + accent: '#4d9dff', + surface: '#0a1828', +} + +export function getAppStorePreviewVisual(appId: string): AppStorePreviewVisual { + if (appId === 'app-store') return EXTERNAL_APP_PREVIEW + return ( + APP_STORE_PREVIEW_STYLES[appId as PreviewableBuiltinAppId] ?? + EXTERNAL_APP_PREVIEW + ) +} + +export const PREVIEWABLE_BUILTIN_APP_IDS = Object.freeze( + Object.keys(APP_STORE_PREVIEW_STYLES) as PreviewableBuiltinAppId[], +) diff --git a/frontend/src/views/apps/AppStoreDetail.contract.test.ts b/frontend/src/views/apps/AppStoreDetail.contract.test.ts index d1d55e2..24b67eb 100644 --- a/frontend/src/views/apps/AppStoreDetail.contract.test.ts +++ b/frontend/src/views/apps/AppStoreDetail.contract.test.ts @@ -7,6 +7,10 @@ const source = readFileSync( fileURLToPath(new URL('./AppStoreDetail.vue', import.meta.url)), 'utf8', ) +const previewSource = readFileSync( + fileURLToPath(new URL('./AppStorePreviewCard.vue', import.meta.url)), + 'utf8', +) describe('AppStoreDetail contract', () => { it('renders an Apple-style product page with metadata and direct action', () => { @@ -29,24 +33,34 @@ describe('AppStoreDetail contract', () => { /\.store-detail__hero \.store-detail__action--icon\s*\{[^}]*width:\s*54px;[^}]*min-width:\s*54px;[^}]*min-height:\s*30px;[^}]*height:\s*30px;/s, ) expect(source).not.toContain(':has(') + expect(previewSource).not.toContain('color-mix(') expect(source).toMatch( /\.store-detail\s*\{[^}]*padding:\s*var\(--sky-space-3\) 0 var\(--sky-space-6\)/s, ) }) - it('generates three app-specific preview panels with visible test data', () => { + it('renders five app-specific screenshot and information panels', () => { expect(source).toContain('class="store-detail__previews"') - expect(source).toContain('store-detail-preview--overview') - expect(source).toContain('store-detail-preview--community') - expect(source).toContain('store-detail-preview--insights') - expect( - source.match(/:src="app\.iconImage"/g)?.length, - ).toBeGreaterThanOrEqual(4) - expect(source).toContain('24') - expect(source).toContain('87%') - expect(source).toContain('+18%') + expect(source).toContain(' import { - Activity, - BarChart3, ChevronLeft, ChevronRight, Share2, - Sparkles, Star, - Users, } from 'lucide-vue-next' import { computed, ref } from 'vue' import { getPhoneAppLabel, isExternalPhoneApp } from '@/config/apps' import { usePhoneStore } from '@/stores/phone' import type { LaunchablePhoneAppDefinition } from '@/types/apps' +import { getAppStorePreviewImage } from '@/utils/appStorePreviewImages' +import { getAppStorePreviewVisual } from '@/utils/appStorePreviews' import AppStoreAction from './AppStoreAction.vue' +import AppStorePreviewCard from './AppStorePreviewCard.vue' const props = defineProps<{ action: 'get' | 'installing' | 'open' @@ -31,7 +30,8 @@ const emit = defineEmits<{ const phone = usePhoneStore() const previews = ref(null) const activePreviewIndex = ref(0) -const previewCount = 3 +const previewCount = 5 +const previewScreens = [0, 1, 2, 3, 4] as const const appName = computed(() => getPhoneAppLabel(props.app, phone.t)) const developer = computed(() => isExternalPhoneApp(props.app) @@ -51,6 +51,29 @@ const chartRank = computed(() => `#${(props.app.gridOrder % 8) + 1}`) const version = computed( () => `1.${(props.app.gridOrder % 9) + 1}.${props.app.gridOrder % 5}`, ) +const detailCopyPrefix = computed(() => + props.app.id === 'crypto' + ? 'Apps.appStore.details.crypto' + : 'Apps.appStore.details', +) +const previewVisual = computed(() => getAppStorePreviewVisual(props.app.id)) +const previewImage = computed(() => getAppStorePreviewImage(props.app.id)) +const previewFeatures = computed(() => { + if (isExternalPhoneApp(props.app)) { + return [props.app.name, props.app.developer, props.app.description] + } + const prefix = `Apps.appStore.previews.${props.app.id}` + return [ + phone.t(`${prefix}.first`), + phone.t(`${prefix}.second`), + phone.t(`${prefix}.third`), + ] +}) +const appTagline = computed(() => + isExternalPhoneApp(props.app) + ? props.app.description.trim() || props.app.developer + : phone.t(`Apps.appStore.taglines.${props.app.id}`), +) const detailStyle = computed(() => { const palettes = [ ['#2365d8', '#69c7ff'], @@ -177,7 +200,7 @@ function updateActivePreview(): void {

{{ - phone.t('Apps.appStore.details.releaseNotes', { + phone.t(`${detailCopyPrefix}.releaseNotes`, { app: appName, }) }} @@ -212,69 +235,19 @@ function updateActivePreview(): void { class="store-detail__previews" @scroll.passive="updateActivePreview" > -

-
- {{ phone.t('Apps.appStore.details.previewLive') }} -
- - {{ appName }} - {{ phone.t('Apps.appStore.details.previewOverview') }} -
- 24{{ phone.t('Apps.appStore.details.today') }} - 87%{{ phone.t('Apps.appStore.details.progress') }} -
-
- -
-
- -
-
- {{ phone.t('Apps.appStore.details.previewCommunity') }} -
-
- AMJSRK -
- {{ - phone.t('Apps.appStore.details.communityTitle', { app: appName }) - }} - {{ phone.t('Apps.appStore.details.communityBody') }} -
-
- -
- -
-
- {{ phone.t('Apps.appStore.details.previewInsights') }} -
- {{ phone.t('Apps.appStore.details.weeklyActivity') }} - -
- +18% - {{ phone.t('Apps.appStore.details.fromLastWeek') }} -
- -
+ @@ -282,7 +255,7 @@ function updateActivePreview(): void {

{{ phone.t('Apps.appStore.details.about') }}

{{ - phone.t('Apps.appStore.details.description', { + phone.t(`${detailCopyPrefix}.description`, { app: appName, category: phone.t(`Home.groups.${app.category}`), }) @@ -546,218 +519,6 @@ function updateActivePreview(): void { display: none; } -.store-detail-preview { - width: 224px; - height: 354px; - position: relative; - display: flex; - overflow: hidden; - flex: 0 0 224px; - flex-direction: column; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: var(--sky-radius-card); - padding: var(--sky-space-4); - color: #fff; - background: - radial-gradient( - circle at 78% 12%, - var(--store-detail-glow), - transparent 32% - ), - linear-gradient(155deg, var(--store-detail-accent), #101523 118%); - box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18); - scroll-snap-align: start; -} - -.store-detail-preview::after { - position: absolute; - inset: 0; - background: repeating-linear-gradient( - -35deg, - transparent 0 20px, - rgba(255, 255, 255, 0.025) 21px 22px - ); - content: ''; - pointer-events: none; -} - -.store-detail-preview__status { - position: relative; - z-index: 1; - display: flex; - align-items: center; - justify-content: space-between; - color: rgba(255, 255, 255, 0.72); - font-size: 10px; - font-weight: 800; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.store-detail-preview--overview > img { - width: 90px; - height: 90px; - z-index: 1; - align-self: center; - margin: 26px 0 16px; - border-radius: 24px; - object-fit: cover; - box-shadow: 0 14px 24px rgba(0, 0, 0, 0.28); - transform: rotate(-4deg); -} - -.store-detail-preview > strong, -.store-detail-preview > small { - position: relative; - z-index: 1; -} - -.store-detail-preview > strong { - font-size: 20px; - line-height: 1.08; -} - -.store-detail-preview > small { - margin-top: 5px; - color: rgba(255, 255, 255, 0.68); - font-size: 11px; -} - -.store-detail-preview__metrics { - z-index: 1; - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--sky-space-2); - margin-top: auto; -} - -.store-detail-preview__metrics span { - display: flex; - flex-direction: column; - border-radius: var(--sky-radius-control); - padding: var(--sky-space-3); - background: rgba(255, 255, 255, 0.11); - color: rgba(255, 255, 255, 0.7); - font-size: 9px; -} - -.store-detail-preview__metrics b { - color: #fff; - font-size: 17px; -} - -.store-detail-preview__activity { - z-index: 1; - height: 28px; - display: flex; - align-items: flex-end; - gap: 5px; - margin-top: var(--sky-space-3); -} - -.store-detail-preview__activity span { - width: 20%; - border-radius: var(--sky-radius-pill); - background: rgba(255, 255, 255, 0.42); -} - -.store-detail-preview__activity span:nth-child(1) { - height: 42%; -} -.store-detail-preview__activity span:nth-child(2) { - height: 72%; -} -.store-detail-preview__activity span:nth-child(3) { - height: 55%; -} -.store-detail-preview__activity span:nth-child(4) { - height: 92%; -} -.store-detail-preview__activity span:nth-child(5) { - height: 68%; -} - -.store-detail-preview__avatars { - z-index: 1; - display: flex; - margin: 42px 0 22px; -} - -.store-detail-preview__avatars span { - width: 54px; - height: 54px; - display: grid; - place-items: center; - margin-right: -12px; - border: 3px solid rgba(255, 255, 255, 0.82); - border-radius: 50%; - background: rgba(17, 22, 38, 0.74); - font-size: 12px; - font-weight: 800; -} - -.store-detail-preview__message { - z-index: 1; - display: flex; - align-items: center; - gap: var(--sky-space-2); - margin-top: auto; - border-radius: var(--sky-radius-control); - padding: var(--sky-space-3); - background: rgba(255, 255, 255, 0.12); - font-size: 10px; -} - -.store-detail-preview--community > img, -.store-detail-preview--insights > img { - width: 42px; - height: 42px; - z-index: 1; - position: absolute; - right: var(--sky-space-4); - bottom: var(--sky-space-4); - border-radius: 12px; - object-fit: cover; - box-shadow: 0 8px 14px rgba(0, 0, 0, 0.24); -} - -.store-detail-preview--insights > strong { - margin-top: 36px; -} - -.store-detail-preview__chart { - height: 145px; - z-index: 1; - display: flex; - align-items: flex-end; - gap: 7px; - margin-top: var(--sky-space-5); - border-bottom: 1px solid rgba(255, 255, 255, 0.3); - padding: 0 var(--sky-space-2); -} - -.store-detail-preview__chart span { - width: 14px; - border-radius: 7px 7px 0 0; - background: linear-gradient(180deg, #fff, rgba(255, 255, 255, 0.24)); -} - -.store-detail-preview__insight { - z-index: 1; - display: flex; - flex-direction: column; - margin-top: var(--sky-space-4); -} - -.store-detail-preview__insight b { - font-size: 22px; -} - -.store-detail-preview__insight span { - color: rgba(255, 255, 255, 0.68); - font-size: 10px; -} - @media (hover: hover) { .store-detail__toolbar button:hover { border-color: rgba(255, 255, 255, 0.16); diff --git a/frontend/src/views/apps/AppStorePreviewCard.vue b/frontend/src/views/apps/AppStorePreviewCard.vue new file mode 100644 index 0000000..ae3c9bc --- /dev/null +++ b/frontend/src/views/apps/AppStorePreviewCard.vue @@ -0,0 +1,334 @@ + + + + + diff --git a/sky_phone/config/locales/de.lua b/sky_phone/config/locales/de.lua index 5e12560..1e6d088 100644 --- a/sky_phone/config/locales/de.lua +++ b/sky_phone/config/locales/de.lua @@ -1913,6 +1913,8 @@ Locales["de"] = { essentialGames = "Unverzichtbare Spiele", }, taglines = { + citywarn = "Live-Stadtwarnungen und Hinweise zur öffentlichen Sicherheit", + crypto = "Synthetische Märkte, Portfolio und Wallet-Transfers", health = "Gesundheit, Aktivität und medizinische Informationen", ["weazel-news"] = "Lokale Nachrichten und Stadtgeschichten", companies = "Unternehmen, Arbeitsplätze und Dienstleistungen", music = "Lieder, Wiedergabelisten und Audio", @@ -1934,6 +1936,48 @@ Locales["de"] = { ["tower-stack"] = "Präzises Turmbauspiel", ["sky-flappy"] = "Schnelle Arcade-Flugherausforderung", citymarkt = "Lokaler Marktplatz und Inserate", ["neon-drop"] = "Neon-Blockrätsel", }, + previews = { + citywarn = { first = "Live-Warnungen", second = "Sicherheitszonen", third = "Vorfallmeldungen" }, + crypto = { first = "Synthetische Märkte", second = "Portfolio", third = "Wallet-Transfers" }, + health = { first = "Aktivitätsringe", second = "Notfallpass", third = "Gesundheitsdaten" }, + ["weazel-news"] = { first = "Topmeldungen", second = "Lokale Berichte", third = "Eilmeldungen" }, + companies = { first = "Unternehmensverzeichnis", second = "Jobanfragen", third = "Dienstleistungen" }, + music = { first = "Aktuelle Wiedergabe", second = "Playlists", third = "Musikmediathek" }, + picstagram = { first = "Foto-Feed", second = "Stories", third = "Profile" }, + feather = { first = "Kurzbeiträge", second = "Abonniert-Feed", third = "Unterhaltungen" }, + fliptok = { first = "Video-Feed", second = "Creator-Werkzeuge", third = "Trends" }, + flare = { first = "Personen entdecken", second = "Matches", third = "Live-Momente" }, + calendar = { first = "Anstehende Termine", second = "Tagesplaner", third = "Erinnerungen" }, + radio = { first = "Live-Kanäle", second = "Teamfunk", third = "Favoriten" }, + ["local-pages"] = { first = "Lokale Seiten", second = "Bewertungen", third = "Stadt entdecken" }, + crewlink = { first = "Crew-Mitglieder", second = "Geteilte Standorte", third = "Koordination" }, + phone = { first = "Letzte Anrufe", second = "Kontakte", third = "Voicemail" }, + messages = { first = "Unterhaltungen", second = "Medien teilen", third = "Schnellantworten" }, + darkchat = { first = "Private Chats", second = "Sichere Gruppen", third = "Einladungen" }, + garage = { first = "Fahrzeugliste", second = "Parkorte", third = "Parkservice" }, + house = { first = "Immobilienzugang", second = "Bewohner", third = "Verwaltung" }, + map = { first = "Live-Navigation", second = "Orte in der Nähe", third = "Routenführung" }, + skyride = { first = "Fahrt buchen", second = "Fahrer verfolgen", third = "Fahrtenverlauf" }, + banking = { first = "Kontostand", second = "Überweisungen", third = "Transaktionen" }, + billing = { first = "Offene Rechnungen", second = "Zahlungsanfragen", third = "Zahlungsverlauf" }, + mail = { first = "Posteingang", second = "Anhänge", third = "Postfächer" }, + notes = { first = "Notizen", second = "Checklisten", third = "Angeheftete Ideen" }, + memos = { first = "Sprachaufnahmen", second = "Wiedergabe", third = "Favoriten" }, + calculator = { first = "Grundrechenarten", second = "Wissenschaftliche Funktionen", third = "Verlauf" }, + camera = { first = "Fotomodus", second = "Videoaufnahme", third = "Zoomsteuerung" }, + clock = { first = "Weltuhr", second = "Wecker", third = "Timer" }, + weather = { first = "Aktuelles Wetter", second = "Stündliche Vorhersage", third = "Sieben-Tage-Ausblick" }, + photos = { first = "Medienmediathek", second = "Alben", third = "Geteilte Medien" }, + settings = { first = "Gerätesteuerung", second = "Datenschutz", third = "Personalisierung" }, + snake = { first = "Highscore", second = "Geschwindigkeit", third = "Klassisches Raster" }, + memory = { first = "Gefundene Paare", second = "Bestzeit", third = "Kartendesigns" }, + ["number-merge"] = { first = "Höchste Kachel", second = "Punktestand", third = "Strategieraster" }, + minesweeper = { first = "Minenzähler", second = "Bestzeit", third = "Schwierigkeit" }, + ["tower-stack"] = { first = "Turmhöhe", second = "Perfekte Ablagen", third = "Highscore" }, + ["sky-flappy"] = { first = "Flugpunkte", second = "Bester Lauf", third = "Hindernisse" }, + citymarkt = { first = "Angebote", second = "Kategorien", third = "Gespeicherte Angebote" }, + ["neon-drop"] = { first = "Gelöschte Linien", second = "Level", third = "Neon-Steine" }, + }, search = { recommended = "Empfohlen", discover = "Entdecken", results = "Suchergebnisse", noResults = "Keine Ergebnisse", noResultsBody = "Versuche nach einer anderen App oder einem anderen Spiel zu suchen.", ad = "Anzeige", @@ -1963,6 +2007,15 @@ Locales["de"] = { featuredMoment = "Vorgestellter Moment von heute", weeklyActivity = "Wöchentliche Aktivität", fromLastWeek = "von letzter Woche", about = "Über diese App", description = "{app} ist ein umfangreiches {category}-Erlebnis für Sky Phone. Entdecke die Funktionen, verbinde dich mit der Stadt und behalte alles griffbereit.", + crypto = { + releaseNotes = "VaultX bietet jetzt eine klarere Portfolio-Entwicklung, aktuelle synthetische Märkte und verbesserte Wallet-Transfers mit servervalidierten Angeboten.", + previewOverview = "Portfolio, Guthaben und Entwicklung auf einen Blick", previewCommunity = "Märkte", + markets = "Märkte", serverPrices = "Serverkurse", communityTitle = "Synthetische Märkte handeln", + communityBody = "Verfolge fiktive Assets, prüfe kurzlebige Angebote und handle gegen die verfügbare Börsenliquidität.", + featuredMoment = "Aktuelle Marktbewegungen", previewInsights = "Portfolio", + weeklyActivity = "Simulierte Entwicklung", fromLastWeek = "fiktive wöchentliche Portfolio-Änderung", + description = "VaultX ist die fiktive Krypto-Börse und Wallet für Sky Phone. Verfolge synthetische Märkte, verwalte dein charaktergebundenes Portfolio, zahle Guthaben ein oder aus, handle mit kurzlebigen Serverangeboten und übertrage Assets über öffentliche VaultX-Keys. Alle Kurse sind fiktiv und haben keinen realen Gegenwert.", + }, }, }, settings = { diff --git a/sky_phone/config/locales/en.lua b/sky_phone/config/locales/en.lua index cde65a2..cca7783 100644 --- a/sky_phone/config/locales/en.lua +++ b/sky_phone/config/locales/en.lua @@ -1913,6 +1913,8 @@ Locales["en"] = { essentialGames = "Essential Games", }, taglines = { + citywarn = "Live city alerts and public safety updates", + crypto = "Synthetic markets, portfolio and wallet transfers", health = "Health, activity and medical information", ["weazel-news"] = "Local news and city stories", companies = "Businesses, jobs and services", music = "Songs, playlists and audio", @@ -1934,6 +1936,48 @@ Locales["en"] = { ["tower-stack"] = "Precision tower-building game", ["sky-flappy"] = "Fast arcade flying challenge", citymarkt = "Local marketplace and listings", ["neon-drop"] = "Neon block-dropping puzzle", }, + previews = { + citywarn = { first = "Live alerts", second = "Safety zones", third = "Incident updates" }, + crypto = { first = "Synthetic markets", second = "Portfolio", third = "Wallet transfers" }, + health = { first = "Activity rings", second = "Medical ID", third = "Health records" }, + ["weazel-news"] = { first = "Top stories", second = "Local reports", third = "Breaking news" }, + companies = { first = "Business directory", second = "Job requests", third = "Services" }, + music = { first = "Now playing", second = "Playlists", third = "Music library" }, + picstagram = { first = "Photo feed", second = "Stories", third = "Profiles" }, + feather = { first = "Short posts", second = "Following feed", third = "Conversations" }, + fliptok = { first = "Video feed", second = "Creator tools", third = "Trends" }, + flare = { first = "Discover people", second = "Matches", third = "Live moments" }, + calendar = { first = "Upcoming events", second = "Day planner", third = "Reminders" }, + radio = { first = "Live channels", second = "Team radio", third = "Favorites" }, + ["local-pages"] = { first = "Local pages", second = "Reviews", third = "City discovery" }, + crewlink = { first = "Crew roster", second = "Shared locations", third = "Coordination" }, + phone = { first = "Recent calls", second = "Contacts", third = "Voicemail" }, + messages = { first = "Conversations", second = "Media sharing", third = "Quick replies" }, + darkchat = { first = "Private chats", second = "Secure groups", third = "Invitations" }, + garage = { first = "Vehicle list", second = "Parking locations", third = "Valet" }, + house = { first = "Property access", second = "Residents", third = "Management" }, + map = { first = "Live navigation", second = "Nearby places", third = "Route guidance" }, + skyride = { first = "Ride booking", second = "Driver tracking", third = "Trip history" }, + banking = { first = "Account balance", second = "Transfers", third = "Transactions" }, + billing = { first = "Open invoices", second = "Payment requests", third = "Payment history" }, + mail = { first = "Inbox", second = "Attachments", third = "Mailboxes" }, + notes = { first = "Notes", second = "Checklists", third = "Pinned ideas" }, + memos = { first = "Voice recordings", second = "Playback", third = "Favorites" }, + calculator = { first = "Basic calculation", second = "Scientific tools", third = "History" }, + camera = { first = "Photo mode", second = "Video capture", third = "Zoom controls" }, + clock = { first = "World clock", second = "Alarms", third = "Timers" }, + weather = { first = "Current weather", second = "Hourly forecast", third = "Seven-day outlook" }, + photos = { first = "Media library", second = "Albums", third = "Shared media" }, + settings = { first = "Device controls", second = "Privacy", third = "Personalization" }, + snake = { first = "High score", second = "Speed", third = "Classic grid" }, + memory = { first = "Matched pairs", second = "Best time", third = "Card themes" }, + ["number-merge"] = { first = "Highest tile", second = "Score", third = "Strategy grid" }, + minesweeper = { first = "Mine counter", second = "Best time", third = "Difficulty" }, + ["tower-stack"] = { first = "Tower height", second = "Perfect drops", third = "High score" }, + ["sky-flappy"] = { first = "Flight score", second = "Best run", third = "Obstacles" }, + citymarkt = { first = "Listings", second = "Categories", third = "Saved offers" }, + ["neon-drop"] = { first = "Lines cleared", second = "Level", third = "Neon pieces" }, + }, search = { recommended = "Recommended", discover = "Discover", results = "Search Results", noResults = "No Results", noResultsBody = "Try searching for a different app or game.", ad = "Ad", @@ -1963,6 +2007,15 @@ Locales["en"] = { featuredMoment = "Featured moment from today", weeklyActivity = "Weekly Activity", fromLastWeek = "from last week", about = "About this App", description = "{app} is a detailed {category} experience made for Sky Phone. Explore its features, connect with the city and keep everything close at hand.", + crypto = { + releaseNotes = "VaultX now offers clearer portfolio performance, live synthetic market updates and improved wallet transfers with server-validated quotes.", + previewOverview = "Portfolio, balance and performance at a glance", previewCommunity = "Markets", + markets = "Markets", serverPrices = "Server prices", communityTitle = "Trade synthetic markets", + communityBody = "Follow fictional assets, review short-lived quotes and trade against available exchange liquidity.", + featuredMoment = "Live market movers", previewInsights = "Portfolio", + weeklyActivity = "Simulated performance", fromLastWeek = "fictional weekly portfolio change", + description = "VaultX is Sky Phone's fictional crypto exchange and wallet. Track synthetic markets, manage your character-bound portfolio, deposit or withdraw funds, trade with short-lived server quotes and transfer assets using public VaultX keys. All prices are fictional and have no real-world value.", + }, }, }, settings = {