diff --git a/frontend/src/components/PhoneControlCenter.vue b/frontend/src/components/PhoneControlCenter.vue index 6c62f6a..d1b1689 100644 --- a/frontend/src/components/PhoneControlCenter.vue +++ b/frontend/src/components/PhoneControlCenter.vue @@ -10,6 +10,7 @@ import { Pause, Plane, Play, + RadioTower, Signal, SkipBack, SkipForward, @@ -30,6 +31,8 @@ import { useRouter } from 'vue-router' import { usePhoneStore } from '@/stores/phone' import { useMusicStore } from '@/stores/music' +import { useEasyShareStore } from '@/stores/easyshare' +import type { EasySharePayload } from '@/types/easyshare' import { nuiCall } from '@/utils/nui' const props = defineProps<{ opened: boolean }>() @@ -44,6 +47,7 @@ type ConnectivityPreference = const phone = usePhoneStore() const music = useMusicStore() +const easyShare = useEasyShareStore() const router = useRouter() const panel = ref(null) const brightness = ref(phone.preferences.settings.screenBrightness) @@ -56,6 +60,7 @@ const volume = ref( ) const flashlightActive = ref(false) const flashlightPending = ref(false) +const easySharePending = ref(false) const previousAlertVolume = ref(volume.value || 75) const inactiveGlassColors = { @@ -87,6 +92,10 @@ const flashlightColors = computed(() => } : inactiveGlassColors, ) +const easyShareColors = { + bgIos: 'bg-[#0a84ff]', + shadowIos: 'shadow-ios-dark-glass', +} const brightnessStyle = computed(() => ({ '--control-level': `${brightness.value}%`, })) @@ -202,6 +211,23 @@ async function toggleFlashlight(): Promise { flashlightPending.value = false } +async function shareOwnContact(): Promise { + if (easySharePending.value) return + easySharePending.value = true + const response = await nuiCall('easyshare:own-contact') + easySharePending.value = false + if (!response.success || !response.data) { + console.error( + '[ControlCenter] Could not load own EasyShare contact.', + response.error, + ) + return + } + emit('close') + easyShare.open(response.data) + await easyShare.showNearby() +} + function openApp(path: string): void { emit('close') void router.push(path) @@ -360,9 +386,7 @@ onBeforeUnmount(() => {
-
+
{
+
+ + +
+ { justify-content: center; } -.control-center__round-control--wide { - grid-column: 1 / span 2; -} - .control-center__round-control > span, .control-center__quick-action > span { width: 100%; @@ -746,6 +780,11 @@ onBeforeUnmount(() => { color: #ff453a; } +.control-center__round-button--easyshare { + background: #0a84ff !important; + box-shadow: 0 8px 22px rgb(10 132 255 / 34%) !important; +} + .control-center__focus-button { display: flex; grid-column: 1 / span 2; diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 66b7e9c..f7adedd 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -3375,6 +3375,7 @@ const defaultLocales: LocaleTree = { camera: 'Camera', cellular: 'Cellular Data', close: 'Close Control Center', + easyShareContact: 'Share my contact with nearby players', flashlight: 'Flashlight', focus: 'Focus', label: 'Control Center', diff --git a/frontend/testserver/index.cjs b/frontend/testserver/index.cjs index 65e699d..ea32d60 100644 --- a/frontend/testserver/index.cjs +++ b/frontend/testserver/index.cjs @@ -6868,6 +6868,21 @@ app.post('/api/:endpoint', (request, response) => { }) return } + if (endpoint === 'easyshare:own-contact') { + response.json({ + success: true, + data: { + appId: 'phone', + copyText: 'Alex Morgan\n5551234567', + id: 'self', + kind: 'contact', + meta: { name: 'Alex Morgan', phoneNumber: '5551234567' }, + subtitle: '5551234567', + title: 'Alex Morgan', + }, + }) + return + } if (endpoint === 'easyshare:set-visibility') { easyShareVisibility = request.body.visibility response.json({ success: true, data: { visibility: easyShareVisibility } }) diff --git a/sky_phone/config/locales/en.lua b/sky_phone/config/locales/en.lua index f9cd31d..6a7cfe0 100644 --- a/sky_phone/config/locales/en.lua +++ b/sky_phone/config/locales/en.lua @@ -72,7 +72,7 @@ Locales["en"] = { }, ControlCenter = { airplaneMode = "Airplane Mode", bluetooth = "Bluetooth", brightness = "Brightness", calculator = "Calculator", - camera = "Camera", cellular = "Cellular Data", close = "Close Control Center", flashlight = "Flashlight", + camera = "Camera", cellular = "Cellular Data", close = "Close Control Center", easyShareContact = "Share my contact with nearby players", flashlight = "Flashlight", focus = "Focus", label = "Control Center", media = "Media", muteRingtone = "Mute ringtone and notifications", next = "Next track", notPlaying = "Not Playing", open = "Open Control Center", play = "Play", previous = "Previous track", quickActions = "Quick actions", timer = "Timer", unmuteRingtone = "Unmute ringtone and notifications", volume = "Volume", wifi = "Wi-Fi", diff --git a/sky_phone/source/client/main.lua b/sky_phone/source/client/main.lua index 047e897..4d73d97 100644 --- a/sky_phone/source/client/main.lua +++ b/sky_phone/source/client/main.lua @@ -217,6 +217,7 @@ local server_callbacks = { "messages:delete", "messages:gifs", "easyshare:bootstrap", + "easyshare:own-contact", "easyshare:set-visibility", "easyshare:request", "easyshare:respond", diff --git a/sky_phone/source/server/easyshare.lua b/sky_phone/source/server/easyshare.lua index 1673621..44d694b 100644 --- a/sky_phone/source/server/easyshare.lua +++ b/sky_phone/source/server/easyshare.lua @@ -604,25 +604,36 @@ local function sanitize_payload(source, device, data) local condition, owner_params = owner_condition(device) if data.kind == "contact" and type(payload.id) == "string" then - local params = { payload.id } - append_params(params, owner_params) - local rows = Bridge.Database.Query(([[ - SELECT `name`, `phone_number`, `organization`, `notes` - FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s LIMIT 1 - ]]):format(condition), params) - local contact = rows[1] - if not contact then - return nil, "not_owned" + if app_id == "phone" and payload.id == "self" then + local contact = canonical_own_contact(source, device) + if not contact then + return nil, "sim_required" + end + payload.title = contact.title + payload.subtitle = contact.subtitle + payload.copyText = contact.copyText + payload.meta = contact.meta + else + local params = { payload.id } + append_params(params, owner_params) + local rows = Bridge.Database.Query(([[ + SELECT `name`, `phone_number`, `organization`, `notes` + FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s LIMIT 1 + ]]):format(condition), params) + local contact = rows[1] + if not contact then + return nil, "not_owned" + end + payload.title = contact.name + payload.subtitle = contact.phone_number + payload.copyText = ("%s\n%s"):format(contact.name, contact.phone_number) + payload.meta = { + name = contact.name, + notes = contact.notes, + organization = contact.organization, + phoneNumber = contact.phone_number, + } end - payload.title = contact.name - payload.subtitle = contact.phone_number - payload.copyText = ("%s\n%s"):format(contact.name, contact.phone_number) - payload.meta = { - name = contact.name, - notes = contact.notes, - organization = contact.organization, - phoneNumber = contact.phone_number, - } elseif data.kind == "note" and type(payload.id) == "string" then local params = { payload.id } append_params(params, owner_params) @@ -808,6 +819,25 @@ local function apply_received_payload(transfer) return true end +local function canonical_own_contact(source, device) + if not device.phone_number then + return nil + end + local name = display_name(source) + return { + appId = "phone", + kind = "contact", + id = "self", + title = name, + subtitle = device.phone_number, + copyText = ("%s\n%s"):format(name, device.phone_number), + meta = { + name = name, + phoneNumber = device.phone_number, + }, + } +end + local function advance_transfer(id) local transfer = active_transfers[id] if not transfer or transfer.status ~= "transferring" then @@ -890,6 +920,24 @@ Bridge.Callbacks.Register("sky_phone:easyshare:bootstrap", function(source) } end) +Bridge.Callbacks.Register("sky_phone:easyshare:own-contact", function(source) + if not Config.EasyShare.Enabled then + return { success = false, error = "disabled" } + end + if not SkyPhone.AllowOperation(source, "easyshare_own_contact", Config.EasyShare.BootstrapRequestsPerMinute, 60) then + return { success = false, error = "rate_limited" } + end + local device, error_response = current_device(source) + if not device then + return error_response + end + local payload = canonical_own_contact(source, device) + if not payload then + return { success = false, error = "sim_required" } + end + return { success = true, data = payload } +end) + Bridge.Callbacks.Register("sky_phone:easyshare:set-visibility", function(source, data) if not Config.EasyShare.Enabled then return { success = false, error = "disabled" }