mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-31 02:08:58 +00:00
408 lines
12 KiB
TypeScript
408 lines
12 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',
|
|
'ocean',
|
|
'sunrise',
|
|
'violet',
|
|
'forest',
|
|
'cobalt',
|
|
'rose',
|
|
'sand',
|
|
'graphite',
|
|
'prism',
|
|
] as const
|
|
export const PHONE_SCALE_MIN = 75
|
|
export const PHONE_SCALE_MAX = 150
|
|
export const PHONE_SCALE_STEP = 1
|
|
export const PHONE_SETUP_LAST_STEP = 9
|
|
|
|
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 BuiltInWallpaperId = (typeof WALLPAPER_IDS)[number]
|
|
export type WallpaperId = BuiltInWallpaperId | 'custom'
|
|
export type WallpaperTarget = 'home' | 'lock'
|
|
export type WallpaperHistoryEntry = {
|
|
imageUrl: string | null
|
|
wallpaper: WallpaperId
|
|
}
|
|
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
|
|
lockWallpaper: WallpaperId
|
|
lockWallpaperImageUrl: string | null
|
|
notificationSound: NotificationSoundId
|
|
notificationDurationSeconds: number
|
|
notificationVolume: number
|
|
notifications: Record<LaunchablePhoneAppId, AppNotificationPreferences>
|
|
phoneScale: number
|
|
ringtone: RingtoneId
|
|
ringtoneVolume: number
|
|
screenBrightness: number
|
|
setupCompleted: boolean
|
|
setupStep: number
|
|
streamerMode: boolean
|
|
wallpaper: WallpaperId
|
|
wallpaperHistory: WallpaperHistoryEntry[]
|
|
wallpaperImageUrl: string | null
|
|
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 },
|
|
health: { 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 },
|
|
citywarn: { enabled: true, sounds: true },
|
|
companies: { enabled: true, sounds: true },
|
|
'weazel-news': { 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 },
|
|
crypto: { 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 },
|
|
memos: { 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: 'ultimate',
|
|
lockWallpaper: 'midnight',
|
|
lockWallpaperImageUrl: null,
|
|
notificationSound: 'chime',
|
|
notificationDurationSeconds: 10,
|
|
notificationVolume: 70,
|
|
notifications: DEFAULT_APP_NOTIFICATIONS,
|
|
phoneScale: 100,
|
|
ringtone: 'skyline',
|
|
ringtoneVolume: 80,
|
|
screenBrightness: 100,
|
|
setupCompleted: false,
|
|
setupStep: 0,
|
|
streamerMode: false,
|
|
wallpaper: 'midnight',
|
|
wallpaperHistory: [{ imageUrl: null, wallpaper: 'midnight' }],
|
|
wallpaperImageUrl: null,
|
|
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 readWallpaperImageUrl(value: unknown): string | null {
|
|
if (typeof value !== 'string') return null
|
|
const imageUrl = value.trim()
|
|
return imageUrl.length > 0 && imageUrl.length <= 2_000_000 ? imageUrl : null
|
|
}
|
|
|
|
function readWallpaperHistory(value: unknown): WallpaperHistoryEntry[] {
|
|
if (!Array.isArray(value)) return []
|
|
|
|
const history: WallpaperHistoryEntry[] = []
|
|
for (const rawEntry of value.slice(0, 4)) {
|
|
if (!rawEntry || typeof rawEntry !== 'object' || Array.isArray(rawEntry)) {
|
|
continue
|
|
}
|
|
const entry = rawEntry as Partial<WallpaperHistoryEntry>
|
|
const imageUrl = readWallpaperImageUrl(entry.imageUrl)
|
|
if (entry.wallpaper === 'custom' && imageUrl) {
|
|
history.push({ imageUrl, wallpaper: 'custom' })
|
|
continue
|
|
}
|
|
if (
|
|
typeof entry.wallpaper === 'string' &&
|
|
WALLPAPER_IDS.includes(entry.wallpaper as BuiltInWallpaperId)
|
|
) {
|
|
history.push({
|
|
imageUrl: null,
|
|
wallpaper: entry.wallpaper as BuiltInWallpaperId,
|
|
})
|
|
}
|
|
}
|
|
return history
|
|
}
|
|
|
|
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 clampPhoneScale(value: number): number {
|
|
return Math.min(PHONE_SCALE_MAX, Math.max(PHONE_SCALE_MIN, value))
|
|
}
|
|
|
|
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
|
|
const wallpaperImageUrl = readWallpaperImageUrl(settings.wallpaperImageUrl)
|
|
const wallpaper =
|
|
settings.wallpaper === 'custom' && wallpaperImageUrl
|
|
? 'custom'
|
|
: readChoice(settings.wallpaper, WALLPAPER_IDS, defaults.wallpaper)
|
|
const requestedLockWallpaperImageUrl = readWallpaperImageUrl(
|
|
settings.lockWallpaperImageUrl,
|
|
)
|
|
const lockWallpaper =
|
|
settings.lockWallpaper === 'custom' && requestedLockWallpaperImageUrl
|
|
? 'custom'
|
|
: typeof settings.lockWallpaper === 'string' &&
|
|
WALLPAPER_IDS.includes(settings.lockWallpaper as BuiltInWallpaperId)
|
|
? (settings.lockWallpaper as BuiltInWallpaperId)
|
|
: wallpaper
|
|
const lockWallpaperImageUrl =
|
|
lockWallpaper === 'custom'
|
|
? (requestedLockWallpaperImageUrl ?? wallpaperImageUrl)
|
|
: null
|
|
const wallpaperHistory = readWallpaperHistory(settings.wallpaperHistory)
|
|
|
|
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,
|
|
),
|
|
lockWallpaper,
|
|
lockWallpaperImageUrl,
|
|
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: clampPhoneScale(
|
|
readNumber(
|
|
settings.phoneScale,
|
|
defaults.phoneScale,
|
|
Number.MIN_SAFE_INTEGER,
|
|
Number.MAX_SAFE_INTEGER,
|
|
),
|
|
),
|
|
ringtone: readChoice(
|
|
settings.ringtone,
|
|
RINGTONE_IDS,
|
|
defaults.ringtone,
|
|
),
|
|
ringtoneVolume: readNumber(
|
|
settings.ringtoneVolume,
|
|
defaults.ringtoneVolume,
|
|
0,
|
|
100,
|
|
),
|
|
screenBrightness: readNumber(
|
|
settings.screenBrightness,
|
|
defaults.screenBrightness,
|
|
10,
|
|
100,
|
|
),
|
|
setupCompleted:
|
|
typeof settings.setupCompleted === 'boolean'
|
|
? settings.setupCompleted
|
|
: true,
|
|
setupStep: Math.floor(
|
|
readNumber(
|
|
settings.setupStep,
|
|
typeof settings.setupCompleted === 'boolean'
|
|
? defaults.setupStep
|
|
: PHONE_SETUP_LAST_STEP,
|
|
0,
|
|
PHONE_SETUP_LAST_STEP,
|
|
),
|
|
),
|
|
streamerMode: readBoolean(settings.streamerMode, defaults.streamerMode),
|
|
wallpaper,
|
|
wallpaperHistory:
|
|
wallpaperHistory.length > 0
|
|
? wallpaperHistory
|
|
: [
|
|
{
|
|
imageUrl: wallpaper === 'custom' ? wallpaperImageUrl : null,
|
|
wallpaper,
|
|
},
|
|
],
|
|
wallpaperImageUrl: wallpaper === 'custom' ? wallpaperImageUrl : null,
|
|
wifiEnabled: readBoolean(settings.wifiEnabled, defaults.wifiEnabled),
|
|
},
|
|
version: 1,
|
|
}
|
|
} catch {
|
|
return cloneJsonData(DEFAULT_PHONE_PREFERENCES)
|
|
}
|
|
}
|