mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-30 17:59:02 +00:00
ADD - build Housing phone app
This commit is contained in:
+38
-8
@@ -201,6 +201,7 @@ const passcodeVisible = ref(false)
|
||||
const pendingUnlockRoute = ref<string | null>(null)
|
||||
const unlockedServicesLoaded = ref(false)
|
||||
const controlCenterOpened = ref(false)
|
||||
const activitySuspended = ref(false)
|
||||
const simPicker = ref<SimPickerPayload | null>(null)
|
||||
const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const viewportScale = ref(getViewportScale())
|
||||
@@ -215,6 +216,10 @@ const phoneResolutionStyle = computed<CSSProperties>(() => ({
|
||||
'--phone-zoom':
|
||||
phoneBaseZoom.value * (phone.preferences.settings.phoneScale / 100),
|
||||
}))
|
||||
const phoneStageStyle = computed<CSSProperties>(() => ({
|
||||
...phoneResolutionStyle.value,
|
||||
visibility: activitySuspended.value ? 'hidden' : 'visible',
|
||||
}))
|
||||
const phoneDisplayStyle = computed<CSSProperties>(() => ({
|
||||
'--phone-display-dim': String(
|
||||
((100 - phone.preferences.settings.screenBrightness) / 100) * 0.8,
|
||||
@@ -226,6 +231,7 @@ const phoneFrameImage = computed(
|
||||
let clockTicker: ReturnType<typeof setInterval> | undefined
|
||||
let unlockTimer: number | undefined
|
||||
let passcodeLockTimer: number | undefined
|
||||
let unlockedServicesFrame: number | undefined
|
||||
|
||||
function getViewportScale(): number {
|
||||
const heightScale = window.innerHeight / REFERENCE_VIEWPORT_HEIGHT
|
||||
@@ -248,12 +254,23 @@ function hydratePhone(payload: PhoneOpenPayload): void {
|
||||
function loadUnlockedPhoneData(): void {
|
||||
if (unlockedServicesLoaded.value) return
|
||||
unlockedServicesLoaded.value = true
|
||||
void mail.bootstrap(account.email)
|
||||
if (account.email) void marketplace.loadCounts()
|
||||
else marketplace.setCounts({ active: 0, unread: 0 })
|
||||
void calls.bootstrap()
|
||||
void messages.loadConversations()
|
||||
if (account.email) void darkchat.bootstrap()
|
||||
|
||||
// Let the lock screen/home screen paint before background app requests compete
|
||||
// with the first visible NUI frame.
|
||||
unlockedServicesFrame = window.requestAnimationFrame(() => {
|
||||
unlockedServicesFrame = undefined
|
||||
if (!phone.isOpen || isLocked.value) {
|
||||
unlockedServicesLoaded.value = false
|
||||
return
|
||||
}
|
||||
|
||||
void mail.bootstrap(account.email)
|
||||
if (account.email) void marketplace.loadCounts()
|
||||
else marketplace.setCounts({ active: 0, unread: 0 })
|
||||
void calls.bootstrap()
|
||||
void messages.loadConversations()
|
||||
if (account.email) void darkchat.bootstrap()
|
||||
})
|
||||
}
|
||||
|
||||
async function hydrateDevelopmentPhone(): Promise<void> {
|
||||
@@ -288,7 +305,12 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
} else if (event.data?.type === 'device:updated') {
|
||||
hydratePhone(event.data.data as PhoneOpenPayload)
|
||||
} else if (event.data?.type === 'app:close') {
|
||||
activitySuspended.value = false
|
||||
phone.close()
|
||||
} else if (event.data?.type === 'app:suspend') {
|
||||
activitySuspended.value = true
|
||||
} else if (event.data?.type === 'app:resume') {
|
||||
activitySuspended.value = false
|
||||
} else if (event.data?.type === 'notification:show' && event.data.data) {
|
||||
notifications.show(event.data.data as PhoneNotificationInput)
|
||||
} else if (event.data?.type === 'mail:changed' && event.data.data) {
|
||||
@@ -560,7 +582,7 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent): void {
|
||||
if (event.key !== 'Escape' || !phone.isOpen) return
|
||||
if (event.key !== 'Escape' || !phone.isOpen || activitySuspended.value) return
|
||||
if (controlCenterOpened.value) {
|
||||
controlCenterOpened.value = false
|
||||
return
|
||||
@@ -746,6 +768,11 @@ watch(
|
||||
(isOpen) => {
|
||||
if (unlockTimer !== undefined) window.clearTimeout(unlockTimer)
|
||||
if (!isOpen) {
|
||||
if (unlockedServicesFrame !== undefined) {
|
||||
window.cancelAnimationFrame(unlockedServicesFrame)
|
||||
unlockedServicesFrame = undefined
|
||||
}
|
||||
activitySuspended.value = false
|
||||
weather.stop()
|
||||
controlCenterOpened.value = false
|
||||
isLocked.value = false
|
||||
@@ -790,6 +817,9 @@ watch(
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (unlockedServicesFrame !== undefined) {
|
||||
window.cancelAnimationFrame(unlockedServicesFrame)
|
||||
}
|
||||
weather.stop()
|
||||
if (clockTicker) clearInterval(clockTicker)
|
||||
if (unlockTimer !== undefined) window.clearTimeout(unlockTimer)
|
||||
@@ -824,7 +854,7 @@ onBeforeUnmount(() => {
|
||||
'phone-stage--landscape': phone.cameraLandscape,
|
||||
'phone-stage--peek': notifications.isPeeking,
|
||||
}"
|
||||
:style="phoneResolutionStyle"
|
||||
:style="phoneStageStyle"
|
||||
>
|
||||
<div class="phone-device-row">
|
||||
<NotificationPhonePreview
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="House">
|
||||
<defs>
|
||||
<linearGradient id="house-bg" x1="18" y1="12" x2="108" y2="118" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#ffb84d"/>
|
||||
<stop offset="1" stop-color="#f06b35"/>
|
||||
</linearGradient>
|
||||
<filter id="house-shadow" x="-20%" y="-20%" width="140%" height="150%">
|
||||
<feDropShadow dx="0" dy="5" stdDeviation="5" flood-color="#8f351c" flood-opacity=".28"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="128" height="128" rx="29" fill="url(#house-bg)"/>
|
||||
<circle cx="101" cy="25" r="23" fill="#fff" opacity=".13"/>
|
||||
<path d="M27 61.5 64 31l37 30.5v36a9 9 0 0 1-9 9H36a9 9 0 0 1-9-9z" fill="#fff" filter="url(#house-shadow)"/>
|
||||
<path d="M19 64.5 64 27l45 37.5" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="11"/>
|
||||
<rect x="53" y="73" width="22" height="33.5" rx="5" fill="#f4823c"/>
|
||||
<circle cx="69" cy="90" r="2.4" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 993 B |
@@ -2847,6 +2847,8 @@ button {
|
||||
}
|
||||
|
||||
.banking-balance {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 22px 20px 20px;
|
||||
border-radius: 27px;
|
||||
}
|
||||
@@ -2895,11 +2897,13 @@ button {
|
||||
}
|
||||
|
||||
.banking-action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 64px;
|
||||
overflow: hidden;
|
||||
padding: 11px 12px;
|
||||
border-radius: 19px;
|
||||
color: rgb(237 243 255 / 82%);
|
||||
@@ -3047,8 +3051,9 @@ button {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
color: rgb(218 229 251 / 50%);
|
||||
font-size: 8px;
|
||||
color: rgb(218 229 251 / 72%);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.banking-chart-legend span {
|
||||
@@ -3151,8 +3156,8 @@ button {
|
||||
.banking-sheet__content > p {
|
||||
margin: 0 0 15px;
|
||||
color: rgb(213 225 248 / 52%);
|
||||
font-size: 10px;
|
||||
line-height: 1.45;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.banking-form-list {
|
||||
@@ -3167,7 +3172,9 @@ button {
|
||||
display: block;
|
||||
margin: 0 4px 6px;
|
||||
color: rgb(213 225 248 / 52%);
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.banking-contact-picker > div {
|
||||
@@ -3179,13 +3186,15 @@ button {
|
||||
.banking-contact-picker > p {
|
||||
margin: 0 4px;
|
||||
color: rgb(213 225 248 / 42%);
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.banking-sheet__content .banking-form-error {
|
||||
margin: 10px 3px 0;
|
||||
color: #ff8b98;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.banking-sheet__content > button:last-child {
|
||||
@@ -3325,21 +3334,18 @@ button {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.weather-hourly {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
overflow-x: auto;
|
||||
padding: 11px 9px 13px;
|
||||
scrollbar-width: none;
|
||||
scroll-snap-type: x proximity;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 2px;
|
||||
padding: 11px 6px 13px;
|
||||
}
|
||||
.weather-hour {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 52px;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 104px;
|
||||
padding: 3px 2px;
|
||||
scroll-snap-align: start;
|
||||
padding: 3px 1px;
|
||||
}
|
||||
.weather-hour span {
|
||||
height: 21px;
|
||||
|
||||
@@ -14,8 +14,12 @@ const canGoHome = computed(
|
||||
function goHome(): void {
|
||||
if (!canGoHome.value) return
|
||||
|
||||
if (route.name !== 'home') {
|
||||
void router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
phone.setCurrentPage(HOME_PAGE)
|
||||
if (route.name !== 'home') void router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -25,7 +29,6 @@ function goHome(): void {
|
||||
:class="{ 'phone-home-indicator--interactive': canGoHome }"
|
||||
type="button"
|
||||
:aria-label="phone.t('Common.home')"
|
||||
@pointerdown.stop="goHome"
|
||||
@click.stop="goHome"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
|
||||
@@ -47,6 +47,11 @@ describe('app registry', () => {
|
||||
labelKey: 'Apps.skyride.name',
|
||||
route: '/apps/skyride',
|
||||
})
|
||||
expect(PHONE_APPS.find((app) => app.id === 'house')).toMatchObject({
|
||||
category: 'utilities',
|
||||
labelKey: 'Apps.house.name',
|
||||
route: '/apps/house',
|
||||
})
|
||||
expect(PHONE_APPS.find((app) => app.id === 'calendar')).toMatchObject({
|
||||
gridOrder: 21,
|
||||
labelKey: 'Apps.calendar.name',
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Tag,
|
||||
MapPinHouse,
|
||||
Flame,
|
||||
House,
|
||||
} from 'lucide-vue-next'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
@@ -54,6 +55,7 @@ import neonDropIcon from '@/assets/img/app-icons/neon-drop.webp'
|
||||
import weatherIcon from '@/assets/img/app-icons/weather.webp'
|
||||
import bankingIcon from '@/assets/img/app-icons/banking.webp'
|
||||
import garageIcon from '@/assets/img/app-icons/garage.webp'
|
||||
import houseIcon from '@/assets/img/app-icons/house.svg'
|
||||
import citymarktIcon from '@/assets/img/app-icons/citymarkt.webp'
|
||||
import localPagesIcon from '@/assets/img/app-icons/local-pages.webp'
|
||||
import flareIcon from '@/assets/img/app-icons/flare.svg'
|
||||
@@ -207,6 +209,20 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
labelKey: 'Apps.garage.name',
|
||||
route: '/apps/garage',
|
||||
},
|
||||
{
|
||||
category: 'utilities',
|
||||
component: markRaw(
|
||||
defineAsyncComponent(() => import('@/views/apps/HouseApp.vue')),
|
||||
),
|
||||
dockOrder: null,
|
||||
gridOrder: 25,
|
||||
icon: markRaw(House),
|
||||
iconClass: 'app-icon--house',
|
||||
iconImage: houseIcon,
|
||||
id: 'house',
|
||||
labelKey: 'Apps.house.name',
|
||||
route: '/apps/house',
|
||||
},
|
||||
{
|
||||
category: 'utilities',
|
||||
component: markRaw(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useHousingStore } from '@/stores/housing'
|
||||
import type { HousingOverview } from '@/types/housing'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() }))
|
||||
|
||||
const mockNuiCall = vi.mocked(nuiCall)
|
||||
const overview: HousingOverview = {
|
||||
available: true,
|
||||
provider: 'esx_property',
|
||||
properties: [
|
||||
{
|
||||
access: 'owner',
|
||||
capabilities: {
|
||||
cctv: true,
|
||||
garageStatus: true,
|
||||
keys: true,
|
||||
lock: true,
|
||||
waypoint: true,
|
||||
},
|
||||
cctv: { enabled: true },
|
||||
entrance: { x: 1, y: 2, z: 3 },
|
||||
garage: { enabled: true, storedVehicles: 2 },
|
||||
id: 'esx_property:1',
|
||||
keys: [],
|
||||
locked: true,
|
||||
name: 'Alta Street 3',
|
||||
providerId: '1',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('housing store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockNuiCall.mockReset()
|
||||
})
|
||||
|
||||
it('loads a provider-backed housing overview', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({ data: overview, success: true })
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.load()).toBe(true)
|
||||
expect(housing.overview).toEqual(overview)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('housing:overview')
|
||||
})
|
||||
|
||||
it('represents a missing provider as an available response with offline data', async () => {
|
||||
const offline = { available: false, properties: [], provider: null }
|
||||
mockNuiCall.mockResolvedValueOnce({ data: offline, success: true })
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.load()).toBe(true)
|
||||
expect(housing.overview).toEqual(offline)
|
||||
})
|
||||
|
||||
it('loads only provider-authorized key candidates', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { candidates: [{ id: 42, name: 'Alex Morgan' }] },
|
||||
success: true,
|
||||
})
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.loadKeyCandidates('esx_property:1')).toBe(true)
|
||||
expect(housing.candidates).toEqual([{ id: 42, name: 'Alex Morgan' }])
|
||||
})
|
||||
|
||||
it('refreshes the overview after a successful state mutation', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockResolvedValueOnce({ data: overview, success: true })
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.command('toggle_lock', 'esx_property:1')).toBe(true)
|
||||
expect(mockNuiCall).toHaveBeenNthCalledWith(1, 'housing:command', {
|
||||
action: 'toggle_lock',
|
||||
propertyId: 'esx_property:1',
|
||||
})
|
||||
expect(mockNuiCall).toHaveBeenNthCalledWith(2, 'housing:overview')
|
||||
})
|
||||
|
||||
it('keeps the overview and exposes a rejected action', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
error: 'property_access_denied',
|
||||
success: false,
|
||||
})
|
||||
const housing = useHousingStore()
|
||||
housing.overview = overview
|
||||
|
||||
expect(await housing.command('toggle_lock', 'esx_property:1')).toBe(false)
|
||||
expect(housing.overview).toEqual(overview)
|
||||
expect(housing.error).toBe('property_access_denied')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import type {
|
||||
HousingCommand,
|
||||
HousingKeyCandidate,
|
||||
HousingOverview,
|
||||
} from '@/types/housing'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
export const useHousingStore = defineStore('housing', {
|
||||
state: () => ({
|
||||
candidates: [] as HousingKeyCandidate[],
|
||||
error: '',
|
||||
isLoading: false,
|
||||
isLoadingCandidates: false,
|
||||
overview: null as HousingOverview | null,
|
||||
pendingAction: '',
|
||||
}),
|
||||
actions: {
|
||||
async load(): Promise<boolean> {
|
||||
this.isLoading = true
|
||||
const response = await nuiCall<HousingOverview>('housing:overview')
|
||||
this.isLoading = false
|
||||
if (response.success && response.data) {
|
||||
this.overview = response.data
|
||||
this.error = ''
|
||||
return true
|
||||
}
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
},
|
||||
async loadKeyCandidates(propertyId: string): Promise<boolean> {
|
||||
this.isLoadingCandidates = true
|
||||
this.candidates = []
|
||||
const response = await nuiCall<{ candidates: HousingKeyCandidate[] }>(
|
||||
'housing:key-candidates',
|
||||
{ propertyId },
|
||||
)
|
||||
this.isLoadingCandidates = false
|
||||
if (response.success && response.data) {
|
||||
this.candidates = response.data.candidates
|
||||
this.error = ''
|
||||
return true
|
||||
}
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
},
|
||||
async command(
|
||||
action: HousingCommand,
|
||||
propertyId: string,
|
||||
payload: Record<string, unknown> = {},
|
||||
): Promise<boolean> {
|
||||
this.pendingAction = `${action}:${propertyId}`
|
||||
const response = await nuiCall('housing:command', {
|
||||
action,
|
||||
propertyId,
|
||||
...payload,
|
||||
})
|
||||
this.pendingAction = ''
|
||||
if (!response.success) {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
}
|
||||
this.error = ''
|
||||
if (
|
||||
action === 'toggle_lock' ||
|
||||
action === 'grant_key' ||
|
||||
action === 'revoke_key'
|
||||
) {
|
||||
await this.load()
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1233,6 +1233,88 @@ const defaultLocales: LocaleTree = {
|
||||
default: 'SkyRide could not complete the request.',
|
||||
},
|
||||
},
|
||||
house: {
|
||||
name: 'House',
|
||||
subtitle: 'Homes and access',
|
||||
myHomes: 'My Homes',
|
||||
properties: 'Properties',
|
||||
owned: 'Owned',
|
||||
shared: 'Shared with you',
|
||||
locked: 'Locked',
|
||||
unlocked: 'Unlocked',
|
||||
owner: 'Owner',
|
||||
keyholder: 'Key holder',
|
||||
openDetails: 'Open property details',
|
||||
tryAgain: 'Try Again',
|
||||
refresh: 'Refresh',
|
||||
offline: 'House is offline',
|
||||
offlineBody:
|
||||
'Start a supported housing resource or review Config.Housing.',
|
||||
unavailable: 'Homes unavailable',
|
||||
empty: 'No Homes',
|
||||
emptyBody: 'Owned homes and homes shared with you will appear here.',
|
||||
provider: 'Housing data · {system}',
|
||||
access: 'Access',
|
||||
status: 'Status',
|
||||
actions: 'Controls',
|
||||
setWaypoint: 'Directions',
|
||||
viewCamera: 'View Camera',
|
||||
garage: 'Garage',
|
||||
garageEnabled: 'Enabled',
|
||||
garageDisabled: 'Unavailable',
|
||||
storedVehicles: '{count} stored',
|
||||
camera: 'Camera',
|
||||
cameraAvailable: 'Ready',
|
||||
cameraUnavailable: 'Unavailable',
|
||||
keys: 'Keys',
|
||||
manageKeys: 'Manage Keys',
|
||||
addKey: 'Add Key',
|
||||
revokeKey: 'Revoke',
|
||||
noKeys: 'No shared keys',
|
||||
noKeysBody: 'Only you currently have access to this home.',
|
||||
chooseResident: 'Give a Key',
|
||||
chooseResidentBody: 'Choose a person currently inside this property.',
|
||||
noCandidates: 'Nobody is waiting inside',
|
||||
noCandidatesBody:
|
||||
'A person must be inside the property before you can give them a key.',
|
||||
revokeTitle: 'Revoke Key?',
|
||||
revokeBody: '{name} will lose access to this property.',
|
||||
offlineKey: 'Offline',
|
||||
onlineKey: 'Online',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
lockSuccess: 'Door status updated.',
|
||||
waypointSuccess: 'Route set on your map.',
|
||||
keyGranted: 'Key granted.',
|
||||
keyRevoked: 'Key revoked.',
|
||||
cameraStarting: 'Opening camera…',
|
||||
errors: {
|
||||
provider_unavailable: 'The housing provider is unavailable.',
|
||||
provider_error: 'The housing provider could not be reached.',
|
||||
housing_unavailable: 'Housing data is unavailable for this character.',
|
||||
property_not_found: 'This property no longer exists.',
|
||||
property_access_denied: 'You no longer have access to this property.',
|
||||
owner_required: 'Only the property owner can do that.',
|
||||
invalid_property: 'Select a valid property.',
|
||||
invalid_action: 'This housing action is unavailable.',
|
||||
capability_unavailable:
|
||||
'This property provider does not support that control.',
|
||||
invalid_target: 'Select a valid resident.',
|
||||
target_not_in_property: 'That person is no longer inside the property.',
|
||||
key_not_found: 'That key no longer exists.',
|
||||
key_already_exists: 'That person already has access to this property.',
|
||||
keyholder_offline:
|
||||
'Offline keys must be revoked through the property menu.',
|
||||
cctv_unavailable: 'The camera is unavailable for this property.',
|
||||
provider_rejected: 'The housing provider rejected the request.',
|
||||
action_failed: 'The property change could not be saved.',
|
||||
rate_limited: 'Please wait before using another housing control.',
|
||||
device_not_open: 'Open the phone again to manage your homes.',
|
||||
device_locked: 'Unlock the phone to manage your homes.',
|
||||
request_failed: 'The housing request failed.',
|
||||
default: 'The housing request failed.',
|
||||
},
|
||||
},
|
||||
calculator: { name: 'Calculator' },
|
||||
snake: {
|
||||
name: 'Snake',
|
||||
|
||||
@@ -11,6 +11,7 @@ export type PhoneAppId =
|
||||
| 'weather'
|
||||
| 'banking'
|
||||
| 'garage'
|
||||
| 'house'
|
||||
| 'mail'
|
||||
| 'map'
|
||||
| 'notes'
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
export type HousingAccess = 'owner' | 'keyholder'
|
||||
|
||||
export type HousingCapabilities = {
|
||||
cctv: boolean
|
||||
garageStatus: boolean
|
||||
keys: boolean
|
||||
lock: boolean
|
||||
waypoint: boolean
|
||||
}
|
||||
|
||||
export type HousingKey = {
|
||||
identifier: string
|
||||
name: string
|
||||
online: boolean
|
||||
revocable?: boolean
|
||||
}
|
||||
|
||||
export type HousingKeyCandidate = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type HousingProperty = {
|
||||
access: HousingAccess
|
||||
capabilities: HousingCapabilities
|
||||
cctv: { enabled: boolean }
|
||||
entrance: { x: number; y: number; z: number }
|
||||
garage: { enabled: boolean; storedVehicles: number } | null
|
||||
id: string
|
||||
keys?: HousingKey[]
|
||||
locked: boolean
|
||||
name: string
|
||||
providerId: string
|
||||
}
|
||||
|
||||
export type HousingOverview = {
|
||||
available: boolean
|
||||
properties: HousingProperty[]
|
||||
provider: string | null
|
||||
}
|
||||
|
||||
export type HousingCommand =
|
||||
| 'grant_key'
|
||||
| 'open_cctv'
|
||||
| 'revoke_key'
|
||||
| 'set_waypoint'
|
||||
| 'toggle_lock'
|
||||
@@ -90,6 +90,7 @@ const DEFAULT_APP_NOTIFICATIONS: Record<
|
||||
banking: { enabled: true, sounds: true },
|
||||
garage: { enabled: true, sounds: true },
|
||||
skyride: { enabled: true, sounds: true },
|
||||
house: { enabled: true, sounds: true },
|
||||
mail: { enabled: true, sounds: true },
|
||||
map: { enabled: true, sounds: true },
|
||||
notes: { enabled: true, sounds: true },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -617,6 +617,48 @@ function skyRideUpdate(fields) {
|
||||
)
|
||||
}
|
||||
|
||||
const mockHousingOverview = {
|
||||
available: true,
|
||||
provider: 'esx_property',
|
||||
properties: [
|
||||
{
|
||||
id: 'esx_property:1',
|
||||
providerId: '1',
|
||||
name: 'Alta Street Apartment',
|
||||
access: 'owner',
|
||||
locked: true,
|
||||
entrance: { x: -268.9, y: -962.3, z: 31.2 },
|
||||
capabilities: {
|
||||
lock: true,
|
||||
keys: true,
|
||||
waypoint: true,
|
||||
cctv: true,
|
||||
garageStatus: true,
|
||||
},
|
||||
cctv: { enabled: true },
|
||||
garage: { enabled: true, storedVehicles: 2 },
|
||||
keys: [{ identifier: 'char2:mock', name: 'Jamie Rivera', online: true }],
|
||||
},
|
||||
{
|
||||
id: 'esx_property:2',
|
||||
providerId: '2',
|
||||
name: 'Vespucci Beach House',
|
||||
access: 'keyholder',
|
||||
locked: false,
|
||||
entrance: { x: -1150.1, y: -1520.8, z: 10.6 },
|
||||
capabilities: {
|
||||
lock: true,
|
||||
keys: false,
|
||||
waypoint: true,
|
||||
cctv: false,
|
||||
garageStatus: false,
|
||||
},
|
||||
cctv: { enabled: false },
|
||||
garage: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let contactSequence = 2
|
||||
const contacts = [
|
||||
{
|
||||
@@ -2859,6 +2901,31 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'housing:overview') {
|
||||
response.json({ success: true, data: mockHousingOverview })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'housing:key-candidates') {
|
||||
response.json({
|
||||
success: true,
|
||||
data: { candidates: [{ id: 27, name: 'Alex Morgan' }] },
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'housing:command') {
|
||||
const property = mockHousingOverview.properties.find(
|
||||
(item) => item.id === request.body.propertyId,
|
||||
)
|
||||
if (!property) {
|
||||
response.json({ success: false, error: 'property_not_found' })
|
||||
return
|
||||
}
|
||||
if (request.body.action === 'toggle_lock') {
|
||||
property.locked = !property.locked
|
||||
}
|
||||
response.json({ success: true, data: { accepted: true } })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'garage:valet-state') {
|
||||
response.json({ success: true, data: mockGarageValet })
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user