From 82ce797a04375868ed300e4114ade047fa5a32cf Mon Sep 17 00:00:00 2001 From: Anton Stjernquist Date: Sat, 21 Sep 2024 15:59:34 +0200 Subject: [PATCH] feat(settings): added theme & scale settings --- pnpm-lock.yaml | 100 ++++++++++++++++++++ src/server/.env copy | 2 + src/server/database/schemas/Contact.ts | 4 +- src/server/database/schemas/Device.ts | 1 + src/server/global.ts | 7 ++ src/server/repositories/AuthRepository.ts | 7 +- src/server/repositories/DeviceRepository.ts | 34 ++++++- src/server/router/devices.ts | 20 ++++ src/server/services/DeviceService.ts | 7 ++ src/shared/Types.ts | 6 ++ src/ui/package.json | 1 + src/ui/src/App.tsx | 2 + src/ui/src/Apps/Settings/SettingsApp.tsx | 43 ++++++--- src/ui/src/Frame.tsx | 19 +++- src/ui/src/api/hooks/useSettings.ts | 20 ++++ src/ui/src/api/settings.ts | 10 ++ src/ui/src/components/ui/slider.tsx | 26 +++++ src/ui/src/hooks/useDebounce.ts | 17 ++++ src/ui/src/hooks/useSyncSettings.ts | 12 +++ 19 files changed, 317 insertions(+), 21 deletions(-) create mode 100644 src/server/.env copy create mode 100644 src/ui/src/api/hooks/useSettings.ts create mode 100644 src/ui/src/api/settings.ts create mode 100644 src/ui/src/components/ui/slider.tsx create mode 100644 src/ui/src/hooks/useDebounce.ts create mode 100644 src/ui/src/hooks/useSyncSettings.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 971f6c4a0..224075594 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.2.0 + version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.5)(react@18.3.1) @@ -1479,10 +1482,37 @@ packages: dev: false optional: true + /@radix-ui/number@1.1.0: + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + dev: false + /@radix-ui/primitive@1.1.0: resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} dev: false + /@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.5)(react@18.3.1): resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: @@ -1542,6 +1572,19 @@ packages: react-remove-scroll: 2.5.7(@types/react@18.3.5)(react@18.3.1) dev: false + /@radix-ui/react-direction@1.1.0(@types/react@18.3.5)(react@18.3.1): + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.5 + react: 18.3.1 + dev: false + /@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} peerDependencies: @@ -1677,6 +1720,36 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-slider@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-dAHCDA4/ySXROEPaRtaMV5WHL8+JB/DbtyTbJjYkY0RXmKMO2Ln8DFZhywG5/mVQ4WqHDBc8smc14yPXPqZHYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-slot@1.1.0(@types/react@18.3.5)(react@18.3.1): resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -1745,6 +1818,33 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-use-previous@1.1.0(@types/react@18.3.5)(react@18.3.1): + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.5 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-size@1.1.0(@types/react@18.3.5)(react@18.3.1): + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@18.3.1) + '@types/react': 18.3.5 + react: 18.3.1 + dev: false + /@remix-run/router@1.19.1: resolution: {integrity: sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==} engines: {node: '>=14.0.0'} diff --git a/src/server/.env copy b/src/server/.env copy new file mode 100644 index 000000000..e54fd65f6 --- /dev/null +++ b/src/server/.env copy @@ -0,0 +1,2 @@ +DROP_TABLE_BEFORE_CREATING=false +MYSQL_CONNECTION_STRING='mysql://root:hejhej123123@172.20.192.1:3307/qbcoreframework_02?charset=utf8mb4' \ No newline at end of file diff --git a/src/server/database/schemas/Contact.ts b/src/server/database/schemas/Contact.ts index 9aa625c6b..2c08e7a4e 100644 --- a/src/server/database/schemas/Contact.ts +++ b/src/server/database/schemas/Contact.ts @@ -4,8 +4,8 @@ import { Contact } from '../../../shared/Types'; export type InsertContact = Pick; -export const createContactsTable = () => { - createDbTable('contact', (table) => { +export const createContactsTable = async () => { + await createDbTable('contact', (table) => { table.increments('id').primary(); table.string('name').notNullable(); table.string('phone_number').notNullable(); diff --git a/src/server/database/schemas/Device.ts b/src/server/database/schemas/Device.ts index 9268ed025..cb40663e4 100644 --- a/src/server/database/schemas/Device.ts +++ b/src/server/database/schemas/Device.ts @@ -12,6 +12,7 @@ export const createDevicesTable = async () => { // Generate random string for the identifier table.string('identifier').notNullable().unique().defaultTo(DBInstance.fn.uuid()); table.integer('sim_card_id').unsigned().references('id').inTable(`${DATABASE_PREFIX}sim_card`); + table.jsonb('settings').nullable(); table.dateTime('created_at').notNullable().defaultTo(DBInstance.fn.now()); table.dateTime('updated_at').notNullable().defaultTo(DBInstance.fn.now()); }); diff --git a/src/server/global.ts b/src/server/global.ts index abb3e1553..e8f72252c 100644 --- a/src/server/global.ts +++ b/src/server/global.ts @@ -1,3 +1,5 @@ +import PlayerService from './services/PlayerService'; + const isRunningIngame = typeof RegisterCommand !== 'undefined'; if (!isRunningIngame) { @@ -28,4 +30,9 @@ if (!isRunningIngame) { }, }, } as unknown as CitizenExports; + + const baseLicense = `license:ef0b12fd95e37572c24c00503c3fd02f3f9b99cb`; + PlayerService.selectDevice(1, `1:${baseLicense}`); + PlayerService.selectDevice(2, `2:${baseLicense}`); + PlayerService.selectDevice(3, `3:${baseLicense}`); } diff --git a/src/server/repositories/AuthRepository.ts b/src/server/repositories/AuthRepository.ts index 0be6515e8..59855b37b 100644 --- a/src/server/repositories/AuthRepository.ts +++ b/src/server/repositories/AuthRepository.ts @@ -1,4 +1,5 @@ import { ResourceConfig, getServerConfig } from '../utils/config'; +import { isRunningInGame } from '../utils/game'; const _exports = global.exports; const frameworks = ['standalone', 'custom'] as const; @@ -25,6 +26,10 @@ const getFramework = () => { const convarFramework = getConvarFramework(); const framework = convarFramework || config.framework; + if (!isRunningInGame()) { + return 'standalone'; + } + if (convarFramework) { console.log(`Using framework "${convarFramework}" from GetConvar`); } else { @@ -65,7 +70,7 @@ class AuthRepository { * Development outside FiveM. */ if (typeof RegisterCommand === 'undefined') { - return `${src}` === deviceIdentifier; + return true; } const playerLicenses = getPlayerIdentifiers(src); diff --git a/src/server/repositories/DeviceRepository.ts b/src/server/repositories/DeviceRepository.ts index 62f1c1aba..c30842a89 100644 --- a/src/server/repositories/DeviceRepository.ts +++ b/src/server/repositories/DeviceRepository.ts @@ -25,7 +25,7 @@ class DeviceRepository { } public async getDeviceById(deviceId: number): Promise { - return await DBInstance(tableName) + const result = await DBInstance(tableName) .leftJoin('tmp_phone_sim_card', 'tmp_phone_device.sim_card_id', 'tmp_phone_sim_card.id') .where('tmp_phone_device.id', deviceId) .select( @@ -35,10 +35,20 @@ class DeviceRepository { 'tmp_phone_sim_card.id as sim_card_id', ) .first(); + + try { + if (result) { + result.settings = JSON.parse(result.settings); + } + } catch (error) { + console.log('Error parsing settings', error); + } + + return result; } public async getDeviceByIdentifier(identifier: string): Promise { - return await DBInstance(tableName) + const result = await DBInstance(tableName) .leftJoin('tmp_phone_sim_card', 'tmp_phone_device.sim_card_id', 'tmp_phone_sim_card.id') .where('identifier', identifier) .select( @@ -48,6 +58,16 @@ class DeviceRepository { 'tmp_phone_sim_card.id as sim_card_id', ) .first(); + + try { + if (result) { + result.settings = JSON.parse(result.settings); + } + } catch (error) { + console.log('Error parsing settings', error); + } + + return result; } public async getDeviceBySid(simCardId: number): Promise { @@ -71,6 +91,16 @@ class DeviceRepository { public async deleteDevice(deviceId: number): Promise { await DBInstance(tableName).where('id', deviceId).delete(); } + + public async updateDeviceSettings( + deviceId: number, + settings: Record, + ): Promise { + await DBInstance(tableName) + .where('id', deviceId) + .update({ settings: JSON.stringify(settings) }); + return await this.getDeviceById(deviceId); + } } export default new DeviceRepository(); diff --git a/src/server/router/devices.ts b/src/server/router/devices.ts index 732e9835d..0eb7d466b 100644 --- a/src/server/router/devices.ts +++ b/src/server/router/devices.ts @@ -74,3 +74,23 @@ devicesRouter.add('/current', async (ctx, next) => { await next(); }); + +const updateSettingsSchema = z.object({ + settings: z.record(z.unknown()), +}); + +devicesRouter.add('/update-settings', async (ctx, next) => { + try { + const { settings } = updateSettingsSchema.parse(ctx.request.body); + const device = await DeviceService.updateDeviceSettings(ctx.device.id, settings); + + ctx.body = { + ok: true, + payload: device, + }; + } catch (error) { + handleError(error, ctx); + } + + await next(); +}); diff --git a/src/server/services/DeviceService.ts b/src/server/services/DeviceService.ts index ade6dbcdf..d950a86af 100644 --- a/src/server/services/DeviceService.ts +++ b/src/server/services/DeviceService.ts @@ -47,6 +47,13 @@ class DeviceService { public async deleteDevice(deviceId: number): Promise { return this.deviceRepository.deleteDevice(deviceId); } + + public async updateDeviceSettings( + deviceId: number, + settings: Record, + ): Promise { + return this.deviceRepository.updateDeviceSettings(deviceId, settings); + } } export default new DeviceService(DeviceRepository); diff --git a/src/shared/Types.ts b/src/shared/Types.ts index b134802e2..424a27474 100644 --- a/src/shared/Types.ts +++ b/src/shared/Types.ts @@ -1,7 +1,13 @@ +export interface Settings extends Record { + theme: 'light' | 'dark'; + scale: number; +} + export interface Device { id: number; sim_card_id: number; identifier: string; + settings: Settings; // JSON created_at: Date; updated_at: Date; } diff --git a/src/ui/package.json b/src/ui/package.json index 17b05e9d6..e0387faed 100644 --- a/src/ui/package.json +++ b/src/ui/package.json @@ -12,6 +12,7 @@ "dependencies": { "@originjs/vite-plugin-federation": "^1.3.6", "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.55.1", "@tanstack/react-query-devtools": "4", diff --git a/src/ui/src/App.tsx b/src/ui/src/App.tsx index 0c05458f4..ebe281b75 100644 --- a/src/ui/src/App.tsx +++ b/src/ui/src/App.tsx @@ -17,6 +17,7 @@ import { isEnvBrowser } from './utils/game'; import { setTheme, Theme } from './utils/theme'; import { useActiveCall } from './api/hooks/useActiveCall'; import { useMessagesNotifications } from './api/hooks/useMessagesNotifications'; +import { useSyncSettings } from './hooks/useSyncSettings'; export const lightTheme: Theme = { type: 'light', @@ -45,6 +46,7 @@ export const darkTheme: Theme = { function App() { useActiveCall(); useMessagesNotifications(); + useSyncSettings(); const location = useLocation(); const navigate = useNavigate(); diff --git a/src/ui/src/Apps/Settings/SettingsApp.tsx b/src/ui/src/Apps/Settings/SettingsApp.tsx index 8cbf304f0..581c41a3e 100644 --- a/src/ui/src/Apps/Settings/SettingsApp.tsx +++ b/src/ui/src/Apps/Settings/SettingsApp.tsx @@ -1,31 +1,50 @@ import { Link } from 'react-router-dom'; - -import { darkTheme, lightTheme } from '../../App'; +import { useSettings } from '@/api/hooks/useSettings'; +import { Slider } from '@/components/ui/slider'; import { useEffect, useState } from 'react'; -import { setTheme } from '@/utils/theme'; +import { useDebounce } from '@/hooks/useDebounce'; export const SettingsApp = () => { - const [themeState, setThemeState] = useState(() => { - return localStorage.getItem('theme-type') === 'dark' ? darkTheme : lightTheme; - }); + const { settings, update } = useSettings(); + const [scale, setScale] = useState(settings?.scale || 100); const toggleTheme = () => { - const otherTheme = themeState.type === lightTheme.type ? darkTheme : lightTheme; - console.log('toggle theme', otherTheme, themeState); - setThemeState(otherTheme); + update({ + theme: settings?.theme === 'dark' ? 'light' : 'dark', + }); }; + const debouncedScale = useDebounce(scale || 100, 450); + useEffect(() => { - setTheme(themeState); - }, [themeState]); + console.log({ debouncedScale }); + update({ + scale: debouncedScale, + }); + }, [debouncedScale]); return (
Settings - ⚙️ + ⚙️ + Set phone scale + { + // handleUpdateScale(value[0]); + setScale(value[0]); + }} + onChange={(event) => { + console.log(event); + }} + /> +