mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 02:01:40 +00:00
Merge branch 'dev' into feature/funk
This commit is contained in:
+49
-4
@@ -11,6 +11,7 @@ import {
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import PhoneHomeIndicator from '@/components/PhoneHomeIndicator.vue'
|
||||
import PhoneControlCenter from '@/components/PhoneControlCenter.vue'
|
||||
import PhoneMediaCapture from '@/components/PhoneMediaCapture.vue'
|
||||
import PhoneLockScreen from '@/components/PhoneLockScreen.vue'
|
||||
import PhoneNotifications from '@/components/PhoneNotifications.vue'
|
||||
@@ -140,6 +141,7 @@ const appTransitionName = computed(() =>
|
||||
)
|
||||
const isLocked = ref(false)
|
||||
const isUnlocking = ref(false)
|
||||
const controlCenterOpened = ref(false)
|
||||
const simPicker = ref<SimPickerPayload | null>(null)
|
||||
const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const viewportScale = ref(getViewportScale())
|
||||
@@ -150,6 +152,11 @@ const phoneResolutionStyle = computed<CSSProperties>(() => ({
|
||||
'--phone-zoom':
|
||||
phoneBaseZoom.value * (phone.preferences.settings.phoneScale / 100),
|
||||
}))
|
||||
const phoneDisplayStyle = computed<CSSProperties>(() => ({
|
||||
'--phone-display-dim': String(
|
||||
((100 - phone.preferences.settings.screenBrightness) / 100) * 0.8,
|
||||
),
|
||||
}))
|
||||
const phoneFrameImage = computed(
|
||||
() => PHONE_FRAME_IMAGES[phone.preferences.settings.frame],
|
||||
)
|
||||
@@ -339,17 +346,26 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
} else if (event.data?.type === 'darkchat:new' && event.data.data) {
|
||||
const data = event.data.data as DarkChatEventData
|
||||
void darkchat.refreshInbox()
|
||||
if (data.conversationId && darkchat.activeConversation?.id === data.conversationId) {
|
||||
if (
|
||||
data.conversationId &&
|
||||
darkchat.activeConversation?.id === data.conversationId
|
||||
) {
|
||||
void darkchat.openThread(data.conversationId)
|
||||
}
|
||||
if (data.notificationMode !== 'hidden') {
|
||||
const notification: PhoneNotificationInput = {
|
||||
appId: 'darkchat',
|
||||
subtitle: data.sender,
|
||||
text: data.text ?? data.preview ?? phone.t('Apps.darkchat.privateNotification'),
|
||||
text:
|
||||
data.text ??
|
||||
data.preview ??
|
||||
phone.t('Apps.darkchat.privateNotification'),
|
||||
title: data.title ?? phone.t('Apps.darkchat.name'),
|
||||
}
|
||||
if (data.device && (!phone.isOpen || data.device.imei !== phone.device?.imei)) {
|
||||
if (
|
||||
data.device &&
|
||||
(!phone.isOpen || data.device.imei !== phone.device?.imei)
|
||||
) {
|
||||
notification.device = {
|
||||
imei: data.device.imei,
|
||||
name: data.device.name,
|
||||
@@ -368,6 +384,7 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
event.data.data
|
||||
) {
|
||||
calls.applyCallState(event.data.data as PhoneCall)
|
||||
controlCenterOpened.value = false
|
||||
isLocked.value = false
|
||||
isUnlocking.value = false
|
||||
window.setTimeout(() => void router.push('/apps/phone'), 0)
|
||||
@@ -380,6 +397,10 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
|
||||
function onKeydown(event: KeyboardEvent): void {
|
||||
if (event.key !== 'Escape' || !phone.isOpen) return
|
||||
if (controlCenterOpened.value) {
|
||||
controlCenterOpened.value = false
|
||||
return
|
||||
}
|
||||
phone.close()
|
||||
void nuiCall('close')
|
||||
}
|
||||
@@ -402,6 +423,11 @@ function unlockPhone(): void {
|
||||
}, 720)
|
||||
}
|
||||
|
||||
function toggleControlCenter(): void {
|
||||
if (isLocked.value) return
|
||||
controlCenterOpened.value = !controlCenterOpened.value
|
||||
}
|
||||
|
||||
function unlockCamera(): void {
|
||||
unlockPhone()
|
||||
window.setTimeout(() => void router.push('/apps/camera'), 0)
|
||||
@@ -488,11 +514,13 @@ watch(
|
||||
if (unlockTimer !== undefined) window.clearTimeout(unlockTimer)
|
||||
if (!isOpen) {
|
||||
weather.stop()
|
||||
controlCenterOpened.value = false
|
||||
isLocked.value = false
|
||||
isUnlocking.value = false
|
||||
return
|
||||
}
|
||||
isLocked.value = true
|
||||
controlCenterOpened.value = false
|
||||
weather.start()
|
||||
isUnlocking.value = false
|
||||
phone.setLaunchOrigin(null)
|
||||
@@ -500,6 +528,13 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => phone.cameraLandscape,
|
||||
(landscape) => {
|
||||
if (landscape) controlCenterOpened.value = false
|
||||
},
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
weather.stop()
|
||||
if (clockTicker) clearInterval(clockTicker)
|
||||
@@ -562,13 +597,18 @@ onBeforeUnmount(() => {
|
||||
:dark="phone.isDarkMode"
|
||||
safe-areas
|
||||
class="phone-app"
|
||||
:style="phoneDisplayStyle"
|
||||
:class="{
|
||||
dark: phone.isDarkMode,
|
||||
'phone-app--light': !phone.isDarkMode,
|
||||
'phone-app--unlocking': isUnlocking,
|
||||
}"
|
||||
>
|
||||
<PhoneStatusBar v-if="!isLocked" />
|
||||
<PhoneStatusBar
|
||||
v-if="!isLocked"
|
||||
:control-center-opened="controlCenterOpened"
|
||||
@control-center="toggleControlCenter"
|
||||
/>
|
||||
<SpringboardView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="appTransitionName">
|
||||
@@ -580,6 +620,10 @@ onBeforeUnmount(() => {
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<PhoneHomeIndicator v-if="!isLocked" />
|
||||
<PhoneControlCenter
|
||||
:opened="controlCenterOpened"
|
||||
@close="controlCenterOpened = false"
|
||||
/>
|
||||
<Transition name="lock-screen">
|
||||
<PhoneLockScreen
|
||||
v-if="isLocked"
|
||||
@@ -591,6 +635,7 @@ onBeforeUnmount(() => {
|
||||
:notification="notifications.current"
|
||||
@close="notifications.dismissCurrent()"
|
||||
/>
|
||||
<div class="phone-display-dimmer" aria-hidden="true"></div>
|
||||
</k-app>
|
||||
</div>
|
||||
<img
|
||||
|
||||
@@ -324,6 +324,27 @@ button {
|
||||
.phone-app--light {
|
||||
color-scheme: light;
|
||||
}
|
||||
.phone-display-dimmer {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
opacity: var(--phone-display-dim, 0);
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms linear;
|
||||
}
|
||||
.control-center__range input[type='range']::-webkit-slider-thumb {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
opacity: 0 !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
.control-center__range input[type='range']::-moz-range-thumb {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
border: 0 !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.phone-notification {
|
||||
z-index: 85 !important;
|
||||
top: 68px !important;
|
||||
@@ -339,12 +360,12 @@ button {
|
||||
}
|
||||
.phone-status-bar {
|
||||
position: absolute;
|
||||
z-index: 70;
|
||||
z-index: 97;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
padding: 11px 42px 0;
|
||||
padding: 19px 42px 0;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -355,9 +376,21 @@ button {
|
||||
pointer-events: none;
|
||||
}
|
||||
.phone-status-bar__indicators {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 3px;
|
||||
width: 118px;
|
||||
height: 45px;
|
||||
padding: 11px 42px 0 0;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.phone-home-indicator {
|
||||
position: absolute;
|
||||
@@ -635,6 +668,26 @@ button {
|
||||
}
|
||||
.darkchat-round--large { width: 43px; height: 43px; }
|
||||
.darkchat-round.active { background: var(--dc-purple); border-color: var(--dc-purple); }
|
||||
.darkchat-glass-action {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
color: #f5f5f7;
|
||||
line-height: 0;
|
||||
transition: transform .14s ease;
|
||||
}
|
||||
.darkchat-glass-action--large { width: 48px; height: 48px; }
|
||||
.darkchat-glass-action--pill {
|
||||
width: 68px;
|
||||
border-radius: 22px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
.darkchat-back {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
@@ -3019,9 +3072,7 @@ button {
|
||||
margin-left: auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: #181818;
|
||||
transition: background 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
@@ -3184,10 +3235,7 @@ button {
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid #ededf0;
|
||||
border-radius: 50%;
|
||||
background: rgb(255 255 255 / 96%);
|
||||
box-shadow: 0 7px 25px rgb(0 0 0 / 9%);
|
||||
color: #111;
|
||||
}
|
||||
.messages-edit-toolbar {
|
||||
@@ -4111,9 +4159,7 @@ button {
|
||||
backdrop-filter: blur(30px) saturate(190%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(190%);
|
||||
}
|
||||
.messages-inbox-header > button:not(.messages-inbox-header__edit),
|
||||
.messages-chat-header__back,
|
||||
.messages-inbox-toolbar > button,
|
||||
.messages-messagebar__tools button {
|
||||
border: 1px solid rgb(255 255 255 / 78%);
|
||||
background: linear-gradient(145deg, rgb(255 255 255 / 90%), rgb(236 238 244 / 68%));
|
||||
@@ -4256,9 +4302,7 @@ button {
|
||||
.phone-app.dark .messages-contact-details__fields input {
|
||||
color: #f5f5f7;
|
||||
}
|
||||
.phone-app.dark .messages-inbox-header > button:not(.messages-inbox-header__edit),
|
||||
.phone-app.dark .messages-chat-header__back,
|
||||
.phone-app.dark .messages-inbox-toolbar > button,
|
||||
.phone-app.dark .messages-messagebar__tools button,
|
||||
.phone-app.dark .messages-attachment-menu > button {
|
||||
border-color: rgb(255 255 255 / 10%);
|
||||
@@ -4644,9 +4688,6 @@ button {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--messages-inbox-control-border);
|
||||
background: var(--messages-inbox-control);
|
||||
box-shadow: var(--messages-inbox-control-shadow);
|
||||
color: var(--messages-inbox-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -4769,9 +4810,6 @@ button {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-color: var(--messages-inbox-control-border);
|
||||
background: var(--messages-inbox-control);
|
||||
box-shadow: var(--messages-inbox-control-shadow);
|
||||
color: var(--messages-inbox-primary);
|
||||
}
|
||||
.messages-inbox-page .messages-inbox-toolbar > button svg {
|
||||
@@ -4794,9 +4832,7 @@ button {
|
||||
--messages-inbox-separator: #2c2c2e;
|
||||
}
|
||||
.phone-app.dark .messages-inbox-page .messages-inbox-header,
|
||||
.phone-app.dark .messages-inbox-page .messages-inbox-header > button,
|
||||
.phone-app.dark .messages-inbox-page .messages-inbox-toolbar label,
|
||||
.phone-app.dark .messages-inbox-page .messages-inbox-toolbar > button {
|
||||
.phone-app.dark .messages-inbox-page .messages-inbox-toolbar label {
|
||||
background: var(--messages-inbox-control);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,927 @@
|
||||
<script setup lang="ts">
|
||||
import { kGlass, kRange } from 'konsta/vue'
|
||||
import {
|
||||
BellOff,
|
||||
Bluetooth,
|
||||
Calculator,
|
||||
Camera,
|
||||
Flashlight,
|
||||
LockKeyhole,
|
||||
Moon,
|
||||
Plane,
|
||||
Play,
|
||||
RotateCw,
|
||||
Signal,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
Sun,
|
||||
TimerReset,
|
||||
Volume2,
|
||||
Wifi,
|
||||
} from 'lucide-vue-next'
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
watch,
|
||||
type CSSProperties,
|
||||
} from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
const props = defineProps<{ opened: boolean }>()
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
|
||||
type ConnectivityPreference =
|
||||
| 'airplaneMode'
|
||||
| 'bluetoothEnabled'
|
||||
| 'cellularEnabled'
|
||||
| 'focusMode'
|
||||
| 'rotationLocked'
|
||||
| 'wifiEnabled'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const router = useRouter()
|
||||
const panel = ref<HTMLElement | null>(null)
|
||||
const brightness = ref(phone.preferences.settings.screenBrightness)
|
||||
const volume = ref(
|
||||
Math.round(
|
||||
(phone.preferences.settings.notificationVolume +
|
||||
phone.preferences.settings.ringtoneVolume) /
|
||||
2,
|
||||
),
|
||||
)
|
||||
const flashlightActive = ref(false)
|
||||
const flashlightPending = ref(false)
|
||||
const previousAlertVolume = ref(volume.value || 75)
|
||||
|
||||
const inactiveGlassColors = {
|
||||
bgIos: 'bg-[rgba(72,72,74,0.58)]',
|
||||
shadowIos: 'shadow-ios-dark-glass',
|
||||
}
|
||||
const rotationColors = computed(() =>
|
||||
phone.preferences.settings.rotationLocked
|
||||
? {
|
||||
bgIos: 'bg-white',
|
||||
shadowIos: 'shadow-ios-light-glass',
|
||||
}
|
||||
: inactiveGlassColors,
|
||||
)
|
||||
const focusColors = computed(() =>
|
||||
phone.preferences.settings.focusMode
|
||||
? {
|
||||
bgIos: 'bg-[#5e5ce6]',
|
||||
shadowIos: 'shadow-ios-dark-glass',
|
||||
}
|
||||
: inactiveGlassColors,
|
||||
)
|
||||
const alertsMuted = computed(() => volume.value === 0)
|
||||
const alertMuteColors = computed(() =>
|
||||
alertsMuted.value
|
||||
? {
|
||||
bgIos: 'bg-white',
|
||||
shadowIos: 'shadow-ios-light-glass',
|
||||
}
|
||||
: inactiveGlassColors,
|
||||
)
|
||||
const flashlightColors = computed(() =>
|
||||
flashlightActive.value
|
||||
? {
|
||||
bgIos: 'bg-white',
|
||||
shadowIos: 'shadow-ios-light-glass',
|
||||
}
|
||||
: inactiveGlassColors,
|
||||
)
|
||||
const brightnessStyle = computed<CSSProperties>(() => ({
|
||||
'--control-level': `${brightness.value}%`,
|
||||
}))
|
||||
const volumeStyle = computed<CSSProperties>(() => ({
|
||||
'--control-level': `${volume.value}%`,
|
||||
}))
|
||||
|
||||
function togglePreference(key: ConnectivityPreference): void {
|
||||
phone.setPreference(key, !phone.preferences.settings[key])
|
||||
}
|
||||
|
||||
function updateBrightness(event: Event): void {
|
||||
const nextValue = Number.parseInt(
|
||||
(event.target as HTMLInputElement).value,
|
||||
10,
|
||||
)
|
||||
brightness.value = nextValue
|
||||
phone.preferences.settings.screenBrightness = nextValue
|
||||
}
|
||||
|
||||
function saveBrightness(event: Event): void {
|
||||
phone.setPreference(
|
||||
'screenBrightness',
|
||||
Number.parseInt((event.target as HTMLInputElement).value, 10),
|
||||
)
|
||||
}
|
||||
|
||||
function updateVolume(event: Event): void {
|
||||
volume.value = Number.parseInt((event.target as HTMLInputElement).value, 10)
|
||||
}
|
||||
|
||||
function saveVolume(event: Event): void {
|
||||
const nextValue = Number.parseInt(
|
||||
(event.target as HTMLInputElement).value,
|
||||
10,
|
||||
)
|
||||
if (nextValue > 0) previousAlertVolume.value = nextValue
|
||||
phone.setAlertVolumes(nextValue)
|
||||
}
|
||||
|
||||
function toggleAlertMute(): void {
|
||||
if (alertsMuted.value) {
|
||||
volume.value = previousAlertVolume.value
|
||||
} else {
|
||||
previousAlertVolume.value = volume.value
|
||||
volume.value = 0
|
||||
}
|
||||
phone.setAlertVolumes(volume.value)
|
||||
}
|
||||
|
||||
function pointerRangeValue(
|
||||
event: PointerEvent,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): number {
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const ratio =
|
||||
1 - Math.min(1, Math.max(0, (event.clientY - rect.top) / rect.height))
|
||||
return Math.round(minimum + ratio * (maximum - minimum))
|
||||
}
|
||||
|
||||
function startBrightnessDrag(event: PointerEvent): void {
|
||||
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
|
||||
const nextValue = pointerRangeValue(event, 10, 100)
|
||||
brightness.value = nextValue
|
||||
phone.preferences.settings.screenBrightness = nextValue
|
||||
}
|
||||
|
||||
function dragBrightness(event: PointerEvent): void {
|
||||
if (!(event.currentTarget as HTMLElement).hasPointerCapture(event.pointerId))
|
||||
return
|
||||
const nextValue = pointerRangeValue(event, 10, 100)
|
||||
brightness.value = nextValue
|
||||
phone.preferences.settings.screenBrightness = nextValue
|
||||
}
|
||||
|
||||
function finishBrightnessDrag(event: PointerEvent): void {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
if (!target.hasPointerCapture(event.pointerId)) return
|
||||
const nextValue = pointerRangeValue(event, 10, 100)
|
||||
brightness.value = nextValue
|
||||
phone.setPreference('screenBrightness', nextValue)
|
||||
target.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
|
||||
function startVolumeDrag(event: PointerEvent): void {
|
||||
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
|
||||
volume.value = pointerRangeValue(event, 0, 100)
|
||||
}
|
||||
|
||||
function dragVolume(event: PointerEvent): void {
|
||||
if (!(event.currentTarget as HTMLElement).hasPointerCapture(event.pointerId))
|
||||
return
|
||||
volume.value = pointerRangeValue(event, 0, 100)
|
||||
}
|
||||
|
||||
function finishVolumeDrag(event: PointerEvent): void {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
if (!target.hasPointerCapture(event.pointerId)) return
|
||||
volume.value = pointerRangeValue(event, 0, 100)
|
||||
if (volume.value > 0) previousAlertVolume.value = volume.value
|
||||
phone.setAlertVolumes(volume.value)
|
||||
target.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
|
||||
async function toggleFlashlight(): Promise<void> {
|
||||
if (flashlightPending.value) return
|
||||
const enabled = !flashlightActive.value
|
||||
flashlightActive.value = enabled
|
||||
flashlightPending.value = true
|
||||
const response = await nuiCall('camera:setFlash', { enabled })
|
||||
if (!response.success) flashlightActive.value = !enabled
|
||||
flashlightPending.value = false
|
||||
}
|
||||
|
||||
function openApp(path: string): void {
|
||||
emit('close')
|
||||
void router.push(path)
|
||||
}
|
||||
|
||||
function openTimer(): void {
|
||||
emit('close')
|
||||
void router.push({ path: '/apps/clock', query: { section: 'timer' } })
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.opened,
|
||||
(opened) => {
|
||||
if (!opened) return
|
||||
brightness.value = phone.preferences.settings.screenBrightness
|
||||
volume.value = Math.round(
|
||||
(phone.preferences.settings.notificationVolume +
|
||||
phone.preferences.settings.ringtoneVolume) /
|
||||
2,
|
||||
)
|
||||
if (volume.value > 0) previousAlertVolume.value = volume.value
|
||||
void nextTick(() => panel.value?.focus({ preventScroll: true }))
|
||||
},
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (flashlightActive.value)
|
||||
void nuiCall('camera:setFlash', { enabled: false })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="control-center">
|
||||
<section
|
||||
v-if="opened"
|
||||
class="control-center"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-label="phone.t('ControlCenter.label')"
|
||||
>
|
||||
<button
|
||||
class="control-center__backdrop"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
:aria-label="phone.t('ControlCenter.close')"
|
||||
@click="emit('close')"
|
||||
></button>
|
||||
|
||||
<div ref="panel" class="control-center__panel" tabindex="-1">
|
||||
<div class="control-center__top-grid">
|
||||
<k-glass
|
||||
class="control-center__connectivity"
|
||||
:colors="inactiveGlassColors"
|
||||
:highlight="false"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="control-center__connectivity-button"
|
||||
:class="{
|
||||
'control-center__connectivity-button--airplane':
|
||||
phone.preferences.settings.airplaneMode,
|
||||
}"
|
||||
:aria-label="phone.t('ControlCenter.airplaneMode')"
|
||||
:aria-pressed="phone.preferences.settings.airplaneMode"
|
||||
@click="togglePreference('airplaneMode')"
|
||||
>
|
||||
<Plane aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="control-center__connectivity-button"
|
||||
:class="{
|
||||
'control-center__connectivity-button--cellular':
|
||||
phone.preferences.settings.cellularEnabled,
|
||||
}"
|
||||
:aria-label="phone.t('ControlCenter.cellular')"
|
||||
:aria-pressed="phone.preferences.settings.cellularEnabled"
|
||||
@click="togglePreference('cellularEnabled')"
|
||||
>
|
||||
<Signal aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="control-center__connectivity-button"
|
||||
:class="{
|
||||
'control-center__connectivity-button--wifi':
|
||||
phone.preferences.settings.wifiEnabled,
|
||||
}"
|
||||
:aria-label="phone.t('ControlCenter.wifi')"
|
||||
:aria-pressed="phone.preferences.settings.wifiEnabled"
|
||||
@click="togglePreference('wifiEnabled')"
|
||||
>
|
||||
<Wifi aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="control-center__connectivity-button"
|
||||
:class="{
|
||||
'control-center__connectivity-button--bluetooth':
|
||||
phone.preferences.settings.bluetoothEnabled,
|
||||
}"
|
||||
:aria-label="phone.t('ControlCenter.bluetooth')"
|
||||
:aria-pressed="phone.preferences.settings.bluetoothEnabled"
|
||||
@click="togglePreference('bluetoothEnabled')"
|
||||
>
|
||||
<Bluetooth aria-hidden="true" />
|
||||
</button>
|
||||
</k-glass>
|
||||
|
||||
<k-glass
|
||||
class="control-center__media"
|
||||
:colors="inactiveGlassColors"
|
||||
:highlight="false"
|
||||
>
|
||||
<div class="control-center__media-copy">
|
||||
<span>{{ phone.t('ControlCenter.media') }}</span>
|
||||
<strong>{{ phone.t('ControlCenter.notPlaying') }}</strong>
|
||||
</div>
|
||||
<div class="control-center__media-controls">
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
:aria-label="phone.t('ControlCenter.previous')"
|
||||
>
|
||||
<SkipBack aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
:aria-label="phone.t('ControlCenter.play')"
|
||||
>
|
||||
<Play aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
:aria-label="phone.t('ControlCenter.next')"
|
||||
>
|
||||
<SkipForward aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</k-glass>
|
||||
</div>
|
||||
|
||||
<div class="control-center__middle-grid">
|
||||
<div class="control-center__round-control">
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="control-center__round-button"
|
||||
:class="{
|
||||
'control-center__round-button--light':
|
||||
phone.preferences.settings.rotationLocked,
|
||||
}"
|
||||
:colors="rotationColors"
|
||||
:aria-label="phone.t('ControlCenter.rotationLock')"
|
||||
:aria-pressed="phone.preferences.settings.rotationLocked"
|
||||
@click="togglePreference('rotationLocked')"
|
||||
>
|
||||
<RotateCw aria-hidden="true" />
|
||||
<LockKeyhole
|
||||
class="control-center__rotation-lock"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</k-glass>
|
||||
</div>
|
||||
|
||||
<div class="control-center__round-control">
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="control-center__round-button"
|
||||
:class="{
|
||||
'control-center__round-button--light': alertsMuted,
|
||||
}"
|
||||
:colors="alertMuteColors"
|
||||
:aria-label="
|
||||
phone.t(
|
||||
alertsMuted
|
||||
? 'ControlCenter.unmuteRingtone'
|
||||
: 'ControlCenter.muteRingtone',
|
||||
)
|
||||
"
|
||||
:aria-pressed="alertsMuted"
|
||||
@click="toggleAlertMute"
|
||||
>
|
||||
<BellOff aria-hidden="true" />
|
||||
</k-glass>
|
||||
</div>
|
||||
|
||||
<k-glass
|
||||
class="control-center__slider"
|
||||
:colors="inactiveGlassColors"
|
||||
:highlight="false"
|
||||
:style="brightnessStyle"
|
||||
@pointerdown="startBrightnessDrag"
|
||||
@pointermove="dragBrightness"
|
||||
@pointerup="finishBrightnessDrag"
|
||||
@pointercancel="finishBrightnessDrag"
|
||||
>
|
||||
<span
|
||||
class="control-center__slider-level"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<k-range
|
||||
class="control-center__range"
|
||||
:value="brightness"
|
||||
:min="10"
|
||||
:max="100"
|
||||
:aria-label="phone.t('ControlCenter.brightness')"
|
||||
@input="updateBrightness"
|
||||
@change="saveBrightness"
|
||||
/>
|
||||
<Sun
|
||||
class="control-center__slider-icon"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</k-glass>
|
||||
|
||||
<k-glass
|
||||
class="control-center__slider"
|
||||
:colors="inactiveGlassColors"
|
||||
:highlight="false"
|
||||
:style="volumeStyle"
|
||||
@pointerdown="startVolumeDrag"
|
||||
@pointermove="dragVolume"
|
||||
@pointerup="finishVolumeDrag"
|
||||
@pointercancel="finishVolumeDrag"
|
||||
>
|
||||
<span
|
||||
class="control-center__slider-level"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<k-range
|
||||
class="control-center__range"
|
||||
:value="volume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:aria-label="phone.t('ControlCenter.volume')"
|
||||
@input="updateVolume"
|
||||
@change="saveVolume"
|
||||
/>
|
||||
<Volume2
|
||||
class="control-center__slider-icon"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</k-glass>
|
||||
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="control-center__focus-button"
|
||||
:colors="focusColors"
|
||||
:aria-label="phone.t('ControlCenter.focus')"
|
||||
:aria-pressed="phone.preferences.settings.focusMode"
|
||||
@click="togglePreference('focusMode')"
|
||||
>
|
||||
<Moon aria-hidden="true" />
|
||||
<span>{{ phone.t('ControlCenter.focus') }}</span>
|
||||
</k-glass>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="control-center__quick-actions"
|
||||
:aria-label="phone.t('ControlCenter.quickActions')"
|
||||
>
|
||||
<div class="control-center__quick-action">
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="control-center__quick-button"
|
||||
:class="{
|
||||
'control-center__quick-button--flashlight-active':
|
||||
flashlightActive,
|
||||
}"
|
||||
:colors="flashlightColors"
|
||||
:disabled="flashlightPending"
|
||||
:aria-label="phone.t('ControlCenter.flashlight')"
|
||||
:aria-pressed="flashlightActive"
|
||||
@click="toggleFlashlight"
|
||||
>
|
||||
<Flashlight aria-hidden="true" />
|
||||
</k-glass>
|
||||
<span>{{ phone.t('ControlCenter.flashlight') }}</span>
|
||||
</div>
|
||||
<div class="control-center__quick-action">
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="control-center__quick-button"
|
||||
:colors="inactiveGlassColors"
|
||||
:aria-label="phone.t('ControlCenter.timer')"
|
||||
@click="openTimer"
|
||||
>
|
||||
<TimerReset aria-hidden="true" />
|
||||
</k-glass>
|
||||
<span>{{ phone.t('ControlCenter.timer') }}</span>
|
||||
</div>
|
||||
<div class="control-center__quick-action">
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="control-center__quick-button"
|
||||
:colors="inactiveGlassColors"
|
||||
:aria-label="phone.t('ControlCenter.camera')"
|
||||
@click="openApp('/apps/camera')"
|
||||
>
|
||||
<Camera aria-hidden="true" />
|
||||
</k-glass>
|
||||
<span>{{ phone.t('ControlCenter.camera') }}</span>
|
||||
</div>
|
||||
<div class="control-center__quick-action">
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="control-center__quick-button"
|
||||
:colors="inactiveGlassColors"
|
||||
:aria-label="phone.t('ControlCenter.calculator')"
|
||||
@click="openApp('/apps/calculator')"
|
||||
>
|
||||
<Calculator aria-hidden="true" />
|
||||
</k-glass>
|
||||
<span>{{ phone.t('ControlCenter.calculator') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.control-center {
|
||||
position: absolute;
|
||||
z-index: 95;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.control-center__backdrop {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: rgba(20, 20, 24, 0.34);
|
||||
backdrop-filter: blur(25px) saturate(1.45);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(1.45);
|
||||
}
|
||||
|
||||
.control-center__panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 70px 18px 30px;
|
||||
outline: none;
|
||||
pointer-events: none;
|
||||
transform-origin: calc(100% - 38px) 18px;
|
||||
}
|
||||
|
||||
.control-center__top-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.control-center__connectivity,
|
||||
.control-center__media {
|
||||
min-width: 0;
|
||||
height: 154px;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.control-center__connectivity {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.control-center__connectivity-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: rgba(72, 72, 74, 0.64);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.14),
|
||||
0 2px 5px rgba(0, 0, 0, 0.28);
|
||||
transition:
|
||||
background 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
transform 160ms ease;
|
||||
}
|
||||
|
||||
.control-center__connectivity-button--airplane {
|
||||
background: #ff9f0a;
|
||||
}
|
||||
|
||||
.control-center__connectivity-button--cellular {
|
||||
background: #34c759;
|
||||
}
|
||||
|
||||
.control-center__connectivity-button--wifi,
|
||||
.control-center__connectivity-button--bluetooth {
|
||||
background: #0a84ff;
|
||||
}
|
||||
|
||||
.control-center__connectivity-button:active,
|
||||
.control-center__round-button:active,
|
||||
.control-center__quick-button:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
.control-center__connectivity-button svg {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
stroke-width: 2.3;
|
||||
}
|
||||
|
||||
.control-center__connectivity-button:first-child svg {
|
||||
fill: currentcolor;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.control-center__media {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 21px 18px 17px;
|
||||
}
|
||||
|
||||
.control-center__media-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.control-center__media-copy span {
|
||||
color: rgba(255, 255, 255, 0.66);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control-center__media-copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 17px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.control-center__media-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.control-center__media-controls button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.control-center__media-controls button:nth-child(2) svg {
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
.control-center__media-controls svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
.control-center__middle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(2, 73px);
|
||||
gap: 10px;
|
||||
min-height: 156px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.control-center__round-control,
|
||||
.control-center__quick-action {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.control-center__round-control {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-center__round-control > span,
|
||||
.control-center__quick-action > span {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.control-center__round-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 73px;
|
||||
height: 73px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.control-center__round-button--light {
|
||||
color: #ff453a;
|
||||
}
|
||||
|
||||
.control-center__focus-button {
|
||||
display: flex;
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
min-width: 0;
|
||||
height: 73px;
|
||||
padding: 0 22px;
|
||||
border: 0;
|
||||
border-radius: 37px;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.control-center__focus-button:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.control-center__focus-button svg {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
.control-center__round-button svg,
|
||||
.control-center__quick-button svg {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.control-center__round-button .control-center__rotation-lock {
|
||||
position: absolute;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
.control-center__slider {
|
||||
position: relative;
|
||||
grid-row: 1 / span 2;
|
||||
height: 156px;
|
||||
overflow: hidden;
|
||||
border-radius: 38px;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.control-center__slider-level {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: var(--control-level);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
transition: height 80ms linear;
|
||||
}
|
||||
|
||||
.control-center__range {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 172px;
|
||||
height: 58px !important;
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.control-center__range :deep(span) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.control-center__range :deep(input) {
|
||||
height: 58px !important;
|
||||
}
|
||||
|
||||
.control-center__range :deep(input::-webkit-slider-thumb) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.control-center__range :deep(input::-moz-range-thumb) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.control-center__slider-icon {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
bottom: 18px;
|
||||
left: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
color: #0a84ff;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.control-center__quick-button.control-center__quick-button--flashlight-active {
|
||||
color: #bf5af2 !important;
|
||||
}
|
||||
|
||||
.control-center__slider:nth-child(3) .control-center__slider-icon {
|
||||
color: #ffd60a;
|
||||
}
|
||||
|
||||
.control-center__quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.control-center__quick-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 73px;
|
||||
height: 73px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.control-center__quick-button:disabled {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.control-center-enter-active,
|
||||
.control-center-leave-active {
|
||||
transition: opacity 280ms ease;
|
||||
}
|
||||
|
||||
.control-center-enter-active .control-center__panel {
|
||||
transition: transform 440ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.control-center-leave-active .control-center__panel {
|
||||
transition: transform 260ms cubic-bezier(0.7, 0, 0.84, 0);
|
||||
}
|
||||
|
||||
.control-center-enter-from,
|
||||
.control-center-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.control-center-enter-from .control-center__panel,
|
||||
.control-center-leave-to .control-center__panel {
|
||||
transform: translate3d(28px, -22px, 0) scale(0.84);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.control-center-enter-active,
|
||||
.control-center-leave-active,
|
||||
.control-center-enter-active .control-center__panel,
|
||||
.control-center-leave-active .control-center__panel,
|
||||
.control-center__connectivity-button,
|
||||
.control-center__round-button,
|
||||
.control-center__focus-button,
|
||||
.control-center__quick-button {
|
||||
transition-duration: 1ms;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { BatteryMedium, Signal, Wifi } from 'lucide-vue-next'
|
||||
import { BatteryMedium, Plane, Signal, Wifi } from 'lucide-vue-next'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
withDefaults(defineProps<{ controlCenterOpened?: boolean }>(), {
|
||||
controlCenterOpened: false,
|
||||
})
|
||||
const emit = defineEmits<{ controlCenter: [] }>()
|
||||
const time = ref('')
|
||||
let intervalId: number | undefined
|
||||
|
||||
@@ -27,10 +31,35 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<header class="phone-status-bar" :aria-label="phone.t('Common.phoneStatus')">
|
||||
<time>{{ time }}</time>
|
||||
<div class="phone-status-bar__indicators" aria-hidden="true">
|
||||
<Signal :size="12" :stroke-width="2.5" />
|
||||
<Wifi :size="13" :stroke-width="2.5" />
|
||||
<button
|
||||
class="phone-status-bar__indicators"
|
||||
type="button"
|
||||
:aria-label="phone.t('ControlCenter.open')"
|
||||
:aria-expanded="controlCenterOpened"
|
||||
@click.stop="emit('controlCenter')"
|
||||
>
|
||||
<Plane
|
||||
v-if="phone.preferences.settings.airplaneMode"
|
||||
:size="12"
|
||||
:stroke-width="2.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Signal
|
||||
v-else-if="phone.preferences.settings.cellularEnabled"
|
||||
:size="12"
|
||||
:stroke-width="2.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Wifi
|
||||
v-if="
|
||||
phone.preferences.settings.wifiEnabled &&
|
||||
!phone.preferences.settings.airplaneMode
|
||||
"
|
||||
:size="13"
|
||||
:stroke-width="2.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<BatteryMedium :size="16" :stroke-width="2.4" />
|
||||
</div>
|
||||
</button>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useNotificationsStore,
|
||||
type PhoneNotificationDevice,
|
||||
} from '@/stores/notifications'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import {
|
||||
DEFAULT_PHONE_PREFERENCES,
|
||||
type PhonePreferencesV1,
|
||||
@@ -104,4 +105,26 @@ describe('notifications store', () => {
|
||||
expect(id).toBeNull()
|
||||
expect(notifications.devicePreviews).toEqual([])
|
||||
})
|
||||
|
||||
it('suppresses normal notifications during Focus but keeps critical alerts', () => {
|
||||
const phone = usePhoneStore()
|
||||
const notifications = useNotificationsStore()
|
||||
phone.preferences.settings.focusMode = true
|
||||
|
||||
const normalId = notifications.show({
|
||||
appId: 'mail',
|
||||
text: 'Quiet message',
|
||||
title: 'Mail',
|
||||
})
|
||||
const criticalId = notifications.show({
|
||||
appId: 'clock',
|
||||
critical: true,
|
||||
text: 'Timer finished',
|
||||
title: 'Clock',
|
||||
})
|
||||
|
||||
expect(normalId).toBeNull()
|
||||
expect(criticalId).not.toBeNull()
|
||||
expect(notifications.current?.text).toBe('Timer finished')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,6 +123,7 @@ export const useNotificationsStore = defineStore('notifications', () => {
|
||||
console.error(`[Phone notifications] Unknown app: ${input.appId}`)
|
||||
return null
|
||||
}
|
||||
if (!input.critical && preferences.settings.focusMode) return null
|
||||
if (!input.critical && !appPreferences.enabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1086,6 +1086,31 @@ const defaultLocales: LocaleTree = {
|
||||
},
|
||||
},
|
||||
},
|
||||
ControlCenter: {
|
||||
airplaneMode: 'Airplane Mode',
|
||||
bluetooth: 'Bluetooth',
|
||||
brightness: 'Brightness',
|
||||
calculator: 'Calculator',
|
||||
camera: 'Camera',
|
||||
cellular: 'Cellular Data',
|
||||
close: 'Close Control Center',
|
||||
flashlight: 'Flashlight',
|
||||
focus: 'Focus',
|
||||
label: 'Control Center',
|
||||
media: 'Media',
|
||||
muteRingtone: 'Mute ringtone and notifications',
|
||||
next: 'Next track',
|
||||
notPlaying: 'Not Playing',
|
||||
open: 'Open Control Center',
|
||||
play: 'Play',
|
||||
previous: 'Previous track',
|
||||
quickActions: 'Quick actions',
|
||||
rotationLock: 'Rotation Lock',
|
||||
timer: 'Timer',
|
||||
unmuteRingtone: 'Unmute ringtone and notifications',
|
||||
volume: 'Volume',
|
||||
wifi: 'Wi-Fi',
|
||||
},
|
||||
Common: {
|
||||
add: 'Add',
|
||||
cancel: 'Cancel',
|
||||
@@ -1245,6 +1270,12 @@ export const usePhoneStore = defineStore('phone', {
|
||||
this.preferences.settings[key] = value
|
||||
this.saveDeviceNamespace('settings', this.preferences)
|
||||
},
|
||||
setAlertVolumes(value: number): void {
|
||||
const volume = Math.min(100, Math.max(0, Math.round(value)))
|
||||
this.preferences.settings.notificationVolume = volume
|
||||
this.preferences.settings.ringtoneVolume = volume
|
||||
this.saveDeviceNamespace('settings', this.preferences)
|
||||
},
|
||||
setSystemDarkMode(value: boolean): void {
|
||||
this.systemDarkMode = value
|
||||
},
|
||||
|
||||
@@ -13,6 +13,9 @@ describe('preferences', () => {
|
||||
version: 1,
|
||||
settings: {
|
||||
appearanceMode: 'light',
|
||||
bluetoothEnabled: false,
|
||||
cellularEnabled: false,
|
||||
focusMode: true,
|
||||
notificationVolume: 45,
|
||||
notificationDurationSeconds: 14,
|
||||
notifications: {
|
||||
@@ -20,11 +23,17 @@ describe('preferences', () => {
|
||||
clock: { enabled: false, sounds: false },
|
||||
},
|
||||
phoneScale: 110,
|
||||
rotationLocked: true,
|
||||
screenBrightness: 64,
|
||||
wallpaper: 'ember',
|
||||
wifiEnabled: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(value.settings.appearanceMode).toBe('light')
|
||||
expect(value.settings.bluetoothEnabled).toBe(false)
|
||||
expect(value.settings.cellularEnabled).toBe(false)
|
||||
expect(value.settings.focusMode).toBe(true)
|
||||
expect(value.settings.notificationVolume).toBe(45)
|
||||
expect(value.settings.notificationDurationSeconds).toBe(14)
|
||||
expect(value.settings.notifications.messages).toEqual({
|
||||
@@ -40,7 +49,10 @@ describe('preferences', () => {
|
||||
sounds: true,
|
||||
})
|
||||
expect(value.settings.phoneScale).toBe(110)
|
||||
expect(value.settings.rotationLocked).toBe(true)
|
||||
expect(value.settings.screenBrightness).toBe(64)
|
||||
expect(value.settings.wallpaper).toBe('ember')
|
||||
expect(value.settings.wifiEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it('clamps ranges and rejects unknown choices', () => {
|
||||
@@ -54,6 +66,7 @@ describe('preferences', () => {
|
||||
notificationDurationSeconds: 100,
|
||||
phoneScale: 500,
|
||||
ringtoneVolume: 120,
|
||||
screenBrightness: -20,
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -64,5 +77,22 @@ describe('preferences', () => {
|
||||
expect(value.settings.notificationDurationSeconds).toBe(30)
|
||||
expect(value.settings.phoneScale).toBe(150)
|
||||
expect(value.settings.ringtoneVolume).toBe(100)
|
||||
expect(value.settings.screenBrightness).toBe(10)
|
||||
})
|
||||
|
||||
it('adds safe control center defaults to legacy version one preferences', () => {
|
||||
const value = parsePhonePreferences(
|
||||
JSON.stringify({ version: 1, settings: { airplaneMode: true } }),
|
||||
)
|
||||
|
||||
expect(value.settings).toMatchObject({
|
||||
airplaneMode: true,
|
||||
bluetoothEnabled: true,
|
||||
cellularEnabled: true,
|
||||
focusMode: false,
|
||||
rotationLocked: false,
|
||||
screenBrightness: 100,
|
||||
wifiEnabled: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,6 +30,9 @@ export type PhonePreferencesV1 = {
|
||||
settings: {
|
||||
airplaneMode: boolean
|
||||
appearanceMode: AppearanceMode
|
||||
bluetoothEnabled: boolean
|
||||
cellularEnabled: boolean
|
||||
focusMode: boolean
|
||||
frame: PhoneFrameId
|
||||
notificationSound: NotificationSoundId
|
||||
notificationDurationSeconds: number
|
||||
@@ -38,8 +41,11 @@ export type PhonePreferencesV1 = {
|
||||
phoneScale: number
|
||||
ringtone: RingtoneId
|
||||
ringtoneVolume: number
|
||||
rotationLocked: boolean
|
||||
screenBrightness: number
|
||||
streamerMode: boolean
|
||||
wallpaper: WallpaperId
|
||||
wifiEnabled: boolean
|
||||
}
|
||||
version: 1
|
||||
}
|
||||
@@ -79,6 +85,9 @@ export const DEFAULT_PHONE_PREFERENCES: PhonePreferencesV1 = {
|
||||
settings: {
|
||||
airplaneMode: false,
|
||||
appearanceMode: 'automatic',
|
||||
bluetoothEnabled: true,
|
||||
cellularEnabled: true,
|
||||
focusMode: false,
|
||||
frame: 'black',
|
||||
notificationSound: 'chime',
|
||||
notificationDurationSeconds: 10,
|
||||
@@ -87,8 +96,11 @@ export const DEFAULT_PHONE_PREFERENCES: PhonePreferencesV1 = {
|
||||
phoneScale: 100,
|
||||
ringtone: 'skyline',
|
||||
ringtoneVolume: 80,
|
||||
rotationLocked: false,
|
||||
screenBrightness: 100,
|
||||
streamerMode: false,
|
||||
wallpaper: 'midnight',
|
||||
wifiEnabled: true,
|
||||
},
|
||||
version: 1,
|
||||
}
|
||||
@@ -164,6 +176,15 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
|
||||
APPEARANCE_MODE_IDS,
|
||||
defaults.appearanceMode,
|
||||
),
|
||||
bluetoothEnabled: readBoolean(
|
||||
settings.bluetoothEnabled,
|
||||
defaults.bluetoothEnabled,
|
||||
),
|
||||
cellularEnabled: readBoolean(
|
||||
settings.cellularEnabled,
|
||||
defaults.cellularEnabled,
|
||||
),
|
||||
focusMode: readBoolean(settings.focusMode, defaults.focusMode),
|
||||
frame: readChoice(settings.frame, PHONE_FRAME_IDS, defaults.frame),
|
||||
notificationSound: readChoice(
|
||||
settings.notificationSound,
|
||||
@@ -200,12 +221,23 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
|
||||
0,
|
||||
100,
|
||||
),
|
||||
rotationLocked: readBoolean(
|
||||
settings.rotationLocked,
|
||||
defaults.rotationLocked,
|
||||
),
|
||||
screenBrightness: readNumber(
|
||||
settings.screenBrightness,
|
||||
defaults.screenBrightness,
|
||||
10,
|
||||
100,
|
||||
),
|
||||
streamerMode: readBoolean(settings.streamerMode, defaults.streamerMode),
|
||||
wallpaper: readChoice(
|
||||
settings.wallpaper,
|
||||
WALLPAPER_IDS,
|
||||
defaults.wallpaper,
|
||||
),
|
||||
wifiEnabled: readBoolean(settings.wifiEnabled, defaults.wifiEnabled),
|
||||
},
|
||||
version: 1,
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
TimerReset,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import AlarmEditor from '@/components/AlarmEditor.vue'
|
||||
import AlarmSoundMenu from '@/components/AlarmSoundMenu.vue'
|
||||
@@ -41,7 +42,10 @@ import {
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const clock = useClockStore()
|
||||
const tab = ref<'world' | 'alarm' | 'stopwatch' | 'timer'>('world')
|
||||
const route = useRoute()
|
||||
const tab = ref<'world' | 'alarm' | 'stopwatch' | 'timer'>(
|
||||
route.query.section === 'timer' ? 'timer' : 'world',
|
||||
)
|
||||
const alarmEditor = ref<
|
||||
{ mode: 'create' } | { id: string; mode: 'edit' } | null
|
||||
>(null)
|
||||
@@ -202,6 +206,13 @@ watch([() => clock.stopwatchStartedAt, tab], ([startedAt, activeTab]) => {
|
||||
: requestAnimationFrame(updateStopwatchFrame)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.section,
|
||||
(section) => {
|
||||
if (section === 'timer') tab.value = 'timer'
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
ticker = setInterval(() => {
|
||||
now.value = Date.now()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { kGlass } from 'konsta/vue'
|
||||
import {
|
||||
ArrowUpCircle,
|
||||
Bell,
|
||||
@@ -48,6 +49,10 @@ import { parseDatabaseDate, type DatabaseDateValue } from '@/utils/date'
|
||||
const VOICE_MAX_DURATION_MS = 60_000
|
||||
const VOICE_MAX_BYTES = 270_000
|
||||
const WAVEFORM_SAMPLES = 48
|
||||
const darkGlassColors = {
|
||||
bgIos: 'bg-ios-dark-glass',
|
||||
shadowIos: 'shadow-ios-dark-glass',
|
||||
}
|
||||
|
||||
const account = useAccountStore()
|
||||
const darkchat = useDarkChatStore()
|
||||
@@ -632,11 +637,11 @@ onBeforeUnmount(() => {
|
||||
<template v-else-if="darkchat.profile">
|
||||
<section v-if="screen === 'inbox'" class="darkchat-inbox">
|
||||
<header class="darkchat-inbox__header">
|
||||
<button type="button" class="darkchat-pill" @click="openProfile">{{ phone.t('Common.edit') }}</button>
|
||||
<k-glass component="button" type="button" class="darkchat-glass-action darkchat-glass-action--pill" :colors="darkGlassColors" @click="openProfile">{{ phone.t('Common.edit') }}</k-glass>
|
||||
<strong>{{ t('name') }}</strong>
|
||||
<button type="button" class="darkchat-round" :aria-label="t('security')" @click="openProfile">
|
||||
<k-glass component="button" type="button" class="darkchat-glass-action" :colors="darkGlassColors" :aria-label="t('security')" @click="openProfile">
|
||||
<ShieldCheck :size="19" />
|
||||
</button>
|
||||
</k-glass>
|
||||
</header>
|
||||
<div class="darkchat-security-strip">
|
||||
<LockKeyhole :size="12" />
|
||||
@@ -669,9 +674,9 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<footer class="darkchat-inbox__toolbar">
|
||||
<label><Search :size="18" /><input v-model="search" type="search" :placeholder="phone.t('Common.search')" /></label>
|
||||
<button type="button" class="darkchat-round darkchat-round--large" :aria-label="t('newChat')" @click="screen = 'new'">
|
||||
<k-glass component="button" type="button" class="darkchat-glass-action darkchat-glass-action--large" :colors="darkGlassColors" :aria-label="t('newChat')" @click="screen = 'new'">
|
||||
<MessageCirclePlus :size="20" />
|
||||
</button>
|
||||
</k-glass>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
kBlock,
|
||||
kGlass,
|
||||
kLink,
|
||||
kList,
|
||||
kListInput,
|
||||
@@ -705,13 +706,14 @@ onBeforeUnmount(() => {
|
||||
:aria-label="phone.t('Apps.messages.name')"
|
||||
>
|
||||
<header class="messages-inbox-header">
|
||||
<button
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
class="messages-inbox-header__edit"
|
||||
@click="toggleListEditing"
|
||||
>
|
||||
<span>{{ phone.t(editingList ? 'Common.done' : 'Common.edit') }}</span>
|
||||
</button>
|
||||
</k-glass>
|
||||
<strong>
|
||||
{{
|
||||
editingList
|
||||
@@ -721,17 +723,19 @@ onBeforeUnmount(() => {
|
||||
: phone.t('Apps.messages.name')
|
||||
}}
|
||||
</strong>
|
||||
<button
|
||||
<k-glass
|
||||
v-if="!editingList"
|
||||
component="button"
|
||||
type="button"
|
||||
:class="{ active: showUnreadOnly }"
|
||||
:aria-label="phone.t('Apps.messages.filterUnread')"
|
||||
@click="showUnreadOnly = !showUnreadOnly"
|
||||
>
|
||||
<ListFilter :size="24" :stroke-width="2.25" />
|
||||
</button>
|
||||
<button
|
||||
</k-glass>
|
||||
<k-glass
|
||||
v-else
|
||||
component="button"
|
||||
type="button"
|
||||
class="messages-inbox-header__delete"
|
||||
:disabled="!selectedNumbers.length"
|
||||
@@ -739,7 +743,7 @@ onBeforeUnmount(() => {
|
||||
@click="deleteSelectedConversations"
|
||||
>
|
||||
<Trash2 :size="18" />
|
||||
</button>
|
||||
</k-glass>
|
||||
</header>
|
||||
|
||||
<div
|
||||
@@ -829,13 +833,14 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
<Mic :size="16" />
|
||||
</label>
|
||||
<button
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.messages.compose')"
|
||||
@click="beginCompose"
|
||||
>
|
||||
<SquarePen :size="20" />
|
||||
</button>
|
||||
</k-glass>
|
||||
</footer>
|
||||
<footer v-else class="messages-edit-toolbar">
|
||||
<span>{{ phone.t('Apps.messages.selectedCount', { count: String(selectedNumbers.length) }) }}</span>
|
||||
|
||||
@@ -19,6 +19,13 @@ Locales["en"] = {
|
||||
phone = "Phone", phoneStatus = "Phone status", reset = "Reset",
|
||||
save = "Save", search = "Search", send = "Send", start = "Start", stop = "Stop",
|
||||
},
|
||||
ControlCenter = {
|
||||
airplaneMode = "Airplane Mode", bluetooth = "Bluetooth", brightness = "Brightness", calculator = "Calculator",
|
||||
camera = "Camera", cellular = "Cellular Data", close = "Close Control Center", flashlight = "Flashlight",
|
||||
focus = "Focus", label = "Control Center", media = "Media", muteRingtone = "Mute ringtone and notifications", next = "Next track", notPlaying = "Not Playing",
|
||||
open = "Open Control Center", play = "Play", previous = "Previous track", quickActions = "Quick actions",
|
||||
rotationLock = "Rotation Lock", timer = "Timer", unmuteRingtone = "Unmute ringtone and notifications", volume = "Volume", wifi = "Wi-Fi",
|
||||
},
|
||||
Notifications = { now = "now" },
|
||||
LockScreen = {
|
||||
label = "Lock Screen", flashlight = "Flashlight", camera = "Camera", swipeUp = "Swipe up to open",
|
||||
|
||||
@@ -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-F_IMmGnH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-Sk51e7nJ.css">
|
||||
<script type="module" crossorigin src="./assets/sky-index-qSN2Wmy5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-DnOTtSzB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user