From d1213701590face671f367a0651be1f5480a19d7 Mon Sep 17 00:00:00 2001 From: DerEchteAlec Date: Mon, 17 Aug 2026 05:16:03 +0200 Subject: [PATCH] ENH - expand Settings controls and wallpaper imports --- frontend/src/stores/phone.ts | 2 + frontend/src/types/media.ts | 5 + frontend/src/ui/settings.css | 70 +++++++++++++- .../ui/settings/SkySettingsRangeRow.test.ts | 36 +++++++- .../src/ui/settings/SkySettingsRangeRow.vue | 47 +++++++--- frontend/src/views/apps/CameraApp.vue | 9 +- .../views/apps/GalleryApp.contract.test.ts | 8 ++ frontend/src/views/apps/GalleryApp.vue | 27 +++++- .../views/apps/SettingsApp.contract.test.ts | 10 +- frontend/src/views/apps/SettingsApp.vue | 92 +++++++++++++++++-- sky_phone/config/locales/de.lua | 2 + sky_phone/config/locales/en.lua | 1 + sky_phone/config/media.lua | 6 ++ sky_phone/source/server/media.lua | 3 + 14 files changed, 284 insertions(+), 34 deletions(-) diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 2517550..85bd05a 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -4247,6 +4247,8 @@ const defaultLocales: LocaleTree = { wallpaperFromPhotosDescription: 'Use one of your photos', wallpaperFromCamera: 'Take Photo', wallpaperFromCameraDescription: 'Create a new wallpaper', + wallpaperCustomUpload: 'Upload Custom Image', + wallpaperCustomUploadDescription: 'Use a verified HTTPS image link', wallpaperHistory: 'Recent', wallpaperCustom: 'Photo wallpaper', wallpaperTarget: 'Wallpaper destination', diff --git a/frontend/src/types/media.ts b/frontend/src/types/media.ts index de819b3..499ea73 100644 --- a/frontend/src/types/media.ts +++ b/frontend/src/types/media.ts @@ -1,6 +1,11 @@ export type MediaType = 'photo' | 'video' export type GalleryFilter = 'all' | MediaType +export type MediaConfig = { + customWallpaperUploadEnabled: boolean + videoBitrateKbps: number +} + export type PhoneMedia = { createdAt: number favorite: boolean diff --git a/frontend/src/ui/settings.css b/frontend/src/ui/settings.css index 9cf773f..ff885f9 100644 --- a/frontend/src/ui/settings.css +++ b/frontend/src/ui/settings.css @@ -190,19 +190,81 @@ label.sky-settings-row__title { } .sky-settings-range-row__frame { - padding-top: var(--sky-space-1); - padding-bottom: var(--sky-space-1); + min-height: 88px; + padding-top: 12px; + padding-bottom: 14px; + align-items: stretch; + flex-direction: column; + gap: 10px; } .sky-settings-group--compact .sky-settings-row__frame, .sky-settings-group--compact .sky-settings-range-row__frame { - min-height: 48px; + min-height: 76px; padding-top: 6px; padding-bottom: 6px; } -.sky-settings-range-row .sky-range { +.sky-settings-range-row__header, +.sky-settings-range-row__control { + min-width: 0; + display: flex; + align-items: center; +} + +.sky-settings-range-row__header { + justify-content: space-between; + gap: 16px; +} + +.sky-settings-range-row__title, +.sky-settings-range-row__value { + font-size: 17px; + line-height: 22px; +} + +.sky-settings-range-row__title { + min-width: 0; + color: var(--sky-text); + overflow-wrap: anywhere; +} + +.sky-settings-range-row__value { + flex: none; + color: var(--sky-muted); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.sky-settings-range-row__control { + min-height: 28px; + gap: 12px; +} + +.sky-settings-range-row__control .sky-range { width: 100%; + flex: 1 1 auto; +} + +.sky-settings-range-row__endpoint { + width: 24px; + height: 28px; + display: inline-flex; + flex: none; + align-items: center; + justify-content: center; + color: var(--sky-muted); +} + +.sky-settings-range-row__endpoint > svg { + width: 22px; + height: 22px; + display: block; +} + +.sky-settings-range-row__endpoint--leading > svg { + width: 17px; + height: 17px; } .sky-settings-icon { diff --git a/frontend/src/ui/settings/SkySettingsRangeRow.test.ts b/frontend/src/ui/settings/SkySettingsRangeRow.test.ts index 9e9b2a6..6c89c8d 100644 --- a/frontend/src/ui/settings/SkySettingsRangeRow.test.ts +++ b/frontend/src/ui/settings/SkySettingsRangeRow.test.ts @@ -1,4 +1,4 @@ -import { createSSRApp } from 'vue' +import { createSSRApp, h } from 'vue' import { renderToString } from 'vue/server-renderer' import { describe, expect, it } from 'vitest' @@ -26,7 +26,9 @@ describe('SkySettingsRangeRow', () => { expect(html).toContain('min="25"') expect(html).toContain('max="100"') expect(html).toContain('step="5"') - expect(html).toContain('75%') + expect(html).toContain('class="sky-settings-range-row__title">Brightness') + expect(html).toContain('class="sky-settings-range-row__value">75%') + expect(html).not.toContain('sky-range__caption') }) it('uses the effective numeric value as its default visible label', async () => { @@ -37,7 +39,9 @@ describe('SkySettingsRangeRow', () => { const html = await renderToString(app) - expect(html).toMatch(/class="sky-range__label">.*1\.25.*<\/span>/) + expect(html).toMatch( + /class="sky-settings-range-row__value">.*1\.25.*<\/span>/, + ) expect(html).not.toContain('aria-valuetext=') }) @@ -51,7 +55,31 @@ describe('SkySettingsRangeRow', () => { const html = await renderToString(app) expect(html).toContain('aria-valuetext="75%"') - expect(html).toMatch(/class="sky-range__label">.*75%.*<\/span>/) + expect(html).toMatch( + /class="sky-settings-range-row__value">.*75%.*<\/span>/, + ) + }) + + it('supports decorative range endpoint icons without changing the label', async () => { + const app = createSSRApp({ + render: () => + h( + SkySettingsRangeRow, + { modelValue: 50, title: 'Volume', valueLabel: '50%' }, + { + leading: () => h('svg', { 'data-endpoint': 'low' }), + trailing: () => h('svg', { 'data-endpoint': 'high' }), + }, + ), + }) + + const html = await renderToString(app) + + expect(html).toContain('sky-settings-range-row__endpoint--leading') + expect(html).toContain('sky-settings-range-row__endpoint--trailing') + expect(html).toContain('data-endpoint="low"') + expect(html).toContain('data-endpoint="high"') + expect(html).toContain('aria-label="Volume"') }) it('exposes input, change, and numeric model update events', () => { diff --git a/frontend/src/ui/settings/SkySettingsRangeRow.vue b/frontend/src/ui/settings/SkySettingsRangeRow.vue index 8019199..8dbbfbd 100644 --- a/frontend/src/ui/settings/SkySettingsRangeRow.vue +++ b/frontend/src/ui/settings/SkySettingsRangeRow.vue @@ -53,21 +53,38 @@ const accessibleValue = computed( :class="{ 'sky-settings-range-row--disabled': disabled }" >
- - {{ visibleValue }} - +
+ {{ title }} + {{ visibleValue }} +
+
+ + + +
diff --git a/frontend/src/views/apps/CameraApp.vue b/frontend/src/views/apps/CameraApp.vue index 34e19f4..3be9103 100644 --- a/frontend/src/views/apps/CameraApp.vue +++ b/frontend/src/views/apps/CameraApp.vue @@ -18,7 +18,12 @@ import { useRoute, useRouter } from 'vue-router' import { useMessageMediaStore } from '@/stores/messageMedia' import { usePhoneStore } from '@/stores/phone' -import type { MediaType, PhoneMedia, UploadResult } from '@/types/media' +import type { + MediaConfig, + MediaType, + PhoneMedia, + UploadResult, +} from '@/types/media' import { SkySegmented, SkySegmentedButton } from '@/ui' import { createGameView, type GameView } from '@/utils/gameView' import { formatRecordingDuration, mediaErrorKey } from '@/utils/media' @@ -416,7 +421,7 @@ onMounted(() => { window.addEventListener('keyup', onKeyup) window.addEventListener('message', onMessage) void nuiCall('camera:setActive', { active: true }) - void nuiCall<{ videoBitrateKbps?: number }>('media:config').then( + void nuiCall('media:config').then( (response) => { if (response.success && response.data?.videoBitrateKbps) { videoBitrateKbps.value = response.data.videoBitrateKbps diff --git a/frontend/src/views/apps/GalleryApp.contract.test.ts b/frontend/src/views/apps/GalleryApp.contract.test.ts index 173d5d9..8c8a898 100644 --- a/frontend/src/views/apps/GalleryApp.contract.test.ts +++ b/frontend/src/views/apps/GalleryApp.contract.test.ts @@ -12,6 +12,14 @@ const headerActions = source.slice( ) describe('GalleryApp import action', () => { + it('opens a configured wallpaper upload and returns the imported photo directly', () => { + expect(source).toContain("route.query.wallpaperUpload === '1'") + expect(source).toContain("requestedMessageMedia.value === 'photo'") + expect(source).toContain('openImport()') + expect(source).toContain('messageMedia.complete(response.data)') + expect(source).toContain('await router.push(returnPath)') + }) + it('keeps import available as a header button', () => { expect(headerActions).toContain('(() => { +const requestedMessageMedia = computed(() => { const value = route.query.mediaAttachment ?? route.query.messageAttachment return value === 'photo' || value === 'video' ? value : null }) @@ -414,7 +415,19 @@ async function loadImportSources(): Promise { if (isDevelopment && !developmentApiEnabled) return const response = await nuiCall('media:import:sources') if (!response.success || !response.data) return - importSources.value = response.data.sources + const requestedType = requestedMessageMedia.value + importSources.value = requestedType + ? response.data.sources.filter((source) => + source.mediaTypes.includes(requestedType), + ) + : response.data.sources + if ( + route.query.wallpaperUpload === '1' && + requestedType === 'photo' && + importSources.value.length > 0 + ) { + openImport() + } } function selectImportSource(source: MediaImportSource): void { @@ -473,6 +486,16 @@ async function commitUrlImport(): Promise { } media.value = mergeMedia(media.value, [response.data]) await fetchCounts() + if ( + route.query.wallpaperUpload === '1' && + requestedMessageMedia.value === 'photo' + ) { + const returnPath = messageMedia.complete(response.data) + if (returnPath) { + await router.push(returnPath) + return + } + } closeImport() showToast(phone.t('Apps.photos.import.linkCompleted')) } diff --git a/frontend/src/views/apps/SettingsApp.contract.test.ts b/frontend/src/views/apps/SettingsApp.contract.test.ts index 6d0c784..6354524 100644 --- a/frontend/src/views/apps/SettingsApp.contract.test.ts +++ b/frontend/src/views/apps/SettingsApp.contract.test.ts @@ -19,6 +19,9 @@ describe('SettingsApp Sky UI contract', () => { expect(source).toContain('') + expect(source).toContain('') expect(source).toContain( `