mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +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"
|
||||||
:class="{
|
:class="{
|
||||||
'phone-screen--app': isAppRoute || isDevelopmentRoute,
|
'phone-screen--app': isAppRoute || isDevelopmentRoute,
|
||||||
|
'phone-screen--camera-landscape':
|
||||||
|
activeAppId === 'camera' && phone.cameraLandscape,
|
||||||
'phone-app--light': !displayedDarkMode,
|
'phone-app--light': !displayedDarkMode,
|
||||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -460,6 +460,7 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.phone-screen {
|
.phone-screen {
|
||||||
|
--phone-screen-portrait-ratio: 2.30951;
|
||||||
position: relative;
|
position: relative;
|
||||||
container-type: size;
|
container-type: size;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -470,6 +471,12 @@ button {
|
|||||||
background: #08080a;
|
background: #08080a;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
}
|
}
|
||||||
|
.phone-screen--camera-landscape {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.phone-screen--camera-landscape .springboard {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
.phone-app {
|
.phone-app {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
inset: 0 !important;
|
inset: 0 !important;
|
||||||
@@ -3239,6 +3246,9 @@ button {
|
|||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
will-change: transform, border-radius, opacity;
|
will-change: transform, border-radius, opacity;
|
||||||
}
|
}
|
||||||
|
.app-window--camera-landscape {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
.app-window--citywarn {
|
.app-window--citywarn {
|
||||||
backface-visibility: visible;
|
backface-visibility: visible;
|
||||||
filter: none;
|
filter: none;
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ const launchStyle = computed(() => {
|
|||||||
<div
|
<div
|
||||||
v-if="app && !app.adminOnly"
|
v-if="app && !app.adminOnly"
|
||||||
class="app-window"
|
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"
|
:style="launchStyle"
|
||||||
>
|
>
|
||||||
<CustomAppFrame
|
<CustomAppFrame
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ const cameraView = readFileSync(
|
|||||||
new URL('./CameraApp.vue', import.meta.url),
|
new URL('./CameraApp.vue', import.meta.url),
|
||||||
'utf8',
|
'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(
|
const mediaCapture = readFileSync(
|
||||||
new URL('../../components/PhoneMediaCapture.vue', import.meta.url),
|
new URL('../../components/PhoneMediaCapture.vue', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
@@ -47,6 +56,99 @@ describe('Camera app controls', () => {
|
|||||||
expect(cameraView).not.toContain('k-navbar')
|
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', () => {
|
it('keeps continuous wheel zoom without an extra slider bar', () => {
|
||||||
expect(cameraView).not.toContain('camera-zoom-slider')
|
expect(cameraView).not.toContain('camera-zoom-slider')
|
||||||
expect(cameraView).not.toContain('type="range"')
|
expect(cameraView).not.toContain('type="range"')
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ const pendingCount = computed(
|
|||||||
() =>
|
() =>
|
||||||
captures.value.filter((capture) => capture.status === 'uploading').length,
|
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 {
|
function correlationId(): string {
|
||||||
return `${Date.now()}-${crypto.randomUUID()}`
|
return `${Date.now()}-${crypto.randomUUID()}`
|
||||||
}
|
}
|
||||||
@@ -453,6 +463,7 @@ onBeforeUnmount(() => {
|
|||||||
class="camera-page"
|
class="camera-page"
|
||||||
:class="{ 'camera-page--landscape': phone.cameraLandscape }"
|
:class="{ 'camera-page--landscape': phone.cameraLandscape }"
|
||||||
:aria-label="phone.t('Apps.camera.name')"
|
:aria-label="phone.t('Apps.camera.name')"
|
||||||
|
dark
|
||||||
>
|
>
|
||||||
<div class="camera-viewport" @wheel.prevent.stop="zoomWithWheel">
|
<div class="camera-viewport" @wheel.prevent.stop="zoomWithWheel">
|
||||||
<canvas
|
<canvas
|
||||||
@@ -526,10 +537,7 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</sky-fab>
|
</sky-fab>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span v-if="noticeText" class="camera-notice-text">
|
||||||
v-if="noticeText"
|
|
||||||
class="camera-focus-pill camera-focus-pill--notice"
|
|
||||||
>
|
|
||||||
{{ noticeText }}
|
{{ noticeText }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="pendingCount" class="camera-upload-pill">
|
<span v-else-if="pendingCount" class="camera-upload-pill">
|
||||||
@@ -585,16 +593,16 @@ onBeforeUnmount(() => {
|
|||||||
<SkyButton
|
<SkyButton
|
||||||
v-for="zoom in zoomLevels"
|
v-for="zoom in zoomLevels"
|
||||||
:key="zoom"
|
:key="zoom"
|
||||||
glass
|
|
||||||
rounded
|
rounded
|
||||||
|
variant="plain"
|
||||||
class="camera-zoom-pill"
|
class="camera-zoom-pill"
|
||||||
:class="{ active: Math.abs(selectedZoom - zoom) < 0.03 }"
|
:class="{ active: zoomPresetIsActive(zoom) }"
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="phone.t('Apps.camera.zoom', { zoom: `${zoom}x` })"
|
: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)"
|
@click="setZoom(zoom)"
|
||||||
>
|
>
|
||||||
{{ zoom }}x
|
{{ zoomPresetLabel(zoom) }}
|
||||||
</SkyButton>
|
</SkyButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -685,10 +693,15 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.camera-page {
|
.camera-page {
|
||||||
|
--sky-bg: #000;
|
||||||
|
--sky-text: #fff;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
background: #000;
|
background: var(--sky-bg);
|
||||||
color: #fff;
|
color: var(--sky-text);
|
||||||
|
}
|
||||||
|
.camera-page--landscape {
|
||||||
|
--sky-bg: rgba(0, 0, 0, 0.42);
|
||||||
}
|
}
|
||||||
.camera-viewport {
|
.camera-viewport {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -700,11 +713,11 @@ onBeforeUnmount(() => {
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
.camera-page--landscape .camera-viewport {
|
.camera-page--landscape .camera-viewport {
|
||||||
top: 46%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: calc(100% * 16 / 9);
|
width: calc(100% * var(--phone-screen-portrait-ratio));
|
||||||
height: auto;
|
height: auto;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: var(--phone-screen-portrait-ratio);
|
||||||
transform: translate(-50%, -50%) rotate(90deg);
|
transform: translate(-50%, -50%) rotate(90deg);
|
||||||
}
|
}
|
||||||
.camera-game-view,
|
.camera-game-view,
|
||||||
@@ -762,6 +775,16 @@ onBeforeUnmount(() => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: linear-gradient(#0008, transparent 22%);
|
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 {
|
.camera-flash {
|
||||||
z-index: 8;
|
z-index: 8;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -822,25 +845,22 @@ onBeforeUnmount(() => {
|
|||||||
.camera-page--landscape .camera-latest svg {
|
.camera-page--landscape .camera-latest svg {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
.camera-focus-pill:not(.sky-button--glass),
|
|
||||||
.camera-upload-pill {
|
.camera-upload-pill {
|
||||||
min-width: 0;
|
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #0006;
|
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-align: center;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 11px;
|
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 {
|
.camera-lock-control {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
@@ -891,7 +911,7 @@ onBeforeUnmount(() => {
|
|||||||
.camera-zoom-control {
|
.camera-zoom-control {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
bottom: 196px;
|
bottom: 216px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: auto;
|
width: auto;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
@@ -899,16 +919,19 @@ onBeforeUnmount(() => {
|
|||||||
.camera-zoom-row {
|
.camera-zoom-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.camera-zoom-pill {
|
.camera-zoom-pill {
|
||||||
width: 44px;
|
width: 36px;
|
||||||
min-width: 44px;
|
min-width: 36px;
|
||||||
height: 44px;
|
height: 36px;
|
||||||
min-height: 44px;
|
min-height: 36px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 10px;
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.2s ease,
|
background-color 0.2s ease,
|
||||||
@@ -916,8 +939,20 @@ onBeforeUnmount(() => {
|
|||||||
border-color 0.2s ease,
|
border-color 0.2s ease,
|
||||||
box-shadow 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 {
|
.camera-zoom-pill.active {
|
||||||
color: #ffd60a;
|
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 {
|
.camera-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ describe('phone apps use Sky UI', () => {
|
|||||||
|
|
||||||
it('uses shared liquid glass for remaining compact interaction controls', () => {
|
it('uses shared liquid glass for remaining compact interaction controls', () => {
|
||||||
const minimumGlassButtons: Record<string, number> = {
|
const minimumGlassButtons: Record<string, number> = {
|
||||||
'CameraApp.vue': 2,
|
'CameraApp.vue': 1,
|
||||||
'FeatherApp.vue': 2,
|
'FeatherApp.vue': 2,
|
||||||
'FlareApp.vue': 1,
|
'FlareApp.vue': 1,
|
||||||
'FlipTokApp.vue': 2,
|
'FlipTokApp.vue': 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user