(
+ eventName: string,
+ data: T = {} as T,
+): Promise {
+ if (isBrower == true) {
+ const debugReturn = await DebugEventCallback(eventName, data);
+ return Promise.resolve(debugReturn);
+ }
+ const options = {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ },
+ body: JSON.stringify(data),
+ };
+
+ const resp: Response = await fetch(
+ `https://${resourceName}/${eventName}`,
+ options,
+ );
+ return await resp.json();
+}
+
+/**
+ * Listen for an event from the Client
+ * @param action The name of the event to listen for
+ * @param handler The callback to run when the event is received
+ * @returns {void}
+ **/
+export function ReceiveEvent(
+ action: string,
+ handler: (data: T) => void,
+) {
+ const eventListener = (event: MessageEvent>) => {
+ const { action: eventAction, data } = event.data;
+
+ eventAction === action && handler(data);
+ };
+
+ // Add the event listener on mount and remove it on unmount
+ onMount(() => window.addEventListener('message', eventListener));
+ onDestroy(() => window.removeEventListener('message', eventListener));
+}
+
+/**
+ * Listen for an event from the Client
+ * YOU NEED TO BE SURE TO REMOVE THE LISTENER WHEN YOU'RE DONE WITH IT
+ * @param action The name of the event to listen for
+ * @param handler The callback to run when the event is received
+ * @returns {function}
+ **/
+export function TempReceiveEvent(
+ action: string,
+ handler: (data: T) => void,
+): {removeListener: () => void} {
+ const eventListener = (event: MessageEvent>) => {
+ const { action: eventAction, data } = event.data;
+
+ eventAction === action && handler(data);
+ };
+
+ function removeListener() {
+ window.removeEventListener('message', eventListener);
+ }
+
+ // Add the event listener on mount and remove it on unmount
+ window.addEventListener('message', eventListener)
+ return {removeListener};
+}
+
+
+/**
+ * Emulate an event sent from the Client
+ * @param action The name of the event to send
+ * @param data The data to send with the event
+ * @param timer The time to wait before sending the event (in ms)
+ * @returns {void}
+ **/
+export async function DebugEventSend(action: string, data?: P, timer = 0) {
+ if (!isBrower) return;
+ setTimeout(() => {
+ const event = new MessageEvent('message', {
+ data: { action, data },
+ });
+ window.dispatchEvent(event);
+ }, timer);
+}
+
+/**
+ * Emulate an NUICallback in the Client
+ * @param action The name of the event to listen for
+ * @param handler The callback to run when the event is received
+ * @returns {void}
+ **/
+export async function DebugEventReceive(
+ action: string,
+ handler?: (data: T) => unknown,
+) {
+ if (!isBrower) return;
+
+ if (debugEventListeners[action] !== undefined) {
+ console.log(
+ `%c[DEBUG] %c${action} %cevent already has a debug receiver.`,
+ 'color: red; font-weight: bold;',
+ 'color: green',
+ '', // Empty CSS style string to reset the color
+ );
+ return;
+ }
+
+ debugEventListeners[action] = handler;
+}
+
+/**
+ * Emulate an NUICallback in the Client
+ * @private
+ * @param action The name of the event to listen for
+ * @param data The data to send with the event
+ * @returns {Promise} The callback response from the Client
+ */
+export async function DebugEventCallback(action: string, data?: T) {
+ if (!isBrower) return;
+
+ const handler = debugEventListeners[action];
+ if (handler === undefined) {
+ console.log(`[DEBUG] ${action} event does not have a debugger.`);
+ return {};
+ }
+
+ const result = await handler(data);
+ return result;
+}
\ No newline at end of file
diff --git a/resources/[tools]/bl_idcard/web/src/utils/listeners.ts b/resources/[tools]/bl_idcard/web/src/utils/listeners.ts
new file mode 100644
index 000000000..710ee416d
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/src/utils/listeners.ts
@@ -0,0 +1,77 @@
+import { Receive, Send } from "@enums/events"
+import { DebugEventCallback } from "@typings/events"
+import { ReceiveEvent, SendEvent } from "./eventsHandlers"
+import { convertImage } from "./txdToBase64"
+import { TIDInfo, TIDTypes } from "@typings/id"
+import { ID_INFO, ID_TYPE, ID_TYPES } from "@stores/stores"
+import { get } from "svelte/store"
+
+const AlwaysListened: DebugEventCallback[] = [
+ {
+ action: Receive.cardData,
+ handler: (data: TIDInfo) => {
+
+ if (!data) {
+ ID_INFO.set(null);
+ return;
+ }
+
+ const idType = data.idType
+ if (!idType) {
+ console.error('No ID Type found in card types config.');
+ return;
+ };
+
+ const idTypes = get(ID_TYPES);
+
+ data.imageURL = data.imageURL.includes("data:image/png;base64") ? data.imageURL : 'nui://bl_idcard/web/mugshots/' + data.imageURL + '.png'
+
+ if (!idTypes) {
+ console.error('No ID Types config.');
+ return;
+ }
+
+ const idTypeConfig = idTypes[idType];
+
+ if (!idTypeConfig) {
+ console.error(`No ID Type config for ${idType}.`);
+ return;
+ }
+
+ const docStyles = document.documentElement.style;
+
+ // set css variables
+ docStyles.setProperty('--text', idTypeConfig.textColour);
+ docStyles.setProperty('--title', idTypeConfig.titleColour);
+ docStyles.setProperty('--bg', idTypeConfig.bgColour);
+ docStyles.setProperty('--bg-secondary', idTypeConfig.bgColourSecondary);
+
+ ID_TYPE.set(idTypeConfig);
+
+ ID_INFO.set(data);
+ }
+ },
+ {
+ action: Receive.requestBaseUrl,
+ handler: async (txd: string) => {
+ const baseUrl = await convertImage(txd);
+ SendEvent(Send.resolveBaseUrl, baseUrl);
+ }
+ },
+ {
+ action: Receive.config,
+ handler: (data: TIDTypes) => {
+ ID_TYPES.set(data);
+ }
+ }
+]
+
+export default AlwaysListened
+
+
+
+export function InitialiseListen() {
+ for (const debug of AlwaysListened) {
+ ReceiveEvent(debug.action, debug.handler);
+ }
+}
\ No newline at end of file
diff --git a/resources/[tools]/bl_idcard/web/src/utils/txdToBase64.ts b/resources/[tools]/bl_idcard/web/src/utils/txdToBase64.ts
new file mode 100644
index 000000000..5712424bd
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/src/utils/txdToBase64.ts
@@ -0,0 +1,29 @@
+export async function convertImage( //https://github.com/BaziForYou/MugShotBase64
+ txd: string,
+ outputFormat: string = 'image/png'
+): Promise {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.crossOrigin = 'Anonymous';
+ img.onload = async () => {
+ try {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ throw new Error('Failed to get 2D context.');
+ }
+ canvas.height = img.naturalHeight;
+ canvas.width = img.naturalWidth;
+ ctx.drawImage(img, 0, 0);
+ resolve(canvas.toDataURL(outputFormat));
+ canvas.remove();
+ } catch (error) {
+ reject(error);
+ } finally {
+ img.remove();
+ }
+ };
+ img.onerror = () => reject(new Error('Failed to load image.'));
+ img.src = `https://nui-img/${txd}/${txd}`;
+ });
+}
\ No newline at end of file
diff --git a/resources/[tools]/bl_idcard/web/src/vite-env.d.ts b/resources/[tools]/bl_idcard/web/src/vite-env.d.ts
new file mode 100644
index 000000000..4078e7476
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/resources/[tools]/bl_idcard/web/svelte.config.js b/resources/[tools]/bl_idcard/web/svelte.config.js
new file mode 100644
index 000000000..b0683fd24
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/svelte.config.js
@@ -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(),
+}
diff --git a/resources/[tools]/bl_idcard/web/tailwind.config.js b/resources/[tools]/bl_idcard/web/tailwind.config.js
new file mode 100644
index 000000000..932665e7d
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/tailwind.config.js
@@ -0,0 +1,32 @@
+/** @type {import('tailwindcss').Config} */
+
+
+
+// --primary: #2c2c2c;
+// --secondary: #424050;
+// --accent: #8685ef;
+
+// --text-primary: #faf7ff;
+// --text-secondary: #2b2b2b;
+
+
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{svelte,js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ 'txt-primary': 'var(--text)',
+ 'txt-secondary': 'var(--title)',
+
+ 'primary': 'var(--bg)',
+ 'secondary': 'var(--bg-secondary)',
+ 'accent': '#8685ef'
+ },
+ },
+ },
+ plugins: [],
+}
+
diff --git a/resources/[tools]/bl_idcard/web/tsconfig.json b/resources/[tools]/bl_idcard/web/tsconfig.json
new file mode 100644
index 000000000..2737b8922
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "resolveJsonModule": true,
+ "verbatimModuleSyntax": false,
+ /**
+ * 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,
+ "baseUrl": ".",
+ "paths": {
+ "@assets/*": ["src/assets/*"],
+ "@components/*": ["src/components/*"],
+ "@providers/*": ["src/providers/*"],
+ "@stores/*": ["src/stores/*"],
+ "@utils/*": ["src/utils/*"],
+ "@typings/*": ["src/typings/*"],
+ "@enums/*": ["src/enums/*"],
+ "@lib/*": ["src/lib/*"]
+ }
+ },
+ "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/resources/[tools]/bl_idcard/web/tsconfig.node.json b/resources/[tools]/bl_idcard/web/tsconfig.node.json
new file mode 100644
index 000000000..494bfe083
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler"
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/resources/[tools]/bl_idcard/web/vite.config.ts b/resources/[tools]/bl_idcard/web/vite.config.ts
new file mode 100644
index 000000000..0bfcb8dca
--- /dev/null
+++ b/resources/[tools]/bl_idcard/web/vite.config.ts
@@ -0,0 +1,45 @@
+import { defineConfig } from 'vite'
+import { svelte } from '@sveltejs/vite-plugin-svelte'
+import postcss from './postcss.config.js';
+import { resolve } from "path";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ css: {
+ postcss,
+ },
+ plugins: [svelte({
+ /* plugin options */
+ })],
+ base: './', // fivem nui needs to have local dir reference
+ resolve: {
+ alias: {
+ '@assets': resolve("./src/assets"),
+ '@components': resolve("./src/components"),
+ '@providers': resolve("./src/providers"),
+ '@stores': resolve("./src/stores"),
+ '@utils': resolve("./src/utils"),
+ '@typings': resolve("./src/typings"),
+ '@enums': resolve('./src/enums'),
+ '@lib': resolve('./src/lib'),
+ },
+ },
+ server: {
+ port: 3000,
+ },
+ build: {
+ emptyOutDir: true,
+ outDir: './build',
+ assetsDir: './',
+ rollupOptions: {
+ output: {
+ // By not having hashes in the name, you don't have to update the manifest, yay!
+ entryFileNames: `[name].js`,
+ chunkFileNames: `[name].js`,
+ assetFileNames: `[name].[ext]`
+ }
+ }
+ }
+
+ })
+
\ No newline at end of file