ADD - separate Home and Lock wallpapers

Store independent Home Screen and Lock Screen wallpaper selections, preserve legacy preferences, and route gallery or camera picks to the selected destination. Add the Settings segmented selector and contract coverage.
This commit is contained in:
Leon.Schmidt
2026-08-15 20:04:45 +02:00
parent f6fb680fbd
commit 14fcfe384d
9 changed files with 152 additions and 31 deletions
@@ -6,8 +6,18 @@ const styles = readFileSync(
new URL('../assets/main.css', import.meta.url),
'utf8',
)
const source = readFileSync(
new URL('./PhoneLockScreen.vue', import.meta.url),
'utf8',
)
describe('PhoneLockScreen notification theme contract', () => {
it('uses the dedicated Lock Screen wallpaper preference', () => {
expect(source).toContain('settings.lockWallpaperImageUrl')
expect(source).toContain('settings.lockWallpaper')
expect(source).not.toContain('settings.wallpaperImageUrl')
})
it('uses explicit light and dark notification material tokens', () => {
expect(styles).toMatch(
/\.lock-screen\s*\{[^}]*--lock-notification-background:\s*rgb\(38 36 40 \/ 76%\)[^}]*--lock-notification-color:\s*#fff/s,
+3 -3
View File
@@ -74,10 +74,10 @@ const time = computed(() =>
.trim(),
)
const dragStyle = computed<Record<string, string>>(() => {
const imageUrl = preferences.value.settings.wallpaperImageUrl
const imageUrl = preferences.value.settings.lockWallpaperImageUrl
return {
'--lock-drag': `${dragOffset.value}px`,
...(preferences.value.settings.wallpaper === 'custom' && imageUrl
...(preferences.value.settings.lockWallpaper === 'custom' && imageUrl
? { '--phone-wallpaper-image': `url(${JSON.stringify(imageUrl)})` }
: {}),
}
@@ -289,7 +289,7 @@ onBeforeUnmount(() => {
<section
class="lock-screen"
:class="[
`wallpaper--${preferences.settings.wallpaper}`,
`wallpaper--${preferences.settings.lockWallpaper}`,
{
'lock-screen--dragging': dragging,
'lock-screen--preview': preview,
@@ -115,7 +115,7 @@ function choosePerformance(mode: GraphicsMode): void {
}
function chooseWallpaper(wallpaper: Exclude<WallpaperId, 'custom'>): void {
phone.setWallpaper(wallpaper)
phone.setWallpaper(wallpaper, null, 'both')
}
function toggleApp(appId: BuiltinPhoneAppId): void {
+18 -3
View File
@@ -19,6 +19,7 @@ import {
type AppNotificationPreferences,
type PhonePreferencesV1,
type WallpaperId,
type WallpaperTarget,
} from '@/utils/preferences'
type LocaleTree = Record<string, unknown>
@@ -4062,6 +4063,9 @@ const defaultLocales: LocaleTree = {
wallpaperFromCameraDescription: 'Create a new wallpaper',
wallpaperHistory: 'Recent',
wallpaperCustom: 'Photo wallpaper',
wallpaperTarget: 'Wallpaper destination',
wallpaperHomeScreen: 'Home Screen',
wallpaperLockScreen: 'Lock Screen',
toggle: {
airplaneMode: 'Toggle Airplane Mode',
streamerMode: 'Toggle Streamer Mode',
@@ -4654,15 +4658,26 @@ export const usePhoneStore = defineStore('phone', {
setSystemDarkMode(value: boolean): void {
this.systemDarkMode = value
},
setWallpaper(wallpaper: WallpaperId, imageUrl: string | null = null): void {
setWallpaper(
wallpaper: WallpaperId,
imageUrl: string | null = null,
target: WallpaperTarget | 'both' = 'home',
): void {
const normalizedImageUrl =
wallpaper === 'custom' ? imageUrl?.trim() : null
if (wallpaper === 'custom' && !normalizedImageUrl) {
throw new Error('Custom wallpapers require a photo URL.')
}
this.preferences.settings.wallpaper = wallpaper
this.preferences.settings.wallpaperImageUrl = normalizedImageUrl ?? null
if (target === 'home' || target === 'both') {
this.preferences.settings.wallpaper = wallpaper
this.preferences.settings.wallpaperImageUrl = normalizedImageUrl ?? null
}
if (target === 'lock' || target === 'both') {
this.preferences.settings.lockWallpaper = wallpaper
this.preferences.settings.lockWallpaperImageUrl =
normalizedImageUrl ?? null
}
this.preferences.settings.wallpaperHistory = [
{ imageUrl: normalizedImageUrl ?? null, wallpaper },
...this.preferences.settings.wallpaperHistory.filter((entry) =>
+24
View File
@@ -196,6 +196,10 @@ describe('preferences', () => {
expect(value.settings.wallpaperImageUrl).toBe(
'https://media.example/current.jpg',
)
expect(value.settings.lockWallpaper).toBe('custom')
expect(value.settings.lockWallpaperImageUrl).toBe(
'https://media.example/current.jpg',
)
expect(value.settings.wallpaperHistory).toHaveLength(4)
expect(
value.settings.wallpaperHistory.map((entry) => entry.wallpaper),
@@ -213,4 +217,24 @@ describe('preferences', () => {
expect(value.settings.wallpaper).toBe('midnight')
expect(value.settings.wallpaperImageUrl).toBeNull()
})
it('restores independent Home Screen and Lock Screen wallpapers', () => {
const value = parsePhonePreferences(
JSON.stringify({
version: 1,
settings: {
wallpaper: 'ocean',
lockWallpaper: 'custom',
lockWallpaperImageUrl: 'https://media.example/lock.jpg',
},
}),
)
expect(value.settings.wallpaper).toBe('ocean')
expect(value.settings.wallpaperImageUrl).toBeNull()
expect(value.settings.lockWallpaper).toBe('custom')
expect(value.settings.lockWallpaperImageUrl).toBe(
'https://media.example/lock.jpg',
)
})
})
+21
View File
@@ -48,6 +48,7 @@ export type RingtoneId = (typeof RINGTONE_IDS)[number]
export type NotificationSoundId = (typeof NOTIFICATION_SOUND_IDS)[number]
export type BuiltInWallpaperId = (typeof WALLPAPER_IDS)[number]
export type WallpaperId = BuiltInWallpaperId | 'custom'
export type WallpaperTarget = 'home' | 'lock'
export type WallpaperHistoryEntry = {
imageUrl: string | null
wallpaper: WallpaperId
@@ -72,6 +73,8 @@ export type PhonePreferencesV1 = {
focusMode: boolean
frame: PhoneFrameId
graphicsMode: GraphicsMode
lockWallpaper: WallpaperId
lockWallpaperImageUrl: string | null
notificationSound: NotificationSoundId
notificationDurationSeconds: number
notificationVolume: number
@@ -145,6 +148,8 @@ export const DEFAULT_PHONE_PREFERENCES: PhonePreferencesV1 = {
focusMode: false,
frame: 'black',
graphicsMode: 'ultimate',
lockWallpaper: 'midnight',
lockWallpaperImageUrl: null,
notificationSound: 'chime',
notificationDurationSeconds: 10,
notificationVolume: 70,
@@ -280,6 +285,20 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
settings.wallpaper === 'custom' && wallpaperImageUrl
? 'custom'
: readChoice(settings.wallpaper, WALLPAPER_IDS, defaults.wallpaper)
const requestedLockWallpaperImageUrl = readWallpaperImageUrl(
settings.lockWallpaperImageUrl,
)
const lockWallpaper =
settings.lockWallpaper === 'custom' && requestedLockWallpaperImageUrl
? 'custom'
: typeof settings.lockWallpaper === 'string' &&
WALLPAPER_IDS.includes(settings.lockWallpaper as BuiltInWallpaperId)
? (settings.lockWallpaper as BuiltInWallpaperId)
: wallpaper
const lockWallpaperImageUrl =
lockWallpaper === 'custom'
? (requestedLockWallpaperImageUrl ?? wallpaperImageUrl)
: null
const wallpaperHistory = readWallpaperHistory(settings.wallpaperHistory)
return {
@@ -305,6 +324,8 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
GRAPHICS_MODE_IDS,
defaults.graphicsMode,
),
lockWallpaper,
lockWallpaperImageUrl,
notificationSound: readChoice(
settings.notificationSound,
NOTIFICATION_SOUND_IDS,
@@ -39,7 +39,9 @@ describe('SettingsApp Sky UI contract', () => {
it('offers photo, camera, recent, and two-column built-in wallpaper choices', () => {
expect(source).toContain("openWallpaperMedia('photos')")
expect(source).toContain("openWallpaperMedia('camera')")
expect(source).toContain("mediaPicker.consume('settings:wallpaper')")
expect(source).toContain('`settings:wallpaper:${wallpaperTarget.value}`')
expect(source).toContain("wallpaperTarget === 'home'")
expect(source).toContain("wallpaperTarget === 'lock'")
expect(source).toContain('class="settings-wallpaper-history"')
expect(source).toContain('class="settings-wallpaper-grid"')
expect(source).toMatch(
+71 -23
View File
@@ -89,6 +89,8 @@ import {
type PhoneFrameId,
type RingtoneId,
type WallpaperHistoryEntry,
type WallpaperId,
type WallpaperTarget,
} from '@/utils/preferences'
type SettingsView =
@@ -156,6 +158,7 @@ const resetOpened = ref(false)
const simEjectOpened = ref(false)
const factoryResetting = ref(false)
const factoryResetProgress = ref(0)
const wallpaperTarget = ref<WallpaperTarget>('home')
const selectedFrameColor = computed(
() => PHONE_FRAME_COLORS[phone.preferences.settings.frame],
)
@@ -201,6 +204,16 @@ let factoryResetAnimationFrame: number | undefined
const wallpaperHistory = computed(
() => phone.preferences.settings.wallpaperHistory,
)
const selectedWallpaper = computed<WallpaperId>(() =>
wallpaperTarget.value === 'home'
? phone.preferences.settings.wallpaper
: phone.preferences.settings.lockWallpaper,
)
const selectedWallpaperImageUrl = computed(() =>
wallpaperTarget.value === 'home'
? phone.preferences.settings.wallpaperImageUrl
: phone.preferences.settings.lockWallpaperImageUrl,
)
const toggleRows = [
{
@@ -273,10 +286,11 @@ function openSkyUiKitchenSink(): void {
}
function openWallpaperMedia(app: 'photos' | 'camera'): void {
const target = wallpaperTarget.value
mediaPicker.begin(
'settings:wallpaper',
`settings:wallpaper:${target}`,
'photo',
'/apps/settings?wallpaper=1',
`/apps/settings?wallpaper=1&wallpaperTarget=${target}`,
1,
)
void router.push({
@@ -293,7 +307,18 @@ function wallpaperPreviewStyle(
}
function selectWallpaperHistory(entry: WallpaperHistoryEntry): void {
phone.setWallpaper(entry.wallpaper, entry.imageUrl)
phone.setWallpaper(entry.wallpaper, entry.imageUrl, wallpaperTarget.value)
}
function isWallpaperSelected(entry: WallpaperHistoryEntry): boolean {
return (
selectedWallpaper.value === entry.wallpaper &&
selectedWallpaperImageUrl.value === entry.imageUrl
)
}
function selectBuiltInWallpaper(wallpaper: WallpaperId): void {
phone.setWallpaper(wallpaper, null, wallpaperTarget.value)
}
const connectivityRows = [
@@ -671,10 +696,18 @@ async function confirmSimEject(): Promise<void> {
}
onMounted(() => {
if (route.query.wallpaper === '1') activeView.value = 'wallpaper'
if (route.query.wallpaper === '1') {
activeView.value = 'wallpaper'
wallpaperTarget.value =
route.query.wallpaperTarget === 'lock' ? 'lock' : 'home'
}
const selectedPhoto = mediaPicker.consume('settings:wallpaper')
if (selectedPhoto) phone.setWallpaper('custom', selectedPhoto.url)
const selectedPhoto = mediaPicker.consume(
`settings:wallpaper:${wallpaperTarget.value}`,
)
if (selectedPhoto) {
phone.setWallpaper('custom', selectedPhoto.url, wallpaperTarget.value)
}
})
onBeforeUnmount(() => {
@@ -1459,6 +1492,28 @@ onBeforeUnmount(() => {
</template>
<template v-else-if="activeView === 'wallpaper'">
<SkySegmented
class="settings-wallpaper-target"
strong
rounded
:active-index="wallpaperTarget === 'home' ? 0 : 1"
:item-count="2"
:aria-label="phone.t('Apps.settings.wallpaperTarget')"
>
<SkySegmentedButton
:active="wallpaperTarget === 'home'"
@click="wallpaperTarget = 'home'"
>
{{ phone.t('Apps.settings.wallpaperHomeScreen') }}
</SkySegmentedButton>
<SkySegmentedButton
:active="wallpaperTarget === 'lock'"
@click="wallpaperTarget = 'lock'"
>
{{ phone.t('Apps.settings.wallpaperLockScreen') }}
</SkySegmentedButton>
</SkySegmented>
<section class="settings-wallpaper-actions">
<button type="button" @click="openWallpaperMedia('photos')">
<span class="settings-wallpaper-actions__icon" aria-hidden="true">
@@ -1502,9 +1557,7 @@ onBeforeUnmount(() => {
`wallpaper--${entry.wallpaper}`,
{
'settings-wallpaper-history__item--selected':
phone.preferences.settings.wallpaper === entry.wallpaper &&
phone.preferences.settings.wallpaperImageUrl ===
entry.imageUrl,
isWallpaperSelected(entry),
},
]"
:style="wallpaperPreviewStyle(entry)"
@@ -1515,14 +1568,7 @@ onBeforeUnmount(() => {
"
@click="selectWallpaperHistory(entry)"
>
<Check
v-if="
phone.preferences.settings.wallpaper === entry.wallpaper &&
phone.preferences.settings.wallpaperImageUrl ===
entry.imageUrl
"
aria-hidden="true"
/>
<Check v-if="isWallpaperSelected(entry)" aria-hidden="true" />
</button>
</div>
</section>
@@ -1537,10 +1583,10 @@ onBeforeUnmount(() => {
class="settings-wallpaper-choice"
:class="{
'settings-wallpaper-choice--selected':
phone.preferences.settings.wallpaper === wallpaper,
selectedWallpaper === wallpaper,
}"
:aria-pressed="phone.preferences.settings.wallpaper === wallpaper"
@click="phone.setWallpaper(wallpaper)"
:aria-pressed="selectedWallpaper === wallpaper"
@click="selectBuiltInWallpaper(wallpaper)"
>
<span
class="settings-wallpaper-choice__preview"
@@ -1548,9 +1594,7 @@ onBeforeUnmount(() => {
aria-hidden="true"
>
<span class="settings-wallpaper-choice__screen"></span>
<Check
v-if="phone.preferences.settings.wallpaper === wallpaper"
/>
<Check v-if="selectedWallpaper === wallpaper" />
</span>
<strong>{{
phone.t('Apps.settings.wallpapers.' + wallpaper)
@@ -1855,6 +1899,10 @@ onBeforeUnmount(() => {
box-shadow: 0 1px 3px rgb(0 0 0 / 18%);
}
.settings-wallpaper-target {
margin-bottom: var(--sky-space-4);
}
.settings-wallpaper-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
+1
View File
@@ -1856,6 +1856,7 @@ Locales["en"] = {
wallpaperFromPhotos = "Choose from Photos", wallpaperFromPhotosDescription = "Use one of your photos",
wallpaperFromCamera = "Take Photo", wallpaperFromCameraDescription = "Create a new wallpaper",
wallpaperHistory = "Recent", wallpaperCustom = "Photo wallpaper",
wallpaperTarget = "Wallpaper destination", wallpaperHomeScreen = "Home Screen", wallpaperLockScreen = "Lock Screen",
imei = "IMEI", linkedDevices = "Linked Devices", thisDevice = "This Phone", removeDevice = "Remove Device",
simCard = "SIM Card", simNumber = "Phone Number", simType = "SIM Type", registeredSim = "Registered",
anonymousSim = "Anonymous", noSim = "No SIM", ejectSim = "Eject SIM", ejectSimBody = "Return this SIM card to your inventory?",