diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e29511d..a24c1a4 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,10 +1,11 @@ diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 9ea04d6..5b965be 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -43,35 +43,35 @@ button { width: min(39vh, 390px); aspect-ratio: 390/844; padding: min(1.05vh, 10px); - overflow: hidden; + overflow: visible; pointer-events: auto; - background: linear-gradient(145deg, #41434a, #090a0d 38%, #2b2d32); - border: 1px solid #ffffff38; + background: transparent; + border: 0; border-radius: min(5.2vh, 52px); - box-shadow: - 0 4vh 10vh #0009, - inset 0 0 0 1px #ffffff1f; + transform: scale(var(--phone-scale, 1)); + transition: transform 0.25s ease; + box-shadow: none; + filter: drop-shadow(0 4vh 10vh #0009); } -.phone-device__island { +.phone-device__frame { position: absolute; - top: min(2.05vh, 20px); - left: 50%; - z-index: 80; - width: min(9.2vh, 92px); - height: min(2.85vh, 28px); - transform: translateX(-50%); - background: #050506; - border-radius: 999px; - box-shadow: inset 0 -1px 1px #ffffff12; + z-index: 100; + inset: 0; + width: 100%; + height: 100%; + object-fit: fill; + pointer-events: none; } .phone-screen { position: relative; container-type: size; - width: 100%; - height: 100%; + margin: auto; + margin-top: 0.9vh; + width: 94%; + height: 97%; overflow: hidden; background: #08080a; - border-radius: min(4.25vh, 42px); + border-radius: 4vh; } .phone-app { position: absolute !important; @@ -1763,6 +1763,118 @@ button { .reference-settings .wallpaper-picker button { height: 105px; } +.konsta-settings { + padding: 0; + overflow: hidden; + background: #000; + color: #fff; +} +.settings-konsta-list { + margin-top: 17px !important; + margin-bottom: 17px !important; +} +.settings-konsta-navbar { + z-index: 8; + margin-top: 44px; + flex: none; +} +.settings-subpage-scroll { + height: calc(100% - 88px); + padding: 1px 0 48px; + overflow-y: auto; +} +.settings-app-icon { + width: 32px; + height: 32px; + display: block; + object-fit: contain; +} +.settings-notification-app { + padding: 20px 20px 3px; + display: flex; + flex-direction: column; + align-items: center; + gap: 9px; +} +.settings-notification-app img { + width: 66px; + height: 66px; + object-fit: contain; +} +.settings-notification-app strong { + font-size: 17px; +} +.settings-range-row, +.settings-scale-row { + width: 100%; + display: flex; + align-items: center; + gap: 11px; +} +.settings-range-row > div, +.settings-scale-row > div { + flex: 1; +} +.settings-scale-row > span { + font-size: 13px; +} +.settings-scale-row > strong { + font-size: 21px; +} +.settings-check { + color: #0a84ff; +} +.settings-frame-picker { + margin: 0 16px 22px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 11px; +} +.settings-frame-picker button { + min-width: 0; + padding: 9px 6px 7px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + border: 2px solid transparent; + border-radius: 13px; + background: #1c1c1e; + color: inherit; + font-size: 10px; +} +.settings-frame-picker button.active { + border-color: #0a84ff; +} +.settings-frame-picker img { + width: 40px; + height: 82px; + object-fit: fill; +} +.settings-wallpaper-picker { + margin-top: 10px; +} +.phone-app--light .konsta-settings { + background: #f2f2f7; + color: #000; +} +.phone-app--light .settings-search, +.phone-app--light .settings-account, +.phone-app--light .settings-frame-picker button { + background: #fff; +} +.phone-app--light .settings-search { + color: #8e8e93; +} +.phone-app--light .settings-search input { + color: #000; +} +.phone-app--light .settings-account small { + color: #666; +} +.phone-screen--app .phone-app--light .phone-status-bar { + color: #000; +} * { scrollbar-width: none; } diff --git a/frontend/src/config/appearance.ts b/frontend/src/config/appearance.ts new file mode 100644 index 0000000..9ebeed0 --- /dev/null +++ b/frontend/src/config/appearance.ts @@ -0,0 +1,14 @@ +import blackFrame from '@/assets/img/frames/black.webp' +import blueFrame from '@/assets/img/frames/blue.webp' +import greenFrame from '@/assets/img/frames/green.webp' +import lavenderFrame from '@/assets/img/frames/lavender.webp' +import whiteFrame from '@/assets/img/frames/white.webp' +import type { PhoneFrameId } from '@/utils/preferences' + +export const PHONE_FRAME_IMAGES: Record = { + black: blackFrame, + blue: blueFrame, + green: greenFrame, + lavender: lavenderFrame, + white: whiteFrame, +} diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 1b31b9a..001b182 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -1,9 +1,11 @@ import { defineStore } from 'pinia' -import type { AppLaunchOrigin } from '@/types/apps' +import type { AppLaunchOrigin, PhoneAppId } from '@/types/apps' import { clampPage } from '@/utils/pages' import { readPhonePreferences, + type AppNotificationPreferences, + type PhonePreferencesV1, type WallpaperId, writePhonePreferences, } from '@/utils/preferences' @@ -132,32 +134,61 @@ const defaultLocales: LocaleTree = { settings: { name: 'Settings', searchPlaceholder: 'Search', - wifi: 'Wi-Fi', - bluetooth: 'Bluetooth', airplaneMode: 'Airplane Mode', + streamerMode: 'Streamer Mode', wallpaper: 'Wallpaper', on: 'On', + off: 'Off', accountName: 'Sky Citizen', accountDetail: 'Apple ID, iCloud, Media & Purchases', - mobileServices: 'Mobile Services', - personalHotspot: 'Personal Hotspot', - vpn: 'VPN', notifications: 'Notifications', sounds: 'Sounds & Haptics', - focus: 'Focus', - screenTime: 'Screen Time', - general: 'General', - display: 'Display & Brightness', - homeScreen: 'Home Screen & App Library', - accessibility: 'Accessibility', - battery: 'Battery', - privacy: 'Privacy & Security', - passwords: 'Passwords', + general: 'General Settings', + appearance: 'Appearance', + allowNotifications: 'Allow Notifications', + notificationSounds: 'Sounds', + ringtoneVolume: 'Ringtone Volume', + notificationVolume: 'Notification Volume', + ringtone: 'Ringtone', + notificationSound: 'Notification Sound', + appearanceMode: 'Appearance Mode', + automatic: 'Automatic', + light: 'Light', + dark: 'Dark', + phoneScale: 'Phone Scale', + phoneFrame: 'Phone Frame', + about: 'About', + deviceName: 'Device Name', + deviceNameValue: 'Sky Phone', + softwareVersion: 'Software Version', + language: 'Language', + languageValue: 'English', + localStorage: 'Local Storage', + localStorageValue: 'On Device', + back: 'Settings', wallpaperPicker: 'Built-in Wallpapers', toggle: { - wifi: 'Toggle Wi-Fi', - bluetooth: 'Toggle Bluetooth', airplaneMode: 'Toggle Airplane Mode', + streamerMode: 'Toggle Streamer Mode', + notifications: 'Toggle notifications for {app}', + notificationSounds: 'Toggle notification sounds for {app}', + }, + frames: { + black: 'Black', + blue: 'Blue', + green: 'Green', + lavender: 'Lavender', + white: 'White', + }, + ringtones: { + skyline: 'Skyline', + horizon: 'Horizon', + pulse: 'Pulse', + }, + notificationSoundsList: { + chime: 'Chime', + signal: 'Signal', + soft: 'Soft', }, wallpapers: { midnight: 'Midnight wallpaper', @@ -224,7 +255,15 @@ export const usePhoneStore = defineStore('phone', { launchOrigin: null as AppLaunchOrigin | null, locales: defaultLocales, preferences: readPhonePreferences(), + systemDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches, }), + getters: { + isDarkMode(state): boolean { + if (state.preferences.settings.appearanceMode === 'dark') return true + if (state.preferences.settings.appearanceMode === 'light') return false + return state.systemDarkMode + }, + }, actions: { close(): void { this.isOpen = false @@ -240,25 +279,42 @@ export const usePhoneStore = defineStore('phone', { setLaunchOrigin(origin: AppLaunchOrigin | null): void { this.launchOrigin = origin }, - setSetting( - key: Exclude, + setAppNotification( + appId: PhoneAppId, + key: keyof AppNotificationPreferences, value: boolean, + ): void { + this.preferences.settings.notifications[appId][key] = value + writePhonePreferences(this.preferences) + }, + setPreference( + key: K, + value: PhonePreferencesV1['settings'][K], ): void { this.preferences.settings[key] = value writePhonePreferences(this.preferences) }, + setSystemDarkMode(value: boolean): void { + this.systemDarkMode = value + }, setWallpaper(wallpaper: WallpaperId): void { this.preferences.settings.wallpaper = wallpaper writePhonePreferences(this.preferences) }, - t(path: string): string { + t(path: string, replacements: Record = {}): string { const translated = getByPath(this.locales, path) const fallback = getByPath(defaultLocales, path) - return typeof translated === 'string' - ? translated - : typeof fallback === 'string' - ? fallback - : path + const value = + typeof translated === 'string' + ? translated + : typeof fallback === 'string' + ? fallback + : path + return Object.entries(replacements).reduce( + (result, [key, replacement]) => + result.split(`{${key}}`).join(replacement), + value, + ) }, }, }) diff --git a/frontend/src/utils/preferences.test.ts b/frontend/src/utils/preferences.test.ts index 0fc4fa4..5a2a007 100644 --- a/frontend/src/utils/preferences.test.ts +++ b/frontend/src/utils/preferences.test.ts @@ -11,10 +11,46 @@ describe('preferences', () => { const value = parsePhonePreferences( JSON.stringify({ version: 1, - settings: { wifi: false, wallpaper: 'ember' }, + settings: { + appearanceMode: 'light', + notificationVolume: 45, + notifications: { + camera: { enabled: false, sounds: false }, + }, + phoneScale: 110, + wallpaper: 'ember', + }, }), ) - expect(value.settings.wifi).toBe(false) + expect(value.settings.appearanceMode).toBe('light') + expect(value.settings.notificationVolume).toBe(45) + expect(value.settings.notifications.camera).toEqual({ + enabled: false, + sounds: false, + }) + expect(value.settings.notifications.clock.enabled).toBe(true) + expect(value.settings.phoneScale).toBe(110) expect(value.settings.wallpaper).toBe('ember') }) + + it('clamps ranges and rejects unknown choices', () => { + const value = parsePhonePreferences( + JSON.stringify({ + version: 1, + settings: { + appearanceMode: 'neon', + frame: 'gold', + notificationVolume: -10, + phoneScale: 500, + ringtoneVolume: 120, + }, + }), + ) + + expect(value.settings.appearanceMode).toBe('automatic') + expect(value.settings.frame).toBe('black') + expect(value.settings.notificationVolume).toBe(0) + expect(value.settings.phoneScale).toBe(115) + expect(value.settings.ringtoneVolume).toBe(100) + }) }) diff --git a/frontend/src/utils/preferences.ts b/frontend/src/utils/preferences.ts index 43f359c..8deaa77 100644 --- a/frontend/src/utils/preferences.ts +++ b/frontend/src/utils/preferences.ts @@ -1,34 +1,126 @@ +import type { PhoneAppId } from '@/types/apps' + export const PHONE_PREFERENCES_KEY = 'sky_phone.preferences.v1' -export type WallpaperId = 'midnight' | 'aurora' | 'ember' +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 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 - bluetooth: boolean - cellular: boolean - personalHotspot: boolean - vpn: boolean + appearanceMode: AppearanceMode + frame: PhoneFrameId + notificationSound: NotificationSoundId + notificationVolume: number + notifications: Record + phoneScale: number + ringtone: RingtoneId + ringtoneVolume: number + streamerMode: boolean wallpaper: WallpaperId - wifi: boolean } version: 1 } +const DEFAULT_APP_NOTIFICATIONS: Record< + PhoneAppId, + AppNotificationPreferences +> = { + 'app-store': { enabled: true, sounds: true }, + calculator: { enabled: true, sounds: true }, + camera: { enabled: true, sounds: true }, + clock: { enabled: true, sounds: true }, + photos: { enabled: true, sounds: true }, + settings: { enabled: true, sounds: true }, +} + export const DEFAULT_PHONE_PREFERENCES: PhonePreferencesV1 = { settings: { airplaneMode: false, - bluetooth: true, - cellular: true, - personalHotspot: false, - vpn: false, + appearanceMode: 'automatic', + frame: 'black', + notificationSound: 'chime', + notificationVolume: 70, + notifications: DEFAULT_APP_NOTIFICATIONS, + phoneScale: 100, + ringtone: 'skyline', + ringtoneVolume: 80, + streamerMode: false, wallpaper: 'midnight', - wifi: true, }, version: 1, } -export const WALLPAPER_IDS: WallpaperId[] = ['midnight', 'aurora', 'ember'] +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 PhoneAppId[]) { + 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) @@ -43,28 +135,48 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 { const defaults = DEFAULT_PHONE_PREFERENCES.settings return { settings: { - airplaneMode: - typeof settings.airplaneMode === 'boolean' - ? settings.airplaneMode - : defaults.airplaneMode, - bluetooth: - typeof settings.bluetooth === 'boolean' - ? settings.bluetooth - : defaults.bluetooth, - cellular: - typeof settings.cellular === 'boolean' - ? settings.cellular - : defaults.cellular, - personalHotspot: - typeof settings.personalHotspot === 'boolean' - ? settings.personalHotspot - : defaults.personalHotspot, - vpn: typeof settings.vpn === 'boolean' ? settings.vpn : defaults.vpn, - wallpaper: WALLPAPER_IDS.includes(settings.wallpaper as WallpaperId) - ? (settings.wallpaper as WallpaperId) - : defaults.wallpaper, - wifi: - typeof settings.wifi === 'boolean' ? settings.wifi : defaults.wifi, + 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, + ), + notificationVolume: readNumber( + settings.notificationVolume, + defaults.notificationVolume, + 0, + 100, + ), + notifications: readNotifications(settings.notifications), + phoneScale: readNumber( + settings.phoneScale, + defaults.phoneScale, + 85, + 115, + ), + 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, } diff --git a/frontend/src/views/apps/SettingsApp.vue b/frontend/src/views/apps/SettingsApp.vue index 4850a0a..ad47a78 100644 --- a/frontend/src/views/apps/SettingsApp.vue +++ b/frontend/src/views/apps/SettingsApp.vue @@ -1,158 +1,589 @@ diff --git a/sky_phone/config/locales/en.lua b/sky_phone/config/locales/en.lua index 479ac9c..cbdf66e 100644 --- a/sky_phone/config/locales/en.lua +++ b/sky_phone/config/locales/en.lua @@ -54,13 +54,23 @@ Locales["en"] = { }, }, settings = { - name = "Settings", searchPlaceholder = "Search", wifi = "Wi-Fi", bluetooth = "Bluetooth", - airplaneMode = "Airplane Mode", wallpaper = "Wallpaper", on = "On", accountName = "Sky Citizen", - accountDetail = "Apple ID, iCloud, Media & Purchases", mobileServices = "Mobile Services", personalHotspot = "Personal Hotspot", - vpn = "VPN", notifications = "Notifications", sounds = "Sounds & Haptics", focus = "Focus", screenTime = "Screen Time", - general = "General", display = "Display & Brightness", homeScreen = "Home Screen & App Library", accessibility = "Accessibility", - battery = "Battery", privacy = "Privacy & Security", passwords = "Passwords", wallpaperPicker = "Built-in Wallpapers", - toggle = { wifi = "Toggle Wi-Fi", bluetooth = "Toggle Bluetooth", airplaneMode = "Toggle Airplane Mode" }, + name = "Settings", searchPlaceholder = "Search", airplaneMode = "Airplane Mode", streamerMode = "Streamer Mode", + wallpaper = "Wallpaper", on = "On", off = "Off", accountName = "Sky Citizen", + accountDetail = "Apple ID, iCloud, Media & Purchases", notifications = "Notifications", sounds = "Sounds & Haptics", + general = "General Settings", appearance = "Appearance", allowNotifications = "Allow Notifications", + notificationSounds = "Sounds", ringtoneVolume = "Ringtone Volume", notificationVolume = "Notification Volume", + ringtone = "Ringtone", notificationSound = "Notification Sound", appearanceMode = "Appearance Mode", + automatic = "Automatic", light = "Light", dark = "Dark", phoneScale = "Phone Scale", phoneFrame = "Phone Frame", + about = "About", deviceName = "Device Name", deviceNameValue = "Sky Phone", softwareVersion = "Software Version", + language = "Language", languageValue = "English", localStorage = "Local Storage", localStorageValue = "On Device", + back = "Settings", wallpaperPicker = "Built-in Wallpapers", + toggle = { + airplaneMode = "Toggle Airplane Mode", streamerMode = "Toggle Streamer Mode", + notifications = "Toggle notifications for {app}", notificationSounds = "Toggle notification sounds for {app}", + }, + frames = { black = "Black", blue = "Blue", green = "Green", lavender = "Lavender", white = "White" }, + ringtones = { skyline = "Skyline", horizon = "Horizon", pulse = "Pulse" }, + notificationSoundsList = { chime = "Chime", signal = "Signal", soft = "Soft" }, wallpapers = { midnight = "Midnight wallpaper", aurora = "Aurora wallpaper", ember = "Ember wallpaper" }, }, }, diff --git a/sky_phone/source/html/index.html b/sky_phone/source/html/index.html index 7430e82..5c3577a 100644 --- a/sky_phone/source/html/index.html +++ b/sky_phone/source/html/index.html @@ -4,8 +4,8 @@ Sky Phone - - + +