import type { LaunchablePhoneAppId } from '@/types/apps' export const APPEARANCE_MODE_IDS = ['automatic', 'light', 'dark'] as const export const PHONE_FRAME_IDS = [ 'black', 'blue', 'green', 'lavender', 'white', ] as const export const RINGTONE_IDS = ['skyline', 'horizon', 'pulse'] as const export const NOTIFICATION_SOUND_IDS = ['chime', 'signal', 'soft'] as const export const WALLPAPER_IDS = ['midnight', 'aurora', 'ember'] as const export const PHONE_SCALE_MIN = 50 export const PHONE_SCALE_MAX = 150 export const PHONE_SCALE_STEP = 5 export type AppearanceMode = (typeof APPEARANCE_MODE_IDS)[number] export type PhoneFrameId = (typeof PHONE_FRAME_IDS)[number] export type RingtoneId = (typeof RINGTONE_IDS)[number] export type NotificationSoundId = (typeof NOTIFICATION_SOUND_IDS)[number] export type WallpaperId = (typeof WALLPAPER_IDS)[number] export type AppNotificationPreferences = { enabled: boolean sounds: boolean } export type PhonePreferencesV1 = { settings: { airplaneMode: boolean appearanceMode: AppearanceMode frame: PhoneFrameId notificationSound: NotificationSoundId notificationDurationSeconds: number notificationVolume: number notifications: Record phoneScale: number ringtone: RingtoneId ringtoneVolume: number streamerMode: boolean wallpaper: WallpaperId } version: 1 } const DEFAULT_APP_NOTIFICATIONS: Record< LaunchablePhoneAppId, AppNotificationPreferences > = { phone: { enabled: true, sounds: true }, 'app-store': { enabled: true, sounds: true }, calculator: { enabled: true, sounds: true }, snake: { enabled: true, sounds: true }, memory: { enabled: true, sounds: true }, 'number-merge': { enabled: true, sounds: true }, minesweeper: { enabled: true, sounds: true }, 'tower-stack': { enabled: true, sounds: true }, 'sky-flappy': { enabled: true, sounds: true }, 'neon-drop': { enabled: true, sounds: true }, camera: { enabled: true, sounds: true }, clock: { enabled: true, sounds: true }, weather: { enabled: true, sounds: true }, mail: { enabled: true, sounds: true }, map: { enabled: true, sounds: true }, notes: { enabled: true, sounds: true }, photos: { enabled: true, sounds: true }, settings: { enabled: true, sounds: true }, } export const DEFAULT_PHONE_PREFERENCES: PhonePreferencesV1 = { settings: { airplaneMode: false, appearanceMode: 'automatic', frame: 'black', notificationSound: 'chime', notificationDurationSeconds: 10, notificationVolume: 70, notifications: DEFAULT_APP_NOTIFICATIONS, phoneScale: 100, ringtone: 'skyline', ringtoneVolume: 80, streamerMode: false, wallpaper: 'midnight', }, version: 1, } function readBoolean(value: unknown, fallback: boolean): boolean { return typeof value === 'boolean' ? value : fallback } function readNumber( value: unknown, fallback: number, minimum: number, maximum: number, ): number { return typeof value === 'number' && Number.isFinite(value) ? Math.min(maximum, Math.max(minimum, value)) : fallback } function readChoice( value: unknown, choices: readonly T[], fallback: T, ): T { return typeof value === 'string' && choices.includes(value as T) ? (value as T) : fallback } function readNotifications( value: unknown, ): Record { const source = value && typeof value === 'object' ? (value as Partial< Record> >) : {} const notifications = structuredClone(DEFAULT_APP_NOTIFICATIONS) for (const appId of Object.keys(notifications) as LaunchablePhoneAppId[]) { notifications[appId] = { enabled: readBoolean( source[appId]?.enabled, DEFAULT_APP_NOTIFICATIONS[appId].enabled, ), sounds: readBoolean( source[appId]?.sounds, DEFAULT_APP_NOTIFICATIONS[appId].sounds, ), } } return notifications } export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 { if (!raw) return structuredClone(DEFAULT_PHONE_PREFERENCES) try { const parsed = JSON.parse(raw) as Partial const settings = parsed.settings if (parsed.version !== 1 || !settings || typeof settings !== 'object') { return structuredClone(DEFAULT_PHONE_PREFERENCES) } const defaults = DEFAULT_PHONE_PREFERENCES.settings return { settings: { airplaneMode: readBoolean(settings.airplaneMode, defaults.airplaneMode), appearanceMode: readChoice( settings.appearanceMode, APPEARANCE_MODE_IDS, defaults.appearanceMode, ), frame: readChoice(settings.frame, PHONE_FRAME_IDS, defaults.frame), notificationSound: readChoice( settings.notificationSound, NOTIFICATION_SOUND_IDS, defaults.notificationSound, ), notificationDurationSeconds: readNumber( settings.notificationDurationSeconds, defaults.notificationDurationSeconds, 3, 30, ), notificationVolume: readNumber( settings.notificationVolume, defaults.notificationVolume, 0, 100, ), notifications: readNotifications(settings.notifications), phoneScale: readNumber( settings.phoneScale, defaults.phoneScale, PHONE_SCALE_MIN, PHONE_SCALE_MAX, ), ringtone: readChoice( settings.ringtone, RINGTONE_IDS, defaults.ringtone, ), ringtoneVolume: readNumber( settings.ringtoneVolume, defaults.ringtoneVolume, 0, 100, ), streamerMode: readBoolean(settings.streamerMode, defaults.streamerMode), wallpaper: readChoice( settings.wallpaper, WALLPAPER_IDS, defaults.wallpaper, ), }, version: 1, } } catch { return structuredClone(DEFAULT_PHONE_PREFERENCES) } }