From 2cf15c70be14a8f8d300dbeaecfd2ca5f96a6ff1 Mon Sep 17 00:00:00 2001 From: DerEchteAlec Date: Thu, 13 Aug 2026 03:10:19 +0200 Subject: [PATCH] ENH - migrate Radio to Sky UI --- frontend/src/assets/img/app-icons/radio.svg | 15 - frontend/src/assets/img/app-icons/radio.webp | Bin 0 -> 20858 bytes frontend/src/config/apps.ts | 2 +- frontend/src/stores/radio.test.ts | 279 ++++++++ frontend/src/stores/radio.ts | 23 +- frontend/src/views/apps/RadioApp.vue | 678 +++++++++---------- 6 files changed, 611 insertions(+), 386 deletions(-) delete mode 100644 frontend/src/assets/img/app-icons/radio.svg create mode 100644 frontend/src/assets/img/app-icons/radio.webp create mode 100644 frontend/src/stores/radio.test.ts diff --git a/frontend/src/assets/img/app-icons/radio.svg b/frontend/src/assets/img/app-icons/radio.svg deleted file mode 100644 index 001fab2..0000000 --- a/frontend/src/assets/img/app-icons/radio.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/img/app-icons/radio.webp b/frontend/src/assets/img/app-icons/radio.webp new file mode 100644 index 0000000000000000000000000000000000000000..bbce3fad008dd7e9847f2cc1fc18cb89171c2b77 GIT binary patch literal 20858 zcmV()K;OSoNk&F;Q2+o}MM6+kP&goFQ2+qYWC5K4DgXii0zN$+jYOg$p{67fX{f*l ziDNUwtW)~4Dcv6|-FevYKr2d==m^_s>uS?rAKqJbYqe+Qq@?mMi=P$oZ{<(iU-&$+ z_pkY0Zr*$Qzx%)NKagKG|L}VM{>}dH{@=K7>&N`>?moL;vp)bob3H|V$$#DdVeaex z)BjKU4|QMGKg@sT|7ZT!&R-wglT zKjVJpdsct!_5lCO>H*YWAN@V?C($RxbaK+)YMP>a=kl-fe>MIr_gBDc+q?AO8|)YS z&q80#|E1{v*iT>|=l|P(-1`;a^Y=gd9>AZ=f3bh!{`L7m^3(sX`)^6#SO4($8UBU+ z>;9*&N3gHnKV-k#(cPR?aL`6pKJ`fj2uOKvRxy!ylNPmBTVj-HCnV?+|DVwOwGxMd zX-UJ%=Jb&29z`RYvFWP*JL^Dgc{B;g7#8sXlv^|@`hN(~*$lfP;p2=gEchXL#g28u zWl2!^HbR#3Z@HYNJ1y$^{SuCSkx0CNT(P zt>jYReeG=R)BgCdrA7<$`;jJj2=$t=Iq{ONW0`n~KZ^}tGxefcJ(A!KoZ^Iy+Hx`= z0vyPL@5H`-@@+3qq)X3O3pA3YL0oN{Av0uW2HD{XA^H|haeCwb^Wb>2u-MQr!F@{# zbxJGxuXM7JEkiR{VdYVZQ-+^Z$%0 zO^1IWu8lRu*6-oiVpga0(Nzk4-qW&r^JAw;R$W6`cRn&Jn}kni-IWydZq^ncSU+b% ztpu*)RM=@R|LM7Z&Qh~vh3;
iI&)p7X3P(zF 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 VYvGVG%Ctw&?_s6$Lq%=s002J%yiEWA literal 0 HcmV?d00001 diff --git a/frontend/src/config/apps.ts b/frontend/src/config/apps.ts index 4052fd4..9cd0a33 100644 --- a/frontend/src/config/apps.ts +++ b/frontend/src/config/apps.ts @@ -47,7 +47,7 @@ import mapIcon from '@/assets/img/app-icons/map.webp' import messagesIcon from '@/assets/img/app-icons/sms.webp' import darkChatIcon from '@/assets/img/app-icons/darkchat.webp' import notesIcon from '@/assets/img/app-icons/notes.webp' -import radioIcon from '@/assets/img/app-icons/radio.svg' +import radioIcon from '@/assets/img/app-icons/radio.webp' import photosIcon from '@/assets/img/app-icons/gallery.webp' import phoneIcon from '@/assets/img/app-icons/phone.webp' import settingsIcon from '@/assets/img/app-icons/settings.svg' diff --git a/frontend/src/stores/radio.test.ts b/frontend/src/stores/radio.test.ts new file mode 100644 index 0000000..8af3358 --- /dev/null +++ b/frontend/src/stores/radio.test.ts @@ -0,0 +1,279 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useRadioStore } from '@/stores/radio' +import type { RadioData } from '@/types/radio' +import { nuiCall } from '@/utils/nui' + +vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() })) + +const mockNuiCall = vi.mocked(nuiCall) +const radioData: RadioData = { + badge: '231', + badgeEnabled: true, + badgeMaxLength: 8, + connected: false, + displayName: 'Unit 21', + displayNameAllowed: true, + displayNameEnabled: true, + displayNameMaxLength: 32, + frequency: 0, + frequencyMax: 999.9, + frequencyMin: 0.1, + frequencyStep: 0.1, + history: [{ primary: 120.5, secondary: 130.7 }], + members: [], + provider: 'yaca', + secondaryFrequency: 0, + secondarySupported: true, + settings: { autoRejoin: false, notifications: true }, + volume: 50, +} + +describe('radio store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + mockNuiCall.mockReset() + }) + + it('hydrates the complete server-authoritative radio state', async () => { + mockNuiCall.mockResolvedValueOnce({ data: radioData, success: true }) + const radio = useRadioStore() + + await radio.load() + + expect(radio.data).toMatchObject(radioData) + expect(radio.error).toBe('') + expect(mockNuiCall).toHaveBeenCalledWith('radio:get') + }) + + it('sends both selected channels and applies the accepted connection', async () => { + mockNuiCall.mockResolvedValueOnce({ + data: { + connected: true, + frequency: 120.5, + secondaryFrequency: 130.7, + }, + success: true, + }) + const radio = useRadioStore() + + expect(await radio.connect(120.5, 130.7)).toBe(true) + expect(radio.data.connected).toBe(true) + expect(mockNuiCall).toHaveBeenCalledWith('radio:connect', { + frequency: 120.5, + secondaryFrequency: 130.7, + }) + }) + + it('keeps the current connection when disconnect is rejected', async () => { + mockNuiCall.mockResolvedValueOnce({ + error: 'voice_unavailable', + success: false, + }) + const radio = useRadioStore() + radio.data.connected = true + radio.data.frequency = 120.5 + + await radio.disconnect() + + expect(radio.data.connected).toBe(true) + expect(radio.data.frequency).toBe(120.5) + expect(radio.error).toBe('voice_unavailable') + expect(radio.isLoading).toBe(false) + }) + + it('rolls an optimistic setting change back after server rejection', async () => { + mockNuiCall.mockResolvedValueOnce({ + error: 'invalid_setting', + success: false, + }) + const radio = useRadioStore() + + await radio.saveSetting('autoRejoin', true) + + expect(radio.data.settings.autoRejoin).toBe(false) + expect(radio.error).toBe('invalid_setting') + }) + + it('ignores a stale volume response that arrives after the newest value', async () => { + let resolveFirst!: (value: { + data: { volume: number } + success: true + }) => void + let resolveSecond!: (value: { + data: { volume: number } + success: true + }) => void + mockNuiCall + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveFirst = resolve + }), + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveSecond = resolve + }), + ) + const radio = useRadioStore() + + const first = radio.setVolume(25) + const second = radio.setVolume(75) + resolveSecond({ data: { volume: 75 }, success: true }) + await second + resolveFirst({ data: { volume: 25 }, success: true }) + await first + + expect(radio.data.volume).toBe(75) + }) + + it('applies canonical profile values accepted by the server', async () => { + mockNuiCall + .mockResolvedValueOnce({ data: { badge: 'A_1' }, success: true }) + .mockResolvedValueOnce({ + data: { displayName: 'Unit Seven' }, + success: true, + }) + const radio = useRadioStore() + + expect(await radio.saveBadge('a_1')).toBe(true) + expect(await radio.saveDisplayName(' Unit Seven ')).toBe(true) + + expect(radio.data.badge).toBe('A_1') + expect(radio.data.displayName).toBe('Unit Seven') + expect(mockNuiCall).toHaveBeenNthCalledWith(1, 'radio:save-badge', { + badge: 'a_1', + }) + expect(mockNuiCall).toHaveBeenNthCalledWith(2, 'radio:save-display-name', { + displayName: ' Unit Seven ', + }) + }) + + it('keeps the authoritative profile value when saving is rejected', async () => { + mockNuiCall.mockResolvedValueOnce({ + error: 'rate_limited', + success: false, + }) + const radio = useRadioStore() + radio.data.badge = '231' + + expect(await radio.saveBadge('232')).toBe(false) + + expect(radio.data.badge).toBe('231') + expect(radio.error).toBe('rate_limited') + }) + + it('ignores stale badge responses after a newer save', async () => { + let resolveFirst!: (value: { + data: { badge: string } + success: true + }) => void + let resolveSecond!: (value: { + data: { badge: string } + success: true + }) => void + mockNuiCall + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveFirst = resolve + }), + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveSecond = resolve + }), + ) + const radio = useRadioStore() + + const first = radio.saveBadge('231') + const second = radio.saveBadge('232') + resolveSecond({ data: { badge: '232' }, success: true }) + await second + resolveFirst({ data: { badge: '231' }, success: true }) + await first + + expect(radio.data.badge).toBe('232') + }) + + it('ignores stale display-name responses after a newer save', async () => { + let resolveFirst!: (value: { + data: { displayName: string } + success: true + }) => void + let resolveSecond!: (value: { + data: { displayName: string } + success: true + }) => void + mockNuiCall + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveFirst = resolve + }), + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveSecond = resolve + }), + ) + const radio = useRadioStore() + + const first = radio.saveDisplayName('Unit One') + const second = radio.saveDisplayName('Unit Two') + resolveSecond({ data: { displayName: 'Unit Two' }, success: true }) + await second + resolveFirst({ data: { displayName: 'Unit One' }, success: true }) + await first + + expect(radio.data.displayName).toBe('Unit Two') + }) + + it('merges setting responses per key without clobbering another toggle', async () => { + let resolveAutoRejoin!: (value: { + data: RadioData['settings'] + success: true + }) => void + let resolveNotifications!: (value: { + data: RadioData['settings'] + success: true + }) => void + mockNuiCall + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveAutoRejoin = resolve + }), + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveNotifications = resolve + }), + ) + const radio = useRadioStore() + + const autoRejoin = radio.saveSetting('autoRejoin', true) + const notifications = radio.saveSetting('notifications', true) + resolveNotifications({ + data: { autoRejoin: false, notifications: true }, + success: true, + }) + await notifications + resolveAutoRejoin({ + data: { autoRejoin: true, notifications: false }, + success: true, + }) + await autoRejoin + + expect(radio.data.settings).toEqual({ + autoRejoin: true, + notifications: true, + }) + }) +}) diff --git a/frontend/src/stores/radio.ts b/frontend/src/stores/radio.ts index 6f1fe3d..b259535 100644 --- a/frontend/src/stores/radio.ts +++ b/frontend/src/stores/radio.ts @@ -30,6 +30,13 @@ export const useRadioStore = defineStore('radio', () => { const data = reactive(structuredClone(defaults)) const error = ref('') const isLoading = ref(false) + const settingRequestIds: Record = { + autoRejoin: 0, + notifications: 0, + } + let badgeRequestId = 0 + let displayNameRequestId = 0 + let volumeRequestId = 0 function apply(next: Partial): void { Object.assign(data, next) @@ -61,10 +68,12 @@ export const useRadioStore = defineStore('radio', () => { } async function disconnect(): Promise { + isLoading.value = true error.value = '' const response = await nuiCall('radio:disconnect') if (!response.success) { error.value = response.error ?? 'request_failed' + isLoading.value = false return } apply({ @@ -73,13 +82,16 @@ export const useRadioStore = defineStore('radio', () => { members: [], secondaryFrequency: 0, }) + isLoading.value = false } async function setVolume(volume: number): Promise { + const requestId = ++volumeRequestId data.volume = volume const response = await nuiCall<{ volume: number }>('radio:set-volume', { volume, }) + if (requestId !== volumeRequestId) return if (response.success && response.data) data.volume = response.data.volume } @@ -87,35 +99,42 @@ export const useRadioStore = defineStore('radio', () => { key: keyof RadioSettings, value: boolean, ): Promise { + const requestId = ++settingRequestIds[key] const previous = data.settings[key] data.settings[key] = value const response = await nuiCall('radio:save-settings', { key, value, }) - if (response.success && response.data) data.settings = response.data - else { + if (requestId !== settingRequestIds[key]) return + if (response.success && response.data) { + data.settings[key] = response.data[key] + } else { data.settings[key] = previous error.value = response.error ?? 'request_failed' } } async function saveBadge(badge: string): Promise { + const requestId = ++badgeRequestId error.value = '' const response = await nuiCall<{ badge: string }>('radio:save-badge', { badge, }) + if (requestId !== badgeRequestId) return true if (response.success && response.data) data.badge = response.data.badge else error.value = response.error ?? 'request_failed' return response.success } async function saveDisplayName(displayName: string): Promise { + const requestId = ++displayNameRequestId error.value = '' const response = await nuiCall<{ displayName: string }>( 'radio:save-display-name', { displayName }, ) + if (requestId !== displayNameRequestId) return true if (response.success && response.data) data.displayName = response.data.displayName else error.value = response.error ?? 'request_failed' diff --git a/frontend/src/views/apps/RadioApp.vue b/frontend/src/views/apps/RadioApp.vue index ea762d9..8658fdb 100644 --- a/frontend/src/views/apps/RadioApp.vue +++ b/frontend/src/views/apps/RadioApp.vue @@ -1,20 +1,4 @@ - - - - {{ feedback }} - - + +