mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-02 19:28:56 +00:00
2636 lines
72 KiB
Vue
2636 lines
72 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
BellRing,
|
|
Bluetooth,
|
|
Camera,
|
|
Check,
|
|
EyeOff,
|
|
KeyRound,
|
|
Images,
|
|
Monitor,
|
|
PanelsTopLeft,
|
|
Moon,
|
|
Plane,
|
|
RotateCcw,
|
|
Settings,
|
|
ShieldCheck,
|
|
Signal,
|
|
Smartphone,
|
|
Sun,
|
|
Upload,
|
|
UserRound,
|
|
Volume1,
|
|
Volume2,
|
|
Wifi,
|
|
} from 'lucide-vue-next'
|
|
import {
|
|
computed,
|
|
nextTick,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
ref,
|
|
type ComponentPublicInstance,
|
|
} from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
import { PHONE_FRAME_COLORS } from '@/config/appearance'
|
|
import {
|
|
getPhoneAppLabel,
|
|
isLaunchablePhoneApp,
|
|
PHONE_APPS,
|
|
} from '@/config/apps'
|
|
import { usePhoneStore } from '@/stores/phone'
|
|
import { useMessageMediaStore } from '@/stores/messageMedia'
|
|
import PhonePasscode from '@/components/PhonePasscode.vue'
|
|
import { useAccountStore } from '@/stores/account'
|
|
import { useAppAuthStore } from '@/stores/app-auth'
|
|
import { useAppStoreStore } from '@/stores/app-store'
|
|
import type {
|
|
LaunchablePhoneAppDefinition,
|
|
LaunchablePhoneAppId,
|
|
} from '@/types/apps'
|
|
import type { MediaConfig, MediaImportSources } from '@/types/media'
|
|
import {
|
|
filterMailAddressInput,
|
|
MAIL_ADDRESS_INPUT_MAX_LENGTH,
|
|
} from '@/utils/mail'
|
|
import { nuiCall } from '@/utils/nui'
|
|
import { formatPhoneNumber } from '@/utils/phone'
|
|
import {
|
|
SkyAppPage,
|
|
SkyBlock,
|
|
SkyButton,
|
|
SkyDialog,
|
|
SkyDialogButton,
|
|
SkyField,
|
|
SkyLink,
|
|
SkyNavbar,
|
|
SkyScrollArea,
|
|
SkySearchbar,
|
|
SkySegmented,
|
|
SkySegmentedButton,
|
|
SkySettingsGroup,
|
|
SkySettingsIcon,
|
|
SkySettingsRangeRow,
|
|
SkySettingsRow,
|
|
SkySpinner,
|
|
SkyNotification,
|
|
} from '@/ui'
|
|
import {
|
|
APPEARANCE_MODE_IDS,
|
|
GRAPHICS_MODE_IDS,
|
|
NOTIFICATION_SOUND_IDS,
|
|
PHONE_FRAME_IDS,
|
|
PHONE_SCALE_MAX,
|
|
PHONE_SCALE_MIN,
|
|
PHONE_SCALE_STEP,
|
|
RINGTONE_IDS,
|
|
WALLPAPER_IDS,
|
|
type AppearanceMode,
|
|
type GraphicsMode,
|
|
type NotificationSoundId,
|
|
type PhoneFrameId,
|
|
type RingtoneId,
|
|
type WallpaperHistoryEntry,
|
|
type WallpaperId,
|
|
type WallpaperTarget,
|
|
} from '@/utils/preferences'
|
|
|
|
type SettingsView =
|
|
| 'root'
|
|
| 'account'
|
|
| 'security'
|
|
| 'notifications'
|
|
| 'notification-detail'
|
|
| 'sounds'
|
|
| 'connectivity'
|
|
| 'focus'
|
|
| 'general'
|
|
| 'appearance'
|
|
| 'wallpaper'
|
|
| 'reset'
|
|
type RootToggleKey =
|
|
| 'airplaneMode'
|
|
| 'streamerMode'
|
|
| 'focusMode'
|
|
| 'wifiEnabled'
|
|
| 'bluetoothEnabled'
|
|
| 'cellularEnabled'
|
|
type SubmenuView = Exclude<SettingsView, 'root' | 'notification-detail'>
|
|
type PasscodeFlow =
|
|
| 'set-new'
|
|
| 'set-confirm'
|
|
| 'change-current'
|
|
| 'change-new'
|
|
| 'change-confirm'
|
|
| 'disable'
|
|
| null
|
|
|
|
const FACTORY_RESET_DURATION_MS = 8_000
|
|
|
|
const phone = usePhoneStore()
|
|
const mediaPicker = useMessageMediaStore()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const isDevelopment = import.meta.env.DEV
|
|
const account = useAccountStore()
|
|
const appAuth = useAppAuthStore()
|
|
const appStore = useAppStoreStore()
|
|
const query = ref('')
|
|
const activeView = ref<SettingsView>('root')
|
|
const selectedNotificationAppId = ref<LaunchablePhoneAppId>('calculator')
|
|
const settingsPage = ref<ComponentPublicInstance | null>(null)
|
|
const framePickerOpened = ref(false)
|
|
const accountMode = ref<'login' | 'register'>('login')
|
|
const accountEmail = ref('')
|
|
const accountPassword = ref('')
|
|
const accountConfirm = ref('')
|
|
const accountSubmitting = ref(false)
|
|
const accountToast = ref('')
|
|
const passcodeBusy = ref(false)
|
|
const passcodeCurrent = ref('')
|
|
const passcodeError = ref('')
|
|
const passcodeFirst = ref('')
|
|
const passcodeFlow = ref<PasscodeFlow>(null)
|
|
const passcodeLength = ref<4 | 6>(6)
|
|
const passcodeResetKey = ref(0)
|
|
const removeDeviceImei = ref('')
|
|
const removeDevicePassword = ref('')
|
|
const removeDeviceOpened = ref(false)
|
|
const resetOpened = ref(false)
|
|
const simEjectOpened = ref(false)
|
|
const factoryResetting = ref(false)
|
|
const factoryResetProgress = ref(0)
|
|
const wallpaperTarget = ref<WallpaperTarget>('home')
|
|
const customWallpaperUploadAvailable = ref(false)
|
|
let wallpaperMediaConfigLoaded = false
|
|
const selectedFrameColor = computed(
|
|
() => PHONE_FRAME_COLORS[phone.preferences.settings.frame],
|
|
)
|
|
const factoryResetPhase = computed(() => {
|
|
if (factoryResetProgress.value < 18) {
|
|
return {
|
|
detail: phone.t('Apps.settings.factoryResetPreparingDetail'),
|
|
index: 0,
|
|
label: phone.t('Apps.settings.factoryResetPreparing'),
|
|
}
|
|
}
|
|
if (factoryResetProgress.value < 72) {
|
|
return {
|
|
detail: phone.t('Apps.settings.factoryResetRemovingDetail'),
|
|
index: 1,
|
|
label: phone.t('Apps.settings.factoryResetRemoving'),
|
|
}
|
|
}
|
|
if (factoryResetProgress.value < 94) {
|
|
return {
|
|
detail: phone.t('Apps.settings.factoryResetSecuringDetail'),
|
|
index: 2,
|
|
label: phone.t('Apps.settings.factoryResetSecuring'),
|
|
}
|
|
}
|
|
return {
|
|
detail: phone.t('Apps.settings.factoryResetFinishingDetail'),
|
|
index: 3,
|
|
label: phone.t('Apps.settings.factoryResetFinishing'),
|
|
}
|
|
})
|
|
const factoryResetSecondsRemaining = computed(() =>
|
|
Math.max(
|
|
0,
|
|
Math.ceil(
|
|
(FACTORY_RESET_DURATION_MS * (1 - factoryResetProgress.value / 100)) /
|
|
1000,
|
|
),
|
|
),
|
|
)
|
|
let factoryResetAnimationFrame: number | undefined
|
|
|
|
const wallpaperHistory = computed(
|
|
() => phone.preferences.settings.wallpaperHistory,
|
|
)
|
|
const selectedWallpaper = computed<WallpaperId>(() =>
|
|
wallpaperTarget.value === 'home'
|
|
? phone.preferences.settings.wallpaper
|
|
: phone.preferences.settings.lockWallpaper,
|
|
)
|
|
const selectedWallpaperImageUrl = computed(() =>
|
|
wallpaperTarget.value === 'home'
|
|
? phone.preferences.settings.wallpaperImageUrl
|
|
: phone.preferences.settings.lockWallpaperImageUrl,
|
|
)
|
|
|
|
const toggleRows = [
|
|
{
|
|
key: 'airplaneMode' as const,
|
|
icon: Plane,
|
|
iconColor: '#ff9500',
|
|
},
|
|
{
|
|
key: 'streamerMode' as const,
|
|
icon: EyeOff,
|
|
iconColor: '#af52de',
|
|
},
|
|
]
|
|
const serviceRows = [
|
|
{
|
|
key: 'notifications',
|
|
view: 'notifications' as const,
|
|
icon: BellRing,
|
|
iconColor: '#ff3b30',
|
|
},
|
|
{
|
|
key: 'sounds',
|
|
view: 'sounds' as const,
|
|
icon: Volume2,
|
|
iconColor: '#ff2d55',
|
|
},
|
|
]
|
|
const preferenceRows = [
|
|
{
|
|
key: 'connectivity',
|
|
view: 'connectivity' as const,
|
|
icon: Wifi,
|
|
iconColor: '#007aff',
|
|
},
|
|
{
|
|
key: 'focus',
|
|
view: 'focus' as const,
|
|
icon: Moon,
|
|
iconColor: '#5856d6',
|
|
},
|
|
{
|
|
key: 'security',
|
|
view: 'security' as const,
|
|
icon: KeyRound,
|
|
iconColor: '#34c759',
|
|
},
|
|
{
|
|
key: 'general',
|
|
view: 'general' as const,
|
|
icon: Settings,
|
|
iconColor: '#8e8e93',
|
|
},
|
|
{
|
|
key: 'appearance',
|
|
view: 'appearance' as const,
|
|
icon: Sun,
|
|
iconColor: '#007aff',
|
|
},
|
|
{
|
|
key: 'wallpaper',
|
|
view: 'wallpaper' as const,
|
|
icon: Monitor,
|
|
iconColor: '#32ade6',
|
|
},
|
|
]
|
|
|
|
function openSkyUiKitchenSink(): void {
|
|
if (!isDevelopment) return
|
|
void router.push({ name: 'development-sky-ui' })
|
|
}
|
|
|
|
function openWallpaperMedia(app: 'photos' | 'camera'): void {
|
|
const target = wallpaperTarget.value
|
|
mediaPicker.begin(
|
|
`settings:wallpaper:${target}`,
|
|
'photo',
|
|
`/apps/settings?wallpaper=1&wallpaperTarget=${target}`,
|
|
1,
|
|
)
|
|
void router.push({
|
|
path: `/apps/${app}`,
|
|
query: { mediaAttachment: 'photo' },
|
|
})
|
|
}
|
|
|
|
async function loadWallpaperMediaConfig(): Promise<void> {
|
|
if (wallpaperMediaConfigLoaded) return
|
|
wallpaperMediaConfigLoaded = true
|
|
|
|
const configResponse = await nuiCall<MediaConfig>('media:config')
|
|
if (
|
|
!configResponse.success ||
|
|
configResponse.data?.customWallpaperUploadEnabled !== true
|
|
) {
|
|
return
|
|
}
|
|
|
|
const sourcesResponse = await nuiCall<MediaImportSources>(
|
|
'media:import:sources',
|
|
)
|
|
customWallpaperUploadAvailable.value =
|
|
sourcesResponse.success &&
|
|
Boolean(
|
|
sourcesResponse.data?.sources.some((source) =>
|
|
source.mediaTypes.includes('photo'),
|
|
),
|
|
)
|
|
}
|
|
|
|
function openWallpaperCustomUpload(): void {
|
|
const target = wallpaperTarget.value
|
|
mediaPicker.begin(
|
|
`settings:wallpaper:${target}`,
|
|
'photo',
|
|
`/apps/settings?wallpaper=1&wallpaperTarget=${target}`,
|
|
1,
|
|
)
|
|
void router.push({
|
|
path: '/apps/photos',
|
|
query: { mediaAttachment: 'photo', wallpaperUpload: '1' },
|
|
})
|
|
}
|
|
|
|
function wallpaperPreviewStyle(
|
|
entry: WallpaperHistoryEntry,
|
|
): Record<string, string> | undefined {
|
|
if (entry.wallpaper !== 'custom' || !entry.imageUrl) return undefined
|
|
return { '--phone-wallpaper-image': `url(${JSON.stringify(entry.imageUrl)})` }
|
|
}
|
|
|
|
function selectWallpaperHistory(entry: WallpaperHistoryEntry): void {
|
|
phone.setWallpaper(entry.wallpaper, entry.imageUrl, wallpaperTarget.value)
|
|
}
|
|
|
|
function isWallpaperSelected(entry: WallpaperHistoryEntry): boolean {
|
|
return (
|
|
selectedWallpaper.value === entry.wallpaper &&
|
|
selectedWallpaperImageUrl.value === entry.imageUrl
|
|
)
|
|
}
|
|
|
|
function selectBuiltInWallpaper(wallpaper: WallpaperId): void {
|
|
phone.setWallpaper(wallpaper, null, wallpaperTarget.value)
|
|
}
|
|
|
|
const connectivityRows = [
|
|
{
|
|
key: 'wifi',
|
|
preferenceKey: 'wifiEnabled' as const,
|
|
icon: Wifi,
|
|
iconColor: '#007aff',
|
|
},
|
|
{
|
|
key: 'bluetooth',
|
|
preferenceKey: 'bluetoothEnabled' as const,
|
|
icon: Bluetooth,
|
|
iconColor: '#007aff',
|
|
},
|
|
{
|
|
key: 'cellular',
|
|
preferenceKey: 'cellularEnabled' as const,
|
|
icon: Signal,
|
|
iconColor: '#34c759',
|
|
},
|
|
]
|
|
|
|
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.filter(isLaunchablePhoneApp).sort(
|
|
(left, right) => left.gridOrder - right.gridOrder,
|
|
),
|
|
)
|
|
const selectedNotificationApp = computed(
|
|
() =>
|
|
notificationApps.value.find(
|
|
(app) => app.id === selectedNotificationAppId.value,
|
|
) ?? notificationApps.value[0],
|
|
)
|
|
const activeTitle = computed(() => {
|
|
if (activeView.value === 'account') {
|
|
return phone.t('Apps.settings.accountName')
|
|
}
|
|
if (activeView.value === 'notification-detail') {
|
|
return selectedNotificationApp.value
|
|
? getPhoneAppLabel(selectedNotificationApp.value, phone.t)
|
|
: phone.t('Apps.settings.notifications')
|
|
}
|
|
return phone.t(`Apps.settings.${activeView.value}`)
|
|
})
|
|
const passcodeTitle = computed(() => {
|
|
if (passcodeFlow.value === 'set-confirm') {
|
|
return phone.t('Apps.settings.passcode.confirmNew')
|
|
}
|
|
if (
|
|
passcodeFlow.value === 'change-current' ||
|
|
passcodeFlow.value === 'disable'
|
|
) {
|
|
return phone.t('Apps.settings.passcode.enterCurrent')
|
|
}
|
|
if (passcodeFlow.value === 'change-confirm') {
|
|
return phone.t('Apps.settings.passcode.confirmNew')
|
|
}
|
|
return phone.t('Apps.settings.passcode.enterNew')
|
|
})
|
|
|
|
function matchesSearch(key: string): boolean {
|
|
return (
|
|
!normalizedQuery.value ||
|
|
phone
|
|
.t(`Apps.settings.${key}`)
|
|
.toLowerCase()
|
|
.includes(normalizedQuery.value)
|
|
)
|
|
}
|
|
|
|
function openView(view: SubmenuView): void {
|
|
if (view === 'security') {
|
|
passcodeLength.value = phone.security.length ?? 6
|
|
}
|
|
if (view === 'wallpaper') void loadWallpaperMediaConfig()
|
|
activeView.value = view
|
|
scrollPageToTop()
|
|
}
|
|
|
|
function openNotificationApp(app: LaunchablePhoneAppDefinition): void {
|
|
selectedNotificationAppId.value = app.id
|
|
activeView.value = 'notification-detail'
|
|
scrollPageToTop()
|
|
}
|
|
|
|
function goBack(): void {
|
|
framePickerOpened.value = false
|
|
activeView.value =
|
|
activeView.value === 'notification-detail' ? 'notifications' : 'root'
|
|
scrollPageToTop()
|
|
}
|
|
|
|
function scrollPageToTop(): void {
|
|
void nextTick(() => {
|
|
const pageElement = settingsPage.value?.$el as HTMLElement | undefined
|
|
pageElement?.scrollTo({ top: 0 })
|
|
})
|
|
}
|
|
|
|
function setRootSetting(key: RootToggleKey, value: boolean): void {
|
|
phone.setPreference(key, value)
|
|
}
|
|
|
|
function resetPasscodeInput(): void {
|
|
passcodeError.value = ''
|
|
passcodeResetKey.value += 1
|
|
}
|
|
|
|
function beginSetPasscode(): void {
|
|
passcodeLength.value = phone.security.length ?? passcodeLength.value
|
|
passcodeFirst.value = ''
|
|
passcodeCurrent.value = ''
|
|
passcodeFlow.value = 'set-new'
|
|
resetPasscodeInput()
|
|
}
|
|
|
|
function beginChangePasscode(): void {
|
|
passcodeFirst.value = ''
|
|
passcodeCurrent.value = ''
|
|
passcodeFlow.value = 'change-current'
|
|
resetPasscodeInput()
|
|
}
|
|
|
|
function beginDisablePasscode(): void {
|
|
passcodeLength.value = phone.security.length ?? 6
|
|
passcodeCurrent.value = ''
|
|
passcodeFlow.value = 'disable'
|
|
resetPasscodeInput()
|
|
}
|
|
|
|
function cancelPasscodeFlow(): void {
|
|
if (passcodeBusy.value) return
|
|
passcodeFlow.value = null
|
|
passcodeFirst.value = ''
|
|
passcodeCurrent.value = ''
|
|
resetPasscodeInput()
|
|
}
|
|
|
|
function passcodeRequestError(error?: string): string {
|
|
if (error === 'invalid_passcode') {
|
|
return phone.t('Apps.settings.passcode.incorrect')
|
|
}
|
|
if (error === 'passcode_locked') {
|
|
return phone.t('Apps.settings.passcode.locked')
|
|
}
|
|
if (error === 'rate_limited') {
|
|
return phone.t('Apps.settings.passcode.rateLimited')
|
|
}
|
|
return phone.t('Apps.settings.passcode.failed')
|
|
}
|
|
|
|
async function submitSettingsPasscode(passcode: string): Promise<void> {
|
|
if (passcodeBusy.value || !passcodeFlow.value) return
|
|
|
|
if (passcodeFlow.value === 'set-new') {
|
|
passcodeFirst.value = passcode
|
|
passcodeFlow.value = 'set-confirm'
|
|
resetPasscodeInput()
|
|
return
|
|
}
|
|
if (passcodeFlow.value === 'change-current') {
|
|
passcodeCurrent.value = passcode
|
|
passcodeFlow.value = 'change-new'
|
|
resetPasscodeInput()
|
|
return
|
|
}
|
|
if (passcodeFlow.value === 'change-new') {
|
|
passcodeFirst.value = passcode
|
|
passcodeFlow.value = 'change-confirm'
|
|
resetPasscodeInput()
|
|
return
|
|
}
|
|
if (
|
|
(passcodeFlow.value === 'set-confirm' ||
|
|
passcodeFlow.value === 'change-confirm') &&
|
|
passcode !== passcodeFirst.value
|
|
) {
|
|
passcodeError.value = phone.t('Apps.settings.passcode.mismatch')
|
|
passcodeResetKey.value += 1
|
|
return
|
|
}
|
|
|
|
passcodeBusy.value = true
|
|
const response =
|
|
passcodeFlow.value === 'set-confirm'
|
|
? await phone.setPasscode(passcode)
|
|
: passcodeFlow.value === 'change-confirm'
|
|
? await phone.changePasscode(passcodeCurrent.value, passcode)
|
|
: await phone.disablePasscode(passcode)
|
|
passcodeBusy.value = false
|
|
if (!response.success) {
|
|
passcodeError.value = passcodeRequestError(response.error)
|
|
if (
|
|
passcodeFlow.value === 'change-confirm' &&
|
|
response.error === 'invalid_passcode'
|
|
) {
|
|
passcodeFlow.value = 'change-current'
|
|
passcodeCurrent.value = ''
|
|
passcodeFirst.value = ''
|
|
}
|
|
passcodeResetKey.value += 1
|
|
return
|
|
}
|
|
|
|
accountToast.value = phone.t(
|
|
passcodeFlow.value === 'disable'
|
|
? 'Apps.settings.passcode.disabled'
|
|
: 'Apps.settings.passcode.saved',
|
|
)
|
|
cancelPasscodeFlow()
|
|
}
|
|
|
|
function updateNumberPreference(
|
|
key:
|
|
| 'notificationDurationSeconds'
|
|
| 'notificationVolume'
|
|
| 'phoneScale'
|
|
| 'ringtoneVolume'
|
|
| 'screenBrightness',
|
|
value: number,
|
|
): void {
|
|
phone.setPreference(key, value)
|
|
}
|
|
|
|
function selectAppearanceMode(mode: AppearanceMode): void {
|
|
phone.setPreference('appearanceMode', mode)
|
|
}
|
|
|
|
function selectGraphicsMode(mode: GraphicsMode): void {
|
|
phone.setPreference('graphicsMode', mode)
|
|
}
|
|
|
|
function selectFrame(frame: PhoneFrameId): void {
|
|
phone.setPreference('frame', frame)
|
|
framePickerOpened.value = false
|
|
}
|
|
|
|
function selectRingtone(ringtone: RingtoneId): void {
|
|
phone.setPreference('ringtone', ringtone)
|
|
}
|
|
|
|
function selectNotificationSound(sound: NotificationSoundId): void {
|
|
phone.setPreference('notificationSound', sound)
|
|
}
|
|
|
|
function updateAccountEmail(event: Event): void {
|
|
const input = event.target as HTMLInputElement
|
|
const original = input.value
|
|
const selectionStart = input.selectionStart ?? original.length
|
|
const filtered = filterMailAddressInput(original)
|
|
|
|
if (filtered !== original) {
|
|
const nextSelection = filterMailAddressInput(
|
|
original.slice(0, selectionStart),
|
|
).length
|
|
input.value = filtered
|
|
input.setSelectionRange(nextSelection, nextSelection)
|
|
}
|
|
|
|
accountEmail.value = filtered
|
|
}
|
|
|
|
function accountError(error?: string): string {
|
|
const known = [
|
|
'invalid_email',
|
|
'invalid_password',
|
|
'invalid_credentials',
|
|
'email_taken',
|
|
'rate_limited',
|
|
'current_device',
|
|
'device_not_found',
|
|
]
|
|
return phone.t(
|
|
`Apps.settings.accountErrors.${error && known.includes(error) ? error : 'default'}`,
|
|
)
|
|
}
|
|
|
|
async function submitAccount(): Promise<void> {
|
|
if (
|
|
accountMode.value === 'register' &&
|
|
accountPassword.value !== accountConfirm.value
|
|
) {
|
|
accountToast.value = phone.t('Apps.mail.passwordsMismatch')
|
|
return
|
|
}
|
|
accountSubmitting.value = true
|
|
const response =
|
|
accountMode.value === 'login'
|
|
? await account.login(accountEmail.value, accountPassword.value)
|
|
: await account.register(accountEmail.value, accountPassword.value)
|
|
accountSubmitting.value = false
|
|
if (!response.success) accountToast.value = accountError(response.error)
|
|
else {
|
|
accountPassword.value = ''
|
|
accountConfirm.value = ''
|
|
}
|
|
}
|
|
|
|
async function logoutAccount(): Promise<void> {
|
|
if (!(await account.logout())) accountToast.value = accountError()
|
|
else appAuth.clear()
|
|
}
|
|
|
|
function requestRemoveDevice(imei: string): void {
|
|
removeDeviceImei.value = imei
|
|
removeDevicePassword.value = ''
|
|
removeDeviceOpened.value = true
|
|
}
|
|
|
|
async function confirmRemoveDevice(): Promise<void> {
|
|
const response = await account.removeDevice(
|
|
removeDeviceImei.value,
|
|
removeDevicePassword.value,
|
|
)
|
|
if (!response.success) accountToast.value = accountError(response.error)
|
|
else removeDeviceOpened.value = false
|
|
}
|
|
|
|
async function confirmFactoryReset(): Promise<void> {
|
|
resetOpened.value = false
|
|
factoryResetting.value = true
|
|
factoryResetProgress.value = 0
|
|
const startedAt = performance.now()
|
|
|
|
const animateProgress = (now: number): void => {
|
|
factoryResetProgress.value = Math.min(
|
|
100,
|
|
((now - startedAt) / FACTORY_RESET_DURATION_MS) * 100,
|
|
)
|
|
if (factoryResetProgress.value < 100) {
|
|
factoryResetAnimationFrame = requestAnimationFrame(animateProgress)
|
|
}
|
|
}
|
|
factoryResetAnimationFrame = requestAnimationFrame(animateProgress)
|
|
|
|
const [success] = await Promise.all([
|
|
account.factoryReset(),
|
|
new Promise<void>((resolve) =>
|
|
window.setTimeout(resolve, FACTORY_RESET_DURATION_MS),
|
|
),
|
|
])
|
|
|
|
if (factoryResetAnimationFrame !== undefined) {
|
|
cancelAnimationFrame(factoryResetAnimationFrame)
|
|
factoryResetAnimationFrame = undefined
|
|
}
|
|
factoryResetProgress.value = 100
|
|
factoryResetting.value = false
|
|
if (!success) accountToast.value = accountError()
|
|
else {
|
|
appAuth.hydrate(undefined, '')
|
|
appStore.hydrate(undefined)
|
|
phone.resetAfterFactoryReset()
|
|
}
|
|
}
|
|
|
|
async function confirmSimEject(): Promise<void> {
|
|
const response = await nuiCall('sim:eject')
|
|
simEjectOpened.value = false
|
|
if (!response.success) {
|
|
accountToast.value = phone.t(
|
|
`Apps.phone.errors.${response.error ?? 'default'}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (
|
|
import.meta.env.DEV &&
|
|
new URLSearchParams(window.location.search).has('factoryResetPreview')
|
|
) {
|
|
factoryResetting.value = true
|
|
factoryResetProgress.value = 46
|
|
}
|
|
|
|
if (route.query.wallpaper === '1') {
|
|
activeView.value = 'wallpaper'
|
|
wallpaperTarget.value =
|
|
route.query.wallpaperTarget === 'lock' ? 'lock' : 'home'
|
|
void loadWallpaperMediaConfig()
|
|
}
|
|
|
|
const selectedPhoto = mediaPicker.consume(
|
|
`settings:wallpaper:${wallpaperTarget.value}`,
|
|
)
|
|
if (selectedPhoto) {
|
|
phone.setWallpaper('custom', selectedPhoto.url, wallpaperTarget.value)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (factoryResetAnimationFrame !== undefined) {
|
|
cancelAnimationFrame(factoryResetAnimationFrame)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<SkyAppPage
|
|
class="settings-app"
|
|
accent="#0a84ff"
|
|
accent-soft="rgba(10, 132, 255, 0.16)"
|
|
:dark="phone.isDarkMode"
|
|
:label="phone.t('Apps.settings.name')"
|
|
>
|
|
<SkyNavbar
|
|
class="settings-navbar"
|
|
:title="
|
|
activeView === 'root' ? phone.t('Apps.settings.name') : activeTitle
|
|
"
|
|
:variant="activeView === 'root' ? 'large' : 'compact'"
|
|
:show-back="activeView !== 'root'"
|
|
back-appearance="surface"
|
|
:back-label="phone.t('Apps.settings.back')"
|
|
@back="goBack"
|
|
>
|
|
<template v-if="activeView === 'account' && !account.email" #right>
|
|
<SkyLink
|
|
@click="accountMode = accountMode === 'login' ? 'register' : 'login'"
|
|
>
|
|
{{
|
|
phone.t(
|
|
accountMode === 'login'
|
|
? 'Apps.mail.registerLink'
|
|
: 'Apps.mail.login',
|
|
)
|
|
}}
|
|
</SkyLink>
|
|
</template>
|
|
</SkyNavbar>
|
|
|
|
<SkyScrollArea
|
|
ref="settingsPage"
|
|
padded
|
|
class="settings-content"
|
|
:class="{ 'settings-content--subpage': activeView !== 'root' }"
|
|
>
|
|
<template v-if="activeView === 'root'">
|
|
<div class="settings-search" role="search">
|
|
<SkySearchbar
|
|
v-model="query"
|
|
:clear-label="phone.t('Common.clear')"
|
|
:label="phone.t('Apps.settings.searchPlaceholder')"
|
|
:placeholder="phone.t('Apps.settings.searchPlaceholder')"
|
|
/>
|
|
</div>
|
|
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.accountName')">
|
|
<SkySettingsRow
|
|
kind="navigation"
|
|
:title="phone.t('Apps.settings.accountName')"
|
|
:description="phone.t('Apps.settings.accountDetail')"
|
|
@activate="openView('account')"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon color="#8e8e93">
|
|
<UserRound aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup
|
|
v-if="visibleToggleRows.length"
|
|
:aria-label="phone.t('Apps.settings.name')"
|
|
>
|
|
<SkySettingsRow
|
|
v-for="row in visibleToggleRows"
|
|
:key="row.key"
|
|
kind="toggle"
|
|
:model-value="phone.preferences.settings[row.key]"
|
|
:title="phone.t('Apps.settings.' + row.key)"
|
|
@update:model-value="setRootSetting(row.key, $event)"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon :color="row.iconColor">
|
|
<component :is="row.icon" aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup
|
|
v-if="visibleServiceRows.length"
|
|
:aria-label="phone.t('Apps.settings.notifications')"
|
|
>
|
|
<SkySettingsRow
|
|
v-for="row in visibleServiceRows"
|
|
:key="row.key"
|
|
kind="navigation"
|
|
:title="phone.t('Apps.settings.' + row.key)"
|
|
@activate="openView(row.view)"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon :color="row.iconColor">
|
|
<component :is="row.icon" aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup
|
|
v-if="visiblePreferenceRows.length"
|
|
:aria-label="phone.t('Apps.settings.name')"
|
|
>
|
|
<SkySettingsRow
|
|
v-for="row in visiblePreferenceRows"
|
|
:key="row.key"
|
|
kind="navigation"
|
|
:title="phone.t('Apps.settings.' + row.key)"
|
|
:value="
|
|
row.key === 'security' || row.key === 'focus'
|
|
? phone.t(
|
|
(row.key === 'security' && phone.security.enabled) ||
|
|
(row.key === 'focus' &&
|
|
phone.preferences.settings.focusMode)
|
|
? 'Apps.settings.on'
|
|
: 'Apps.settings.off',
|
|
)
|
|
: undefined
|
|
"
|
|
@activate="openView(row.view)"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon :color="row.iconColor">
|
|
<component :is="row.icon" aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup
|
|
v-if="isDevelopment && matchesSearch('development')"
|
|
:title="phone.t('Apps.settings.development')"
|
|
>
|
|
<SkySettingsRow
|
|
kind="navigation"
|
|
:title="phone.t('Apps.settings.skyUiKitchenSink')"
|
|
:description="phone.t('Apps.settings.skyUiKitchenSinkDescription')"
|
|
@activate="openSkyUiKitchenSink"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon color="#5856d6">
|
|
<PanelsTopLeft aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'account'">
|
|
<template v-if="!account.email">
|
|
<SkyBlock class="settings-copy">
|
|
{{
|
|
phone.t(
|
|
accountMode === 'login'
|
|
? 'Apps.settings.accountLoginBody'
|
|
: 'Apps.mail.passwordWarning',
|
|
)
|
|
}}
|
|
</SkyBlock>
|
|
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.accountName')">
|
|
<SkyField
|
|
id="sky-cloud-email"
|
|
:model-value="accountEmail"
|
|
:label="
|
|
phone.t(
|
|
accountMode === 'login'
|
|
? 'Apps.mail.email'
|
|
: 'Apps.mail.localPart',
|
|
)
|
|
"
|
|
autocomplete="username"
|
|
autocapitalize="none"
|
|
autocorrect="off"
|
|
input-mode="email"
|
|
:maxlength="MAIL_ADDRESS_INPUT_MAX_LENGTH"
|
|
pattern="[A-Za-z0-9@._-]*"
|
|
:spellcheck="false"
|
|
:clear-button="accountMode === 'login'"
|
|
:clear-label="phone.t('Common.clear')"
|
|
@input="updateAccountEmail"
|
|
@clear="accountEmail = ''"
|
|
>
|
|
<template v-if="accountMode === 'register'" #trailing>
|
|
<span class="settings-account-suffix">@ifruit.com</span>
|
|
</template>
|
|
</SkyField>
|
|
<SkyField
|
|
id="sky-cloud-password"
|
|
v-model="accountPassword"
|
|
type="password"
|
|
:label="phone.t('Apps.mail.password')"
|
|
autocomplete="current-password"
|
|
/>
|
|
<SkyField
|
|
v-if="accountMode === 'register'"
|
|
id="sky-cloud-confirm-password"
|
|
v-model="accountConfirm"
|
|
type="password"
|
|
:label="phone.t('Apps.mail.confirmPassword')"
|
|
autocomplete="new-password"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<div class="settings-primary-action">
|
|
<SkyButton
|
|
block
|
|
large
|
|
:disabled="accountSubmitting"
|
|
@click="submitAccount"
|
|
>
|
|
<SkySpinner v-if="accountSubmitting" :size="18" />
|
|
<template v-else>
|
|
{{
|
|
phone.t(
|
|
accountMode === 'login'
|
|
? 'Apps.mail.login'
|
|
: 'Apps.mail.register',
|
|
)
|
|
}}
|
|
</template>
|
|
</SkyButton>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<SkySettingsGroup :aria-label="account.email">
|
|
<SkySettingsRow
|
|
:title="account.email"
|
|
:description="phone.t('Apps.settings.accountCloudDetail')"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon color="#0a84ff">
|
|
<UserRound aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.linkedDevices')">
|
|
<template v-for="device in account.devices" :key="device.imei">
|
|
<SkySettingsRow
|
|
:title="device.device_name"
|
|
:description="device.imei"
|
|
:value="
|
|
device.current
|
|
? phone.t('Apps.settings.thisDevice')
|
|
: undefined
|
|
"
|
|
>
|
|
<template #leading>
|
|
<Smartphone :size="20" aria-hidden="true" />
|
|
</template>
|
|
</SkySettingsRow>
|
|
<SkySettingsRow
|
|
v-if="!device.current"
|
|
kind="action"
|
|
tone="danger"
|
|
:title="phone.t('Apps.settings.removeDevice')"
|
|
@activate="requestRemoveDevice(device.imei)"
|
|
/>
|
|
</template>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.signOut')">
|
|
<SkySettingsRow
|
|
kind="action"
|
|
tone="danger"
|
|
:title="phone.t('Apps.settings.signOut')"
|
|
@activate="logoutAccount"
|
|
/>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'security'">
|
|
<SkyBlock class="settings-copy">
|
|
{{ phone.t('Apps.settings.passcode.description') }}
|
|
</SkyBlock>
|
|
|
|
<template v-if="!phone.security.enabled">
|
|
<SkySettingsGroup
|
|
:title="phone.t('Apps.settings.passcode.codeLength')"
|
|
>
|
|
<li class="settings-segmented-row">
|
|
<SkySegmented
|
|
:aria-label="phone.t('Apps.settings.passcode.codeLength')"
|
|
>
|
|
<SkySegmentedButton
|
|
:active="passcodeLength === 6"
|
|
@click="passcodeLength = 6"
|
|
>
|
|
{{ phone.t('Apps.settings.passcode.sixDigit') }}
|
|
</SkySegmentedButton>
|
|
<SkySegmentedButton
|
|
:active="passcodeLength === 4"
|
|
@click="passcodeLength = 4"
|
|
>
|
|
{{ phone.t('Apps.settings.passcode.fourDigit') }}
|
|
</SkySegmentedButton>
|
|
</SkySegmented>
|
|
</li>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup
|
|
:aria-label="phone.t('Apps.settings.passcode.turnOn')"
|
|
>
|
|
<SkySettingsRow
|
|
kind="action"
|
|
:title="phone.t('Apps.settings.passcode.turnOn')"
|
|
@activate="beginSetPasscode"
|
|
/>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<SkySettingsGroup
|
|
:aria-label="phone.t('Apps.settings.passcode.status')"
|
|
>
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.passcode.status')"
|
|
:value="phone.t('Apps.settings.on')"
|
|
/>
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.passcode.codeLength')"
|
|
:value="
|
|
phone.t(
|
|
phone.security.length === 4
|
|
? 'Apps.settings.passcode.fourDigit'
|
|
: 'Apps.settings.passcode.sixDigit',
|
|
)
|
|
"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup
|
|
:title="phone.t('Apps.settings.passcode.codeLength')"
|
|
>
|
|
<li class="settings-segmented-row">
|
|
<SkySegmented
|
|
:aria-label="phone.t('Apps.settings.passcode.codeLength')"
|
|
>
|
|
<SkySegmentedButton
|
|
:active="passcodeLength === 6"
|
|
@click="passcodeLength = 6"
|
|
>
|
|
{{ phone.t('Apps.settings.passcode.sixDigit') }}
|
|
</SkySegmentedButton>
|
|
<SkySegmentedButton
|
|
:active="passcodeLength === 4"
|
|
@click="passcodeLength = 4"
|
|
>
|
|
{{ phone.t('Apps.settings.passcode.fourDigit') }}
|
|
</SkySegmentedButton>
|
|
</SkySegmented>
|
|
</li>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.security')">
|
|
<SkySettingsRow
|
|
kind="action"
|
|
:title="phone.t('Apps.settings.passcode.change')"
|
|
@activate="beginChangePasscode"
|
|
/>
|
|
<SkySettingsRow
|
|
kind="action"
|
|
tone="danger"
|
|
:title="phone.t('Apps.settings.passcode.turnOff')"
|
|
@activate="beginDisablePasscode"
|
|
/>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'notifications'">
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.notifications')">
|
|
<SkySettingsRow
|
|
v-for="app in notificationApps"
|
|
:key="app.id"
|
|
kind="navigation"
|
|
:title="getPhoneAppLabel(app, phone.t)"
|
|
:value="
|
|
phone.t(
|
|
phone.preferences.settings.notifications[app.id].enabled
|
|
? 'Apps.settings.on'
|
|
: 'Apps.settings.off',
|
|
)
|
|
"
|
|
@activate="openNotificationApp(app)"
|
|
>
|
|
<template #leading>
|
|
<img
|
|
class="settings-app-icon"
|
|
:src="app.iconImage"
|
|
alt=""
|
|
draggable="false"
|
|
/>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template
|
|
v-else-if="
|
|
activeView === 'notification-detail' && selectedNotificationApp
|
|
"
|
|
>
|
|
<SkySettingsGroup
|
|
:aria-label="getPhoneAppLabel(selectedNotificationApp, phone.t)"
|
|
>
|
|
<SkySettingsRow
|
|
:title="getPhoneAppLabel(selectedNotificationApp, phone.t)"
|
|
>
|
|
<template #leading>
|
|
<img
|
|
class="settings-app-icon"
|
|
:src="selectedNotificationApp.iconImage"
|
|
alt=""
|
|
draggable="false"
|
|
/>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.notifications')">
|
|
<SkySettingsRow
|
|
kind="toggle"
|
|
:model-value="
|
|
phone.preferences.settings.notifications[
|
|
selectedNotificationApp.id
|
|
].enabled
|
|
"
|
|
:title="phone.t('Apps.settings.allowNotifications')"
|
|
@update:model-value="
|
|
phone.setAppNotification(
|
|
selectedNotificationApp.id,
|
|
'enabled',
|
|
$event,
|
|
)
|
|
"
|
|
/>
|
|
<SkySettingsRow
|
|
kind="toggle"
|
|
:disabled="
|
|
!phone.preferences.settings.notifications[
|
|
selectedNotificationApp.id
|
|
].enabled
|
|
"
|
|
:model-value="
|
|
phone.preferences.settings.notifications[
|
|
selectedNotificationApp.id
|
|
].sounds
|
|
"
|
|
:title="phone.t('Apps.settings.notificationSounds')"
|
|
@update:model-value="
|
|
phone.setAppNotification(
|
|
selectedNotificationApp.id,
|
|
'sounds',
|
|
$event,
|
|
)
|
|
"
|
|
/>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'sounds'">
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.sounds')">
|
|
<SkySettingsRangeRow
|
|
:model-value="phone.preferences.settings.ringtoneVolume"
|
|
:title="phone.t('Apps.settings.ringtoneVolume')"
|
|
:value-label="phone.preferences.settings.ringtoneVolume + '%'"
|
|
:aria-value-text="phone.preferences.settings.ringtoneVolume + '%'"
|
|
:min="0"
|
|
:max="100"
|
|
@update:model-value="
|
|
updateNumberPreference('ringtoneVolume', $event)
|
|
"
|
|
>
|
|
<template #leading><Volume1 /></template>
|
|
<template #trailing><Volume2 /></template>
|
|
</SkySettingsRangeRow>
|
|
<SkySettingsRangeRow
|
|
:model-value="phone.preferences.settings.notificationVolume"
|
|
:title="phone.t('Apps.settings.notificationVolume')"
|
|
:value-label="phone.preferences.settings.notificationVolume + '%'"
|
|
:aria-value-text="
|
|
phone.preferences.settings.notificationVolume + '%'
|
|
"
|
|
:min="0"
|
|
:max="100"
|
|
@update:model-value="
|
|
updateNumberPreference('notificationVolume', $event)
|
|
"
|
|
>
|
|
<template #leading><Volume1 /></template>
|
|
<template #trailing><Volume2 /></template>
|
|
</SkySettingsRangeRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.ringtone')">
|
|
<SkySettingsRow
|
|
v-for="ringtone in RINGTONE_IDS"
|
|
:key="ringtone"
|
|
kind="choice"
|
|
:selected="phone.preferences.settings.ringtone === ringtone"
|
|
:title="phone.t('Apps.settings.ringtones.' + ringtone)"
|
|
@activate="selectRingtone(ringtone)"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.notificationSound')">
|
|
<SkySettingsRow
|
|
v-for="sound in NOTIFICATION_SOUND_IDS"
|
|
:key="sound"
|
|
kind="choice"
|
|
:selected="phone.preferences.settings.notificationSound === sound"
|
|
:title="phone.t('Apps.settings.notificationSoundsList.' + sound)"
|
|
@activate="selectNotificationSound(sound)"
|
|
/>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'connectivity'">
|
|
<SkySettingsGroup
|
|
:title="phone.t('Apps.settings.connections')"
|
|
:footer="phone.t('Apps.settings.connectivityDescription')"
|
|
>
|
|
<SkySettingsRow
|
|
v-for="row in connectivityRows"
|
|
:key="row.key"
|
|
kind="toggle"
|
|
:disabled="phone.preferences.settings.airplaneMode"
|
|
:model-value="phone.preferences.settings[row.preferenceKey]"
|
|
:title="phone.t('Apps.settings.' + row.key)"
|
|
@update:model-value="setRootSetting(row.preferenceKey, $event)"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon :color="row.iconColor">
|
|
<component :is="row.icon" aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'focus'">
|
|
<SkySettingsGroup
|
|
:footer="phone.t('Apps.settings.focusDescription')"
|
|
:aria-label="phone.t('Apps.settings.focusMode')"
|
|
>
|
|
<SkySettingsRow
|
|
kind="toggle"
|
|
:model-value="phone.preferences.settings.focusMode"
|
|
:title="phone.t('Apps.settings.focusMode')"
|
|
@update:model-value="setRootSetting('focusMode', $event)"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon color="#5856d6">
|
|
<Moon aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'general'">
|
|
<SkySettingsGroup
|
|
:aria-label="phone.t('Apps.settings.notificationDuration')"
|
|
>
|
|
<SkySettingsRangeRow
|
|
:model-value="
|
|
phone.preferences.settings.notificationDurationSeconds
|
|
"
|
|
:title="phone.t('Apps.settings.notificationDuration')"
|
|
:value-label="
|
|
phone.t('Apps.settings.seconds', {
|
|
seconds: String(
|
|
phone.preferences.settings.notificationDurationSeconds,
|
|
),
|
|
})
|
|
"
|
|
:aria-value-text="
|
|
phone.t('Apps.settings.seconds', {
|
|
seconds: String(
|
|
phone.preferences.settings.notificationDurationSeconds,
|
|
),
|
|
})
|
|
"
|
|
:min="3"
|
|
:max="30"
|
|
:step="1"
|
|
@update:model-value="
|
|
updateNumberPreference('notificationDurationSeconds', $event)
|
|
"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.about')">
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.deviceName')"
|
|
:value="phone.t('Apps.settings.deviceNameValue')"
|
|
/>
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.softwareVersion')"
|
|
value="0.1.0"
|
|
/>
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.language')"
|
|
:value="phone.t('Apps.settings.languageValue')"
|
|
/>
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.localStorage')"
|
|
:value="phone.t('Apps.settings.localStorageValue')"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.deviceInformation')">
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.imei')"
|
|
:value="phone.device?.imei ?? '—'"
|
|
/>
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.simNumber')"
|
|
:value="
|
|
phone.device?.sim
|
|
? formatPhoneNumber(phone.device.sim.number)
|
|
: phone.t('Apps.settings.noSim')
|
|
"
|
|
/>
|
|
<SkySettingsRow
|
|
v-if="phone.device?.sim?.removable"
|
|
:title="phone.t('Apps.settings.simType')"
|
|
:value="
|
|
phone.t(
|
|
phone.device.sim.type === 'registered'
|
|
? 'Apps.settings.registeredSim'
|
|
: 'Apps.settings.anonymousSim',
|
|
)
|
|
"
|
|
/>
|
|
<SkySettingsRow
|
|
v-if="phone.device?.sim?.removable"
|
|
kind="action"
|
|
tone="danger"
|
|
:title="phone.t('Apps.settings.ejectSim')"
|
|
@activate="simEjectOpened = true"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup>
|
|
<SkySettingsRow
|
|
kind="navigation"
|
|
:title="phone.t('Apps.settings.transferOrReset')"
|
|
:description="phone.t('Apps.settings.transferOrResetDescription')"
|
|
@activate="openView('reset')"
|
|
>
|
|
<template #leading>
|
|
<SkySettingsIcon color="#8e8e93">
|
|
<RotateCcw :size="18" aria-hidden="true" />
|
|
</SkySettingsIcon>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'reset'">
|
|
<section class="settings-reset-hero">
|
|
<div class="settings-reset-hero__icon">
|
|
<RotateCcw :size="35" :stroke-width="1.65" />
|
|
</div>
|
|
<h2>{{ phone.t('Apps.settings.resetHeroTitle') }}</h2>
|
|
<p>{{ phone.t('Apps.settings.resetHeroBody') }}</p>
|
|
</section>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.erasedFromPhone')">
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.eraseDeviceSettings')"
|
|
/>
|
|
<SkySettingsRow :title="phone.t('Apps.settings.eraseLocalContent')" />
|
|
<SkySettingsRow :title="phone.t('Apps.settings.eraseLocalApps')" />
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.keptSafe')">
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.keepSimData')"
|
|
:description="phone.t('Apps.settings.keepSimDataBody')"
|
|
/>
|
|
<SkySettingsRow
|
|
:title="phone.t('Apps.settings.keepCloudData')"
|
|
:description="phone.t('Apps.settings.keepCloudDataBody')"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup>
|
|
<SkySettingsRow
|
|
kind="action"
|
|
tone="danger"
|
|
:title="phone.t('Apps.settings.factoryReset')"
|
|
@activate="resetOpened = true"
|
|
/>
|
|
</SkySettingsGroup>
|
|
<p class="settings-reset-footnote">
|
|
{{ phone.t('Apps.settings.resetSetupAssistant') }}
|
|
</p>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'appearance'">
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.graphicsMode')">
|
|
<SkySettingsRow
|
|
v-for="mode in GRAPHICS_MODE_IDS"
|
|
:key="mode"
|
|
kind="choice"
|
|
:selected="phone.preferences.settings.graphicsMode === mode"
|
|
:title="phone.t('Apps.settings.' + mode + 'Mode')"
|
|
:description="phone.t('Apps.settings.' + mode + 'ModeDescription')"
|
|
@activate="selectGraphicsMode(mode)"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.appearanceMode')">
|
|
<SkySettingsRow
|
|
v-for="mode in APPEARANCE_MODE_IDS"
|
|
:key="mode"
|
|
kind="choice"
|
|
:selected="phone.preferences.settings.appearanceMode === mode"
|
|
:title="phone.t('Apps.settings.' + mode)"
|
|
@activate="selectAppearanceMode(mode)"
|
|
/>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup
|
|
:aria-label="phone.t('Apps.settings.screenBrightness')"
|
|
>
|
|
<SkySettingsRangeRow
|
|
:model-value="phone.preferences.settings.screenBrightness"
|
|
:title="phone.t('Apps.settings.screenBrightness')"
|
|
:value-label="phone.preferences.settings.screenBrightness + '%'"
|
|
:aria-value-text="phone.preferences.settings.screenBrightness + '%'"
|
|
:min="10"
|
|
:max="100"
|
|
@update:model-value="
|
|
updateNumberPreference('screenBrightness', $event)
|
|
"
|
|
>
|
|
<template #leading><Sun /></template>
|
|
<template #trailing><Sun /></template>
|
|
</SkySettingsRangeRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.settings.phoneScale')">
|
|
<SkySettingsRangeRow
|
|
:model-value="phone.preferences.settings.phoneScale"
|
|
:title="phone.t('Apps.settings.phoneScale')"
|
|
:value-label="phone.preferences.settings.phoneScale + '%'"
|
|
:aria-value-text="phone.preferences.settings.phoneScale + '%'"
|
|
:min="PHONE_SCALE_MIN"
|
|
:max="PHONE_SCALE_MAX"
|
|
:step="PHONE_SCALE_STEP"
|
|
@update:model-value="updateNumberPreference('phoneScale', $event)"
|
|
>
|
|
<template #leading><Smartphone /></template>
|
|
<template #trailing><Smartphone /></template>
|
|
</SkySettingsRangeRow>
|
|
</SkySettingsGroup>
|
|
|
|
<SkySettingsGroup :title="phone.t('Apps.settings.phoneFrame')">
|
|
<SkySettingsRow
|
|
kind="navigation"
|
|
:title="phone.t('Apps.settings.phoneFrame')"
|
|
@activate="framePickerOpened = true"
|
|
>
|
|
<template #trailing>
|
|
<span
|
|
class="settings-frame-swatch"
|
|
:style="{ background: selectedFrameColor }"
|
|
aria-hidden="true"
|
|
/>
|
|
</template>
|
|
</SkySettingsRow>
|
|
</SkySettingsGroup>
|
|
</template>
|
|
|
|
<template v-else-if="activeView === 'wallpaper'">
|
|
<SkySegmented
|
|
class="settings-wallpaper-target"
|
|
strong
|
|
rounded
|
|
:active-index="wallpaperTarget === 'home' ? 0 : 1"
|
|
:item-count="2"
|
|
:aria-label="phone.t('Apps.settings.wallpaperTarget')"
|
|
>
|
|
<SkySegmentedButton
|
|
:active="wallpaperTarget === 'home'"
|
|
@click="wallpaperTarget = 'home'"
|
|
>
|
|
{{ phone.t('Apps.settings.wallpaperHomeScreen') }}
|
|
</SkySegmentedButton>
|
|
<SkySegmentedButton
|
|
:active="wallpaperTarget === 'lock'"
|
|
@click="wallpaperTarget = 'lock'"
|
|
>
|
|
{{ phone.t('Apps.settings.wallpaperLockScreen') }}
|
|
</SkySegmentedButton>
|
|
</SkySegmented>
|
|
|
|
<section class="settings-wallpaper-actions">
|
|
<button type="button" @click="openWallpaperMedia('photos')">
|
|
<span class="settings-wallpaper-actions__icon" aria-hidden="true">
|
|
<Images />
|
|
</span>
|
|
<span>
|
|
<strong>{{
|
|
phone.t('Apps.settings.wallpaperFromPhotos')
|
|
}}</strong>
|
|
<small>{{
|
|
phone.t('Apps.settings.wallpaperFromPhotosDescription')
|
|
}}</small>
|
|
</span>
|
|
</button>
|
|
<button type="button" @click="openWallpaperMedia('camera')">
|
|
<span class="settings-wallpaper-actions__icon" aria-hidden="true">
|
|
<Camera />
|
|
</span>
|
|
<span>
|
|
<strong>{{
|
|
phone.t('Apps.settings.wallpaperFromCamera')
|
|
}}</strong>
|
|
<small>{{
|
|
phone.t('Apps.settings.wallpaperFromCameraDescription')
|
|
}}</small>
|
|
</span>
|
|
</button>
|
|
<button
|
|
v-if="customWallpaperUploadAvailable"
|
|
type="button"
|
|
class="settings-wallpaper-actions__custom"
|
|
@click="openWallpaperCustomUpload"
|
|
>
|
|
<span class="settings-wallpaper-actions__icon" aria-hidden="true">
|
|
<Upload />
|
|
</span>
|
|
<span>
|
|
<strong>{{
|
|
phone.t('Apps.settings.wallpaperCustomUpload')
|
|
}}</strong>
|
|
<small>{{
|
|
phone.t('Apps.settings.wallpaperCustomUploadDescription')
|
|
}}</small>
|
|
</span>
|
|
</button>
|
|
</section>
|
|
|
|
<section
|
|
v-if="wallpaperHistory.length"
|
|
class="settings-wallpaper-section"
|
|
>
|
|
<h2>{{ phone.t('Apps.settings.wallpaperHistory') }}</h2>
|
|
<div class="settings-wallpaper-history">
|
|
<button
|
|
v-for="(entry, index) in wallpaperHistory"
|
|
:key="`${entry.wallpaper}-${entry.imageUrl ?? index}`"
|
|
type="button"
|
|
:class="[
|
|
`wallpaper--${entry.wallpaper}`,
|
|
{
|
|
'settings-wallpaper-history__item--selected':
|
|
isWallpaperSelected(entry),
|
|
},
|
|
]"
|
|
:style="wallpaperPreviewStyle(entry)"
|
|
:aria-label="
|
|
entry.wallpaper === 'custom'
|
|
? phone.t('Apps.settings.wallpaperCustom')
|
|
: phone.t('Apps.settings.wallpapers.' + entry.wallpaper)
|
|
"
|
|
@click="selectWallpaperHistory(entry)"
|
|
>
|
|
<Check v-if="isWallpaperSelected(entry)" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="settings-wallpaper-section">
|
|
<h2>{{ phone.t('Apps.settings.wallpaperPicker') }}</h2>
|
|
<div class="settings-wallpaper-grid">
|
|
<button
|
|
v-for="wallpaper in WALLPAPER_IDS"
|
|
:key="wallpaper"
|
|
type="button"
|
|
class="settings-wallpaper-choice"
|
|
:class="{
|
|
'settings-wallpaper-choice--selected':
|
|
selectedWallpaper === wallpaper,
|
|
}"
|
|
:aria-pressed="selectedWallpaper === wallpaper"
|
|
@click="selectBuiltInWallpaper(wallpaper)"
|
|
>
|
|
<span
|
|
class="settings-wallpaper-choice__preview"
|
|
:class="`wallpaper--${wallpaper}`"
|
|
aria-hidden="true"
|
|
>
|
|
<span class="settings-wallpaper-choice__screen"></span>
|
|
<Check v-if="selectedWallpaper === wallpaper" />
|
|
</span>
|
|
<strong>{{
|
|
phone.t('Apps.settings.wallpapers.' + wallpaper)
|
|
}}</strong>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
</SkyScrollArea>
|
|
|
|
<PhonePasscode
|
|
v-if="passcodeFlow"
|
|
:busy="passcodeBusy"
|
|
:error="passcodeError"
|
|
:length="passcodeLength"
|
|
:reset-key="passcodeResetKey"
|
|
:subtitle="phone.t('Apps.settings.passcode.screenSubtitle')"
|
|
:title="passcodeTitle"
|
|
@cancel="cancelPasscodeFlow"
|
|
@complete="submitSettingsPasscode"
|
|
/>
|
|
|
|
<div
|
|
v-if="factoryResetting"
|
|
class="settings-reset-overlay"
|
|
aria-live="polite"
|
|
>
|
|
<div class="settings-reset-content">
|
|
<div class="settings-reset-mark" aria-hidden="true">
|
|
<div class="settings-reset-mark__face">
|
|
<Smartphone :size="38" :stroke-width="1.45" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-reset-heading">
|
|
<h2>{{ phone.t('Apps.settings.factoryResetProgress') }}</h2>
|
|
<p>{{ factoryResetPhase.label }}</p>
|
|
</div>
|
|
|
|
<div class="settings-reset-progress-copy">
|
|
<strong>{{ Math.floor(factoryResetProgress) }}%</strong>
|
|
<span>{{ factoryResetSecondsRemaining }}s</span>
|
|
</div>
|
|
<div
|
|
class="settings-reset-progress"
|
|
role="progressbar"
|
|
:aria-valuenow="Math.floor(factoryResetProgress)"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
>
|
|
<span :style="{ width: `${factoryResetProgress}%` }" />
|
|
</div>
|
|
<p class="settings-reset-detail">{{ factoryResetPhase.detail }}</p>
|
|
|
|
<div class="settings-reset-assurance">
|
|
<span class="settings-reset-assurance__icon">
|
|
<ShieldCheck :size="18" :stroke-width="1.8" />
|
|
</span>
|
|
<span>
|
|
<strong>{{ phone.t('Apps.settings.factoryResetSimSafe') }}</strong>
|
|
<small>{{ phone.t('Apps.settings.factoryResetCloudSafe') }}</small>
|
|
</span>
|
|
</div>
|
|
<p class="settings-reset-warning">
|
|
{{ phone.t('Apps.settings.factoryResetWarning') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<SkyDialog
|
|
:opened="framePickerOpened"
|
|
:title="phone.t('Apps.settings.phoneFrame')"
|
|
@backdropclick="framePickerOpened = false"
|
|
@escape="framePickerOpened = false"
|
|
>
|
|
<div
|
|
class="settings-frame-grid"
|
|
role="group"
|
|
:aria-label="phone.t('Apps.settings.phoneFrame')"
|
|
>
|
|
<button
|
|
v-for="frame in PHONE_FRAME_IDS"
|
|
:key="frame"
|
|
type="button"
|
|
class="settings-frame-choice"
|
|
:class="{
|
|
'settings-frame-choice--selected':
|
|
phone.preferences.settings.frame === frame,
|
|
}"
|
|
:aria-label="phone.t('Apps.settings.frames.' + frame)"
|
|
:aria-pressed="phone.preferences.settings.frame === frame"
|
|
@click="selectFrame(frame)"
|
|
>
|
|
<span
|
|
class="settings-frame-choice__swatch"
|
|
:style="{ background: PHONE_FRAME_COLORS[frame] }"
|
|
aria-hidden="true"
|
|
/>
|
|
<Check
|
|
v-if="phone.preferences.settings.frame === frame"
|
|
:size="17"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
</div>
|
|
<template #buttons>
|
|
<SkyDialogButton @click="framePickerOpened = false">
|
|
{{ phone.t('Common.cancel') }}
|
|
</SkyDialogButton>
|
|
</template>
|
|
</SkyDialog>
|
|
|
|
<SkyDialog
|
|
:opened="removeDeviceOpened"
|
|
:title="phone.t('Apps.settings.removeDevice')"
|
|
:content="phone.t('Apps.settings.removeDeviceBody')"
|
|
@backdropclick="removeDeviceOpened = false"
|
|
@escape="removeDeviceOpened = false"
|
|
>
|
|
<SkySettingsGroup :aria-label="phone.t('Apps.mail.password')">
|
|
<SkyField
|
|
v-model="removeDevicePassword"
|
|
type="password"
|
|
:label="phone.t('Apps.mail.password')"
|
|
autocomplete="current-password"
|
|
/>
|
|
</SkySettingsGroup>
|
|
<template #buttons>
|
|
<SkyDialogButton @click="removeDeviceOpened = false">
|
|
{{ phone.t('Common.cancel') }}
|
|
</SkyDialogButton>
|
|
<SkyDialogButton
|
|
class="settings-dialog-button--danger"
|
|
strong
|
|
@click="confirmRemoveDevice"
|
|
>
|
|
{{ phone.t('Apps.settings.removeDevice') }}
|
|
</SkyDialogButton>
|
|
</template>
|
|
</SkyDialog>
|
|
|
|
<SkyDialog
|
|
v-if="phone.device?.sim?.removable"
|
|
:opened="simEjectOpened"
|
|
:title="phone.t('Apps.settings.ejectSim')"
|
|
:content="phone.t('Apps.settings.ejectSimBody')"
|
|
@backdropclick="simEjectOpened = false"
|
|
@escape="simEjectOpened = false"
|
|
>
|
|
<template #buttons>
|
|
<SkyDialogButton @click="simEjectOpened = false">
|
|
{{ phone.t('Common.cancel') }}
|
|
</SkyDialogButton>
|
|
<SkyDialogButton
|
|
class="settings-dialog-button--danger"
|
|
strong
|
|
@click="confirmSimEject"
|
|
>
|
|
{{ phone.t('Apps.settings.ejectSim') }}
|
|
</SkyDialogButton>
|
|
</template>
|
|
</SkyDialog>
|
|
|
|
<SkyDialog
|
|
:opened="resetOpened"
|
|
:title="phone.t('Apps.settings.factoryReset')"
|
|
:content="phone.t('Apps.settings.factoryResetBody')"
|
|
@backdropclick="resetOpened = false"
|
|
@escape="resetOpened = false"
|
|
>
|
|
<template #buttons>
|
|
<SkyDialogButton @click="resetOpened = false">
|
|
{{ phone.t('Common.cancel') }}
|
|
</SkyDialogButton>
|
|
<SkyDialogButton
|
|
class="settings-dialog-button--danger"
|
|
strong
|
|
@click="confirmFactoryReset"
|
|
>
|
|
{{ phone.t('Common.reset') }}
|
|
</SkyDialogButton>
|
|
</template>
|
|
</SkyDialog>
|
|
|
|
<SkyNotification
|
|
:opened="Boolean(accountToast)"
|
|
:text="accountToast"
|
|
@click="accountToast = ''"
|
|
/>
|
|
</SkyAppPage>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.settings-navbar.sky-navbar--large) {
|
|
min-height: calc(
|
|
var(--sky-navbar-safe-area-top) + var(--sky-navbar-large-title-height)
|
|
);
|
|
}
|
|
|
|
:deep(.settings-navbar.sky-navbar--large.sky-navbar--no-navigation) {
|
|
padding-top: calc(var(--sky-navbar-safe-area-top) + var(--sky-space-3));
|
|
}
|
|
|
|
.settings-search {
|
|
margin-bottom: var(--sky-space-4);
|
|
}
|
|
|
|
.settings-content {
|
|
padding-top: var(--sky-space-2);
|
|
}
|
|
|
|
.settings-content--subpage {
|
|
padding-top: 0;
|
|
}
|
|
|
|
.settings-reset-hero {
|
|
padding: 18px 24px 22px;
|
|
text-align: center;
|
|
}
|
|
|
|
.settings-reset-hero__icon {
|
|
display: grid;
|
|
width: 78px;
|
|
height: 78px;
|
|
margin: 0 auto 16px;
|
|
place-items: center;
|
|
border: 1px solid rgb(255 255 255 / 10%);
|
|
border-radius: 24px;
|
|
color: #ff665f;
|
|
background: linear-gradient(
|
|
145deg,
|
|
rgb(255 74 66 / 18%),
|
|
rgb(255 255 255 / 5%)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px rgb(255 255 255 / 12%),
|
|
0 18px 35px rgb(0 0 0 / 14%);
|
|
}
|
|
|
|
.settings-reset-hero h2 {
|
|
margin: 0;
|
|
font-size: 22px;
|
|
line-height: 1.08;
|
|
letter-spacing: -0.035em;
|
|
}
|
|
|
|
.settings-reset-hero p,
|
|
.settings-reset-footnote {
|
|
color: var(--sky-color-text-secondary);
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.settings-reset-hero p {
|
|
margin: 10px 0 0;
|
|
}
|
|
|
|
.settings-reset-footnote {
|
|
margin: -3px 20px 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.settings-copy {
|
|
margin: 0 var(--sky-space-1) var(--sky-space-4);
|
|
color: var(--sky-muted);
|
|
font-size: 13px;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.settings-primary-action {
|
|
margin-bottom: var(--sky-space-5);
|
|
}
|
|
|
|
.settings-account-suffix {
|
|
color: var(--sky-muted);
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.settings-segmented-row {
|
|
min-width: 0;
|
|
padding: var(--sky-space-2) var(--sky-space-3);
|
|
list-style: none;
|
|
}
|
|
|
|
.settings-app-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: block;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.settings-frame-swatch {
|
|
width: 28px;
|
|
height: 28px;
|
|
display: block;
|
|
flex: none;
|
|
border: 1px solid var(--sky-hairline);
|
|
border-radius: 50%;
|
|
box-shadow: 0 1px 3px rgb(0 0 0 / 18%);
|
|
}
|
|
|
|
.settings-wallpaper-target {
|
|
margin-bottom: var(--sky-space-4);
|
|
}
|
|
|
|
.settings-wallpaper-actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: var(--sky-space-2);
|
|
margin-bottom: var(--sky-space-5);
|
|
}
|
|
|
|
.settings-wallpaper-actions > button {
|
|
min-width: 0;
|
|
min-height: 116px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--sky-space-3);
|
|
padding: var(--sky-space-4);
|
|
border: 1px solid var(--sky-hairline);
|
|
border-radius: 20px;
|
|
background: var(--sky-surface);
|
|
color: var(--sky-text);
|
|
text-align: left;
|
|
box-shadow: 0 8px 20px rgb(0 0 0 / 8%);
|
|
cursor: pointer;
|
|
transition:
|
|
transform 150ms ease,
|
|
background 150ms ease;
|
|
}
|
|
|
|
.settings-wallpaper-actions > button:hover {
|
|
background: var(--sky-surface-muted);
|
|
}
|
|
|
|
.settings-wallpaper-actions > button:active {
|
|
transform: scale(0.97);
|
|
}
|
|
|
|
.settings-wallpaper-actions > .settings-wallpaper-actions__custom {
|
|
grid-column: 1 / -1;
|
|
min-height: 92px;
|
|
}
|
|
|
|
.settings-wallpaper-actions > button:focus-visible,
|
|
.settings-wallpaper-history > button:focus-visible,
|
|
.settings-wallpaper-choice:focus-visible {
|
|
outline: 2px solid var(--sky-app-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.settings-wallpaper-actions__icon {
|
|
width: 38px;
|
|
height: 38px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 12px;
|
|
background: var(--sky-surface-muted);
|
|
color: #fff;
|
|
}
|
|
|
|
.settings-wallpaper-actions__icon > svg {
|
|
width: 21px;
|
|
height: 21px;
|
|
}
|
|
|
|
.settings-wallpaper-actions strong,
|
|
.settings-wallpaper-actions small {
|
|
display: block;
|
|
}
|
|
|
|
.settings-wallpaper-actions strong {
|
|
font-size: 14px;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.settings-wallpaper-actions small {
|
|
margin-top: 3px;
|
|
color: var(--sky-muted);
|
|
font-size: 11px;
|
|
line-height: 14px;
|
|
}
|
|
|
|
.settings-wallpaper-section {
|
|
margin-bottom: var(--sky-space-5);
|
|
}
|
|
|
|
.settings-wallpaper-section > h2 {
|
|
margin: 0 var(--sky-space-1) var(--sky-space-2);
|
|
color: var(--sky-muted);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.settings-wallpaper-history {
|
|
display: flex;
|
|
gap: var(--sky-space-2);
|
|
overflow-x: auto;
|
|
padding: 2px var(--sky-space-1) var(--sky-space-2);
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.settings-wallpaper-history::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.settings-wallpaper-history > button {
|
|
width: 62px;
|
|
height: 88px;
|
|
position: relative;
|
|
flex: 0 0 auto;
|
|
display: grid;
|
|
place-items: center;
|
|
overflow: hidden;
|
|
border: 2px solid transparent;
|
|
border-radius: 15px;
|
|
background-position: center;
|
|
background-size: cover;
|
|
color: #ffffff;
|
|
box-shadow: 0 5px 14px rgb(0 0 0 / 18%);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.settings-wallpaper-history > button::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border: 1px solid rgb(255 255 255 / 13%);
|
|
border-radius: inherit;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.settings-wallpaper-history > button > svg {
|
|
width: 21px;
|
|
height: 21px;
|
|
position: relative;
|
|
z-index: 1;
|
|
padding: 4px;
|
|
border-radius: 50%;
|
|
background: var(--sky-app-accent);
|
|
stroke-width: 3;
|
|
}
|
|
|
|
.settings-wallpaper-history__item--selected {
|
|
border-color: var(--sky-app-accent) !important;
|
|
}
|
|
|
|
.settings-wallpaper-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: var(--sky-space-3);
|
|
}
|
|
|
|
.settings-wallpaper-choice {
|
|
min-width: 0;
|
|
padding: 0 0 var(--sky-space-2);
|
|
border: 0;
|
|
border-radius: 21px;
|
|
background: var(--sky-surface);
|
|
color: var(--sky-text);
|
|
text-align: left;
|
|
box-shadow: 0 8px 20px rgb(0 0 0 / 8%);
|
|
cursor: pointer;
|
|
transition:
|
|
transform 150ms ease,
|
|
box-shadow 150ms ease;
|
|
}
|
|
|
|
.settings-wallpaper-choice:hover {
|
|
box-shadow: 0 11px 24px rgb(0 0 0 / 13%);
|
|
}
|
|
|
|
.settings-wallpaper-choice:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.settings-wallpaper-choice__preview {
|
|
aspect-ratio: 0.78;
|
|
position: relative;
|
|
display: grid;
|
|
place-items: center;
|
|
overflow: hidden;
|
|
border: 2px solid transparent;
|
|
border-radius: 21px 21px 14px 14px;
|
|
background-position: center;
|
|
background-size: cover;
|
|
}
|
|
|
|
.settings-wallpaper-choice__screen {
|
|
width: 28%;
|
|
height: 7px;
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 50%;
|
|
border-radius: 999px;
|
|
background: rgb(0 0 0 / 58%);
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.settings-wallpaper-choice__preview > svg {
|
|
width: 26px;
|
|
height: 26px;
|
|
padding: 5px;
|
|
border-radius: 50%;
|
|
background: var(--sky-app-accent);
|
|
color: #ffffff;
|
|
stroke-width: 3;
|
|
}
|
|
|
|
.settings-wallpaper-choice > strong {
|
|
display: block;
|
|
overflow: hidden;
|
|
padding: var(--sky-space-2) var(--sky-space-3) 0;
|
|
font-size: 13px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.settings-wallpaper-choice--selected .settings-wallpaper-choice__preview {
|
|
border-color: var(--sky-app-accent);
|
|
}
|
|
|
|
.settings-frame-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 52px);
|
|
justify-content: center;
|
|
gap: var(--sky-space-2);
|
|
margin-top: var(--sky-space-4);
|
|
}
|
|
|
|
.settings-frame-choice {
|
|
width: 52px;
|
|
height: 52px;
|
|
position: relative;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 0;
|
|
border-radius: var(--sky-radius-control);
|
|
background: transparent;
|
|
color: #ffffff;
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
.settings-frame-choice:focus-visible {
|
|
outline: 2px solid var(--sky-app-accent);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
.settings-frame-choice:active {
|
|
background: var(--sky-app-accent-soft);
|
|
}
|
|
|
|
.settings-frame-choice__swatch {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: block;
|
|
border: 1px solid var(--sky-hairline);
|
|
border-radius: 50%;
|
|
box-shadow: 0 1px 4px rgb(0 0 0 / 22%);
|
|
}
|
|
|
|
.settings-frame-choice > svg {
|
|
position: absolute;
|
|
}
|
|
|
|
.settings-frame-choice--selected .settings-frame-choice__swatch {
|
|
box-shadow:
|
|
0 0 0 2px var(--sky-surface),
|
|
0 0 0 4px var(--sky-app-accent);
|
|
}
|
|
|
|
.settings-reset-overlay {
|
|
position: absolute;
|
|
z-index: 100;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
padding: 52px 28px 28px;
|
|
background:
|
|
radial-gradient(ellipse at 8% 13%, rgb(38 117 255 / 31%), transparent 38%),
|
|
radial-gradient(ellipse at 92% 70%, rgb(118 66 255 / 24%), transparent 41%),
|
|
radial-gradient(ellipse at 42% 102%, rgb(0 191 184 / 14%), transparent 36%),
|
|
linear-gradient(148deg, #050b1b 0%, #080719 47%, #02060d 100%);
|
|
color: #ffffff;
|
|
text-align: center;
|
|
}
|
|
|
|
.settings-reset-overlay::before {
|
|
position: absolute;
|
|
inset: -30%;
|
|
background:
|
|
linear-gradient(
|
|
118deg,
|
|
transparent 36%,
|
|
rgb(100 151 255 / 7%) 36.2%,
|
|
transparent 36.7%
|
|
),
|
|
linear-gradient(
|
|
118deg,
|
|
transparent 49%,
|
|
rgb(164 122 255 / 6%) 49.2%,
|
|
transparent 49.8%
|
|
),
|
|
linear-gradient(
|
|
118deg,
|
|
transparent 63%,
|
|
rgb(72 187 255 / 5%) 63.2%,
|
|
transparent 63.7%
|
|
);
|
|
content: '';
|
|
transform: translateX(-7%);
|
|
}
|
|
|
|
.settings-reset-overlay::after {
|
|
position: absolute;
|
|
top: 12%;
|
|
right: -20%;
|
|
width: 78%;
|
|
height: 1px;
|
|
background: linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
rgb(155 187 255 / 18%),
|
|
transparent
|
|
);
|
|
box-shadow:
|
|
0 132px rgb(155 187 255 / 7%),
|
|
0 264px rgb(155 187 255 / 5%);
|
|
content: '';
|
|
transform: rotate(-28deg);
|
|
}
|
|
|
|
.settings-reset-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
width: 100%;
|
|
max-width: 294px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.settings-reset-mark {
|
|
position: relative;
|
|
width: 126px;
|
|
height: 102px;
|
|
}
|
|
|
|
.settings-reset-mark__layer,
|
|
.settings-reset-mark__face {
|
|
position: absolute;
|
|
width: 82px;
|
|
height: 68px;
|
|
border: 1px solid rgb(255 255 255 / 13%);
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.settings-reset-mark__layer--back {
|
|
top: 0;
|
|
left: 13px;
|
|
background: linear-gradient(
|
|
145deg,
|
|
rgb(72 135 255 / 22%),
|
|
rgb(82 53 190 / 12%)
|
|
);
|
|
transform: rotate(-12deg);
|
|
}
|
|
|
|
.settings-reset-mark__layer--middle {
|
|
top: 13px;
|
|
right: 11px;
|
|
border-color: rgb(168 126 255 / 20%);
|
|
background: linear-gradient(
|
|
145deg,
|
|
rgb(134 87 255 / 19%),
|
|
rgb(41 89 194 / 9%)
|
|
);
|
|
transform: rotate(9deg);
|
|
}
|
|
|
|
.settings-reset-mark__face {
|
|
top: 28px;
|
|
left: 22px;
|
|
display: grid;
|
|
overflow: hidden;
|
|
border-color: rgb(255 255 255 / 20%);
|
|
background: linear-gradient(
|
|
145deg,
|
|
rgb(255 255 255 / 16%),
|
|
rgb(255 255 255 / 6%)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px rgb(255 255 255 / 15%),
|
|
0 22px 48px rgb(0 0 0 / 28%);
|
|
color: rgb(255 255 255 / 92%);
|
|
place-items: center;
|
|
}
|
|
|
|
.settings-reset-mark__erase {
|
|
position: absolute;
|
|
right: 9px;
|
|
bottom: 9px;
|
|
display: flex;
|
|
gap: 3px;
|
|
}
|
|
|
|
.settings-reset-mark__erase i {
|
|
display: block;
|
|
width: 11px;
|
|
height: 2px;
|
|
background: rgb(127 183 255 / 35%);
|
|
}
|
|
|
|
.settings-reset-mark__erase i:nth-child(2) {
|
|
background: #62aaff;
|
|
box-shadow: 0 0 9px rgb(81 158 255 / 50%);
|
|
}
|
|
|
|
.settings-reset-heading {
|
|
margin-top: 25px;
|
|
}
|
|
|
|
.settings-reset-heading h2 {
|
|
margin: 0;
|
|
font-size: 22px;
|
|
letter-spacing: -0.035em;
|
|
}
|
|
|
|
.settings-reset-heading p {
|
|
margin: 7px 0 0;
|
|
color: rgb(255 255 255 / 54%);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.settings-reset-progress-copy {
|
|
display: flex;
|
|
width: 100%;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
margin-top: 34px;
|
|
}
|
|
|
|
.settings-reset-progress-copy strong {
|
|
font-size: 15px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.settings-reset-progress-copy span {
|
|
color: rgb(255 255 255 / 44%);
|
|
font-size: 12px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.settings-reset-progress {
|
|
width: 100%;
|
|
height: 4px;
|
|
overflow: hidden;
|
|
margin-top: 9px;
|
|
border-radius: 999px;
|
|
background: rgb(255 255 255 / 10%);
|
|
}
|
|
|
|
.settings-reset-progress > span {
|
|
display: block;
|
|
height: 100%;
|
|
border-radius: inherit;
|
|
background: linear-gradient(90deg, #208cff, #7774ff);
|
|
box-shadow: 0 0 12px rgb(60 132 255 / 42%);
|
|
transition: width 120ms linear;
|
|
}
|
|
|
|
.settings-reset-detail {
|
|
width: 100%;
|
|
margin: 9px 0 0;
|
|
color: rgb(255 255 255 / 48%);
|
|
font-size: 11px;
|
|
text-align: left;
|
|
}
|
|
|
|
.settings-reset-assurance {
|
|
display: flex;
|
|
width: 100%;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-top: 30px;
|
|
padding: 13px 14px;
|
|
border: 1px solid rgb(255 255 255 / 11%);
|
|
border-radius: 16px;
|
|
background: linear-gradient(
|
|
135deg,
|
|
rgb(255 255 255 / 8%),
|
|
rgb(255 255 255 / 4%)
|
|
);
|
|
box-shadow: inset 0 1px rgb(255 255 255 / 5%);
|
|
text-align: left;
|
|
}
|
|
|
|
.settings-reset-assurance__icon {
|
|
display: grid;
|
|
width: 34px;
|
|
height: 34px;
|
|
flex: 0 0 34px;
|
|
border-radius: 11px;
|
|
background: rgb(10 132 255 / 14%);
|
|
color: #5eb0ff;
|
|
place-items: center;
|
|
}
|
|
|
|
.settings-reset-assurance > span:last-child {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
}
|
|
|
|
.settings-reset-assurance strong {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.settings-reset-assurance small {
|
|
color: rgb(255 255 255 / 45%);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.settings-reset-warning {
|
|
max-width: 258px;
|
|
margin: 14px 0 0;
|
|
color: rgb(255 255 255 / 34%);
|
|
font-size: 9px;
|
|
line-height: 14px;
|
|
}
|
|
|
|
/* Factory reset stays intentionally quiet: white, direct and system-like. */
|
|
.settings-reset-hero__icon {
|
|
border: 0;
|
|
border-radius: 50%;
|
|
color: #ff3b30;
|
|
background: #f2f2f7;
|
|
box-shadow: none;
|
|
}
|
|
.settings-reset-overlay {
|
|
padding: 58px 30px 34px;
|
|
color: #1d1d1f;
|
|
background: #ffffff;
|
|
}
|
|
.settings-reset-overlay::before,
|
|
.settings-reset-overlay::after {
|
|
content: none;
|
|
}
|
|
.settings-reset-content {
|
|
max-width: 300px;
|
|
}
|
|
.settings-reset-mark {
|
|
display: grid;
|
|
width: 88px;
|
|
height: 88px;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
background: #f2f2f7;
|
|
}
|
|
.settings-reset-mark__face {
|
|
position: static;
|
|
display: grid;
|
|
width: 88px;
|
|
height: 88px;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
color: #ff3b30;
|
|
background: transparent;
|
|
box-shadow: none;
|
|
place-items: center;
|
|
}
|
|
.settings-reset-heading {
|
|
margin-top: 28px;
|
|
}
|
|
.settings-reset-heading h2 {
|
|
color: #1d1d1f;
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.04em;
|
|
}
|
|
.settings-reset-heading p {
|
|
color: #6e6e73;
|
|
}
|
|
.settings-reset-progress-copy {
|
|
margin-top: 40px;
|
|
}
|
|
.settings-reset-progress-copy strong {
|
|
color: #1d1d1f;
|
|
font-size: 15px;
|
|
}
|
|
.settings-reset-progress-copy span {
|
|
color: #8e8e93;
|
|
}
|
|
.settings-reset-progress {
|
|
height: 5px;
|
|
background: #e5e5ea;
|
|
}
|
|
.settings-reset-progress > span {
|
|
background: #007aff;
|
|
box-shadow: none;
|
|
}
|
|
.settings-reset-detail {
|
|
color: #8e8e93;
|
|
}
|
|
.settings-reset-assurance {
|
|
margin-top: 34px;
|
|
padding: 14px;
|
|
border: 0;
|
|
border-radius: 16px;
|
|
background: #f2f2f7;
|
|
box-shadow: none;
|
|
}
|
|
.settings-reset-assurance__icon {
|
|
border-radius: 50%;
|
|
color: #007aff;
|
|
background: #e4f1ff;
|
|
}
|
|
.settings-reset-assurance strong {
|
|
color: #1d1d1f;
|
|
}
|
|
.settings-reset-assurance small {
|
|
color: #6e6e73;
|
|
}
|
|
.settings-reset-warning {
|
|
color: #8e8e93;
|
|
}
|
|
|
|
.settings-dialog-button--danger:not(:disabled) {
|
|
background: var(--sky-danger);
|
|
color: #ffffff;
|
|
}
|
|
|
|
.settings-dialog-button--danger:active:not(:disabled) {
|
|
filter: brightness(0.86);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.settings-frame-choice,
|
|
.settings-wallpaper-actions > button,
|
|
.settings-wallpaper-choice {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|