diff --git a/frontend/env.d.ts b/frontend/env.d.ts index 5bdd12d..45f2474 100644 --- a/frontend/env.d.ts +++ b/frontend/env.d.ts @@ -1,6 +1,12 @@ /// +declare module 'framework7-icons/vue/vue/*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent + export default component +} + interface Window { GetParentResourceName?: () => string } - diff --git a/frontend/package.json b/frontend/package.json index e97147b..b562569 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "dompurify": "^3.4.13", "emoji-picker-element-data": "^1.8.0", "fix-webm-duration": "^1.0.6", + "framework7-icons": "^5.0.5", "konsta": "~5.2.0", "lucide-vue-next": "^0.525.0", "pinia": "^3.0.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5588678..2d07394 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: fix-webm-duration: specifier: ^1.0.6 version: 1.0.6 + framework7-icons: + specifier: ^5.0.5 + version: 5.0.5 konsta: specifier: ~5.2.0 version: 5.2.0 @@ -1589,6 +1592,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + framework7-icons@5.0.5: + resolution: {integrity: sha512-bvHMLyujV9TFuudehd3ORZ/EvNp19Ir3ckVzYAOf3MkLymHba/9oHLsgopCh0x5UsrYZUpkrE+fd7ggj5y4wRw==} + engines: {node: '>= 0.10.0'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -4085,6 +4092,8 @@ snapshots: forwarded@0.2.0: {} + framework7-icons@5.0.5: {} + fresh@2.0.0: {} fsevents@2.3.3: diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 8fb380c..3e723d7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -70,6 +70,7 @@ import type { EasyShareEvent } from '@/types/easyshare' import { nuiCall } from '@/utils/nui' import { formatTimer } from '@/utils/clock' import { parsePhonePreferences } from '@/utils/preferences' +import { getHairlinePixelStyle } from '@/utils/rendering' import { isTrustedRootMessageSource } from '@/utils/windowMessages' import SpringboardView from '@/views/SpringboardView.vue' @@ -317,6 +318,7 @@ const setupRequired = computed( ) const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)') const viewportScale = ref(getViewportScale()) +const browserDevicePixelRatio = ref(window.devicePixelRatio || 1) const phoneBaseZoom = computed( () => viewportScale.value * @@ -347,6 +349,7 @@ const phoneZoom = computed(() => { ) }) const phoneResolutionStyle = computed(() => ({ + ...getHairlinePixelStyle(phoneZoom.value, browserDevicePixelRatio.value), '--phone-edge-gap': `${24 * viewportScale.value}px`, '--phone-stack-gap': `${16 * viewportScale.value}px`, '--phone-zoom': phoneZoom.value, @@ -1077,6 +1080,7 @@ function onSystemColorSchemeChange(event: MediaQueryListEvent): void { function updateViewportScale(): void { viewportScale.value = getViewportScale() + browserDevicePixelRatio.value = window.devicePixelRatio || 1 } function completeUnlock(): void { @@ -1401,6 +1405,7 @@ onBeforeUnmount(() => { v-for="notification in notifications.devicePreviews" :key="notification.device?.imei" :notification="notification" + :device-pixel-ratio="browserDevicePixelRatio" :zoom=" phoneBaseZoom * ((notification.device?.preferences.settings.phoneScale ?? 100) / @@ -1424,7 +1429,7 @@ onBeforeUnmount(() => {
{ ) expect(source).toContain('else loadUnlockedPhoneData()') }) + + it('keeps hairlines at one rendered device pixel through phone zoom', () => { + expect(source).toContain( + '...getHairlinePixelStyle(phoneZoom.value, browserDevicePixelRatio.value)', + ) + expect(source).toContain(':device-pixel-ratio="browserDevicePixelRatio"') + }) }) diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 73b8497..1997b10 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -357,9 +357,12 @@ button { .phone-app--performance .sky-ui-provider, .phone-app--performance .sky-app, .phone-app--performance .sky-app-page { - --sky-glass: var(--phone-performance-glass); - --sky-glass-solid: var(--phone-performance-glass); + /* Resolve Glass from the nearest Sky theme instead of the outer phone theme. */ + --sky-glass: var(--sky-glass-solid); --sky-navbar-glass: var(--sky-bg); + --sky-shadow-glass: none; + --sky-shadow-glass-fab: none; + --sky-shadow-glass-fab-outer: none; --sky-hold-thumb-scale: 1; --sky-range-hold-background: #ffffff; --sky-range-hold-shadow: var(--sky-shadow-thumb); @@ -378,28 +381,42 @@ button { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; } -.phone-app--performance .k-glass { - background-color: var(--phone-performance-glass) !important; - scale: 1 !important; +.phone-app--performance .k-glass, +.phone-app--performance + .k-range + [class~='group-has-[input:active]/range:scale-[1.4]'], +.phone-app--performance .k-toggle [class~='group-active:scale-[0.75]'], +.phone-app--performance .k-toggle [class~='group-active:scale-[1.4]'] { + --tw-scale-x: 1 !important; + --tw-scale-y: 1 !important; + --tw-scale-z: 1 !important; + transform: + translate3d( + var(--tw-translate-x, 0), + var(--tw-translate-y, 0), + 0 + ) + scale(1) !important; } -.phone-app--performance .k-glass > [class~='rounded-[inherit]'] { - display: none !important; +.phone-app--performance .k-glass { + box-shadow: none !important; +} +/* Konsta's default background utility is the solid-surface opt-in marker. + Custom color utilities replace it, so semantic Glass colors stay intact. */ +.phone-app--performance + .k-glass:where([class~='bg-ios-light-glass']) { + background: var(--phone-performance-glass) !important; } .phone-app--performance .k-range [class~='group-has-[input:active]/range:scale-[1.4]'] { background-color: #ffffff !important; - scale: 1 !important; } .phone-app--performance .k-range [class~='group-has-[input:active]/range:opacity-100'] { opacity: 0 !important; } -.phone-app--performance .k-toggle [class~='group-active:scale-[0.75]'], -.phone-app--performance .k-toggle [class~='group-active:scale-[1.4]'] { - scale: 1 !important; -} .phone-app--performance .k-toggle [class~='group-active:bg-transparent'] { background-color: #ffffff !important; } @@ -412,16 +429,20 @@ button { .phone-app--performance .sky-glass--touch-highlight { transform: none; } +.phone-app--performance .sky-glass, +.phone-app--performance .sky-navbar__back--surface { + box-shadow: none !important; +} .phone-app--performance .sky-segmented--navigation { - background: var(--phone-performance-glass); + background: var(--sky-glass-solid); box-shadow: none; } .phone-app--performance .sky-segmented--navbar { - background: var(--phone-performance-glass); + background: var(--sky-glass-solid); box-shadow: none; } .phone-app--performance .sky-tabbar__pane { - background: var(--phone-performance-glass); + background: var(--sky-glass-solid); box-shadow: none; } .phone-app--performance .control-center__backdrop { @@ -461,6 +482,22 @@ button { .phone-app--performance .banking-app__aurora { display: none; } +.phone-app--performance .phone-effect--expensive-shadow { + box-shadow: none !important; +} +.phone-app--performance .phone-effect--decorative { + display: none !important; +} +.phone-app--performance .phone-effect--filtered-media { + filter: none !important; +} +.phone-app--performance .phone-effect--solid-surface { + background: var( + --phone-effect-solid-background, + var(--phone-performance-glass) + ) !important; + box-shadow: none !important; +} /* Chromium 103 cannot resolve color-mix() when the mixed color is a variable. */ @supports not (color: color-mix(in srgb, white, black)) { diff --git a/frontend/src/components/NotificationPhonePreview.vue b/frontend/src/components/NotificationPhonePreview.vue index 3d253fc..cccefab 100644 --- a/frontend/src/components/NotificationPhonePreview.vue +++ b/frontend/src/components/NotificationPhonePreview.vue @@ -7,8 +7,10 @@ import PhoneNotifications from '@/components/PhoneNotifications.vue' import { PHONE_FRAME_IMAGES } from '@/config/appearance' import type { PhoneNotification } from '@/stores/notifications' import { usePhoneStore } from '@/stores/phone' +import { getHairlinePixelStyle } from '@/utils/rendering' const props = defineProps<{ + devicePixelRatio: number notification: PhoneNotification zoom: number }>() @@ -28,7 +30,10 @@ const isDarkMode = computed(() => { const frameImage = computed( () => PHONE_FRAME_IMAGES[preferences.value.settings.frame], ) -const wrapperStyle = computed(() => ({ zoom: props.zoom })) +const wrapperStyle = computed(() => ({ + ...getHairlinePixelStyle(props.zoom, props.devicePixelRatio), + zoom: props.zoom, +})) - +
{ /> @@ -1898,7 +1899,7 @@ onBeforeUnmount(() => { back-appearance="surface" @back="composeOpen = false" /> - +
@@ -2095,7 +2096,7 @@ onBeforeUnmount(() => { - +