mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
ADD - activate phone hardware controls
Overlay accessible hit targets on the rendered phone frame for mute, volume, and power controls. Add the compact iOS volume HUD, localized labels, and full passcode locking when the device is explicitly locked.
This commit is contained in:
+135
-1
@@ -330,6 +330,8 @@ const passcodeResetKey = ref(0)
|
||||
const passcodeRetrySeconds = ref(0)
|
||||
const passcodeVisible = ref(false)
|
||||
const passcodeRequired = ref(false)
|
||||
const previousHardwareAlertVolume = ref(75)
|
||||
const hardwareVolumeHudVisible = ref(false)
|
||||
const setupPreviewDismissed = ref(false)
|
||||
const setupDevelopmentSkipped = ref(false)
|
||||
const pendingUnlockRoute = ref<string | null>(null)
|
||||
@@ -345,6 +347,19 @@ const setupRequired = computed(
|
||||
developmentParameters.has('setupPreview') &&
|
||||
!setupPreviewDismissed.value)),
|
||||
)
|
||||
const hardwareAlertVolume = computed(() =>
|
||||
Math.round(
|
||||
(phone.preferences.settings.notificationVolume +
|
||||
phone.preferences.settings.ringtoneVolume) /
|
||||
2,
|
||||
),
|
||||
)
|
||||
const hardwareVolumeHudStyle = computed<CSSProperties>(
|
||||
() =>
|
||||
({
|
||||
'--phone-hardware-volume-fill': `${32 + hardwareAlertVolume.value * 0.76}px`,
|
||||
}) as CSSProperties,
|
||||
)
|
||||
const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const viewportScale = ref(getViewportScale())
|
||||
const browserDevicePixelRatio = ref(window.devicePixelRatio || 1)
|
||||
@@ -400,6 +415,7 @@ let companiesChangeTimer: number | undefined
|
||||
let pendingCompaniesChange: CompanyChangedPayload | null = null
|
||||
let unlockTimer: number | undefined
|
||||
let passcodeLockTimer: number | undefined
|
||||
let hardwareVolumeHudTimer: number | undefined
|
||||
let unlockedServicesIdle: number | undefined
|
||||
let phoneClosePending = false
|
||||
let simPickerClosePending = false
|
||||
@@ -1264,11 +1280,51 @@ function lockPhone(): void {
|
||||
passcodeVisible.value = false
|
||||
passcodeBusy.value = false
|
||||
passcodeError.value = ''
|
||||
passcodeRequired.value = false
|
||||
passcodeRequired.value = phone.security.enabled
|
||||
pendingUnlockRoute.value = null
|
||||
isLocked.value = true
|
||||
}
|
||||
|
||||
function showHardwareVolumeHud(): void {
|
||||
hardwareVolumeHudVisible.value = true
|
||||
if (hardwareVolumeHudTimer !== undefined) {
|
||||
window.clearTimeout(hardwareVolumeHudTimer)
|
||||
}
|
||||
hardwareVolumeHudTimer = window.setTimeout(() => {
|
||||
hardwareVolumeHudVisible.value = false
|
||||
hardwareVolumeHudTimer = undefined
|
||||
}, 1300)
|
||||
}
|
||||
|
||||
function toggleHardwareLock(): void {
|
||||
if (setupRequired.value) return
|
||||
if (isLocked.value) {
|
||||
unlockPhone()
|
||||
return
|
||||
}
|
||||
lockPhone()
|
||||
}
|
||||
|
||||
function changeHardwareAlertVolume(delta: number): void {
|
||||
if (setupRequired.value) return
|
||||
const volume = Math.min(100, Math.max(0, hardwareAlertVolume.value + delta))
|
||||
if (volume > 0) previousHardwareAlertVolume.value = volume
|
||||
phone.setAlertVolumes(volume)
|
||||
showHardwareVolumeHud()
|
||||
}
|
||||
|
||||
function toggleHardwareAlertMute(): void {
|
||||
if (setupRequired.value) return
|
||||
if (hardwareAlertVolume.value > 0) {
|
||||
previousHardwareAlertVolume.value = hardwareAlertVolume.value
|
||||
phone.setAlertVolumes(0)
|
||||
showHardwareVolumeHud()
|
||||
return
|
||||
}
|
||||
phone.setAlertVolumes(previousHardwareAlertVolume.value)
|
||||
showHardwareVolumeHud()
|
||||
}
|
||||
|
||||
function returnToActiveCall(): void {
|
||||
if (!calls.activeCall) return
|
||||
controlCenterOpened.value = false
|
||||
@@ -1394,6 +1450,11 @@ watch(
|
||||
passcodeBusy.value = false
|
||||
passcodeError.value = ''
|
||||
passcodeRequired.value = false
|
||||
hardwareVolumeHudVisible.value = false
|
||||
if (hardwareVolumeHudTimer !== undefined) {
|
||||
window.clearTimeout(hardwareVolumeHudTimer)
|
||||
hardwareVolumeHudTimer = undefined
|
||||
}
|
||||
pendingUnlockRoute.value = null
|
||||
unlockedServicesLoaded.value = false
|
||||
if (passcodeLockTimer !== undefined) {
|
||||
@@ -1443,6 +1504,9 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
if (unlockTimer !== undefined) window.clearTimeout(unlockTimer)
|
||||
if (passcodeLockTimer !== undefined) window.clearInterval(passcodeLockTimer)
|
||||
if (hardwareVolumeHudTimer !== undefined) {
|
||||
window.clearTimeout(hardwareVolumeHudTimer)
|
||||
}
|
||||
window.removeEventListener('message', onMessage)
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
window.removeEventListener('resize', updateViewportScale)
|
||||
@@ -1503,6 +1567,45 @@ onBeforeUnmount(() => {
|
||||
}"
|
||||
:aria-label="phone.t('Common.phone')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="phone-hardware-button phone-hardware-button--action"
|
||||
:aria-label="
|
||||
phone.t(
|
||||
hardwareAlertVolume > 0
|
||||
? 'HardwareButtons.mute'
|
||||
: 'HardwareButtons.unmute',
|
||||
)
|
||||
"
|
||||
:aria-pressed="hardwareAlertVolume === 0"
|
||||
:disabled="setupRequired"
|
||||
@click="toggleHardwareAlertMute"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
class="phone-hardware-button phone-hardware-button--volume-up"
|
||||
:aria-label="phone.t('HardwareButtons.volumeUp')"
|
||||
:disabled="setupRequired"
|
||||
@click="changeHardwareAlertVolume(10)"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
class="phone-hardware-button phone-hardware-button--volume-down"
|
||||
:aria-label="phone.t('HardwareButtons.volumeDown')"
|
||||
:disabled="setupRequired"
|
||||
@click="changeHardwareAlertVolume(-10)"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
class="phone-hardware-button phone-hardware-button--power"
|
||||
:aria-label="
|
||||
phone.t(
|
||||
isLocked ? 'HardwareButtons.unlock' : 'HardwareButtons.lock',
|
||||
)
|
||||
"
|
||||
:disabled="setupRequired"
|
||||
@click="toggleHardwareLock"
|
||||
></button>
|
||||
<div
|
||||
class="phone-screen"
|
||||
:class="{
|
||||
@@ -1511,6 +1614,37 @@ onBeforeUnmount(() => {
|
||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
>
|
||||
<Transition name="phone-volume-hud">
|
||||
<div
|
||||
v-if="hardwareVolumeHudVisible"
|
||||
class="phone-volume-hud"
|
||||
:style="hardwareVolumeHudStyle"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
:aria-label="
|
||||
phone.t('HardwareButtons.volumeLevel', {
|
||||
value: String(hardwareAlertVolume),
|
||||
})
|
||||
"
|
||||
>
|
||||
<svg
|
||||
class="phone-volume-hud__icon"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M4 9v6h4l5 4V5L8 9H4Z" />
|
||||
<path
|
||||
v-if="hardwareAlertVolume > 0"
|
||||
d="M16 8.2c1.1 1 1.7 2.2 1.7 3.8s-.6 2.8-1.7 3.8"
|
||||
/>
|
||||
<path v-else d="m16.2 9.2 4.6 4.6m0-4.6-4.6 4.6" />
|
||||
</svg>
|
||||
<span
|
||||
class="phone-volume-hud__level"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</div>
|
||||
</Transition>
|
||||
<k-app
|
||||
theme="ios"
|
||||
:dark="phone.isDarkMode"
|
||||
|
||||
@@ -3,15 +3,15 @@ import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(new URL('./App.vue', import.meta.url), 'utf8')
|
||||
const styles = readFileSync(
|
||||
new URL('./assets/main.css', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('browser development preview contract', () => {
|
||||
it('starts unlocked while preserving an explicit lock screen preview', () => {
|
||||
expect(source).toContain(
|
||||
"developmentParameters.has('lockScreenPreview')",
|
||||
)
|
||||
expect(source).toContain(
|
||||
': !isDevelopment || developmentLockScreenPreview',
|
||||
)
|
||||
expect(source).toContain("developmentParameters.has('lockScreenPreview')")
|
||||
expect(source).toContain(': !isDevelopment || developmentLockScreenPreview')
|
||||
expect(source).toContain("developmentParameters.has('setupPreview')")
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('browser development preview contract', () => {
|
||||
expect(source).toContain('else loadUnlockedPhoneData()')
|
||||
})
|
||||
|
||||
it('requires the passcode only for the first unlock of a device session', () => {
|
||||
it('requires the passcode again after a full device lock', () => {
|
||||
expect(source).toContain('const passcodeRequired = ref(false)')
|
||||
expect(source).toContain(
|
||||
'if (phone.security.enabled && passcodeRequired.value)',
|
||||
@@ -31,7 +31,7 @@ describe('browser development preview contract', () => {
|
||||
'passcodeRequired.value = isLocked.value && phone.security.enabled',
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/function lockPhone\(\): void \{[\s\S]*?passcodeRequired\.value = false[\s\S]*?isLocked\.value = true/,
|
||||
/function lockPhone\(\): void \{[\s\S]*?passcodeRequired\.value = phone\.security\.enabled[\s\S]*?isLocked\.value = true/,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -41,4 +41,24 @@ describe('browser development preview contract', () => {
|
||||
)
|
||||
expect(source).toContain(':device-pixel-ratio="browserDevicePixelRatio"')
|
||||
})
|
||||
|
||||
it('maps the visible device side controls to phone actions', () => {
|
||||
expect(source).toContain('@click="toggleHardwareAlertMute"')
|
||||
expect(source).toContain('@click="changeHardwareAlertVolume(10)"')
|
||||
expect(source).toContain('@click="changeHardwareAlertVolume(-10)"')
|
||||
expect(source).toContain('@click="toggleHardwareLock"')
|
||||
expect(source).toMatch(
|
||||
/function toggleHardwareLock\(\): void \{[\s\S]*?unlockPhone\(\)[\s\S]*?lockPhone\(\)/,
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/function changeHardwareAlertVolume\(delta: number\): void \{[\s\S]*?phone\.setAlertVolumes\(volume\)/,
|
||||
)
|
||||
expect(styles).toMatch(
|
||||
/\.phone-hardware-button\s*\{[\s\S]*?position:\s*absolute;[\s\S]*?z-index:\s*101;/,
|
||||
)
|
||||
expect(styles).toContain('.phone-hardware-button--power')
|
||||
expect(source).toContain('class="phone-volume-hud"')
|
||||
expect(source).toContain('showHardwareVolumeHud()')
|
||||
expect(styles).toContain('.phone-volume-hud__level')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -322,6 +322,100 @@ button {
|
||||
object-fit: fill;
|
||||
pointer-events: none;
|
||||
}
|
||||
.phone-hardware-button {
|
||||
position: absolute;
|
||||
z-index: 101;
|
||||
width: 26px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.phone-hardware-button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.phone-hardware-button:focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: -6px;
|
||||
}
|
||||
.phone-hardware-button--action {
|
||||
top: 176px;
|
||||
left: -7px;
|
||||
height: 44px;
|
||||
}
|
||||
.phone-hardware-button--volume-up {
|
||||
top: 238px;
|
||||
left: -7px;
|
||||
height: 74px;
|
||||
}
|
||||
.phone-hardware-button--volume-down {
|
||||
top: 315px;
|
||||
left: -7px;
|
||||
height: 74px;
|
||||
}
|
||||
.phone-hardware-button--power {
|
||||
top: 252px;
|
||||
right: -7px;
|
||||
height: 62px;
|
||||
}
|
||||
.phone-volume-hud {
|
||||
position: absolute;
|
||||
z-index: 220;
|
||||
top: 188px;
|
||||
left: 6px;
|
||||
width: 30px;
|
||||
height: 108px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(255 255 255 / 16%);
|
||||
border-radius: 15px;
|
||||
color: #0a84ff;
|
||||
background: rgb(47 47 51 / 98%);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 28%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.phone-volume-hud__icon {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.phone-volume-hud__level {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: var(--phone-hardware-volume-fill);
|
||||
background: #fff;
|
||||
transition: height 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.phone-volume-hud-enter-active,
|
||||
.phone-volume-hud-leave-active {
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.phone-volume-hud-enter-from,
|
||||
.phone-volume-hud-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px) scale(0.92);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.phone-volume-hud,
|
||||
.phone-volume-hud__level {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.phone-screen {
|
||||
position: relative;
|
||||
container-type: size;
|
||||
|
||||
@@ -4533,6 +4533,15 @@ const defaultLocales: LocaleTree = {
|
||||
rateLimited: 'Too many attempts. Please wait.',
|
||||
},
|
||||
},
|
||||
HardwareButtons: {
|
||||
lock: 'Lock phone',
|
||||
mute: 'Mute ringtone and notifications',
|
||||
unlock: 'Unlock phone',
|
||||
unmute: 'Unmute ringtone and notifications',
|
||||
volumeDown: 'Volume down',
|
||||
volumeLevel: 'Volume {value} percent',
|
||||
volumeUp: 'Volume up',
|
||||
},
|
||||
Home: {
|
||||
appLibrary: 'App Library',
|
||||
appLibrarySearch: 'Search apps',
|
||||
|
||||
@@ -411,6 +411,11 @@ merge(german, {
|
||||
rateLimited = "Zu viele Versuche. Bitte warte.",
|
||||
},
|
||||
},
|
||||
HardwareButtons = {
|
||||
lock = "Handy sperren", mute = "Klingelton und Mitteilungen stummschalten", unlock = "Handy entsperren",
|
||||
unmute = "Klingelton und Mitteilungen einschalten", volumeDown = "Leiser",
|
||||
volumeLevel = "Lautstärke {value} Prozent", volumeUp = "Lauter",
|
||||
},
|
||||
Home = {
|
||||
appLibrary = "App-Mediathek", appLibrarySearch = "Apps suchen", allApps = "Alle Apps",
|
||||
apps = "Apps", dock = "Dock", noApps = "Keine Apps gefunden",
|
||||
|
||||
@@ -155,6 +155,11 @@ Locales["en"] = {
|
||||
tryAgain = "Try again in {seconds} seconds", rateLimited = "Too many attempts. Please wait.",
|
||||
},
|
||||
},
|
||||
HardwareButtons = {
|
||||
lock = "Lock phone", mute = "Mute ringtone and notifications", unlock = "Unlock phone",
|
||||
unmute = "Unmute ringtone and notifications", volumeDown = "Volume down",
|
||||
volumeLevel = "Volume {value} percent", volumeUp = "Volume up",
|
||||
},
|
||||
Home = {
|
||||
appLibrary = "App Library", appLibrarySearch = "Search apps", allApps = "All Apps", apps = "Apps",
|
||||
dock = "Dock", noApps = "No apps found", removeApp = "Remove {app} from Home Screen", addToHome = "Add {app} to Home Screen", addPage = "Add Home Screen page", deletePage = "Delete current Home Screen page", removedFromHome = "Removed from Home Screen", page = "Page", pages = "Home screen pages",
|
||||
|
||||
Reference in New Issue
Block a user