Files
sky_phone/frontend/src/utils/preferences.ts
T
Leon.Schmidt 42ac955052 ADD - expand phone application platform
Add the Companies app, resilient call handling, custom app registry, vendor compatibility, storage policies, frontend bridge, tests, documentation, config, locale, and SQL migrations.
2026-08-11 00:22:39 +02:00

289 lines
8.7 KiB
TypeScript

import type { LaunchablePhoneAppId } from '@/types/apps'
import { cloneJsonData } from '@/utils/clone'
export const APPEARANCE_MODE_IDS = ['automatic', 'light', 'dark'] as const
export const GRAPHICS_MODE_IDS = ['performance', 'ultimate'] as const
export const PHONE_FRAME_IDS = [
'black',
'blue',
'green',
'lavender',
'red',
'white',
'orange',
'yellow',
'lime',
'teal',
'cyan',
'purple',
'pink',
'gold',
'rgb',
] 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 GraphicsMode = (typeof GRAPHICS_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 const DEFAULT_APP_NOTIFICATION_PREFERENCES: AppNotificationPreferences =
{
enabled: true,
sounds: true,
}
export type PhonePreferencesV1 = {
settings: {
airplaneMode: boolean
appearanceMode: AppearanceMode
bluetoothEnabled: boolean
cellularEnabled: boolean
focusMode: boolean
frame: PhoneFrameId
graphicsMode: GraphicsMode
notificationSound: NotificationSoundId
notificationDurationSeconds: number
notificationVolume: number
notifications: Record<LaunchablePhoneAppId, AppNotificationPreferences>
phoneScale: number
ringtone: RingtoneId
ringtoneVolume: number
screenBrightness: number
streamerMode: boolean
wallpaper: WallpaperId
wifiEnabled: boolean
}
version: 1
}
const DEFAULT_APP_NOTIFICATIONS: Record<
LaunchablePhoneAppId,
AppNotificationPreferences
> = {
phone: { enabled: true, sounds: true },
messages: { enabled: true, sounds: true },
darkchat: { enabled: true, sounds: true },
flare: { 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 },
citymarkt: { enabled: true, sounds: true },
companies: { enabled: true, sounds: true },
'local-pages': { enabled: true, sounds: true },
picstagram: { enabled: true, sounds: true },
fliptok: { enabled: true, sounds: true },
feather: { enabled: true, sounds: true },
crewlink: { enabled: true, sounds: true },
camera: { enabled: true, sounds: true },
clock: { enabled: true, sounds: true },
calendar: { enabled: true, sounds: true },
weather: { enabled: true, sounds: true },
banking: { enabled: true, sounds: true },
billing: { enabled: true, sounds: true },
garage: { enabled: true, sounds: true },
skyride: { enabled: true, sounds: true },
house: { enabled: true, sounds: true },
mail: { enabled: true, sounds: true },
map: { enabled: true, sounds: true },
notes: { enabled: true, sounds: true },
radio: { enabled: true, sounds: true },
music: { 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',
bluetoothEnabled: true,
cellularEnabled: true,
focusMode: false,
frame: 'black',
graphicsMode: 'performance',
notificationSound: 'chime',
notificationDurationSeconds: 10,
notificationVolume: 70,
notifications: DEFAULT_APP_NOTIFICATIONS,
phoneScale: 100,
ringtone: 'skyline',
ringtoneVolume: 80,
screenBrightness: 100,
streamerMode: false,
wallpaper: 'midnight',
wifiEnabled: true,
},
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<T extends string>(
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<LaunchablePhoneAppId, AppNotificationPreferences> {
const source: Record<string, unknown> =
value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
const notifications = cloneJsonData(DEFAULT_APP_NOTIFICATIONS)
for (const appId of Object.keys(source)) {
if (!/^[a-z0-9][a-z0-9._-]{1,63}$/.test(appId)) continue
const rawPreferences =
source[appId] &&
typeof source[appId] === 'object' &&
!Array.isArray(source[appId])
? (source[appId] as Partial<AppNotificationPreferences>)
: {}
const defaults =
DEFAULT_APP_NOTIFICATIONS[appId as LaunchablePhoneAppId] ??
DEFAULT_APP_NOTIFICATION_PREFERENCES
notifications[appId as LaunchablePhoneAppId] = {
enabled: readBoolean(rawPreferences.enabled, defaults.enabled),
sounds: readBoolean(rawPreferences.sounds, defaults.sounds),
}
}
return notifications
}
export function ensureAppNotificationPreferences(
preferences: PhonePreferencesV1,
appIds: LaunchablePhoneAppId[],
): void {
for (const appId of appIds) {
preferences.settings.notifications[appId] ??= cloneJsonData(
DEFAULT_APP_NOTIFICATION_PREFERENCES,
)
}
}
export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
if (!raw) return cloneJsonData(DEFAULT_PHONE_PREFERENCES)
try {
const parsed = JSON.parse(raw) as Partial<PhonePreferencesV1>
const settings = parsed.settings
if (parsed.version !== 1 || !settings || typeof settings !== 'object') {
return cloneJsonData(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,
),
bluetoothEnabled: readBoolean(
settings.bluetoothEnabled,
defaults.bluetoothEnabled,
),
cellularEnabled: readBoolean(
settings.cellularEnabled,
defaults.cellularEnabled,
),
focusMode: readBoolean(settings.focusMode, defaults.focusMode),
frame: readChoice(settings.frame, PHONE_FRAME_IDS, defaults.frame),
graphicsMode: readChoice(
settings.graphicsMode,
GRAPHICS_MODE_IDS,
defaults.graphicsMode,
),
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,
),
screenBrightness: readNumber(
settings.screenBrightness,
defaults.screenBrightness,
10,
100,
),
streamerMode: readBoolean(settings.streamerMode, defaults.streamerMode),
wallpaper: readChoice(
settings.wallpaper,
WALLPAPER_IDS,
defaults.wallpaper,
),
wifiEnabled: readBoolean(settings.wifiEnabled, defaults.wifiEnabled),
},
version: 1,
}
} catch {
return cloneJsonData(DEFAULT_PHONE_PREFERENCES)
}
}