From 33cf49e8b65fade3af279415c41ddb6e55f1d6fb Mon Sep 17 00:00:00 2001 From: "smx.pusha" <139338836+smxpusha@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:19:05 +0200 Subject: [PATCH] ENH - overhaul wallpaper customization --- frontend/src/assets/main.css | 364 ++++++++++++++++- frontend/src/components/PhoneLockScreen.vue | 12 +- frontend/src/stores/phone.ts | 41 +- frontend/src/utils/preferences.test.ts | 54 ++- frontend/src/utils/preferences.ts | 82 +++- frontend/src/views/SpringboardView.vue | 9 + .../views/apps/SettingsApp.contract.test.ts | 11 + frontend/src/views/apps/SettingsApp.vue | 372 +++++++++++++++++- sky_phone/config/locales/en.lua | 11 +- 9 files changed, 917 insertions(+), 39 deletions(-) diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 582cdc9..bf56e93 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -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); diff --git a/frontend/src/components/PhoneLockScreen.vue b/frontend/src/components/PhoneLockScreen.vue index d1dd7c4..a81424f 100644 --- a/frontend/src/components/PhoneLockScreen.vue +++ b/frontend/src/components/PhoneLockScreen.vue @@ -73,9 +73,15 @@ const time = computed(() => .join('') .trim(), ) -const dragStyle = computed(() => ({ - '--lock-drag': `${dragOffset.value}px`, -})) +const dragStyle = computed>(() => { + 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 ? { diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index a0271d7..91000e7 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -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( diff --git a/frontend/src/utils/preferences.test.ts b/frontend/src/utils/preferences.test.ts index 884d55b..6c80d0f 100644 --- a/frontend/src/utils/preferences.test.ts +++ b/frontend/src/utils/preferences.test.ts @@ -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() + }) }) diff --git a/frontend/src/utils/preferences.ts b/frontend/src/utils/preferences.ts index ee15f0c..4df8df1 100644 --- a/frontend/src/utils/preferences.ts +++ b/frontend/src/utils/preferences.ts @@ -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( : 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 + 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 { @@ -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, diff --git a/frontend/src/views/SpringboardView.vue b/frontend/src/views/SpringboardView.vue index 5ce5913..4ffb93d 100644 --- a/frontend/src/views/SpringboardView.vue +++ b/frontend/src/views/SpringboardView.vue @@ -57,6 +57,14 @@ const HOME_FOLDER_HOVER_DURATION = 900 const phone = usePhoneStore() const appStore = useAppStoreStore() const widgets = useWidgetsStore() +const wallpaperStyle = computed>(() => { + const style: Record = {} + 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" >
{ /\.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,/, + ) + }) }) diff --git a/frontend/src/views/apps/SettingsApp.vue b/frontend/src/views/apps/SettingsApp.vue index b50078d..4f31e69 100644 --- a/frontend/src/views/apps/SettingsApp.vue +++ b/frontend/src/views/apps/SettingsApp.vue @@ -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 | 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 { } } +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(() => { @@ -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; } } diff --git a/sky_phone/config/locales/en.lua b/sky_phone/config/locales/en.lua index 152d894..15ddbec 100644 --- a/sky_phone/config/locales/en.lua +++ b/sky_phone/config/locales/en.lua @@ -1755,7 +1755,10 @@ Locales["en"] = { skyUiKitchenSinkDescription = "Interactive catalog of first-party Sky UI components", about = "About", deviceName = "Device Name", deviceNameValue = "Sky Phone", softwareVersion = "Software Version", language = "Language", languageValue = "English", localStorage = "Local Storage", localStorageValue = "On Device", - back = "Settings", wallpaperPicker = "Built-in Wallpapers", deviceInformation = "Device Information", + back = "Settings", wallpaperPicker = "Sky Wallpapers", deviceInformation = "Device Information", + wallpaperFromPhotos = "Choose from Photos", wallpaperFromPhotosDescription = "Use one of your photos", + wallpaperFromCamera = "Take Photo", wallpaperFromCameraDescription = "Create a new wallpaper", + wallpaperHistory = "Recent", wallpaperCustom = "Photo wallpaper", 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?", @@ -1789,7 +1792,11 @@ Locales["en"] = { }, ringtones = { skyline = "Skyline", horizon = "Horizon", pulse = "Pulse" }, notificationSoundsList = { chime = "Chime", signal = "Signal", soft = "Soft" }, - wallpapers = { midnight = "Midnight wallpaper", aurora = "Aurora wallpaper", ember = "Ember wallpaper" }, + wallpapers = { + midnight = "Midnight", aurora = "Aurora", ember = "Ember", ocean = "Ocean", sunrise = "Sunrise", + violet = "Violet", forest = "Forest", cobalt = "Cobalt", rose = "Rose", sand = "Sand", + graphite = "Graphite", prism = "Prism", + }, }, }, },