From 13ddeb893b9318d52979411dc1fed48fa2f3afc5 Mon Sep 17 00:00:00 2001 From: Eichenholz Date: Wed, 5 Aug 2026 17:43:42 +0200 Subject: [PATCH] ADD - calls app and sim cards --- README.md | 5 +- frontend/src/App.vue | 71 ++- frontend/src/assets/img/app-icons/phone.webp | Bin 0 -> 7694 bytes frontend/src/assets/img/app-icons/sms.webp | Bin 0 -> 4056 bytes frontend/src/assets/main.css | 197 +++++++ frontend/src/components/SimPhonePicker.vue | 117 +++++ frontend/src/config/apps.test.ts | 18 +- frontend/src/config/apps.ts | 35 +- frontend/src/stores/calls.test.ts | 71 +++ frontend/src/stores/calls.ts | 108 ++++ frontend/src/stores/phone.ts | 72 ++- frontend/src/types/apps.ts | 1 + frontend/src/types/device.ts | 2 + frontend/src/types/phone.ts | 51 ++ frontend/src/utils/phone.test.ts | 15 + frontend/src/utils/phone.ts | 12 + frontend/src/utils/preferences.ts | 1 + frontend/src/views/apps/PhoneApp.vue | 434 +++++++++++++++ frontend/src/views/apps/SettingsApp.vue | 104 +++- frontend/testserver/index.cjs | 8 + sky_phone/config/config.lua | 15 + sky_phone/config/locales/en.lua | 32 ++ sky_phone/fxmanifest.lua | 6 +- sky_phone/source/client/main.lua | 83 ++- sky_phone/source/html/index.html | 4 +- sky_phone/source/server/calls.lua | 521 +++++++++++++++++++ sky_phone/source/server/db_migrate.lua | 104 ++++ sky_phone/source/server/phone.lua | 44 +- sky_phone/source/server/sim.lua | 315 +++++++++++ sky_phone/source/shared/sim_number.lua | 59 +++ tests/sim_number.lua | 19 + 31 files changed, 2470 insertions(+), 54 deletions(-) create mode 100644 frontend/src/assets/img/app-icons/phone.webp create mode 100644 frontend/src/assets/img/app-icons/sms.webp create mode 100644 frontend/src/components/SimPhonePicker.vue create mode 100644 frontend/src/stores/calls.test.ts create mode 100644 frontend/src/stores/calls.ts create mode 100644 frontend/src/types/phone.ts create mode 100644 frontend/src/utils/phone.test.ts create mode 100644 frontend/src/utils/phone.ts create mode 100644 frontend/src/views/apps/PhoneApp.vue create mode 100644 sky_phone/source/server/calls.lua create mode 100644 sky_phone/source/server/sim.lua create mode 100644 sky_phone/source/shared/sim_number.lua create mode 100644 tests/sim_number.lua diff --git a/README.md b/README.md index 52521b1..634fa99 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,16 @@ Standalone FiveM phone built with Vue 3, TypeScript, Pinia, Vue Router, Konsta UI 5, and Tailwind CSS 4. Each non-stackable `sky_phone` item receives a unique 15-digit IMEI and owns its server-persisted device state. The phone opens through the usable item; `/phone` is disabled unless `Config.Phone.DevelopmentCommand` is enabled explicitly. -An iFruit account is optional. Unlinked devices retain local settings, alarms, media, apps, and notes. Linking from Mail or Settings moves local notes into the account and exposes the same notes and mailbox on every linked device. Signing out hides cloud data without deleting it, while a factory reset removes only the current device's local data and account link. +An iFruit account is optional. Unlinked devices retain local settings, alarms, media, apps, notes, contacts, and recent calls. Linking from Mail or Settings moves local data into an empty cloud account; an existing cloud dataset wins over local contacts and recents. Signing out keeps an editable local snapshot without deleting cloud data. ## Requirements - `sky_base` with a slot-aware inventory adapter implementing `GetInventorySlot`, `GetInventorySlotsWithItem`, and `SetInventorySlotMetadata`. +- `sky_jobs_base` for the authoritative character identifier captured by registered SIM cards. - A non-stackable inventory item named `sky_phone`. +- Two unique, non-stackable inventory items named `sky_phone_sim_registered` and `sky_phone_sim_anonymous`. Their metadata is initialized automatically on first use, so shops and crafting recipes add plain items without supplying a number. - MySQL/MariaDB through the database driver configured in `sky_base`. +- `pma-voice` when `Config.Calls.VoiceProvider` is set to `"pma"`. Database migrations run automatically. Existing `sky_phone_mail_accounts` installations are renamed to `sky_phone_accounts` while preserving account IDs and mail foreign keys. iFruit passwords are intentional in-character credentials and remain plaintext `VARCHAR(64)` values; registration screens warn players never to reuse a real password. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6d711d9..377bd4e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,8 +15,12 @@ import PhoneLockScreen from '@/components/PhoneLockScreen.vue' import PhoneNotifications from '@/components/PhoneNotifications.vue' import NotificationPhonePreview from '@/components/NotificationPhonePreview.vue' import PhoneStatusBar from '@/components/PhoneStatusBar.vue' +import SimPhonePicker, { + type SimPhoneChoice, +} from '@/components/SimPhonePicker.vue' import { PHONE_FRAME_IMAGES } from '@/config/appearance' import { useClockStore } from '@/stores/clock' +import { useCallsStore } from '@/stores/calls' import { useAccountStore } from '@/stores/account' import { useMailStore } from '@/stores/mail' import { useMediaStore } from '@/stores/media' @@ -28,6 +32,7 @@ import { import { usePhoneStore, type PhoneOpenPayload } from '@/stores/phone' import type { PhoneNotificationDevicePayload } from '@/types/device' import type { MailCounts } from '@/types/mail' +import type { PhoneCall } from '@/types/phone' import { nuiCall } from '@/utils/nui' import { formatTimer } from '@/utils/clock' import { parsePhonePreferences } from '@/utils/preferences' @@ -35,7 +40,12 @@ import SpringboardView from '@/views/SpringboardView.vue' type AppMessage = { type?: string - data?: MailEventData | PhoneNotificationInput | PhoneOpenPayload + data?: MailEventData | PhoneCall | PhoneNotificationInput | PhoneOpenPayload +} + +type SimPickerPayload = { + choices: SimPhoneChoice[] + number: string } type MailEventData = { @@ -55,6 +65,7 @@ const isDevelopment = import.meta.env.DEV const phone = usePhoneStore() const account = useAccountStore() const clock = useClockStore() +const calls = useCallsStore() const mail = useMailStore() const media = useMediaStore() const notes = useNotesStore() @@ -64,6 +75,7 @@ const router = useRouter() const isAppRoute = computed(() => route.name === 'app') const isLocked = ref(false) const isUnlocking = ref(false) +const simPicker = ref(null) const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)') const viewportScale = ref(getViewportScale()) const phoneBaseZoom = computed(() => viewportScale.value * PHONE_BASE_SCALE) @@ -94,6 +106,7 @@ function hydratePhone(payload: PhoneOpenPayload): void { clock.hydrate(payload.device?.data.alarms?.payload) media.hydrate(payload.device?.data.media?.payload) void mail.bootstrap(payload.account?.email ?? '') + void calls.bootstrap() } function onMessage(event: MessageEvent): void { @@ -136,6 +149,23 @@ function onMessage(event: MessageEvent): void { } } notifications.show(notification) + } else if (event.data?.type === 'contacts:changed') { + void calls.loadContacts() + } else if (event.data?.type === 'calls:changed') { + void calls.loadRecents() + } else if ( + (event.data?.type === 'call:incoming' || + event.data?.type === 'call:state') && + event.data.data + ) { + calls.applyCallState(event.data.data as PhoneCall) + isLocked.value = false + isUnlocking.value = false + 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') { + simPicker.value = null } } @@ -210,17 +240,43 @@ onMounted(() => { data: {}, imei: '356938035643809', name: 'iFruit Phone', + sim: { + id: 'development-sim', + number: '5551234567', + registered: true, + type: 'registered', + }, }, notes: [], token: 'development', }) + if (new URLSearchParams(window.location.search).has('simPickerPreview')) { + simPicker.value = { + choices: [ + { + imei: '356938035643809', + name: 'Personal Phone', + occupied: false, + }, + { + imei: '356938035643810', + name: 'Work Phone', + number: '5559876543', + occupied: true, + }, + ], + number: '5551234567', + } + } } }) watch( - () => notifications.requiresAttention, - (active) => { - void nuiCall('notification:focus', { active }) + [() => notifications.requiresAttention, () => calls.activeCall], + ([requiresAttention, activeCall]) => { + void nuiCall('notification:focus', { + active: requiresAttention || activeCall !== null, + }) }, ) @@ -253,11 +309,18 @@ onBeforeUnmount(() => {