Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Alec Schitzkat
2026-08-18 00:30:14 +02:00
40 changed files with 1524 additions and 559 deletions
+7 -6
View File
@@ -103,7 +103,7 @@ The files contain clearly separated sections for:
| `Config.Phone` | Phone item, movement, unique-device mode, and development command |
| `Config.Sim` | Physical or virtual SIM behavior and number formatting |
| `Config.Calls` / `Config.Radio` | Voice providers, call behavior, radio limits, and permissions |
| `Config.Payphones` | Payphone pricing, props, validation, and server-owned locations |
| `Config.Payphones` | Payphone pricing, detected props, validation, and custom spawned locations |
| `Config.Animations` | Phone prop, animations, and portrait/landscape transforms |
| App sections | Limits and behavior for every built-in app |
| `Config.Server` | Stable password and passcode peppers |
@@ -353,17 +353,18 @@ Radio display-name permissions are configured in `Config.Radio.DisplayName.Allow
## Payphones
Sky Phone includes server-authoritative GTA V payphone locations. Pricing, payment account, props, validation distances, and custom locations are configured under `Config.Payphones`.
Sky Phone automatically detects nearby world props listed in `Config.Payphones.Props`; GTA V payphones do not need configured coordinates. Pricing, payment account, prop models, and validation distances are configured under `Config.Payphones`.
Custom location example:
Use `CustomLocations` only when Sky Phone should spawn additional payphone props at custom positions:
```lua
Config.Payphones.Locations = {
{ model = "prop_phonebox_01a", coords = { x = 123.45, y = 678.90, z = 21.0 } },
Config.Payphones.CustomProp = "prop_phonebox_01b"
Config.Payphones.CustomLocations = {
vector4(123.45, 678.90, 21.0, 90.0),
}
```
The model must also be listed in `Config.Payphones.Props`.
`CustomProp` must also be listed in `Config.Payphones.Props`. Each custom position uses `vector4(x, y, z, heading)`.
## Commands
+4 -16
View File
@@ -13,6 +13,7 @@ import { useRoute, useRouter } from 'vue-router'
import { SkyProvider } from '@/ui'
import PhoneHomeIndicator from '@/components/PhoneHomeIndicator.vue'
import PhoneControlCenter from '@/components/PhoneControlCenter.vue'
import PhoneDynamicIsland from '@/components/PhoneDynamicIsland.vue'
import PhoneMediaCapture from '@/components/PhoneMediaCapture.vue'
import PhoneMemoRecorder from '@/components/PhoneMemoRecorder.vue'
import PhoneLockScreen from '@/components/PhoneLockScreen.vue'
@@ -316,9 +317,6 @@ const DARK_STATUS_BAR_APP_IDS = new Set([
const isDevelopmentRoute = computed(
() => isDevelopment && route.name === 'development-sky-ui',
)
const showActiveCallReturn = computed(
() => Boolean(calls.activeCall) && route.params.appId !== 'phone',
)
const appTransitionName = computed(() =>
route.query.transition === 'app-switch' ? 'app-switch' : 'app-window',
)
@@ -1084,7 +1082,6 @@ function onMessage(event: MessageEvent<AppMessage>): void {
isUnlocking.value = false
loadUnlockedPhoneData()
}
window.setTimeout(() => void router.push('/apps/phone'), 0)
} else if (event.data?.type === 'sim:picker' && event.data.data) {
simPicker.value = event.data.data as unknown as SimPickerPayload
} else if (event.data?.type === 'sim:picker-close') {
@@ -1330,12 +1327,6 @@ function toggleHardwareAlertMute(): void {
showHardwareVolumeHud()
}
function returnToActiveCall(): void {
if (!calls.activeCall) return
controlCenterOpened.value = false
void router.push('/apps/phone')
}
function unlockCamera(): void {
if (phone.security.enabled) {
pendingUnlockRoute.value = '/apps/camera'
@@ -1414,9 +1405,7 @@ onMounted(() => {
window.setTimeout(() => {
notifications.show({
appId: 'messages',
persistent: !developmentParameters.has(
'mutedNotificationPreview',
),
persistent: !developmentParameters.has('mutedNotificationPreview'),
route: '/apps/messages',
text: 'You still got that spare alternator?',
title: 'Tommy V',
@@ -1570,7 +1559,7 @@ onBeforeUnmount(() => {
@open="openNotificationPreview"
/>
<div
v-if="phone.isOpen || notifications.current"
v-if="phone.isOpen || notifications.current || calls.activeCall"
class="phone-resolution-wrapper phone-resolution-wrapper--primary"
>
<div class="phone-resolution-canvas phone-resolution-canvas--primary">
@@ -1682,14 +1671,13 @@ onBeforeUnmount(() => {
>
<PhoneStatusBar
v-if="!isLocked && !(isHomeRoute && springboardEditing)"
:active-call-return="showActiveCallReturn"
:control-center-opened="controlCenterOpened"
:interactive="!setupRequired"
:lockable="!setupRequired"
@active-call="returnToActiveCall"
@control-center="toggleControlCenter"
@lock="lockPhone"
/>
<PhoneDynamicIsland v-if="!setupRequired" />
<SpringboardView
v-if="!isDevelopmentRoute && !setupRequired"
@edit-mode-change="springboardEditing = $event"
@@ -0,0 +1,87 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const source = readFileSync(
new URL('./PhoneDynamicIsland.vue', import.meta.url),
'utf8',
)
const appSource = readFileSync(new URL('../App.vue', import.meta.url), 'utf8')
const recorderSource = readFileSync(
new URL('./PhoneMemoRecorder.vue', import.meta.url),
'utf8',
)
const memosSource = readFileSync(
new URL('../views/apps/MemosApp.vue', import.meta.url),
'utf8',
)
const clockSource = readFileSync(
new URL('../views/apps/ClockApp.vue', import.meta.url),
'utf8',
)
describe('Phone Dynamic Island contract', () => {
it('renders once at phone shell level instead of forcing calls into Phone', () => {
expect(appSource).toContain(
"import PhoneDynamicIsland from '@/components/PhoneDynamicIsland.vue'",
)
expect(appSource).toContain('<PhoneDynamicIsland v-if="!setupRequired" />')
expect(appSource).toContain(
'phone.isOpen || notifications.current || calls.activeCall',
)
expect(appSource).not.toContain(
"window.setTimeout(() => void router.push('/apps/phone'), 0)",
)
})
it('prioritizes calls and exposes answer, decline, and hangup controls', () => {
expect(source.indexOf("return 'incoming-call'")).toBeLessThan(
source.indexOf("return 'recording'"),
)
expect(source).toContain('@click.stop="calls.answer()"')
expect(source).toContain('@click.stop="endCall"')
expect(source).toContain('void calls.decline()')
expect(source).toContain('void calls.hangup()')
expect(source).toContain('call.answeredAt ?? call.startedAt')
})
it('connects music, recorder, timer, and stopwatch controls to their stores', () => {
expect(source).toContain('@click.stop="music.previous()"')
expect(source).toContain('@click.stop="music.toggle()"')
expect(source).toContain('@click.stop="music.next()"')
expect(source).toContain("postRecorderCommand('memo:recordStop')")
expect(source).toContain('clock.pauseTimer(Date.now())')
expect(source).toContain('clock.pauseStopwatch(Date.now())')
expect(source).toContain('clock.addLap(Date.now())')
})
it('keeps recorder state available across app changes', () => {
expect(recorderSource).toContain(
"message.type === 'memo:recordStateRequest'",
)
expect(memosSource).toContain(
"postRecorderCommand('memo:recordStateRequest')",
)
expect(memosSource).not.toContain(
"if (recordingActive.value) postRecorderCommand('memo:recordCancel')",
)
})
it('opens both clock live activities on the correct clock tab', () => {
expect(source).toContain("'/apps/clock?section=timer'")
expect(source).toContain("'/apps/clock?section=stopwatch'")
expect(clockSource).toContain(
"section === 'timer' || section === 'stopwatch'",
)
})
it('animates state changes and moves popup notifications below expanded UI', () => {
expect(source).toContain('<Transition name="phone-dynamic-island">')
expect(source).toContain(
'<Transition name="phone-dynamic-island-content" mode="out-in">',
)
expect(source).toContain(".phone-dynamic-island[data-expanded='true']")
expect(source).toContain('~ .phone-notification-provider')
expect(source).toContain('@media (prefers-reduced-motion: reduce)')
})
})
File diff suppressed because it is too large Load Diff
@@ -557,6 +557,8 @@ function onMessage(event: MessageEvent): void {
void stopRecording(data)
} else if (message.type === 'memo:recordCancel') {
cancelRecording()
} else if (message.type === 'memo:recordStateRequest') {
postRecorderState(currentState)
} else if (message.type === 'memos:uploadReady') {
void uploadReady(data as MemoUploadReady)
} else if (message.type === 'memos:uploadResult') {
@@ -42,7 +42,7 @@ describe('Banking app Sky UI migration', () => {
'SkyButton',
'SkySpinner',
'SkyEmptyState',
'SkyToast',
'SkyNotification',
]) {
expect(source).toContain(`<${component}`)
}
+4 -7
View File
@@ -44,7 +44,7 @@ import {
SkySpinner,
SkyTabBar,
SkyTabButton,
SkyToast,
SkyNotification,
} from '@/ui'
type BankingTab = 'home' | 'activity'
@@ -696,13 +696,10 @@ onBeforeUnmount(() => {
</section>
</SkySheet>
<SkyToast
<SkyNotification
:opened="cooldownToastOpened"
position="center"
vertical-position="bottom"
:text="errorMessage('reload_cooldown')"
@click="closeCooldownToast"
>
{{ errorMessage('reload_cooldown') }}
</SkyToast>
/>
</SkyAppPage>
</template>
@@ -29,7 +29,7 @@ describe('Billing app Sky UI migration', () => {
'SkyTabBar',
'SkyTabButton',
'SkySheet',
'SkyToast',
'SkyNotification',
]) {
expect(source).toContain(`<${component}`)
}
+7 -5
View File
@@ -47,7 +47,7 @@ import {
SkySpinner,
SkyTabBar,
SkyTabButton,
SkyToast,
SkyNotification,
} from '@/ui'
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
@@ -771,9 +771,11 @@ onBeforeUnmount(() => {
</section>
</SkySheet>
<SkyToast :opened="toastOpen" position="center" class="billing-toast">
{{ toastText }}
</SkyToast>
<SkyNotification
:opened="toastOpen"
:text="toastText"
class="billing-notification"
/>
</SkyAppPage>
</template>
@@ -1523,7 +1525,7 @@ onBeforeUnmount(() => {
var(--billing-panel);
}
}
.billing-toast {
.billing-notification {
z-index: 50;
}
</style>
+3 -31
View File
@@ -38,6 +38,7 @@ import {
SkyButton,
SkyGlass,
SkyIcon,
SkyNotification,
SkyNavbar,
SkyAppPage,
SkySearchbar,
@@ -2113,11 +2114,7 @@ onMounted(async () => {
app-id="citymarkt"
:app-name="phone.t('Apps.citymarkt.name')"
/>
<Transition name="toast"
><div v-if="feedback" class="citymarkt__toast">
{{ feedback }}
</div></Transition
>
<SkyNotification :opened="Boolean(feedback)" :text="feedback" />
</sky-app-page>
</template>
@@ -3014,30 +3011,6 @@ onMounted(async () => {
background: var(--panel);
color: #ff796f;
}
.citymarkt__toast {
position: absolute;
z-index: 20;
right: 18px;
bottom: 92px;
left: 18px;
padding: 10px 12px;
border-radius: 11px;
background: #f4f4ee;
color: #171816;
box-shadow: 0 8px 30px #0007;
font-size: 10px;
font-weight: 800;
text-align: center;
}
.toast-enter-active,
.toast-leave-active {
transition: 0.2s;
}
.toast-enter-from,
.toast-leave-to {
transform: translateY(8px);
opacity: 0;
}
.citymarkt {
--color-primary: var(--yellow);
position: relative;
@@ -3740,8 +3713,7 @@ onMounted(async () => {
font-size: 10.5px;
}
.citymarkt__meta,
.citymarkt__inline-auth,
.citymarkt__toast {
.citymarkt__inline-auth {
font-size: 12px;
}
.citymarkt__composer textarea,
+4 -2
View File
@@ -47,7 +47,9 @@ const phone = usePhoneStore()
const clock = useClockStore()
const route = useRoute()
const tab = ref<'world' | 'alarm' | 'stopwatch' | 'timer'>(
route.query.section === 'timer' ? 'timer' : 'world',
route.query.section === 'timer' || route.query.section === 'stopwatch'
? route.query.section
: 'world',
)
const alarmEditor = ref<
{ mode: 'create' } | { id: string; mode: 'edit' } | null
@@ -205,7 +207,7 @@ watch([() => clock.stopwatchStartedAt, tab], ([startedAt, activeTab]) => {
watch(
() => route.query.section,
(section) => {
if (section === 'timer') tab.value = 'timer'
if (section === 'timer' || section === 'stopwatch') tab.value = section
},
)
+4 -7
View File
@@ -37,7 +37,7 @@ import {
SkyScrollRail,
SkySection,
SkySheet,
SkyToast,
SkyNotification,
SkyToggle,
SkyToolbarPane,
} from '@/ui'
@@ -2599,14 +2599,11 @@ onBeforeUnmount(() => {
</template>
</SkyDialog>
<SkyToast
<SkyNotification
:opened="toastOpened"
position="center"
vertical-position="center"
:text="toastText"
@click="toastOpened = false"
>
{{ toastText }}
</SkyToast>
/>
</SkyAppPage>
</template>
+2 -4
View File
@@ -17,7 +17,7 @@ import {
SkySheet,
SkyTabBar,
SkyTabButton,
SkyToast,
SkyNotification,
SkyToggle,
SkyToolbarPane,
} from '@/ui'
@@ -2352,9 +2352,7 @@ onBeforeUnmount(() => {
>
</sky-dialog>
<sky-toast :opened="Boolean(toastText)" position="center">{{
toastText
}}</sky-toast>
<sky-notification :opened="Boolean(toastText)" :text="toastText" />
</sky-app-page>
</template>
+6 -9
View File
@@ -72,7 +72,7 @@ import {
SkySettingsRow,
SkySheet,
SkySpinner,
SkyToast,
SkyNotification,
} from '@/ui'
const VOICE_MAX_DURATION_MS = 60_000
@@ -1896,7 +1896,7 @@ onBeforeUnmount(() => {
margin: 12px 0 8px;
}
.dc-toast {
.dc-notification {
z-index: 150;
}
@@ -2852,13 +2852,10 @@ onBeforeUnmount(() => {
</template>
</SkyDialog>
<SkyToast
<SkyNotification
:opened="Boolean(toast)"
position="center"
vertical-position="center"
class="dc-toast"
>
{{ toast }}
</SkyToast>
:text="toast"
class="dc-notification"
/>
</SkyAppPage>
</template>
+2 -2
View File
@@ -56,7 +56,7 @@ import {
SkySegmented,
SkySegmentedButton,
SkySheet,
SkyToast,
SkyNotification,
SkyToggle,
SkyMessagebar,
} from '@/ui'
@@ -2334,7 +2334,7 @@ onMounted(async () => {
</template>
</div>
<SkyToast :opened="Boolean(feedback)">{{ feedback }}</SkyToast>
<SkyNotification :opened="Boolean(feedback)" :text="feedback" />
</SkyAppPage>
</template>
+2 -4
View File
@@ -26,7 +26,7 @@ import {
SkyTabBar,
SkyTabButton,
SkyToolbarPane,
SkyToast,
SkyNotification,
SkyToggle,
} from '@/ui'
import {
@@ -2237,9 +2237,7 @@ onBeforeUnmount(() => {
</template>
</sky-dialog>
<sky-toast :opened="Boolean(actionToast)" position="center">
{{ actionToast }}
</sky-toast>
<sky-notification :opened="Boolean(actionToast)" :text="actionToast" />
</sky-app-page>
</template>
+4 -6
View File
@@ -73,7 +73,7 @@ import {
SkySegmentedButton,
SkySheet,
SkySpinner,
SkyToast,
SkyNotification,
SkyToggle,
} from '@/ui'
import { nuiCall } from '@/utils/nui'
@@ -3361,13 +3361,11 @@ onBeforeUnmount(() => {
</SkyDialogButton>
</template>
</SkyDialog>
<SkyToast
<SkyNotification
:opened="Boolean(feedback)"
position="center"
:text="feedback"
@click="feedback = ''"
>
{{ feedback }}
</SkyToast>
/>
</SkyAppPage>
</template>
+4 -6
View File
@@ -11,7 +11,7 @@ import {
SkySpinner,
SkySegmented,
SkySegmentedButton,
SkyToast,
SkyNotification,
} from '@/ui'
import {
ChevronLeft,
@@ -1625,13 +1625,11 @@ onBeforeUnmount(() => {
</template>
</sky-dialog>
<sky-toast
<sky-notification
:opened="toastOpened"
position="center"
:text="toastText"
@click="toastOpened = false"
>
{{ toastText }}
</sky-toast>
/>
</template>
<style scoped>
+4 -7
View File
@@ -42,7 +42,7 @@ import {
SkySegmentedButton,
SkySheet,
SkySpinner,
SkyToast,
SkyNotification,
} from '@/ui'
type GarageFilter = 'all' | GarageVehicleStatus
@@ -570,14 +570,11 @@ onBeforeUnmount(() => {
</template>
</SkyDialog>
<SkyToast
<SkyNotification
:opened="toastOpened"
position="center"
vertical-position="center"
:text="toastText"
@click="toastOpened = false"
>
{{ toastText }}
</SkyToast>
/>
</SkyAppPage>
</template>
+4 -7
View File
@@ -38,7 +38,7 @@ import {
SkyNavbar,
SkySheet,
SkySpinner,
SkyToast,
SkyNotification,
} from '@/ui'
const phone = usePhoneStore()
@@ -572,14 +572,11 @@ onBeforeUnmount(() => {
</template>
</sky-dialog>
<SkyToast
<SkyNotification
:opened="toastOpened"
position="center"
vertical-position="bottom"
:text="toastText"
@click="toastOpened = false"
>
{{ toastText }}
</SkyToast>
/>
</sky-app-page>
</template>
+2 -32
View File
@@ -24,6 +24,7 @@ import {
SkyButton,
SkyGlass,
SkyNavbar,
SkyNotification,
SkyAppPage,
SkyPillNavigation,
SkySearchbar,
@@ -1507,11 +1508,7 @@ onMounted(async () => {
:app-name="phone.t('Apps.localPages.name')"
@logged-out="tab = 'profile'"
/>
<Transition name="toast"
><div v-if="feedback" class="pages__toast">
{{ feedback }}
</div></Transition
>
<SkyNotification :opened="Boolean(feedback)" :text="feedback" />
</sky-app-page>
</template>
@@ -2189,30 +2186,6 @@ onMounted(async () => {
font-style: normal;
font-weight: 900;
}
.pages__toast {
position: absolute;
z-index: 20;
right: 17px;
bottom: 91px;
left: 17px;
padding: 11px;
border-radius: 11px;
background: #fff6cf;
color: #1b2023;
box-shadow: 0 8px 30px #0007;
font-size: 10px;
font-weight: 800;
text-align: center;
}
.toast-enter-active,
.toast-leave-active {
transition: 0.2s;
}
.toast-enter-from,
.toast-leave-to {
transform: translateY(8px);
opacity: 0;
}
.pages__header {
height: 64px;
padding: 6px 16px 8px;
@@ -2738,9 +2711,6 @@ onMounted(async () => {
.pages__photo-picker i {
font-size: 10px;
}
.pages__toast {
font-size: 12px;
}
.pages {
--color-primary: var(--yellow);
position: relative;
+4 -6
View File
@@ -12,7 +12,7 @@ import {
SkyAppPage,
SkySheet,
SkySpinner,
SkyToast,
SkyNotification,
SkyToolbar,
SkyToolbarPane,
} from '@/ui'
@@ -2046,13 +2046,11 @@ onBeforeUnmount(() => {
</template>
</sky-dialog>
<sky-toast
<sky-notification
:opened="toastOpened"
position="center"
:text="toastText"
@click="toastOpened = false"
>
{{ toastText }}
</sky-toast>
/>
</template>
<style scoped>
+2 -4
View File
@@ -8,7 +8,7 @@ import {
SkyGlass,
SkySpinner,
SkySheet,
SkyToast,
SkyNotification,
} from '@/ui'
import {
LocateFixed,
@@ -850,9 +850,7 @@ onBeforeUnmount(() => {
</sky-sheet>
</div>
<sky-toast :opened="Boolean(toastText)" position="center">
{{ toastText }}
</sky-toast>
<sky-notification :opened="Boolean(toastText)" :text="toastText" />
</SkyAppPage>
</template>
+11 -2
View File
@@ -412,6 +412,13 @@ function onMessage(event: MessageEvent): void {
const state = message.data as MemoRecorderState
if (!state || !Array.isArray(state.levels)) return
recorderState.value = state
if (
['paused', 'recording', 'starting', 'stopping', 'uploading'].includes(
state.state,
)
) {
view.value = 'recording'
}
if (state.error) showNotification(recorderError(state.error))
} else if (message.type === 'memo:saved') {
const memo = message.data as MemoDto
@@ -423,12 +430,14 @@ function onMessage(event: MessageEvent): void {
}
}
onMounted(() => window.addEventListener('message', onMessage))
onMounted(() => {
window.addEventListener('message', onMessage)
postRecorderCommand('memo:recordStateRequest')
})
onBeforeUnmount(() => {
window.removeEventListener('message', onMessage)
stopPlayback()
if (recordingActive.value) postRecorderCommand('memo:recordCancel')
if (view.value === 'detail') void persistSelected()
})
</script>
@@ -308,8 +308,11 @@ describe('MessagesApp Sky UI contract', () => {
const contactProfile = source.slice(contactProfileStart, threadScrollStart)
const blockDialogOpened = source.indexOf(':opened="blockDialogOpened"')
const blockDialogStart = source.lastIndexOf('<SkyDialog', blockDialogOpened)
const toastStart = source.indexOf('<SkyToast', blockDialogStart)
const blockDialog = source.slice(blockDialogStart, toastStart)
const notificationStart = source.indexOf(
'<SkyNotification',
blockDialogStart,
)
const blockDialog = source.slice(blockDialogStart, notificationStart)
expect(contactProfile).toContain('kind="action"')
expect(contactProfile).toContain('tone="danger"')
+4 -6
View File
@@ -75,7 +75,7 @@ import {
SkySettingsRow,
SkySheet,
SkySpinner,
SkyToast,
SkyNotification,
SkyToolbar,
} from '@/ui'
@@ -1964,13 +1964,11 @@ onBeforeUnmount(() => {
</template>
</SkyDialog>
<SkyToast
<SkyNotification
:opened="toastOpened"
position="center"
:text="toastText"
@click="toastOpened = false"
>
{{ toastText }}
</SkyToast>
/>
</template>
<style scoped>
+2 -4
View File
@@ -20,7 +20,7 @@ import {
SkyRange,
SkySearchbar,
SkySheet,
SkyToast,
SkyNotification,
} from '@/ui'
import {
Check,
@@ -1479,9 +1479,7 @@ onBeforeUnmount(() => {
</template>
</sky-dialog>
<sky-toast position="center" :opened="Boolean(toastText)">{{
toastText
}}</sky-toast>
<sky-notification :opened="Boolean(toastText)" :text="toastText" />
</sky-app-page>
</template>
+2 -7
View File
@@ -72,7 +72,7 @@ import {
SkySegmentedButton,
SkySheet,
SkySpinner,
SkyToast,
SkyNotification,
SkyToggle,
} from '@/ui'
@@ -2498,12 +2498,7 @@ onBeforeUnmount(() => {
}}</SkyDialogButton></template
></SkyDialog
>
<SkyToast
:opened="Boolean(feedback)"
position="center"
vertical-position="center"
>{{ feedback }}</SkyToast
>
<SkyNotification :opened="Boolean(feedback)" :text="feedback" />
</SkyAppPage>
</template>
@@ -21,7 +21,7 @@ describe('RadioApp Sky UI contract', () => {
expect(source).toContain('<SkyRange')
expect(source).toContain('<SkySettingsGroup')
expect(source).toContain('<SkySettingsRow')
expect(source).toContain('<SkyToast')
expect(source).toContain('<SkyNotification')
})
it('renders Radio and Settings in full-width bottom Glass navigation', () => {
@@ -186,6 +186,7 @@ describe('RadioApp Sky UI contract', () => {
expect(source).toContain('<SkyEmptyState')
expect(source).toContain('role="alert"')
expect(source).toContain(':opened="Boolean(feedback)"')
expect(source).toContain('vertical-position="center"')
expect(source).toContain(':text="feedback"')
expect(source).not.toContain('vertical-position=')
})
})
+4 -7
View File
@@ -30,7 +30,7 @@ import {
SkySettingsRow,
SkySpinner,
SkyStatusCard,
SkyToast,
SkyNotification,
SkyToggle,
} from '@/ui'
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
@@ -537,14 +537,11 @@ onBeforeUnmount(() => {
</SkySegmented>
</SkyPillNavigation>
<SkyToast
<SkyNotification
:opened="Boolean(feedback)"
position="center"
vertical-position="center"
:text="feedback"
@click="feedback = ''"
>
{{ feedback }}
</SkyToast>
/>
</SkyAppPage>
</template>
+4 -7
View File
@@ -74,7 +74,7 @@ import {
SkySettingsRangeRow,
SkySettingsRow,
SkySpinner,
SkyToast,
SkyNotification,
} from '@/ui'
import {
APPEARANCE_MODE_IDS,
@@ -1863,14 +1863,11 @@ onBeforeUnmount(() => {
</template>
</SkyDialog>
<SkyToast
<SkyNotification
:opened="Boolean(accountToast)"
position="center"
vertical-position="center"
:text="accountToast"
@click="accountToast = ''"
>
{{ accountToast }}
</SkyToast>
/>
</SkyAppPage>
</template>
+2 -4
View File
@@ -75,7 +75,7 @@ import {
SkySpinner as kPreloader,
SkyTabBar as kTabbar,
SkyTabButton as kTabbarLink,
SkyToast as kToast,
SkyNotification as kNotification,
SkyToggle as kToggle,
} from '@/ui'
@@ -1747,9 +1747,7 @@ onBeforeUnmount(() => {
>
</k-dialog>
<k-toast :opened="Boolean(toastText)" position="center">{{
toastText
}}</k-toast>
<k-notification :opened="Boolean(toastText)" :text="toastText" />
</k-page>
</template>
@@ -83,4 +83,11 @@ describe('phone apps use Sky UI', () => {
expect(source, file).not.toMatch(/\b:colors=/)
}
})
it('uses top notifications instead of app-level toasts', () => {
for (const { file, source } of appSources) {
expect(source, file).not.toMatch(/\bSkyToast\b|<sky-toast|<k-toast/)
expect(source, file).not.toMatch(/(?:citymarkt|pages)__toast/)
}
})
})
+4 -7
View File
@@ -22,7 +22,7 @@ import {
SkyNavbar,
SkyScrollArea,
SkySpinner,
SkyToast,
SkyNotification,
} from '@/ui'
const phone = usePhoneStore()
@@ -283,14 +283,11 @@ watch(
</SkyLink>
</div>
<SkyToast
<SkyNotification
:opened="cooldownToastOpened"
position="center"
vertical-position="bottom"
:text="phone.t('Apps.weather.errors.reload_cooldown')"
@click="closeCooldownToast"
>
{{ phone.t('Apps.weather.errors.reload_cooldown') }}
</SkyToast>
/>
</SkyAppPage>
</template>
+4 -6
View File
@@ -19,7 +19,7 @@ import {
SkySearchbar,
SkySegmented,
SkySegmentedButton,
SkyToast,
SkyNotification,
} from '@/ui'
import {
BriefcaseBusiness,
@@ -1514,13 +1514,11 @@ onBeforeUnmount(() => {
</template>
</sky-dialog>
<sky-toast
<sky-notification
:opened="toastOpened"
position="center"
:text="toastText"
@click="toastOpened = false"
>
{{ toastText }}
</sky-toast>
/>
</sky-app-page>
</template>
+6 -253
View File
@@ -90,12 +90,18 @@ Config.Calls = {
Config.Payphones = {
Enabled = true,
-- Existing world props with these models are detected automatically.
Props = {
"prop_phonebox_01b",
"p_phonebox_01b_s",
"prop_phonebox_01a",
"prop_phonebox_04",
},
-- Additional booths are spawned only at these custom positions.
CustomProp = "prop_phonebox_01b",
CustomLocations = {
-- vector4(x, y, z, heading),
},
ReplacementProp = "sf_prop_sf_phonebox_01b_s",
PricePerSecond = 1,
PaymentAccount = "cash", -- cash or bank
@@ -659,259 +665,6 @@ if IsDuplicityVersion() then
PicstagramPasswordPepper = "653e41dd19aba5ef750a668ad1886273ba7d0e0420bd5c745748df80a6e22676",
}
-- -------------------------------------------------------------------------
-- Server-owned payphone locations
-- -------------------------------------------------------------------------
-- Server-owned vanilla payphone positions used for authoritative proximity checks.
-- Generated from DurtyFree/gta-v-data-dumps worldPublicPhones.json at commit b65684e00f689fdec405c5f1055322c802d3c895.
-- Add custom-map booths here; their model must also be listed in Config.Payphones.Props.
Config.Payphones.Locations = {
{ model = "prop_phonebox_01a", coords = { x = -1819.2284, y = 796.32294, z = 137.12784 } },
{ model = "prop_phonebox_01a", coords = { x = -1773.0256, y = -503.15234, z = 37.80706 } },
{ model = "prop_phonebox_01a", coords = { x = -1772.2114, y = -504.00488, z = 37.81461 } },
{ model = "prop_phonebox_01a", coords = { x = -1457.3734, y = -148.68604, z = 48.7486 } },
{ model = "prop_phonebox_01a", coords = { x = -1456.838, y = -149.31949, z = 48.68604 } },
{ model = "prop_phonebox_01a", coords = { x = -1456.3501, y = -149.95428, z = 48.61642 } },
{ model = "prop_phonebox_01a", coords = { x = -1438.6545, y = -210.77448, z = 47.10766 } },
{ model = "prop_phonebox_01a", coords = { x = -1418.2983, y = -291.36002, z = 42.96778 } },
{ model = "prop_phonebox_01a", coords = { x = -1417.4769, y = -290.64453, z = 42.93899 } },
{ model = "prop_phonebox_01a", coords = { x = -1416.5211, y = -289.8119, z = 42.90134 } },
{ model = "prop_phonebox_01a", coords = { x = -1318.0975, y = -380.79547, z = 35.73553 } },
{ model = "prop_phonebox_01a", coords = { x = -1316.9534, y = -378.42676, z = 35.74885 } },
{ model = "prop_phonebox_01a", coords = { x = -1316.2688, y = -378.07245, z = 35.73169 } },
{ model = "prop_phonebox_01a", coords = { x = -1315.5924, y = -377.7347, z = 35.72274 } },
{ model = "prop_phonebox_01a", coords = { x = -1261.6484, y = -519.13086, z = 30.83657 } },
{ model = "prop_phonebox_01a", coords = { x = -1260.9482, y = -519.9344, z = 30.75686 } },
{ model = "prop_phonebox_01a", coords = { x = -1260.0995, y = -520.9083, z = 30.66707 } },
{ model = "prop_phonebox_01a", coords = { x = -1121.5917, y = -825.6313, z = 14.94339 } },
{ model = "prop_phonebox_01a", coords = { x = -1120.9409, y = -825.0698, z = 14.98657 } },
{ model = "prop_phonebox_01a", coords = { x = -1120.2446, y = -824.4812, z = 15.06407 } },
{ model = "prop_phonebox_01a", coords = { x = -1079.6154, y = -451.0622, z = 35.6144 } },
{ model = "prop_phonebox_01a", coords = { x = -1079.23, y = -451.8739, z = 35.62138 } },
{ model = "prop_phonebox_01a", coords = { x = -1078.874, y = -452.55182, z = 35.62138 } },
{ model = "prop_phonebox_01a", coords = { x = -956.56256, y = -403.17123, z = 36.81676 } },
{ model = "prop_phonebox_01a", coords = { x = -956.1004, y = -404.09232, z = 36.81755 } },
{ model = "prop_phonebox_01a", coords = { x = -524.4403, y = -300.71704, z = 34.26753 } },
{ model = "prop_phonebox_01a", coords = { x = -523.64453, y = -300.41074, z = 34.26273 } },
{ model = "prop_phonebox_01a", coords = { x = -522.872, y = -300.1134, z = 34.25807 } },
{ model = "prop_phonebox_01a", coords = { x = -449.44238, y = -272.8244, z = 34.93996 } },
{ model = "prop_phonebox_01a", coords = { x = -448.8816, y = -274.12805, z = 34.96191 } },
{ model = "prop_phonebox_01a", coords = { x = -448.36926, y = -275.31906, z = 34.96507 } },
{ model = "prop_phonebox_01a", coords = { x = -329.23917, y = 6224.885, z = 30.47861 } },
{ model = "prop_phonebox_01a", coords = { x = -310.30554, y = 6205.3, z = 30.4465 } },
{ model = "prop_phonebox_01a", coords = { x = -280.64215, y = 6224.2314, z = 30.45544 } },
{ model = "prop_phonebox_01a", coords = { x = -243.25082, y = 279.90717, z = 91.04989 } },
{ model = "prop_phonebox_01a", coords = { x = -234.61494, y = 6176.931, z = 30.43884 } },
{ model = "prop_phonebox_01a", coords = { x = -233.6865, y = 6176.0547, z = 30.43884 } },
{ model = "prop_phonebox_01a", coords = { x = -184.35658, y = 6331.4697, z = 30.48767 } },
{ model = "prop_phonebox_01a", coords = { x = -183.68753, y = 6332.1597, z = 30.48987 } },
{ model = "prop_phonebox_01a", coords = { x = -154.76048, y = 6352.27, z = 30.56079 } },
{ model = "prop_phonebox_01a", coords = { x = -153.88358, y = 6351.417, z = 30.56079 } },
{ model = "prop_phonebox_01a", coords = { x = -119.91727, y = 6287.283, z = 30.45911 } },
{ model = "prop_phonebox_01a", coords = { x = -119.44898, y = 6286.768, z = 30.45911 } },
{ model = "prop_phonebox_01a", coords = { x = -92.08037, y = 6462.644, z = 30.44397 } },
{ model = "prop_phonebox_01a", coords = { x = -90.61212, y = 6464.0566, z = 30.44397 } },
{ model = "prop_phonebox_01a", coords = { x = -46.3855, y = 6511.0156, z = 30.44861 } },
{ model = "prop_phonebox_01a", coords = { x = -25.6331, y = 6495.123, z = 30.48767 } },
{ model = "prop_phonebox_01a", coords = { x = -24.59157, y = 6496.0537, z = 30.48767 } },
{ model = "prop_phonebox_01a", coords = { x = 110.07694, y = -1694.206, z = 28.29156 } },
{ model = "prop_phonebox_01a", coords = { x = 136.9704, y = 196.05042, z = 105.73364 } },
{ model = "prop_phonebox_01a", coords = { x = 137.75732, y = 195.764, z = 105.70988 } },
{ model = "prop_phonebox_01a", coords = { x = 138.48807, y = 195.49808, z = 105.69342 } },
{ model = "prop_phonebox_01a", coords = { x = 173.45892, y = -1547.1165, z = 28.25158 } },
{ model = "prop_phonebox_01a", coords = { x = 215.71725, y = -1783.2102, z = 27.99637 } },
{ model = "prop_phonebox_01a", coords = { x = 215.94612, y = -1518.9603, z = 28.29362 } },
{ model = "prop_phonebox_01a", coords = { x = 228.6216, y = -1545.9579, z = 28.28108 } },
{ model = "prop_phonebox_01a", coords = { x = 229.1375, y = -1545.6771, z = 28.28108 } },
{ model = "prop_phonebox_01a", coords = { x = 295.67847, y = -1360.8624, z = 30.91401 } },
{ model = "prop_phonebox_01a", coords = { x = 296.17984, y = -1360.2825, z = 30.91899 } },
{ model = "prop_phonebox_01a", coords = { x = 532.401, y = -151.79816, z = 56.07613 } },
{ model = "prop_phonebox_01a", coords = { x = 539.5577, y = -166.04846, z = 53.4862 } },
{ model = "prop_phonebox_01a", coords = { x = 812.21716, y = -289.03873, z = 65.46264 } },
{ model = "prop_phonebox_01a", coords = { x = 812.3678, y = -289.84793, z = 65.46264 } },
{ model = "prop_phonebox_01a", coords = { x = 819.023, y = -94.03439, z = 79.57648 } },
{ model = "prop_phonebox_01a", coords = { x = 819.37396, y = -93.47577, z = 79.57648 } },
{ model = "prop_phonebox_01a", coords = { x = 891.8809, y = -140.80609, z = 76.11372 } },
{ model = "prop_phonebox_01a", coords = { x = 963.6167, y = -142.79822, z = 73.46588 } },
{ model = "prop_phonebox_01a", coords = { x = 1079.2015, y = -776.68054, z = 57.25418 } },
{ model = "prop_phonebox_01a", coords = { x = 1156.3375, y = -776.99866, z = 56.58559 } },
{ model = "prop_phonebox_01a", coords = { x = 1159.7463, y = -374.87518, z = 66.51784 } },
{ model = "prop_phonebox_01a", coords = { x = 1166.4825, y = -321.59958, z = 68.25383 } },
{ model = "prop_phonebox_01a", coords = { x = 1169.4127, y = 2702.8025, z = 36.99265 } },
{ model = "prop_phonebox_01a", coords = { x = 1170.3059, y = -455.76053, z = 65.49249 } },
{ model = "prop_phonebox_01a", coords = { x = 1172.7772, y = -297.61606, z = 68.01613 } },
{ model = "prop_phonebox_01a", coords = { x = 1172.8646, y = -298.23972, z = 68.01981 } },
{ model = "prop_phonebox_01a", coords = { x = 1173.8961, y = -421.43643, z = 66.07632 } },
{ model = "prop_phonebox_01a", coords = { x = 1201.426, y = -488.8848, z = 64.67129 } },
{ model = "prop_phonebox_01a", coords = { x = 1222.6125, y = -397.32706, z = 67.32355 } },
{ model = "prop_phonebox_01a", coords = { x = 1801.4076, y = 4597.0137, z = 36.67796 } },
{ model = "prop_phonebox_01a", coords = { x = 2558.9167, y = 367.14368, z = 107.63403 } },
{ model = "prop_phonebox_01b", coords = { x = -1684.4233, y = -266.45306, z = 50.89204 } },
{ model = "prop_phonebox_01b", coords = { x = -1683.9019, y = -265.70874, z = 50.89204 } },
{ model = "prop_phonebox_01b", coords = { x = -1543.9277, y = -433.13232, z = 34.57933 } },
{ model = "prop_phonebox_01b", coords = { x = -1543.0599, y = -432.05966, z = 34.58469 } },
{ model = "prop_phonebox_01b", coords = { x = -1522.4791, y = -407.05118, z = 34.58695 } },
{ model = "prop_phonebox_01b", coords = { x = -1412.1201, y = -383.80542, z = 35.68469 } },
{ model = "prop_phonebox_01b", coords = { x = -1205.1965, y = -1393.6274, z = 3.07721 } },
{ model = "prop_phonebox_01b", coords = { x = -1150.6671, y = -1392.6455, z = 4.11812 } },
{ model = "prop_phonebox_01b", coords = { x = -1142.0598, y = -725.3442, z = 19.77577 } },
{ model = "prop_phonebox_01b", coords = { x = -1080.87, y = -2574.942, z = 12.91528 } },
{ model = "prop_phonebox_01b", coords = { x = -1061.7015, y = -2541.7412, z = 12.91528 } },
{ model = "prop_phonebox_01b", coords = { x = -1061.5474, y = -2541.4744, z = 19.15571 } },
{ model = "prop_phonebox_01b", coords = { x = -1046.9408, y = -2516.175, z = 12.91528 } },
{ model = "prop_phonebox_01b", coords = { x = -1046.8787, y = -2516.0674, z = 19.15571 } },
{ model = "prop_phonebox_01b", coords = { x = -1034.5115, y = -2494.6467, z = 19.15571 } },
{ model = "prop_phonebox_01b", coords = { x = -1024.4517, y = -2477.2227, z = 12.91528 } },
{ model = "prop_phonebox_01b", coords = { x = -765.40045, y = -848.8706, z = 21.11398 } },
{ model = "prop_phonebox_01b", coords = { x = -764.83673, y = -848.8654, z = 21.13071 } },
{ model = "prop_phonebox_01b", coords = { x = -756.90735, y = 5586.8223, z = 35.71351 } },
{ model = "prop_phonebox_01b", coords = { x = -756.90735, y = 5587.636, z = 35.71351 } },
{ model = "prop_phonebox_01b", coords = { x = -745.72156, y = 5558.714, z = 35.71351 } },
{ model = "prop_phonebox_01b", coords = { x = -743.95874, y = 5558.714, z = 35.71351 } },
{ model = "prop_phonebox_01b", coords = { x = -715.921, y = 123.33502, z = 54.99648 } },
{ model = "prop_phonebox_01b", coords = { x = -715.40906, y = 123.65546, z = 55.01274 } },
{ model = "prop_phonebox_01b", coords = { x = -700.7271, y = -916.8699, z = 18.21408 } },
{ model = "prop_phonebox_01b", coords = { x = -700.1226, y = -916.8699, z = 18.21408 } },
{ model = "prop_phonebox_01b", coords = { x = -685.93774, y = -854.8967, z = 22.88396 } },
{ model = "prop_phonebox_01b", coords = { x = -670.3093, y = -819.59973, z = 23.4098 } },
{ model = "prop_phonebox_01b", coords = { x = -669.60815, y = -819.6056, z = 23.42427 } },
{ model = "prop_phonebox_01b", coords = { x = -668.81213, y = -819.6378, z = 23.43811 } },
{ model = "prop_phonebox_01b", coords = { x = -665.19354, y = -670.83777, z = 30.40002 } },
{ model = "prop_phonebox_01b", coords = { x = -664.59607, y = -670.8341, z = 30.40393 } },
{ model = "prop_phonebox_01b", coords = { x = -663.99866, y = -670.83044, z = 30.41797 } },
{ model = "prop_phonebox_01b", coords = { x = -655.2001, y = -859.74493, z = 23.50043 } },
{ model = "prop_phonebox_01b", coords = { x = -654.1605, y = -707.27155, z = 28.40153 } },
{ model = "prop_phonebox_01b", coords = { x = -654.1605, y = -706.51654, z = 28.47383 } },
{ model = "prop_phonebox_01b", coords = { x = -654.1605, y = -705.7615, z = 28.54753 } },
{ model = "prop_phonebox_01b", coords = { x = -611.56744, y = -2237.6104, z = 5.10603 } },
{ model = "prop_phonebox_01b", coords = { x = -530.0182, y = -1286.1543, z = 25.03622 } },
{ model = "prop_phonebox_01b", coords = { x = -529.77765, y = -1285.6444, z = 25.04207 } },
{ model = "prop_phonebox_01b", coords = { x = -529.6009, y = -1286.3458, z = 25.04428 } },
{ model = "prop_phonebox_01b", coords = { x = -529.36365, y = -1285.8345, z = 25.03622 } },
{ model = "prop_phonebox_01b", coords = { x = -468.24023, y = -396.44247, z = 32.8951 } },
{ model = "prop_phonebox_01b", coords = { x = -467.58286, y = -396.50574, z = 32.8951 } },
{ model = "prop_phonebox_01b", coords = { x = -259.3869, y = -604.76556, z = 32.59827 } },
{ model = "prop_phonebox_01b", coords = { x = -259.14352, y = -603.98016, z = 32.65002 } },
{ model = "prop_phonebox_01b", coords = { x = -241.57574, y = -766.2026, z = 31.73654 } },
{ model = "prop_phonebox_01b", coords = { x = -241.29694, y = -765.4682, z = 31.77955 } },
{ model = "prop_phonebox_01b", coords = { x = -239.74597, y = -978.22943, z = 28.26393 } },
{ model = "prop_phonebox_01b", coords = { x = -239.51797, y = -977.59296, z = 28.26393 } },
{ model = "prop_phonebox_01b", coords = { x = -178.84961, y = -52.06338, z = 51.10093 } },
{ model = "prop_phonebox_01b", coords = { x = -177.28662, y = -713.48505, z = 33.39728 } },
{ model = "prop_phonebox_01b", coords = { x = -147.61765, y = -287.15277, z = 39.43044 } },
{ model = "prop_phonebox_01b", coords = { x = -147.37784, y = -286.44186, z = 39.49831 } },
{ model = "prop_phonebox_01b", coords = { x = -73.17541, y = -641.5052, z = 35.24065 } },
{ model = "prop_phonebox_01b", coords = { x = -72.92773, y = -640.8415, z = 35.24065 } },
{ model = "prop_phonebox_01b", coords = { x = -53.45404, y = -94.169, z = 56.7686 } },
{ model = "prop_phonebox_01b", coords = { x = -27.98413, y = -100.90671, z = 56.35694 } },
{ model = "prop_phonebox_01b", coords = { x = -26.48154, y = -110.65947, z = 56.06785 } },
{ model = "prop_phonebox_01b", coords = { x = -8.06136, y = -731.61005, z = 43.22259 } },
{ model = "prop_phonebox_01b", coords = { x = -7.37235, y = -731.8549, z = 43.22768 } },
{ model = "prop_phonebox_01b", coords = { x = 43.99314, y = -680.87616, z = 43.20672 } },
{ model = "prop_phonebox_01b", coords = { x = 44.30204, y = -680.0512, z = 43.20672 } },
{ model = "prop_phonebox_01b", coords = { x = 120.37446, y = -205.12677, z = 53.61985 } },
{ model = "prop_phonebox_01b", coords = { x = 121.18489, y = -205.42413, z = 53.61985 } },
{ model = "prop_phonebox_01b", coords = { x = 129.40686, y = 245.3497, z = 106.42847 } },
{ model = "prop_phonebox_01b", coords = { x = 140.18076, y = -1033.1602, z = 28.34242 } },
{ model = "prop_phonebox_01b", coords = { x = 174.5452, y = -1116.4456, z = 28.28443 } },
{ model = "prop_phonebox_01b", coords = { x = 175.22937, y = -1116.4185, z = 28.28425 } },
{ model = "prop_phonebox_01b", coords = { x = 213.8157, y = -852.6208, z = 29.38956 } },
{ model = "prop_phonebox_01b", coords = { x = 214.44952, y = -852.86725, z = 29.38709 } },
{ model = "prop_phonebox_01b", coords = { x = 233.49872, y = 334.44766, z = 104.52145 } },
{ model = "prop_phonebox_01b", coords = { x = 296.66183, y = -1359.7725, z = 30.92093 } },
{ model = "prop_phonebox_01b", coords = { x = 372.30563, y = -966.37286, z = 28.41298 } },
{ model = "prop_phonebox_01b", coords = { x = 394.25433, y = -799.7535, z = 28.23798 } },
{ model = "prop_phonebox_01b", coords = { x = 394.25433, y = -798.6486, z = 28.23798 } },
{ model = "prop_phonebox_01b", coords = { x = 397.567, y = -921.3287, z = 28.3982 } },
{ model = "prop_phonebox_01b", coords = { x = 397.567, y = -920.658, z = 28.3982 } },
{ model = "prop_phonebox_01b", coords = { x = 415.17245, y = -910.94476, z = 28.3982 } },
{ model = "prop_phonebox_01b", coords = { x = 436.0411, y = 137.20285, z = 99.43968 } },
{ model = "prop_phonebox_01b", coords = { x = 436.83813, y = 136.89954, z = 99.38892 } },
{ model = "prop_phonebox_01b", coords = { x = 439.8047, y = -606.65063, z = 27.69825 } },
{ model = "prop_phonebox_01b", coords = { x = 439.99564, y = -604.67474, z = 27.69747 } },
{ model = "prop_phonebox_01b", coords = { x = 445.3808, y = 3567.5305, z = 32.21765 } },
{ model = "prop_phonebox_01b", coords = { x = 452.6532, y = -612.11816, z = 27.54012 } },
{ model = "prop_phonebox_01b", coords = { x = 452.7997, y = -610.4434, z = 27.5457 } },
{ model = "prop_phonebox_01b", coords = { x = 535.5781, y = 102.93228, z = 95.56698 } },
{ model = "prop_phonebox_01b", coords = { x = 779.83923, y = -1755.3914, z = 28.47611 } },
{ model = "prop_phonebox_01b", coords = { x = 780.2285, y = -1755.4475, z = 28.46564 } },
{ model = "prop_phonebox_01b", coords = { x = 809.3085, y = -1074.9281, z = 27.67919 } },
{ model = "prop_phonebox_01b", coords = { x = 903.52423, y = 3646.1294, z = 31.70571 } },
{ model = "prop_phonebox_01b", coords = { x = 1051.0497, y = 2661.3877, z = 38.52392 } },
{ model = "prop_phonebox_01b", coords = { x = 1181.1997, y = 2703.214, z = 37.1464 } },
{ model = "prop_phonebox_01b", coords = { x = 1206.4275, y = 2647.8894, z = 36.81204 } },
{ model = "prop_phonebox_01b", coords = { x = 1401.1744, y = 3602.0786, z = 34.01619 } },
{ model = "prop_phonebox_01b", coords = { x = 1662.8026, y = 4841.53, z = 41.0313 } },
{ model = "prop_phonebox_01b", coords = { x = 1662.9365, y = 4840.332, z = 41.0313 } },
{ model = "prop_phonebox_01b", coords = { x = 1692.9031, y = 6432.025, z = 31.73361 } },
{ model = "prop_phonebox_01b", coords = { x = 1696.5485, y = 3776.0618, z = 33.71252 } },
{ model = "prop_phonebox_01b", coords = { x = 1696.7177, y = 4790.302, z = 40.89749 } },
{ model = "prop_phonebox_01b", coords = { x = 1860.4072, y = 3696.2563, z = 33.26152 } },
{ model = "prop_phonebox_01b", coords = { x = 1861.0801, y = 3695.0903, z = 33.26152 } },
{ model = "prop_phonebox_01b", coords = { x = 2005.9707, y = 3782.6196, z = 31.15662 } },
{ model = "prop_phonebox_01b", coords = { x = 2006.7942, y = 3783.1003, z = 31.14984 } },
{ model = "prop_phonebox_04", coords = { x = -2969.4265, y = 397.46487, z = 14.10208 } },
{ model = "prop_phonebox_04", coords = { x = -1417.7056, y = -94.50974, z = 51.41046 } },
{ model = "prop_phonebox_04", coords = { x = -1416.8014, y = -94.11159, z = 51.44441 } },
{ model = "prop_phonebox_04", coords = { x = -1415.9122, y = -93.72023, z = 51.49042 } },
{ model = "prop_phonebox_04", coords = { x = -1294.0234, y = -390.34976, z = 35.44277 } },
{ model = "prop_phonebox_04", coords = { x = -1293.4121, y = -391.38864, z = 35.44632 } },
{ model = "prop_phonebox_04", coords = { x = -1241.7491, y = -464.37216, z = 32.537 } },
{ model = "prop_phonebox_04", coords = { x = -1224.2532, y = -322.51794, z = 36.57259 } },
{ model = "prop_phonebox_04", coords = { x = -1223.0624, y = -321.95178, z = 36.59326 } },
{ model = "prop_phonebox_04", coords = { x = -1074.0209, y = -397.75607, z = 35.95449 } },
{ model = "prop_phonebox_04", coords = { x = -1025.3336, y = -216.04681, z = 36.93829 } },
{ model = "prop_phonebox_04", coords = { x = -1023.97375, y = -216.74884, z = 36.9369 } },
{ model = "prop_phonebox_04", coords = { x = -985.19824, y = -414.10977, z = 36.85289 } },
{ model = "prop_phonebox_04", coords = { x = -979.73254, y = -369.2069, z = 36.856 } },
{ model = "prop_phonebox_04", coords = { x = -979.1488, y = -370.3092, z = 36.856 } },
{ model = "prop_phonebox_04", coords = { x = -965.37616, y = -2524.3992, z = 13.00643 } },
{ model = "prop_phonebox_04", coords = { x = -963.65985, y = -247.05435, z = 37.0568 } },
{ model = "prop_phonebox_04", coords = { x = -896.8487, y = -247.80585, z = 39.07844 } },
{ model = "prop_phonebox_04", coords = { x = -865.3409, y = -2528.5645, z = 13.00643 } },
{ model = "prop_phonebox_04", coords = { x = -821.83124, y = -251.49579, z = 36.0627 } },
{ model = "prop_phonebox_04", coords = { x = -821.29346, y = -252.4495, z = 36.05127 } },
{ model = "prop_phonebox_04", coords = { x = -701.46173, y = -371.56976, z = 33.2833 } },
{ model = "prop_phonebox_04", coords = { x = -700.3627, y = -372.04968, z = 33.27078 } },
{ model = "prop_phonebox_04", coords = { x = -619.5328, y = -207.68121, z = 36.3736 } },
{ model = "prop_phonebox_04", coords = { x = -618.975, y = -208.61508, z = 36.35005 } },
{ model = "prop_phonebox_04", coords = { x = -617.05457, y = -422.20978, z = 33.7873 } },
{ model = "prop_phonebox_04", coords = { x = -557.25586, y = -386.67517, z = 34.11347 } },
{ model = "prop_phonebox_04", coords = { x = -556.05286, y = -386.66565, z = 34.11989 } },
{ model = "prop_phonebox_04", coords = { x = -554.8701, y = -386.6563, z = 34.12583 } },
{ model = "prop_phonebox_04", coords = { x = -546.57184, y = -334.10083, z = 34.16116 } },
{ model = "prop_phonebox_04", coords = { x = -544.17737, y = -157.39006, z = 37.53791 } },
{ model = "prop_phonebox_04", coords = { x = -388.49542, y = -321.52948, z = 32.10458 } },
{ model = "prop_phonebox_04", coords = { x = -387.68488, y = -322.21454, z = 32.05279 } },
{ model = "prop_phonebox_04", coords = { x = -360.33978, y = -267.18268, z = 32.73604 } },
{ model = "prop_phonebox_04", coords = { x = -347.059, y = -1490.9738, z = 29.79159 } },
{ model = "prop_phonebox_04", coords = { x = -345.83786, y = -1490.9738, z = 29.7867 } },
{ model = "prop_phonebox_04", coords = { x = -263.0376, y = -766.90546, z = 31.57576 } },
{ model = "prop_phonebox_04", coords = { x = -262.6508, y = -766.04095, z = 31.60592 } },
{ model = "prop_phonebox_04", coords = { x = -213.1738, y = -696.5944, z = 32.80729 } },
{ model = "prop_phonebox_04", coords = { x = -174.76907, y = -674.9272, z = 33.27862 } },
{ model = "prop_phonebox_04", coords = { x = -173.80676, y = -675.35236, z = 33.29762 } },
{ model = "prop_phonebox_04", coords = { x = -138.00961, y = -799.9025, z = 31.10711 } },
{ model = "prop_phonebox_04", coords = { x = -137.64026, y = -798.8024, z = 31.14563 } },
{ model = "prop_phonebox_04", coords = { x = 55.44337, y = -1081.1333, z = 28.45174 } },
{ model = "prop_phonebox_04", coords = { x = 55.90165, y = -1080.282, z = 28.45174 } },
{ model = "prop_phonebox_04", coords = { x = 188.01767, y = -1043.9451, z = 28.32789 } },
{ model = "prop_phonebox_04", coords = { x = 189.79306, y = -1044.5588, z = 28.32789 } },
{ model = "prop_phonebox_04", coords = { x = 298.28317, y = -795.153, z = 28.4778 } },
{ model = "prop_phonebox_04", coords = { x = 298.62607, y = -794.289, z = 28.4778 } },
{ model = "prop_phonebox_04", coords = { x = 347.45938, y = -730.9255, z = 28.28353 } },
{ model = "prop_phonebox_04", coords = { x = 564.7501, y = -1748.7141, z = 28.31245 } },
{ model = "prop_phonebox_04", coords = { x = 653.1738, y = 272.5705, z = 102.29323 } },
{ model = "prop_phonebox_04", coords = { x = 654.2313, y = 271.95996, z = 102.29323 } },
{ model = "prop_phonebox_04", coords = { x = 1214.8445, y = -1385.6348, z = 34.34755 } },
{ model = "prop_phonebox_04", coords = { x = 1214.8445, y = -1384.5386, z = 34.34755 } },
{ model = "prop_phonebox_04", coords = { x = 1662.002, y = 4819.523, z = 41.04535 } },
{ model = "prop_phonebox_04", coords = { x = 1816.4236, y = 3671.6497, z = 33.29268 } },
{ model = "prop_phonebox_04", coords = { x = 1818.1936, y = 3668.7485, z = 33.29268 } },
{ model = "prop_phonebox_04", coords = { x = 2007.0574, y = 3784.7974, z = 31.20895 } },
}
-- -------------------------------------------------------------------------
-- Company directory
-- -------------------------------------------------------------------------
+65
View File
@@ -18,6 +18,7 @@ local visuals_starting = false
local visuals_ending = false
local hangup_requested = false
local remote_visuals = {}
local custom_payphone_props = {}
local configured_models = {}
for _, model_name in ipairs(Config.Payphones.Props or {}) do
@@ -97,6 +98,64 @@ local function load_model(model_hash)
return HasModelLoaded(model_hash)
end
local function spawn_custom_payphones()
local locations = Config.Payphones.CustomLocations or {}
if not Config.Payphones.Enabled or #locations == 0 then
return
end
local model_name = Config.Payphones.CustomProp
local model_hash = type(model_name) == "string" and joaat(model_name) or 0
if not configured_models[model_hash] then
Bridge.Debug(
"error",
"[sky_phone] Config.Payphones.CustomProp must also be listed in Config.Payphones.Props.",
{ always = true }
)
return
end
if not load_model(model_hash) then
Bridge.Debug("error", "[sky_phone] The configured custom payphone prop could not be loaded.", {
always = true,
})
return
end
for index, coords in ipairs(locations) do
local coords_type = type(coords)
local x = (coords_type == "vector4" or coords_type == "table")
and valid_visual_number(coords.x, 10000.0) or nil
local y = x and valid_visual_number(coords.y, 10000.0) or nil
local z = y and valid_visual_number(coords.z, 2000.0) or nil
local heading = z and valid_visual_number(coords.w, 360000.0) or nil
if not heading then
Bridge.Debug(
"error",
"[sky_phone] Ignored invalid Config.Payphones.CustomLocations entry %s; use vector4(x, y, z, heading).",
index,
{ always = true }
)
else
local entity = CreateObjectNoOffset(model_hash, x, y, z, false, true, false)
if entity == 0 or not DoesEntityExist(entity) then
Bridge.Debug(
"error",
"[sky_phone] Failed to spawn custom payphone %s.",
index,
{ always = true }
)
else
SetEntityHeading(entity, heading % 360.0)
FreezeEntityPosition(entity, true)
custom_payphone_props[#custom_payphone_props + 1] = entity
end
end
end
SetModelAsNoLongerNeeded(model_hash)
end
CreateThread(spawn_custom_payphones)
local function load_animation(dictionary)
if HasAnimDictLoaded(dictionary) then
return true
@@ -667,4 +726,10 @@ AddEventHandler("onResourceStop", function(resource_name)
for _, id in ipairs(visual_ids) do
restore_remote_visual(id)
end
for _, entity in ipairs(custom_payphone_props) do
if DoesEntityExist(entity) then
SetEntityAsMissionEntity(entity, true, true)
DeleteEntity(entity)
end
end
end)
+8 -19
View File
@@ -1074,34 +1074,23 @@ for _, model_name in ipairs(Config.Payphones.Props or {}) do
end
end
local payphone_locations, rejected_payphone_locations = SkyPhonePayphones.ValidateLocations(
Config.Payphones.Locations,
payphone_models
)
if Config.Payphones.Enabled and #payphone_locations == 0 then
if Config.Payphones.Enabled and not next(payphone_models) then
Bridge.Debug(
"error",
"[sky_phone] Payphones are enabled, but no valid server-owned locations are configured; payphone calls will be rejected.",
{ always = true }
)
elseif Config.Payphones.Enabled and rejected_payphone_locations > 0 then
Bridge.Debug(
"warn",
"[sky_phone] Ignored %s invalid server-owned payphone location(s).",
rejected_payphone_locations,
"[sky_phone] Payphones are enabled, but Config.Payphones.Props contains no valid models; payphone calls will be rejected.",
{ always = true }
)
end
local function valid_payphone_position(source)
local function valid_payphone_position(source, detected_booth)
local ped = GetPlayerPed(source)
if not ped or ped == 0 then
return nil
end
local player_coords = GetEntityCoords(ped)
local location = SkyPhonePayphones.FindNearest(
payphone_locations,
player_coords,
local location = SkyPhonePayphones.ValidateDetected(
detected_booth,
payphone_models,
GetEntityCoords(ped),
Config.Payphones.ServerValidationDistance
)
if not location then
@@ -1129,7 +1118,7 @@ Bridge.Callbacks.Register("sky_phone:payphone:dial", function(source, data)
if not Config.Payphones.Enabled or not SkyPhone.AllowOperation(source, "payphone_dial", 15, 60) then
return { success = false, error = "rate_limited" }
end
local booth_coords, booth_model = valid_payphone_position(source)
local booth_coords, booth_model = valid_payphone_position(source, data)
if not booth_coords then
return { success = false, error = "invalid_payphone" }
end
+12 -47
View File
@@ -12,7 +12,8 @@ end
local function normalize_coordinates(value, allow_vector)
local value_type = type(value)
if value_type ~= "table" and (not allow_vector or value_type ~= "vector3") then
local supported_vector = value_type == "vector3" or value_type == "vector4"
if value_type ~= "table" and (not allow_vector or not supported_vector) then
return nil
end
@@ -46,60 +47,24 @@ local function normalize_location(location, allowed_models)
}
end
function SkyPhonePayphones.ValidateLocations(locations, allowed_models)
if type(locations) ~= "table" or type(allowed_models) ~= "table" then
return {}, 0
end
local validated = {}
local rejected = 0
for index, location in pairs(locations) do
local valid_index = type(index) == "number" and index >= 1 and index % 1 == 0
local normalized = valid_index and normalize_location(location, allowed_models) or nil
if normalized then
normalized.index = index
validated[#validated + 1] = normalized
else
rejected = rejected + 1
end
end
table.sort(validated, function(left, right)
return left.index < right.index
end)
for index = 1, #validated do
validated[index].index = nil
end
return validated, rejected
end
function SkyPhonePayphones.FindNearest(locations, player_coords, maximum_distance)
if type(locations) ~= "table" then
function SkyPhonePayphones.ValidateDetected(location, allowed_models, player_coords, maximum_distance)
if type(allowed_models) ~= "table" then
return nil
end
local normalized = normalize_location(location, allowed_models)
local coords = normalize_coordinates(player_coords, true)
local distance = finite_number(maximum_distance)
if not coords or not distance or distance <= 0 then
if not normalized or not coords or not distance or distance <= 0 then
return nil
end
local nearest = nil
local nearest_distance_squared = distance * distance
for index = 1, #locations do
local location = locations[index]
if type(location) == "table" and type(location.coords) == "table" then
local dx = coords.x - location.coords.x
local dy = coords.y - location.coords.y
local dz = coords.z - location.coords.z
local distance_squared = dx * dx + dy * dy + dz * dz
if distance_squared <= nearest_distance_squared then
nearest = location
nearest_distance_squared = distance_squared
end
end
local dx = coords.x - normalized.coords.x
local dy = coords.y - normalized.coords.y
local dz = coords.z - normalized.coords.z
if dx * dx + dy * dy + dz * dz > distance * distance then
return nil
end
return nearest
return normalized
end
+29 -23
View File
@@ -5,35 +5,41 @@ local allowed_models = {
prop_phonebox_04 = true,
}
local locations, rejected = SkyPhonePayphones.ValidateLocations({
local detected = SkyPhonePayphones.ValidateDetected(
{ model = "prop_phonebox_01a", coords = { x = 100.0, y = 200.0, z = 30.0 } },
{ model = "prop_phonebox_04", coords = { x = 105.0, y = 200.0, z = 30.0 } },
{ model = "not_allowed", coords = { x = 100.0, y = 200.0, z = 30.0 } },
{ model = "prop_phonebox_01a", coords = { x = "100", y = 200.0, z = 30.0 } },
{ model = "prop_phonebox_01a", coords = { x = 10001.0, y = 200.0, z = 30.0 } },
"malformed",
}, allowed_models)
assert(#locations == 2, "only strictly valid configured locations must be accepted")
assert(rejected == 4, "every malformed or disallowed configured location must be reported")
local first = SkyPhonePayphones.FindNearest(locations, { x = 101.0, y = 200.0, z = 30.0 }, 3.0)
assert(first and first.model == "prop_phonebox_01a", "nearest configured booth must be selected")
local second = SkyPhonePayphones.FindNearest(locations, { x = 104.0, y = 200.0, z = 30.0 }, 3.0)
assert(second and second.model == "prop_phonebox_04", "another configured booth must be selected by proximity")
allowed_models,
{ x = 101.0, y = 200.0, z = 30.0 },
3.0
)
assert(detected and detected.model == "prop_phonebox_01a", "a nearby detected booth must be accepted")
assert(detected.coords.x == 100.0, "validated booth coordinates must be preserved")
assert(
not SkyPhonePayphones.FindNearest(locations, { x = 0.0, y = 0.0, z = 0.0 }, 3.0),
"a player away from every configured booth must be rejected"
not SkyPhonePayphones.ValidateDetected(
{ model = "not_allowed", coords = { x = 100.0, y = 200.0, z = 30.0 } },
allowed_models,
{ x = 101.0, y = 200.0, z = 30.0 },
3.0
),
"a detected booth with a disallowed model must be rejected"
)
assert(
not SkyPhonePayphones.FindNearest(locations, { x = "100", y = 200.0, z = 30.0 }, 3.0),
"malformed player coordinates must be rejected"
not SkyPhonePayphones.ValidateDetected(
{ model = "prop_phonebox_01a", coords = { x = "100", y = 200.0, z = 30.0 } },
allowed_models,
{ x = 101.0, y = 200.0, z = 30.0 },
3.0
),
"malformed detected booth coordinates must be rejected"
)
assert(
not SkyPhonePayphones.FindNearest(locations, { x = 100.0, y = 200.0, z = 30.0 }, 0.0),
"an invalid validation distance must be rejected"
not SkyPhonePayphones.ValidateDetected(
{ model = "prop_phonebox_01a", coords = { x = 100.0, y = 200.0, z = 30.0 } },
allowed_models,
{ x = 110.0, y = 200.0, z = 30.0 },
3.0
),
"a player away from the detected booth must be rejected"
)
print("Server payphone validation tests passed")
print("Detected payphone validation tests passed")