mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b65710bef0 |
@@ -89,7 +89,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
|
|||||||
| **Inventories** | ak47_inventory, codem-inventory, core_inventory, jaksam_inventory, jpr-inventory, lj-inventory, mf-inventory, one_inventory, origen_inventory, ox_inventory, ps-inventory, qb-inventory, qs-inventory, smx-inventory, tgiann-inventory, hex_4_inventory, and native ESX inventory |
|
| **Inventories** | ak47_inventory, codem-inventory, core_inventory, jaksam_inventory, jpr-inventory, lj-inventory, mf-inventory, one_inventory, origen_inventory, ox_inventory, ps-inventory, qb-inventory, qs-inventory, smx-inventory, tgiann-inventory, hex_4_inventory, and native ESX inventory |
|
||||||
| **Calls** | YACA, PMA Voice, SaltyChat |
|
| **Calls** | YACA, PMA Voice, SaltyChat |
|
||||||
| **Radio** | YACA, PMA Voice, SaltyChat |
|
| **Radio** | YACA, PMA Voice, SaltyChat |
|
||||||
| **Housing** | RTX Housing, Quasar Housing, TGIANN House, VMS Housing, RX Housing, NoLag Properties, SN Properties, ESX Property, qbx_properties |
|
| **Housing** | RTX Housing, Quasar Housing, VMS Housing, RX Housing, NoLag Properties, SN Properties, ESX Property, qbx_properties |
|
||||||
| **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge |
|
| **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge |
|
||||||
| **Custom app contracts** | Sky Phone, LB Phone, 17Movement, High Phone, Quasar Smartphone, YSeries |
|
| **Custom app contracts** | Sky Phone, LB Phone, 17Movement, High Phone, Quasar Smartphone, YSeries |
|
||||||
| **Languages** | English, German |
|
| **Languages** | English, German |
|
||||||
@@ -650,7 +650,7 @@ Select the provider under `Config.Garage.System`. Vehicle images use the configu
|
|||||||
|
|
||||||
### Housing
|
### Housing
|
||||||
|
|
||||||
Select `rtx`, `quasar`, `tgiann`, `vms`, `rx`, `nolag`, `sn`, `esx_property`, or `qbx_properties` under `Config.Housing.System`. Automatic mode uses `Config.Housing.AutoPriority` and keeps the existing `esx_property` and `qbx_properties` defaults ahead of newly supported providers. Select a provider explicitly when multiple housing resources are running. Each bridge exposes only the capabilities supported by the documented provider API. The TGIANN bridge expects `tgiann-core` to start before `tgiann-house`, lists server-authorized owner properties, and supports entrance waypoints; TGIANN does not publish stable external contracts for keyholders, lock controls, CCTV, or live garage status.
|
Select `rtx`, `quasar`, `vms`, `rx`, `nolag`, `sn`, `esx_property`, or `qbx_properties` under `Config.Housing.System`. Automatic mode uses `Config.Housing.AutoPriority` and keeps the existing `esx_property` and `qbx_properties` defaults ahead of newly supported providers. Select a provider explicitly when multiple housing resources are running. Each bridge exposes only the capabilities supported by the documented provider API.
|
||||||
|
|
||||||
### Companies
|
### Companies
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -86,7 +86,6 @@ import { parsePhonePreferences } from '@/utils/preferences'
|
|||||||
import { getHairlinePixelStyle } from '@/utils/rendering'
|
import { getHairlinePixelStyle } from '@/utils/rendering'
|
||||||
import { isTextInputElement } from '@/utils/textInputFocus'
|
import { isTextInputElement } from '@/utils/textInputFocus'
|
||||||
import { configurePhoneNumberFormat } from '@/utils/phone'
|
import { configurePhoneNumberFormat } from '@/utils/phone'
|
||||||
import { consumeEscape } from '@/utils/keyboard'
|
|
||||||
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
|
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
|
||||||
import SpringboardView from '@/views/SpringboardView.vue'
|
import SpringboardView from '@/views/SpringboardView.vue'
|
||||||
|
|
||||||
@@ -1316,17 +1315,20 @@ async function closePhone(): Promise<void> {
|
|||||||
function onKeydown(event: KeyboardEvent): void {
|
function onKeydown(event: KeyboardEvent): void {
|
||||||
if (event.key !== 'Escape') return
|
if (event.key !== 'Escape') return
|
||||||
if (simPicker.value) {
|
if (simPicker.value) {
|
||||||
if (!consumeEscape(event)) return
|
event.preventDefault()
|
||||||
void closeSimPicker()
|
void closeSimPicker()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!phone.isOpen || activitySuspended.value || !consumeEscape(event)) return
|
queueMicrotask(() => {
|
||||||
if (controlCenterOpened.value) {
|
if (event.defaultPrevented || !phone.isOpen || activitySuspended.value)
|
||||||
controlCenterOpened.value = false
|
return
|
||||||
return
|
if (controlCenterOpened.value) {
|
||||||
}
|
controlCenterOpened.value = false
|
||||||
void closePhone()
|
return
|
||||||
|
}
|
||||||
|
void closePhone()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSystemColorSchemeChange(event: MediaQueryListEvent): void {
|
function onSystemColorSchemeChange(event: MediaQueryListEvent): void {
|
||||||
|
|||||||
@@ -70,31 +70,6 @@ describe('browser development preview contract', () => {
|
|||||||
expect(source).toContain(':device-pixel-ratio="browserDevicePixelRatio"')
|
expect(source).toContain(':device-pixel-ratio="browserDevicePixelRatio"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('clips composited app and overlay layers to the curved display', () => {
|
|
||||||
expect(mainCss).toMatch(
|
|
||||||
/\.phone-screen\s*\{[^}]*--phone-screen-radius:\s*40px;[^}]*overflow:\s*hidden;[^}]*border-radius:\s*var\(--phone-screen-radius\);[^}]*clip-path:\s*inset\(0 round var\(--phone-screen-radius\)\);/s,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('replaces the CEF button focus rectangle around the home indicator', () => {
|
|
||||||
expect(mainCss).toMatch(
|
|
||||||
/\.phone-home-indicator:focus\s*\{[^}]*outline:\s*none;/s,
|
|
||||||
)
|
|
||||||
expect(mainCss).toMatch(
|
|
||||||
/\.phone-home-indicator:focus-visible span\s*\{[^}]*0 0 0 2px #0a84ff,/s,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('consumes Escape synchronously before FiveM can open the pause menu', () => {
|
|
||||||
expect(source).toContain("import { consumeEscape } from '@/utils/keyboard'")
|
|
||||||
expect(source).toContain(
|
|
||||||
'if (!phone.isOpen || activitySuspended.value || !consumeEscape(event)) return',
|
|
||||||
)
|
|
||||||
expect(source).not.toMatch(
|
|
||||||
/function onKeydown\(event: KeyboardEvent\): void \{[\s\S]*?queueMicrotask/,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('maps the visible device side controls to phone actions', () => {
|
it('maps the visible device side controls to phone actions', () => {
|
||||||
expect(source).toContain('@click="toggleHardwareAlertMute"')
|
expect(source).toContain('@click="toggleHardwareAlertMute"')
|
||||||
expect(source).toContain('@click="changeHardwareAlertVolume(10)"')
|
expect(source).toContain('@click="changeHardwareAlertVolume(10)"')
|
||||||
|
|||||||
@@ -460,7 +460,6 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.phone-screen {
|
.phone-screen {
|
||||||
--phone-screen-radius: 40px;
|
|
||||||
--phone-screen-portrait-ratio: 2.30951;
|
--phone-screen-portrait-ratio: 2.30951;
|
||||||
position: relative;
|
position: relative;
|
||||||
container-type: size;
|
container-type: size;
|
||||||
@@ -470,8 +469,7 @@ button {
|
|||||||
height: 98%;
|
height: 98%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #08080a;
|
background: #08080a;
|
||||||
border-radius: var(--phone-screen-radius);
|
border-radius: 40px;
|
||||||
clip-path: inset(0 round var(--phone-screen-radius));
|
|
||||||
}
|
}
|
||||||
.phone-screen--camera-landscape {
|
.phone-screen--camera-landscape {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -858,14 +856,6 @@ button {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 1px 4px #0008;
|
box-shadow: 0 1px 4px #0008;
|
||||||
}
|
}
|
||||||
.phone-home-indicator:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.phone-home-indicator:focus-visible span {
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 2px #0a84ff,
|
|
||||||
0 1px 4px #0008;
|
|
||||||
}
|
|
||||||
.phone-home-indicator--interactive {
|
.phone-home-indicator--interactive {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -380,10 +380,10 @@ function toggleOptionalString(event: Event): void {
|
|||||||
function addListRow(): void {
|
function addListRow(): void {
|
||||||
const rows = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
const rows = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||||||
const index = rows.length
|
const index = rows.length
|
||||||
const value = listTemplate.value
|
const value = rows.length
|
||||||
? blankFromConfiguratorStructure(listTemplate.value)
|
? blankLike(rows[0])
|
||||||
: rows.length
|
: listTemplate.value
|
||||||
? blankLike(rows[0])
|
? blankFromConfiguratorStructure(listTemplate.value)
|
||||||
: blankValue(newArrayKind.value)
|
: blankValue(newArrayKind.value)
|
||||||
rows.push(value)
|
rows.push(value)
|
||||||
emit('update:modelValue', rows)
|
emit('update:modelValue', rows)
|
||||||
|
|||||||
@@ -440,9 +440,6 @@ describe('standalone admin panel contracts', () => {
|
|||||||
expect(source).toContain("selectConfiguratorScope('media')")
|
expect(source).toContain("selectConfiguratorScope('media')")
|
||||||
expect(source).not.toContain('class="admin-panel-config-meta"')
|
expect(source).not.toContain('class="admin-panel-config-meta"')
|
||||||
expect(configuratorValueEditor).toContain('function addListRow()')
|
expect(configuratorValueEditor).toContain('function addListRow()')
|
||||||
expect(configuratorValueEditor).toMatch(
|
|
||||||
/const value = listTemplate\.value\s+\? blankFromConfiguratorStructure\(listTemplate\.value\)\s+: rows\.length\s+\? blankLike\(rows\[0\]\)/,
|
|
||||||
)
|
|
||||||
expect(configuratorValueEditor).toContain('function addTableField()')
|
expect(configuratorValueEditor).toContain('function addTableField()')
|
||||||
expect(configuratorValueEditor).toContain(
|
expect(configuratorValueEditor).toContain(
|
||||||
'const canExtendTable = computed(',
|
'const canExtendTable = computed(',
|
||||||
|
|||||||
@@ -24,13 +24,4 @@ describe('EasyShareSheet Sky UI contract', () => {
|
|||||||
/\.easyshare-history\s*\{[^}]*overflow:\s*hidden[^}]*background:\s*var\(--easyshare-list-surface\)/s,
|
/\.easyshare-history\s*\{[^}]*overflow:\s*hidden[^}]*background:\s*var\(--easyshare-list-surface\)/s,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('only exposes and opens installed share destinations', () => {
|
|
||||||
expect(source).toContain("if (appStore.isInstalled('flare'))")
|
|
||||||
expect(source).toContain("if (appStore.isInstalled('darkchat'))")
|
|
||||||
expect(source).toContain('.filter((id) => appStore.isInstalled(id))')
|
|
||||||
expect(source).toContain('if (!appStore.isInstalled(kind)) return')
|
|
||||||
expect(source).toContain('if (!appStore.isInstalled(appId)) return')
|
|
||||||
expect(source).not.toContain('appStore.homeLayout.hidden.includes')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const sharePeople = computed(() => {
|
|||||||
}> = []
|
}> = []
|
||||||
const phoneNumbers = new Set<string>()
|
const phoneNumbers = new Set<string>()
|
||||||
|
|
||||||
if (appStore.isInstalled('flare')) {
|
if (!appStore.homeLayout.hidden.includes('flare')) {
|
||||||
for (const match of flare.matches) {
|
for (const match of flare.matches) {
|
||||||
people.push({
|
people.push({
|
||||||
avatar: match.profile.photoUrls[0],
|
avatar: match.profile.photoUrls[0],
|
||||||
@@ -102,7 +102,7 @@ const sharePeople = computed(() => {
|
|||||||
phoneNumbers.add(contact.phone_number)
|
phoneNumbers.add(contact.phone_number)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appStore.isInstalled('darkchat')) {
|
if (!appStore.homeLayout.hidden.includes('darkchat')) {
|
||||||
for (const conversation of darkChat.conversations.slice(0, 8)) {
|
for (const conversation of darkChat.conversations.slice(0, 8)) {
|
||||||
people.push({
|
people.push({
|
||||||
kind: 'darkchat',
|
kind: 'darkchat',
|
||||||
@@ -116,7 +116,7 @@ const sharePeople = computed(() => {
|
|||||||
})
|
})
|
||||||
const shareApps = computed(() =>
|
const shareApps = computed(() =>
|
||||||
(easyShare.payload ? easyShareDestinationAppIds(easyShare.payload) : [])
|
(easyShare.payload ? easyShareDestinationAppIds(easyShare.payload) : [])
|
||||||
.filter((id) => appStore.isInstalled(id))
|
.filter((id) => !appStore.homeLayout.hidden.includes(id))
|
||||||
.flatMap((id) => {
|
.flatMap((id) => {
|
||||||
const app = getPhoneApp(id)
|
const app = getPhoneApp(id)
|
||||||
return app ? [{ app, id }] : []
|
return app ? [{ app, id }] : []
|
||||||
@@ -195,21 +195,18 @@ function endDrag(event: PointerEvent): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shareToChat(kind: EasyShareChatApp, targetId: string): void {
|
function shareToChat(kind: EasyShareChatApp, targetId: string): void {
|
||||||
if (!appStore.isInstalled(kind)) return
|
|
||||||
if (!easyShare.prepareChatDraft(kind, targetId)) return
|
if (!easyShare.prepareChatDraft(kind, targetId)) return
|
||||||
close()
|
close()
|
||||||
void router.push(`/apps/${kind}`)
|
void router.push(`/apps/${kind}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openChatApp(kind: EasyShareChatApp): void {
|
function openChatApp(kind: EasyShareChatApp): void {
|
||||||
if (!appStore.isInstalled(kind)) return
|
|
||||||
if (!easyShare.prepareChatDraft(kind)) return
|
if (!easyShare.prepareChatDraft(kind)) return
|
||||||
close()
|
close()
|
||||||
void router.push(`/apps/${kind}`)
|
void router.push(`/apps/${kind}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openShareApp(appId: EasyShareDestinationApp): void {
|
function openShareApp(appId: EasyShareDestinationApp): void {
|
||||||
if (!appStore.isInstalled(appId)) return
|
|
||||||
if (appId === 'messages' || appId === 'darkchat' || appId === 'flare') {
|
if (appId === 'messages' || appId === 'darkchat' || appId === 'flare') {
|
||||||
openChatApp(appId)
|
openChatApp(appId)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { readFileSync } from 'node:fs'
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
|
|
||||||
const readResourceFile = (path: string) =>
|
|
||||||
readFileSync(
|
|
||||||
new URL(`../../sky_phone/${path}`, import.meta.url),
|
|
||||||
'utf8',
|
|
||||||
).replace(/\r\n/g, '\n')
|
|
||||||
|
|
||||||
describe('housing provider contracts', () => {
|
|
||||||
it('registers TGIANN House with server-authorized owner properties', () => {
|
|
||||||
const adapter = readResourceFile('source/bridge/server/housing/tgiann.lua')
|
|
||||||
|
|
||||||
expect(adapter).toContain('local resource_name = "tgiann-house"')
|
|
||||||
expect(adapter).toContain('FROM `tgiann_house`')
|
|
||||||
expect(adapter).toContain('WHERE `owner` = ?')
|
|
||||||
expect(adapter).toContain('Bridge.Framework.GetIdentifier(source)')
|
|
||||||
expect(adapter).toContain(
|
|
||||||
'Bridge.Housing.RegisterProvider(provider_name, {',
|
|
||||||
)
|
|
||||||
expect(adapter).toContain('return nil, "property_access_denied"')
|
|
||||||
expect(adapter).not.toContain('`houseKeys`')
|
|
||||||
expect(adapter).not.toContain('tgiann-house:getPlayerHouses')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('uses the published client export for entrance waypoints only', () => {
|
|
||||||
const adapter = readResourceFile('source/bridge/client/housing/tgiann.lua')
|
|
||||||
|
|
||||||
expect(adapter).toContain(
|
|
||||||
'return exports[resource_name]:getHouseData(house)',
|
|
||||||
)
|
|
||||||
expect(adapter).toContain(
|
|
||||||
'Bridge.Normalize.Coordinates(house_data.doorCoord)',
|
|
||||||
)
|
|
||||||
expect(adapter).toContain(
|
|
||||||
'Bridge.Housing.RegisterClientProvider(provider_name, {',
|
|
||||||
)
|
|
||||||
expect(adapter).toContain('SetNewWaypoint(')
|
|
||||||
expect(adapter).not.toContain('enterHouse(')
|
|
||||||
expect(adapter).not.toContain('forceOpenDoorHouse')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('advertises TGIANN in housing configuration and documentation', () => {
|
|
||||||
const config = readResourceFile('config/config.lua')
|
|
||||||
const readme = readFileSync(
|
|
||||||
new URL('../../README.md', import.meta.url),
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(config).toContain(
|
|
||||||
'"rtx", "quasar", "tgiann", "vms", "rx", "nolag", "sn"',
|
|
||||||
)
|
|
||||||
expect(readme).toContain('Quasar Housing, TGIANN House, VMS Housing')
|
|
||||||
expect(readme).toContain('`quasar`, `tgiann`, `vms`')
|
|
||||||
expect(readme).toContain(
|
|
||||||
'expects `tgiann-core` to start before `tgiann-house`',
|
|
||||||
)
|
|
||||||
expect(readme).toContain(
|
|
||||||
'lists server-authorized owner properties, and supports entrance waypoints',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -264,23 +264,6 @@ describe('app store', () => {
|
|||||||
expect(apps.homeLayout.hidden).not.toContain('snake')
|
expect(apps.homeLayout.hidden).not.toContain('snake')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uninstalls claimed Banking and Picstagram apps from every app state', () => {
|
|
||||||
const apps = useAppStoreStore()
|
|
||||||
apps.hydrate({ claimedApps: ['banking', 'picstagram'] })
|
|
||||||
mocks.phone.saveDeviceNamespace.mockClear()
|
|
||||||
|
|
||||||
for (const appId of ['banking', 'picstagram'] as const) {
|
|
||||||
expect(apps.isInstalled(appId)).toBe(true)
|
|
||||||
expect(apps.uninstallApp(appId)).toBe(true)
|
|
||||||
expect(apps.isInstalled(appId)).toBe(false)
|
|
||||||
expect(apps.claimedApps).not.toContain(appId)
|
|
||||||
expect(apps.uninstalledApps).toContain(appId)
|
|
||||||
expect(apps.homeLayout.hidden).toContain(appId)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(mocks.phone.saveDeviceNamespace).toHaveBeenCalledTimes(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hydrates persisted removals while rejecting protected and invalid ids', () => {
|
it('hydrates persisted removals while rejecting protected and invalid ids', () => {
|
||||||
const apps = useAppStoreStore()
|
const apps = useAppStoreStore()
|
||||||
|
|
||||||
|
|||||||
@@ -3027,7 +3027,6 @@ const defaultLocales: LocaleTree = {
|
|||||||
uninstallTitle: 'Uninstall this app?',
|
uninstallTitle: 'Uninstall this app?',
|
||||||
uninstallBody:
|
uninstallBody:
|
||||||
'{app} will be removed from this phone. You can download it again from the App Store.',
|
'{app} will be removed from this phone. You can download it again from the App Store.',
|
||||||
uninstallFailed: 'The app could not be uninstalled. Please try again.',
|
|
||||||
},
|
},
|
||||||
details: {
|
details: {
|
||||||
skyStudios: 'Sky Studios',
|
skyStudios: 'Sky Studios',
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import type { AdminConfiguratorStructure } from '@/types/admin'
|
import type { AdminConfiguratorStructure } from '@/types/admin'
|
||||||
import {
|
import { createMutableTableEntry } from '@/utils/adminConfiguratorDefaults'
|
||||||
blankFromConfiguratorStructure,
|
|
||||||
createMutableTableEntry,
|
|
||||||
} from '@/utils/adminConfiguratorDefaults'
|
|
||||||
|
|
||||||
describe('admin configurator defaults', () => {
|
describe('admin configurator defaults', () => {
|
||||||
it('creates a usable company draft from its key and the server defaults', () => {
|
it('creates a usable company draft from its key and the server defaults', () => {
|
||||||
@@ -114,33 +111,4 @@ describe('admin configurator defaults', () => {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('preserves required nested list fields in schema-derived rows', () => {
|
|
||||||
const structure: AdminConfiguratorStructure = {
|
|
||||||
fields: {
|
|
||||||
jobs: {
|
|
||||||
fields: {
|
|
||||||
police: { kind: 'value', valueType: 'boolean' },
|
|
||||||
},
|
|
||||||
kind: 'table',
|
|
||||||
mutableKeys: true,
|
|
||||||
template: { kind: 'value', valueType: 'boolean' },
|
|
||||||
},
|
|
||||||
range: {
|
|
||||||
items: [
|
|
||||||
{ kind: 'value', valueType: 'number' },
|
|
||||||
{ kind: 'value', valueType: 'number' },
|
|
||||||
],
|
|
||||||
kind: 'list',
|
|
||||||
template: { kind: 'value', valueType: 'number' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
kind: 'table',
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(blankFromConfiguratorStructure(structure)).toEqual({
|
|
||||||
jobs: { police: false },
|
|
||||||
range: [0, 0],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -101,10 +101,6 @@ describe('AppStoreApp Sky navigation contract', () => {
|
|||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
'appStore.uninstallApp(uninstallCandidate.value.id)',
|
'appStore.uninstallApp(uninstallCandidate.value.id)',
|
||||||
)
|
)
|
||||||
expect(source).toContain('@click.stop="requestUninstall(app)"')
|
|
||||||
expect(source).toContain('if (!appStore.uninstallApp(')
|
|
||||||
expect(source).toContain('Apps.appStore.account.uninstallFailed')
|
|
||||||
expect(source).toContain('role="alert"')
|
|
||||||
expect(source).toContain('v-if="isPhoneAppRemovable(app)"')
|
expect(source).toContain('v-if="isPhoneAppRemovable(app)"')
|
||||||
expect(source).toContain(':opened="Boolean(uninstallCandidate)"')
|
expect(source).toContain(':opened="Boolean(uninstallCandidate)"')
|
||||||
expect(source).toContain('class="store-account__grabber"')
|
expect(source).toContain('class="store-account__grabber"')
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const openedAt = new Date()
|
|||||||
const featuredSlide = ref(0)
|
const featuredSlide = ref(0)
|
||||||
const profileOpened = ref(false)
|
const profileOpened = ref(false)
|
||||||
const uninstallCandidate = ref<LaunchablePhoneAppDefinition | null>(null)
|
const uninstallCandidate = ref<LaunchablePhoneAppDefinition | null>(null)
|
||||||
const uninstallError = ref('')
|
|
||||||
const selectedApp = ref<LaunchablePhoneAppDefinition | null>(null)
|
const selectedApp = ref<LaunchablePhoneAppDefinition | null>(null)
|
||||||
const storeScroll = ref<ComponentPublicInstance | null>(null)
|
const storeScroll = ref<ComponentPublicInstance | null>(null)
|
||||||
const featuredScroller = ref<HTMLElement | null>(null)
|
const featuredScroller = ref<HTMLElement | null>(null)
|
||||||
@@ -289,16 +288,6 @@ function closeProfile(): void {
|
|||||||
profileDragOffset.value = 0
|
profileDragOffset.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestUninstall(app: LaunchablePhoneAppDefinition): void {
|
|
||||||
uninstallError.value = ''
|
|
||||||
uninstallCandidate.value = app
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeUninstallDialog(): void {
|
|
||||||
uninstallError.value = ''
|
|
||||||
uninstallCandidate.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function beginProfileDrag(event: PointerEvent): void {
|
function beginProfileDrag(event: PointerEvent): void {
|
||||||
if (!profileOpened.value || event.button !== 0) return
|
if (!profileOpened.value || event.button !== 0) return
|
||||||
profileDragPointerId = event.pointerId
|
profileDragPointerId = event.pointerId
|
||||||
@@ -329,11 +318,8 @@ function endProfileDrag(event: PointerEvent): void {
|
|||||||
|
|
||||||
function confirmUninstall(): void {
|
function confirmUninstall(): void {
|
||||||
if (!uninstallCandidate.value) return
|
if (!uninstallCandidate.value) return
|
||||||
if (!appStore.uninstallApp(uninstallCandidate.value.id)) {
|
appStore.uninstallApp(uninstallCandidate.value.id)
|
||||||
uninstallError.value = phone.t('Apps.appStore.account.uninstallFailed')
|
uninstallCandidate.value = null
|
||||||
return
|
|
||||||
}
|
|
||||||
closeUninstallDialog()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightStyle(index: number): Record<string, string> {
|
function highlightStyle(index: number): Record<string, string> {
|
||||||
@@ -1121,7 +1107,7 @@ watch(
|
|||||||
app: getPhoneAppLabel(app, phone.t),
|
app: getPhoneAppLabel(app, phone.t),
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
@click.stop="requestUninstall(app)"
|
@click="uninstallCandidate = app"
|
||||||
>
|
>
|
||||||
<Trash2 :size="17" :stroke-width="2" aria-hidden="true" />
|
<Trash2 :size="17" :stroke-width="2" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1133,8 +1119,8 @@ watch(
|
|||||||
<SkyDialog
|
<SkyDialog
|
||||||
:opened="Boolean(uninstallCandidate)"
|
:opened="Boolean(uninstallCandidate)"
|
||||||
role="alertdialog"
|
role="alertdialog"
|
||||||
@backdropclick="closeUninstallDialog"
|
@backdropclick="uninstallCandidate = null"
|
||||||
@escape="closeUninstallDialog"
|
@escape="uninstallCandidate = null"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ phone.t('Apps.appStore.account.uninstallTitle') }}
|
{{ phone.t('Apps.appStore.account.uninstallTitle') }}
|
||||||
@@ -1146,15 +1132,8 @@ watch(
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<p
|
|
||||||
v-if="uninstallError"
|
|
||||||
class="store-account__uninstall-error"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
{{ uninstallError }}
|
|
||||||
</p>
|
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
<SkyDialogButton @click="closeUninstallDialog">
|
<SkyDialogButton @click="uninstallCandidate = null">
|
||||||
{{ phone.t('Common.cancel') }}
|
{{ phone.t('Common.cancel') }}
|
||||||
</SkyDialogButton>
|
</SkyDialogButton>
|
||||||
<SkyDialogButton strong @click="confirmUninstall">
|
<SkyDialogButton strong @click="confirmUninstall">
|
||||||
@@ -1497,11 +1476,6 @@ watch(
|
|||||||
background: var(--sky-danger-soft);
|
background: var(--sky-danger-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-account__uninstall-error {
|
|
||||||
color: var(--sky-danger);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-scroll {
|
.store-scroll {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|||||||
@@ -540,25 +540,6 @@ function buildStructure(value, scope, path) {
|
|||||||
if (scope === 'config' && path === 'Phone.Keybind') {
|
if (scope === 'config' && path === 'Phone.Keybind') {
|
||||||
return { kind: 'optionalString' }
|
return { kind: 'optionalString' }
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
scope === 'config' &&
|
|
||||||
/^Radio\.LockedChannels\.\d+\.jobs$/.test(path) &&
|
|
||||||
value !== null &&
|
|
||||||
typeof value === 'object' &&
|
|
||||||
!Array.isArray(value)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
fields: Object.fromEntries(
|
|
||||||
Object.entries(value).map(([key, child]) => [
|
|
||||||
key,
|
|
||||||
buildStructure(child, scope, `${path}.${key}`),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
kind: 'table',
|
|
||||||
mutableKeys: true,
|
|
||||||
template: { kind: 'value', valueType: 'boolean' },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
scope === 'config' &&
|
scope === 'config' &&
|
||||||
path === 'Companies.Definitions' &&
|
path === 'Companies.Definitions' &&
|
||||||
|
|||||||
@@ -193,29 +193,6 @@ describe('admin configurator fixture', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows custom jobs in locked radio channel entries', () => {
|
|
||||||
const radio = loadConfiguratorSections()
|
|
||||||
.flatMap((section) => section.fields)
|
|
||||||
.find((field) => field.path === 'Radio')
|
|
||||||
const lockedChannels = radio?.structure?.fields?.LockedChannels
|
|
||||||
const jobs = lockedChannels?.items?.[0]?.fields?.jobs
|
|
||||||
|
|
||||||
expect(jobs).toMatchObject({
|
|
||||||
fields: {
|
|
||||||
ambulance: { kind: 'value', valueType: 'boolean' },
|
|
||||||
police: { kind: 'value', valueType: 'boolean' },
|
|
||||||
},
|
|
||||||
kind: 'table',
|
|
||||||
mutableKeys: true,
|
|
||||||
template: { kind: 'value', valueType: 'boolean' },
|
|
||||||
})
|
|
||||||
expect(lockedChannels?.template?.fields?.jobs).toMatchObject({
|
|
||||||
kind: 'table',
|
|
||||||
mutableKeys: true,
|
|
||||||
template: { kind: 'value', valueType: 'boolean' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('publishes fixed schemas for every empty configurable collection', () => {
|
it('publishes fixed schemas for every empty configurable collection', () => {
|
||||||
const fields = loadConfiguratorSections().flatMap(
|
const fields = loadConfiguratorSections().flatMap(
|
||||||
(section) => section.fields,
|
(section) => section.fields,
|
||||||
|
|||||||
@@ -450,8 +450,8 @@ Config.Garage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Config.Housing = {
|
Config.Housing = {
|
||||||
System = "auto", -- auto, rtx, quasar, tgiann, vms, rx, nolag, sn, esx_property, qbx_properties
|
System = "auto", -- auto, rtx, quasar, vms, rx, nolag, sn, esx_property, qbx_properties
|
||||||
AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "tgiann", "vms", "rx", "nolag", "sn" },
|
AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "vms", "rx", "nolag", "sn" },
|
||||||
MaximumProperties = 50,
|
MaximumProperties = 50,
|
||||||
OverviewRequestsPerMinute = 30,
|
OverviewRequestsPerMinute = 30,
|
||||||
ActionsPerMinute = 12,
|
ActionsPerMinute = 12,
|
||||||
|
|||||||
@@ -2022,11 +2022,10 @@ Locales["de"] = {
|
|||||||
},
|
},
|
||||||
account = {
|
account = {
|
||||||
account = "Account", title = "App-Verwaltung", skyAccount = "Sky Phone Konto",
|
account = "Account", title = "App-Verwaltung", skyAccount = "Sky Phone Konto",
|
||||||
apps = "Installierte Apps", games = "Spiele", library = "Deine Bibliothek.", myApps = "Meine Apps",
|
apps = "Installation von Apps", games = "Spiele", library = "Deine Bibliothek.", myApps = "Meine Apps",
|
||||||
downloadedOn = "Gespeichert {date}", uninstall = "Deinstallieren", uninstallApp = "Deinstallieren {app}",
|
downloadedOn = "Gespeichert {date}", uninstall = "Deinstallieren", uninstallApp = "Deinstallieren {app}",
|
||||||
uninstallTitle = "Diese App deinstallieren?",
|
uninstallTitle = "Diese App deinstallieren?",
|
||||||
uninstallBody = "{app} wird von diesem Handy entfernt. Du kannst die App erneut aus dem App Store laden.",
|
uninstallBody = "{app} wird von diesem Handy entfernt. Du kannst die App erneut aus dem App Store laden.",
|
||||||
uninstallFailed = "Die App konnte nicht deinstalliert werden. Bitte versuche es erneut.",
|
|
||||||
},
|
},
|
||||||
details = {
|
details = {
|
||||||
skyStudios = "Sky Studios", share = "App teilen", openDetails = "Ansicht {app}",
|
skyStudios = "Sky Studios", share = "App teilen", openDetails = "Ansicht {app}",
|
||||||
|
|||||||
@@ -2026,7 +2026,6 @@ Locales["en"] = {
|
|||||||
downloadedOn = "Downloaded {date}", uninstall = "Uninstall", uninstallApp = "Uninstall {app}",
|
downloadedOn = "Downloaded {date}", uninstall = "Uninstall", uninstallApp = "Uninstall {app}",
|
||||||
uninstallTitle = "Uninstall this app?",
|
uninstallTitle = "Uninstall this app?",
|
||||||
uninstallBody = "{app} will be removed from this phone. You can download it again from the App Store.",
|
uninstallBody = "{app} will be removed from this phone. You can download it again from the App Store.",
|
||||||
uninstallFailed = "The app could not be uninstalled. Please try again.",
|
|
||||||
},
|
},
|
||||||
details = {
|
details = {
|
||||||
skyStudios = "Sky Studios", share = "Share app", openDetails = "View {app}",
|
skyStudios = "Sky Studios", share = "Share app", openDetails = "View {app}",
|
||||||
|
|||||||
@@ -2026,7 +2026,6 @@ Locales["es"] = {
|
|||||||
downloadedOn = "Descargado {date}", uninstall = "Desinstalar", uninstallApp = "Desinstalar {app}",
|
downloadedOn = "Descargado {date}", uninstall = "Desinstalar", uninstallApp = "Desinstalar {app}",
|
||||||
uninstallTitle = "¿Desinstalar esta aplicación?",
|
uninstallTitle = "¿Desinstalar esta aplicación?",
|
||||||
uninstallBody = "{app} será eliminado de este teléfono. Puedes descargarlo de nuevo de la App Store.",
|
uninstallBody = "{app} será eliminado de este teléfono. Puedes descargarlo de nuevo de la App Store.",
|
||||||
uninstallFailed = "No se ha podido desinstalar la aplicación. Inténtalo de nuevo.",
|
|
||||||
},
|
},
|
||||||
details = {
|
details = {
|
||||||
skyStudios = "Sky Studios", share = "Compartir aplicación", openDetails = "Ver {app}",
|
skyStudios = "Sky Studios", share = "Compartir aplicación", openDetails = "Ver {app}",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use_experimental_fxv2_oal 'yes'
|
|||||||
|
|
||||||
author 'Sky-Systems'
|
author 'Sky-Systems'
|
||||||
description 'Sky Phone'
|
description 'Sky Phone'
|
||||||
version '0.3.3'
|
version '0.3.1'
|
||||||
|
|
||||||
provide 'lb-phone'
|
provide 'lb-phone'
|
||||||
provide '17mov_Phone'
|
provide '17mov_Phone'
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
local provider_name = "tgiann"
|
|
||||||
local resource_name = "tgiann-house"
|
|
||||||
|
|
||||||
local function house_reference(value)
|
|
||||||
if type(value) ~= "string" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return value ~= "" and value or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function house_entrance(house)
|
|
||||||
local success, house_data = pcall(function()
|
|
||||||
return exports[resource_name]:getHouseData(house)
|
|
||||||
end)
|
|
||||||
if not success then
|
|
||||||
Bridge.Debug(
|
|
||||||
"error",
|
|
||||||
"[sky_phone] tgiann-house:getHouseData failed for '%s': %s",
|
|
||||||
house,
|
|
||||||
tostring(house_data)
|
|
||||||
)
|
|
||||||
return nil, "provider_error"
|
|
||||||
end
|
|
||||||
local entrance = type(house_data) == "table"
|
|
||||||
and Bridge.Normalize.Coordinates(house_data.doorCoord) or nil
|
|
||||||
if not entrance then
|
|
||||||
return nil, "invalid_coordinates"
|
|
||||||
end
|
|
||||||
return entrance
|
|
||||||
end
|
|
||||||
|
|
||||||
local function enrich_overview(properties)
|
|
||||||
local result = {}
|
|
||||||
if GetResourceState(resource_name) ~= "started" or type(properties) ~= "table" then
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, property in ipairs(properties) do
|
|
||||||
local house = type(property) == "table" and house_reference(property.providerId) or nil
|
|
||||||
if house and property.id == provider_name .. ":" .. house then
|
|
||||||
local entrance = house_entrance(house)
|
|
||||||
if entrance then
|
|
||||||
result[#result + 1] = {
|
|
||||||
id = property.id,
|
|
||||||
entrance = entrance,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
Bridge.Housing.RegisterClientProvider(provider_name, {
|
|
||||||
enrich_overview = enrich_overview,
|
|
||||||
execute = function(action, data)
|
|
||||||
if GetResourceState(resource_name) ~= "started" then
|
|
||||||
return false, "provider_unavailable"
|
|
||||||
end
|
|
||||||
if action ~= "set_waypoint" then
|
|
||||||
return false, "capability_unavailable"
|
|
||||||
end
|
|
||||||
local house = type(data) == "table" and house_reference(data.providerId) or nil
|
|
||||||
if not house then
|
|
||||||
return false, "invalid_property"
|
|
||||||
end
|
|
||||||
local entrance, error_code = house_entrance(house)
|
|
||||||
if not entrance then
|
|
||||||
return false, error_code
|
|
||||||
end
|
|
||||||
SetNewWaypoint(entrance.x + 0.0, entrance.y + 0.0)
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
local provider_name = "tgiann"
|
|
||||||
local resource_name = "tgiann-house"
|
|
||||||
|
|
||||||
local function query_owned_houses(identifier)
|
|
||||||
local success, properties = pcall(function()
|
|
||||||
return MySQL.query.await([[
|
|
||||||
SELECT `name`
|
|
||||||
FROM `tgiann_house`
|
|
||||||
WHERE `owner` = ?
|
|
||||||
ORDER BY `name` ASC
|
|
||||||
]], { identifier })
|
|
||||||
end)
|
|
||||||
if not success or type(properties) ~= "table" then
|
|
||||||
Bridge.Debug("error", "[sky_phone] tgiann-house overview query failed: %s", tostring(properties))
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return properties
|
|
||||||
end
|
|
||||||
|
|
||||||
local function owns_house(identifier, house)
|
|
||||||
local success, property = pcall(function()
|
|
||||||
return MySQL.single.await([[
|
|
||||||
SELECT `name`
|
|
||||||
FROM `tgiann_house`
|
|
||||||
WHERE `name` = ? AND `owner` = ?
|
|
||||||
LIMIT 1
|
|
||||||
]], { house, identifier })
|
|
||||||
end)
|
|
||||||
if not success then
|
|
||||||
Bridge.Debug("error", "[sky_phone] tgiann-house ownership query failed: %s", tostring(property))
|
|
||||||
return nil, "provider_error"
|
|
||||||
end
|
|
||||||
return type(property) == "table"
|
|
||||||
end
|
|
||||||
|
|
||||||
local function house_from_property_id(value)
|
|
||||||
if type(value) ~= "string" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local house = value:match("^tgiann:(.+)$")
|
|
||||||
return house and house ~= "" and house or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function normalized_property(house)
|
|
||||||
return {
|
|
||||||
id = provider_name .. ":" .. house,
|
|
||||||
providerId = house,
|
|
||||||
name = house,
|
|
||||||
access = "owner",
|
|
||||||
locked = false,
|
|
||||||
capabilities = {
|
|
||||||
lock = false,
|
|
||||||
keys = false,
|
|
||||||
waypoint = true,
|
|
||||||
cctv = false,
|
|
||||||
garageStatus = false,
|
|
||||||
},
|
|
||||||
cctv = { enabled = false },
|
|
||||||
garage = nil,
|
|
||||||
keys = nil,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
Bridge.Housing.RegisterProvider(provider_name, {
|
|
||||||
resource_name = resource_name,
|
|
||||||
is_available = function()
|
|
||||||
return GetResourceState(resource_name) == "started"
|
|
||||||
end,
|
|
||||||
get_overview = function(source)
|
|
||||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
|
||||||
if not identifier then
|
|
||||||
return nil, "housing_unavailable"
|
|
||||||
end
|
|
||||||
local properties = query_owned_houses(identifier)
|
|
||||||
if not properties then
|
|
||||||
return nil, "provider_error"
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = {}
|
|
||||||
local seen = {}
|
|
||||||
local maximum = math.max(0, math.floor(tonumber(Config.Housing.MaximumProperties) or 0))
|
|
||||||
for _, property in ipairs(properties) do
|
|
||||||
if #result >= maximum then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
local house = type(property.name) == "string" and property.name or nil
|
|
||||||
if house and house ~= "" and not seen[house] then
|
|
||||||
seen[house] = true
|
|
||||||
result[#result + 1] = normalized_property(house)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end,
|
|
||||||
prepare = function(source, action, data)
|
|
||||||
if action == "toggle_lock" or action == "grant_key" or action == "revoke_key"
|
|
||||||
or action == "key_candidates"
|
|
||||||
then
|
|
||||||
return nil, "capability_unavailable"
|
|
||||||
end
|
|
||||||
if action == "open_cctv" then
|
|
||||||
return nil, "cctv_unavailable"
|
|
||||||
end
|
|
||||||
if action ~= "set_waypoint" then
|
|
||||||
return nil, "invalid_action"
|
|
||||||
end
|
|
||||||
|
|
||||||
local house = house_from_property_id(data and data.propertyId)
|
|
||||||
if not house then
|
|
||||||
return nil, "invalid_property"
|
|
||||||
end
|
|
||||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
|
||||||
if not identifier then
|
|
||||||
return nil, "housing_unavailable"
|
|
||||||
end
|
|
||||||
local owned, error_code = owns_house(identifier, house)
|
|
||||||
if owned == nil then
|
|
||||||
return nil, error_code
|
|
||||||
end
|
|
||||||
if not owned then
|
|
||||||
return nil, "property_access_denied"
|
|
||||||
end
|
|
||||||
return { providerId = house }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
SkyPhoneFocus = {}
|
SkyPhoneFocus = {}
|
||||||
|
|
||||||
local blocked_phone_controls = { 24, 140, 141, 142, 199, 200, 257, 263, 264 }
|
local blocked_phone_controls = { 24, 140, 141, 142, 257, 263, 264 }
|
||||||
local blocked_phone_look_controls = { 1, 2, 3, 4, 5, 6 }
|
local blocked_phone_look_controls = { 1, 2, 3, 4, 5, 6 }
|
||||||
local focused_control_groups = { 0, 1, 2 }
|
local focused_control_groups = { 0, 1, 2 }
|
||||||
local hold_to_look_enabled = false
|
local hold_to_look_enabled = false
|
||||||
|
|||||||
@@ -637,21 +637,6 @@ local function build_structure(value, scope, path)
|
|||||||
if scope == "config" and path == "Phone.Keybind" then
|
if scope == "config" and path == "Phone.Keybind" then
|
||||||
return { kind = "optionalString" }
|
return { kind = "optionalString" }
|
||||||
end
|
end
|
||||||
if scope == "config"
|
|
||||||
and path:match("^Radio%.LockedChannels%.%d+%.jobs$")
|
|
||||||
and value_type == "table"
|
|
||||||
then
|
|
||||||
local fields = {}
|
|
||||||
for key, child in pairs(value) do
|
|
||||||
fields[key] = build_structure(child, scope, path .. "." .. tostring(key))
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
fields = fields,
|
|
||||||
kind = "table",
|
|
||||||
mutableKeys = true,
|
|
||||||
template = { kind = "value", valueType = "boolean" },
|
|
||||||
}
|
|
||||||
end
|
|
||||||
if scope == "config" and path == "Companies.Definitions" and value_type == "table" then
|
if scope == "config" and path == "Companies.Definitions" and value_type == "table" then
|
||||||
local keys = {}
|
local keys = {}
|
||||||
for key in pairs(value) do
|
for key in pairs(value) do
|
||||||
|
|||||||
@@ -435,8 +435,8 @@ Config.Garage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Config.Housing = {
|
Config.Housing = {
|
||||||
System = "auto", -- auto, rtx, quasar, tgiann, vms, rx, nolag, sn, esx_property, qbx_properties
|
System = "auto", -- auto, rtx, quasar, vms, rx, nolag, sn, esx_property, qbx_properties
|
||||||
AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "tgiann", "vms", "rx", "nolag", "sn" },
|
AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "vms", "rx", "nolag", "sn" },
|
||||||
MaximumProperties = 50,
|
MaximumProperties = 50,
|
||||||
OverviewRequestsPerMinute = 30,
|
OverviewRequestsPerMinute = 30,
|
||||||
ActionsPerMinute = 12,
|
ActionsPerMinute = 12,
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ assert(firing_disabled, "focused phone cursor must block attacks while typing")
|
|||||||
all_controls_disabled = {}
|
all_controls_disabled = {}
|
||||||
firing_disabled = false
|
firing_disabled = false
|
||||||
SkyPhoneFocus.ApplyGameInputControls(true)
|
SkyPhoneFocus.ApplyGameInputControls(true)
|
||||||
for _, control in ipairs({ 24, 140, 141, 142, 199, 200, 257, 263, 264 }) do
|
for _, control in ipairs({ 24, 140, 141, 142, 257, 263, 264 }) do
|
||||||
assert(disabled_controls[control], ("phone control %d must remain disabled"):format(control))
|
assert(disabled_controls[control], ("phone control %d must remain disabled"):format(control))
|
||||||
end
|
end
|
||||||
assert(not disabled_controls[19], "Alt must remain available while no phone text input is focused")
|
assert(not disabled_controls[19], "Alt must remain available while no phone text input is focused")
|
||||||
|
|||||||
Reference in New Issue
Block a user