mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
ENH - frame
This commit is contained in:
+38
-4
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { kApp } from 'konsta/vue'
|
||||
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, type CSSProperties } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import PhoneHomeIndicator from '@/components/PhoneHomeIndicator.vue'
|
||||
import PhoneStatusBar from '@/components/PhoneStatusBar.vue'
|
||||
import { PHONE_FRAME_IMAGES } from '@/config/appearance'
|
||||
import { usePhoneStore, type PhoneOpenPayload } from '@/stores/phone'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
import SpringboardView from '@/views/SpringboardView.vue'
|
||||
@@ -17,6 +18,13 @@ type AppMessage = {
|
||||
const phone = usePhoneStore()
|
||||
const route = useRoute()
|
||||
const isAppRoute = computed(() => route.name === 'app')
|
||||
const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const phoneDeviceStyle = computed<CSSProperties>(() => ({
|
||||
'--phone-scale': phone.preferences.settings.phoneScale / 100,
|
||||
}))
|
||||
const phoneFrameImage = computed(
|
||||
() => PHONE_FRAME_IMAGES[phone.preferences.settings.frame],
|
||||
)
|
||||
|
||||
function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
if (event.data?.type === 'app:open') {
|
||||
@@ -32,9 +40,15 @@ function onKeydown(event: KeyboardEvent): void {
|
||||
void nuiCall('close')
|
||||
}
|
||||
|
||||
function onSystemColorSchemeChange(event: MediaQueryListEvent): void {
|
||||
phone.setSystemDarkMode(event.matches)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', onMessage)
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
systemColorScheme.addEventListener('change', onSystemColorSchemeChange)
|
||||
phone.setSystemDarkMode(systemColorScheme.matches)
|
||||
void nuiCall('ui:ready')
|
||||
if (import.meta.env.DEV) phone.open()
|
||||
})
|
||||
@@ -42,15 +56,28 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', onMessage)
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
systemColorScheme.removeEventListener('change', onSystemColorSchemeChange)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main v-if="phone.isOpen" class="phone-stage">
|
||||
<section class="phone-device" :aria-label="phone.t('Common.phone')">
|
||||
<div class="phone-device__island" aria-hidden="true"></div>
|
||||
<section
|
||||
class="phone-device"
|
||||
:style="phoneDeviceStyle"
|
||||
:aria-label="phone.t('Common.phone')"
|
||||
>
|
||||
<div class="phone-screen" :class="{ 'phone-screen--app': isAppRoute }">
|
||||
<k-app theme="ios" dark safe-areas class="phone-app">
|
||||
<k-app
|
||||
theme="ios"
|
||||
:dark="phone.isDarkMode"
|
||||
safe-areas
|
||||
class="phone-app"
|
||||
:class="{
|
||||
dark: phone.isDarkMode,
|
||||
'phone-app--light': !phone.isDarkMode,
|
||||
}"
|
||||
>
|
||||
<PhoneStatusBar />
|
||||
<SpringboardView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
@@ -61,6 +88,13 @@ onBeforeUnmount(() => {
|
||||
<PhoneHomeIndicator />
|
||||
</k-app>
|
||||
</div>
|
||||
<img
|
||||
class="phone-device__frame"
|
||||
:src="phoneFrameImage"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
draggable="false"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
+131
-19
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<PhoneFrameId, string> = {
|
||||
black: blackFrame,
|
||||
blue: blueFrame,
|
||||
green: greenFrame,
|
||||
lavender: lavenderFrame,
|
||||
white: whiteFrame,
|
||||
}
|
||||
@@ -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<keyof typeof this.preferences.settings, 'wallpaper'>,
|
||||
setAppNotification(
|
||||
appId: PhoneAppId,
|
||||
key: keyof AppNotificationPreferences,
|
||||
value: boolean,
|
||||
): void {
|
||||
this.preferences.settings.notifications[appId][key] = value
|
||||
writePhonePreferences(this.preferences)
|
||||
},
|
||||
setPreference<K extends keyof PhonePreferencesV1['settings']>(
|
||||
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, string> = {}): 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,
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<PhoneAppId, AppNotificationPreferences>
|
||||
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<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<PhoneAppId, AppNotificationPreferences> {
|
||||
const source =
|
||||
value && typeof value === 'object'
|
||||
? (value as Partial<
|
||||
Record<PhoneAppId, Partial<AppNotificationPreferences>>
|
||||
>)
|
||||
: {}
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -1,158 +1,589 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Accessibility,
|
||||
BatteryMedium,
|
||||
kBlockTitle,
|
||||
kList,
|
||||
kListItem,
|
||||
kNavbar,
|
||||
kNavbarBackLink,
|
||||
kPage,
|
||||
kRange,
|
||||
kToggle,
|
||||
} from 'konsta/vue'
|
||||
import {
|
||||
BellRing,
|
||||
Bluetooth,
|
||||
Check,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
Focus,
|
||||
Globe2,
|
||||
Grid2X2,
|
||||
KeyRound,
|
||||
LockKeyhole,
|
||||
EyeOff,
|
||||
Monitor,
|
||||
Plane,
|
||||
Radio,
|
||||
Search,
|
||||
Settings,
|
||||
Smartphone,
|
||||
Sun,
|
||||
UserRound,
|
||||
Volume1,
|
||||
Volume2,
|
||||
Wifi,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { PHONE_FRAME_IMAGES } from '@/config/appearance'
|
||||
import { PHONE_APPS } from '@/config/apps'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { WALLPAPER_IDS } from '@/utils/preferences'
|
||||
import type { PhoneAppDefinition, PhoneAppId } from '@/types/apps'
|
||||
import {
|
||||
APPEARANCE_MODE_IDS,
|
||||
NOTIFICATION_SOUND_IDS,
|
||||
PHONE_FRAME_IDS,
|
||||
RINGTONE_IDS,
|
||||
WALLPAPER_IDS,
|
||||
type AppearanceMode,
|
||||
type NotificationSoundId,
|
||||
type PhoneFrameId,
|
||||
type RingtoneId,
|
||||
} from '@/utils/preferences'
|
||||
|
||||
type SettingsView =
|
||||
| 'root'
|
||||
| 'notifications'
|
||||
| 'notification-detail'
|
||||
| 'sounds'
|
||||
| 'general'
|
||||
| 'appearance'
|
||||
| 'wallpaper'
|
||||
type RootToggleKey = 'airplaneMode' | 'streamerMode'
|
||||
type SubmenuView = Exclude<SettingsView, 'root' | 'notification-detail'>
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const query = ref('')
|
||||
const groups = [
|
||||
[
|
||||
{ key: 'airplaneMode', icon: Plane, color: '#ff9500', toggle: true },
|
||||
{
|
||||
key: 'wifi',
|
||||
icon: Wifi,
|
||||
color: '#0a84ff',
|
||||
value: 'SkyNet 5G',
|
||||
toggle: true,
|
||||
},
|
||||
{
|
||||
key: 'bluetooth',
|
||||
icon: Bluetooth,
|
||||
color: '#0a84ff',
|
||||
valueKey: 'Apps.settings.on',
|
||||
toggle: true,
|
||||
},
|
||||
{ key: 'mobileServices', icon: Radio, color: '#30d158' },
|
||||
{ key: 'personalHotspot', icon: Smartphone, color: '#30d158' },
|
||||
{ key: 'vpn', icon: Globe2, color: '#0a84ff' },
|
||||
],
|
||||
[
|
||||
{ key: 'notifications', icon: BellRing, color: '#ff453a' },
|
||||
{ key: 'sounds', icon: Volume2, color: '#ff2d55' },
|
||||
{ key: 'focus', icon: Focus, color: '#5e5ce6' },
|
||||
{ key: 'screenTime', icon: Clock3, color: '#5e5ce6' },
|
||||
],
|
||||
[
|
||||
{ key: 'general', icon: Settings, color: '#8e8e93' },
|
||||
{ key: 'display', icon: Sun, color: '#0a84ff' },
|
||||
{ key: 'homeScreen', icon: Grid2X2, color: '#5e5ce6' },
|
||||
{ key: 'accessibility', icon: Accessibility, color: '#0a84ff' },
|
||||
{ key: 'wallpaper', icon: Monitor, color: '#64d2ff' },
|
||||
{ key: 'battery', icon: BatteryMedium, color: '#30d158' },
|
||||
{ key: 'privacy', icon: LockKeyhole, color: '#0a84ff' },
|
||||
],
|
||||
[{ key: 'passwords', icon: KeyRound, color: '#636366' }],
|
||||
const activeView = ref<SettingsView>('root')
|
||||
const selectedNotificationAppId = ref<PhoneAppId>('calculator')
|
||||
|
||||
const toggleRows = [
|
||||
{ key: 'airplaneMode' as const, icon: Plane, color: '#ff9500' },
|
||||
{ key: 'streamerMode' as const, icon: EyeOff, color: '#af52de' },
|
||||
]
|
||||
const filteredGroups = computed(() =>
|
||||
groups
|
||||
.map((group) =>
|
||||
group.filter((row) =>
|
||||
phone
|
||||
.t(`Apps.settings.${row.key}`)
|
||||
.toLowerCase()
|
||||
.includes(query.value.toLowerCase()),
|
||||
),
|
||||
)
|
||||
.filter((group) => group.length),
|
||||
const serviceRows = [
|
||||
{
|
||||
key: 'notifications',
|
||||
view: 'notifications' as const,
|
||||
icon: BellRing,
|
||||
color: '#ff453a',
|
||||
},
|
||||
{
|
||||
key: 'sounds',
|
||||
view: 'sounds' as const,
|
||||
icon: Volume2,
|
||||
color: '#ff2d55',
|
||||
},
|
||||
]
|
||||
const preferenceRows = [
|
||||
{
|
||||
key: 'general',
|
||||
view: 'general' as const,
|
||||
icon: Settings,
|
||||
color: '#8e8e93',
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
view: 'appearance' as const,
|
||||
icon: Sun,
|
||||
color: '#0a84ff',
|
||||
},
|
||||
{
|
||||
key: 'wallpaper',
|
||||
view: 'wallpaper' as const,
|
||||
icon: Monitor,
|
||||
color: '#64d2ff',
|
||||
},
|
||||
]
|
||||
|
||||
const normalizedQuery = computed(() => query.value.trim().toLowerCase())
|
||||
const visibleToggleRows = computed(() =>
|
||||
toggleRows.filter((row) => matchesSearch(row.key)),
|
||||
)
|
||||
const visibleServiceRows = computed(() =>
|
||||
serviceRows.filter((row) => matchesSearch(row.key)),
|
||||
)
|
||||
const visiblePreferenceRows = computed(() =>
|
||||
preferenceRows.filter((row) => matchesSearch(row.key)),
|
||||
)
|
||||
const notificationApps = computed(() =>
|
||||
[...PHONE_APPS].sort((left, right) => left.gridOrder - right.gridOrder),
|
||||
)
|
||||
const selectedNotificationApp = computed(
|
||||
() =>
|
||||
PHONE_APPS.find((app) => app.id === selectedNotificationAppId.value) ??
|
||||
PHONE_APPS[0],
|
||||
)
|
||||
const activeTitle = computed(() => {
|
||||
if (activeView.value === 'notification-detail') {
|
||||
return selectedNotificationApp.value
|
||||
? phone.t(selectedNotificationApp.value.labelKey)
|
||||
: phone.t('Apps.settings.notifications')
|
||||
}
|
||||
return phone.t(`Apps.settings.${activeView.value}`)
|
||||
})
|
||||
|
||||
function matchesSearch(key: string): boolean {
|
||||
return (
|
||||
!normalizedQuery.value ||
|
||||
phone
|
||||
.t(`Apps.settings.${key}`)
|
||||
.toLowerCase()
|
||||
.includes(normalizedQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
function openView(view: SubmenuView): void {
|
||||
activeView.value = view
|
||||
}
|
||||
|
||||
function openNotificationApp(app: PhoneAppDefinition): void {
|
||||
selectedNotificationAppId.value = app.id
|
||||
activeView.value = 'notification-detail'
|
||||
}
|
||||
|
||||
function goBack(): void {
|
||||
activeView.value =
|
||||
activeView.value === 'notification-detail' ? 'notifications' : 'root'
|
||||
}
|
||||
|
||||
function toggleRootSetting(key: RootToggleKey): void {
|
||||
phone.setPreference(key, !phone.preferences.settings[key])
|
||||
}
|
||||
|
||||
function updateVolume(
|
||||
key: 'notificationVolume' | 'phoneScale' | 'ringtoneVolume',
|
||||
event: Event,
|
||||
): void {
|
||||
phone.setPreference(
|
||||
key,
|
||||
Number.parseInt((event.target as HTMLInputElement).value, 10),
|
||||
)
|
||||
}
|
||||
|
||||
function selectAppearanceMode(mode: AppearanceMode): void {
|
||||
phone.setPreference('appearanceMode', mode)
|
||||
}
|
||||
|
||||
function selectFrame(frame: PhoneFrameId): void {
|
||||
phone.setPreference('frame', frame)
|
||||
}
|
||||
|
||||
function selectRingtone(ringtone: RingtoneId): void {
|
||||
phone.setPreference('ringtone', ringtone)
|
||||
}
|
||||
|
||||
function selectNotificationSound(sound: NotificationSoundId): void {
|
||||
phone.setPreference('notificationSound', sound)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="native-app reference-settings">
|
||||
<section class="settings-scroll">
|
||||
<k-page class="native-app konsta-settings">
|
||||
<section v-if="activeView === 'root'" class="settings-scroll">
|
||||
<h1>{{ phone.t('Apps.settings.name') }}</h1>
|
||||
<div class="settings-search">
|
||||
<Search :size="17" /><input
|
||||
<Search :size="17" />
|
||||
<input
|
||||
v-model="query"
|
||||
:placeholder="phone.t('Apps.settings.searchPlaceholder')"
|
||||
type="search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<article class="settings-account">
|
||||
<span><UserRound :size="34" /></span>
|
||||
<div>
|
||||
<strong>{{ phone.t('Apps.settings.accountName') }}</strong
|
||||
><small>{{ phone.t('Apps.settings.accountDetail') }}</small>
|
||||
<strong>{{ phone.t('Apps.settings.accountName') }}</strong>
|
||||
<small>{{ phone.t('Apps.settings.accountDetail') }}</small>
|
||||
</div>
|
||||
<ChevronRight :size="19" />
|
||||
</article>
|
||||
<section
|
||||
v-for="(group, groupIndex) in filteredGroups"
|
||||
:key="groupIndex"
|
||||
class="reference-settings-group"
|
||||
|
||||
<k-list
|
||||
v-if="visibleToggleRows.length"
|
||||
strong
|
||||
inset
|
||||
class="settings-konsta-list"
|
||||
>
|
||||
<article v-for="row in group" :key="row.key">
|
||||
<span class="settings-symbol" :style="{ background: row.color }"
|
||||
><component :is="row.icon" :size="16"
|
||||
/></span>
|
||||
<div>
|
||||
<strong>{{ phone.t(`Apps.settings.${row.key}`) }}</strong
|
||||
><small v-if="row.value">{{ row.value }}</small
|
||||
><small v-else-if="row.valueKey">{{ phone.t(row.valueKey) }}</small>
|
||||
</div>
|
||||
<button
|
||||
v-if="row.toggle"
|
||||
class="ios-switch"
|
||||
:class="{
|
||||
active:
|
||||
phone.preferences.settings[
|
||||
row.key as 'airplaneMode' | 'wifi' | 'bluetooth'
|
||||
],
|
||||
}"
|
||||
type="button"
|
||||
:aria-label="phone.t(`Apps.settings.toggle.${row.key}`)"
|
||||
@click="
|
||||
phone.setSetting(
|
||||
row.key as 'airplaneMode' | 'wifi' | 'bluetooth',
|
||||
!phone.preferences.settings[
|
||||
row.key as 'airplaneMode' | 'wifi' | 'bluetooth'
|
||||
],
|
||||
)
|
||||
"
|
||||
>
|
||||
<span /></button
|
||||
><ChevronRight v-else :size="18" class="settings-chevron" />
|
||||
</article>
|
||||
</section>
|
||||
<h2 class="settings-section-title">
|
||||
{{ phone.t('Apps.settings.wallpaperPicker') }}
|
||||
</h2>
|
||||
<section class="wallpaper-picker">
|
||||
<button
|
||||
v-for="wallpaper in WALLPAPER_IDS"
|
||||
:key="wallpaper"
|
||||
type="button"
|
||||
:class="[
|
||||
`wallpaper--${wallpaper}`,
|
||||
{ active: phone.preferences.settings.wallpaper === wallpaper },
|
||||
]"
|
||||
:aria-label="phone.t(`Apps.settings.wallpapers.${wallpaper}`)"
|
||||
@click="phone.setWallpaper(wallpaper)"
|
||||
/>
|
||||
</section>
|
||||
<k-list-item
|
||||
v-for="row in visibleToggleRows"
|
||||
:key="row.key"
|
||||
:title="phone.t(`Apps.settings.${row.key}`)"
|
||||
>
|
||||
<template #media>
|
||||
<span class="settings-symbol" :style="{ background: row.color }">
|
||||
<component :is="row.icon" :size="16" />
|
||||
</span>
|
||||
</template>
|
||||
<template #after>
|
||||
<k-toggle
|
||||
component="div"
|
||||
:checked="phone.preferences.settings[row.key]"
|
||||
:aria-label="phone.t(`Apps.settings.toggle.${row.key}`)"
|
||||
@change="toggleRootSetting(row.key)"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-list
|
||||
v-if="visibleServiceRows.length"
|
||||
strong
|
||||
inset
|
||||
class="settings-konsta-list"
|
||||
>
|
||||
<k-list-item
|
||||
v-for="row in visibleServiceRows"
|
||||
:key="row.key"
|
||||
link
|
||||
link-component="button"
|
||||
:title="phone.t(`Apps.settings.${row.key}`)"
|
||||
@click="openView(row.view)"
|
||||
>
|
||||
<template #media>
|
||||
<span class="settings-symbol" :style="{ background: row.color }">
|
||||
<component :is="row.icon" :size="16" />
|
||||
</span>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-list
|
||||
v-if="visiblePreferenceRows.length"
|
||||
strong
|
||||
inset
|
||||
class="settings-konsta-list"
|
||||
>
|
||||
<k-list-item
|
||||
v-for="row in visiblePreferenceRows"
|
||||
:key="row.key"
|
||||
link
|
||||
link-component="button"
|
||||
:title="phone.t(`Apps.settings.${row.key}`)"
|
||||
@click="openView(row.view)"
|
||||
>
|
||||
<template #media>
|
||||
<span class="settings-symbol" :style="{ background: row.color }">
|
||||
<component :is="row.icon" :size="16" />
|
||||
</span>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<template v-else>
|
||||
<k-navbar :title="activeTitle" class="settings-konsta-navbar">
|
||||
<template #left>
|
||||
<k-navbar-back-link
|
||||
component="button"
|
||||
:show-text="false"
|
||||
:text="phone.t('Apps.settings.back')"
|
||||
:aria-label="phone.t('Apps.settings.back')"
|
||||
@click="goBack"
|
||||
/>
|
||||
</template>
|
||||
</k-navbar>
|
||||
|
||||
<section class="settings-subpage-scroll">
|
||||
<template v-if="activeView === 'notifications'">
|
||||
<k-list strong inset class="settings-konsta-list">
|
||||
<k-list-item
|
||||
v-for="app in notificationApps"
|
||||
:key="app.id"
|
||||
link
|
||||
link-component="button"
|
||||
:title="phone.t(app.labelKey)"
|
||||
:after="
|
||||
phone.t(
|
||||
phone.preferences.settings.notifications[app.id].enabled
|
||||
? 'Apps.settings.on'
|
||||
: 'Apps.settings.off',
|
||||
)
|
||||
"
|
||||
@click="openNotificationApp(app)"
|
||||
>
|
||||
<template #media>
|
||||
<img
|
||||
class="settings-app-icon"
|
||||
:src="app.iconImage"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-else-if="
|
||||
activeView === 'notification-detail' && selectedNotificationApp
|
||||
"
|
||||
>
|
||||
<div class="settings-notification-app">
|
||||
<img
|
||||
:src="selectedNotificationApp.iconImage"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<strong>{{ phone.t(selectedNotificationApp.labelKey) }}</strong>
|
||||
</div>
|
||||
<k-list strong inset class="settings-konsta-list">
|
||||
<k-list-item :title="phone.t('Apps.settings.allowNotifications')">
|
||||
<template #after>
|
||||
<k-toggle
|
||||
component="div"
|
||||
:checked="
|
||||
phone.preferences.settings.notifications[
|
||||
selectedNotificationApp.id
|
||||
].enabled
|
||||
"
|
||||
:aria-label="
|
||||
phone.t('Apps.settings.toggle.notifications', {
|
||||
app: phone.t(selectedNotificationApp.labelKey),
|
||||
})
|
||||
"
|
||||
@change="
|
||||
phone.setAppNotification(
|
||||
selectedNotificationApp.id,
|
||||
'enabled',
|
||||
!phone.preferences.settings.notifications[
|
||||
selectedNotificationApp.id
|
||||
].enabled,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
<k-list-item :title="phone.t('Apps.settings.notificationSounds')">
|
||||
<template #after>
|
||||
<k-toggle
|
||||
component="div"
|
||||
:disabled="
|
||||
!phone.preferences.settings.notifications[
|
||||
selectedNotificationApp.id
|
||||
].enabled
|
||||
"
|
||||
:checked="
|
||||
phone.preferences.settings.notifications[
|
||||
selectedNotificationApp.id
|
||||
].sounds
|
||||
"
|
||||
:aria-label="
|
||||
phone.t('Apps.settings.toggle.notificationSounds', {
|
||||
app: phone.t(selectedNotificationApp.labelKey),
|
||||
})
|
||||
"
|
||||
@change="
|
||||
phone.setAppNotification(
|
||||
selectedNotificationApp.id,
|
||||
'sounds',
|
||||
!phone.preferences.settings.notifications[
|
||||
selectedNotificationApp.id
|
||||
].sounds,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeView === 'sounds'">
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.ringtoneVolume') }}
|
||||
</k-block-title>
|
||||
<k-list strong inset class="settings-konsta-list settings-range-list">
|
||||
<k-list-item>
|
||||
<template #inner>
|
||||
<div class="settings-range-row">
|
||||
<Volume1 :size="17" />
|
||||
<k-range
|
||||
:value="phone.preferences.settings.ringtoneVolume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="updateVolume('ringtoneVolume', $event)"
|
||||
/>
|
||||
<Volume2 :size="19" />
|
||||
</div>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.notificationVolume') }}
|
||||
</k-block-title>
|
||||
<k-list strong inset class="settings-konsta-list settings-range-list">
|
||||
<k-list-item>
|
||||
<template #inner>
|
||||
<div class="settings-range-row">
|
||||
<Volume1 :size="17" />
|
||||
<k-range
|
||||
:value="phone.preferences.settings.notificationVolume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="updateVolume('notificationVolume', $event)"
|
||||
/>
|
||||
<Volume2 :size="19" />
|
||||
</div>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-block-title>{{ phone.t('Apps.settings.ringtone') }}</k-block-title>
|
||||
<k-list strong inset class="settings-konsta-list">
|
||||
<k-list-item
|
||||
v-for="ringtone in RINGTONE_IDS"
|
||||
:key="ringtone"
|
||||
link
|
||||
link-component="button"
|
||||
:chevron="false"
|
||||
:title="phone.t(`Apps.settings.ringtones.${ringtone}`)"
|
||||
@click="selectRingtone(ringtone)"
|
||||
>
|
||||
<template #after>
|
||||
<Check
|
||||
v-if="phone.preferences.settings.ringtone === ringtone"
|
||||
:size="19"
|
||||
class="settings-check"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.notificationSound') }}
|
||||
</k-block-title>
|
||||
<k-list strong inset class="settings-konsta-list">
|
||||
<k-list-item
|
||||
v-for="sound in NOTIFICATION_SOUND_IDS"
|
||||
:key="sound"
|
||||
link
|
||||
link-component="button"
|
||||
:chevron="false"
|
||||
:title="phone.t(`Apps.settings.notificationSoundsList.${sound}`)"
|
||||
@click="selectNotificationSound(sound)"
|
||||
>
|
||||
<template #after>
|
||||
<Check
|
||||
v-if="phone.preferences.settings.notificationSound === sound"
|
||||
:size="19"
|
||||
class="settings-check"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeView === 'general'">
|
||||
<k-block-title>{{ phone.t('Apps.settings.about') }}</k-block-title>
|
||||
<k-list strong inset class="settings-konsta-list">
|
||||
<k-list-item
|
||||
:title="phone.t('Apps.settings.deviceName')"
|
||||
:after="phone.t('Apps.settings.deviceNameValue')"
|
||||
/>
|
||||
<k-list-item
|
||||
:title="phone.t('Apps.settings.softwareVersion')"
|
||||
after="0.1.0"
|
||||
/>
|
||||
<k-list-item
|
||||
:title="phone.t('Apps.settings.language')"
|
||||
:after="phone.t('Apps.settings.languageValue')"
|
||||
/>
|
||||
<k-list-item
|
||||
:title="phone.t('Apps.settings.localStorage')"
|
||||
:after="phone.t('Apps.settings.localStorageValue')"
|
||||
/>
|
||||
</k-list>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeView === 'appearance'">
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.appearanceMode') }}
|
||||
</k-block-title>
|
||||
<k-list strong inset class="settings-konsta-list">
|
||||
<k-list-item
|
||||
v-for="mode in APPEARANCE_MODE_IDS"
|
||||
:key="mode"
|
||||
link
|
||||
link-component="button"
|
||||
:chevron="false"
|
||||
:title="phone.t(`Apps.settings.${mode}`)"
|
||||
@click="selectAppearanceMode(mode)"
|
||||
>
|
||||
<template #after>
|
||||
<Check
|
||||
v-if="phone.preferences.settings.appearanceMode === mode"
|
||||
:size="19"
|
||||
class="settings-check"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.phoneScale') }} ·
|
||||
{{ phone.preferences.settings.phoneScale }}%
|
||||
</k-block-title>
|
||||
<k-list strong inset class="settings-konsta-list settings-range-list">
|
||||
<k-list-item>
|
||||
<template #inner>
|
||||
<div class="settings-scale-row">
|
||||
<span>A</span>
|
||||
<k-range
|
||||
:value="phone.preferences.settings.phoneScale"
|
||||
:min="85"
|
||||
:max="115"
|
||||
:step="5"
|
||||
@input="updateVolume('phoneScale', $event)"
|
||||
/>
|
||||
<strong>A</strong>
|
||||
</div>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.phoneFrame') }}
|
||||
</k-block-title>
|
||||
<div class="settings-frame-picker">
|
||||
<button
|
||||
v-for="frame in PHONE_FRAME_IDS"
|
||||
:key="frame"
|
||||
type="button"
|
||||
:class="{
|
||||
active: phone.preferences.settings.frame === frame,
|
||||
}"
|
||||
:aria-label="phone.t(`Apps.settings.frames.${frame}`)"
|
||||
@click="selectFrame(frame)"
|
||||
>
|
||||
<img :src="PHONE_FRAME_IMAGES[frame]" alt="" draggable="false" />
|
||||
<span>{{ phone.t(`Apps.settings.frames.${frame}`) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeView === 'wallpaper'">
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.wallpaperPicker') }}
|
||||
</k-block-title>
|
||||
<section class="wallpaper-picker settings-wallpaper-picker">
|
||||
<button
|
||||
v-for="wallpaper in WALLPAPER_IDS"
|
||||
:key="wallpaper"
|
||||
type="button"
|
||||
:class="[
|
||||
`wallpaper--${wallpaper}`,
|
||||
{
|
||||
active: phone.preferences.settings.wallpaper === wallpaper,
|
||||
},
|
||||
]"
|
||||
:aria-label="phone.t(`Apps.settings.wallpapers.${wallpaper}`)"
|
||||
@click="phone.setWallpaper(wallpaper)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
</k-page>
|
||||
</template>
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sky Phone</title>
|
||||
<script type="module" crossorigin src="./assets/sky-index-CWWwvo5a.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-RGdqVA4s.css">
|
||||
<script type="module" crossorigin src="./assets/sky-index-CRNziHJr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-DINlYwL-.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user