mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-30 01:38:55 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4879d4569b | |||
| 5b5e9fe914 | |||
| daa67cfb1e | |||
| 3eae2b8d30 |
@@ -249,6 +249,11 @@ When enabled, Sky Phone prints debug and informational messages. Warnings and er
|
|||||||
|
|
||||||
The short LB Phone detection notice also remains visible when debug mode is disabled.
|
The short LB Phone detection notice also remains visible when debug mode is disabled.
|
||||||
|
|
||||||
|
On every resource start, Sky Phone compares the `version` in `fxmanifest.lua` with the tag of the
|
||||||
|
latest published [GitHub release](https://github.com/sky-systems/sky_phone/releases/latest). The
|
||||||
|
server console reports whether the installed version is current and shows the release link when an
|
||||||
|
update is available. A failed GitHub request is reported but does not prevent the phone from starting.
|
||||||
|
|
||||||
## Security values
|
## Security values
|
||||||
|
|
||||||
Sky Phone ships with stable generated defaults in `Config.Server`:
|
Sky Phone ships with stable generated defaults in `Config.Server`:
|
||||||
|
|||||||
+47
-7
@@ -77,6 +77,7 @@ import { nuiCall } from '@/utils/nui'
|
|||||||
import { formatTimer } from '@/utils/clock'
|
import { formatTimer } from '@/utils/clock'
|
||||||
import { parsePhonePreferences } from '@/utils/preferences'
|
import { parsePhonePreferences } from '@/utils/preferences'
|
||||||
import { getHairlinePixelStyle } from '@/utils/rendering'
|
import { getHairlinePixelStyle } from '@/utils/rendering'
|
||||||
|
import { isTextInputElement } from '@/utils/textInputFocus'
|
||||||
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
|
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
|
||||||
import SpringboardView from '@/views/SpringboardView.vue'
|
import SpringboardView from '@/views/SpringboardView.vue'
|
||||||
|
|
||||||
@@ -271,12 +272,14 @@ const MIN_PRODUCTION_PHONE_ZOOM = 260 / PHONE_PORTRAIT_WIDTH
|
|||||||
const developmentParameters = new URLSearchParams(window.location.search)
|
const developmentParameters = new URLSearchParams(window.location.search)
|
||||||
const isBrowserPreview =
|
const isBrowserPreview =
|
||||||
developmentParameters.has('browserPreview') &&
|
developmentParameters.has('browserPreview') &&
|
||||||
developmentParameters.get('apiBase')?.startsWith('/') === true
|
(import.meta.env.DEV ||
|
||||||
|
developmentParameters.get('apiBase')?.startsWith('/') === true)
|
||||||
const isDevelopment =
|
const isDevelopment =
|
||||||
import.meta.env.DEV ||
|
import.meta.env.DEV ||
|
||||||
developmentParameters.get('apiBase')?.startsWith('/') === true
|
developmentParameters.get('apiBase')?.startsWith('/') === true
|
||||||
const developmentLockScreenPreview =
|
const developmentLockScreenPreview =
|
||||||
isDevelopment && developmentParameters.has('lockScreenPreview')
|
isDevelopment && developmentParameters.has('lockScreenPreview')
|
||||||
|
let textInputFocused = false
|
||||||
|
|
||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const account = useAccountStore()
|
const account = useAccountStore()
|
||||||
@@ -346,6 +349,7 @@ const previousHardwareAlertVolume = ref(75)
|
|||||||
const hardwareVolumeHudVisible = ref(false)
|
const hardwareVolumeHudVisible = ref(false)
|
||||||
const setupPreviewDismissed = ref(false)
|
const setupPreviewDismissed = ref(false)
|
||||||
const setupDevelopmentSkipped = ref(false)
|
const setupDevelopmentSkipped = ref(false)
|
||||||
|
const setupAppearanceSelected = ref(false)
|
||||||
const pendingUnlockRoute = ref<string | null>(null)
|
const pendingUnlockRoute = ref<string | null>(null)
|
||||||
const unlockedServicesLoaded = ref(false)
|
const unlockedServicesLoaded = ref(false)
|
||||||
const controlCenterOpened = ref(false)
|
const controlCenterOpened = ref(false)
|
||||||
@@ -359,6 +363,13 @@ const setupRequired = computed(
|
|||||||
developmentParameters.has('setupPreview') &&
|
developmentParameters.has('setupPreview') &&
|
||||||
!setupPreviewDismissed.value)),
|
!setupPreviewDismissed.value)),
|
||||||
)
|
)
|
||||||
|
const displayedDarkMode = computed(
|
||||||
|
() =>
|
||||||
|
phone.isDarkMode &&
|
||||||
|
(!setupRequired.value ||
|
||||||
|
setupAppearanceSelected.value ||
|
||||||
|
phone.preferences.settings.setupStep > 4),
|
||||||
|
)
|
||||||
const hardwareAlertVolume = computed(() =>
|
const hardwareAlertVolume = computed(() =>
|
||||||
Math.round(
|
Math.round(
|
||||||
(phone.preferences.settings.notificationVolume +
|
(phone.preferences.settings.notificationVolume +
|
||||||
@@ -572,6 +583,7 @@ function loadUnlockedPhoneData(): void {
|
|||||||
|
|
||||||
function completePhoneSetup(): void {
|
function completePhoneSetup(): void {
|
||||||
setupPreviewDismissed.value = true
|
setupPreviewDismissed.value = true
|
||||||
|
setupAppearanceSelected.value = false
|
||||||
isLocked.value = false
|
isLocked.value = false
|
||||||
isUnlocking.value = false
|
isUnlocking.value = false
|
||||||
passcodeVisible.value = false
|
passcodeVisible.value = false
|
||||||
@@ -1394,7 +1406,29 @@ function unlockCamera(): void {
|
|||||||
window.setTimeout(() => void router.push('/apps/camera'), 0)
|
window.setTimeout(() => void router.push('/apps/camera'), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTextInputFocus(active: boolean): void {
|
||||||
|
if (textInputFocused === active) return
|
||||||
|
textInputFocused = active
|
||||||
|
void nuiCall('ui:input-focus', { active })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocusIn(event: FocusEvent): void {
|
||||||
|
const target = event.target
|
||||||
|
updateTextInputFocus(
|
||||||
|
target instanceof HTMLElement && isTextInputElement(target),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocusOut(event: FocusEvent): void {
|
||||||
|
const nextTarget = event.relatedTarget
|
||||||
|
updateTextInputFocus(
|
||||||
|
nextTarget instanceof HTMLElement && isTextInputElement(nextTarget),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
document.addEventListener('focusin', onFocusIn)
|
||||||
|
document.addEventListener('focusout', onFocusOut)
|
||||||
window.addEventListener('message', onMessage)
|
window.addEventListener('message', onMessage)
|
||||||
window.addEventListener('keydown', onKeydown)
|
window.addEventListener('keydown', onKeydown)
|
||||||
window.addEventListener('resize', updateViewportScale)
|
window.addEventListener('resize', updateViewportScale)
|
||||||
@@ -1500,6 +1534,7 @@ watch(
|
|||||||
(isOpen) => {
|
(isOpen) => {
|
||||||
if (unlockTimer !== undefined) window.clearTimeout(unlockTimer)
|
if (unlockTimer !== undefined) window.clearTimeout(unlockTimer)
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
updateTextInputFocus(false)
|
||||||
cancelUnlockedPhoneDataLoad()
|
cancelUnlockedPhoneDataLoad()
|
||||||
appStore.cancelPendingInstalls()
|
appStore.cancelPendingInstalls()
|
||||||
activitySuspended.value = false
|
activitySuspended.value = false
|
||||||
@@ -1557,6 +1592,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
updateTextInputFocus(false)
|
||||||
cancelUnlockedPhoneDataLoad()
|
cancelUnlockedPhoneDataLoad()
|
||||||
weather.stop()
|
weather.stop()
|
||||||
if (clockTicker) clearInterval(clockTicker)
|
if (clockTicker) clearInterval(clockTicker)
|
||||||
@@ -1571,6 +1607,8 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('message', onMessage)
|
window.removeEventListener('message', onMessage)
|
||||||
window.removeEventListener('keydown', onKeydown)
|
window.removeEventListener('keydown', onKeydown)
|
||||||
window.removeEventListener('resize', updateViewportScale)
|
window.removeEventListener('resize', updateViewportScale)
|
||||||
|
document.removeEventListener('focusin', onFocusIn)
|
||||||
|
document.removeEventListener('focusout', onFocusOut)
|
||||||
systemColorScheme.removeEventListener('change', onSystemColorSchemeChange)
|
systemColorScheme.removeEventListener('change', onSystemColorSchemeChange)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -1624,7 +1662,7 @@ onBeforeUnmount(() => {
|
|||||||
<section
|
<section
|
||||||
class="phone-device"
|
class="phone-device"
|
||||||
:class="{
|
:class="{
|
||||||
'phone-app--light': !phone.isDarkMode,
|
'phone-app--light': !displayedDarkMode,
|
||||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||||
}"
|
}"
|
||||||
:aria-label="phone.t('Common.phone')"
|
:aria-label="phone.t('Common.phone')"
|
||||||
@@ -1674,7 +1712,7 @@ onBeforeUnmount(() => {
|
|||||||
class="phone-screen"
|
class="phone-screen"
|
||||||
:class="{
|
:class="{
|
||||||
'phone-screen--app': isAppRoute || isDevelopmentRoute,
|
'phone-screen--app': isAppRoute || isDevelopmentRoute,
|
||||||
'phone-app--light': !phone.isDarkMode,
|
'phone-app--light': !displayedDarkMode,
|
||||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -1711,18 +1749,19 @@ onBeforeUnmount(() => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
<k-app
|
<k-app
|
||||||
theme="ios"
|
theme="ios"
|
||||||
:dark="phone.isDarkMode"
|
:dark="displayedDarkMode"
|
||||||
safe-areas
|
safe-areas
|
||||||
class="phone-app"
|
class="phone-app"
|
||||||
:style="phoneDisplayStyle"
|
:style="phoneDisplayStyle"
|
||||||
:class="{
|
:class="{
|
||||||
dark: phone.isDarkMode,
|
dark: displayedDarkMode,
|
||||||
'phone-app--light': !phone.isDarkMode,
|
'phone-app--light': !displayedDarkMode,
|
||||||
'phone-app--messages': route.params.appId === 'messages',
|
'phone-app--messages': route.params.appId === 'messages',
|
||||||
'phone-app--status-light':
|
'phone-app--status-light':
|
||||||
WHITE_STATUS_BAR_APP_IDS.has(activeAppId),
|
WHITE_STATUS_BAR_APP_IDS.has(activeAppId),
|
||||||
'phone-app--status-dark':
|
'phone-app--status-dark':
|
||||||
DARK_STATUS_BAR_APP_IDS.has(activeAppId),
|
DARK_STATUS_BAR_APP_IDS.has(activeAppId),
|
||||||
|
'phone-app--setup': setupRequired,
|
||||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||||
'phone-app--unlocking': isUnlocking,
|
'phone-app--unlocking': isUnlocking,
|
||||||
}"
|
}"
|
||||||
@@ -1742,7 +1781,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
<SkyProvider
|
<SkyProvider
|
||||||
class="phone-app-theme"
|
class="phone-app-theme"
|
||||||
:dark="phone.isDarkMode"
|
:dark="displayedDarkMode"
|
||||||
safe-areas
|
safe-areas
|
||||||
>
|
>
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
@@ -1801,6 +1840,7 @@ onBeforeUnmount(() => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
<PhoneSetupAssistant
|
<PhoneSetupAssistant
|
||||||
v-if="setupRequired"
|
v-if="setupRequired"
|
||||||
|
@appearance-selected="setupAppearanceSelected = $event"
|
||||||
@complete="completePhoneSetup"
|
@complete="completePhoneSetup"
|
||||||
@skip="skipPhoneSetupForDevelopment"
|
@skip="skipPhoneSetupForDevelopment"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ const mainCss = readFileSync(
|
|||||||
)
|
)
|
||||||
|
|
||||||
describe('browser development preview contract', () => {
|
describe('browser development preview contract', () => {
|
||||||
|
it('keeps setup light until the appearance choice has been confirmed', () => {
|
||||||
|
expect(source).toContain('const displayedDarkMode = computed(')
|
||||||
|
expect(source).toContain('phone.preferences.settings.setupStep > 4')
|
||||||
|
expect(source).toContain('setupAppearanceSelected.value')
|
||||||
|
expect(source).toContain(
|
||||||
|
'@appearance-selected="setupAppearanceSelected = $event"',
|
||||||
|
)
|
||||||
|
expect(source).toContain(':dark="displayedDarkMode"')
|
||||||
|
expect(source).toContain('dark: displayedDarkMode')
|
||||||
|
})
|
||||||
|
|
||||||
it('starts unlocked while preserving an explicit lock screen preview', () => {
|
it('starts unlocked while preserving an explicit lock screen preview', () => {
|
||||||
expect(source).toContain("developmentParameters.has('lockScreenPreview')")
|
expect(source).toContain("developmentParameters.has('lockScreenPreview')")
|
||||||
expect(source).toContain(': !isDevelopment || developmentLockScreenPreview')
|
expect(source).toContain(': !isDevelopment || developmentLockScreenPreview')
|
||||||
@@ -87,6 +98,7 @@ describe('browser development preview contract', () => {
|
|||||||
|
|
||||||
it('fills the dedicated browser embed without clipping the hardware controls', () => {
|
it('fills the dedicated browser embed without clipping the hardware controls', () => {
|
||||||
expect(source).toContain("developmentParameters.has('browserPreview')")
|
expect(source).toContain("developmentParameters.has('browserPreview')")
|
||||||
|
expect(source).toContain('import.meta.env.DEV ||')
|
||||||
expect(source).toContain("'phone-stage--browser-preview': isBrowserPreview")
|
expect(source).toContain("'phone-stage--browser-preview': isBrowserPreview")
|
||||||
expect(source).toContain('return (availableScale * 0.94) / PHONE_BASE_SCALE')
|
expect(source).toContain('return (availableScale * 0.94) / PHONE_BASE_SCALE')
|
||||||
expect(mainCss).toMatch(
|
expect(mainCss).toMatch(
|
||||||
|
|||||||
+31
-322
@@ -904,361 +904,63 @@ button {
|
|||||||
}
|
}
|
||||||
.wallpaper--midnight {
|
.wallpaper--midnight {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 76% 17%, #ffffff 0 0.7px, transparent 1.2px),
|
radial-gradient(circle at 78% 12%, rgb(87 103 160 / 18%), transparent 38%),
|
||||||
radial-gradient(circle at 28% 8%, #9fceff 0 0.6px, transparent 1px),
|
linear-gradient(160deg, #141724 0%, #090b12 100%);
|
||||||
radial-gradient(circle at 82% 48%, #d8c8ff 0 0.8px, transparent 1.3px),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 69% 19%,
|
|
||||||
#faf7ff 0 3%,
|
|
||||||
#a57be74d 8%,
|
|
||||||
transparent 23%
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 214deg at 63% 35%,
|
|
||||||
transparent 0 29%,
|
|
||||||
#7352a51f 36%,
|
|
||||||
transparent 44%
|
|
||||||
),
|
|
||||||
radial-gradient(ellipse at 10% 84%, #1767999e 0, transparent 45%),
|
|
||||||
linear-gradient(158deg, #050511 0%, #11132d 42%, #120a25 68%, #02040c 100%);
|
|
||||||
background-size:
|
|
||||||
43px 47px,
|
|
||||||
67px 71px,
|
|
||||||
89px 83px,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto;
|
|
||||||
}
|
}
|
||||||
.wallpaper--aurora {
|
.wallpaper--aurora {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 17% 14%, #dfffff 0 0.6px, transparent 1.1px),
|
radial-gradient(circle at 74% 18%, rgb(80 188 166 / 22%), transparent 42%),
|
||||||
radial-gradient(circle at 78% 9%, #c6e7ff 0 0.7px, transparent 1.2px),
|
linear-gradient(155deg, #163c3c 0%, #17263c 56%, #0b101b 100%);
|
||||||
conic-gradient(
|
|
||||||
from 218deg at 84% 8%,
|
|
||||||
transparent 0 18%,
|
|
||||||
#4effbf70 27%,
|
|
||||||
#6bf5e220 35%,
|
|
||||||
transparent 43%
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 224deg at 62% 26%,
|
|
||||||
transparent 0 20%,
|
|
||||||
#27cfd46b 28%,
|
|
||||||
#695eff42 38%,
|
|
||||||
transparent 47%
|
|
||||||
),
|
|
||||||
radial-gradient(ellipse at 12% 80%, #1860bb9c 0%, transparent 48%),
|
|
||||||
radial-gradient(ellipse at 76% 20%, #2adfbb6e 0%, transparent 38%),
|
|
||||||
linear-gradient(155deg, #031715 0%, #08293a 43%, #151643 68%, #040914 100%);
|
|
||||||
background-size:
|
|
||||||
59px 61px,
|
|
||||||
83px 79px,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto;
|
|
||||||
}
|
}
|
||||||
.wallpaper--ember {
|
.wallpaper--ember {
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
radial-gradient(circle at 82% 14%, rgb(237 142 96 / 28%), transparent 40%),
|
||||||
circle at 80% 18%,
|
linear-gradient(155deg, #5b2925 0%, #321b25 58%, #17131b 100%);
|
||||||
#fff4ca 0 1.7%,
|
|
||||||
#ffbd67 5%,
|
|
||||||
#ff62335c 17%,
|
|
||||||
transparent 31%
|
|
||||||
),
|
|
||||||
repeating-radial-gradient(
|
|
||||||
ellipse at 78% 21%,
|
|
||||||
transparent 0 16px,
|
|
||||||
#ffb15e12 18px 20px
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 196deg at 12% 78%,
|
|
||||||
transparent 0 18%,
|
|
||||||
#ff3d4c66 28%,
|
|
||||||
#f3943a29 38%,
|
|
||||||
transparent 48%
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 22deg at 78% 90%,
|
|
||||||
transparent 0 22%,
|
|
||||||
#b52a755e 31%,
|
|
||||||
transparent 44%
|
|
||||||
),
|
|
||||||
radial-gradient(ellipse at 8% 71%, #c8205c91 0, transparent 46%),
|
|
||||||
linear-gradient(152deg, #20080a 0%, #4b171d 37%, #3c1537 66%, #090710 100%);
|
|
||||||
}
|
}
|
||||||
.wallpaper--ocean {
|
.wallpaper--ocean {
|
||||||
background:
|
background:
|
||||||
linear-gradient(116deg, transparent 43%, #c7fbff1c 47%, transparent 51%),
|
radial-gradient(circle at 22% 10%, rgb(119 203 222 / 28%), transparent 40%),
|
||||||
linear-gradient(64deg, transparent 44%, #8defff17 48%, transparent 52%),
|
linear-gradient(175deg, #327e98 0%, #185273 46%, #0b2944 100%);
|
||||||
radial-gradient(
|
|
||||||
ellipse at 18% 12%,
|
|
||||||
#d7ffff 0 1%,
|
|
||||||
#70e7ff85 12%,
|
|
||||||
transparent 34%
|
|
||||||
),
|
|
||||||
repeating-radial-gradient(
|
|
||||||
ellipse at 48% 107%,
|
|
||||||
transparent 0 24px,
|
|
||||||
#73e8ff2e 27px 30px,
|
|
||||||
transparent 33px 49px
|
|
||||||
),
|
|
||||||
radial-gradient(ellipse at 87% 70%, #056bd1ce 0, transparent 48%),
|
|
||||||
linear-gradient(
|
|
||||||
172deg,
|
|
||||||
#b5f4f8 0%,
|
|
||||||
#148eb0 22%,
|
|
||||||
#075b91 54%,
|
|
||||||
#03284d 78%,
|
|
||||||
#020c20 100%
|
|
||||||
);
|
|
||||||
background-size:
|
|
||||||
82px 82px,
|
|
||||||
82px 82px,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto;
|
|
||||||
}
|
}
|
||||||
.wallpaper--sunrise {
|
.wallpaper--sunrise {
|
||||||
background:
|
background:
|
||||||
linear-gradient(18deg, #281c46 0 13%, transparent 13.3%),
|
radial-gradient(circle at 68% 68%, rgb(255 205 153 / 30%), transparent 42%),
|
||||||
linear-gradient(-22deg, #51304a 0 18%, transparent 18.3%),
|
linear-gradient(175deg, #6b6488 0%, #b46f78 52%, #d29673 100%);
|
||||||
radial-gradient(
|
|
||||||
circle at 58% 69%,
|
|
||||||
#fffbd7 0 5.5%,
|
|
||||||
#ffd28d 6%,
|
|
||||||
#ff9f666e 18%,
|
|
||||||
transparent 35%
|
|
||||||
),
|
|
||||||
repeating-linear-gradient(0deg, transparent 0 16px, #ffe2ae0d 17px 18px),
|
|
||||||
radial-gradient(ellipse at 20% 44%, #f888a360 0, transparent 42%),
|
|
||||||
linear-gradient(
|
|
||||||
178deg,
|
|
||||||
#342b78 0%,
|
|
||||||
#8a477f 35%,
|
|
||||||
#ed706f 57%,
|
|
||||||
#ffc277 72%,
|
|
||||||
#672e50 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
.wallpaper--violet {
|
.wallpaper--violet {
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
radial-gradient(circle at 74% 18%, rgb(164 129 199 / 24%), transparent 42%),
|
||||||
circle at 78% 14%,
|
linear-gradient(150deg, #513d69 0%, #302b50 56%, #171828 100%);
|
||||||
transparent 0 9%,
|
|
||||||
#e0b6ff35 9.5% 10.3%,
|
|
||||||
transparent 11%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 18% 74%,
|
|
||||||
transparent 0 16%,
|
|
||||||
#a1a2ff29 16.5% 17.2%,
|
|
||||||
transparent 18%
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 202deg at 83% 25%,
|
|
||||||
transparent 0 17%,
|
|
||||||
#e48dff91 25%,
|
|
||||||
#8d63ff32 35%,
|
|
||||||
transparent 45%
|
|
||||||
),
|
|
||||||
linear-gradient(122deg, transparent 36%, #f0c9ff13 39%, transparent 42%),
|
|
||||||
radial-gradient(circle at 13% 81%, #5555e5c4 0, transparent 46%),
|
|
||||||
radial-gradient(circle at 72% 28%, #a53dcc70 0, transparent 41%),
|
|
||||||
linear-gradient(148deg, #11072b 0%, #391164 48%, #20103f 72%, #080519 100%);
|
|
||||||
}
|
}
|
||||||
.wallpaper--forest {
|
.wallpaper--forest {
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
radial-gradient(circle at 76% 14%, rgb(126 161 121 / 24%), transparent 42%),
|
||||||
ellipse at 82% 12%,
|
linear-gradient(160deg, #395c4c 0%, #243e35 54%, #13231f 100%);
|
||||||
transparent 0 7%,
|
|
||||||
#c8f49c2c 7.5% 8.2%,
|
|
||||||
transparent 9%
|
|
||||||
),
|
|
||||||
repeating-radial-gradient(
|
|
||||||
ellipse at -8% 108%,
|
|
||||||
transparent 0 34px,
|
|
||||||
#c1e6a218 36px 39px
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
135deg,
|
|
||||||
transparent 42%,
|
|
||||||
#bfe8a31c 45% 47%,
|
|
||||||
transparent 50%
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 55deg at 85% 24%,
|
|
||||||
transparent 0 24%,
|
|
||||||
#8dcf7361 31%,
|
|
||||||
transparent 39%
|
|
||||||
),
|
|
||||||
radial-gradient(ellipse at 76% 16%, #b7df8675 0, transparent 36%),
|
|
||||||
radial-gradient(ellipse at 10% 78%, #0b8b70b8 0, transparent 49%),
|
|
||||||
linear-gradient(162deg, #061713 0%, #144833 45%, #123326 68%, #040d0b 100%);
|
|
||||||
background-size:
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
100% 100%;
|
|
||||||
}
|
}
|
||||||
.wallpaper--cobalt {
|
.wallpaper--cobalt {
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
radial-gradient(circle at 72% 16%, rgb(91 142 218 / 28%), transparent 40%),
|
||||||
145deg,
|
linear-gradient(155deg, #264f8b 0%, #1b3768 54%, #101e3b 100%);
|
||||||
transparent 0 47%,
|
|
||||||
#c4ddff18 48% 49%,
|
|
||||||
transparent 50%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
35deg,
|
|
||||||
transparent 0 47%,
|
|
||||||
#70a5ff12 48% 49%,
|
|
||||||
transparent 50%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 76% 25%,
|
|
||||||
transparent 0 12%,
|
|
||||||
#62b8ff45 12.5% 13.2%,
|
|
||||||
transparent 14%
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 215deg at 80% 28%,
|
|
||||||
transparent 0 22%,
|
|
||||||
#52b5ff85 30%,
|
|
||||||
#5457d62e 41%,
|
|
||||||
transparent 48%
|
|
||||||
),
|
|
||||||
radial-gradient(circle at 14% 78%, #134ed1a3 0, transparent 43%),
|
|
||||||
linear-gradient(152deg, #041438 0%, #0b3182 43%, #172d82 64%, #030a25 100%);
|
|
||||||
background-size:
|
|
||||||
58px 58px,
|
|
||||||
58px 58px,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto;
|
|
||||||
}
|
}
|
||||||
.wallpaper--rose {
|
.wallpaper--rose {
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
radial-gradient(circle at 24% 12%, rgb(224 154 172 / 26%), transparent 42%),
|
||||||
circle at 16% 18%,
|
linear-gradient(155deg, #9a596c 0%, #724354 54%, #412936 100%);
|
||||||
#fff6f3 0 1.5%,
|
|
||||||
#ffd9df9e 8%,
|
|
||||||
transparent 27%
|
|
||||||
),
|
|
||||||
repeating-radial-gradient(
|
|
||||||
ellipse at 108% 8%,
|
|
||||||
transparent 0 24px,
|
|
||||||
#ffd5e121 26px 28px
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 214deg at 86% 72%,
|
|
||||||
transparent 0 18%,
|
|
||||||
#f078aa8c 27%,
|
|
||||||
#8d3d7b38 39%,
|
|
||||||
transparent 48%
|
|
||||||
),
|
|
||||||
linear-gradient(138deg, transparent 38%, #fff0f217 43%, transparent 48%),
|
|
||||||
radial-gradient(circle at 87% 77%, #e84e92a8 0, transparent 43%),
|
|
||||||
linear-gradient(148deg, #40132f 0%, #8e3159 39%, #b64b70 59%, #351128 100%);
|
|
||||||
}
|
}
|
||||||
.wallpaper--sand {
|
.wallpaper--sand {
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
radial-gradient(circle at 76% 14%, rgb(222 190 145 / 25%), transparent 42%),
|
||||||
circle at 76% 16%,
|
linear-gradient(160deg, #a17d61 0%, #80604e 56%, #513e36 100%);
|
||||||
#fffce7 0 2.5%,
|
|
||||||
#ffe5a5a8 9%,
|
|
||||||
transparent 25%
|
|
||||||
),
|
|
||||||
repeating-radial-gradient(
|
|
||||||
ellipse at -2% 110%,
|
|
||||||
transparent 0 28px,
|
|
||||||
#fff3d12b 31px 34px,
|
|
||||||
transparent 37px 52px
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 82deg at 110% 76%,
|
|
||||||
transparent 0 24%,
|
|
||||||
#ecc37e52 32%,
|
|
||||||
transparent 43%
|
|
||||||
),
|
|
||||||
linear-gradient(115deg, transparent 41%, #fff6d21a 44%, transparent 47%),
|
|
||||||
radial-gradient(ellipse at 15% 78%, #9f654b9e 0, transparent 48%),
|
|
||||||
linear-gradient(163deg, #8b5c45 0%, #d4a262 43%, #b87950 68%, #4f302b 100%);
|
|
||||||
}
|
}
|
||||||
.wallpaper--graphite {
|
.wallpaper--graphite {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 77% 17%, #ffffff42 0 0.8%, transparent 12%),
|
radial-gradient(circle at 76% 14%, rgb(133 140 151 / 18%), transparent 40%),
|
||||||
linear-gradient(
|
linear-gradient(160deg, #363a42 0%, #23262c 54%, #121417 100%);
|
||||||
125deg,
|
|
||||||
transparent 39%,
|
|
||||||
#ffffff12 40% 42%,
|
|
||||||
transparent 43%
|
|
||||||
),
|
|
||||||
linear-gradient(35deg, transparent 39%, #ffffff0a 40% 42%, transparent 43%),
|
|
||||||
repeating-linear-gradient(
|
|
||||||
92deg,
|
|
||||||
transparent 0 3px,
|
|
||||||
#ffffff09 4px,
|
|
||||||
transparent 5px 9px
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 215deg at 79% 24%,
|
|
||||||
transparent 0 25%,
|
|
||||||
#b7c1d045 32%,
|
|
||||||
transparent 43%
|
|
||||||
),
|
|
||||||
radial-gradient(circle at 17% 78%, #51596c73 0, transparent 44%),
|
|
||||||
linear-gradient(157deg, #06070a 0%, #242832 43%, #171920 67%, #030405 100%);
|
|
||||||
background-size:
|
|
||||||
auto,
|
|
||||||
54px 54px,
|
|
||||||
54px 54px,
|
|
||||||
100% 100%,
|
|
||||||
auto,
|
|
||||||
auto,
|
|
||||||
auto;
|
|
||||||
}
|
}
|
||||||
.wallpaper--prism {
|
.wallpaper--prism {
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
radial-gradient(circle at 72% 16%, rgb(165 151 218 / 24%), transparent 42%),
|
||||||
132deg,
|
linear-gradient(150deg, #655b86 0%, #42516f 52%, #293545 100%);
|
||||||
transparent 0 32%,
|
|
||||||
#ffffff36 32.5% 33.2%,
|
|
||||||
transparent 34%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
42deg,
|
|
||||||
transparent 0 56%,
|
|
||||||
#bffcff25 56.5% 57.2%,
|
|
||||||
transparent 58%
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 218deg at 70% 29%,
|
|
||||||
#ff527ca1,
|
|
||||||
#ffc75f91,
|
|
||||||
#4cead2a6,
|
|
||||||
#4f72ffae,
|
|
||||||
#c64deaa3,
|
|
||||||
#ff527ca1
|
|
||||||
),
|
|
||||||
conic-gradient(
|
|
||||||
from 38deg at 16% 78%,
|
|
||||||
transparent 0 19%,
|
|
||||||
#4ae3d8a1 28%,
|
|
||||||
#596cff48 39%,
|
|
||||||
transparent 48%
|
|
||||||
),
|
|
||||||
radial-gradient(circle at 16% 76%, #35d8d399 0, transparent 38%),
|
|
||||||
linear-gradient(152deg, #120d35 0%, #47205f 46%, #10205a 70%, #041321 100%);
|
|
||||||
background-blend-mode: screen, screen, screen, screen, screen, normal;
|
|
||||||
}
|
}
|
||||||
.wallpaper--custom {
|
.wallpaper--custom {
|
||||||
background-color: #090a0d;
|
background-color: #090a0d;
|
||||||
@@ -5475,6 +5177,13 @@ button {
|
|||||||
color: #111;
|
color: #111;
|
||||||
text-shadow: 0 1px 3px #fff8;
|
text-shadow: 0 1px 3px #fff8;
|
||||||
}
|
}
|
||||||
|
.phone-app--setup .phone-status-bar {
|
||||||
|
color: #1d1d1f;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
.phone-app.dark.phone-app--setup .phone-status-bar {
|
||||||
|
color: #f5f5f7;
|
||||||
|
}
|
||||||
* {
|
* {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,18 @@ describe('PhoneSetupAssistant contract', () => {
|
|||||||
it('uses first-party controls and exposes the complete setup journey', () => {
|
it('uses first-party controls and exposes the complete setup journey', () => {
|
||||||
expect(source).not.toContain("from 'konsta/vue'")
|
expect(source).not.toContain("from 'konsta/vue'")
|
||||||
expect(source).toContain('PhonePasscode')
|
expect(source).toContain('PhonePasscode')
|
||||||
expect(source).toContain("step === 1")
|
expect(source).toContain('step === 1')
|
||||||
expect(source).toContain("step === 8")
|
expect(source).toContain('step === 8')
|
||||||
expect(source).toContain("['performance', 'ultimate']")
|
expect(source).toContain("['performance', 'ultimate']")
|
||||||
|
expect(source).toContain('setup-mode-preview__card--front')
|
||||||
|
expect(source).toContain('setup-mode-preview__card--back')
|
||||||
expect(source).toContain('WALLPAPER_IDS')
|
expect(source).toContain('WALLPAPER_IDS')
|
||||||
expect(source).toContain('setAllAppNotifications')
|
expect(source).toContain('setAllAppNotifications')
|
||||||
expect(source).toContain('appStore.claimApp')
|
expect(source).toContain('appStore.claimApp')
|
||||||
expect(source).toContain('await phone.completeSetup()')
|
expect(source).toContain('await phone.completeSetup()')
|
||||||
|
expect(source).toMatch(
|
||||||
|
/if \(step\.value === 8\) \{[\s\S]*void finish\(\)[\s\S]*return/,
|
||||||
|
)
|
||||||
expect(source).toContain(':disabled="setupCompleteBusy"')
|
expect(source).toContain(':disabled="setupCompleteBusy"')
|
||||||
expect(source).toContain("phone.t('Setup.ready.saveFailed')")
|
expect(source).toContain("phone.t('Setup.ready.saveFailed')")
|
||||||
})
|
})
|
||||||
@@ -27,4 +32,30 @@ describe('PhoneSetupAssistant contract', () => {
|
|||||||
expect(source).toContain('phone.setSetupStep(step.value)')
|
expect(source).toContain('phone.setSetupStep(step.value)')
|
||||||
expect(source).toContain('@click="moveTo(step - 1)"')
|
expect(source).toContain('@click="moveTo(step - 1)"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps the development skip control away from setup progress', () => {
|
||||||
|
expect(source).toContain('v-if="showDevelopmentSkip && step === 0"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('follows the selected system appearance throughout setup', () => {
|
||||||
|
expect(source).toContain(
|
||||||
|
'(appearanceSelected || step > 4) && phone.isDarkMode',
|
||||||
|
)
|
||||||
|
expect(source).toContain("emit('appearanceSelected', true)")
|
||||||
|
expect(source).toContain("phone.setPreference('appearanceMode', 'light')")
|
||||||
|
expect(source).toContain('.setup-assistant--dark.setup-assistant--step-0')
|
||||||
|
expect(source).toContain('--setup-background: #000000')
|
||||||
|
expect(source).toContain('background: var(--setup-background)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies the selected graphics mode to the setup experience immediately', () => {
|
||||||
|
expect(source).toContain("'setup-assistant--performance':")
|
||||||
|
expect(source).toContain("'setup-assistant--ultimate':")
|
||||||
|
expect(source).toContain(
|
||||||
|
'.setup-assistant--performance .setup-forward-enter-active',
|
||||||
|
)
|
||||||
|
expect(source).toContain(
|
||||||
|
'.setup-assistant--ultimate .setup-mode-stack button',
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Palette,
|
Palette,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Signal,
|
Signal,
|
||||||
|
Smartphone,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Wifi,
|
Wifi,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
@@ -30,7 +31,11 @@ import {
|
|||||||
type WallpaperId,
|
type WallpaperId,
|
||||||
} from '@/utils/preferences'
|
} from '@/utils/preferences'
|
||||||
|
|
||||||
const emit = defineEmits<{ complete: []; skip: [] }>()
|
const emit = defineEmits<{
|
||||||
|
appearanceSelected: [selected: boolean]
|
||||||
|
complete: []
|
||||||
|
skip: []
|
||||||
|
}>()
|
||||||
|
|
||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const account = useAccountStore()
|
const account = useAccountStore()
|
||||||
@@ -38,6 +43,10 @@ const appStore = useAppStoreStore()
|
|||||||
const step = ref(
|
const step = ref(
|
||||||
Math.min(PHONE_SETUP_LAST_STEP, phone.preferences.settings.setupStep),
|
Math.min(PHONE_SETUP_LAST_STEP, phone.preferences.settings.setupStep),
|
||||||
)
|
)
|
||||||
|
const appearanceSelected = ref(step.value > 4)
|
||||||
|
if (step.value === 4) {
|
||||||
|
phone.setPreference('appearanceMode', 'light')
|
||||||
|
}
|
||||||
const direction = ref<'back' | 'forward'>('forward')
|
const direction = ref<'back' | 'forward'>('forward')
|
||||||
const accountMode = ref<'login' | 'register'>('login')
|
const accountMode = ref<'login' | 'register'>('login')
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
@@ -93,6 +102,13 @@ const showDevelopmentSkip = import.meta.env.DEV
|
|||||||
function moveTo(nextStep: number): void {
|
function moveTo(nextStep: number): void {
|
||||||
direction.value = nextStep < step.value ? 'back' : 'forward'
|
direction.value = nextStep < step.value ? 'back' : 'forward'
|
||||||
step.value = Math.min(PHONE_SETUP_LAST_STEP, Math.max(0, nextStep))
|
step.value = Math.min(PHONE_SETUP_LAST_STEP, Math.max(0, nextStep))
|
||||||
|
if (step.value < 4) {
|
||||||
|
appearanceSelected.value = false
|
||||||
|
emit('appearanceSelected', false)
|
||||||
|
}
|
||||||
|
if (step.value === 4 && !appearanceSelected.value) {
|
||||||
|
phone.setPreference('appearanceMode', 'light')
|
||||||
|
}
|
||||||
phone.setSetupStep(step.value)
|
phone.setSetupStep(step.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,12 +121,16 @@ function continueSetup(): void {
|
|||||||
}
|
}
|
||||||
if (step.value === 8) {
|
if (step.value === 8) {
|
||||||
for (const appId of selectedApps.value) appStore.claimApp(appId)
|
for (const appId of selectedApps.value) appStore.claimApp(appId)
|
||||||
|
void finish()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
moveTo(step.value + 1)
|
moveTo(step.value + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseAppearance(mode: AppearanceMode): void {
|
function chooseAppearance(mode: AppearanceMode): void {
|
||||||
|
appearanceSelected.value = true
|
||||||
phone.setPreference('appearanceMode', mode)
|
phone.setPreference('appearanceMode', mode)
|
||||||
|
emit('appearanceSelected', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function choosePerformance(mode: GraphicsMode): void {
|
function choosePerformance(mode: GraphicsMode): void {
|
||||||
@@ -227,13 +247,22 @@ function skipSetupForDevelopment(): void {
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="setup-assistant"
|
class="setup-assistant"
|
||||||
:class="`setup-assistant--step-${step}`"
|
:class="[
|
||||||
|
`setup-assistant--step-${step}`,
|
||||||
|
{
|
||||||
|
'setup-assistant--dark':
|
||||||
|
(appearanceSelected || step > 4) && phone.isDarkMode,
|
||||||
|
'setup-assistant--performance':
|
||||||
|
phone.preferences.settings.graphicsMode === 'performance',
|
||||||
|
'setup-assistant--ultimate':
|
||||||
|
phone.preferences.settings.graphicsMode === 'ultimate',
|
||||||
|
},
|
||||||
|
]"
|
||||||
:style="currentWallpaperStyle"
|
:style="currentWallpaperStyle"
|
||||||
:aria-label="phone.t('Setup.title')"
|
:aria-label="phone.t('Setup.title')"
|
||||||
>
|
>
|
||||||
<div class="setup-assistant__aurora" aria-hidden="true"></div>
|
|
||||||
<button
|
<button
|
||||||
v-if="showDevelopmentSkip"
|
v-if="showDevelopmentSkip && step === 0"
|
||||||
type="button"
|
type="button"
|
||||||
class="setup-assistant__development-skip"
|
class="setup-assistant__development-skip"
|
||||||
@click="skipSetupForDevelopment"
|
@click="skipSetupForDevelopment"
|
||||||
@@ -262,15 +291,8 @@ function skipSetupForDevelopment(): void {
|
|||||||
<main :key="step" class="setup-assistant__page">
|
<main :key="step" class="setup-assistant__page">
|
||||||
<template v-if="step === 0">
|
<template v-if="step === 0">
|
||||||
<div class="setup-welcome__hero" aria-hidden="true">
|
<div class="setup-welcome__hero" aria-hidden="true">
|
||||||
<div class="setup-welcome__greetings">
|
<div class="setup-welcome__device">
|
||||||
<span>{{ phone.t('Setup.welcome.hello') }}</span>
|
<Smartphone :size="43" :stroke-width="1.55" />
|
||||||
<span>{{ phone.t('Setup.welcome.hallo') }}</span>
|
|
||||||
<span>{{ phone.t('Setup.welcome.bonjour') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="setup-welcome__signature">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setup-welcome__copy">
|
<div class="setup-welcome__copy">
|
||||||
@@ -283,19 +305,6 @@ function skipSetupForDevelopment(): void {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<footer class="setup-welcome__footer">
|
<footer class="setup-welcome__footer">
|
||||||
<div class="setup-welcome__privacy">
|
|
||||||
<span
|
|
||||||
><ShieldCheck :size="14" />{{
|
|
||||||
phone.t('Setup.welcome.private')
|
|
||||||
}}</span
|
|
||||||
>
|
|
||||||
<i aria-hidden="true"></i>
|
|
||||||
<span
|
|
||||||
><Sparkles :size="14" />{{
|
|
||||||
phone.t('Setup.welcome.personal')
|
|
||||||
}}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<SkyButton class="setup-assistant__primary" @click="continueSetup">
|
<SkyButton class="setup-assistant__primary" @click="continueSetup">
|
||||||
{{ phone.t('Setup.getStarted') }}
|
{{ phone.t('Setup.getStarted') }}
|
||||||
</SkyButton>
|
</SkyButton>
|
||||||
@@ -632,7 +641,20 @@ function skipSetupForDevelopment(): void {
|
|||||||
<span
|
<span
|
||||||
class="setup-mode-stack__orb"
|
class="setup-mode-stack__orb"
|
||||||
:class="`setup-mode-stack__orb--${mode}`"
|
:class="`setup-mode-stack__orb--${mode}`"
|
||||||
></span>
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<i class="setup-mode-preview__backdrop"></i>
|
||||||
|
<i
|
||||||
|
class="setup-mode-preview__card setup-mode-preview__card--back"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
class="setup-mode-preview__card setup-mode-preview__card--front"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
class="setup-mode-preview__line setup-mode-preview__line--wide"
|
||||||
|
></i>
|
||||||
|
<i class="setup-mode-preview__line"></i>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
><strong>{{ phone.t(`Apps.settings.${mode}Mode`) }}</strong
|
><strong>{{ phone.t(`Apps.settings.${mode}Mode`) }}</strong
|
||||||
><small>{{ phone.t(`Setup.performance.${mode}`) }}</small></span
|
><small>{{ phone.t(`Setup.performance.${mode}`) }}</small></span
|
||||||
@@ -2126,6 +2148,462 @@ function skipSetupForDevelopment(): void {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Minimal system setup language, matching the restrained iOS setup hierarchy. */
|
||||||
|
.setup-assistant,
|
||||||
|
.setup-assistant--step-0 {
|
||||||
|
--setup-background: #ffffff;
|
||||||
|
--setup-text: #1d1d1f;
|
||||||
|
--setup-secondary: #6e6e73;
|
||||||
|
--setup-tertiary: #8e8e93;
|
||||||
|
--setup-muted: #aeaeb2;
|
||||||
|
--setup-surface: #f2f2f7;
|
||||||
|
--setup-selected-surface: #eef6ff;
|
||||||
|
--setup-control: #e9e9eb;
|
||||||
|
--setup-control-fill: #ffffff;
|
||||||
|
--setup-separator: #d1d1d6;
|
||||||
|
--setup-blue: #007aff;
|
||||||
|
--setup-green: #34c759;
|
||||||
|
--setup-red: #ff3b30;
|
||||||
|
--setup-orb: #dcecff;
|
||||||
|
--setup-orb-ultimate: #e7e2ff;
|
||||||
|
--setup-home-indicator: #1d1d1f;
|
||||||
|
color: var(--setup-text);
|
||||||
|
background: var(--setup-background);
|
||||||
|
}
|
||||||
|
.setup-assistant--dark,
|
||||||
|
.setup-assistant--dark.setup-assistant--step-0 {
|
||||||
|
--setup-background: #000000;
|
||||||
|
--setup-text: #f5f5f7;
|
||||||
|
--setup-secondary: #98989d;
|
||||||
|
--setup-tertiary: #8e8e93;
|
||||||
|
--setup-muted: #636366;
|
||||||
|
--setup-surface: #1c1c1e;
|
||||||
|
--setup-selected-surface: #102a43;
|
||||||
|
--setup-control: #2c2c2e;
|
||||||
|
--setup-control-fill: #636366;
|
||||||
|
--setup-separator: #38383a;
|
||||||
|
--setup-blue: #0a84ff;
|
||||||
|
--setup-green: #30d158;
|
||||||
|
--setup-red: #ff453a;
|
||||||
|
--setup-orb: #102a43;
|
||||||
|
--setup-orb-ultimate: #28203d;
|
||||||
|
--setup-home-indicator: #ffffff;
|
||||||
|
}
|
||||||
|
.setup-assistant__development-skip {
|
||||||
|
top: 48px;
|
||||||
|
right: 13px;
|
||||||
|
border: 0;
|
||||||
|
color: var(--setup-blue);
|
||||||
|
background: transparent;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.setup-assistant__chrome {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.setup-assistant__back {
|
||||||
|
border: 0;
|
||||||
|
color: var(--setup-blue);
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
.setup-assistant__progress {
|
||||||
|
height: 3px;
|
||||||
|
background: var(--setup-separator);
|
||||||
|
}
|
||||||
|
.setup-assistant__progress span {
|
||||||
|
background: var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-assistant__counter {
|
||||||
|
color: var(--setup-tertiary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.setup-assistant__page h1 {
|
||||||
|
margin: 12px 0 10px;
|
||||||
|
color: var(--setup-text);
|
||||||
|
font-size: 31px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
.setup-assistant__lead {
|
||||||
|
color: var(--setup-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.42;
|
||||||
|
}
|
||||||
|
.setup-assistant__eyebrow,
|
||||||
|
.setup-welcome__eyebrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.setup-assistant__icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--setup-blue);
|
||||||
|
background: var(--setup-surface);
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
.setup-assistant__icon--signal,
|
||||||
|
.setup-assistant__icon--cloud,
|
||||||
|
.setup-assistant__icon--security,
|
||||||
|
.setup-assistant__icon--appearance,
|
||||||
|
.setup-assistant__icon--performance,
|
||||||
|
.setup-assistant__icon--wallpaper,
|
||||||
|
.setup-assistant__icon--notifications,
|
||||||
|
.setup-assistant__icon--apps {
|
||||||
|
color: var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-assistant__primary {
|
||||||
|
min-height: 50px;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
background: var(--setup-blue) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.setup-assistant__later {
|
||||||
|
color: var(--setup-blue);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.setup-assistant__notice {
|
||||||
|
padding: 5px 4px;
|
||||||
|
border: 0;
|
||||||
|
color: var(--setup-blue);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.setup-assistant__notice p {
|
||||||
|
color: var(--setup-secondary);
|
||||||
|
}
|
||||||
|
.setup-assistant__error {
|
||||||
|
color: var(--setup-red);
|
||||||
|
}
|
||||||
|
.setup-assistant--step-0 .setup-assistant__page {
|
||||||
|
padding: 105px 28px 0;
|
||||||
|
}
|
||||||
|
.setup-welcome__hero {
|
||||||
|
width: 94px;
|
||||||
|
height: 94px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--setup-surface);
|
||||||
|
}
|
||||||
|
.setup-welcome__hero::before,
|
||||||
|
.setup-welcome__hero::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
.setup-welcome__device {
|
||||||
|
display: grid;
|
||||||
|
width: 94px;
|
||||||
|
height: 94px;
|
||||||
|
color: var(--setup-blue);
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.setup-welcome__copy {
|
||||||
|
margin-top: 31px;
|
||||||
|
}
|
||||||
|
.setup-welcome__copy h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
.setup-welcome__footer {
|
||||||
|
width: calc(100% + 56px);
|
||||||
|
margin-right: -28px;
|
||||||
|
margin-left: -28px;
|
||||||
|
padding: 0 28px 13px;
|
||||||
|
border: 0;
|
||||||
|
background: var(--setup-background);
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
.setup-welcome__footer .setup-assistant__primary {
|
||||||
|
margin-top: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-welcome__home-indicator {
|
||||||
|
background: var(--setup-home-indicator);
|
||||||
|
}
|
||||||
|
.setup-connectivity-card {
|
||||||
|
min-height: 142px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 22px;
|
||||||
|
color: var(--setup-text);
|
||||||
|
background: var(--setup-surface);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-connectivity-card__waves {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.setup-connectivity-card__label,
|
||||||
|
.setup-connectivity-card svg {
|
||||||
|
color: var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-connectivity-card > span:last-of-type {
|
||||||
|
color: var(--setup-tertiary);
|
||||||
|
}
|
||||||
|
.setup-cloud-hero {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--setup-blue);
|
||||||
|
background: var(--setup-surface);
|
||||||
|
}
|
||||||
|
.setup-cloud-hero__orbit,
|
||||||
|
.setup-cloud-hero__glow,
|
||||||
|
.setup-cloud-hero i {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.setup-cloud-hero svg {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
.setup-cloud-identity {
|
||||||
|
border: 0;
|
||||||
|
background: var(--setup-surface);
|
||||||
|
}
|
||||||
|
.setup-cloud-identity > span {
|
||||||
|
border: 0;
|
||||||
|
background: var(--setup-blue);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-cloud-identity small,
|
||||||
|
.setup-cloud-strength,
|
||||||
|
.setup-cloud-security-note {
|
||||||
|
color: var(--setup-tertiary);
|
||||||
|
}
|
||||||
|
.setup-cloud-identity strong,
|
||||||
|
.setup-cloud-connected strong {
|
||||||
|
color: var(--setup-text);
|
||||||
|
}
|
||||||
|
.setup-cloud-identity em,
|
||||||
|
.setup-cloud-suffix {
|
||||||
|
color: var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-cloud-fields {
|
||||||
|
border: 0;
|
||||||
|
background: var(--setup-surface);
|
||||||
|
}
|
||||||
|
.setup-cloud-fields :deep(.sky-field + .sky-field) {
|
||||||
|
border-top-color: var(--setup-separator);
|
||||||
|
}
|
||||||
|
.setup-cloud-fields :deep(.sky-field__label) {
|
||||||
|
color: var(--setup-tertiary);
|
||||||
|
}
|
||||||
|
.setup-cloud-fields :deep(.sky-field__input) {
|
||||||
|
color: var(--setup-text);
|
||||||
|
caret-color: var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-cloud-fields :deep(.sky-field__input::placeholder) {
|
||||||
|
color: var(--setup-muted);
|
||||||
|
}
|
||||||
|
.setup-cloud-strength span {
|
||||||
|
background: var(--setup-separator);
|
||||||
|
}
|
||||||
|
.setup-cloud-strength span.active {
|
||||||
|
background: var(--setup-blue);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-assistant__selector {
|
||||||
|
background: var(--setup-control);
|
||||||
|
}
|
||||||
|
.setup-assistant__selector-indicator {
|
||||||
|
border: 0;
|
||||||
|
background: var(--setup-control-fill);
|
||||||
|
box-shadow: 0 1px 4px rgb(0 0 0 / 14%);
|
||||||
|
}
|
||||||
|
.setup-assistant__selector button {
|
||||||
|
color: var(--setup-secondary);
|
||||||
|
}
|
||||||
|
.setup-assistant__selector button.active {
|
||||||
|
color: var(--setup-text);
|
||||||
|
}
|
||||||
|
.setup-cloud-connected {
|
||||||
|
color: var(--setup-green);
|
||||||
|
}
|
||||||
|
.setup-cloud-connected span {
|
||||||
|
color: var(--setup-secondary);
|
||||||
|
}
|
||||||
|
.setup-security-visual {
|
||||||
|
border: 0;
|
||||||
|
background: var(--setup-surface);
|
||||||
|
}
|
||||||
|
.setup-security-visual span {
|
||||||
|
background: var(--setup-text);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-security-visual svg {
|
||||||
|
color: var(--setup-muted);
|
||||||
|
}
|
||||||
|
.setup-passcode-length button,
|
||||||
|
.setup-choice-grid button,
|
||||||
|
.setup-mode-stack button,
|
||||||
|
.setup-toggle-card,
|
||||||
|
.setup-app-list button,
|
||||||
|
.setup-ready__summary span {
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--setup-text);
|
||||||
|
background: var(--setup-surface);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-passcode-length button.selected,
|
||||||
|
.setup-choice-grid button.selected,
|
||||||
|
.setup-mode-stack button.selected,
|
||||||
|
.setup-app-list button.selected {
|
||||||
|
border-color: var(--setup-blue);
|
||||||
|
background: var(--setup-selected-surface);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-passcode-length small,
|
||||||
|
.setup-mode-stack small,
|
||||||
|
.setup-toggle-card small,
|
||||||
|
.setup-app-list small {
|
||||||
|
color: var(--setup-secondary);
|
||||||
|
}
|
||||||
|
.setup-mode-stack__orb {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--setup-orb);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-mode-stack__orb--ultimate {
|
||||||
|
background: var(--setup-orb-ultimate);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-mode-preview__backdrop,
|
||||||
|
.setup-mode-preview__card,
|
||||||
|
.setup-mode-preview__line {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.setup-mode-preview__backdrop {
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(145deg, #d9eaff 0%, #b9d8ff 100%);
|
||||||
|
}
|
||||||
|
.setup-mode-preview__card {
|
||||||
|
width: 37px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.setup-mode-preview__card--back {
|
||||||
|
top: 11px;
|
||||||
|
left: 9px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.setup-mode-preview__card--front {
|
||||||
|
right: 8px;
|
||||||
|
bottom: 10px;
|
||||||
|
background: #e7f1ff;
|
||||||
|
}
|
||||||
|
.setup-mode-preview__line {
|
||||||
|
z-index: 2;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 17px;
|
||||||
|
width: 17px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #5d80ad;
|
||||||
|
}
|
||||||
|
.setup-mode-preview__line--wide {
|
||||||
|
bottom: 23px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
.setup-mode-stack__orb--ultimate .setup-mode-preview__backdrop {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 25%, #f4c7ff 0 12%, transparent 35%),
|
||||||
|
linear-gradient(145deg, #776de4 0%, #342760 100%);
|
||||||
|
}
|
||||||
|
.setup-mode-stack__orb--ultimate .setup-mode-preview__card {
|
||||||
|
border: 1px solid rgb(255 255 255 / 38%);
|
||||||
|
background: rgb(255 255 255 / 25%);
|
||||||
|
box-shadow: 0 7px 14px rgb(31 18 70 / 24%);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
.setup-mode-stack__orb--ultimate .setup-mode-preview__card--back {
|
||||||
|
transform: rotate(-8deg);
|
||||||
|
}
|
||||||
|
.setup-mode-stack__orb--ultimate .setup-mode-preview__card--front {
|
||||||
|
background: rgb(255 255 255 / 34%);
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
.setup-mode-stack__orb--ultimate .setup-mode-preview__line {
|
||||||
|
background: rgb(255 255 255 / 72%);
|
||||||
|
}
|
||||||
|
.setup-assistant--dark
|
||||||
|
.setup-mode-stack__orb--performance
|
||||||
|
.setup-mode-preview__backdrop {
|
||||||
|
background: linear-gradient(145deg, #203147 0%, #142133 100%);
|
||||||
|
}
|
||||||
|
.setup-assistant--dark
|
||||||
|
.setup-mode-stack__orb--performance
|
||||||
|
.setup-mode-preview__card--back {
|
||||||
|
background: #334965;
|
||||||
|
}
|
||||||
|
.setup-assistant--dark
|
||||||
|
.setup-mode-stack__orb--performance
|
||||||
|
.setup-mode-preview__card--front {
|
||||||
|
background: #29405d;
|
||||||
|
}
|
||||||
|
.setup-assistant--dark
|
||||||
|
.setup-mode-stack__orb--performance
|
||||||
|
.setup-mode-preview__line {
|
||||||
|
background: #a9c7e9;
|
||||||
|
}
|
||||||
|
.setup-mode-stack__check,
|
||||||
|
.setup-app-list button > i {
|
||||||
|
border-color: var(--setup-muted);
|
||||||
|
}
|
||||||
|
.selected .setup-mode-stack__check,
|
||||||
|
.setup-app-list button.selected > i {
|
||||||
|
border-color: var(--setup-blue);
|
||||||
|
background: var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-wallpapers button.selected {
|
||||||
|
border-color: var(--setup-background);
|
||||||
|
box-shadow: 0 0 0 2px var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-toggle-card {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.setup-toggle-card button {
|
||||||
|
color: var(--setup-text);
|
||||||
|
}
|
||||||
|
.setup-toggle-card button + button {
|
||||||
|
border-top-color: var(--setup-separator);
|
||||||
|
}
|
||||||
|
.setup-toggle-card i {
|
||||||
|
background: var(--setup-separator);
|
||||||
|
}
|
||||||
|
.setup-ready__halo {
|
||||||
|
width: 112px;
|
||||||
|
height: 112px;
|
||||||
|
border: 0;
|
||||||
|
color: var(--setup-blue);
|
||||||
|
background: var(--setup-surface);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.setup-ready__summary span {
|
||||||
|
color: var(--setup-blue);
|
||||||
|
}
|
||||||
|
.setup-ready__summary b {
|
||||||
|
color: var(--setup-text);
|
||||||
|
}
|
||||||
|
.setup-assistant--performance .setup-forward-enter-active,
|
||||||
|
.setup-assistant--performance .setup-forward-leave-active,
|
||||||
|
.setup-assistant--performance .setup-back-enter-active,
|
||||||
|
.setup-assistant--performance .setup-back-leave-active,
|
||||||
|
.setup-assistant--performance .setup-assistant__progress span,
|
||||||
|
.setup-assistant--performance .setup-mode-stack button,
|
||||||
|
.setup-assistant--performance .setup-mode-stack__orb {
|
||||||
|
transition-duration: 0.01ms;
|
||||||
|
}
|
||||||
|
.setup-assistant--ultimate .setup-mode-stack button,
|
||||||
|
.setup-assistant--ultimate .setup-mode-stack__orb {
|
||||||
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
background-color 0.2s ease,
|
||||||
|
transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.setup-assistant,
|
.setup-assistant,
|
||||||
.setup-assistant *,
|
.setup-assistant *,
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ describe('Sky UI Konsta 5.3 parity contracts', () => {
|
|||||||
expect(html).toContain('id="directory-search"')
|
expect(html).toContain('id="directory-search"')
|
||||||
expect(html).toContain('sky-glass')
|
expect(html).toContain('sky-glass')
|
||||||
expect(html).not.toContain('sky-glass--highlight')
|
expect(html).not.toContain('sky-glass--highlight')
|
||||||
|
expect(html).toContain('<svg class="sky-searchbar__icon"')
|
||||||
|
expect(html).toContain('fill-rule="evenodd"')
|
||||||
|
expect(html).not.toContain('<circle')
|
||||||
expect(html).toContain('sky-searchbar__clear')
|
expect(html).toContain('sky-searchbar__clear')
|
||||||
expect(html).toContain('aria-label="Clear query"')
|
expect(html).toContain('aria-label="Clear query"')
|
||||||
expect(html).toContain('sky-searchbar__disable')
|
expect(html).toContain('sky-searchbar__disable')
|
||||||
|
|||||||
@@ -1465,26 +1465,13 @@ label.sky-list-item__row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sky-searchbar__icon {
|
.sky-searchbar__icon {
|
||||||
width: 14px;
|
width: 18px;
|
||||||
height: 14px;
|
height: 18px;
|
||||||
position: relative;
|
display: block;
|
||||||
flex: none;
|
flex: none;
|
||||||
border: 1.6px solid var(--sky-muted, rgba(0, 0, 0, 0.55));
|
color: var(--sky-muted, rgba(0, 0, 0, 0.55));
|
||||||
border-radius: 50%;
|
fill: currentColor;
|
||||||
opacity: 0.8;
|
opacity: 0.72;
|
||||||
}
|
|
||||||
|
|
||||||
.sky-searchbar__icon::after {
|
|
||||||
width: 6px;
|
|
||||||
height: 1.6px;
|
|
||||||
position: absolute;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -2px;
|
|
||||||
content: '';
|
|
||||||
border-radius: 2px;
|
|
||||||
background: var(--sky-muted, rgba(0, 0, 0, 0.55));
|
|
||||||
transform: rotate(45deg);
|
|
||||||
transform-origin: left center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sky-searchbar__input {
|
.sky-searchbar__input {
|
||||||
|
|||||||
@@ -141,7 +141,13 @@ function disable(event: MouseEvent): void {
|
|||||||
<label v-if="label" class="sky-visually-hidden" :for="resolvedInputId">
|
<label v-if="label" class="sky-visually-hidden" :for="resolvedInputId">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
<span class="sky-searchbar__icon" aria-hidden="true" />
|
<svg class="sky-searchbar__icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M9.5 3a6.5 6.5 0 1 0 3.98 11.64l4.44 4.44a1 1 0 0 0 1.42-1.42l-4.44-4.44A6.5 6.5 0 0 0 9.5 3Zm0 2a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
<input
|
<input
|
||||||
:id="resolvedInputId"
|
:id="resolvedInputId"
|
||||||
ref="input"
|
ref="input"
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
PREVIEWABLE_BUILTIN_APP_IDS,
|
PREVIEWABLE_BUILTIN_APP_IDS,
|
||||||
} from '@/utils/appStorePreviews'
|
} from '@/utils/appStorePreviews'
|
||||||
|
|
||||||
|
function escapeRegExp(value: string): string {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
}
|
||||||
|
|
||||||
describe('App Store preview catalog', () => {
|
describe('App Store preview catalog', () => {
|
||||||
it('contains a real captured screenshot for every built-in store app', () => {
|
it('contains a real captured screenshot for every built-in store app', () => {
|
||||||
const storeAppIds = PHONE_APPS.filter(
|
const storeAppIds = PHONE_APPS.filter(
|
||||||
@@ -56,10 +60,11 @@ describe('App Store preview catalog', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
for (const appId of PREVIEWABLE_BUILTIN_APP_IDS) {
|
for (const appId of PREVIEWABLE_BUILTIN_APP_IDS) {
|
||||||
|
const escapedAppId = escapeRegExp(appId)
|
||||||
for (const source of localeSources) {
|
for (const source of localeSources) {
|
||||||
expect(source).toMatch(
|
expect(source).toMatch(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`(?:["']${appId}["']\\]?|${appId.replace(/-/g, '\\-')})\\s*[:=]\\s*\\{\\s*first\\s*[:=]`,
|
`(?:["']${escapedAppId}["']\\]?|${escapedAppId})\\s*[:=]\\s*\\{\\s*first\\s*[:=]`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,8 +112,16 @@ describe('LB Phone app bridge', () => {
|
|||||||
expect(document).toContain('https://cfx-nui-snake_app/ui/dist/')
|
expect(document).toContain('https://cfx-nui-snake_app/ui/dist/')
|
||||||
expect(document).not.toContain('</script><script>window.injected=true')
|
expect(document).not.toContain('</script><script>window.injected=true')
|
||||||
|
|
||||||
const runtime = /<script>([\s\S]*?)<\/script>/.exec(document)?.[1]
|
const openingTag = '<script>'
|
||||||
expect(runtime).toBeTruthy()
|
const runtimeStart = document.indexOf(openingTag)
|
||||||
expect(() => new Function(runtime ?? '')).not.toThrow()
|
const runtimeEnd = document.indexOf(
|
||||||
|
'</script>',
|
||||||
|
runtimeStart + openingTag.length,
|
||||||
|
)
|
||||||
|
expect(runtimeStart).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(runtimeEnd).toBeGreaterThan(runtimeStart)
|
||||||
|
|
||||||
|
const runtime = document.slice(runtimeStart + openingTag.length, runtimeEnd)
|
||||||
|
expect(() => new Function(runtime)).not.toThrow()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,4 +47,12 @@ describe('notes rich text', () => {
|
|||||||
'Briefing\nMeet outside.\n• Radio\n• Vest',
|
'Briefing\nMeet outside.\n• Radio\n• Vest',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps encoded markup as literal preview text', () => {
|
||||||
|
const body = serializeRichNoteBody(
|
||||||
|
'<p><script>literal</script></p>',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(noteBodyToPlainText(body)).toBe('<script>literal</script>')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { isTextInputElement } from '@/utils/textInputFocus'
|
||||||
|
|
||||||
|
function element(
|
||||||
|
tagName: string,
|
||||||
|
attributes: Record<string, string> = {},
|
||||||
|
isContentEditable = false,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
getAttribute(name: string) {
|
||||||
|
return attributes[name] ?? null
|
||||||
|
},
|
||||||
|
isContentEditable,
|
||||||
|
tagName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('text input focus', () => {
|
||||||
|
it('recognizes fields that accept typed text', () => {
|
||||||
|
expect(isTextInputElement(element('INPUT'))).toBe(true)
|
||||||
|
expect(isTextInputElement(element('INPUT', { type: 'number' }))).toBe(true)
|
||||||
|
expect(isTextInputElement(element('TEXTAREA'))).toBe(true)
|
||||||
|
expect(isTextInputElement(element('DIV', {}, true))).toBe(true)
|
||||||
|
expect(isTextInputElement(element('DIV', { role: 'textbox' }))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores non-text and read-only controls', () => {
|
||||||
|
expect(isTextInputElement(element('INPUT', { type: 'checkbox' }))).toBe(
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
expect(isTextInputElement(element('INPUT', { type: 'range' }))).toBe(false)
|
||||||
|
expect(isTextInputElement(element('INPUT', { readonly: '' }))).toBe(false)
|
||||||
|
expect(isTextInputElement(element('BUTTON'))).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const nonTextInputTypes = new Set([
|
||||||
|
'button',
|
||||||
|
'checkbox',
|
||||||
|
'color',
|
||||||
|
'file',
|
||||||
|
'hidden',
|
||||||
|
'image',
|
||||||
|
'radio',
|
||||||
|
'range',
|
||||||
|
'reset',
|
||||||
|
'submit',
|
||||||
|
])
|
||||||
|
|
||||||
|
type FocusableElement = Pick<
|
||||||
|
HTMLElement,
|
||||||
|
'getAttribute' | 'isContentEditable' | 'tagName'
|
||||||
|
>
|
||||||
|
|
||||||
|
export function isTextInputElement(element: FocusableElement): boolean {
|
||||||
|
if (
|
||||||
|
element.getAttribute('readonly') !== null ||
|
||||||
|
element.getAttribute('aria-readonly') === 'true'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (element.isContentEditable || element.getAttribute('role') === 'textbox') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (element.tagName === 'TEXTAREA') return true
|
||||||
|
if (element.tagName !== 'INPUT') return false
|
||||||
|
|
||||||
|
const inputType = element.getAttribute('type')?.toLowerCase() ?? 'text'
|
||||||
|
return !nonTextInputTypes.has(inputType)
|
||||||
|
}
|
||||||
@@ -19,7 +19,17 @@ const mainCss = readFileSync(
|
|||||||
new URL('../assets/main.css', import.meta.url),
|
new URL('../assets/main.css', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
)
|
)
|
||||||
|
const builtInWallpaperCss = mainCss.slice(
|
||||||
|
mainCss.indexOf('.wallpaper--midnight'),
|
||||||
|
mainCss.indexOf('.wallpaper--custom'),
|
||||||
|
)
|
||||||
describe('Springboard page swipe contract', () => {
|
describe('Springboard page swipe contract', () => {
|
||||||
|
it('keeps the built-in wallpapers visually restrained', () => {
|
||||||
|
expect(builtInWallpaperCss).not.toMatch(/(?:conic|repeating-\w+)-gradient/)
|
||||||
|
expect(builtInWallpaperCss.match(/radial-gradient/g)).toHaveLength(12)
|
||||||
|
expect(builtInWallpaperCss.match(/linear-gradient/g)).toHaveLength(12)
|
||||||
|
})
|
||||||
|
|
||||||
it('replaces the home status row with the edit controls', () => {
|
it('replaces the home status row with the edit controls', () => {
|
||||||
expect(viewSource).toContain("emit('editModeChange', editing)")
|
expect(viewSource).toContain("emit('editModeChange', editing)")
|
||||||
expect(viewSource).toContain("emit('editModeChange', false)")
|
expect(viewSource).toContain("emit('editModeChange', false)")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('phone app theme contract', () => {
|
|||||||
it('provides the reactive phone theme to every routed app', () => {
|
it('provides the reactive phone theme to every routed app', () => {
|
||||||
expect(appShellSource).toContain('import { SkyProvider }')
|
expect(appShellSource).toContain('import { SkyProvider }')
|
||||||
expect(appShellSource).toContain('class="phone-app-theme"')
|
expect(appShellSource).toContain('class="phone-app-theme"')
|
||||||
expect(appShellSource).toContain(':dark="phone.isDarkMode"')
|
expect(appShellSource).toContain(':dark="displayedDarkMode"')
|
||||||
expect(appShellSource).toMatch(
|
expect(appShellSource).toMatch(
|
||||||
/<SkyProvider[\s\S]*?<RouterView[\s\S]*?<component/,
|
/<SkyProvider[\s\S]*?<RouterView[\s\S]*?<component/,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ describe('SettingsApp Sky UI contract', () => {
|
|||||||
expect(source).not.toMatch(/<\/?k-[a-z]/)
|
expect(source).not.toMatch(/<\/?k-[a-z]/)
|
||||||
expect(source).toContain('<SkyAppPage')
|
expect(source).toContain('<SkyAppPage')
|
||||||
expect(source).toContain('<SkyNavbar')
|
expect(source).toContain('<SkyNavbar')
|
||||||
|
expect(source).toContain('class="settings-navbar"')
|
||||||
|
expect(source).toContain(
|
||||||
|
'.settings-navbar.sky-navbar--large.sky-navbar--no-navigation',
|
||||||
|
)
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
":variant=\"activeView === 'root' ? 'large' : 'compact'\"",
|
":variant=\"activeView === 'root' ? 'large' : 'compact'\"",
|
||||||
)
|
)
|
||||||
@@ -64,4 +68,10 @@ describe('SettingsApp Sky UI contract', () => {
|
|||||||
expect(source).toContain("phone.t('Apps.settings.keepCloudData')")
|
expect(source).toContain("phone.t('Apps.settings.keepCloudData')")
|
||||||
expect(source).toContain('phone.resetAfterFactoryReset()')
|
expect(source).toContain('phone.resetAfterFactoryReset()')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('provides a non-destructive development preview for the reset progress screen', () => {
|
||||||
|
expect(source).toContain('import.meta.env.DEV')
|
||||||
|
expect(source).toContain("has('factoryResetPreview')")
|
||||||
|
expect(source).toContain('factoryResetProgress.value = 46')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -740,6 +740,14 @@ async function confirmSimEject(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (
|
||||||
|
import.meta.env.DEV &&
|
||||||
|
new URLSearchParams(window.location.search).has('factoryResetPreview')
|
||||||
|
) {
|
||||||
|
factoryResetting.value = true
|
||||||
|
factoryResetProgress.value = 46
|
||||||
|
}
|
||||||
|
|
||||||
if (route.query.wallpaper === '1') {
|
if (route.query.wallpaper === '1') {
|
||||||
activeView.value = 'wallpaper'
|
activeView.value = 'wallpaper'
|
||||||
wallpaperTarget.value =
|
wallpaperTarget.value =
|
||||||
@@ -771,6 +779,7 @@ onBeforeUnmount(() => {
|
|||||||
:label="phone.t('Apps.settings.name')"
|
:label="phone.t('Apps.settings.name')"
|
||||||
>
|
>
|
||||||
<SkyNavbar
|
<SkyNavbar
|
||||||
|
class="settings-navbar"
|
||||||
:title="
|
:title="
|
||||||
activeView === 'root' ? phone.t('Apps.settings.name') : activeTitle
|
activeView === 'root' ? phone.t('Apps.settings.name') : activeTitle
|
||||||
"
|
"
|
||||||
@@ -1699,17 +1708,8 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<div class="settings-reset-content">
|
<div class="settings-reset-content">
|
||||||
<div class="settings-reset-mark" aria-hidden="true">
|
<div class="settings-reset-mark" aria-hidden="true">
|
||||||
<span
|
|
||||||
class="settings-reset-mark__layer settings-reset-mark__layer--back"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
class="settings-reset-mark__layer settings-reset-mark__layer--middle"
|
|
||||||
></span>
|
|
||||||
<div class="settings-reset-mark__face">
|
<div class="settings-reset-mark__face">
|
||||||
<Smartphone :size="35" :stroke-width="1.45" />
|
<Smartphone :size="38" :stroke-width="1.45" />
|
||||||
<span class="settings-reset-mark__erase">
|
|
||||||
<i></i><i></i><i></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1872,6 +1872,16 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.settings-search {
|
||||||
margin-bottom: var(--sky-space-4);
|
margin-bottom: var(--sky-space-4);
|
||||||
}
|
}
|
||||||
@@ -2510,6 +2520,102 @@ onBeforeUnmount(() => {
|
|||||||
line-height: 14px;
|
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) {
|
.settings-dialog-button--danger:not(:disabled) {
|
||||||
background: var(--sky-danger);
|
background: var(--sky-danger);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const lifecycleEndpoints = new Set([
|
|||||||
'device:notification-open',
|
'device:notification-open',
|
||||||
'notification:focus',
|
'notification:focus',
|
||||||
'sim:picker-close',
|
'sim:picker-close',
|
||||||
|
'ui:input-focus',
|
||||||
'ui:opened',
|
'ui:opened',
|
||||||
'ui:ready',
|
'ui:ready',
|
||||||
])
|
])
|
||||||
@@ -4656,7 +4657,7 @@ app.post('/api/:endpoint', async (request, response, next) => {
|
|||||||
if (endpoint === 'memos:devCapture') {
|
if (endpoint === 'memos:devCapture') {
|
||||||
loggedBody.audioDataUrl = `<${String(request.body.audioDataUrl ?? '').length} characters>`
|
loggedBody.audioDataUrl = `<${String(request.body.audioDataUrl ?? '').length} characters>`
|
||||||
}
|
}
|
||||||
console.log(`[NUI] ${endpoint}`, loggedBody)
|
console.log('[NUI]', endpoint, loggedBody)
|
||||||
if (endpoint === 'music:bootstrap') {
|
if (endpoint === 'music:bootstrap') {
|
||||||
response.json({ success: true, data: musicBootstrap() })
|
response.json({ success: true, data: musicBootstrap() })
|
||||||
return
|
return
|
||||||
@@ -10407,6 +10408,16 @@ app.post('/api/:endpoint', (request, response) => {
|
|||||||
mockPasscode = ''
|
mockPasscode = ''
|
||||||
mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
|
mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
|
||||||
for (const key of Object.keys(deviceData)) delete deviceData[key]
|
for (const key of Object.keys(deviceData)) delete deviceData[key]
|
||||||
|
Object.assign(deviceData, {
|
||||||
|
apps: { payload: { claimedApps: [] }, revision: 0 },
|
||||||
|
settings: {
|
||||||
|
payload: {
|
||||||
|
settings: { setupCompleted: false, setupStep: 0 },
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
revision: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
response.json({ success: true })
|
response.json({ success: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1156,6 +1156,7 @@ async function main() {
|
|||||||
'device:notification-open',
|
'device:notification-open',
|
||||||
'notification:focus',
|
'notification:focus',
|
||||||
'sim:picker-close',
|
'sim:picker-close',
|
||||||
|
'ui:input-focus',
|
||||||
'ui:opened',
|
'ui:opened',
|
||||||
'ui:ready',
|
'ui:ready',
|
||||||
]
|
]
|
||||||
@@ -1163,6 +1164,31 @@ async function main() {
|
|||||||
await expectSuccess(baseUrl, endpoint)
|
await expectSuccess(baseUrl, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await expectSuccess(baseUrl, 'device:factory-reset')
|
||||||
|
const resetBootstrap = await expectSuccess(
|
||||||
|
baseUrl,
|
||||||
|
'development:bootstrap',
|
||||||
|
{ _testScenario: 'setupPreview' },
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
resetBootstrap.device.data.settings.payload.settings.setupCompleted,
|
||||||
|
false,
|
||||||
|
'factory reset did not restore a browser-testable setup state',
|
||||||
|
)
|
||||||
|
|
||||||
|
const loggedRequests = []
|
||||||
|
const originalConsoleLog = console.log
|
||||||
|
try {
|
||||||
|
console.log = (...values) => loggedRequests.push(values)
|
||||||
|
await post(baseUrl, '%25s', { marker: 'format-string' })
|
||||||
|
} finally {
|
||||||
|
console.log = originalConsoleLog
|
||||||
|
}
|
||||||
|
assert.deepEqual(loggedRequests, [
|
||||||
|
['[NUI]', '%s', { marker: 'format-string' }],
|
||||||
|
])
|
||||||
|
|
||||||
const unknown = await post(baseUrl, 'development:missing-mock', {})
|
const unknown = await post(baseUrl, 'development:missing-mock', {})
|
||||||
assert.deepEqual(unknown, {
|
assert.deepEqual(unknown, {
|
||||||
error: 'mock_endpoint_missing',
|
error: 'mock_endpoint_missing',
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ server_scripts {
|
|||||||
'config/media.lua',
|
'config/media.lua',
|
||||||
'config/locales/en.lua',
|
'config/locales/en.lua',
|
||||||
'config/locales/de.lua',
|
'config/locales/de.lua',
|
||||||
|
'source/server/update_check.lua',
|
||||||
'source/bridge/server/database.lua',
|
'source/bridge/server/database.lua',
|
||||||
'source/bridge/server/migrations.lua',
|
'source/bridge/server/migrations.lua',
|
||||||
'source/bridge/server/callbacks.lua',
|
'source/bridge/server/callbacks.lua',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
SkyPhoneFocus = {}
|
SkyPhoneFocus = {}
|
||||||
|
|
||||||
local blocked_phone_controls = { 19, 24, 140, 141, 142, 257, 263, 264 }
|
local blocked_phone_controls = { 24, 140, 141, 142, 257, 263, 264 }
|
||||||
local blocked_phone_look_controls = { 1, 2, 3, 4, 5, 6 }
|
local blocked_phone_look_controls = { 1, 2, 3, 4, 5, 6 }
|
||||||
local focused_control_groups = { 0, 1, 2 }
|
local focused_control_groups = { 0, 1, 2 }
|
||||||
|
|
||||||
@@ -39,10 +39,10 @@ function SkyPhoneFocus.Resolve(state)
|
|||||||
or state.payphone_focus
|
or state.payphone_focus
|
||||||
or state.sim_picker_open
|
or state.sim_picker_open
|
||||||
or (state.camera_active and state.camera_nui_focused)
|
or (state.camera_active and state.camera_nui_focused)
|
||||||
local cursor = focused and not (game_input and state.cursor_disabled)
|
local cursor = focused
|
||||||
return {
|
return {
|
||||||
block_game = cursor,
|
block_game = cursor and (not game_input or state.text_input_focused),
|
||||||
block_look = game_input and not state.cursor_disabled,
|
block_look = game_input,
|
||||||
cursor = cursor,
|
cursor = cursor,
|
||||||
focused = focused,
|
focused = focused,
|
||||||
game_input = game_input,
|
game_input = game_input,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ local activity_suspended = false
|
|||||||
local phone_block_game = false
|
local phone_block_game = false
|
||||||
local phone_block_look = false
|
local phone_block_look = false
|
||||||
local phone_game_input = false
|
local phone_game_input = false
|
||||||
local phone_cursor_disabled = false
|
local phone_text_input_focused = false
|
||||||
|
|
||||||
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsOpen", function()
|
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsOpen", function()
|
||||||
return is_open
|
return is_open
|
||||||
@@ -327,20 +327,17 @@ local function update_nui_focus()
|
|||||||
call_focus = call_focus,
|
call_focus = call_focus,
|
||||||
camera_active = camera_active,
|
camera_active = camera_active,
|
||||||
camera_nui_focused = camera_nui_focused,
|
camera_nui_focused = camera_nui_focused,
|
||||||
cursor_disabled = phone_cursor_disabled,
|
|
||||||
is_open = is_open,
|
is_open = is_open,
|
||||||
notification_focus = notification_focus,
|
notification_focus = notification_focus,
|
||||||
payphone_focus = payphone_focus,
|
payphone_focus = payphone_focus,
|
||||||
sim_picker_open = sim_picker_open,
|
sim_picker_open = sim_picker_open,
|
||||||
|
text_input_focused = phone_text_input_focused,
|
||||||
})
|
})
|
||||||
SetNuiFocus(focus.focused, focus.cursor)
|
SetNuiFocus(focus.focused, focus.cursor)
|
||||||
SetNuiFocusKeepInput(focus.keep_input)
|
SetNuiFocusKeepInput(focus.keep_input)
|
||||||
phone_block_game = focus.block_game == true
|
phone_block_game = focus.block_game == true
|
||||||
phone_block_look = focus.block_look == true
|
phone_block_look = focus.block_look == true
|
||||||
phone_game_input = focus.game_input
|
phone_game_input = focus.game_input
|
||||||
if not phone_game_input and not phone_block_game then
|
|
||||||
phone_cursor_disabled = false
|
|
||||||
end
|
|
||||||
TriggerEvent("sky_phone:client:cameraFocusApplied", {
|
TriggerEvent("sky_phone:client:cameraFocusApplied", {
|
||||||
active = camera_active,
|
active = camera_active,
|
||||||
cursor = focus.cursor,
|
cursor = focus.cursor,
|
||||||
@@ -357,10 +354,6 @@ CreateThread(function()
|
|||||||
else
|
else
|
||||||
SkyPhoneFocus.ApplyGameInputControls(phone_block_look)
|
SkyPhoneFocus.ApplyGameInputControls(phone_block_look)
|
||||||
end
|
end
|
||||||
if IsDisabledControlJustPressed(0, 19) then
|
|
||||||
phone_cursor_disabled = not phone_cursor_disabled
|
|
||||||
update_nui_focus()
|
|
||||||
end
|
|
||||||
Wait(0)
|
Wait(0)
|
||||||
else
|
else
|
||||||
Wait(250)
|
Wait(250)
|
||||||
@@ -418,6 +411,7 @@ local function close_phone(close_device_session)
|
|||||||
open_requested = false
|
open_requested = false
|
||||||
call_focus = false
|
call_focus = false
|
||||||
activity_suspended = false
|
activity_suspended = false
|
||||||
|
phone_text_input_focused = false
|
||||||
TriggerEvent("sky_phone:animation:phone", false)
|
TriggerEvent("sky_phone:animation:phone", false)
|
||||||
is_open = false
|
is_open = false
|
||||||
if was_open then
|
if was_open then
|
||||||
@@ -500,6 +494,7 @@ RegisterNUICallback("ui:ready", function(data, cb)
|
|||||||
-- Browser state is recreated on a CEF reload. A notification focus claim
|
-- Browser state is recreated on a CEF reload. A notification focus claim
|
||||||
-- cannot survive unless its notification is replayed as part of this handshake.
|
-- cannot survive unless its notification is replayed as part of this handshake.
|
||||||
notification_focus = false
|
notification_focus = false
|
||||||
|
phone_text_input_focused = false
|
||||||
Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true })
|
Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true })
|
||||||
SkyPhoneApps.SendCatalog()
|
SkyPhoneApps.SendCatalog()
|
||||||
if open_requested and device_payload then
|
if open_requested and device_payload then
|
||||||
@@ -555,6 +550,16 @@ RegisterNUICallback("ui:opened", function(data, cb)
|
|||||||
cb({ success = true })
|
cb({ success = true })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
RegisterNUICallback("ui:input-focus", function(data, cb)
|
||||||
|
if type(data) ~= "table" or type(data.active) ~= "boolean" then
|
||||||
|
cb({ success = false, error = "invalid_request" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
phone_text_input_focused = data.active and (is_open or open_requested)
|
||||||
|
update_nui_focus()
|
||||||
|
cb({ success = true })
|
||||||
|
end)
|
||||||
|
|
||||||
RegisterNUICallback("close", function(data, cb)
|
RegisterNUICallback("close", function(data, cb)
|
||||||
if type(data) ~= "table" then
|
if type(data) ~= "table" then
|
||||||
cb({ success = false, error = "invalid_request" })
|
cb({ success = false, error = "invalid_request" })
|
||||||
@@ -1086,7 +1091,7 @@ AddEventHandler("onResourceStop", function(resource_name)
|
|||||||
active_call_payload = nil
|
active_call_payload = nil
|
||||||
activity_suspended = false
|
activity_suspended = false
|
||||||
phone_game_input = false
|
phone_game_input = false
|
||||||
phone_cursor_disabled = false
|
phone_text_input_focused = false
|
||||||
SetNuiFocusKeepInput(false)
|
SetNuiFocusKeepInput(false)
|
||||||
SetNuiFocus(false, false)
|
SetNuiFocus(false, false)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
local RELEASE_API_URL = "https://api.github.com/repos/sky-systems/sky_phone/releases/latest"
|
||||||
|
local RELEASE_PAGE_URL = "https://github.com/sky-systems/sky_phone/releases/latest"
|
||||||
|
|
||||||
|
local function parse_version(version)
|
||||||
|
if type(version) ~= "string" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local major, minor, patch = version:match("^v?(%d+)%.(%d+)%.(%d+)$")
|
||||||
|
if not major then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return tonumber(major), tonumber(minor), tonumber(patch)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function compare_versions(installed_version, release_version)
|
||||||
|
local installed_major, installed_minor, installed_patch = parse_version(installed_version)
|
||||||
|
local release_major, release_minor, release_patch = parse_version(release_version)
|
||||||
|
if not installed_major or not release_major then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if installed_major ~= release_major then
|
||||||
|
return installed_major < release_major and -1 or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if installed_minor ~= release_minor then
|
||||||
|
return installed_minor < release_minor and -1 or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if installed_patch ~= release_patch then
|
||||||
|
return installed_patch < release_patch and -1 or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function print_update_notice(installed_version, release_version)
|
||||||
|
local border = "======================================================================"
|
||||||
|
print(([[
|
||||||
|
^1%s^0
|
||||||
|
^1 SKY PHONE UPDATE AVAILABLE ^0
|
||||||
|
^1%s^0
|
||||||
|
^3 Installed version: ^1%s^0
|
||||||
|
^3 Latest release: ^2%s^0
|
||||||
|
|
||||||
|
^5 Download: %s^0
|
||||||
|
^1%s^0]]):format(border, border, installed_version, release_version, RELEASE_PAGE_URL, border))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_for_update()
|
||||||
|
local resource_name = GetCurrentResourceName()
|
||||||
|
local installed_version = GetResourceMetadata(resource_name, "version", 0)
|
||||||
|
if not parse_version(installed_version) then
|
||||||
|
print(("^3[sky_phone] Update check skipped because fxmanifest.lua has an invalid version: %s.^0")
|
||||||
|
:format(tostring(installed_version)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
PerformHttpRequest(RELEASE_API_URL, function(status_code, response_body)
|
||||||
|
if status_code ~= 200 then
|
||||||
|
print(("^3[sky_phone] GitHub update check failed with HTTP %s.^0"):format(tostring(status_code)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local decoded, release = pcall(json.decode, response_body)
|
||||||
|
local release_version = decoded and type(release) == "table" and release.tag_name or nil
|
||||||
|
local comparison = compare_versions(installed_version, release_version)
|
||||||
|
if not comparison then
|
||||||
|
print(("^3[sky_phone] GitHub update check returned an invalid release tag: %s.^0")
|
||||||
|
:format(tostring(release_version)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if comparison < 0 then
|
||||||
|
print_update_notice(installed_version, release_version)
|
||||||
|
elseif comparison > 0 then
|
||||||
|
print(("^3[sky_phone] Installed version %s is newer than the latest GitHub release %s.^0")
|
||||||
|
:format(installed_version, release_version))
|
||||||
|
else
|
||||||
|
print(("^2[sky_phone] Version %s is up to date.^0"):format(installed_version))
|
||||||
|
end
|
||||||
|
end, "GET", "", {
|
||||||
|
["Accept"] = "application/vnd.github+json",
|
||||||
|
["User-Agent"] = "sky_phone-update-check",
|
||||||
|
["X-GitHub-Api-Version"] = "2022-11-28",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
CreateThread(check_for_update)
|
||||||
+16
-14
@@ -29,11 +29,11 @@ local function resolve(overrides)
|
|||||||
call_focus = false,
|
call_focus = false,
|
||||||
camera_active = false,
|
camera_active = false,
|
||||||
camera_nui_focused = true,
|
camera_nui_focused = true,
|
||||||
cursor_disabled = false,
|
|
||||||
is_open = false,
|
is_open = false,
|
||||||
notification_focus = false,
|
notification_focus = false,
|
||||||
payphone_focus = false,
|
payphone_focus = false,
|
||||||
sim_picker_open = false,
|
sim_picker_open = false,
|
||||||
|
text_input_focused = false,
|
||||||
}
|
}
|
||||||
for key, value in pairs(overrides or {}) do
|
for key, value in pairs(overrides or {}) do
|
||||||
state[key] = value
|
state[key] = value
|
||||||
@@ -71,22 +71,23 @@ assert(
|
|||||||
and movable_phone.focused
|
and movable_phone.focused
|
||||||
and movable_phone.keep_input
|
and movable_phone.keep_input
|
||||||
and movable_phone.game_input
|
and movable_phone.game_input
|
||||||
and movable_phone.block_game,
|
and not movable_phone.block_game
|
||||||
"an open phone with the cursor active must block GTA hotkeys while keeping NUI input"
|
and movable_phone.block_look,
|
||||||
|
"an open phone must allow movement without hiding the NUI cursor"
|
||||||
)
|
)
|
||||||
|
|
||||||
local cursor_disabled_phone = resolve({
|
local typing_phone = resolve({
|
||||||
allow_movement = true,
|
allow_movement = true,
|
||||||
cursor_disabled = true,
|
|
||||||
is_open = true,
|
is_open = true,
|
||||||
|
text_input_focused = true,
|
||||||
})
|
})
|
||||||
assert(
|
assert(
|
||||||
not cursor_disabled_phone.cursor
|
typing_phone.cursor
|
||||||
and cursor_disabled_phone.focused
|
and typing_phone.focused
|
||||||
and cursor_disabled_phone.keep_input
|
and typing_phone.keep_input
|
||||||
and cursor_disabled_phone.game_input
|
and typing_phone.game_input
|
||||||
and not cursor_disabled_phone.block_game,
|
and typing_phone.block_game,
|
||||||
"toggling Alt must release the NUI cursor while preserving phone and GTA input"
|
"a focused phone text input must block GTA controls without hiding the NUI cursor"
|
||||||
)
|
)
|
||||||
|
|
||||||
SkyPhoneFocus.ApplyFocusedControls()
|
SkyPhoneFocus.ApplyFocusedControls()
|
||||||
@@ -98,9 +99,10 @@ assert(firing_disabled, "focused phone cursor must block attacks while typing")
|
|||||||
all_controls_disabled = {}
|
all_controls_disabled = {}
|
||||||
firing_disabled = false
|
firing_disabled = false
|
||||||
SkyPhoneFocus.ApplyGameInputControls(true)
|
SkyPhoneFocus.ApplyGameInputControls(true)
|
||||||
for _, control in ipairs({ 19, 24, 140, 141, 142, 257, 263, 264 }) do
|
for _, control in ipairs({ 24, 140, 141, 142, 257, 263, 264 }) do
|
||||||
assert(disabled_controls[control], ("phone control %d must remain disabled"):format(control))
|
assert(disabled_controls[control], ("phone control %d must remain disabled"):format(control))
|
||||||
end
|
end
|
||||||
|
assert(not disabled_controls[19], "Alt must remain available while no phone text input is focused")
|
||||||
for _, control in ipairs({ 1, 2, 3, 4, 5, 6 }) do
|
for _, control in ipairs({ 1, 2, 3, 4, 5, 6 }) do
|
||||||
assert(disabled_controls[control], ("look control %d must be disabled while the phone cursor is active"):format(control))
|
assert(disabled_controls[control], ("look control %d must be disabled while the phone cursor is active"):format(control))
|
||||||
end
|
end
|
||||||
@@ -111,8 +113,8 @@ assert(firing_disabled, "player attacks must remain disabled while the phone is
|
|||||||
disabled_controls = {}
|
disabled_controls = {}
|
||||||
firing_disabled = false
|
firing_disabled = false
|
||||||
SkyPhoneFocus.ApplyGameInputControls(false)
|
SkyPhoneFocus.ApplyGameInputControls(false)
|
||||||
assert(not disabled_controls[1] and not disabled_controls[2], "Alt cursor toggle must restore camera look")
|
assert(not disabled_controls[1] and not disabled_controls[2], "camera passthrough must preserve camera look")
|
||||||
assert(firing_disabled, "player attacks must remain disabled after the cursor is toggled off")
|
assert(firing_disabled, "player attacks must remain disabled during camera passthrough")
|
||||||
|
|
||||||
local movable_notification = resolve({ allow_movement = true, notification_focus = true })
|
local movable_notification = resolve({ allow_movement = true, notification_focus = true })
|
||||||
assert(
|
assert(
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
local source_path = "sky_phone/source/server/update_check.lua"
|
||||||
|
local original_print = print
|
||||||
|
|
||||||
|
local function run_check(installed_version, status_code, release_version)
|
||||||
|
local output = {}
|
||||||
|
local request
|
||||||
|
|
||||||
|
print = function(message)
|
||||||
|
output[#output + 1] = tostring(message)
|
||||||
|
end
|
||||||
|
GetCurrentResourceName = function()
|
||||||
|
return "sky_phone"
|
||||||
|
end
|
||||||
|
GetResourceMetadata = function(resource_name, key, index)
|
||||||
|
assert(resource_name == "sky_phone", "update check must read its own resource metadata")
|
||||||
|
assert(key == "version" and index == 0, "update check must read the fxmanifest version")
|
||||||
|
return installed_version
|
||||||
|
end
|
||||||
|
PerformHttpRequest = function(url, callback, method, body, headers)
|
||||||
|
request = {
|
||||||
|
url = url,
|
||||||
|
callback = callback,
|
||||||
|
method = method,
|
||||||
|
body = body,
|
||||||
|
headers = headers,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
CreateThread = function(callback)
|
||||||
|
callback()
|
||||||
|
end
|
||||||
|
json = {
|
||||||
|
decode = function()
|
||||||
|
return { tag_name = release_version }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
dofile(source_path)
|
||||||
|
|
||||||
|
if request then
|
||||||
|
assert(request.url == "https://api.github.com/repos/sky-systems/sky_phone/releases/latest")
|
||||||
|
assert(request.method == "GET" and request.body == "", "update check must use a read-only GET request")
|
||||||
|
assert(request.headers["Accept"] == "application/vnd.github+json")
|
||||||
|
assert(request.headers["User-Agent"] == "sky_phone-update-check")
|
||||||
|
request.callback(status_code, "{}")
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(output, "\n"), request
|
||||||
|
end
|
||||||
|
|
||||||
|
local current_output = run_check("0.1.0", 200, "0.1.0")
|
||||||
|
assert(current_output:find("Version 0.1.0 is up to date", 1, true), "matching versions must report up to date")
|
||||||
|
|
||||||
|
local outdated_output = run_check("0.1.0", 200, "0.2.0")
|
||||||
|
assert(outdated_output:find("SKY PHONE UPDATE AVAILABLE", 1, true), "newer releases must show an update notice")
|
||||||
|
assert(outdated_output:find("Installed version: ^10.1.0", 1, true), "notice must show the manifest version")
|
||||||
|
assert(outdated_output:find("Latest release: ^20.2.0", 1, true), "notice must show the release tag")
|
||||||
|
|
||||||
|
local ahead_output = run_check("1.0.0", 200, "0.9.9")
|
||||||
|
assert(ahead_output:find("newer than the latest GitHub release", 1, true), "ahead versions must be distinguished")
|
||||||
|
|
||||||
|
local invalid_output, invalid_request = run_check("development", 200, "0.1.0")
|
||||||
|
assert(not invalid_request, "invalid manifest versions must not make an HTTP request")
|
||||||
|
assert(invalid_output:find("fxmanifest.lua has an invalid version", 1, true), "invalid manifests must be visible")
|
||||||
|
|
||||||
|
local failed_output = run_check("0.1.0", 429, "0.1.0")
|
||||||
|
assert(failed_output:find("GitHub update check failed with HTTP 429", 1, true), "HTTP failures must be visible")
|
||||||
|
|
||||||
|
print = original_print
|
||||||
|
io.write("Sky Phone update check tests passed\n")
|
||||||
Reference in New Issue
Block a user