From 165d26ec9b0a5b9d02ce6197ddaff2b0ebff5c95 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Tue, 18 Aug 2026 15:36:26 +0200 Subject: [PATCH] ADD - register CityWarn phone app Register CityWarn as a built-in, non-removable phone app with notification defaults and catalog ordering. Add its runtime WebP icon and update app inventory contracts to cover installation and launch metadata. --- .../src/assets/img/app-icons/citywarn.webp | Bin 0 -> 9054 bytes frontend/src/config/apps.test.ts | 10 +++++++++- frontend/src/config/apps.ts | 18 ++++++++++++++++++ frontend/src/stores/app-store.test.ts | 4 +++- frontend/src/types/apps.ts | 1 + frontend/src/utils/preferences.ts | 1 + 6 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/img/app-icons/citywarn.webp diff --git a/frontend/src/assets/img/app-icons/citywarn.webp b/frontend/src/assets/img/app-icons/citywarn.webp new file mode 100644 index 0000000000000000000000000000000000000000..d15f7c6beae0ba85f0d8cf4c038f878039bd74b3 GIT binary patch literal 9054 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 literal 0 HcmV?d00001 diff --git a/frontend/src/config/apps.test.ts b/frontend/src/config/apps.test.ts index 0eefc08..ad9117c 100644 --- a/frontend/src/config/apps.test.ts +++ b/frontend/src/config/apps.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { Newspaper } from 'lucide-vue-next' +import { Newspaper, Siren } from 'lucide-vue-next' import { isPhoneAppId, PHONE_APPS } from './apps' describe('app registry', () => { @@ -84,6 +84,14 @@ describe('app registry', () => { expect(PHONE_APPS.find((app) => app.id === 'weazel-news')?.icon).toBe( Newspaper, ) + expect(PHONE_APPS.find((app) => app.id === 'citywarn')).toMatchObject({ + category: 'utilities', + gridOrder: 30, + labelKey: 'Apps.citywarn.name', + route: '/apps/citywarn', + }) + expect(PHONE_APPS.find((app) => app.id === 'citywarn')?.icon).toBe(Siren) + expect(isPhoneAppId('citywarn')).toBe(true) expect(PHONE_APPS.find((app) => app.id === 'music')).toMatchObject({ category: 'utilities', gridOrder: 26, diff --git a/frontend/src/config/apps.ts b/frontend/src/config/apps.ts index 6dbeeae..b3a4ee8 100644 --- a/frontend/src/config/apps.ts +++ b/frontend/src/config/apps.ts @@ -35,6 +35,7 @@ import { UsersRound, Building2, Newspaper, + Siren, HeartPulse, ChartCandlestick, } from 'lucide-vue-next' @@ -80,6 +81,7 @@ import featherIcon from '@/assets/img/app-icons/feather.svg' import crewLinkIcon from '@/assets/img/app-icons/crewlink.svg' import companiesIcon from '@/assets/img/app-icons/companies.svg' import weazelNewsIcon from '@/assets/img/app-icons/weazel-news.svg' +import cityWarnIcon from '@/assets/img/app-icons/citywarn.webp' import type { BuiltinPhoneAppDefinition, BuiltinPhoneAppId, @@ -91,6 +93,20 @@ import type { } from '@/types/apps' export const PHONE_APPS = shallowReactive([ + { + category: 'utilities', + component: markRaw( + defineAsyncComponent(() => import('@/views/apps/CityWarnApp.vue')), + ), + dockOrder: null, + gridOrder: 30, + icon: markRaw(Siren), + iconClass: 'app-icon--citywarn', + iconImage: cityWarnIcon, + id: 'citywarn', + labelKey: 'Apps.citywarn.name', + route: '/apps/citywarn', + }, { category: 'utilities', component: markRaw( @@ -676,6 +692,7 @@ export const DEFAULT_INSTALLED_PHONE_APP_IDS: ReadonlySet = 'map', 'calendar', 'health', + 'citywarn', ]) export const NON_REMOVABLE_PHONE_APP_IDS: ReadonlySet = @@ -688,6 +705,7 @@ export const NON_REMOVABLE_PHONE_APP_IDS: ReadonlySet = 'messages', 'mail', 'health', + 'citywarn', ]) export const PHONE_APP_IDS = PHONE_APPS.map((app) => app.id) diff --git a/frontend/src/stores/app-store.test.ts b/frontend/src/stores/app-store.test.ts index 45129df..f65e70f 100644 --- a/frontend/src/stores/app-store.test.ts +++ b/frontend/src/stores/app-store.test.ts @@ -33,7 +33,7 @@ describe('app store', () => { vi.useRealTimers() }) - it('installs the fifteen system apps by default', () => { + it('installs the sixteen system apps by default', () => { const apps = useAppStoreStore() apps.hydrate(null) @@ -54,6 +54,7 @@ describe('app store', () => { 'map', 'calendar', 'health', + 'citywarn', ]) for (const appId of DEFAULT_INSTALLED_PHONE_APP_IDS) { expect(apps.isInstalled(appId)).toBe(true) @@ -62,6 +63,7 @@ describe('app store', () => { expect(apps.isInstalled('feather')).toBe(false) expect(apps.isInstalled('snake')).toBe(false) expect(apps.isInstalled('health')).toBe(true) + expect(apps.isInstalled('citywarn')).toBe(true) }) it('removes old automatic apps unless the player installed them', () => { diff --git a/frontend/src/types/apps.ts b/frontend/src/types/apps.ts index 388590c..f0cae91 100644 --- a/frontend/src/types/apps.ts +++ b/frontend/src/types/apps.ts @@ -41,6 +41,7 @@ export type BuiltinPhoneAppId = | 'crewlink' | 'companies' | 'weazel-news' + | 'citywarn' declare const externalPhoneAppId: unique symbol diff --git a/frontend/src/utils/preferences.ts b/frontend/src/utils/preferences.ts index 5a4c3af..48b0358 100644 --- a/frontend/src/utils/preferences.ts +++ b/frontend/src/utils/preferences.ts @@ -113,6 +113,7 @@ const DEFAULT_APP_NOTIFICATIONS: Record< 'sky-flappy': { enabled: true, sounds: true }, 'neon-drop': { enabled: true, sounds: true }, citymarkt: { enabled: true, sounds: true }, + citywarn: { enabled: true, sounds: true }, companies: { enabled: true, sounds: true }, 'weazel-news': { enabled: true, sounds: true }, 'local-pages': { enabled: true, sounds: true },