diff --git a/README.md b/README.md index 9786b89..a23642c 100644 --- a/README.md +++ b/README.md @@ -88,16 +88,68 @@ set sky_phone_fliptok_password_pepper "replace-with-a-long-random-secret" Passwords are stored as salted hashes. The pepper is read server-side from the convar and is never included in the NUI bundle. -Standalone FiveM phone built with Vue 3, TypeScript, Pinia, Vue Router, Konsta UI 5, and Tailwind CSS 4. Each non-stackable `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. +Standalone FiveM phone built with Vue 3, TypeScript, Pinia, Vue Router, Konsta UI 5, and Tailwind CSS 4. The phone opens through the usable item; `/phone` is disabled unless `Config.Phone.DevelopmentCommand` is enabled explicitly. Phone identity and SIM-card behavior are selected independently through `Config.Phone.Unique` and `Config.Sim.Enabled`. 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. +## Phone identity and SIM modes + +Both compatibility switches default to the physical, unique-item behavior: + +```lua +Config.Phone.Unique = true +Config.Sim.Enabled = true +``` + +For backwards compatibility, an omitted switch is also treated as `true`. + +With `Config.Phone.Unique = true`, every phone item is one transferable physical Device. Its unique +15-digit IMEI is stored in item metadata, so its settings, PIN, local app data, linked iFruit +account, and installed SIM travel with that item. The item must be non-stackable. + +With `Config.Phone.Unique = false`, the server assigns each framework character one persistent +virtual Device. Possessing any configured phone item grants access to that Device, but the item does +not own its IMEI or data. Losing a handset therefore removes access only until the character obtains +another one; the replacement opens the same data, PIN, account session, and number. Phone items may +be stackable in this mode, although at least one phone item is still required to use or receive the +phone. The character mapping is server-authoritative and uses the framework character identifier, +never a client-supplied owner value. + +With `Config.Sim.Enabled = true`, a physical registered or anonymous SIM item must be inserted before +the Device has cellular service. SIM items remain unique and non-stackable. With +`Config.Sim.Enabled = false`, SIM inventory items are not required. The first time a Device without +an attached SIM is resolved, the server creates an anonymous virtual SIM with a random unique phone +number using `Config.Sim.NumberPrefix` and `NumberLength`; `NumberGroups` controls its display +format. Physical SIM insertion and ejection are disabled in this mode. An already attached physical +SIM and its number are preserved when SIM requirements are disabled. + +The switches are independent. An automatic number follows a physical handset when unique phones are +enabled and follows the character's persistent virtual Device when unique phones are disabled. +Likewise, a physical SIM inserted while unique phones are disabled belongs to the character's +virtual Device and remains available after replacing the handset. + +### Existing installations and first use + +After changing an existing installation to `Config.Phone.Unique = false`, the first phone item a +character uses establishes the persistent mapping. If that item has a valid legacy IMEI which is not +already mapped to another character, the complete existing Device is adopted. This preserves its +local content, PIN, linked account, and attached SIM. If the legacy IMEI cannot be adopted, the +server creates a fresh virtual Device instead. Historic unique Devices did not record an owner, so +the character carrying a handset on its first use after the change is the character that claims it. + +When `Config.Sim.Enabled = false`, an existing attached physical SIM is kept; only Devices without a +SIM receive a random virtual one. If SIM cards are enabled again, automatically created virtual SIMs +are detached during resource startup so those Devices once again require a physical SIM. Stored +character-to-Device mappings remain available, but changing back to unique phones does not copy a +virtual Device's data onto an arbitrary inventory item. Treat production mode changes as migrations +and restart the resource after updating the configuration. + ## Requirements - ESX Legacy (`es_extended`), Qbox (`qbx_core`), or QBCore (`qb-core`). The bridge selects a running supported framework when `Config.Bridge.Framework` is set to `"auto"`. - A supported inventory: `ox_inventory`, `qb-inventory`, `lj-inventory`, `qs-inventory`, `codem-inventory`, `core_inventory`, `mf-inventory`, or `smx-inventory`. The bridge auto-detects a running provider and normalizes metadata, slots, counts, item mutations, and usable-item callbacks. `mf-inventory` and `smx-inventory` require ESX. Because SMX stores standard ESX items as stacks, its adapter persists one active Phone/SIM metadata record per player and item type in ESX player metadata. -- A non-stackable inventory item named `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. +- An inventory item named `phone`. It must be non-stackable when `Config.Phone.Unique = true` and may be stackable when it is `false`. +- When `Config.Sim.Enabled = true`, 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. These item definitions are not required when SIM cards are disabled. - `oxmysql` with MySQL/MariaDB. - `pma-voice` when `Config.Calls.VoiceProvider` is set to `"pma"`. - A FiveManage V3 Media API token for Camera photo/video uploads and Gallery deletion. Set the @@ -118,7 +170,7 @@ GIPHY provides trending and searched GIFs through a paginated server-side proxy. only the server uses the GIPHY key. Photo and video actions in Messages are intentionally inactive until their dedicated implementation is available. -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. +Database migrations run automatically. Existing `sky_phone_mail_accounts` installations are renamed to `sky_phone_accounts` while preserving account IDs and mail foreign keys. The migration also creates `sky_phone_character_devices` for persistent non-unique phone mappings and marks automatic SIMs through `sky_phone_sims.is_virtual`. iFruit passwords are intentional in-character credentials and remain plaintext `VARCHAR(64)` values; registration screens warn players never to reuse a real password. Camera and Gallery media is stored in `sky_phone_media`. Signed-out captures belong to the current IMEI; linking an iFruit account moves those rows into the account gallery so every linked phone sees @@ -137,13 +189,13 @@ Configure frequency bounds and precision, restricted channel ranges and allowed Radio profiles are stored in `sky_phone_radio_profiles`. Runtime migration creates the table automatically; fresh installations receive it through `sky_phone/sql/install.sql`. -Inventory metadata has no framework-wide standard: providers differ in export names, callback payloads, slot handling, and whether metadata is called `metadata` or `info`. For that reason, `sky_phone` uses explicit provider adapters instead of guessing exports at runtime. Every supported adapter implements slot lookup, item lookup, metadata replacement, capacity handling, add/remove operations, and usable-item registration. Providers without a separate capacity export use their authoritative add operation as the final capacity gate. +Inventory metadata has no framework-wide standard: providers differ in export names, callback payloads, slot handling, and whether metadata is called `metadata` or `info`. For that reason, `sky_phone` uses explicit provider adapters instead of guessing exports at runtime. Every supported adapter implements slot lookup, item lookup, metadata replacement, capacity handling, add/remove operations, and usable-item registration. Providers without a separate capacity export use their authoritative add operation as the final capacity gate. Phone item metadata is authoritative only when `Config.Phone.Unique = true`; in non-unique mode the persistent character mapping is authoritative instead. -When a SIM is ejected or replaced, the returned inventory item is rebuilt from the authoritative `sky_phone_sims` row. Its metadata contains `sim_metadata_version`, `sim_id`, `phone_number`, `formatted_number`, and `sim_type`. Registered SIMs additionally contain `firstname`, `lastname`, `birthdate`, and `registered_at`. The internal framework owner identifier remains database-only. Inserting the item again resolves the SIM by `sim_id`; contacts and device/cloud data remain attached to their existing phone-owned persistence instead of being copied into inventory metadata. +When physical SIMs are enabled and a SIM is ejected or replaced, the returned inventory item is rebuilt from the authoritative `sky_phone_sims` row. Its metadata contains `sim_metadata_version`, `sim_id`, `phone_number`, `formatted_number`, and `sim_type`. Registered SIMs additionally contain `firstname`, `lastname`, `birthdate`, and `registered_at`. The internal framework owner identifier remains database-only. Inserting the item again resolves the SIM by `sim_id`; contacts and device/cloud data remain attached to their existing Device or cloud persistence instead of being copied into inventory metadata. Automatically created virtual SIMs remain database-only and never become inventory items. -For `ox_inventory`, configure all three items with `stack = false` and `consume = 0`. Do not configure a client event or export. Ox then completes its normal server-authoritative use flow and emits `ox_inventory:usedItem`; the bridge resolves the authoritative slot again and only opens the matching device or SIM. A client export would return before Ox calls `useItem` and therefore prevent `ox_inventory:usedItem` from being emitted. +For `ox_inventory`, configure the phone with `stack = false` when `Config.Phone.Unique = true`; it may use `stack = true` in non-unique mode. Physical SIM items always use `stack = false` and are only needed when `Config.Sim.Enabled = true`. Every usable item should use `consume = 0`. Do not configure a client event or export. Ox then completes its normal server-authoritative use flow and emits `ox_inventory:usedItem`; the bridge resolves the authoritative slot again and only opens the matching Device or SIM. A client export would return before Ox calls `useItem` and therefore prevent `ox_inventory:usedItem` from being emitted. -Example `ox_inventory/data/items.lua` entries: +Example `ox_inventory/data/items.lua` entries for the default unique-phone, physical-SIM modes: ```lua ["phone"] = { @@ -169,7 +221,7 @@ Example `ox_inventory/data/items.lua` entries: }, ``` -For QBCore-style item tables, define the same names with `unique = true`, `useable = true`, and `shouldClose = true`. The provider adapter registers the server-side usable callbacks; no `lb-phone` event or export is used. +For QBCore-style item tables, set the phone's `unique` value to match `Config.Phone.Unique`, and use `useable = true` and `shouldClose = true`. When physical SIMs are enabled, define both SIM items with `unique = true`, `useable = true`, and `shouldClose = true`; omit them when SIMs are disabled. The provider adapter registers the server-side usable callbacks; no `lb-phone` event or export is used. The homescreen is an original implementation inspired by the interaction and layout concepts in [lukejacksonn/homescreen](https://github.com/lukejacksonn/homescreen), inspected at commit [`98a812f`](https://github.com/lukejacksonn/homescreen/tree/98a812f4f7c33594e791d65092f73b8f54b3c598). No source code or image assets from that project are included. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index dee09c2..e0e15e2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -261,6 +261,9 @@ const notifications = useNotificationsStore() const route = useRoute() const router = useRouter() const isAppRoute = computed(() => route.name === 'app') +const showActiveCallReturn = computed( + () => Boolean(calls.activeCall) && route.params.appId !== 'phone', +) const appTransitionName = computed(() => route.query.transition === 'app-switch' ? 'app-switch' : 'app-window', ) @@ -407,6 +410,7 @@ async function hydrateDevelopmentPhone(): Promise { sim: { id: 'development-sim', number: '5551234567', + removable: true, registered: true, type: 'registered', }, @@ -974,6 +978,12 @@ function lockPhone(): void { isLocked.value = true } +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' @@ -1205,8 +1215,10 @@ onBeforeUnmount(() => { > diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index fb69afa..6ce0c77 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -467,6 +467,47 @@ button { cursor: pointer; pointer-events: auto; } +.phone-status-bar__time-button--active-call { + isolation: isolate; + text-shadow: none; +} +.phone-status-bar__time-button--active-call::before { + position: absolute; + z-index: -1; + top: 19px; + left: calc(50% - 3px); + width: 90px; + height: 30px; + border-radius: 15px; + background: #34c759; + box-shadow: 0 2px 8px #0b6f2b66; + animation: phone-active-call-pill-in 220ms cubic-bezier(0.22, 1, 0.36, 1); + content: ''; + transform-origin: center; + transform: translateX(-50%); + transition: transform 140ms ease; +} +.phone-status-bar__time-button--active-call:active::before { + transform: translateX(-50%) scale(0.94); +} +.phone-status-bar__active-call-icon { + position: absolute; + top: 27px; + left: calc(50% - 39px); + width: 13px; + height: 13px; + stroke-width: 2.5; +} +@keyframes phone-active-call-pill-in { + from { + opacity: 0; + transform: translateX(-50%) scale(0.82); + } + to { + opacity: 1; + transform: translateX(-50%) scale(1); + } +} .phone-status-bar__indicators { position: absolute; top: 0; diff --git a/frontend/src/components/PhoneStatusBar.vue b/frontend/src/components/PhoneStatusBar.vue index 78d6706..99c7902 100644 --- a/frontend/src/components/PhoneStatusBar.vue +++ b/frontend/src/components/PhoneStatusBar.vue @@ -1,17 +1,23 @@