mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
ENH - refine phone apps and FiveM integration
This commit is contained in:
@@ -7,7 +7,7 @@ An iFruit account is optional. Unlinked devices retain local settings, alarms, m
|
||||
## Requirements
|
||||
|
||||
- ESX Legacy (`es_extended`), Qbox (`qbx_core`), or QBCore (`qb-core`). The bridge selects a running supported framework when `Config.Bridge.Framework` is set to `"auto"`.
|
||||
- A supported metadata inventory: `ox_inventory`, `qb-inventory`, `lj-inventory`, `qs-inventory`, `codem-inventory`, `core_inventory`, or `mf-inventory`. The bridge auto-detects a running provider and normalizes `metadata`/`info`, slots, counts, item mutations, and usable-item callbacks. `mf-inventory` requires ESX.
|
||||
- A supported inventory: `ox_inventory`, `qb-inventory`, `lj-inventory`, `qs-inventory`, `codem-inventory`, `core_inventory`, `mf-inventory`, or `smx-inventory`. The bridge auto-detects a running provider and normalizes metadata, slots, counts, item mutations, and usable-item callbacks. `mf-inventory` and `smx-inventory` require ESX. Because SMX stores standard ESX items as stacks, its adapter persists one active Phone/SIM metadata record per player and item type in ESX player metadata.
|
||||
- A non-stackable inventory item named `sky_phone`.
|
||||
- Two unique, non-stackable inventory items named `sky_phone_sim_registered` and `sky_phone_sim_anonymous`. Their metadata is initialized automatically on first use, so shops and crafting recipes add plain items without supplying a number.
|
||||
- `oxmysql` with MySQL/MariaDB.
|
||||
|
||||
+47
-43
@@ -150,9 +150,35 @@ function hydratePhone(payload: PhoneOpenPayload): void {
|
||||
void calls.bootstrap()
|
||||
}
|
||||
|
||||
async function hydrateDevelopmentPhone(): Promise<void> {
|
||||
const response = await nuiCall<PhoneOpenPayload>('development:bootstrap')
|
||||
if (response.success && response.data) {
|
||||
hydratePhone(response.data)
|
||||
return
|
||||
}
|
||||
|
||||
hydratePhone({
|
||||
account: null,
|
||||
device: {
|
||||
data: {},
|
||||
imei: '356938035643809',
|
||||
name: 'iFruit Phone',
|
||||
sim: {
|
||||
id: 'development-sim',
|
||||
number: '5551234567',
|
||||
registered: true,
|
||||
type: 'registered',
|
||||
},
|
||||
},
|
||||
notes: [],
|
||||
token: 'development',
|
||||
})
|
||||
}
|
||||
|
||||
function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
if (event.data?.type === 'app:open') {
|
||||
hydratePhone(event.data.data as PhoneOpenPayload)
|
||||
void nuiCall('ui:opened')
|
||||
} else if (event.data?.type === 'device:updated') {
|
||||
hydratePhone(event.data.data as PhoneOpenPayload)
|
||||
} else if (event.data?.type === 'app:close') {
|
||||
@@ -193,15 +219,23 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
} else if (event.data?.type === 'marketplace:changed' && event.data.data) {
|
||||
const data = event.data.data as MarketplaceEventData
|
||||
if (data.counts) marketplace.setCounts(data.counts)
|
||||
} else if (event.data?.type === 'marketplace:new-message' && event.data.data) {
|
||||
} else if (
|
||||
event.data?.type === 'marketplace:new-message' &&
|
||||
event.data.data
|
||||
) {
|
||||
const data = event.data.data as MarketplaceEventData
|
||||
const notification: PhoneNotificationInput = {
|
||||
appId: 'citymarkt',
|
||||
subtitle: data.sender,
|
||||
text: data.text ?? phone.t('Apps.citymarkt.newMessage', { sender: data.sender ?? '' }),
|
||||
text:
|
||||
data.text ??
|
||||
phone.t('Apps.citymarkt.newMessage', { sender: data.sender ?? '' }),
|
||||
title: data.title ?? phone.t('Apps.citymarkt.name'),
|
||||
}
|
||||
if (data.device && (!phone.isOpen || data.device.imei !== phone.device?.imei)) {
|
||||
if (
|
||||
data.device &&
|
||||
(!phone.isOpen || data.device.imei !== phone.device?.imei)
|
||||
) {
|
||||
notification.device = {
|
||||
imei: data.device.imei,
|
||||
name: data.device.name,
|
||||
@@ -226,7 +260,10 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
}),
|
||||
title: data.title ?? phone.t('Apps.calendar.name'),
|
||||
}
|
||||
if (data.device && (!phone.isOpen || data.device.imei !== phone.device?.imei)) {
|
||||
if (
|
||||
data.device &&
|
||||
(!phone.isOpen || data.device.imei !== phone.device?.imei)
|
||||
) {
|
||||
notification.device = {
|
||||
imei: data.device.imei,
|
||||
name: data.device.name,
|
||||
@@ -318,44 +355,7 @@ onMounted(() => {
|
||||
}
|
||||
}, 1000)
|
||||
if (isDevelopment) {
|
||||
hydratePhone({
|
||||
account: {
|
||||
devices: [
|
||||
{
|
||||
created_at: '2026-08-04 12:00:00',
|
||||
current: true,
|
||||
device_name: 'iFruit Phone',
|
||||
imei: '356938035643809',
|
||||
updated_at: '2026-08-06 12:00:00',
|
||||
},
|
||||
],
|
||||
email: 'demo@ifruit.com',
|
||||
id: 1,
|
||||
},
|
||||
device: {
|
||||
data: {},
|
||||
imei: '356938035643809',
|
||||
name: 'iFruit Phone',
|
||||
sim: {
|
||||
id: 'development-sim',
|
||||
number: '5551234567',
|
||||
registered: true,
|
||||
type: 'registered',
|
||||
},
|
||||
},
|
||||
notes: [
|
||||
{
|
||||
body: 'Meet Morgan in Vinewood and inspect the Sultan RS.',
|
||||
createdAt: Date.now() - 3_600_000,
|
||||
id: 'demo-note-marketplace',
|
||||
pinned: true,
|
||||
revision: 1,
|
||||
title: 'CityMarkt viewing',
|
||||
updatedAt: Date.now() - 3_600_000,
|
||||
},
|
||||
],
|
||||
token: 'development',
|
||||
})
|
||||
void hydrateDevelopmentPhone()
|
||||
if (new URLSearchParams(window.location.search).has('simPickerPreview')) {
|
||||
simPicker.value = {
|
||||
choices: [
|
||||
@@ -475,7 +475,11 @@ onBeforeUnmount(() => {
|
||||
<SpringboardView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="appTransitionName">
|
||||
<component :is="Component" v-if="isAppRoute" :key="route.path" />
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="isAppRoute"
|
||||
:key="route.path"
|
||||
/>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<PhoneHomeIndicator v-if="!isLocked" />
|
||||
|
||||
@@ -16,11 +16,15 @@ import {
|
||||
import { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAccountStore } from '@/stores/account'
|
||||
import { useCalendarStore } from '@/stores/calendar'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { useWeatherStore } from '@/stores/weather'
|
||||
import type { WeatherConditionId } from '@/types/weather'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const account = useAccountStore()
|
||||
const calendar = useCalendarStore()
|
||||
const weather = useWeatherStore()
|
||||
const router = useRouter()
|
||||
const now = ref(new Date())
|
||||
@@ -57,15 +61,46 @@ const date = computed(() =>
|
||||
}).format(now.value),
|
||||
)
|
||||
const day = computed(() => now.value.getDate())
|
||||
const nextCalendarEvent = computed(() =>
|
||||
calendar.events
|
||||
.filter((event) => event.endsAt >= now.value.getTime())
|
||||
.sort((left, right) => left.startsAt - right.startsAt)
|
||||
.at(0),
|
||||
)
|
||||
const calendarEventLabel = computed(
|
||||
() =>
|
||||
nextCalendarEvent.value?.title ?? phone.t('Home.widgets.calendar.event'),
|
||||
)
|
||||
|
||||
async function loadCalendarDay(): Promise<void> {
|
||||
if (!account.email) {
|
||||
calendar.events = []
|
||||
return
|
||||
}
|
||||
|
||||
const start = new Date(now.value)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
const end = new Date(start)
|
||||
end.setDate(end.getDate() + 1)
|
||||
await calendar.load(start.getTime(), end.getTime())
|
||||
}
|
||||
|
||||
function openWeather(): void {
|
||||
phone.setLaunchOrigin(null)
|
||||
void router.push('/apps/weather')
|
||||
}
|
||||
|
||||
function openCalendar(): void {
|
||||
phone.setLaunchOrigin(null)
|
||||
void router.push('/apps/calendar')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadCalendarDay()
|
||||
intervalId = window.setInterval(() => {
|
||||
const previousDay = now.value.toDateString()
|
||||
now.value = new Date()
|
||||
if (now.value.toDateString() !== previousDay) void loadCalendarDay()
|
||||
}, 60_000)
|
||||
})
|
||||
|
||||
@@ -91,11 +126,16 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
|
||||
<div class="widget-row">
|
||||
<article class="widget widget--calendar">
|
||||
<button
|
||||
type="button"
|
||||
class="widget widget--calendar"
|
||||
:aria-label="phone.t('Apps.calendar.name')"
|
||||
@click="openCalendar"
|
||||
>
|
||||
<span>{{ date }}</span>
|
||||
<strong>{{ day }}</strong>
|
||||
<small>{{ phone.t('Home.widgets.calendar.event') }}</small>
|
||||
</article>
|
||||
<small>{{ calendarEventLabel }}</small>
|
||||
</button>
|
||||
<article class="widget widget--battery">
|
||||
<BatteryCharging :size="25" aria-hidden="true" />
|
||||
<strong>78%</strong>
|
||||
|
||||
@@ -76,4 +76,7 @@ function move(direction: number): void {
|
||||
|
||||
<style scoped>
|
||||
.citymarkt-gallery{position:relative;overflow:hidden;background:#252724}.citymarkt-gallery__image{position:absolute;inset:0;background-position:center!important;background-size:cover!important;transition:background .2s ease}.citymarkt-gallery__empty{position:absolute;inset:0;padding:18px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:var(--muted)}.citymarkt-gallery__empty span{width:48px;height:48px;margin-bottom:8px;border:1px solid #ffffff12;border-radius:16px;display:grid;place-items:center;background:#ffffff08;color:var(--yellow)}.citymarkt-gallery__empty strong{font-size:12px}.citymarkt-gallery__empty small{max-width:190px;margin-top:3px;font-size:8px;line-height:1.35}.citymarkt-gallery__arrow{position:absolute;z-index:2;top:50%;width:31px;height:31px;padding:0;border:1px solid #ffffff24;border-radius:50%;display:grid;place-items:center;background:#11120fba;color:#fff;box-shadow:0 4px 13px #0005;transform:translateY(-50%)}.citymarkt-gallery__arrow--left{left:9px}.citymarkt-gallery__arrow--right{right:9px}.citymarkt-gallery__dots{position:absolute;z-index:2;right:52px;bottom:12px;left:52px;display:flex;justify-content:center;gap:4px}.citymarkt-gallery__dots i{width:4px;height:4px;border-radius:50%;background:#ffffff66;box-shadow:0 1px 3px #0008;transition:width .18s ease,background .18s ease}.citymarkt-gallery__dots i.active{width:11px;border-radius:4px;background:var(--yellow)}.citymarkt-gallery__count{position:absolute;z-index:2;right:9px;bottom:8px;padding:4px 7px;border-radius:8px;background:#11120fc7;color:#fff;font-size:8px;font-weight:800}:global(.citymarkt--light) .citymarkt-gallery--empty{background:#e9eae5}:global(.citymarkt--light) .citymarkt-gallery__empty span{border-color:#00000012;background:#00000008}
|
||||
.citymarkt-gallery__empty strong{font-size:14px}
|
||||
.citymarkt-gallery__empty small{margin-top:4px;font-size:11.5px;line-height:1.4}
|
||||
.citymarkt-gallery__count{font-size:10.5px}
|
||||
</style>
|
||||
|
||||
@@ -101,4 +101,6 @@ onUnmounted(() => window.removeEventListener('pointerdown', handleOutsidePointer
|
||||
|
||||
<style scoped>
|
||||
.citymarkt-select{position:relative;min-width:0}.citymarkt-select__trigger{width:100%;height:36px;padding:0 10px;border:1px solid #ffffff0d;border-radius:10px;display:flex;align-items:center;justify-content:space-between;gap:6px;background:var(--panel);color:inherit;font-size:10px;text-align:left}.citymarkt-select__trigger span{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.citymarkt-select__trigger svg{flex:none;color:var(--yellow);transition:transform .18s ease}.citymarkt-select__trigger svg.open{transform:rotate(180deg)}.citymarkt-select__menu{position:absolute;z-index:12;top:calc(100% + 5px);right:0;left:0;max-height:176px;padding:4px;border:1px solid #ffffff16;border-radius:11px;overflow-y:auto;background:#292a27;box-shadow:0 12px 28px #0009;scrollbar-width:none}:global(.citymarkt--light) .citymarkt-select__menu{border-color:#00000014;background:#fff;box-shadow:0 12px 28px #0003}.citymarkt-select__menu button{width:100%;min-height:31px;padding:6px 7px;border:0;border-radius:8px;display:flex;align-items:center;justify-content:space-between;gap:5px;background:none;color:var(--muted);font-size:10px;text-align:left}.citymarkt-select__menu button.highlighted{background:#ffffff0b;color:inherit}:global(.citymarkt--light) .citymarkt-select__menu button.highlighted{background:#0000000b}.citymarkt-select__menu button.selected{color:var(--yellow);font-weight:800}.citymarkt-select-enter-active,.citymarkt-select-leave-active{transition:opacity .15s ease,transform .15s ease}.citymarkt-select-enter-from,.citymarkt-select-leave-to{opacity:0;transform:translateY(-4px) scale(.98)}
|
||||
.citymarkt-select__trigger{height:40px;font-size:13px}
|
||||
.citymarkt-select__menu button{min-height:36px;padding:7px 8px;font-size:12px}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { useGamesStore } from '@/features/games/store'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
|
||||
import {
|
||||
createMinesweeperGame,
|
||||
@@ -58,7 +59,7 @@ export const useMinesweeperStore = defineStore('minesweeper', {
|
||||
? saved.elapsedMs
|
||||
: 0
|
||||
this.game = isMinesweeperGameState(saved?.game)
|
||||
? structuredClone(saved.game)
|
||||
? cloneJsonData(saved.game)
|
||||
: null
|
||||
if (typeof saved?.soundEnabled === 'boolean') {
|
||||
this.soundEnabled = saved.soundEnabled
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { useGamesStore } from '@/features/games/store'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
|
||||
import {
|
||||
continueNumberMerge,
|
||||
@@ -45,7 +46,7 @@ export const useNumberMergeStore = defineStore('number-merge', {
|
||||
)
|
||||
this.game = isNumberMergeGameState(saved?.game)
|
||||
? {
|
||||
...structuredClone(saved.game),
|
||||
...cloneJsonData(saved.game),
|
||||
tiles: saved.game.tiles.map((tile) => ({
|
||||
...tile,
|
||||
isNew: false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
|
||||
export const useGamesStore = defineStore('games', {
|
||||
state: () => ({
|
||||
@@ -10,14 +11,14 @@ export const useGamesStore = defineStore('games', {
|
||||
hydrate(payload: unknown): void {
|
||||
this.games =
|
||||
payload && typeof payload === 'object'
|
||||
? structuredClone(payload as Record<string, unknown>)
|
||||
? cloneJsonData(payload as Record<string, unknown>)
|
||||
: {}
|
||||
},
|
||||
readGame<T>(gameId: string): T | undefined {
|
||||
return this.games[gameId] as T | undefined
|
||||
},
|
||||
saveGame(gameId: string, payload: unknown): void {
|
||||
this.games[gameId] = structuredClone(payload)
|
||||
this.games[gameId] = cloneJsonData(payload)
|
||||
usePhoneStore().saveDeviceNamespace('games', this.games)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,11 +10,12 @@ import {
|
||||
parseAlarms,
|
||||
} from '@/utils/alarms'
|
||||
import { elapsedMilliseconds, remainingMilliseconds } from '@/utils/clock'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
export const useClockStore = defineStore('clock', {
|
||||
state: () => ({
|
||||
alarms: structuredClone(DEFAULT_ALARMS),
|
||||
alarms: cloneJsonData(DEFAULT_ALARMS),
|
||||
laps: [] as number[],
|
||||
stopwatchAccumulated: 0,
|
||||
stopwatchStartedAt: null as number | null,
|
||||
@@ -27,7 +28,7 @@ export const useClockStore = defineStore('clock', {
|
||||
actions: {
|
||||
createAlarm(draft: AlarmDraft): Alarm {
|
||||
const alarm: Alarm = {
|
||||
...structuredClone(draft),
|
||||
...cloneJsonData(draft),
|
||||
enabled: true,
|
||||
id: `alarm-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||
lastTriggeredMinute: null,
|
||||
@@ -144,7 +145,7 @@ export const useClockStore = defineStore('clock', {
|
||||
updateAlarm(id: string, draft: AlarmDraft): void {
|
||||
const alarm = this.alarms.find((candidate) => candidate.id === id)
|
||||
if (!alarm) return
|
||||
Object.assign(alarm, structuredClone(draft), {
|
||||
Object.assign(alarm, cloneJsonData(draft), {
|
||||
lastTriggeredMinute: null,
|
||||
})
|
||||
this.persistAlarms()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
import { type Note, type NoteDraft } from '@/utils/notes'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
|
||||
export const useNotesStore = defineStore('notes', {
|
||||
state: () => ({
|
||||
@@ -29,7 +30,7 @@ export const useNotesStore = defineStore('notes', {
|
||||
})
|
||||
},
|
||||
hydrate(notes: Note[]): void {
|
||||
this.notes = structuredClone(notes)
|
||||
this.notes = cloneJsonData(notes)
|
||||
},
|
||||
async createRemote(note: Note): Promise<void> {
|
||||
const response = await nuiCall<Note[]>('notes:create', note)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { defineStore } from 'pinia'
|
||||
import type { AppLaunchOrigin, LaunchablePhoneAppId } from '@/types/apps'
|
||||
import type { DeviceBootstrap, PhoneDevice } from '@/types/device'
|
||||
import { clampPage } from '@/utils/pages'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
import {
|
||||
DEFAULT_PHONE_PREFERENCES,
|
||||
@@ -417,7 +418,7 @@ const defaultLocales: LocaleTree = {
|
||||
},
|
||||
localPages: {
|
||||
name: 'Local Pages', eyebrow: 'Your city. Your stories.', cityPulse: 'Live from Los Santos',
|
||||
heroTitle: 'What is happening nearby?', heroBody: 'Places, people, tips and local discoveries from the community.',
|
||||
heroTitle: 'What is happening nearby?', heroBody: 'Discover places, people and local tips.',
|
||||
searchPlaceholder: 'Search posts and places', search: 'Search', allCategories: 'All categories',
|
||||
allLosSantos: 'Los Santos', hoursAgo: '{count}h ago', daysAgo: '{count}d ago',
|
||||
categories: { recommendation: 'Recommended', wanted: 'Wanted', service: 'Service', event: 'Event', place: 'Place', community: 'Community', citymarkt: 'CityMarkt' },
|
||||
@@ -524,6 +525,13 @@ const defaultLocales: LocaleTree = {
|
||||
},
|
||||
calendar: {
|
||||
name: 'Calendar',
|
||||
today: 'Today',
|
||||
calendars: 'Calendars',
|
||||
calendar: 'Calendar',
|
||||
calendarName: 'iFruit',
|
||||
searchPlaceholder: 'Search events',
|
||||
previousMonth: 'Previous month',
|
||||
nextMonth: 'Next month',
|
||||
eyebrow: 'Your time. Clearly planned.',
|
||||
schedule: 'Schedule',
|
||||
event: 'Event',
|
||||
@@ -545,6 +553,12 @@ const defaultLocales: LocaleTree = {
|
||||
signInTitle: 'Your iFruit calendar',
|
||||
signInBody: 'Sign in to iFruit in Settings to sync appointments and receive reminders.',
|
||||
reminderNotification: 'Upcoming: {title}',
|
||||
views: {
|
||||
compact: 'Compact',
|
||||
stacked: 'Stacked',
|
||||
details: 'Details',
|
||||
list: 'List',
|
||||
},
|
||||
reminders: {
|
||||
none: 'No reminder',
|
||||
atStart: 'At start time',
|
||||
@@ -869,6 +883,7 @@ const defaultLocales: LocaleTree = {
|
||||
Common: {
|
||||
add: 'Add',
|
||||
cancel: 'Cancel',
|
||||
clear: 'Clear',
|
||||
close: 'Close',
|
||||
back: 'Back',
|
||||
delete: 'Delete',
|
||||
@@ -940,7 +955,7 @@ export const usePhoneStore = defineStore('phone', {
|
||||
lang: 'en',
|
||||
launchOrigin: null as AppLaunchOrigin | null,
|
||||
locales: defaultLocales,
|
||||
preferences: structuredClone(DEFAULT_PHONE_PREFERENCES),
|
||||
preferences: cloneJsonData(DEFAULT_PHONE_PREFERENCES),
|
||||
systemDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
|
||||
}),
|
||||
getters: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export type PagesPost = {
|
||||
category: PagesCategory
|
||||
citymarkt_listing_id: string | null
|
||||
citymarkt_price: number | string | null
|
||||
created_at: string
|
||||
created_at: number
|
||||
district: string | null
|
||||
id: string
|
||||
image: string | null
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
|
||||
export const ALARM_SOUND_IDS = [
|
||||
'radar',
|
||||
'beacon',
|
||||
@@ -89,7 +91,7 @@ function readAlarm(value: unknown): Alarm | null {
|
||||
}
|
||||
|
||||
export function parseAlarms(value: unknown): Alarm[] {
|
||||
if (!Array.isArray(value)) return structuredClone(DEFAULT_ALARMS)
|
||||
if (!Array.isArray(value)) return cloneJsonData(DEFAULT_ALARMS)
|
||||
return value.map(readAlarm).filter((alarm): alarm is Alarm => !!alarm)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function cloneJsonData<T>(value: T): T {
|
||||
const serialized = JSON.stringify(value)
|
||||
if (serialized === undefined) return value
|
||||
return JSON.parse(serialized) as T
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LaunchablePhoneAppId } from '@/types/apps'
|
||||
import { cloneJsonData } from '@/utils/clone'
|
||||
|
||||
export const APPEARANCE_MODE_IDS = ['automatic', 'light', 'dark'] as const
|
||||
export const PHONE_FRAME_IDS = [
|
||||
@@ -122,7 +123,7 @@ function readNotifications(
|
||||
Record<LaunchablePhoneAppId, Partial<AppNotificationPreferences>>
|
||||
>)
|
||||
: {}
|
||||
const notifications = structuredClone(DEFAULT_APP_NOTIFICATIONS)
|
||||
const notifications = cloneJsonData(DEFAULT_APP_NOTIFICATIONS)
|
||||
|
||||
for (const appId of Object.keys(notifications) as LaunchablePhoneAppId[]) {
|
||||
notifications[appId] = {
|
||||
@@ -141,13 +142,13 @@ function readNotifications(
|
||||
}
|
||||
|
||||
export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
|
||||
if (!raw) return structuredClone(DEFAULT_PHONE_PREFERENCES)
|
||||
if (!raw) return cloneJsonData(DEFAULT_PHONE_PREFERENCES)
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<PhonePreferencesV1>
|
||||
const settings = parsed.settings
|
||||
if (parsed.version !== 1 || !settings || typeof settings !== 'object') {
|
||||
return structuredClone(DEFAULT_PHONE_PREFERENCES)
|
||||
return cloneJsonData(DEFAULT_PHONE_PREFERENCES)
|
||||
}
|
||||
|
||||
const defaults = DEFAULT_PHONE_PREFERENCES.settings
|
||||
@@ -205,6 +206,6 @@ export function parsePhonePreferences(raw: string | null): PhonePreferencesV1 {
|
||||
version: 1,
|
||||
}
|
||||
} catch {
|
||||
return structuredClone(DEFAULT_PHONE_PREFERENCES)
|
||||
return cloneJsonData(DEFAULT_PHONE_PREFERENCES)
|
||||
}
|
||||
}
|
||||
|
||||
+1644
-145
File diff suppressed because one or more lines are too long
@@ -910,4 +910,39 @@ onMounted(async () => {
|
||||
.citymarkt__sell>header strong{font-size:13px}.citymarkt__sell>header small{font-size:10px}.citymarkt__sell>header>button:last-child{font-size:11px}.citymarkt__sell-body h2{font-size:23px;line-height:1.15}.citymarkt__sell-body>p{font-size:11px;line-height:1.45}.citymarkt__sell-body label{font-size:10.5px}.citymarkt__sell-body input:not([type=checkbox]),.citymarkt__sell-body textarea{padding:11px 12px;font-size:12px}.citymarkt__sell-body input:not([type=checkbox]){min-height:41px}.citymarkt__sell-body textarea{line-height:1.4}.citymarkt__switch{font-size:10.5px!important}.citymarkt__previous{font-size:11px}.citymarkt__photo-actions strong{font-size:11px}.citymarkt__photo-actions small{font-size:8.5px;line-height:1.4}.citymarkt__selected-heading strong{font-size:12px}.citymarkt__selected-heading span{font-size:9px}.citymarkt__sell-body h3{font-size:18px}.citymarkt__sell-body>small{font-size:10px}.citymarkt__sell :deep(.citymarkt-select__trigger){height:41px;padding:0 12px;font-size:12px}.citymarkt__sell :deep(.citymarkt-select__menu button){min-height:35px;padding:8px;font-size:11px}.citymarkt__sell :deep(.citymarkt-gallery__empty strong){font-size:13px}.citymarkt__sell :deep(.citymarkt-gallery__empty small){font-size:9px;line-height:1.4}
|
||||
.citymarkt__field-heading{display:flex;align-items:center;justify-content:space-between;gap:8px}.citymarkt__field-heading>small{color:#ff9c72;font-size:8.5px;font-weight:850;white-space:nowrap;transition:color .18s ease}.citymarkt__field-heading>small.valid{color:#62dc8e}
|
||||
.citymarkt__pages-share{width:100%;margin:4px 0 8px;padding:10px 12px;border:1px solid #ffc92855;border-radius:12px;display:flex;align-items:center;gap:8px;text-align:left;background:#ffc92816;color:var(--yellow)!important}.citymarkt__pages-share span{flex:1}.citymarkt__pages-share strong,.citymarkt__pages-share small{display:block}.citymarkt__pages-share strong{font-size:10px}.citymarkt__pages-share small{color:var(--muted);font-size:8px}
|
||||
|
||||
/* Keep marketplace copy readable at the physical phone scale. */
|
||||
.citymarkt__brand{font-size:12px}
|
||||
.citymarkt__categories button{font-size:10.5px}
|
||||
.citymarkt__filters select,.citymarkt__section-title small{font-size:12px}
|
||||
.citymarkt__card-image>i,.citymarkt__hero>span,.citymarkt__price-row span,.citymarkt__chat-listing i,.citymarkt__reserve{font-size:10.5px}
|
||||
.citymarkt__grid button>span:not(.citymarkt__card-image){font-size:13px}
|
||||
.citymarkt__grid button>small,.citymarkt__preview-price+*+*+small{font-size:11.5px}
|
||||
.citymarkt__empty span,.citymarkt__auth p,.citymarkt__description{font-size:13px}
|
||||
.citymarkt__inquiries strong,.citymarkt__list strong{font-size:14px}
|
||||
.citymarkt__inquiries b,.citymarkt__list b,.citymarkt__profile small,.citymarkt__price-row>small,.citymarkt__seller small,.citymarkt__phone{font-size:11.5px}
|
||||
.citymarkt__inquiries small,.citymarkt__list small{font-size:11px}
|
||||
.citymarkt__inquiries i,.citymarkt__tabbar button i,.citymarkt__photo-picker i,.citymarkt__selected-strip button i{font-size:10px}
|
||||
.citymarkt__tabbar button{font-size:10.5px}
|
||||
.citymarkt__meta,.citymarkt__inline-auth,.citymarkt__toast{font-size:12px}
|
||||
.citymarkt__composer textarea,.citymarkt__chat-composer input{font-size:13px}
|
||||
.citymarkt__composer button,.citymarkt__owner-actions button,.citymarkt__report>div button{font-size:13px}
|
||||
.citymarkt__sell>header small,.citymarkt__chat>header small,.citymarkt__sell>header>button:last-child{font-size:12px}
|
||||
.citymarkt__sell-body>p,.citymarkt__sell-body label,.citymarkt__switch,.citymarkt__previous,.citymarkt__sell-body>small{font-size:12px!important}
|
||||
.citymarkt__sell-body input:not([type=checkbox]),.citymarkt__sell-body textarea,.citymarkt__sell-body select,.citymarkt__report select,.citymarkt__report textarea{font-size:13px}
|
||||
.citymarkt__chat-listing strong,.citymarkt__chat-listing span,.citymarkt__messages p{font-size:13px}
|
||||
.citymarkt__messages small{font-size:10.5px}
|
||||
.citymarkt__segmented button span{font-size:10.5px}
|
||||
.citymarkt__chat-actions button,.citymarkt__offer-panel>button{font-size:12px}
|
||||
.citymarkt__offer-panel header small,.citymarkt__offer-panel label{font-size:11.5px}
|
||||
.citymarkt__offer-panel header strong{font-size:14px}
|
||||
.citymarkt__image-placeholder small,.citymarkt__photo-actions small,.citymarkt__capture p{font-size:11.5px}
|
||||
.citymarkt__photo-actions strong,.citymarkt__selected-heading strong{font-size:13px}
|
||||
.citymarkt__selected-heading span,.citymarkt__photo-source>header small,.citymarkt__photo-source>header>span{font-size:11px}
|
||||
.citymarkt__field-heading>small{font-size:10.5px}
|
||||
.citymarkt__pages-share strong{font-size:12.5px}
|
||||
.citymarkt__pages-share small{font-size:11px}
|
||||
.citymarkt__sell :deep(.citymarkt-select__trigger){font-size:13px}
|
||||
.citymarkt__sell :deep(.citymarkt-select__menu button){font-size:12px}
|
||||
.citymarkt__sell :deep(.citymarkt-gallery__empty small){font-size:11.5px}
|
||||
</style>
|
||||
|
||||
@@ -93,8 +93,8 @@ function label(group: string, value: string): string {
|
||||
return phone.t(`Apps.localPages.${group}.${value}`)
|
||||
}
|
||||
|
||||
function relativeDate(value: string): string {
|
||||
const elapsed = Math.max(0, Date.now() - new Date(value.replace(' ', 'T')).getTime())
|
||||
function relativeDate(value: number): string {
|
||||
const elapsed = Math.max(0, Date.now() - value)
|
||||
const hours = Math.max(1, Math.floor(elapsed / 3_600_000))
|
||||
return hours < 24
|
||||
? phone.t('Apps.localPages.hoursAgo', { count: String(hours) })
|
||||
@@ -369,4 +369,30 @@ onMounted(() => void loadFeed())
|
||||
.pages__post-open{width:100%;padding:0;text-align:left;background:none}.pages__post-foot{gap:6px}.pages__post-foot button{min-height:28px;padding:5px 8px;border-radius:9px;display:flex;align-items:center;gap:4px;background:#ffffff08;color:var(--muted);font-size:9px;font-weight:800}.pages__post-foot button:last-child{margin-left:auto}.pages__post-foot button:first-child.active{background:#ff647318;color:#ff6473}.pages__post-foot button:last-child.active{background:#ffd63e1c;color:var(--yellow)}.pages__post-foot button:disabled{opacity:.55}.pages__post-foot button svg:last-child{margin-left:0}.pages--light .pages__post-foot button{background:#00000008}
|
||||
.pages__photos{margin-top:5px}.pages__photos>svg{color:var(--yellow)}.pages__photos>h2{margin:7px 0 3px;font-size:19px}.pages__photos>p{margin:0 0 11px;color:var(--muted);font-size:9px;line-height:1.4}.pages__photo-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.pages__photo-actions>button{min-width:0;padding:11px 9px;border:1px solid #ffffff12;border-radius:14px;display:flex;flex-direction:column;align-items:flex-start;text-align:left;background:var(--panel)}.pages__photo-actions>button>span{width:34px;height:34px;margin-bottom:8px;border-radius:11px;display:grid;place-items:center;background:#ffd63e1c;color:var(--yellow)}.pages__photo-actions strong{font-size:11px}.pages__photo-actions small{margin-top:2px;color:var(--muted);font-size:8px;line-height:1.35}.pages__selected-heading{margin:15px 1px 7px;display:flex;align-items:center;justify-content:space-between}.pages__selected-heading strong{font-size:12px}.pages__selected-heading span{padding:3px 6px;border-radius:7px;background:var(--panel);color:var(--yellow);font-size:9px;font-weight:900}.pages__selection-gallery{height:142px;border-radius:14px}.pages__selected-strip{margin-top:7px;display:flex;gap:6px;overflow-x:auto;scrollbar-width:none}.pages__selected-strip button{position:relative;width:46px;height:46px;flex:none;border:1px solid #ffffff1d;border-radius:9px;background-position:center!important;background-size:cover!important}.pages__selected-strip button i{position:absolute;left:3px;bottom:3px;width:15px;height:15px;border-radius:50%;display:grid;place-items:center;background:var(--yellow);color:#17191a;font-size:7px;font-style:normal;font-weight:900}.pages__selected-strip button svg{position:absolute;top:3px;right:3px;padding:2px;box-sizing:content-box;border-radius:50%;background:#11120fc7;color:#fff}
|
||||
.pages__photo-source{position:absolute;z-index:8;inset:47px 0 0;padding:14px 14px 33px;background:#12171b}.pages--light .pages__photo-source{background:#fbfbf6}.pages__photo-source>header{height:52px;display:flex;align-items:center;gap:8px}.pages__photo-source>header>div{min-width:0;flex:1}.pages__photo-source>header small,.pages__photo-source>header strong{display:block}.pages__photo-source>header small{color:var(--yellow);font-size:8px;font-weight:900;text-transform:uppercase}.pages__photo-source>header strong{font-size:18px}.pages__photo-source>header>span{padding:4px 7px;border-radius:8px;background:var(--panel);color:var(--yellow);font-size:8px;font-weight:900}.pages__photo-source>header>button{width:31px;height:31px;padding:0;border-radius:50%;display:grid;place-items:center;background:var(--panel)}.pages__photo-picker{max-height:calc(100% - 58px);display:grid;grid-template-columns:repeat(3,1fr);gap:7px;overflow-y:auto;scrollbar-width:none}.pages__photo-picker button{position:relative;aspect-ratio:1;border:2px solid transparent;border-radius:11px;background-position:center!important;background-size:cover!important}.pages__photo-picker button.active{border-color:var(--yellow)}.pages__photo-picker i{width:19px;height:19px;margin:5px;border-radius:50%;display:grid;place-items:center;background:var(--yellow);color:#17191a;font-size:9px;font-style:normal;font-weight:900}.pages__capture{height:calc(100% - 52px);display:flex;flex-direction:column;align-items:center}.pages__viewfinder{position:relative;width:100%;min-height:305px;overflow:hidden;border-radius:18px;background-position:center!important;background-size:cover!important;box-shadow:inset 0 0 0 1px #ffffff1c}.pages__viewfinder:after{content:'';position:absolute;inset:0;background:linear-gradient(180deg,#0001,#00000038)}.pages__viewfinder>i{position:absolute;z-index:2;width:25px;height:25px;border-color:#fff;border-style:solid}.pages__viewfinder .corner-tl{top:18px;left:18px;border-width:2px 0 0 2px}.pages__viewfinder .corner-tr{top:18px;right:18px;border-width:2px 2px 0 0}.pages__viewfinder .corner-bl{bottom:18px;left:18px;border-width:0 0 2px 2px}.pages__viewfinder .corner-br{right:18px;bottom:18px;border-width:0 2px 2px 0}.pages__camera-flash{position:absolute;z-index:4;inset:0;background:#fff;opacity:0;pointer-events:none;transition:opacity .12s}.pages__camera-flash.active{opacity:.9}.pages__capture p{max-width:230px;margin:9px 0;color:var(--muted);font-size:8px;text-align:center}.pages__shutter{width:58px;height:58px;padding:0;border:5px solid #f5f5ee;border-radius:50%;display:grid;place-items:center;background:var(--yellow);color:#17191a;box-shadow:0 0 0 2px #ffffff42}.pages--light .pages__photo-actions>button,.pages--light .pages__selected-strip button{border-color:#00000012}
|
||||
/* Keep community content readable at the physical phone scale. */
|
||||
.pages__header span,.pages__compose header small,.pages__header .pages__brand{font-size:12px}
|
||||
.pages__hero{padding:13px 15px;gap:12px}.pages__hero div{min-width:0;max-width:205px}.pages__hero small{font-size:9.5px;line-height:1.1}.pages__hero strong{margin:4px 0 3px;font-size:16px;line-height:1.12}.pages__hero span{font-size:11px;line-height:1.3}.pages__hero>svg{flex:none}
|
||||
.pages__search input{font-size:13px}.pages__search button{font-size:12px}
|
||||
.pages__post-head strong{font-size:13px}.pages__post-head small{font-size:11.5px}
|
||||
.pages__post-head i,.pages__author i{font-size:10.5px}
|
||||
.pages__cover>b,.pages__gallery>span{font-size:10.5px}
|
||||
.pages__cover--empty,.pages__empty span{font-size:12px}
|
||||
.pages__post h2{font-size:16px}.pages__post p{font-size:13px}
|
||||
.pages__post-foot,.pages__post-foot button{font-size:11.5px}
|
||||
.pages__profile small,.pages__profile b{font-size:11.5px}
|
||||
.pages__tabbar button{font-size:10.5px}
|
||||
.pages__author strong{font-size:14px}
|
||||
.pages__author small,.pages__location small,.pages__market-link small{font-size:11.5px}
|
||||
.pages__detail article>p{font-size:13px}
|
||||
.pages__location strong,.pages__market-link strong{font-size:13px}.pages__market-link>b{font-size:14px}
|
||||
.pages__detail-actions button{font-size:12.5px}
|
||||
.pages__compose>header strong{font-size:14px}.pages__compose>header>button:last-child{font-size:12px}
|
||||
.pages__compose-scroll label{font-size:12px}.pages__compose-scroll label>span{font-size:10.5px}
|
||||
.pages__compose input,.pages__compose textarea{font-size:13px}
|
||||
.pages__photo-title strong,.pages__gallery-label,.pages__selected-heading strong{font-size:14px}
|
||||
.pages__photo-title small,.pages__photos>p,.pages__photo-actions small,.pages__capture p{font-size:11.5px}
|
||||
.pages__photo-title button{font-size:12px}.pages__photo-actions strong{font-size:13px}
|
||||
.pages__selected-heading span,.pages__photo-source>header small,.pages__photo-source>header>span{font-size:11px}
|
||||
.pages__selected-strip button i,.pages__photo-picker i{font-size:10px}
|
||||
.pages__toast{font-size:12px}
|
||||
</style>
|
||||
|
||||
@@ -288,7 +288,7 @@ onBeforeUnmount(() => {
|
||||
.memory-header span {
|
||||
display: block;
|
||||
color: #8175a2;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1.25px;
|
||||
text-transform: uppercase;
|
||||
@@ -366,7 +366,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.memory-intro { text-align: center; }
|
||||
.memory-intro h2 { margin: 0; font-size: 23px; letter-spacing: -0.6px; }
|
||||
.memory-intro p { margin: 6px 25px 0; color: #7d7397; font-size: 12px; line-height: 1.4; }
|
||||
.memory-intro p { margin: 7px 18px 0; color: #74698f; font-size: 14px; line-height: 1.4; }
|
||||
|
||||
.memory-difficulties {
|
||||
display: grid;
|
||||
@@ -374,7 +374,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.memory-difficulties button {
|
||||
min-height: 58px;
|
||||
min-height: 62px;
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr auto;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
@@ -397,12 +397,12 @@ onBeforeUnmount(() => {
|
||||
border-radius: 11px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #9a7be1, #7558c4);
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.memory-difficulty__name { align-self: end; font-size: 13px; font-weight: 800; }
|
||||
.memory-difficulty__best { align-self: start; color: #8b819f; font-size: 9px; }
|
||||
.memory-difficulty__name { align-self: end; font-size: 15px; font-weight: 800; }
|
||||
.memory-difficulty__best { align-self: start; color: #74698f; font-size: 11.5px; line-height: 1.2; }
|
||||
.memory-difficulties svg { grid-column: 3; grid-row: 1 / 3; color: #7658c7; }
|
||||
|
||||
.memory-game { padding-top: 5px; }
|
||||
@@ -416,9 +416,9 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.memory-stats div { display: grid; }
|
||||
.memory-stats span { color: #81769c; font-size: 8px; font-weight: 800; text-transform: uppercase; }
|
||||
.memory-stats strong { font-size: 18px; line-height: 1.05; }
|
||||
.memory-stats button { justify-self: end; border: 0; color: #7052bf; background: transparent; font-size: 11px; font-weight: 800; }
|
||||
.memory-stats span { color: #74698f; font-size: 10.5px; font-weight: 800; text-transform: uppercase; }
|
||||
.memory-stats strong { font-size: 19px; line-height: 1.05; }
|
||||
.memory-stats button { justify-self: end; border: 0; color: #7052bf; background: transparent; font-size: 14px; font-weight: 800; }
|
||||
.memory-stats .memory-menu-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
@@ -562,16 +562,16 @@ onBeforeUnmount(() => {
|
||||
box-shadow: 0 9px 22px rgb(100 75 151 / 17%);
|
||||
}
|
||||
|
||||
.memory-complete > span { color: #8175a2; font-size: 9px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.memory-complete > span { color: #74698f; font-size: 11px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.memory-complete h2 { margin: 0; font-size: 27px; }
|
||||
.memory-complete p { margin: -4px 0 4px; color: #756b8e; font-size: 13px; }
|
||||
.memory-complete p { margin: -4px 0 4px; color: #756b8e; font-size: 14px; }
|
||||
|
||||
.memory-primary,
|
||||
.memory-secondary {
|
||||
min-width: 155px;
|
||||
min-height: 42px;
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
|
||||
@@ -431,14 +431,14 @@ onBeforeUnmount(() => {
|
||||
.neon-menu__copy > span {
|
||||
display: block;
|
||||
color: #7cf6e7;
|
||||
font-size: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 1.2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.neon-header h1 {
|
||||
margin: 1px 0 0;
|
||||
font-size: 23px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
.neon-header button,
|
||||
@@ -531,14 +531,15 @@ onBeforeUnmount(() => {
|
||||
color: #7a6cff;
|
||||
}
|
||||
.neon-menu__copy h2 {
|
||||
margin: 2px 0 4px;
|
||||
font-size: 20px;
|
||||
margin: 4px 0 7px;
|
||||
font-size: 30px;
|
||||
}
|
||||
.neon-menu__copy p {
|
||||
max-width: 272px;
|
||||
max-width: 296px;
|
||||
margin: 0;
|
||||
color: #b6c0db;
|
||||
font-size: 10px;
|
||||
color: #e1e7f5;
|
||||
font-size: 18px;
|
||||
font-weight: 550;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.neon-records {
|
||||
@@ -548,8 +549,8 @@ onBeforeUnmount(() => {
|
||||
gap: 7px;
|
||||
}
|
||||
.neon-records div {
|
||||
padding: 9px;
|
||||
border: 1px solid #ffffff12;
|
||||
padding: 12px 9px;
|
||||
border: 1px solid #ffffff24;
|
||||
border-radius: 13px;
|
||||
background: #ffffff08;
|
||||
}
|
||||
@@ -557,20 +558,20 @@ onBeforeUnmount(() => {
|
||||
.neon-side > span,
|
||||
.neon-toolbar span {
|
||||
display: block;
|
||||
color: #99a8c9;
|
||||
font-size: 8px;
|
||||
color: #d2dcef;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.neon-records strong {
|
||||
font-size: 18px;
|
||||
font-size: 26px;
|
||||
}
|
||||
.neon-how {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 9px 11px;
|
||||
padding: 12px 13px;
|
||||
border: 1px solid #6ff5e42a;
|
||||
border-radius: 13px;
|
||||
color: #72f5e4;
|
||||
@@ -579,25 +580,25 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.neon-how p {
|
||||
margin: 0;
|
||||
color: #aeb9d4;
|
||||
font-size: 9px;
|
||||
line-height: 1.35;
|
||||
color: #dce4f3;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.neon-how strong {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.neon-primary,
|
||||
.neon-secondary {
|
||||
width: 100%;
|
||||
min-height: 41px;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
border-radius: 13px;
|
||||
font-size: 11px;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
.neon-primary {
|
||||
@@ -626,7 +627,7 @@ onBeforeUnmount(() => {
|
||||
text-align: center;
|
||||
}
|
||||
.neon-toolbar strong {
|
||||
font-size: 14px;
|
||||
font-size: 21px;
|
||||
}
|
||||
.neon-play-area {
|
||||
display: flex;
|
||||
@@ -635,7 +636,7 @@ onBeforeUnmount(() => {
|
||||
gap: 5px;
|
||||
}
|
||||
.neon-board {
|
||||
width: 230px;
|
||||
width: 218px;
|
||||
height: 414px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
@@ -699,29 +700,29 @@ onBeforeUnmount(() => {
|
||||
animation: neon-clear 0.24s ease-out;
|
||||
}
|
||||
.neon-side {
|
||||
width: 47px;
|
||||
width: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 3px 9px;
|
||||
border: 1px solid #65f4e41f;
|
||||
gap: 9px;
|
||||
padding: 10px 4px 11px;
|
||||
border: 1px solid #65f4e448;
|
||||
border-radius: 13px;
|
||||
background: linear-gradient(180deg, #1420478f, #090d24a3);
|
||||
box-shadow: inset 0 0 16px #53e8d90a;
|
||||
}
|
||||
.neon-side > strong {
|
||||
font-size: 22px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.neon-preview {
|
||||
width: 43px;
|
||||
height: 43px;
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 1px;
|
||||
padding: 4px;
|
||||
border: 1px solid #ffffff16;
|
||||
gap: 2px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ffffff35;
|
||||
border-radius: 10px;
|
||||
background: #ffffff08;
|
||||
}
|
||||
@@ -733,8 +734,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.neon-level {
|
||||
position: relative;
|
||||
width: 7px;
|
||||
height: 246px;
|
||||
width: 12px;
|
||||
height: 221px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #ffffff0c;
|
||||
@@ -756,7 +757,7 @@ onBeforeUnmount(() => {
|
||||
margin-top: 9px;
|
||||
}
|
||||
.neon-controls button {
|
||||
height: 41px;
|
||||
height: 48px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
@@ -769,10 +770,14 @@ onBeforeUnmount(() => {
|
||||
color: #071225;
|
||||
background: linear-gradient(135deg, #ffe265, #ff9167);
|
||||
}
|
||||
.neon-controls svg {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.neon-hint {
|
||||
margin: 5px 0 0;
|
||||
color: #8d9bb9;
|
||||
font-size: 8px;
|
||||
margin: 7px 0 0;
|
||||
color: #d6deed;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
.neon-overlay {
|
||||
@@ -795,19 +800,19 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.neon-overlay > span {
|
||||
color: #ff7fa0;
|
||||
font-size: 9px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.neon-overlay h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-size: 30px;
|
||||
}
|
||||
.neon-overlay p {
|
||||
margin: 0 0 4px;
|
||||
color: #aeb8d0;
|
||||
font-size: 10px;
|
||||
color: #dce4f2;
|
||||
font-size: 16px;
|
||||
}
|
||||
@keyframes neon-fall {
|
||||
0% {
|
||||
|
||||
@@ -370,7 +370,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
.number-merge-header span {
|
||||
display: block;
|
||||
color: #9c7465;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1.2px;
|
||||
text-transform: uppercase;
|
||||
@@ -435,8 +435,8 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
.number-merge-hero__tile--4 { color: #fff7e4; background: #e96c2c; }
|
||||
.number-merge-hero__tile--8 { color: #fff7e4; background: #713c31; }
|
||||
|
||||
.number-merge-menu__intro h2 { margin: 0; font-size: 22px; letter-spacing: -0.5px; }
|
||||
.number-merge-menu__intro p { max-width: 270px; margin: 5px 0 0; color: #8b675b; font-size: 11px; line-height: 1.4; }
|
||||
.number-merge-menu__intro h2 { margin: 0; font-size: 25px; letter-spacing: -0.5px; }
|
||||
.number-merge-menu__intro p { max-width: 300px; margin: 6px 0 0; color: #8b675b; font-size: 14px; line-height: 1.45; }
|
||||
|
||||
.number-merge-records {
|
||||
width: 100%;
|
||||
@@ -457,22 +457,22 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
.number-merge-records span,
|
||||
.number-merge-toolbar span {
|
||||
color: #9d7567;
|
||||
font-size: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.number-merge-records strong { font-size: 17px; }
|
||||
.number-merge-records strong { font-size: 20px; }
|
||||
|
||||
.number-merge-menu__actions { width: 100%; display: grid; gap: 7px; }
|
||||
|
||||
.number-merge-primary,
|
||||
.number-merge-secondary,
|
||||
.number-merge-danger {
|
||||
min-height: 42px;
|
||||
min-height: 46px;
|
||||
padding: 0 18px;
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 850;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -497,16 +497,16 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
background: rgb(255 255 255 / 28%);
|
||||
}
|
||||
|
||||
.number-merge-how-to strong { font-size: 10px; text-transform: uppercase; }
|
||||
.number-merge-how-to p { margin: 3px 0 0; font-size: 9px; line-height: 1.35; }
|
||||
.number-merge-how-to strong { font-size: 13px; text-transform: uppercase; }
|
||||
.number-merge-how-to p { margin: 5px 0 0; font-size: 12px; line-height: 1.4; }
|
||||
.number-merge-how-to div {
|
||||
margin: 6px 0 3px;
|
||||
margin: 7px 0 4px;
|
||||
color: #a7472d;
|
||||
font-size: 11px;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
.number-merge-how-to small { display: block; color: #8b6559; font-size: 8px; line-height: 1.25; }
|
||||
.number-merge-how-to small { display: block; color: #8b6559; font-size: 11px; line-height: 1.35; }
|
||||
|
||||
.number-merge-game { padding-top: 5px; }
|
||||
|
||||
@@ -525,7 +525,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.number-merge-toolbar strong { max-width: 80px; overflow: hidden; font-size: 16px; text-overflow: ellipsis; }
|
||||
.number-merge-toolbar strong { max-width: 80px; overflow: hidden; font-size: 18px; text-overflow: ellipsis; }
|
||||
.number-merge-toolbar__restart { justify-self: end; }
|
||||
|
||||
.number-merge-board {
|
||||
@@ -625,7 +625,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
|
||||
.number-merge-overlay > span { color: #ffc75b; font-size: 29px; font-weight: 900; }
|
||||
.number-merge-overlay h2 { margin: 0; font-size: 25px; }
|
||||
.number-merge-overlay p { margin: -3px 0 4px; color: #e7cabd; font-size: 11px; line-height: 1.35; }
|
||||
.number-merge-overlay p { margin: -3px 0 4px; color: #e7cabd; font-size: 14px; line-height: 1.4; }
|
||||
.number-merge-overlay button { min-width: 160px; }
|
||||
.number-merge-overlay .number-merge-secondary { color: #fff1df; background: rgb(255 255 255 / 8%); border-color: rgb(255 255 255 / 13%); }
|
||||
|
||||
@@ -634,11 +634,11 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
border: 0;
|
||||
color: #e9cec2;
|
||||
background: transparent;
|
||||
font-size: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.number-merge-hint { margin: 10px 0 7px; color: #976f61; font-size: 9px; text-align: center; }
|
||||
.number-merge-hint { margin: 10px 0 7px; color: #976f61; font-size: 12px; text-align: center; }
|
||||
|
||||
.number-merge-controls {
|
||||
display: grid;
|
||||
@@ -683,7 +683,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
}
|
||||
|
||||
.number-merge-confirm h2 { margin: 0; font-size: 21px; }
|
||||
.number-merge-confirm p { margin: 0 0 5px; color: #876359; font-size: 11px; line-height: 1.4; }
|
||||
.number-merge-confirm p { margin: 0 0 5px; color: #876359; font-size: 14px; line-height: 1.4; }
|
||||
.number-merge-danger { border: 0; color: #fff; background: #b64f39; }
|
||||
|
||||
button:active { transform: scale(0.97); }
|
||||
|
||||
@@ -159,10 +159,33 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flappy-app { --sky-a:#53c9ed;--sky-b:#7970df;--tower:#645bd1; position:absolute;inset:0;overflow:hidden;padding:52px 16px 27px;color:#fff;background:linear-gradient(160deg,#19375e,#433b80);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;user-select:none;touch-action:manipulation; }
|
||||
.flappy-app--neon { --sky-a:#1b1e58;--sky-b:#7b258c;--tower:#23cbd1; }.flappy-app--storm { --sky-a:#526779;--sky-b:#24354e;--tower:#b06f64; }
|
||||
.flappy-header{height:55px;display:flex;align-items:center;justify-content:space-between}.flappy-header span{display:block;color:#c3d7ef;font-size:9px;font-weight:850;letter-spacing:1.1px;text-transform:uppercase}.flappy-header h1{margin:0;font-size:24px;line-height:1}.flappy-header button,.flappy-toolbar button{width:36px;height:36px;display:grid;place-items:center;padding:0;border:1px solid #ffffff1c;border-radius:12px;color:#fff;background:#ffffff12}
|
||||
.flappy-app { --sky-a:#50d8f2;--sky-b:#765ce8;--tower:#574be8;--tower-light:#9b94ff;--tower-dark:#3429a6;--tower-glow:#79e7ff; position:absolute;inset:0;overflow:hidden;padding:52px 16px 27px;color:#fff;background:linear-gradient(160deg,#19375e,#433b80);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;user-select:none;touch-action:manipulation; }
|
||||
.flappy-app--neon { --sky-a:#151c58;--sky-b:#a329a2;--tower:#19dfe6;--tower-light:#82ffff;--tower-dark:#087f91;--tower-glow:#26fbff; }.flappy-app--storm { --sky-a:#6f8497;--sky-b:#2b3955;--tower:#df765f;--tower-light:#ffb18e;--tower-dark:#8d3e39;--tower-glow:#ff997d; }
|
||||
.flappy-header{height:55px;display:flex;align-items:center;justify-content:space-between}.flappy-header span{display:block;color:#d9e9fa;font-size:14px;font-weight:850;letter-spacing:1.1px;text-transform:uppercase}.flappy-header h1{margin:1px 0 0;font-size:32px;line-height:1}.flappy-header button,.flappy-toolbar button{width:36px;height:36px;display:grid;place-items:center;padding:0;border:1px solid #ffffff35;border-radius:12px;color:#fff;background:#ffffff18}
|
||||
.flappy-menu{height:calc(100% - 55px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center}.flappy-menu__hero{position:relative;width:220px;height:210px;overflow:hidden;border-radius:28px;background:linear-gradient(var(--sky-a),var(--sky-b));box-shadow:inset 0 0 28px #223c6d35,0 18px 32px #101b3b66}.flappy-menu__hero::before{position:absolute;z-index:0;top:22px;left:25px;width:39px;height:39px;border-radius:50%;background:#ffe8a7cc;box-shadow:0 0 18px #fff1b46b;content:""}.flappy-menu__hero::after{position:absolute;z-index:1;bottom:30px;left:-12px;width:68px;height:18px;border-radius:999px;background:#ffffff3d;box-shadow:27px -9px 0 3px #ffffff36,57px 1px 0 -2px #ffffff2c;content:""}.flappy-menu__tower{position:absolute;z-index:3;right:10px;width:42px;background:linear-gradient(90deg,color-mix(in srgb,var(--tower),white 30%),var(--tower));box-shadow:inset 7px 0 0 #ffffff22,inset -6px 0 9px #0003}.flappy-menu__tower::after{position:absolute;left:-8px;width:58px;height:22px;border:3px solid color-mix(in srgb,var(--tower),black 22%);border-radius:7px;background:linear-gradient(90deg,color-mix(in srgb,var(--tower),white 36%),var(--tower));box-shadow:inset 0 4px 0 #ffffff26;content:""}.flappy-menu__tower--top{top:0;height:61px;border-radius:0 0 5px 5px}.flappy-menu__tower--top::after{bottom:-11px}.flappy-menu__tower--bottom{bottom:0;height:67px;border-radius:5px 5px 0 0}.flappy-menu__tower--bottom::after{top:-11px}.flappy-menu__trail{position:absolute;z-index:2;top:102px;left:11px;width:46px;height:4px;border-radius:99px;background:#edfbffb8;box-shadow:13px 12px 0 -1px #e9faff8c,6px -12px 0 -1px #e9faff73;animation:flappy-trail 1s ease-in-out infinite}.sky-bird{position:absolute;z-index:4;width:66px;height:46px;overflow:visible;filter:drop-shadow(0 4px 5px #071d3e66)}.sky-bird :deep(path){stroke-linecap:round;stroke-linejoin:round}.sky-bird :deep(.sky-flappy-bird__far-wing){fill:#2d7188;stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__far-wing path:last-child){fill:none;stroke:#8ec6ce;stroke-width:1.4;opacity:.65}.sky-bird :deep(.sky-flappy-bird__tail){fill:#285e78;stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__body){fill:url(#sky-bird-body);stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__belly){fill:#b7e5df;opacity:.84}.sky-bird :deep(.sky-flappy-bird__neck){fill:#377f98}.sky-bird :deep(.sky-flappy-bird__wing){transform-box:view-box;transform-origin:52px 38px}.sky-bird :deep(.sky-flappy-bird__wing-shape){fill:url(#sky-bird-wing);stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__feather){fill:none;stroke:#d4f0ed;stroke-width:1.5;opacity:.78}.sky-bird :deep(.sky-flappy-bird__face){fill:#d8f0e9}.sky-bird :deep(.sky-flappy-bird__eye-ring){fill:#f5fbf7}.sky-bird :deep(.sky-flappy-bird__eye){fill:#102d3d}.sky-bird :deep(.sky-flappy-bird__beak){fill:#f2a84b;stroke:#6f4727;stroke-width:1.5}.sky-bird--hero{top:72px;left:42px;width:100px;height:69px;animation:flappy-hero 1.1s ease-in-out infinite alternate}.sky-bird--hero :deep(.sky-flappy-bird__wing){animation:flappy-bird-glide 1.1s ease-in-out infinite}.sky-bird--hero :deep(.sky-flappy-bird__far-wing){transform-box:view-box;transform-origin:50px 38px;animation:flappy-bird-far-glide 1.1s ease-in-out infinite}.flappy-menu__copy>span{color:#ffda70;font-size:9px;font-weight:900;letter-spacing:1px;text-transform:uppercase}.flappy-menu__copy h2{margin:3px 0 5px;font-size:21px}.flappy-menu__copy p{max-width:275px;margin:0;color:#c0c8df;font-size:10px;line-height:1.4}.flappy-record{width:100%;display:flex;align-items:center;justify-content:space-between;padding:9px 14px;border:1px solid #ffffff14;border-radius:13px;background:#ffffff0c}.flappy-record span{color:#bdc8df;font-size:9px;font-weight:800;text-transform:uppercase}.flappy-record strong{font-size:19px}.flappy-designs{width:100%;display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.flappy-designs button{display:grid;place-items:center;gap:3px;padding:7px 3px;border:1px solid #ffffff12;border-radius:12px;color:#ccd4e9;background:#ffffff0a;font-size:8px}.flappy-designs button.active{border-color:#ffdd72;color:#fff;background:#ffffff1b}.flappy-designs i{width:26px;height:13px;border-radius:8px}.flappy-designs__dawn{background:linear-gradient(90deg,#58d6ee,#ff9b80)}.flappy-designs__neon{background:linear-gradient(90deg,#24255f,#ef55ca)}.flappy-designs__storm{background:linear-gradient(90deg,#71899b,#253750)}.flappy-primary,.flappy-secondary{width:100%;min-height:43px;display:flex;align-items:center;justify-content:center;gap:7px;border-radius:14px;font-size:11px;font-weight:850}.flappy-primary{border:0;color:#173353;background:linear-gradient(135deg,#ffe16c,#ff9d68)}.flappy-secondary{border:1px solid #ffffff18;color:#fff;background:#ffffff0b}.flappy-menu>p,.flappy-game__hint{margin:0;color:#aeb9d2;font-size:9px}
|
||||
.flappy-game{position:relative;height:calc(100% - 55px)}.flappy-toolbar{height:47px;display:grid;grid-template-columns:36px 1fr 1fr 36px;align-items:center;gap:7px}.flappy-toolbar div{display:grid;justify-items:center}.flappy-toolbar span{color:#bac9df;font-size:8px;font-weight:850;text-transform:uppercase}.flappy-toolbar strong{font-size:16px}.flappy-stage{position:relative;width:100%;height:500px;display:block;overflow:hidden;padding:0;border:1px solid #ffffff1c;border-radius:22px;background:linear-gradient(var(--sky-a),var(--sky-b));box-shadow:inset 0 0 35px #15244c55,0 16px 30px #10193477;touch-action:manipulation}.flappy-clouds{position:absolute;z-index:1;inset:0;overflow:hidden;pointer-events:none}.flappy-clouds i{--cloud-scale:1;--cloud-opacity:.34;--cloud-duration:18s;--cloud-delay:0s;position:absolute;left:100%;width:70px;height:18px;border-radius:999px;background:linear-gradient(180deg,#ffffffd9,#eaf7ff9c);box-shadow:0 8px 16px #24376518;opacity:var(--cloud-opacity);animation:cloud-drift var(--cloud-duration) linear var(--cloud-delay) infinite;will-change:transform}.flappy-clouds i::before{position:absolute;bottom:4px;left:12px;width:29px;height:29px;border-radius:50%;background:#f8fcffe6;box-shadow:22px -8px 0 4px #f7fcff,40px 1px 0 -2px #eef9ff;content:""}.flappy-clouds i::after{position:absolute;right:8px;bottom:-3px;left:8px;height:8px;border-radius:50%;background:#bcdff477;filter:blur(4px);content:""}.flappy-clouds i:nth-child(1){--cloud-scale:.7;--cloud-opacity:.3;--cloud-duration:20s;--cloud-delay:-4s;top:9%}.flappy-clouds i:nth-child(2){--cloud-scale:1.05;--cloud-opacity:.4;--cloud-duration:15s;--cloud-delay:-11s;top:22%}.flappy-clouds i:nth-child(3){--cloud-scale:.52;--cloud-opacity:.25;--cloud-duration:23s;--cloud-delay:-17s;top:38%}.flappy-clouds i:nth-child(4){--cloud-scale:.88;--cloud-opacity:.36;--cloud-duration:17s;--cloud-delay:-7s;top:53%}.flappy-clouds i:nth-child(5){--cloud-scale:1.18;--cloud-opacity:.42;--cloud-duration:14s;--cloud-delay:-2s;top:68%}.flappy-clouds i:nth-child(6){--cloud-scale:.62;--cloud-opacity:.27;--cloud-duration:21s;--cloud-delay:-14s;top:78%}.flappy-clouds i:nth-child(7){--cloud-scale:.96;--cloud-opacity:.34;--cloud-duration:16s;--cloud-delay:-9s;top:86%}.flappy-obstacle{position:absolute;z-index:2;top:0;bottom:0}.flappy-obstacle span{position:absolute;right:0;left:0;background:linear-gradient(90deg,color-mix(in srgb,var(--tower),white 20%),var(--tower));box-shadow:inset -6px 0 8px #0003,0 0 13px #17204e55}.flappy-obstacle span::after{position:absolute;right:-4px;left:-4px;height:14px;border-radius:6px;background:color-mix(in srgb,var(--tower),white 10%);box-shadow:inset 0 3px 0 #ffffff25;content:""}.flappy-obstacle__top{top:0;border-radius:0 0 7px 7px}.flappy-obstacle__top::after{bottom:0}.flappy-obstacle__bottom{bottom:0;border-radius:7px 7px 0 0}.flappy-obstacle__bottom::after{top:0}.sky-bird--player{left:23%;animation:flappy-wing .24s ease-out}.sky-bird--player :deep(.sky-flappy-bird__wing){animation:flappy-bird-flap .24s cubic-bezier(.2,.75,.35,1)}.sky-bird--player :deep(.sky-flappy-bird__far-wing){transform-box:view-box;transform-origin:50px 38px;animation:flappy-bird-far-flap .24s cubic-bezier(.2,.75,.35,1)}.flappy-ready{position:absolute;z-index:6;top:36%;left:50%;padding:9px 15px;border-radius:17px;background:#15284fbb;font-size:11px;transform:translateX(-50%)}.flappy-horizon{position:absolute;z-index:3;right:0;bottom:0;left:0;height:12px;background:#263a62;box-shadow:0 -5px 14px #ffffff26}.flappy-stage--crashed{animation:flappy-crash .55s ease-out}.flappy-game__hint{margin-top:8px;text-align:center}.flappy-overlay{position:absolute;z-index:12;inset:47px 0 25px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;padding:28px;border-radius:22px;background:#101a38dc;backdrop-filter:blur(7px);text-align:center}.flappy-overlay>svg{color:#ffdc71}.flappy-overlay>span{color:#ffad78;font-size:9px;font-weight:900;letter-spacing:1px;text-transform:uppercase}.flappy-overlay h2{margin:0 0 5px;font-size:26px}
|
||||
.flappy-menu__tower{border:2px solid var(--tower-dark);background:linear-gradient(90deg,var(--tower-light),var(--tower),var(--tower-dark));box-shadow:inset 7px 0 0 #ffffff38,inset -6px 0 9px #0004,0 0 18px var(--tower-glow)}
|
||||
.flappy-menu__tower::after{border-color:var(--tower-dark);background:linear-gradient(90deg,var(--tower-light),var(--tower));box-shadow:inset 0 4px 0 #ffffff45,0 0 14px var(--tower-glow)}
|
||||
.flappy-menu__copy>span{color:#ffdf7d;font-size:14px}
|
||||
.flappy-menu__copy h2{margin:4px 0 7px;font-size:30px}
|
||||
.flappy-menu__copy p{max-width:295px;color:#e4ebf7;font-size:18px;font-weight:550;line-height:1.4}
|
||||
.flappy-record{padding:12px 14px;border-color:#ffffff2b;background:#ffffff12}
|
||||
.flappy-record span{color:#d9e4f3;font-size:15px;font-weight:850}
|
||||
.flappy-record strong{font-size:26px}
|
||||
.flappy-designs button{gap:5px;padding:9px 3px;border-color:#ffffff25;color:#edf3ff;background:#ffffff0e;font-size:14px;font-weight:750}
|
||||
.flappy-designs button.active{background:#ffffff24}
|
||||
.flappy-designs i{width:32px;height:15px}
|
||||
.flappy-primary,.flappy-secondary{min-height:50px;gap:8px;font-size:18px}
|
||||
.flappy-secondary{border-color:#ffffff2c;background:#ffffff12}
|
||||
.flappy-menu>p,.flappy-game__hint{color:#e1e9f6;font-size:16px;font-weight:700;line-height:1.35}
|
||||
.flappy-toolbar span{color:#e0eafa;font-size:12px;letter-spacing:.35px}
|
||||
.flappy-toolbar strong{font-size:21px}
|
||||
.flappy-stage{border:2px solid #ffffff70;box-shadow:inset 0 0 35px #15244c38,0 16px 30px #10193477}
|
||||
.flappy-obstacle span{border:2px solid var(--tower-dark);background:linear-gradient(90deg,var(--tower-light),var(--tower),var(--tower-dark));box-shadow:inset 7px 0 0 #ffffff42,inset -6px 0 8px #0004,0 0 18px var(--tower-glow)}
|
||||
.flappy-obstacle span::after{right:-5px;left:-5px;height:17px;border:2px solid var(--tower-dark);background:linear-gradient(180deg,var(--tower-light),var(--tower));box-shadow:inset 0 4px 0 #ffffff55,0 0 14px var(--tower-glow)}
|
||||
.flappy-ready{padding:11px 18px;border:2px solid #ffffff55;border-radius:19px;background:#15284fe6;font-size:16px;font-weight:850}
|
||||
.flappy-horizon{background:var(--tower-dark);box-shadow:0 -5px 18px var(--tower-glow)}
|
||||
.flappy-overlay>span{color:#ffc09b;font-size:13px}
|
||||
.flappy-overlay h2{font-size:30px}
|
||||
@keyframes flappy-hero{from{transform:translateY(7px) rotate(-5deg)}to{transform:translateY(-8px) rotate(7deg)}}@keyframes flappy-trail{0%,100%{opacity:.18;transform:scaleX(.65)}50%{opacity:.62;transform:scaleX(1)}}@keyframes flappy-wing{0%{filter:brightness(1.12) drop-shadow(0 4px 5px #071d3e66);scale:1.04}100%{filter:brightness(1) drop-shadow(0 4px 5px #071d3e66);scale:1}}@keyframes flappy-bird-flap{0%{transform:rotate(13deg) scaleY(.92)}42%{transform:rotate(-42deg) scaleY(.84)}100%{transform:rotate(8deg)}}@keyframes flappy-bird-far-flap{0%{transform:rotate(5deg)}45%{transform:rotate(-27deg)}100%{transform:rotate(3deg)}}@keyframes flappy-bird-glide{0%,100%{transform:rotate(7deg)}50%{transform:rotate(-32deg) scaleY(.88)}}@keyframes flappy-bird-far-glide{0%,100%{transform:rotate(2deg)}50%{transform:rotate(-20deg)}}@keyframes cloud-drift{from{transform:translate3d(40px,0,0) scale(var(--cloud-scale))}to{transform:translate3d(-440px,0,0) scale(var(--cloud-scale))}}@keyframes flappy-crash{0%,100%{transform:translate(0)}25%{transform:translate(-5px,3px)}50%{transform:translate(5px,-2px)}75%{transform:translate(-3px,1px)}}button:active{transform:scale(.97)}@media(prefers-reduced-motion:reduce){.sky-bird,.sky-bird :deep(*),.flappy-menu__trail,.flappy-clouds i,.flappy-stage{animation:none}}
|
||||
</style>
|
||||
|
||||
@@ -37,12 +37,12 @@ let previousFrameTime = 0
|
||||
let effectTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const blockColors = [
|
||||
'#ff6b68',
|
||||
'#ffbf3f',
|
||||
'#39c9d3',
|
||||
'#8c63e8',
|
||||
'#ff8454',
|
||||
'#54d5a5',
|
||||
'#ff4d67',
|
||||
'#ffd23f',
|
||||
'#27dff2',
|
||||
'#a879ff',
|
||||
'#ff7a3d',
|
||||
'#36e6a0',
|
||||
]
|
||||
|
||||
function blockStyle(block: TowerBlock, index: number): CSSProperties {
|
||||
@@ -306,51 +306,52 @@ onBeforeUnmount(() => {
|
||||
<style scoped>
|
||||
.tower-app { position: absolute; inset: 0; overflow: hidden; padding: 52px 16px 27px; color: #eef5ff; background: radial-gradient(circle at 75% 8%, #7146c866, transparent 35%), linear-gradient(170deg, #161634, #242054 52%, #10132c); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; user-select: none; touch-action: manipulation; }
|
||||
.tower-header { height: 55px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.tower-header span { display: block; color: #a69dd3; font-size: 9px; font-weight: 850; letter-spacing: 1.1px; text-transform: uppercase; }
|
||||
.tower-header h1 { margin: 0; font-size: 24px; line-height: 1; letter-spacing: -0.8px; }
|
||||
.tower-header span { display: block; color: #c1b8f1; font-size: 14px; font-weight: 850; letter-spacing: 1.1px; text-transform: uppercase; }
|
||||
.tower-header h1 { margin: 1px 0 0; font-size: 32px; line-height: 1; letter-spacing: -0.8px; }
|
||||
.tower-header button, .tower-toolbar button { width: 36px; height: 36px; display: grid; place-items: center; padding: 0; border: 1px solid #ffffff14; border-radius: 12px; color: #f2edff; background: #ffffff0d; }
|
||||
.tower-menu { height: calc(100% - 55px); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 13px; text-align: center; }
|
||||
.tower-menu__preview { position: relative; width: 190px; height: 235px; }
|
||||
.tower-menu { height: calc(100% - 55px); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; text-align: center; }
|
||||
.tower-menu__preview { position: relative; width: 190px; height: 210px; }
|
||||
.tower-menu__preview i { position: absolute; right: 12px; bottom: calc(var(--preview-index) * 25px); left: 25px; height: 29px; border-radius: 7px; background: hsl(calc(var(--preview-index) * 49deg + 5deg) 82% 62%); box-shadow: inset 0 4px 0 #ffffff35, 0 8px 15px #08091c5c; transform: perspective(200px) rotateX(5deg); }
|
||||
.tower-menu__preview i:nth-child(even) { right: 25px; left: 12px; }
|
||||
.tower-menu__preview span { position: absolute; top: 4px; left: 2px; width: 120px; height: 29px; border-radius: 7px; background: #ff6b68; box-shadow: 0 0 24px #ff6b6877; animation: tower-preview-slide 1.5s ease-in-out infinite alternate; }
|
||||
.tower-menu__copy > span { color: #ffbd45; font-size: 9px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.tower-menu__copy h2 { margin: 3px 0 5px; font-size: 21px; }
|
||||
.tower-menu__copy p { max-width: 275px; margin: 0; color: #aca7cc; font-size: 10px; line-height: 1.4; }
|
||||
.tower-menu__copy > span { color: #ffcf59; font-size: 14px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.tower-menu__copy h2 { margin: 4px 0 7px; font-size: 30px; }
|
||||
.tower-menu__copy p { max-width: 295px; margin: 0; color: #e0dcf2; font-size: 18px; font-weight: 550; line-height: 1.4; }
|
||||
.tower-records { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.tower-records div { display: grid; gap: 2px; padding: 9px; border: 1px solid #ffffff0e; border-radius: 13px; background: #ffffff0a; }
|
||||
.tower-records span { color: #9790bd; font-size: 8px; font-weight: 800; text-transform: uppercase; }
|
||||
.tower-records strong { font-size: 18px; }
|
||||
.tower-primary, .tower-secondary { width: 100%; min-height: 43px; display: flex; align-items: center; justify-content: center; gap: 7px; border-radius: 14px; font-size: 11px; font-weight: 850; }
|
||||
.tower-records div { display: grid; gap: 3px; padding: 12px 9px; border: 1px solid #ffffff1f; border-radius: 13px; background: #ffffff0d; }
|
||||
.tower-records span { color: #d1caea; font-size: 15px; font-weight: 850; text-transform: uppercase; }
|
||||
.tower-records strong { font-size: 26px; }
|
||||
.tower-primary, .tower-secondary { width: 100%; min-height: 50px; display: flex; align-items: center; justify-content: center; gap: 8px; border-radius: 14px; font-size: 18px; font-weight: 850; }
|
||||
.tower-primary { border: 0; color: #1e1839; background: linear-gradient(135deg, #ffca4f, #ff8760); box-shadow: 0 8px 18px #ff895330; }
|
||||
.tower-secondary { border: 1px solid #ffffff16; color: #eeeaff; background: #ffffff0a; }
|
||||
.tower-menu__hint, .tower-game__hint { margin: 0; color: #8d87b3; font-size: 9px; }
|
||||
.tower-menu__hint, .tower-game__hint { margin: 0; color: #e0daf0; font-size: 16px; font-weight: 700; line-height: 1.35; }
|
||||
.tower-game { position: relative; height: calc(100% - 55px); }
|
||||
.tower-toolbar { height: 47px; display: grid; grid-template-columns: 36px 1fr 1fr 36px; align-items: center; gap: 7px; }
|
||||
.tower-toolbar div { display: grid; justify-items: center; line-height: 1.05; }
|
||||
.tower-toolbar span { color: #918ab9; font-size: 8px; font-weight: 850; text-transform: uppercase; }
|
||||
.tower-toolbar strong { font-size: 16px; }
|
||||
.tower-stage { position: relative; width: 100%; height: 500px; display: block; overflow: hidden; padding: 0; border: 1px solid #ffffff12; border-radius: 22px; background: linear-gradient(#17173b, #322361 60%, #70426c); box-shadow: inset 0 0 35px #08091d99, 0 16px 28px #08091c66; touch-action: manipulation; }
|
||||
.tower-toolbar span { color: #c9c2e9; font-size: 12px; font-weight: 850; letter-spacing: .35px; text-transform: uppercase; }
|
||||
.tower-toolbar strong { font-size: 21px; }
|
||||
.tower-stage { position: relative; width: 100%; height: 500px; display: block; overflow: hidden; padding: 0; border: 1px solid #ffffff35; border-radius: 22px; background: linear-gradient(#1d1b52, #433078 60%, #8e4c78); box-shadow: inset 0 0 35px #08091d80, 0 16px 28px #08091c66; touch-action: manipulation; }
|
||||
.tower-sky { position: absolute; inset: 0; pointer-events: none; }
|
||||
.tower-sky i { position: absolute; width: 3px; height: 3px; border-radius: 50%; background: #fff; box-shadow: 0 0 7px #c5c2ff; opacity: .65; }
|
||||
.tower-sky i:nth-child(1) { top: 8%; left: 12%; } .tower-sky i:nth-child(2) { top: 17%; left: 72%; } .tower-sky i:nth-child(3) { top: 28%; left: 42%; } .tower-sky i:nth-child(4) { top: 37%; left: 88%; } .tower-sky i:nth-child(5) { top: 46%; left: 18%; } .tower-sky i:nth-child(6) { top: 58%; left: 64%; } .tower-sky i:nth-child(7) { top: 70%; left: 31%; } .tower-sky i:nth-child(8) { top: 11%; left: 91%; } .tower-sky i:nth-child(9) { top: 22%; left: 25%; } .tower-sky i:nth-child(10) { top: 50%; left: 78%; } .tower-sky i:nth-child(11) { top: 64%; left: 8%; } .tower-sky i:nth-child(12) { top: 77%; left: 92%; } .tower-sky i:nth-child(13) { top: 33%; left: 58%; }
|
||||
.tower-stack { position: absolute; inset: 0 14px; }
|
||||
.tower-block { position: absolute; height: 31px; border-radius: 7px; background: linear-gradient(180deg, color-mix(in srgb, var(--tower-block-color), white 18%), var(--tower-block-color)); box-shadow: inset 0 4px 0 #ffffff35, inset 0 -4px 0 #00000016, 0 7px 10px #08091d55; }
|
||||
.tower-block--active { z-index: 3; box-shadow: inset 0 4px 0 #ffffff45, 0 0 18px color-mix(in srgb, var(--tower-block-color), transparent 45%); }
|
||||
.tower-block { position: absolute; height: 32px; border: 1px solid #ffffff80; border-radius: 7px; background: var(--tower-block-color); box-shadow: inset 0 5px 0 #ffffff52, inset 0 -4px 0 #00000024, 0 7px 12px #08091d80; }
|
||||
.tower-block--active { z-index: 3; border: 2px solid #fff; filter: saturate(1.3) brightness(1.2); box-shadow: inset 0 5px 0 #ffffff70, 0 0 8px #fff, 0 0 24px var(--tower-block-color), 0 9px 14px #08091d90; animation: tower-active-pulse .7s ease-in-out infinite alternate; }
|
||||
.tower-block--placed { animation: tower-land 180ms ease-out; }
|
||||
.tower-block--falling { z-index: 4; animation: tower-fall 850ms ease-in forwards; }
|
||||
.tower-ground { position: absolute; right: 0; bottom: 0; left: 0; height: 35px; background: linear-gradient(#31224f, #16152e); box-shadow: 0 -8px 20px #a9547928; }
|
||||
.tower-ground { position: absolute; right: 0; bottom: 0; left: 0; height: 35px; background: linear-gradient(#5b3468, #191630); box-shadow: 0 -8px 24px #ff729150; }
|
||||
.tower-perfect { position: absolute; z-index: 8; top: 24%; left: 50%; padding: 8px 17px; border-radius: 18px; color: #332149; background: #ffdc65; box-shadow: 0 0 24px #ffcf64aa; font-size: 13px; font-weight: 950; letter-spacing: .8px; transform: translateX(-50%); animation: tower-perfect-pop 650ms ease-out forwards; }
|
||||
.tower-stage--perfect { animation: tower-perfect-glow 500ms ease-out; }
|
||||
.tower-stage--missed { animation: tower-stage-shake 500ms ease-out; }
|
||||
.tower-game__hint { margin-top: 8px; text-align: center; }
|
||||
.tower-overlay { position: absolute; z-index: 15; inset: 47px 0 25px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; padding: 28px; border-radius: 22px; color: #f7f2ff; background: #11122bd9; backdrop-filter: blur(7px); text-align: center; }
|
||||
.tower-overlay > svg { color: #ffbd4d; }
|
||||
.tower-overlay > span { color: #ff9b65; font-size: 9px; font-weight: 900; letter-spacing: 1.2px; text-transform: uppercase; }
|
||||
.tower-overlay > span { color: #ffad78; font-size: 12px; font-weight: 900; letter-spacing: 1.2px; text-transform: uppercase; }
|
||||
.tower-overlay h2 { margin: 0; font-size: 25px; }
|
||||
.tower-overlay p { margin: -3px 0 5px; color: #a9a3c9; font-size: 10px; }
|
||||
.tower-overlay p { margin: -3px 0 5px; color: #c7c1e4; font-size: 14px; }
|
||||
@keyframes tower-preview-slide { from { transform: translateX(0) rotate(-2deg); } to { transform: translateX(66px) rotate(2deg); } }
|
||||
@keyframes tower-land { from { filter: brightness(1.8); transform: translateY(-8px) scaleY(.85); } to { filter: brightness(1); transform: translateY(0) scaleY(1); } }
|
||||
@keyframes tower-active-pulse { from { filter: saturate(1.2) brightness(1.08); } to { filter: saturate(1.45) brightness(1.35); } }
|
||||
@keyframes tower-fall { 0% { opacity: 1; transform: translate(0) rotate(0); } 100% { opacity: 0; transform: translate(45px, 390px) rotate(145deg); } }
|
||||
@keyframes tower-perfect-pop { 0% { opacity: 0; transform: translateX(-50%) scale(.4); } 35% { opacity: 1; transform: translateX(-50%) scale(1.2); } 100% { opacity: 0; transform: translateX(-50%) scale(1); } }
|
||||
@keyframes tower-perfect-glow { 0%, 100% { box-shadow: inset 0 0 35px #08091d99, 0 16px 28px #08091c66; } 45% { box-shadow: inset 0 0 45px #ffd75c55, 0 0 30px #ffd75c66; } }
|
||||
|
||||
+877
-101
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
Config.Bridge = {
|
||||
Framework = "auto", -- auto, esx, qbox, qb
|
||||
Inventory = "auto", -- auto, ox, qb, lj, qs, codem, core, mf
|
||||
Inventory = "auto", -- auto, ox, qb, lj, qs, codem, core, mf, smx
|
||||
Locale = "en",
|
||||
CallbackTimeout = 5000,
|
||||
Debug = false,
|
||||
|
||||
@@ -15,7 +15,7 @@ Locales["en"] = {
|
||||
},
|
||||
Nui = {
|
||||
Common = {
|
||||
add = "Add", back = "Back", cancel = "Cancel", close = "Close", delete = "Delete", done = "Done", edit = "Edit", home = "Home", loading = "Loading", pause = "Pause",
|
||||
add = "Add", back = "Back", cancel = "Cancel", clear = "Clear", close = "Close", delete = "Delete", done = "Done", edit = "Edit", home = "Home", loading = "Loading", pause = "Pause",
|
||||
phone = "Phone", phoneStatus = "Phone status", reset = "Reset",
|
||||
save = "Save", search = "Search", send = "Send", start = "Start", stop = "Stop",
|
||||
},
|
||||
@@ -168,7 +168,29 @@ Locales["en"] = {
|
||||
},
|
||||
},
|
||||
calendar = {
|
||||
name = "Calendar", reminder = "Upcoming: {title}",
|
||||
name = "Calendar", today = "Today", calendars = "Calendars", calendar = "Calendar", calendarName = "iFruit",
|
||||
searchPlaceholder = "Search events", previousMonth = "Previous month", nextMonth = "Next month",
|
||||
eyebrow = "Your time. Clearly planned.", schedule = "Schedule", event = "Event", appointment = "Appointment",
|
||||
newEvent = "New event", editEvent = "Edit event", deleteEvent = "Delete event", details = "Event details",
|
||||
title = "Title", titlePlaceholder = "What is planned?", date = "Date", starts = "Starts", ends = "Ends",
|
||||
reminder = "Reminder", reminderNotification = "Upcoming: {title}", note = "Note",
|
||||
notePlaceholder = "Add details, an address or anything to remember...", noEvents = "Nothing planned",
|
||||
noEventsBody = "This day is still free. Add an event whenever you are ready.",
|
||||
signInTitle = "Your iFruit calendar",
|
||||
signInBody = "Sign in to iFruit in Settings to sync appointments and receive reminders.",
|
||||
views = { compact = "Compact", stacked = "Stacked", details = "Details", list = "List" },
|
||||
reminders = {
|
||||
none = "No reminder", atStart = "At start time", tenMinutes = "10 minutes before",
|
||||
thirtyMinutes = "30 minutes before", oneHour = "1 hour before", oneDay = "1 day before",
|
||||
},
|
||||
errors = {
|
||||
invalid_event = "Enter a title and make sure the end is after the start.",
|
||||
invalid_range = "This calendar period is invalid.",
|
||||
conflict = "This event changed on another phone. Open it again.",
|
||||
rate_limited = "Too many changes. Try again shortly.",
|
||||
not_authenticated = "Sign in to your iFruit account first.",
|
||||
request_failed = "Calendar is temporarily unavailable.", default = "The calendar request failed.",
|
||||
},
|
||||
},
|
||||
clock = {
|
||||
name = "Clock", lap = "Lap", minutes = "Minutes", add = "Add alarm", location = "Los Santos",
|
||||
@@ -281,7 +303,7 @@ Locales["en"] = {
|
||||
},
|
||||
localPages = {
|
||||
name = "Local Pages", eyebrow = "Your city. Your stories.", cityPulse = "Live from Los Santos",
|
||||
heroTitle = "What is happening nearby?", heroBody = "Places, people, tips and local discoveries from the community.",
|
||||
heroTitle = "What is happening nearby?", heroBody = "Discover places, people and local tips.",
|
||||
searchPlaceholder = "Search posts and places", search = "Search", allCategories = "All categories",
|
||||
allLosSantos = "Los Santos", hoursAgo = "{count}h ago", daysAgo = "{count}d ago",
|
||||
categories = { recommendation = "Recommended", wanted = "Wanted", service = "Service", event = "Event", place = "Place", community = "Community", citymarkt = "CityMarkt" },
|
||||
|
||||
@@ -15,6 +15,8 @@ if configured_inventory == "auto" then
|
||||
configured_inventory = "core"
|
||||
elseif GetResourceState("mf-inventory") == "started" then
|
||||
configured_inventory = "mf"
|
||||
elseif GetResourceState("smx-inventory") == "started" then
|
||||
configured_inventory = "smx"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,6 +28,7 @@ local supported_inventories = {
|
||||
codem = true,
|
||||
core = true,
|
||||
mf = true,
|
||||
smx = true,
|
||||
}
|
||||
|
||||
if not supported_inventories[configured_inventory] then
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
if Bridge.Inventory.Name ~= "smx" then
|
||||
return
|
||||
end
|
||||
|
||||
if Bridge.Framework.GetName() ~= "esx" then
|
||||
error("[sky_phone] smx-inventory is only supported with ESX.")
|
||||
end
|
||||
|
||||
local ESX = exports["es_extended"]:getSharedObject()
|
||||
local metadata_key = "sky_phone_inventory"
|
||||
|
||||
ESX.AddItems({
|
||||
{ name = Config.Phone.Item, label = Config.Phone.DeviceName, weight = 100, rare = false, canRemove = true },
|
||||
{ name = Config.Sim.RegisteredItem, label = "Registered SIM", weight = 5, rare = false, canRemove = true },
|
||||
{ name = Config.Sim.AnonymousItem, label = "Anonymous SIM", weight = 5, rare = false, canRemove = true },
|
||||
})
|
||||
|
||||
local function get_player(source)
|
||||
return ESX.GetPlayerFromId(source)
|
||||
end
|
||||
|
||||
local function get_item(source, item_name)
|
||||
local player = get_player(source)
|
||||
local item = player and player.getInventoryItem(item_name) or nil
|
||||
if not item or (tonumber(item.count) or 0) < 1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local player_metadata = player.getMeta()
|
||||
local all_metadata = type(player_metadata) == "table" and player_metadata[metadata_key] or nil
|
||||
local metadata = type(all_metadata) == "table" and all_metadata[item_name] or nil
|
||||
return {
|
||||
name = item_name,
|
||||
slot = item_name,
|
||||
count = 1,
|
||||
amount = 1,
|
||||
metadata = type(metadata) == "table" and metadata or {},
|
||||
}
|
||||
end
|
||||
|
||||
local function set_item_metadata(source, item_name, metadata)
|
||||
local player = get_player(source)
|
||||
if not player or not get_item(source, item_name) then
|
||||
return false
|
||||
end
|
||||
|
||||
local player_metadata = player.getMeta()
|
||||
local all_metadata = type(player_metadata) == "table" and player_metadata[metadata_key] or nil
|
||||
all_metadata = type(all_metadata) == "table" and all_metadata or {}
|
||||
all_metadata[item_name] = metadata or {}
|
||||
player.setMeta(metadata_key, all_metadata)
|
||||
return true
|
||||
end
|
||||
|
||||
local function clear_item_metadata(player, item_name)
|
||||
local player_metadata = player.getMeta()
|
||||
local all_metadata = type(player_metadata) == "table" and player_metadata[metadata_key] or nil
|
||||
if type(all_metadata) ~= "table" or all_metadata[item_name] == nil then
|
||||
return
|
||||
end
|
||||
|
||||
all_metadata[item_name] = nil
|
||||
player.setMeta(metadata_key, all_metadata)
|
||||
end
|
||||
|
||||
function Bridge.Inventory.GetResourceName()
|
||||
return "smx-inventory"
|
||||
end
|
||||
|
||||
function Bridge.Inventory.GetSlot(source, slot_id)
|
||||
return get_item(source, tostring(slot_id))
|
||||
end
|
||||
|
||||
function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata)
|
||||
local item = get_item(source, item_name)
|
||||
if not item or not Bridge.Inventory.MetadataMatches(item.metadata, metadata) then
|
||||
return {}
|
||||
end
|
||||
return { item }
|
||||
end
|
||||
|
||||
function Bridge.Inventory.SetSlotMetadata(source, slot_id, metadata)
|
||||
return set_item_metadata(source, tostring(slot_id), metadata)
|
||||
end
|
||||
|
||||
function Bridge.Inventory.CanCarryItem(source, item_name, count)
|
||||
local player = get_player(source)
|
||||
return player and player.canCarryItem(item_name, count) or false
|
||||
end
|
||||
|
||||
function Bridge.Inventory.AddItem(source, item_name, count, _, metadata)
|
||||
local player = get_player(source)
|
||||
if not player or not player.canCarryItem(item_name, count) then
|
||||
return false
|
||||
end
|
||||
|
||||
local before = player.getInventoryItem(item_name)
|
||||
local before_count = before and tonumber(before.count) or 0
|
||||
player.addInventoryItem(item_name, count)
|
||||
local after = player.getInventoryItem(item_name)
|
||||
local added = (after and tonumber(after.count) or 0) - before_count
|
||||
if added ~= count then
|
||||
return false
|
||||
end
|
||||
|
||||
if metadata and not set_item_metadata(source, item_name, metadata) then
|
||||
player.removeInventoryItem(item_name, count)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Bridge.Inventory.RemoveItem(source, item_name, count)
|
||||
local player = get_player(source)
|
||||
local before = player and player.getInventoryItem(item_name) or nil
|
||||
local before_count = before and tonumber(before.count) or 0
|
||||
if not player or before_count < count then
|
||||
return 0
|
||||
end
|
||||
|
||||
player.removeInventoryItem(item_name, count)
|
||||
local after = player.getInventoryItem(item_name)
|
||||
local removed = before_count - (after and tonumber(after.count) or 0)
|
||||
if removed == count then
|
||||
clear_item_metadata(player, item_name)
|
||||
end
|
||||
return removed
|
||||
end
|
||||
|
||||
function Bridge.Inventory.RegisterUsableItem(item_name, callback)
|
||||
ESX.RegisterUsableItem(item_name, function(source)
|
||||
local item = get_item(source, item_name)
|
||||
if not item then
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
"[sky_phone] smx-inventory usable callback could not resolve item '%s' for source %s.",
|
||||
item_name,
|
||||
tostring(source)
|
||||
)
|
||||
return
|
||||
end
|
||||
callback(source, item)
|
||||
end)
|
||||
return true
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
local is_open = false
|
||||
local open_requested = false
|
||||
local notification_focus = false
|
||||
local device_payload = nil
|
||||
local sim_picker_open = false
|
||||
@@ -96,13 +97,11 @@ local function open_phone()
|
||||
return
|
||||
end
|
||||
|
||||
is_open = true
|
||||
notification_focus = false
|
||||
SetNuiFocus(true, true)
|
||||
send_open_message()
|
||||
end
|
||||
|
||||
local function close_phone()
|
||||
open_requested = false
|
||||
if not is_open then
|
||||
return
|
||||
end
|
||||
@@ -149,13 +148,31 @@ if Config.Phone.DevelopmentCommand then
|
||||
end
|
||||
|
||||
RegisterNUICallback("ui:ready", function(_, cb)
|
||||
if is_open then
|
||||
Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true })
|
||||
if open_requested and device_payload then
|
||||
send_open_message()
|
||||
end
|
||||
|
||||
cb({ success = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback("ui:opened", function(_, cb)
|
||||
if not open_requested or not device_payload then
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
"[sky_phone] Ignored a NUI open confirmation without a pending device open.",
|
||||
{ always = true }
|
||||
)
|
||||
cb({ success = false, error = "open_not_requested" })
|
||||
return
|
||||
end
|
||||
|
||||
is_open = true
|
||||
notification_focus = false
|
||||
SetNuiFocus(true, true)
|
||||
cb({ success = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback("close", function(_, cb)
|
||||
close_phone()
|
||||
cb({ success = true })
|
||||
@@ -257,6 +274,7 @@ RegisterNetEvent("sky_phone:device:open", function(data)
|
||||
{ always = true }
|
||||
)
|
||||
device_payload = data
|
||||
open_requested = true
|
||||
open_phone()
|
||||
end)
|
||||
|
||||
@@ -266,6 +284,7 @@ RegisterNetEvent("sky_phone:device:updated", function(data)
|
||||
end)
|
||||
|
||||
RegisterNetEvent("sky_phone:device:invalidated", function()
|
||||
open_requested = false
|
||||
device_payload = nil
|
||||
close_phone()
|
||||
end)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sky Phone</title>
|
||||
<script type="module" crossorigin src="./assets/sky-index-Dj6qNtrv.js"></script>
|
||||
<script type="module" crossorigin src="./assets/sky-index-C2xxN8xi.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-DnowIFpD.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -72,6 +72,12 @@ end
|
||||
|
||||
local function hydrate_posts(rows)
|
||||
for _, post in ipairs(rows) do
|
||||
local created_at = tonumber(post.created_at_unix)
|
||||
if not created_at then
|
||||
error("[sky_phone] Local Pages post has an invalid created_at timestamp.")
|
||||
end
|
||||
post.created_at = created_at * 1000
|
||||
post.created_at_unix = nil
|
||||
post.images = load_images(post.id)
|
||||
post.like_count = tonumber(post.like_count) or 0
|
||||
post.is_liked = tonumber(post.is_liked) or 0
|
||||
@@ -88,7 +94,7 @@ local function list_posts(account_id, where_clause, values, limit, offset)
|
||||
parameters[#parameters + 1] = offset
|
||||
return hydrate_posts(Bridge.Database.Query(([[
|
||||
SELECT p.`id`, p.`title`, p.`body`, p.`category`, p.`district`, p.`source_type`,
|
||||
p.`citymarkt_listing_id`, p.`created_at`,
|
||||
p.`citymarkt_listing_id`, UNIX_TIMESTAMP(p.`created_at`) AS `created_at_unix`,
|
||||
SUBSTRING_INDEX(a.`email`, '@', 1) AS `author_name`,
|
||||
(p.`account_id` = ?) AS `is_owner`,
|
||||
EXISTS(SELECT 1 FROM `sky_phone_pages_reactions` r WHERE r.`post_id` = p.`id`
|
||||
|
||||
Reference in New Issue
Block a user