mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
FIX - polish camera controls and landscape layout (#37)
Co-authored-by: Leon.Schmidt <159480018+leonw21342315@users.noreply.github.com>
This commit is contained in:
@@ -1885,6 +1885,8 @@ onBeforeUnmount(() => {
|
||||
class="phone-screen"
|
||||
:class="{
|
||||
'phone-screen--app': isAppRoute || isDevelopmentRoute,
|
||||
'phone-screen--camera-landscape':
|
||||
activeAppId === 'camera' && phone.cameraLandscape,
|
||||
'phone-app--light': !displayedDarkMode,
|
||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
|
||||
@@ -460,6 +460,7 @@ button {
|
||||
}
|
||||
}
|
||||
.phone-screen {
|
||||
--phone-screen-portrait-ratio: 2.30951;
|
||||
position: relative;
|
||||
container-type: size;
|
||||
margin: auto;
|
||||
@@ -470,6 +471,12 @@ button {
|
||||
background: #08080a;
|
||||
border-radius: 40px;
|
||||
}
|
||||
.phone-screen--camera-landscape {
|
||||
background: transparent;
|
||||
}
|
||||
.phone-screen--camera-landscape .springboard {
|
||||
visibility: hidden;
|
||||
}
|
||||
.phone-app {
|
||||
position: absolute !important;
|
||||
inset: 0 !important;
|
||||
@@ -3239,6 +3246,9 @@ button {
|
||||
backface-visibility: hidden;
|
||||
will-change: transform, border-radius, opacity;
|
||||
}
|
||||
.app-window--camera-landscape {
|
||||
background: transparent;
|
||||
}
|
||||
.app-window--citywarn {
|
||||
backface-visibility: visible;
|
||||
filter: none;
|
||||
|
||||
@@ -30,7 +30,11 @@ const launchStyle = computed(() => {
|
||||
<div
|
||||
v-if="app && !app.adminOnly"
|
||||
class="app-window"
|
||||
:class="{ 'app-window--citywarn': app.id === 'citywarn' }"
|
||||
:class="{
|
||||
'app-window--camera-landscape':
|
||||
app.id === 'camera' && phone.cameraLandscape,
|
||||
'app-window--citywarn': app.id === 'citywarn',
|
||||
}"
|
||||
:style="launchStyle"
|
||||
>
|
||||
<CustomAppFrame
|
||||
|
||||
@@ -6,6 +6,15 @@ const cameraView = readFileSync(
|
||||
new URL('./CameraApp.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const appShell = readFileSync(new URL('../../App.vue', import.meta.url), 'utf8')
|
||||
const appWindow = readFileSync(
|
||||
new URL('../PhoneAppWindow.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const shellStyles = readFileSync(
|
||||
new URL('../../assets/main.css', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const mediaCapture = readFileSync(
|
||||
new URL('../../components/PhoneMediaCapture.vue', import.meta.url),
|
||||
'utf8',
|
||||
@@ -47,6 +56,99 @@ describe('Camera app controls', () => {
|
||||
expect(cameraView).not.toContain('k-navbar')
|
||||
})
|
||||
|
||||
it('stays dark when the global phone appearance is light', () => {
|
||||
const cameraPageTag = cameraView.match(/<sky-app-page\b[^>]*>/s)?.[0]
|
||||
|
||||
expect(cameraPageTag).toBeDefined()
|
||||
expect(cameraPageTag).toMatch(/\bdark\b/)
|
||||
expect(cameraView).toMatch(
|
||||
/\.camera-page\s*\{[^}]*--sky-bg:\s*#000;[^}]*--sky-text:\s*#fff;[^}]*background:\s*var\(--sky-bg\);[^}]*color:\s*var\(--sky-text\);/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('fills the translucent landscape shell without letterbox gaps', () => {
|
||||
const landscapeViewport = cameraView.match(
|
||||
/\.camera-page--landscape \.camera-viewport\s*\{([^}]*)\}/s,
|
||||
)?.[1]
|
||||
|
||||
expect(cameraView).toMatch(
|
||||
/\.camera-page--landscape\s*\{[^}]*--sky-bg:\s*rgba\(0, 0, 0, 0\.42\);/s,
|
||||
)
|
||||
expect(landscapeViewport).toBeDefined()
|
||||
expect(landscapeViewport).toContain('top: 50%')
|
||||
expect(landscapeViewport).toContain(
|
||||
'width: calc(100% * var(--phone-screen-portrait-ratio))',
|
||||
)
|
||||
expect(landscapeViewport).toContain(
|
||||
'aspect-ratio: var(--phone-screen-portrait-ratio)',
|
||||
)
|
||||
expect(landscapeViewport).not.toContain('16 / 9')
|
||||
expect(cameraView).toMatch(
|
||||
/\.camera-page--landscape \.camera-shade\s*\{[^}]*linear-gradient\([^)]*90deg,[^)]*rgba\(0, 0, 0, 0\.42\) 0 18%,[^)]*transparent 18% 75%,[^)]*rgba\(0, 0, 0, 0\.42\) 75% 100%/s,
|
||||
)
|
||||
expect(appShell).toMatch(
|
||||
/'phone-screen--camera-landscape':\s*activeAppId === 'camera' && phone\.cameraLandscape/,
|
||||
)
|
||||
expect(appWindow).toMatch(
|
||||
/'app-window--camera-landscape':\s*app\.id === 'camera' && phone\.cameraLandscape/,
|
||||
)
|
||||
expect(shellStyles).toMatch(
|
||||
/\.phone-screen\s*\{[^}]*--phone-screen-portrait-ratio:\s*2\.30951;/s,
|
||||
)
|
||||
expect(shellStyles).toMatch(
|
||||
/\.phone-screen--camera-landscape\s*\{[^}]*background:\s*transparent;/s,
|
||||
)
|
||||
expect(shellStyles).toMatch(
|
||||
/\.phone-screen--camera-landscape \.springboard\s*\{[^}]*visibility:\s*hidden;/s,
|
||||
)
|
||||
expect(shellStyles).toMatch(
|
||||
/\.app-window--camera-landscape\s*\{[^}]*background:\s*transparent;/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('renders camera notices as plain text without a glass pill', () => {
|
||||
const noticeMarkup = cameraView.slice(
|
||||
cameraView.indexOf('<span v-if="noticeText"'),
|
||||
cameraView.indexOf('<span v-else-if="pendingCount"'),
|
||||
)
|
||||
const noticeStyles = cameraView.match(
|
||||
/\.camera-notice-text\s*,[\s\S]*?\{([^}]*)\}/,
|
||||
)?.[1]
|
||||
|
||||
expect(noticeMarkup).toContain('class="camera-notice-text"')
|
||||
expect(noticeMarkup).not.toContain('camera-focus-pill')
|
||||
expect(noticeStyles).toBeDefined()
|
||||
expect(noticeStyles).toContain('color: #ffd60a')
|
||||
expect(noticeStyles).not.toMatch(
|
||||
/(?:background|backdrop-filter|border-radius|padding)\s*:/,
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the current compact lens selector treatment', () => {
|
||||
const zoomControl = cameraView.slice(
|
||||
cameraView.indexOf('<div class="camera-zoom-control">'),
|
||||
cameraView.indexOf('<footer class="camera-controls">'),
|
||||
)
|
||||
|
||||
expect(zoomControl).toContain('variant="plain"')
|
||||
expect(zoomControl).not.toContain('glass')
|
||||
expect(zoomControl).toContain('{{ zoomPresetLabel(zoom) }}')
|
||||
expect(cameraView).toContain("return zoom === 0.5 ? '.5' : `${zoom}`")
|
||||
expect(cameraView).toMatch(
|
||||
/\.camera-zoom-control\s*\{[^}]*bottom:\s*216px;/s,
|
||||
)
|
||||
expect(cameraView).toMatch(/\.camera-zoom-row\s*\{[^}]*gap:\s*10px;/s)
|
||||
expect(cameraView).toMatch(
|
||||
/\.camera-zoom-pill\s*\{[^}]*width:\s*36px;[^}]*min-width:\s*36px;[^}]*height:\s*36px;[^}]*min-height:\s*36px;[^}]*padding:\s*0;/s,
|
||||
)
|
||||
expect(cameraView).toMatch(
|
||||
/\.camera-zoom-pill::before\s*\{[^}]*width:\s*var\(--sky-touch-target, 44px\);[^}]*inset-block:\s*-4px;/s,
|
||||
)
|
||||
expect(cameraView).toMatch(
|
||||
/\.camera-zoom-pill\.active\s*\{[^}]*color:\s*#ffd60a;[^}]*background:\s*rgba\(28, 28, 30, 0\.78\);/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps continuous wheel zoom without an extra slider bar', () => {
|
||||
expect(cameraView).not.toContain('camera-zoom-slider')
|
||||
expect(cameraView).not.toContain('type="range"')
|
||||
|
||||
@@ -77,6 +77,16 @@ const pendingCount = computed(
|
||||
() =>
|
||||
captures.value.filter((capture) => capture.status === 'uploading').length,
|
||||
)
|
||||
|
||||
function zoomPresetIsActive(zoom: (typeof zoomLevels)[number]): boolean {
|
||||
return Math.abs(selectedZoom.value - zoom) < 0.03
|
||||
}
|
||||
|
||||
function zoomPresetLabel(zoom: (typeof zoomLevels)[number]): string {
|
||||
if (zoomPresetIsActive(zoom)) return `${zoom}x`
|
||||
return zoom === 0.5 ? '.5' : `${zoom}`
|
||||
}
|
||||
|
||||
function correlationId(): string {
|
||||
return `${Date.now()}-${crypto.randomUUID()}`
|
||||
}
|
||||
@@ -453,6 +463,7 @@ onBeforeUnmount(() => {
|
||||
class="camera-page"
|
||||
:class="{ 'camera-page--landscape': phone.cameraLandscape }"
|
||||
:aria-label="phone.t('Apps.camera.name')"
|
||||
dark
|
||||
>
|
||||
<div class="camera-viewport" @wheel.prevent.stop="zoomWithWheel">
|
||||
<canvas
|
||||
@@ -526,10 +537,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</sky-fab>
|
||||
</div>
|
||||
<span
|
||||
v-if="noticeText"
|
||||
class="camera-focus-pill camera-focus-pill--notice"
|
||||
>
|
||||
<span v-if="noticeText" class="camera-notice-text">
|
||||
{{ noticeText }}
|
||||
</span>
|
||||
<span v-else-if="pendingCount" class="camera-upload-pill">
|
||||
@@ -585,16 +593,16 @@ onBeforeUnmount(() => {
|
||||
<SkyButton
|
||||
v-for="zoom in zoomLevels"
|
||||
:key="zoom"
|
||||
glass
|
||||
rounded
|
||||
variant="plain"
|
||||
class="camera-zoom-pill"
|
||||
:class="{ active: Math.abs(selectedZoom - zoom) < 0.03 }"
|
||||
:class="{ active: zoomPresetIsActive(zoom) }"
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.camera.zoom', { zoom: `${zoom}x` })"
|
||||
:aria-pressed="Math.abs(selectedZoom - zoom) < 0.03"
|
||||
:aria-pressed="zoomPresetIsActive(zoom)"
|
||||
@click="setZoom(zoom)"
|
||||
>
|
||||
{{ zoom }}x
|
||||
{{ zoomPresetLabel(zoom) }}
|
||||
</SkyButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -685,10 +693,15 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.camera-page {
|
||||
--sky-bg: #000;
|
||||
--sky-text: #fff;
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
background: var(--sky-bg);
|
||||
color: var(--sky-text);
|
||||
}
|
||||
.camera-page--landscape {
|
||||
--sky-bg: rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
.camera-viewport {
|
||||
position: absolute;
|
||||
@@ -700,11 +713,11 @@ onBeforeUnmount(() => {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.camera-page--landscape .camera-viewport {
|
||||
top: 46%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: calc(100% * 16 / 9);
|
||||
width: calc(100% * var(--phone-screen-portrait-ratio));
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
aspect-ratio: var(--phone-screen-portrait-ratio);
|
||||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
}
|
||||
.camera-game-view,
|
||||
@@ -762,6 +775,16 @@ onBeforeUnmount(() => {
|
||||
pointer-events: none;
|
||||
background: linear-gradient(#0008, transparent 22%);
|
||||
}
|
||||
.camera-page--landscape .camera-shade {
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0.42) 0 18%,
|
||||
transparent 18% 75%,
|
||||
rgba(0, 0, 0, 0.42) 75% 100%
|
||||
),
|
||||
linear-gradient(#0008, transparent 22%);
|
||||
}
|
||||
.camera-flash {
|
||||
z-index: 8;
|
||||
pointer-events: none;
|
||||
@@ -822,25 +845,22 @@ onBeforeUnmount(() => {
|
||||
.camera-page--landscape .camera-latest svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.camera-focus-pill:not(.sky-button--glass),
|
||||
.camera-upload-pill {
|
||||
min-width: 0;
|
||||
padding: 7px 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #0006;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
.camera-notice-text,
|
||||
.camera-upload-pill {
|
||||
min-width: 0;
|
||||
color: #ffd60a;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
.camera-upload-pill {
|
||||
color: #ffd60a;
|
||||
}
|
||||
.camera-focus-pill--notice {
|
||||
color: #ffd60a;
|
||||
}
|
||||
.camera-lock-control {
|
||||
min-height: 44px;
|
||||
@@ -891,7 +911,7 @@ onBeforeUnmount(() => {
|
||||
.camera-zoom-control {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
bottom: 196px;
|
||||
bottom: 216px;
|
||||
left: 50%;
|
||||
width: auto;
|
||||
transform: translateX(-50%);
|
||||
@@ -899,16 +919,19 @@ onBeforeUnmount(() => {
|
||||
.camera-zoom-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
.camera-zoom-pill {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
@@ -916,8 +939,20 @@ onBeforeUnmount(() => {
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
.camera-zoom-pill::before {
|
||||
width: var(--sky-touch-target, 44px);
|
||||
inset-block: -4px;
|
||||
}
|
||||
.camera-zoom-pill.active {
|
||||
color: #ffd60a;
|
||||
background: rgba(28, 28, 30, 0.78);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.camera-zoom-pill:active:not(:disabled) {
|
||||
background: rgba(28, 28, 30, 0.42);
|
||||
}
|
||||
.camera-zoom-pill.active:active:not(:disabled) {
|
||||
background: rgba(28, 28, 30, 0.86);
|
||||
}
|
||||
.camera-controls {
|
||||
position: absolute;
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('phone apps use Sky UI', () => {
|
||||
|
||||
it('uses shared liquid glass for remaining compact interaction controls', () => {
|
||||
const minimumGlassButtons: Record<string, number> = {
|
||||
'CameraApp.vue': 2,
|
||||
'CameraApp.vue': 1,
|
||||
'FeatherApp.vue': 2,
|
||||
'FlareApp.vue': 1,
|
||||
'FlipTokApp.vue': 2,
|
||||
|
||||
Reference in New Issue
Block a user