ADD - Graphics modes and new phone frames
Introduces Graphics Mode settings to toggle between "Performance" (blur-free) and "Ultimate" (full effects) to optimize CEF rendering. Expands device customization with ten new phone frames, standardizes 24-hour time formatting across all apps, and updates the build target to Chrome 103 for better compatibility.
@@ -353,6 +353,7 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
appId: 'calendar',
|
||||
subtitle: new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(startsAt),
|
||||
text:
|
||||
@@ -747,10 +748,21 @@ onBeforeUnmount(() => {
|
||||
v-if="phone.isOpen || notifications.current"
|
||||
class="phone-resolution-wrapper phone-resolution-wrapper--primary"
|
||||
>
|
||||
<section class="phone-device" :aria-label="phone.t('Common.phone')">
|
||||
<section
|
||||
class="phone-device"
|
||||
:class="{
|
||||
'phone-app--light': !phone.isDarkMode,
|
||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
:aria-label="phone.t('Common.phone')"
|
||||
>
|
||||
<div
|
||||
class="phone-screen"
|
||||
:class="{ 'phone-screen--app': isAppRoute }"
|
||||
:class="{
|
||||
'phone-screen--app': isAppRoute,
|
||||
'phone-app--light': !phone.isDarkMode,
|
||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
>
|
||||
<k-app
|
||||
theme="ios"
|
||||
@@ -761,6 +773,7 @@ onBeforeUnmount(() => {
|
||||
:class="{
|
||||
dark: phone.isDarkMode,
|
||||
'phone-app--light': !phone.isDarkMode,
|
||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||
'phone-app--unlocking': isUnlocking,
|
||||
}"
|
||||
>
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -324,6 +324,81 @@ button {
|
||||
.phone-app--light {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* FiveM CEF blur-free glass fallback. Ultimate keeps the native effects. */
|
||||
.phone-app--performance {
|
||||
--phone-performance-glass: rgb(28 28 31 / 96%);
|
||||
}
|
||||
.phone-app--performance.phone-app--light {
|
||||
--phone-performance-glass: rgb(245 245 248 / 97%);
|
||||
}
|
||||
.phone-device.phone-app--performance {
|
||||
filter: none;
|
||||
}
|
||||
.phone-app--performance,
|
||||
.phone-app--performance *,
|
||||
.phone-app--performance *::before,
|
||||
.phone-app--performance *::after {
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
.phone-app--performance .k-glass {
|
||||
background-color: var(--phone-performance-glass) !important;
|
||||
}
|
||||
.phone-app--performance .control-center__backdrop {
|
||||
background: rgb(20 20 24 / 92%);
|
||||
}
|
||||
.phone-app--performance .app-dock {
|
||||
background-color: rgb(38 38 46 / 92%);
|
||||
}
|
||||
.phone-app--performance .widget {
|
||||
background-color: rgb(21 24 34 / 95%);
|
||||
}
|
||||
.phone-app--performance .app-library-search,
|
||||
.phone-app--performance .app-search,
|
||||
.phone-app--performance .app-library-group__icons {
|
||||
background-color: rgb(18 18 25 / 92%);
|
||||
}
|
||||
.phone-app--performance .app-library-all {
|
||||
background-color: rgb(9 9 16 / 97%);
|
||||
}
|
||||
.phone-screen--app .phone-app--performance .springboard,
|
||||
.phone-app--performance .lock-screen__time,
|
||||
.phone-app--performance .lock-screen-leave-to,
|
||||
.phone-app--performance .minesweeper-marker-drop__trail,
|
||||
.phone-app--performance .flappy-clouds i::after {
|
||||
filter: none !important;
|
||||
}
|
||||
.phone-app--performance .springboard {
|
||||
transition-property: transform !important;
|
||||
}
|
||||
.phone-app--performance .lock-screen {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.phone-app--performance .lock-screen-leave-active {
|
||||
transition-property: transform, opacity !important;
|
||||
}
|
||||
.phone-app--performance .sim-picker__glow,
|
||||
.phone-app--performance .banking-app__aurora {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Chromium 103 cannot resolve color-mix() when the mixed color is a variable. */
|
||||
@supports not (color: color-mix(in srgb, white, black)) {
|
||||
.flappy-menu__tower,
|
||||
.flappy-obstacle span {
|
||||
background: var(--tower);
|
||||
}
|
||||
|
||||
.flappy-menu__tower::after {
|
||||
border: 3px solid var(--tower);
|
||||
background: var(--tower);
|
||||
}
|
||||
|
||||
.flappy-obstacle span::after {
|
||||
background: var(--tower);
|
||||
}
|
||||
}
|
||||
.phone-display-dimmer {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
@@ -369,7 +444,7 @@ button {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
@@ -377,6 +452,12 @@ button {
|
||||
text-shadow: 0 1px 3px #0009;
|
||||
pointer-events: none;
|
||||
}
|
||||
.phone-status-bar__time {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: calc(50% - 70px);
|
||||
text-align: center;
|
||||
}
|
||||
.phone-status-bar__indicators {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -483,13 +564,13 @@ button {
|
||||
.lock-screen__status {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 11px;
|
||||
right: 26px;
|
||||
left: 26px;
|
||||
top: 17px;
|
||||
right: 30px;
|
||||
left: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-height: 18px;
|
||||
min-height: 26px;
|
||||
filter: drop-shadow(0 1px 2px #0009);
|
||||
}
|
||||
.lock-screen__lock {
|
||||
@@ -505,7 +586,7 @@ button {
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
gap: 6px;
|
||||
}
|
||||
.lock-screen__content {
|
||||
position: relative;
|
||||
@@ -5987,6 +6068,7 @@ button {
|
||||
}
|
||||
.messages-inbox-page .messages-conversation:active,
|
||||
.messages-inbox-page .messages-conversation--selected {
|
||||
background: var(--messages-inbox-control);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--messages-inbox-control) 86%,
|
||||
|
||||
@@ -37,9 +37,19 @@ const wrapperStyle = computed<CSSProperties>(() => ({ zoom: props.zoom }))
|
||||
>
|
||||
<section
|
||||
class="phone-device phone-device--notification"
|
||||
:class="{
|
||||
'phone-app--light': !isDarkMode,
|
||||
[`phone-app--${preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
:aria-label="device.name"
|
||||
>
|
||||
<div class="phone-screen">
|
||||
<div
|
||||
class="phone-screen"
|
||||
:class="{
|
||||
'phone-app--light': !isDarkMode,
|
||||
[`phone-app--${preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
>
|
||||
<k-app
|
||||
theme="ios"
|
||||
:dark="isDarkMode"
|
||||
@@ -48,6 +58,7 @@ const wrapperStyle = computed<CSSProperties>(() => ({ zoom: props.zoom }))
|
||||
:class="{
|
||||
dark: isDarkMode,
|
||||
'phone-app--light': !isDarkMode,
|
||||
[`phone-app--${preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { kFab } from 'konsta/vue'
|
||||
import {
|
||||
BatteryMedium,
|
||||
Camera,
|
||||
Flashlight,
|
||||
LockKeyhole,
|
||||
Signal,
|
||||
Wifi,
|
||||
} from 'lucide-vue-next'
|
||||
import { Camera, Flashlight, LockKeyhole } from 'lucide-vue-next'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import PhoneStatusIndicators from '@/components/PhoneStatusIndicators.vue'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
@@ -41,7 +35,8 @@ const date = computed(() =>
|
||||
)
|
||||
const time = computed(() =>
|
||||
new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: 'numeric',
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
})
|
||||
.formatToParts(now.value)
|
||||
@@ -140,11 +135,7 @@ onBeforeUnmount(() => {
|
||||
<div class="lock-screen__shade" aria-hidden="true"></div>
|
||||
|
||||
<header class="lock-screen__status">
|
||||
<div class="lock-screen__indicators" aria-hidden="true">
|
||||
<Signal :size="12" :stroke-width="2.5" />
|
||||
<Wifi :size="13" :stroke-width="2.5" />
|
||||
<BatteryMedium :size="17" :stroke-width="2.4" />
|
||||
</div>
|
||||
<PhoneStatusIndicators class="lock-screen__indicators" />
|
||||
</header>
|
||||
<LockKeyhole
|
||||
class="lock-screen__lock"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { BatteryMedium, Plane, Signal, Wifi } from 'lucide-vue-next'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import PhoneStatusIndicators from '@/components/PhoneStatusIndicators.vue'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
@@ -13,7 +13,8 @@ let intervalId: number | undefined
|
||||
|
||||
function updateTime(): void {
|
||||
time.value = new Intl.DateTimeFormat([], {
|
||||
hour: 'numeric',
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(new Date())
|
||||
}
|
||||
@@ -30,7 +31,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<header class="phone-status-bar" :aria-label="phone.t('Common.phoneStatus')">
|
||||
<time>{{ time }}</time>
|
||||
<time class="phone-status-bar__time">{{ time }}</time>
|
||||
<button
|
||||
class="phone-status-bar__indicators"
|
||||
type="button"
|
||||
@@ -38,28 +39,11 @@ onBeforeUnmount(() => {
|
||||
:aria-expanded="controlCenterOpened"
|
||||
@click.stop="emit('controlCenter')"
|
||||
>
|
||||
<Plane
|
||||
v-if="phone.preferences.settings.airplaneMode"
|
||||
:size="18"
|
||||
:stroke-width="2.5"
|
||||
aria-hidden="true"
|
||||
<PhoneStatusIndicators
|
||||
:airplane-mode="phone.preferences.settings.airplaneMode"
|
||||
:cellular-enabled="phone.preferences.settings.cellularEnabled"
|
||||
:wifi-enabled="phone.preferences.settings.wifiEnabled"
|
||||
/>
|
||||
<Signal
|
||||
v-else-if="phone.preferences.settings.cellularEnabled"
|
||||
:size="19"
|
||||
:stroke-width="2.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Wifi
|
||||
v-if="
|
||||
phone.preferences.settings.wifiEnabled &&
|
||||
!phone.preferences.settings.airplaneMode
|
||||
"
|
||||
:size="20"
|
||||
:stroke-width="2.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<BatteryMedium :size="26" :stroke-width="2.4" aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { Plane } from 'lucide-vue-next'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
airplaneMode?: boolean
|
||||
cellularEnabled?: boolean
|
||||
wifiEnabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
airplaneMode: false,
|
||||
cellularEnabled: true,
|
||||
wifiEnabled: true,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="phone-status-indicators" aria-hidden="true">
|
||||
<Plane
|
||||
v-if="airplaneMode"
|
||||
:size="18"
|
||||
:stroke-width="2.6"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<svg
|
||||
v-else-if="cellularEnabled"
|
||||
class="phone-status-indicators__signal"
|
||||
viewBox="0 0 20 16"
|
||||
>
|
||||
<rect x="0" y="11" width="3.5" height="5" rx="1.75" />
|
||||
<rect x="5.5" y="8" width="3.5" height="8" rx="1.75" />
|
||||
<rect x="11" y="4" width="3.5" height="12" rx="1.75" />
|
||||
<rect x="16.5" width="3.5" height="16" rx="1.75" opacity="0.42" />
|
||||
</svg>
|
||||
<svg
|
||||
v-if="wifiEnabled && !airplaneMode"
|
||||
class="phone-status-indicators__wifi"
|
||||
viewBox="0 0 22 17"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M1.5 5.4a14.2 14.2 0 0 1 19 0" />
|
||||
<path d="M5 9.2a9 9 0 0 1 12 0" />
|
||||
<path d="M8.5 13a3.8 3.8 0 0 1 5 0" />
|
||||
<circle cx="11" cy="15.3" r="1.45" />
|
||||
</svg>
|
||||
<svg class="phone-status-indicators__battery" viewBox="0 0 29 16">
|
||||
<rect x="0.75" y="0.75" width="25" height="14.5" rx="4" />
|
||||
<rect x="3" y="3" width="14.5" height="10" rx="2.25" />
|
||||
<rect x="26.6" y="5" width="2.4" height="6" rx="1.2" />
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.phone-status-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.phone-status-indicators > svg {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.phone-status-indicators__signal {
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.phone-status-indicators__wifi {
|
||||
width: 19px;
|
||||
height: 15px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2.2;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.phone-status-indicators__wifi circle {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.phone-status-indicators__battery {
|
||||
width: 25px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.phone-status-indicators__battery rect:first-child {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -48,7 +48,11 @@ function close(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sim-picker-backdrop" @click.self="close">
|
||||
<div
|
||||
class="sim-picker-backdrop"
|
||||
:class="`phone-app--${phone.preferences.settings.graphicsMode}`"
|
||||
@click.self="close"
|
||||
>
|
||||
<section class="sim-picker" aria-modal="true" role="dialog">
|
||||
<div class="sim-picker__glow sim-picker__glow--top" />
|
||||
<div class="sim-picker__glow sim-picker__glow--bottom" />
|
||||
|
||||
@@ -154,6 +154,7 @@ function avatar(name: string): string {
|
||||
function formatForecastHour(timestamp: number): string {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(timestamp)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import blackFrame from '@/assets/img/frames/black.webp'
|
||||
import blueFrame from '@/assets/img/frames/blue.webp'
|
||||
import cyanFrame from '@/assets/img/frames/cyan.webp'
|
||||
import goldFrame from '@/assets/img/frames/gold.webp'
|
||||
import greenFrame from '@/assets/img/frames/green.webp'
|
||||
import lavenderFrame from '@/assets/img/frames/lavender.webp'
|
||||
import limeFrame from '@/assets/img/frames/lime.webp'
|
||||
import orangeFrame from '@/assets/img/frames/orange.webp'
|
||||
import pinkFrame from '@/assets/img/frames/pink.webp'
|
||||
import purpleFrame from '@/assets/img/frames/purple.webp'
|
||||
import redFrame from '@/assets/img/frames/red.webp'
|
||||
import rgbFrame from '@/assets/img/frames/rgb.webp'
|
||||
import tealFrame from '@/assets/img/frames/teal.webp'
|
||||
import whiteFrame from '@/assets/img/frames/white.webp'
|
||||
import yellowFrame from '@/assets/img/frames/yellow.webp'
|
||||
import type { PhoneFrameId } from '@/utils/preferences'
|
||||
|
||||
export const PHONE_FRAME_IMAGES: Record<PhoneFrameId, string> = {
|
||||
@@ -10,7 +20,17 @@ export const PHONE_FRAME_IMAGES: Record<PhoneFrameId, string> = {
|
||||
blue: blueFrame,
|
||||
green: greenFrame,
|
||||
lavender: lavenderFrame,
|
||||
red: redFrame,
|
||||
white: whiteFrame,
|
||||
orange: orangeFrame,
|
||||
yellow: yellowFrame,
|
||||
lime: limeFrame,
|
||||
teal: tealFrame,
|
||||
cyan: cyanFrame,
|
||||
purple: purpleFrame,
|
||||
pink: pinkFrame,
|
||||
gold: goldFrame,
|
||||
rgb: rgbFrame,
|
||||
}
|
||||
|
||||
export const PHONE_FRAME_COLORS: Record<PhoneFrameId, string> = {
|
||||
@@ -18,5 +38,15 @@ export const PHONE_FRAME_COLORS: Record<PhoneFrameId, string> = {
|
||||
blue: '#7294c2',
|
||||
green: '#889b6e',
|
||||
lavender: '#aaa1c8',
|
||||
red: '#d93f45',
|
||||
white: '#f2f2f2',
|
||||
orange: '#e68a3f',
|
||||
yellow: '#e5d134',
|
||||
lime: '#75d13d',
|
||||
teal: '#36a992',
|
||||
cyan: '#35bfe3',
|
||||
purple: '#8251df',
|
||||
pink: '#db82b0',
|
||||
gold: '#d4aa45',
|
||||
rgb: 'conic-gradient(#ff3b30, #ffcc00, #34c759, #00c7be, #007aff, #af52de, #ff2d55, #ff3b30)',
|
||||
}
|
||||
|
||||
@@ -90,20 +90,6 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
labelKey: 'Apps.calendar.name',
|
||||
route: '/apps/calendar',
|
||||
},
|
||||
{
|
||||
category: 'social',
|
||||
component: markRaw(
|
||||
defineAsyncComponent(() => import('@/views/apps/LocalPagesApp.vue')),
|
||||
),
|
||||
dockOrder: null,
|
||||
gridOrder: 20,
|
||||
icon: markRaw(MapPinHouse),
|
||||
iconClass: 'app-icon--local-pages',
|
||||
iconImage: localPagesIcon,
|
||||
id: 'local-pages',
|
||||
labelKey: 'Apps.localPages.name',
|
||||
route: '/apps/local-pages',
|
||||
},
|
||||
{
|
||||
category: 'social',
|
||||
component: markRaw(
|
||||
@@ -118,6 +104,20 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
labelKey: 'Apps.radio.name',
|
||||
route: '/apps/radio',
|
||||
},
|
||||
{
|
||||
category: 'social',
|
||||
component: markRaw(
|
||||
defineAsyncComponent(() => import('@/views/apps/LocalPagesApp.vue')),
|
||||
),
|
||||
dockOrder: null,
|
||||
gridOrder: 20,
|
||||
icon: markRaw(MapPinHouse),
|
||||
iconClass: 'app-icon--local-pages',
|
||||
iconImage: localPagesIcon,
|
||||
id: 'local-pages',
|
||||
labelKey: 'Apps.localPages.name',
|
||||
route: '/apps/local-pages',
|
||||
},
|
||||
{
|
||||
category: 'social',
|
||||
component: markRaw(
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useClockService() {
|
||||
time: computed(() =>
|
||||
new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(now.value),
|
||||
),
|
||||
|
||||
@@ -1530,6 +1530,13 @@ const defaultLocales: LocaleTree = {
|
||||
notificationVolume: 'Notification Volume',
|
||||
ringtone: 'Ringtone',
|
||||
notificationSound: 'Notification Sound',
|
||||
graphicsMode: 'Graphics Mode',
|
||||
performanceMode: 'Performance Mode',
|
||||
performanceModeDescription:
|
||||
'Blur-free glass simulation for better CEF performance',
|
||||
ultimateMode: 'Ultimate Mode',
|
||||
ultimateModeDescription:
|
||||
'Full blur and glass effects for compatible FiveM clients',
|
||||
appearanceMode: 'Appearance Mode',
|
||||
automatic: 'Automatic',
|
||||
light: 'Light',
|
||||
@@ -1617,7 +1624,17 @@ const defaultLocales: LocaleTree = {
|
||||
blue: 'Blue',
|
||||
green: 'Green',
|
||||
lavender: 'Lavender',
|
||||
red: 'Red',
|
||||
white: 'White',
|
||||
orange: 'Orange',
|
||||
yellow: 'Yellow',
|
||||
lime: 'Lime',
|
||||
teal: 'Teal',
|
||||
cyan: 'Cyan',
|
||||
purple: 'Purple',
|
||||
pink: 'Pink',
|
||||
gold: 'Gold',
|
||||
rgb: 'RGB',
|
||||
},
|
||||
ringtones: {
|
||||
skyline: 'Skyline',
|
||||
|
||||
@@ -16,6 +16,8 @@ describe('preferences', () => {
|
||||
bluetoothEnabled: false,
|
||||
cellularEnabled: false,
|
||||
focusMode: true,
|
||||
frame: 'rgb',
|
||||
graphicsMode: 'ultimate',
|
||||
notificationVolume: 45,
|
||||
notificationDurationSeconds: 14,
|
||||
notifications: {
|
||||
@@ -34,6 +36,8 @@ describe('preferences', () => {
|
||||
expect(value.settings.bluetoothEnabled).toBe(false)
|
||||
expect(value.settings.cellularEnabled).toBe(false)
|
||||
expect(value.settings.focusMode).toBe(true)
|
||||
expect(value.settings.frame).toBe('rgb')
|
||||
expect(value.settings.graphicsMode).toBe('ultimate')
|
||||
expect(value.settings.notificationVolume).toBe(45)
|
||||
expect(value.settings.notificationDurationSeconds).toBe(14)
|
||||
expect(value.settings.notifications.messages).toEqual({
|
||||
@@ -61,7 +65,8 @@ describe('preferences', () => {
|
||||
version: 1,
|
||||
settings: {
|
||||
appearanceMode: 'neon',
|
||||
frame: 'gold',
|
||||
frame: 'bronze',
|
||||
graphicsMode: 'cinematic',
|
||||
notificationVolume: -10,
|
||||
notificationDurationSeconds: 100,
|
||||
phoneScale: 500,
|
||||
@@ -73,6 +78,7 @@ describe('preferences', () => {
|
||||
|
||||
expect(value.settings.appearanceMode).toBe('automatic')
|
||||
expect(value.settings.frame).toBe('black')
|
||||
expect(value.settings.graphicsMode).toBe('performance')
|
||||
expect(value.settings.notificationVolume).toBe(0)
|
||||
expect(value.settings.notificationDurationSeconds).toBe(30)
|
||||
expect(value.settings.phoneScale).toBe(150)
|
||||
@@ -90,6 +96,7 @@ describe('preferences', () => {
|
||||
bluetoothEnabled: true,
|
||||
cellularEnabled: true,
|
||||
focusMode: false,
|
||||
graphicsMode: 'performance',
|
||||
rotationLocked: false,
|
||||
screenBrightness: 100,
|
||||
wifiEnabled: true,
|
||||
|
||||
@@ -2,12 +2,23 @@ import type { LaunchablePhoneAppId } from '@/types/apps'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
|
||||
export const APPEARANCE_MODE_IDS = ['automatic', 'light', 'dark'] as const
|
||||
export const GRAPHICS_MODE_IDS = ['performance', 'ultimate'] as const
|
||||
export const PHONE_FRAME_IDS = [
|
||||
'black',
|
||||
'blue',
|
||||
'green',
|
||||
'lavender',
|
||||
'red',
|
||||
'white',
|
||||
'orange',
|
||||
'yellow',
|
||||
'lime',
|
||||
'teal',
|
||||
'cyan',
|
||||
'purple',
|
||||
'pink',
|
||||
'gold',
|
||||
'rgb',
|
||||
] as const
|
||||
export const RINGTONE_IDS = ['skyline', 'horizon', 'pulse'] as const
|
||||
export const NOTIFICATION_SOUND_IDS = ['chime', 'signal', 'soft'] as const
|
||||
@@ -17,6 +28,7 @@ export const PHONE_SCALE_MAX = 150
|
||||
export const PHONE_SCALE_STEP = 5
|
||||
|
||||
export type AppearanceMode = (typeof APPEARANCE_MODE_IDS)[number]
|
||||
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]
|
||||
@@ -34,6 +46,7 @@ export type PhonePreferencesV1 = {
|
||||
cellularEnabled: boolean
|
||||
focusMode: boolean
|
||||
frame: PhoneFrameId
|
||||
graphicsMode: GraphicsMode
|
||||
notificationSound: NotificationSoundId
|
||||
notificationDurationSeconds: number
|
||||
notificationVolume: number
|
||||
@@ -91,6 +104,7 @@ export const DEFAULT_PHONE_PREFERENCES: PhonePreferencesV1 = {
|
||||
cellularEnabled: true,
|
||||
focusMode: false,
|
||||
frame: 'black',
|
||||
graphicsMode: 'performance',
|
||||
notificationSound: 'chime',
|
||||
notificationDurationSeconds: 10,
|
||||
notificationVolume: 70,
|
||||
@@ -188,6 +202,11 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
|
||||
),
|
||||
focusMode: readBoolean(settings.focusMode, defaults.focusMode),
|
||||
frame: readChoice(settings.frame, PHONE_FRAME_IDS, defaults.frame),
|
||||
graphicsMode: readChoice(
|
||||
settings.graphicsMode,
|
||||
GRAPHICS_MODE_IDS,
|
||||
defaults.graphicsMode,
|
||||
),
|
||||
notificationSound: readChoice(
|
||||
settings.notificationSound,
|
||||
NOTIFICATION_SOUND_IDS,
|
||||
|
||||
@@ -127,6 +127,7 @@ function formatDate(timestamp: number): string {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
month: 'short',
|
||||
}).format(timestamp)
|
||||
|
||||
@@ -236,6 +236,7 @@ function eventsForDay(day: Date): CalendarEvent[] {
|
||||
function formatTime(value: number): string {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(value)
|
||||
}
|
||||
@@ -1584,6 +1585,7 @@ onMounted(async () => {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--bg);
|
||||
background: color-mix(in srgb, var(--bg) 82%, transparent);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
@@ -1902,6 +1904,7 @@ onMounted(async () => {
|
||||
border-bottom: 0;
|
||||
border-radius: 28px 28px 0 0;
|
||||
overflow: hidden;
|
||||
background: var(--elevated);
|
||||
background: color-mix(in srgb, var(--elevated) 94%, transparent);
|
||||
box-shadow: 0 -18px 60px rgb(0 0 0 / 38%);
|
||||
backdrop-filter: blur(30px) saturate(180%);
|
||||
|
||||
@@ -345,6 +345,7 @@ function relativeDate(value: DatabaseDateValue): string {
|
||||
function messageTime(value: DatabaseDateValue): string {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(parseDatabaseDate(value))
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ const timerPicker = computed({
|
||||
const currentTime = computed(() =>
|
||||
new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
timeZone: browserTimeZone,
|
||||
}).format(now.value),
|
||||
|
||||
@@ -239,6 +239,7 @@ function formatDate(value: DatabaseDateValue): string {
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ function formatDate(value: string): string {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
month: 'short',
|
||||
}).format(date)
|
||||
@@ -197,6 +198,7 @@ function formatListDate(value: string): string {
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ function formatConversationDate(value: DatabaseDateValue): string {
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
@@ -228,6 +229,7 @@ function timeLabel(value: DatabaseDateValue): string {
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
@@ -295,7 +295,11 @@ function togglePinned(): void {
|
||||
<k-popover
|
||||
:opened="menuOpened"
|
||||
:target="menuTarget"
|
||||
:class="{ dark: phone.isDarkMode }"
|
||||
:class="{
|
||||
dark: phone.isDarkMode,
|
||||
'phone-app--light': !phone.isDarkMode,
|
||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||
}"
|
||||
angle
|
||||
@backdropclick="menuOpened = false"
|
||||
>
|
||||
|
||||
@@ -342,7 +342,7 @@ onBeforeUnmount(() => {
|
||||
@input="displayNameInput = eventValue($event)"
|
||||
/>
|
||||
</k-list>
|
||||
<k-block inset class="radio-hint-block">
|
||||
<k-block class="radio-hint-block">
|
||||
<p class="radio-setting-hint">
|
||||
{{
|
||||
phone.t(
|
||||
@@ -442,20 +442,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
.radio-tabs :deep(button) {
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radio-tabs {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.radio-tabs :deep(> span.start-1) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.radio-loading {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -530,8 +521,6 @@ onBeforeUnmount(() => {
|
||||
.radio-hint-block {
|
||||
margin-bottom: 0;
|
||||
margin-top: 8px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.radio-settings-title {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { nuiCall } from '@/utils/nui'
|
||||
import { formatPhoneNumber } from '@/utils/phone'
|
||||
import {
|
||||
APPEARANCE_MODE_IDS,
|
||||
GRAPHICS_MODE_IDS,
|
||||
NOTIFICATION_SOUND_IDS,
|
||||
PHONE_FRAME_IDS,
|
||||
PHONE_SCALE_MAX,
|
||||
@@ -74,6 +75,7 @@ import {
|
||||
RINGTONE_IDS,
|
||||
WALLPAPER_IDS,
|
||||
type AppearanceMode,
|
||||
type GraphicsMode,
|
||||
type NotificationSoundId,
|
||||
type PhoneFrameId,
|
||||
type RingtoneId,
|
||||
@@ -111,7 +113,17 @@ type PasscodeFlow =
|
||||
const FACTORY_RESET_DURATION_MS = 60_000
|
||||
const FACTORY_RESET_CIRCUMFERENCE = 2 * Math.PI * 48
|
||||
const FRAME_PICKER_WIDTH = 240
|
||||
const FRAME_PICKER_HEIGHT = 140
|
||||
const FRAME_PICKER_COLUMN_COUNT = 3
|
||||
const FRAME_PICKER_SWATCH_SIZE = 40
|
||||
const FRAME_PICKER_GRID_GAP = 20
|
||||
const FRAME_PICKER_PADDING = 20
|
||||
const FRAME_PICKER_ROW_COUNT = Math.ceil(
|
||||
PHONE_FRAME_IDS.length / FRAME_PICKER_COLUMN_COUNT,
|
||||
)
|
||||
const FRAME_PICKER_HEIGHT =
|
||||
FRAME_PICKER_PADDING * 2 +
|
||||
FRAME_PICKER_ROW_COUNT * FRAME_PICKER_SWATCH_SIZE +
|
||||
Math.max(0, FRAME_PICKER_ROW_COUNT - 1) * FRAME_PICKER_GRID_GAP
|
||||
const FRAME_PICKER_INSET = 8
|
||||
const FRAME_PICKER_GAP = 8
|
||||
|
||||
@@ -465,6 +477,10 @@ function selectAppearanceMode(mode: AppearanceMode): void {
|
||||
phone.setPreference('appearanceMode', mode)
|
||||
}
|
||||
|
||||
function selectGraphicsMode(mode: GraphicsMode): void {
|
||||
phone.setPreference('graphicsMode', mode)
|
||||
}
|
||||
|
||||
function selectFrame(frame: PhoneFrameId): void {
|
||||
phone.setPreference('frame', frame)
|
||||
framePickerOpened.value = false
|
||||
@@ -1357,6 +1373,28 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeView === 'appearance'">
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.graphicsMode') }}
|
||||
</k-block-title>
|
||||
<k-list strong inset>
|
||||
<k-list-item
|
||||
v-for="mode in GRAPHICS_MODE_IDS"
|
||||
:key="mode"
|
||||
link
|
||||
:chevron="false"
|
||||
:title="phone.t(`Apps.settings.${mode}Mode`)"
|
||||
:subtitle="phone.t(`Apps.settings.${mode}ModeDescription`)"
|
||||
@click="selectGraphicsMode(mode)"
|
||||
>
|
||||
<template #after>
|
||||
<Check
|
||||
v-if="phone.preferences.settings.graphicsMode === mode"
|
||||
class="w-5 h-5 text-primary"
|
||||
/>
|
||||
</template>
|
||||
</k-list-item>
|
||||
</k-list>
|
||||
|
||||
<k-block-title>
|
||||
{{ phone.t('Apps.settings.appearanceMode') }}
|
||||
</k-block-title>
|
||||
@@ -1451,7 +1489,7 @@ onBeforeUnmount(() => {
|
||||
<template #after>
|
||||
<span
|
||||
class="h-7 w-7 rounded-full border border-black/15 shadow-sm"
|
||||
:style="{ backgroundColor: selectedFrameColor }"
|
||||
:style="{ background: selectedFrameColor }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -1463,7 +1501,11 @@ onBeforeUnmount(() => {
|
||||
:opened="framePickerOpened"
|
||||
:class="[
|
||||
'settings-frame-popover !absolute !z-[200] !left-[var(--settings-frame-picker-left)] !top-[var(--settings-frame-picker-top)]',
|
||||
{ dark: phone.isDarkMode },
|
||||
{
|
||||
dark: phone.isDarkMode,
|
||||
'phone-app--light': !phone.isDarkMode,
|
||||
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
|
||||
},
|
||||
]"
|
||||
@backdropclick="framePickerOpened = false"
|
||||
>
|
||||
@@ -1477,7 +1519,7 @@ onBeforeUnmount(() => {
|
||||
:key="frame"
|
||||
type="button"
|
||||
class="h-10 w-10 rounded-full border border-black/15 shadow-sm"
|
||||
:style="{ backgroundColor: PHONE_FRAME_COLORS[frame] }"
|
||||
:style="{ background: PHONE_FRAME_COLORS[frame] }"
|
||||
:aria-label="phone.t(`Apps.settings.frames.${frame}`)"
|
||||
:aria-pressed="phone.preferences.settings.frame === frame"
|
||||
@click="selectFrame(frame)"
|
||||
|
||||
@@ -32,7 +32,8 @@ function conditionLabel(condition: WeatherConditionId): string {
|
||||
function formatHour(timestamp: number, index: number): string {
|
||||
if (index === 0) return phone.t('Apps.weather.now')
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: 'numeric',
|
||||
hour: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
timeZone: 'UTC',
|
||||
}).format(timestamp)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,12 @@ export default defineConfig({
|
||||
base: './',
|
||||
build: {
|
||||
assetsDir: 'assets',
|
||||
// Keep the published NUI compatible with the Chromium 103 CEF runtime.
|
||||
cssMinify: 'lightningcss',
|
||||
cssTarget: 'chrome103',
|
||||
emptyOutDir: true,
|
||||
outDir: 'dist',
|
||||
target: 'chrome103',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'assets/sky-[name]-[hash].[ext]',
|
||||
|
||||
@@ -614,7 +614,10 @@ Locales["en"] = {
|
||||
focus = "Focus", focusMode = "Focus", focusDescription = "Focus silences non-critical notifications while keeping alarms and important alerts available.",
|
||||
notificationSounds = "Sounds", notificationDuration = "Notification Duration", seconds = "{seconds} seconds",
|
||||
ringtoneVolume = "Ringtone Volume", notificationVolume = "Notification Volume",
|
||||
ringtone = "Ringtone", notificationSound = "Notification Sound", appearanceMode = "Appearance Mode",
|
||||
ringtone = "Ringtone", notificationSound = "Notification Sound", graphicsMode = "Graphics Mode",
|
||||
performanceMode = "Performance Mode", performanceModeDescription = "Blur-free glass simulation for better CEF performance",
|
||||
ultimateMode = "Ultimate Mode", ultimateModeDescription = "Full blur and glass effects for compatible FiveM clients",
|
||||
appearanceMode = "Appearance Mode",
|
||||
automatic = "Automatic", light = "Light", dark = "Dark", phoneScale = "Phone Scale", phoneFrame = "Phone Frame",
|
||||
screenBrightness = "Screen Brightness", rotationLock = "Rotation Lock",
|
||||
about = "About", deviceName = "Device Name", deviceNameValue = "Sky Phone", softwareVersion = "Software Version",
|
||||
@@ -647,7 +650,11 @@ Locales["en"] = {
|
||||
rotationLocked = "Toggle Rotation Lock",
|
||||
notifications = "Toggle notifications for {app}", notificationSounds = "Toggle notification sounds for {app}",
|
||||
},
|
||||
frames = { black = "Black", blue = "Blue", green = "Green", lavender = "Lavender", white = "White" },
|
||||
frames = {
|
||||
black = "Black", blue = "Blue", green = "Green", lavender = "Lavender", red = "Red", white = "White",
|
||||
orange = "Orange", yellow = "Yellow", lime = "Lime", teal = "Teal", cyan = "Cyan", purple = "Purple",
|
||||
pink = "Pink", gold = "Gold", rgb = "RGB",
|
||||
},
|
||||
ringtones = { skyline = "Skyline", horizon = "Horizon", pulse = "Pulse" },
|
||||
notificationSoundsList = { chime = "Chime", signal = "Signal", soft = "Soft" },
|
||||
wallpapers = { midnight = "Midnight wallpaper", aurora = "Aurora wallpaper", ember = "Ember wallpaper" },
|
||||
|
||||