ENH - overhaul wallpaper customization

This commit is contained in:
smx.pusha
2026-08-14 13:19:05 +02:00
parent 1c90c62e62
commit 33cf49e8b6
9 changed files with 917 additions and 39 deletions
+355 -9
View File
@@ -645,21 +645,367 @@ button {
}
.wallpaper--midnight {
background:
radial-gradient(circle at 70% 18%, #5a397eaa, transparent 35%),
radial-gradient(circle at 20% 72%, #164e7daa, transparent 37%),
linear-gradient(155deg, #080714, #111126 50%, #02040b);
radial-gradient(circle at 76% 17%, #ffffff 0 0.7px, transparent 1.2px),
radial-gradient(circle at 28% 8%, #9fceff 0 0.6px, transparent 1px),
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 {
background:
radial-gradient(circle at 72% 20%, #31e2b288, transparent 33%),
radial-gradient(circle at 15% 68%, #246fe099, transparent 42%),
linear-gradient(145deg, #07191b, #132147 55%, #081019);
radial-gradient(circle at 17% 14%, #dfffff 0 0.6px, transparent 1.1px),
radial-gradient(circle at 78% 9%, #c6e7ff 0 0.7px, transparent 1.2px),
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 {
background:
radial-gradient(circle at 70% 22%, #ff764f99, transparent 32%),
radial-gradient(circle at 15% 65%, #9a306f88, transparent 44%),
linear-gradient(150deg, #21090b, #29142e 58%, #080810);
radial-gradient(
circle at 80% 18%,
#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 {
background:
linear-gradient(116deg, transparent 43%, #c7fbff1c 47%, transparent 51%),
linear-gradient(64deg, transparent 44%, #8defff17 48%, transparent 52%),
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 {
background:
linear-gradient(18deg, #281c46 0 13%, transparent 13.3%),
linear-gradient(-22deg, #51304a 0 18%, transparent 18.3%),
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 {
background:
radial-gradient(
circle at 78% 14%,
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 {
background:
radial-gradient(
ellipse at 82% 12%,
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 {
background:
linear-gradient(
145deg,
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 {
background:
radial-gradient(
circle at 16% 18%,
#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 {
background:
radial-gradient(
circle at 76% 16%,
#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 {
background:
radial-gradient(circle at 77% 17%, #ffffff42 0 0.8%, transparent 12%),
linear-gradient(
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 {
background:
linear-gradient(
132deg,
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 {
background-color: #090a0d;
background-image: var(--phone-wallpaper-image);
background-position: center;
background-size: cover;
}
.phone-screen--app .springboard {
transform: scale(0.91);
+9 -3
View File
@@ -73,9 +73,15 @@ const time = computed(() =>
.join('')
.trim(),
)
const dragStyle = computed(() => ({
'--lock-drag': `${dragOffset.value}px`,
}))
const dragStyle = computed<Record<string, string>>(() => {
const imageUrl = preferences.value.settings.wallpaperImageUrl
return {
'--lock-drag': `${dragOffset.value}px`,
...(preferences.value.settings.wallpaper === 'custom' && imageUrl
? { '--phone-wallpaper-image': `url(${JSON.stringify(imageUrl)})` }
: {}),
}
})
const flashlightShortcutColors = computed(() =>
flashlightActive.value
? {
+36 -5
View File
@@ -3938,7 +3938,13 @@ const defaultLocales: LocaleTree = {
default: 'The account request failed.',
},
back: 'Settings',
wallpaperPicker: 'Built-in Wallpapers',
wallpaperPicker: 'Sky Wallpapers',
wallpaperFromPhotos: 'Choose from Photos',
wallpaperFromPhotosDescription: 'Use one of your photos',
wallpaperFromCamera: 'Take Photo',
wallpaperFromCameraDescription: 'Create a new wallpaper',
wallpaperHistory: 'Recent',
wallpaperCustom: 'Photo wallpaper',
toggle: {
airplaneMode: 'Toggle Airplane Mode',
streamerMode: 'Toggle Streamer Mode',
@@ -3977,9 +3983,18 @@ const defaultLocales: LocaleTree = {
soft: 'Soft',
},
wallpapers: {
midnight: 'Midnight wallpaper',
aurora: 'Aurora wallpaper',
ember: 'Ember wallpaper',
midnight: 'Midnight',
aurora: 'Aurora',
ember: 'Ember',
ocean: 'Ocean',
sunrise: 'Sunrise',
violet: 'Violet',
forest: 'Forest',
cobalt: 'Cobalt',
rose: 'Rose',
sand: 'Sand',
graphite: 'Graphite',
prism: 'Prism',
},
},
},
@@ -4361,8 +4376,24 @@ export const usePhoneStore = defineStore('phone', {
setSystemDarkMode(value: boolean): void {
this.systemDarkMode = value
},
setWallpaper(wallpaper: WallpaperId): void {
setWallpaper(wallpaper: WallpaperId, imageUrl: string | null = null): 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
this.preferences.settings.wallpaperHistory = [
{ imageUrl: normalizedImageUrl ?? null, wallpaper },
...this.preferences.settings.wallpaperHistory.filter((entry) =>
wallpaper === 'custom'
? entry.wallpaper !== 'custom' ||
entry.imageUrl !== normalizedImageUrl
: entry.wallpaper !== wallpaper,
),
].slice(0, 4)
this.saveDeviceNamespace('settings', this.preferences)
},
async unlockWithPasscode(
+53 -1
View File
@@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest'
import type { LaunchablePhoneAppId } from '@/types/apps'
import { DEFAULT_PHONE_PREFERENCES, parsePhonePreferences } from './preferences'
import {
DEFAULT_PHONE_PREFERENCES,
parsePhonePreferences,
WALLPAPER_IDS,
} from './preferences'
describe('preferences', () => {
it('falls back for malformed and obsolete records', () => {
expect(parsePhonePreferences('{')).toEqual(DEFAULT_PHONE_PREFERENCES)
@@ -132,4 +136,52 @@ describe('preferences', () => {
wifiEnabled: true,
})
})
it('provides twelve built-in wallpapers', () => {
expect(WALLPAPER_IDS).toHaveLength(12)
expect(new Set(WALLPAPER_IDS).size).toBe(12)
})
it('restores custom photo wallpapers and only the latest four history entries', () => {
const value = parsePhonePreferences(
JSON.stringify({
version: 1,
settings: {
wallpaper: 'custom',
wallpaperImageUrl: 'https://media.example/current.jpg',
wallpaperHistory: [
{
wallpaper: 'custom',
imageUrl: 'https://media.example/current.jpg',
},
{ wallpaper: 'prism', imageUrl: null },
{ wallpaper: 'ocean', imageUrl: null },
{ wallpaper: 'rose', imageUrl: null },
{ wallpaper: 'sand', imageUrl: null },
],
},
}),
)
expect(value.settings.wallpaper).toBe('custom')
expect(value.settings.wallpaperImageUrl).toBe(
'https://media.example/current.jpg',
)
expect(value.settings.wallpaperHistory).toHaveLength(4)
expect(
value.settings.wallpaperHistory.map((entry) => entry.wallpaper),
).toEqual(['custom', 'prism', 'ocean', 'rose'])
})
it('rejects a custom wallpaper without a photo URL', () => {
const value = parsePhonePreferences(
JSON.stringify({
version: 1,
settings: { wallpaper: 'custom', wallpaperImageUrl: '' },
}),
)
expect(value.settings.wallpaper).toBe('midnight')
expect(value.settings.wallpaperImageUrl).toBeNull()
})
})
+75 -7
View File
@@ -22,7 +22,20 @@ export const PHONE_FRAME_IDS = [
] as const
export const RINGTONE_IDS = ['skyline', 'horizon', 'pulse'] as const
export const NOTIFICATION_SOUND_IDS = ['chime', 'signal', 'soft'] as const
export const WALLPAPER_IDS = ['midnight', 'aurora', 'ember'] as const
export const WALLPAPER_IDS = [
'midnight',
'aurora',
'ember',
'ocean',
'sunrise',
'violet',
'forest',
'cobalt',
'rose',
'sand',
'graphite',
'prism',
] as const
export const PHONE_SCALE_MIN = 75
export const PHONE_SCALE_MAX = 150
export const PHONE_SCALE_STEP = 5
@@ -32,7 +45,12 @@ export type GraphicsMode = (typeof GRAPHICS_MODE_IDS)[number]
export type PhoneFrameId = (typeof PHONE_FRAME_IDS)[number]
export type RingtoneId = (typeof RINGTONE_IDS)[number]
export type NotificationSoundId = (typeof NOTIFICATION_SOUND_IDS)[number]
export type WallpaperId = (typeof WALLPAPER_IDS)[number]
export type BuiltInWallpaperId = (typeof WALLPAPER_IDS)[number]
export type WallpaperId = BuiltInWallpaperId | 'custom'
export type WallpaperHistoryEntry = {
imageUrl: string | null
wallpaper: WallpaperId
}
export type AppNotificationPreferences = {
enabled: boolean
sounds: boolean
@@ -63,6 +81,8 @@ export type PhonePreferencesV1 = {
screenBrightness: number
streamerMode: boolean
wallpaper: WallpaperId
wallpaperHistory: WallpaperHistoryEntry[]
wallpaperImageUrl: string | null
wifiEnabled: boolean
}
version: 1
@@ -131,6 +151,8 @@ export const DEFAULT_PHONE_PREFERENCES: PhonePreferencesV1 = {
screenBrightness: 100,
streamerMode: false,
wallpaper: 'midnight',
wallpaperHistory: [{ imageUrl: null, wallpaper: 'midnight' }],
wallpaperImageUrl: null,
wifiEnabled: true,
},
version: 1,
@@ -161,6 +183,39 @@ function readChoice<T extends string>(
: fallback
}
function readWallpaperImageUrl(value: unknown): string | null {
if (typeof value !== 'string') return null
const imageUrl = value.trim()
return imageUrl.length > 0 && imageUrl.length <= 2_000_000 ? imageUrl : null
}
function readWallpaperHistory(value: unknown): WallpaperHistoryEntry[] {
if (!Array.isArray(value)) return []
const history: WallpaperHistoryEntry[] = []
for (const rawEntry of value.slice(0, 4)) {
if (!rawEntry || typeof rawEntry !== 'object' || Array.isArray(rawEntry)) {
continue
}
const entry = rawEntry as Partial<WallpaperHistoryEntry>
const imageUrl = readWallpaperImageUrl(entry.imageUrl)
if (entry.wallpaper === 'custom' && imageUrl) {
history.push({ imageUrl, wallpaper: 'custom' })
continue
}
if (
typeof entry.wallpaper === 'string' &&
WALLPAPER_IDS.includes(entry.wallpaper as BuiltInWallpaperId)
) {
history.push({
imageUrl: null,
wallpaper: entry.wallpaper as BuiltInWallpaperId,
})
}
}
return history
}
function readNotifications(
value: unknown,
): Record<LaunchablePhoneAppId, AppNotificationPreferences> {
@@ -214,6 +269,13 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
}
const defaults = DEFAULT_PHONE_PREFERENCES.settings
const wallpaperImageUrl = readWallpaperImageUrl(settings.wallpaperImageUrl)
const wallpaper =
settings.wallpaper === 'custom' && wallpaperImageUrl
? 'custom'
: readChoice(settings.wallpaper, WALLPAPER_IDS, defaults.wallpaper)
const wallpaperHistory = readWallpaperHistory(settings.wallpaperHistory)
return {
settings: {
airplaneMode: readBoolean(settings.airplaneMode, defaults.airplaneMode),
@@ -281,11 +343,17 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
100,
),
streamerMode: readBoolean(settings.streamerMode, defaults.streamerMode),
wallpaper: readChoice(
settings.wallpaper,
WALLPAPER_IDS,
defaults.wallpaper,
),
wallpaper,
wallpaperHistory:
wallpaperHistory.length > 0
? wallpaperHistory
: [
{
imageUrl: wallpaper === 'custom' ? wallpaperImageUrl : null,
wallpaper,
},
],
wallpaperImageUrl: wallpaper === 'custom' ? wallpaperImageUrl : null,
wifiEnabled: readBoolean(settings.wifiEnabled, defaults.wifiEnabled),
},
version: 1,
+9
View File
@@ -57,6 +57,14 @@ const HOME_FOLDER_HOVER_DURATION = 900
const phone = usePhoneStore()
const appStore = useAppStoreStore()
const widgets = useWidgetsStore()
const wallpaperStyle = computed<Record<string, string>>(() => {
const style: Record<string, string> = {}
const imageUrl = phone.preferences.settings.wallpaperImageUrl
if (phone.preferences.settings.wallpaper === 'custom' && imageUrl) {
style['--phone-wallpaper-image'] = `url(${JSON.stringify(imageUrl)})`
}
return style
})
const searchQuery = ref('')
const searchFocused = ref(false)
const showAllApps = ref(false)
@@ -1296,6 +1304,7 @@ onBeforeUnmount(() => {
'springboard--widget-dragging': draggingWidgetId !== null,
},
]"
:style="wallpaperStyle"
>
<div
class="springboard-track"
@@ -35,4 +35,15 @@ describe('SettingsApp Sky UI contract', () => {
/\.settings-content--subpage\s*\{[^}]*padding-top:\s*0/,
)
})
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('class="settings-wallpaper-history"')
expect(source).toContain('class="settings-wallpaper-grid"')
expect(source).toMatch(
/\.settings-wallpaper-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2,/,
)
})
})
+360 -12
View File
@@ -2,9 +2,11 @@
import {
BellRing,
Bluetooth,
Camera,
Check,
EyeOff,
KeyRound,
Images,
Monitor,
PanelsTopLeft,
Moon,
@@ -22,10 +24,11 @@ import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
type ComponentPublicInstance,
} from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { PHONE_FRAME_COLORS } from '@/config/appearance'
import {
@@ -34,6 +37,7 @@ import {
PHONE_APPS,
} from '@/config/apps'
import { usePhoneStore } from '@/stores/phone'
import { useMessageMediaStore } from '@/stores/messageMedia'
import PhonePasscode from '@/components/PhonePasscode.vue'
import { useAccountStore } from '@/stores/account'
import { useAppAuthStore } from '@/stores/app-auth'
@@ -83,6 +87,7 @@ import {
type NotificationSoundId,
type PhoneFrameId,
type RingtoneId,
type WallpaperHistoryEntry,
} from '@/utils/preferences'
type SettingsView =
@@ -117,6 +122,8 @@ type PasscodeFlow =
const FACTORY_RESET_DURATION_MS = 60_000
const phone = usePhoneStore()
const mediaPicker = useMessageMediaStore()
const route = useRoute()
const router = useRouter()
const isDevelopment = import.meta.env.DEV
const account = useAccountStore()
@@ -151,6 +158,10 @@ const selectedFrameColor = computed(
)
let factoryResetAnimationFrame: number | undefined
const wallpaperHistory = computed(
() => phone.preferences.settings.wallpaperHistory,
)
const toggleRows = [
{
key: 'airplaneMode' as const,
@@ -221,6 +232,30 @@ function openSkyUiKitchenSink(): void {
void router.push({ name: 'development-sky-ui' })
}
function openWallpaperMedia(app: 'photos' | 'camera'): void {
mediaPicker.begin(
'settings:wallpaper',
'photo',
'/apps/settings?wallpaper=1',
1,
)
void router.push({
path: `/apps/${app}`,
query: { mediaAttachment: 'photo' },
})
}
function wallpaperPreviewStyle(
entry: WallpaperHistoryEntry,
): Record<string, string> | undefined {
if (entry.wallpaper !== 'custom' || !entry.imageUrl) return undefined
return { '--phone-wallpaper-image': `url(${JSON.stringify(entry.imageUrl)})` }
}
function selectWallpaperHistory(entry: WallpaperHistoryEntry): void {
phone.setWallpaper(entry.wallpaper, entry.imageUrl)
}
const connectivityRows = [
{
key: 'wifi',
@@ -591,6 +626,13 @@ async function confirmSimEject(): Promise<void> {
}
}
onMounted(() => {
if (route.query.wallpaper === '1') activeView.value = 'wallpaper'
const selectedPhoto = mediaPicker.consume('settings:wallpaper')
if (selectedPhoto) phone.setWallpaper('custom', selectedPhoto.url)
})
onBeforeUnmount(() => {
if (factoryResetAnimationFrame !== undefined) {
cancelAnimationFrame(factoryResetAnimationFrame)
@@ -1326,16 +1368,105 @@ onBeforeUnmount(() => {
</template>
<template v-else-if="activeView === 'wallpaper'">
<SkySettingsGroup :title="phone.t('Apps.settings.wallpaperPicker')">
<SkySettingsRow
v-for="wallpaper in WALLPAPER_IDS"
:key="wallpaper"
kind="choice"
:selected="phone.preferences.settings.wallpaper === wallpaper"
:title="phone.t('Apps.settings.wallpapers.' + wallpaper)"
@activate="phone.setWallpaper(wallpaper)"
/>
</SkySettingsGroup>
<section class="settings-wallpaper-actions">
<button type="button" @click="openWallpaperMedia('photos')">
<span class="settings-wallpaper-actions__icon" aria-hidden="true">
<Images />
</span>
<span>
<strong>{{
phone.t('Apps.settings.wallpaperFromPhotos')
}}</strong>
<small>{{
phone.t('Apps.settings.wallpaperFromPhotosDescription')
}}</small>
</span>
</button>
<button type="button" @click="openWallpaperMedia('camera')">
<span class="settings-wallpaper-actions__icon" aria-hidden="true">
<Camera />
</span>
<span>
<strong>{{
phone.t('Apps.settings.wallpaperFromCamera')
}}</strong>
<small>{{
phone.t('Apps.settings.wallpaperFromCameraDescription')
}}</small>
</span>
</button>
</section>
<section
v-if="wallpaperHistory.length"
class="settings-wallpaper-section"
>
<h2>{{ phone.t('Apps.settings.wallpaperHistory') }}</h2>
<div class="settings-wallpaper-history">
<button
v-for="(entry, index) in wallpaperHistory"
:key="`${entry.wallpaper}-${entry.imageUrl ?? index}`"
type="button"
:class="[
`wallpaper--${entry.wallpaper}`,
{
'settings-wallpaper-history__item--selected':
phone.preferences.settings.wallpaper === entry.wallpaper &&
phone.preferences.settings.wallpaperImageUrl ===
entry.imageUrl,
},
]"
:style="wallpaperPreviewStyle(entry)"
:aria-label="
entry.wallpaper === 'custom'
? phone.t('Apps.settings.wallpaperCustom')
: phone.t('Apps.settings.wallpapers.' + entry.wallpaper)
"
@click="selectWallpaperHistory(entry)"
>
<Check
v-if="
phone.preferences.settings.wallpaper === entry.wallpaper &&
phone.preferences.settings.wallpaperImageUrl ===
entry.imageUrl
"
aria-hidden="true"
/>
</button>
</div>
</section>
<section class="settings-wallpaper-section">
<h2>{{ phone.t('Apps.settings.wallpaperPicker') }}</h2>
<div class="settings-wallpaper-grid">
<button
v-for="wallpaper in WALLPAPER_IDS"
:key="wallpaper"
type="button"
class="settings-wallpaper-choice"
:class="{
'settings-wallpaper-choice--selected':
phone.preferences.settings.wallpaper === wallpaper,
}"
:aria-pressed="phone.preferences.settings.wallpaper === wallpaper"
@click="phone.setWallpaper(wallpaper)"
>
<span
class="settings-wallpaper-choice__preview"
:class="`wallpaper--${wallpaper}`"
aria-hidden="true"
>
<span class="settings-wallpaper-choice__screen"></span>
<Check
v-if="phone.preferences.settings.wallpaper === wallpaper"
/>
</span>
<strong>{{
phone.t('Apps.settings.wallpapers.' + wallpaper)
}}</strong>
</button>
</div>
</section>
</template>
</SkyScrollArea>
@@ -1546,6 +1677,221 @@ onBeforeUnmount(() => {
box-shadow: 0 1px 3px rgb(0 0 0 / 18%);
}
.settings-wallpaper-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--sky-space-2);
margin-bottom: var(--sky-space-5);
}
.settings-wallpaper-actions > button {
min-width: 0;
min-height: 116px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--sky-space-3);
padding: var(--sky-space-4);
border: 1px solid var(--sky-hairline);
border-radius: 20px;
background: var(--sky-surface);
color: var(--sky-text);
text-align: left;
box-shadow: 0 8px 20px rgb(0 0 0 / 8%);
cursor: pointer;
transition:
transform 150ms ease,
background 150ms ease;
}
.settings-wallpaper-actions > button:hover {
background: var(--sky-surface-muted);
}
.settings-wallpaper-actions > button:active {
transform: scale(0.97);
}
.settings-wallpaper-actions > button:focus-visible,
.settings-wallpaper-history > button:focus-visible,
.settings-wallpaper-choice:focus-visible {
outline: 2px solid var(--sky-app-accent);
outline-offset: 2px;
}
.settings-wallpaper-actions__icon {
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 12px;
background: var(--sky-app-accent-soft);
color: var(--sky-app-accent);
}
.settings-wallpaper-actions__icon > svg {
width: 21px;
height: 21px;
}
.settings-wallpaper-actions strong,
.settings-wallpaper-actions small {
display: block;
}
.settings-wallpaper-actions strong {
font-size: 14px;
line-height: 18px;
}
.settings-wallpaper-actions small {
margin-top: 3px;
color: var(--sky-muted);
font-size: 11px;
line-height: 14px;
}
.settings-wallpaper-section {
margin-bottom: var(--sky-space-5);
}
.settings-wallpaper-section > h2 {
margin: 0 var(--sky-space-1) var(--sky-space-2);
color: var(--sky-muted);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.settings-wallpaper-history {
display: flex;
gap: var(--sky-space-2);
overflow-x: auto;
padding: 2px var(--sky-space-1) var(--sky-space-2);
scrollbar-width: none;
}
.settings-wallpaper-history::-webkit-scrollbar {
display: none;
}
.settings-wallpaper-history > button {
width: 62px;
height: 88px;
position: relative;
flex: 0 0 auto;
display: grid;
place-items: center;
overflow: hidden;
border: 2px solid transparent;
border-radius: 15px;
background-position: center;
background-size: cover;
color: #ffffff;
box-shadow: 0 5px 14px rgb(0 0 0 / 18%);
cursor: pointer;
}
.settings-wallpaper-history > button::after {
content: '';
position: absolute;
inset: 0;
border: 1px solid rgb(255 255 255 / 13%);
border-radius: inherit;
pointer-events: none;
}
.settings-wallpaper-history > button > svg {
width: 21px;
height: 21px;
position: relative;
z-index: 1;
padding: 4px;
border-radius: 50%;
background: var(--sky-app-accent);
stroke-width: 3;
}
.settings-wallpaper-history__item--selected {
border-color: var(--sky-app-accent) !important;
}
.settings-wallpaper-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--sky-space-3);
}
.settings-wallpaper-choice {
min-width: 0;
padding: 0 0 var(--sky-space-2);
border: 0;
border-radius: 21px;
background: var(--sky-surface);
color: var(--sky-text);
text-align: left;
box-shadow: 0 8px 20px rgb(0 0 0 / 8%);
cursor: pointer;
transition:
transform 150ms ease,
box-shadow 150ms ease;
}
.settings-wallpaper-choice:hover {
box-shadow: 0 11px 24px rgb(0 0 0 / 13%);
}
.settings-wallpaper-choice:active {
transform: scale(0.98);
}
.settings-wallpaper-choice__preview {
aspect-ratio: 0.78;
position: relative;
display: grid;
place-items: center;
overflow: hidden;
border: 2px solid transparent;
border-radius: 21px 21px 14px 14px;
background-position: center;
background-size: cover;
}
.settings-wallpaper-choice__screen {
width: 28%;
height: 7px;
position: absolute;
top: 8px;
left: 50%;
border-radius: 999px;
background: rgb(0 0 0 / 58%);
transform: translateX(-50%);
}
.settings-wallpaper-choice__preview > svg {
width: 26px;
height: 26px;
padding: 5px;
border-radius: 50%;
background: var(--sky-app-accent);
color: #ffffff;
stroke-width: 3;
}
.settings-wallpaper-choice > strong {
display: block;
overflow: hidden;
padding: var(--sky-space-2) var(--sky-space-3) 0;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-wallpaper-choice--selected .settings-wallpaper-choice__preview {
border-color: var(--sky-app-accent);
}
.settings-frame-grid {
display: grid;
grid-template-columns: repeat(3, 52px);
@@ -1639,7 +1985,9 @@ onBeforeUnmount(() => {
}
@media (prefers-reduced-motion: reduce) {
.settings-frame-choice {
.settings-frame-choice,
.settings-wallpaper-actions > button,
.settings-wallpaper-choice {
transition: none;
}
}