fix(qb-core): post-update recovery + centralizare notify 17mov_Hud

Restaurat jobs.lua din git (Quasar fork a suprascris joburile 17mov). Adăugat item map în items.lua (lipsea, rupt rv-maphold). Setat licences.driver = false în config.lua. Override QBCore.Functions.Notify + QBCore:Notify event → 17mov_Hud:ShowNotification (toate notificările merg automat prin 17mov_Hud).
This commit is contained in:
2026-04-03 02:47:59 +03:00
parent 06414ed181
commit e756e29294
1539 changed files with 51926 additions and 39806 deletions
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
build
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,11 @@
{
"printWidth": 120,
"singleQuote": true,
"useTabs": false,
"tabWidth": 2,
"semi": true,
"bracketSpacing": true,
"trailingComma": "es5",
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": false
}
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
@@ -0,0 +1,37 @@
{
"name": "oxmysql",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "vite",
"start:game": "vite build --watch",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.5.3",
"@tsconfig/svelte": "^3.0.0",
"svelte": "^3.59.2",
"svelte-check": "^2.10.3",
"tslib": "^2.7.0",
"typescript": "^4.9.5",
"vite": "^4.5.5"
},
"dependencies": {
"@tabler/icons-svelte": "^2.47.0",
"@tanstack/svelte-table": "8.8.0",
"autoprefixer": "^10.4.20",
"chart.js": "^4.4.4",
"postcss": "^8.4.47",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"prettier-plugin-tailwindcss": "^0.2.8",
"svelte-chartjs": "^3.1.5",
"svelte-floating-ui": "^1.5.9",
"svelte-portal": "2.2.0",
"tailwindcss": "^3.4.13",
"tinro": "^0.6.12"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,85 @@
<script lang="ts">
import { Route, router } from 'tinro';
import Resource from './pages/resource/Resource.svelte';
import Root from './pages/root/Root.svelte';
import { useNuiEvent } from './utils/useNuiEvent';
import { resources, generalData, chartData } from './store';
import { debugData } from './utils/debugData';
import { visible } from './store';
import { scale } from 'svelte/transition';
import { fetchNui } from './utils/fetchNui';
interface OpenData {
resources: string[];
totalQueries: number;
slowQueries: number;
totalTime: number;
chartData: {
labels: string[];
data: { queries: number; time: number }[];
};
}
router.mode.hash();
router.goto('/');
useNuiEvent('openUI', (data: OpenData) => {
$visible = true;
$resources = data.resources;
$generalData = {
queries: data.totalQueries,
slowQueries: data.slowQueries,
timeQuerying: data.totalTime,
};
$chartData = {
labels: data.chartData.labels,
data: data.chartData.data,
};
});
debugData<OpenData>([
{
action: 'openUI',
data: {
resources: ['ox_core', 'oxmysql', 'ox_inventory', 'ox_doorlock', 'ox_lib', 'ox_vehicleshop', 'ox_target'],
slowQueries: 13,
totalQueries: 332,
totalTime: 230123,
chartData: {
labels: ['oxmysql', 'ox_core', 'ox_inventory', 'ox_doorlock'],
data: [
{ queries: 25, time: 133 },
{ queries: 5, time: 12 },
{ queries: 3, time: 2 },
{ queries: 72, time: 133 },
],
},
},
},
]);
const handleESC = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
$visible = false;
fetchNui('exit');
};
$: $visible ? window.addEventListener('keydown', handleESC) : window.removeEventListener('keydown', handleESC);
</script>
{#if $visible}
<main
transition:scale={{ start: 0.95, duration: 150 }}
class="font-main flex h-full w-full items-center justify-center"
>
<div class="bg-dark-800 flex h-[700px] w-[1200px] rounded-md text-white">
<Route path="/">
<Root />
</Route>
<Route path="/:resource">
<Resource />
</Route>
</div>
</main>
{/if}
@@ -0,0 +1,31 @@
@import url('https://use.typekit.net/qgr5ebd.css');
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
}
#app {
width: 100vw;
height: 100vh;
}
::-webkit-scrollbar {
width: 2px;
}
::-webkit-scrollbar-track {
background: #25262b;
}
::-webkit-scrollbar-thumb {
background: #909296;
}
::-webkit-scrollbar-thumb:hover {
background: #a6a7ab;
}
@@ -0,0 +1,19 @@
import './index.css';
import App from './App.svelte';
import { isEnvBrowser } from './utils/misc';
const app = new App({
target: document.getElementById('app'),
});
if (isEnvBrowser()) {
const root = document.getElementById('app');
// https://i.imgur.com/iPTAdYV.png - Night time img
root!.style.backgroundImage = 'url("https://i.imgur.com/3pzRj9n.png")';
root!.style.backgroundSize = 'cover';
root!.style.backgroundRepeat = 'no-repeat';
root!.style.backgroundPosition = 'center';
}
export default app;
@@ -0,0 +1,67 @@
<script lang="ts">
import { fetchNui } from '../../utils/fetchNui';
import Pagination from './components/Pagination.svelte';
import QueryTable from './components/QueryTable.svelte';
import ResourceHeader from './components/ResourceHeader.svelte';
import { meta } from 'tinro';
import { useNuiEvent } from '../../utils/useNuiEvent';
import { queries, resourceData, type QueryData } from '../../store';
import { debugData } from '../../utils/debugData';
import { onDestroy } from 'svelte';
import { filterData } from '../../store';
import QuerySearch from './components/QuerySearch.svelte';
import IconSearch from '@tabler/icons-svelte/dist/svelte/icons/IconSearch.svelte';
let maxPage = 0;
onDestroy(() => {
$queries = [];
$filterData.page = 0;
});
interface ResourceData {
queries: QueryData[];
pageCount: number;
resourceQueriesCount: number;
resourceSlowQueries: number;
resourceTime: number;
}
debugData<ResourceData>([
{
action: 'loadResource',
data: {
queries: [
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 3, slow: false, date: Date.now() },
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 23, slow: true, date: Date.now() },
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 15, slow: false, date: Date.now() },
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 122, slow: true, date: Date.now() },
],
resourceQueriesCount: 3,
resourceSlowQueries: 2,
resourceTime: 1342,
pageCount: 3,
},
},
]);
// I miss callbacks :(
useNuiEvent('loadResource', (data: ResourceData) => {
maxPage = data.pageCount;
$queries = data.queries;
$resourceData = {
resourceQueriesCount: data.resourceQueriesCount,
resourceSlowQueries: data.resourceSlowQueries,
resourceTime: data.resourceTime,
};
});
</script>
<div class="flex w-full flex-col justify-between">
<div>
<ResourceHeader />
<QuerySearch icon={IconSearch} />
<QueryTable />
</div>
<Pagination {maxPage} />
</div>
@@ -0,0 +1,44 @@
<script lang="ts">
import IconChevronLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronLeft.svelte';
import IconChevronRight from '@tabler/icons-svelte/dist/svelte/icons/IconChevronRight.svelte';
import IconChevronsLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronsLeft.svelte';
import IconChevronsRight from '@tabler/icons-svelte/dist/svelte/icons/IconChevronsRight.svelte';
import { onDestroy } from 'svelte';
import { filterData } from '../../../store';
export let maxPage: number;
onDestroy(() => (maxPage = 0));
</script>
<div class="flex items-center justify-center gap-6 pb-5">
<button
disabled={$filterData.page === 0}
on:click={() => ($filterData.page = 0)}
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
>
<IconChevronsLeft />
</button>
<button
disabled={$filterData.page === 0}
on:click={() => $filterData.page--}
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
>
<IconChevronLeft />
</button>
<p>Page {$filterData.page + 1} of {maxPage}</p>
<button
disabled={$filterData.page >= maxPage - 1}
on:click={() => $filterData.page++}
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
>
<IconChevronRight />
</button>
<button
disabled={$filterData.page === maxPage - 1}
on:click={() => ($filterData.page = maxPage - 1)}
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
>
<IconChevronsRight />
</button>
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import { filterData } from '../../../store/resource';
let value = '';
$: {
$filterData.search = value;
$filterData.page = 0;
}
export let icon: ConstructorOfATypedSvelteComponent;
</script>
<div
class="bg-dark-600 m-4 mt-0 flex items-center rounded-md border-[1px] border-transparent p-2 outline-none transition-all duration-100 focus-within:border-cyan-600"
>
<div class="pr-2">
<svelte:component this={icon} class="text-dark-300" />
</div>
<input type="text" bind:value class="w-full bg-transparent outline-none" placeholder="Search queries..." />
</div>
@@ -0,0 +1,138 @@
<script lang="ts">
import IconChevronDown from '@tabler/icons-svelte/dist/svelte/icons/IconChevronDown.svelte';
import IconChevronUp from '@tabler/icons-svelte/dist/svelte/icons/IconChevronUp.svelte';
import { queries, type QueryData } from '../../../store';
import {
createSvelteTable,
getCoreRowModel,
type TableOptions,
type ColumnDef,
flexRender,
type SortingState,
} from '@tanstack/svelte-table';
import { writable } from 'svelte/store';
import { meta } from 'tinro';
import { fetchNui } from '../../../utils/fetchNui';
import { filterData } from '../../../store';
import QueryTooltip from './QueryTooltip.svelte';
const route = meta();
const columns: ColumnDef<QueryData, number>[] = [
{
accessorKey: 'query',
header: 'Query',
cell: (info) => info.getValue(),
enableSorting: true,
},
{
accessorKey: 'executionTime',
header: 'Time (ms)',
cell: (info) => info.getValue().toFixed(4),
enableSorting: true,
},
];
let sorting: SortingState = [];
const setSorting = (updater) => {
if (updater instanceof Function) {
sorting = updater(sorting);
} else {
sorting = updater;
}
options.update((old) => ({
...old,
state: {
...old.state,
sorting,
},
}));
};
const options = writable<TableOptions<QueryData>>({
data: $queries,
columns,
manualPagination: true,
manualSorting: true,
pageCount: -1,
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
state: {
sorting,
},
});
$: options.update((prev) => ({ ...prev, data: $queries }));
const table = createSvelteTable(options);
let timer: NodeJS.Timeout;
$: {
clearTimeout(timer);
timer = setTimeout(() => {
fetchNui('fetchResource', {
resource: route.params.resource,
pageIndex: $filterData.page,
search: $filterData.search,
sortBy: sorting,
});
}, 300);
}
</script>
<div class="px-4">
<table class="w-full">
<thead class="bg-dark-600">
{#each $table.getHeaderGroups() as headerGroup}
<tr>
{#each headerGroup.headers as header}
<th class={`bg-dark-600 select-none p-1 ${header.id === 'executionTime' ? 'w-1/4' : 'w-3/4'}`}>
{#if !header.isPlaceholder}
<button
class:cursor-pointer={header.column.getCanSort()}
class:select-none={header.column.getCanSort()}
class="flex w-full items-center justify-center gap-1"
on:click={header.column.getToggleSortingHandler()}
>
<svelte:component this={flexRender(header.column.columnDef.header, header.getContext())} />
{#if header.column.getIsSorted() === 'asc'}
<IconChevronUp />
{:else if header.column.getIsSorted() === 'desc'}
<IconChevronDown />
{/if}
</button>
{/if}
</th>
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $table.getRowModel().rows as row}
<tr>
{#each row.getVisibleCells() as cell}
<QueryTooltip
content={cell.getValue()}
let:floatingRef
let:displayTooltip
let:hideTooltip
disabled={cell.column.id !== 'query'}
>
<td
use:floatingRef
on:mouseenter={displayTooltip}
on:mouseleave={hideTooltip}
class={`${cell.column.id === 'executionTime' && 'text-center'} bg-dark-700 p-2 ${
row.original.slow && 'text-yellow-500'
} max-w-[200px] truncate`}
>
<svelte:component this={flexRender(cell.column.columnDef.cell, cell.getContext())} />
</td>
</QueryTooltip>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
@@ -0,0 +1,46 @@
<script lang="ts">
import { offset, flip, shift } from 'svelte-floating-ui/dom';
import { createFloatingActions } from 'svelte-floating-ui';
import { fade } from 'svelte/transition';
import Portal from 'svelte-portal';
export let content: unknown;
export let disabled: boolean;
const [floatingRef, floatingContent] = createFloatingActions({
strategy: 'absolute',
placement: 'top',
middleware: [offset(6), flip(), shift()],
});
let display = false;
let timer: NodeJS.Timer;
const displayTooltip = () => {
if (disabled) return;
clearTimeout(timer);
timer = setTimeout(() => {
display = true;
}, 300);
};
const hideTooltip = () => {
if (disabled) return;
clearTimeout(timer);
display = false;
};
</script>
<slot {floatingRef} {displayTooltip} {hideTooltip} />
{#if display && !disabled}
<Portal target="body">
<div
transition:fade={{ duration: 150 }}
use:floatingContent
class="absolute p-2 text-sm bg-dark-50 text-dark-400 rounded-md max-w-xl font-main"
>
{content}
</div>
</Portal>
{/if}
@@ -0,0 +1,23 @@
<script lang="ts">
import IconChevronLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronLeft.svelte';
import { meta, router } from 'tinro';
import { resourceData } from '../../../store';
const route = meta();
</script>
<div class="p-4 grid grid-flow-col grid-cols-3 items-center ">
<button
on:click={() => router.goto('/')}
class="flex p-2 w-12 bg-dark-600 text-dark-100 hover:text-white rounded-md justify-center items-center hover:bg-dark-500 outline-none border-[1px] border-transparent focus-visible:border-cyan-600"
>
<IconChevronLeft />
</button>
<p class="text-center text-lg">{route.params.resource}</p>
<div class="text-end text-dark-100 flex flex-col text-xs">
<p>Queries: {$resourceData.resourceQueriesCount}</p>
<p>Time: {$resourceData.resourceTime.toFixed(4)} ms</p>
<p class="text-yellow-500">Slow queries: {$resourceData.resourceSlowQueries}</p>
</div>
</div>
@@ -0,0 +1,45 @@
<script lang="ts">
import Search from './components/Search.svelte';
import IconFileAnalytics from '@tabler/icons-svelte/dist/svelte/icons/IconFileAnalytics.svelte';
import IconSearch from '@tabler/icons-svelte/dist/svelte/icons/IconSearch.svelte';
import IconSourceCode from '@tabler/icons-svelte/dist/svelte/icons/IconSourceCode.svelte';
import { router } from 'tinro';
import { search, filteredResources, generalData } from '../../store';
import Chart from './components/Chart.svelte';
</script>
<div class="p-2 w-full h-full flex justify-between gap-2">
<div class="bg-dark-700 p-4 pr-0 flex flex-col w-2/3 rounded-md">
<div class="pr-4 flex justify-between items-center">
<div class="flex gap-3 items-center ">
<p class="text-2xl">Resources</p>
<IconSourceCode />
</div>
<Search icon={IconSearch} bind:value={$search} />
</div>
<div class="flex flex-col gap-3 mt-6 overflow-y-auto pr-4">
{#each $filteredResources as resource}
<button
on:click={() => router.goto(`/${resource}`)}
class="bg-dark-600 p-3 border-[1px] outline-none border-transparent focus-visible:border-cyan-600 text-left hover:bg-dark-400 rounded-md"
>
{resource}
</button>
{/each}
</div>
</div>
<div class="bg-dark-700 p-4 flex flex-col justify-between w-1/3 rounded-md">
<div class="flex flex-col">
<div class="flex gap-3 items-center mb-4">
<p class="text-2xl">General data</p>
<IconFileAnalytics />
</div>
<div class="flex flex-col text-dark-50">
<p>Queries: {$generalData.queries}</p>
<p>Time querying: {$generalData.timeQuerying.toFixed(4)} ms</p>
<p class="text-yellow-500">Slow queries: {$generalData.slowQueries}</p>
</div>
</div>
<Chart />
</div>
</div>
@@ -0,0 +1,40 @@
<script lang="ts">
import { Pie } from 'svelte-chartjs';
import { Chart as ChartJS, Title, Tooltip, ArcElement, CategoryScale, Colors, type TooltipItem } from 'chart.js';
import { chartData } from '../../../store';
ChartJS.register(Title, Tooltip, ArcElement, CategoryScale, Colors);
</script>
<Pie
class="self-center"
width={256}
height={256}
data={{
labels: $chartData.labels,
datasets: [
{
// @ts-ignore
data: $chartData.data,
},
],
}}
options={{
maintainAspectRatio: false,
responsive: false,
parsing: {
key: 'time',
},
animation: false,
plugins: {
tooltip: {
callbacks: {
label: (context) => {
// @ts-ignore
return `Queries: ${context.raw.queries}, Time: ${context.raw.time.toFixed(4)} ms`;
},
},
},
},
}}
/>
@@ -0,0 +1,13 @@
<script lang="ts">
export let icon: ConstructorOfATypedSvelteComponent;
export let value: string;
</script>
<div
class="p-2 flex items-center outline-none border-[1px] border-transparent transition-all duration-100 focus-within:border-cyan-600 rounded-md bg-dark-600"
>
<div class="pr-2">
<svelte:component this={icon} class="text-dark-300" />
</div>
<input type="text" bind:value class="bg-transparent outline-none w-full" placeholder="Search resources..." />
</div>
@@ -0,0 +1,34 @@
import { derived, writable } from 'svelte/store';
export const visible = writable(false);
export const search = writable('');
let timeout: NodeJS.Timeout;
export const debouncedSearch = derived(search, (value, set: (value: string) => void) => {
timeout = setTimeout(() => set(value), 500);
return () => clearTimeout(timeout);
});
export const resources = writable<string[]>([]);
export const filteredResources = derived(
[resources, debouncedSearch],
([$resources, $debouncedSearch], set: (value: string[]) => void) => {
if ($debouncedSearch === '' || !$debouncedSearch) return set($resources);
const query = $debouncedSearch.toLowerCase();
return set($resources.filter((resource) => resource.toLowerCase().includes(query)));
}
);
export const generalData = writable({
queries: 0,
timeQuerying: 0,
slowQueries: 0,
});
export const chartData = writable<{ labels: string[]; data: { queries: number; time: number }[] }>({
labels: [],
data: [],
});
@@ -0,0 +1,2 @@
export * from './general';
export * from './resource';
@@ -0,0 +1,22 @@
import { writable } from 'svelte/store';
export interface QueryData {
date: number;
query: string;
executionTime: number;
slow?: boolean;
}
export const queries = writable<QueryData[]>([]);
export const resourceData = writable<{
resourceQueriesCount: number;
resourceSlowQueries: number;
resourceTime: number;
}>({
resourceQueriesCount: 0,
resourceSlowQueries: 0,
resourceTime: 0,
});
export const filterData = writable({ search: '', page: 0 });
@@ -0,0 +1,30 @@
import { isEnvBrowser } from './misc';
interface DebugEvent<T = any> {
action: string;
data: T;
}
/**
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
* This is used when developing in browser
*
* @param events - The event you want to cover
* @param timer - How long until it should trigger (ms)
*/
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
if (isEnvBrowser()) {
for (const event of events) {
setTimeout(() => {
window.dispatchEvent(
new MessageEvent('message', {
data: {
action: event.action,
data: event.data,
},
})
);
}, timer);
}
}
};
@@ -0,0 +1,24 @@
/**
* @param eventName - The endpoint eventname to target
* @param data - Data you wish to send in the NUI Callback
*
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
*/
export async function fetchNui<T = any>(eventName: string, data: unknown = {}): Promise<T> {
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: JSON.stringify(data),
};
const resourceName = (window as any).GetParentResourceName
? (window as any).GetParentResourceName()
: 'nui-frame-app';
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
return await resp.json();
}
@@ -0,0 +1 @@
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
@@ -0,0 +1,28 @@
import { onMount, onDestroy } from 'svelte';
interface NuiMessage<T = unknown> {
action: string;
data: T;
}
/**
* A function that manage events listeners for receiving data from the client scripts
* @param action The specific `action` that should be listened for.
* @param handler The callback function that will handle data relayed by this function
*
* @example
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
* // whatever logic you want
* })
*
**/
export function useNuiEvent<T = unknown>(action: string, handler: (data: T) => void) {
const eventListener = (event: MessageEvent<NuiMessage<T>>) => {
const { action: eventAction, data } = event.data;
eventAction === action && handler(data);
};
onMount(() => window.addEventListener('message', eventListener));
onDestroy(() => window.removeEventListener('message', eventListener));
}
@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />
@@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
};
@@ -0,0 +1,27 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,svelte}'],
theme: {
extend: {
fontFamily: {
main: 'Roboto',
},
colors: {
// Mantine dark colours
dark: {
50: '#C1C2C5',
100: '#A6A7AB',
200: '#909296',
300: '#5C5F66',
400: '#373A40',
500: '#2C2E33',
600: '#25262B',
700: '#1A1B1E',
800: '#141517',
900: '#101113',
},
},
},
},
plugins: [],
};
@@ -0,0 +1,20 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node"
},
"include": ["vite.config.ts"]
}
@@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
base: './',
server: {
port: 3000,
},
build: {
outDir: 'build',
},
});