diff --git a/README.md b/README.md index 7606ef3..902ee91 100644 --- a/README.md +++ b/README.md @@ -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 | | **Calls** | YACA, PMA Voice, SaltyChat | | **Radio** | YACA, PMA Voice, SaltyChat | -| **Housing** | RTX Housing, Quasar Housing, VMS Housing, RX Housing, NoLag Properties, SN Properties, ESX Property, qbx_properties | +| **Housing** | RTX Housing, Quasar Housing, TGIANN House, 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 | | **Custom app contracts** | Sky Phone, LB Phone, 17Movement, High Phone, Quasar Smartphone, YSeries | | **Languages** | English, German | @@ -650,7 +650,7 @@ Select the provider under `Config.Garage.System`. Vehicle images use the configu ### Housing -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. +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. ### Companies diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 73f4c88..f9f619e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -87,6 +87,7 @@ import { getHairlinePixelStyle } from '@/utils/rendering' import { isTextInputElement } from '@/utils/textInputFocus' import { configurePhoneNumberFormat } from '@/utils/phone' import { consumeEscape } from '@/utils/keyboard' +import type { CustomPhoneToneCatalog } from '@/utils/customTones' import { isTrustedRootMessageSource } from '@/utils/windowMessages' import SpringboardView from '@/views/SpringboardView.vue' @@ -118,6 +119,7 @@ type AppMessage = { | CustomAppEventData | NavigationEventData | AdminPanelOpenPayload + | CustomPhoneToneCatalog } type AdminPanelOpenPayload = Required< @@ -750,6 +752,8 @@ function onMessage(event: MessageEvent): void { adminPanelOpen.value = true } else if (event.data?.type === 'admin:close') { adminPanelOpen.value = false + } else if (event.data?.type === 'phone:tones') { + phone.setCustomTones(event.data.data) } else if (event.data?.type === 'custom-apps:catalog') { appCatalog.replaceCatalog(event.data.data) const catalogPayload = event.data.data as diff --git a/frontend/src/AppDevelopmentPreview.contract.test.ts b/frontend/src/AppDevelopmentPreview.contract.test.ts index 17e233a..7e8fa4b 100644 --- a/frontend/src/AppDevelopmentPreview.contract.test.ts +++ b/frontend/src/AppDevelopmentPreview.contract.test.ts @@ -84,8 +84,8 @@ describe('browser development preview contract', () => { /\.phone-home-indicator:focus-visible span\s*\{[^}]*0 0 0 2px #0a84ff,/s, ) }) - - it('replaces rectangular CEF focus outlines on the side hardware controls', () => { + + it('replaces rectangular CEF focus outlines on the side hardware controls', () => { expect(mainCss).toMatch( /\.phone-hardware-button:focus\s*\{[^}]*outline:\s*none;/s, ) diff --git a/frontend/src/components/AdminConfigValueEditor.vue b/frontend/src/components/AdminConfigValueEditor.vue index 4a6d6f4..d07875b 100644 --- a/frontend/src/components/AdminConfigValueEditor.vue +++ b/frontend/src/components/AdminConfigValueEditor.vue @@ -400,10 +400,10 @@ function toggleOptionalString(event: Event): void { function addListRow(): void { const rows = Array.isArray(props.modelValue) ? [...props.modelValue] : [] const index = rows.length - const value = rows.length - ? blankLike(rows[0]) - : listTemplate.value - ? blankFromConfiguratorStructure(listTemplate.value) + const value = listTemplate.value + ? blankFromConfiguratorStructure(listTemplate.value) + : rows.length + ? blankLike(rows[0]) : blankValue(newArrayKind.value) rows.push(value) emit('update:modelValue', rows) diff --git a/frontend/src/components/AdminCustomToneManager.vue b/frontend/src/components/AdminCustomToneManager.vue new file mode 100644 index 0000000..2cc9eb4 --- /dev/null +++ b/frontend/src/components/AdminCustomToneManager.vue @@ -0,0 +1,809 @@ + + + + + diff --git a/frontend/src/components/AdminPanel.contract.test.ts b/frontend/src/components/AdminPanel.contract.test.ts index 4d3bbcf..e48f36d 100644 --- a/frontend/src/components/AdminPanel.contract.test.ts +++ b/frontend/src/components/AdminPanel.contract.test.ts @@ -144,6 +144,7 @@ describe('standalone admin panel contracts', () => { 'messages', 'calls', 'moderation', + 'tones', 'audit', 'configurator', ]) { @@ -440,6 +441,9 @@ describe('standalone admin panel contracts', () => { expect(source).toContain("selectConfiguratorScope('media')") expect(source).not.toContain('class="admin-panel-config-meta"') 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( 'const canExtendTable = computed(', diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue index 97cc576..47628d7 100644 --- a/frontend/src/components/AdminPanel.vue +++ b/frontend/src/components/AdminPanel.vue @@ -2,6 +2,7 @@ import { ChartNoAxesCombined, BadgeDollarSign, + BellRing, BriefcaseBusiness, Check, ChevronRight, @@ -65,6 +66,7 @@ import { nuiCall } from '@/utils/nui' import AdminConfigValueEditor, { type AdminConfigEditorLabels, } from './AdminConfigValueEditor.vue' +import AdminCustomToneManager from './AdminCustomToneManager.vue' type AdminTab = | 'overview' @@ -75,6 +77,7 @@ type AdminTab = | 'messages' | 'calls' | 'moderation' + | 'tones' | 'audit' | 'configurator' type ConfiguratorScope = 'config' | 'media' @@ -540,8 +543,15 @@ function selectTab(nextTab: AdminTab): void { if (nextTab === 'overview' && admin.initialized && !admin.loading) { void admin.load() } - if (nextTab === 'configurator' && !admin.configurator) { - void admin.loadConfigurator().then((loaded) => { + if (nextTab === 'configurator') { + void ( + admin.configurator ? Promise.resolve(true) : admin.loadConfigurator() + ).then((loaded) => { + if (!loaded) showToast(errorText(), 'error') + }) + } + if (nextTab === 'tones') { + void admin.loadCustomTones().then((loaded) => { if (!loaded) showToast(errorText(), 'error') }) } @@ -865,7 +875,9 @@ onBeforeUnmount(() => { {{ tab === 'configurator' ? t('configurator.context') - : admin.selectedPlayer?.name || t('editor.noSelection') + : tab === 'tones' + ? t('configurator.customTones.context') + : admin.selectedPlayer?.name || t('editor.noSelection') }} @@ -971,6 +983,15 @@ onBeforeUnmount(() => { > + +