diff --git a/client/functions.lua b/client/functions.lua index 4f30682..3e93d1d 100644 --- a/client/functions.lua +++ b/client/functions.lua @@ -58,6 +58,10 @@ function QBCore.Functions.DrawText3D(x, y, z, text) -- Use local function instea ClearDrawOrigin() end +RegisterNUICallback('getNotifyConfig', function(_, cb) + cb(QBCore.Config.Notify) +end) + function QBCore.Functions.Notify(text, textype, length) if type(text) == "table" then local ttext = text.text or 'Placeholder' @@ -65,6 +69,7 @@ function QBCore.Functions.Notify(text, textype, length) local ttype = textype or 'primary' local length = length or 5000 SendNUIMessage({ + action = 'notify', type = ttype, length = length, text = ttext, @@ -73,7 +78,8 @@ function QBCore.Functions.Notify(text, textype, length) else local ttype = textype or 'primary' local length = length or 5000 - SendNUIMessage({ + SendNUIMessage({ + action = 'notify', type = ttype, length = length, text = text diff --git a/config.lua b/config.lua index c679db3..9a1ba87 100644 --- a/config.lua +++ b/config.lua @@ -33,3 +33,37 @@ QBConfig.Server.uptime = 0 -- Time the server has been up. QBConfig.Server.whitelist = false -- Enable or disable whitelist on the server QBConfig.Server.discord = "" -- Discord invite link QBConfig.Server.PermissionList = {} -- permission list + +QBConfig.Notify = {} + +QBConfig.Notify.NotificationStyling = { + group = false, -- Allow notifications to stack with a badge instead of repeating + position = "right", -- top-left | top-right | bottom-left | bottom-right | top | bottom | left | right | center + progress = true -- Display Progress Bar +} + +-- These are how you define different notification variants +-- The "color" key is background of the notification +-- The "icon" key is the css-icon code, this project uses `Material Icons` & `Font Awesome` +QBconfig.Notify.VariantDefinitions = { + success = { + color = 'green', + icon = 'done' + }, + primary = { + color = 'blue', + icon = 'info' + }, + error = { + color = 'red', + icon = 'dangerous' + }, + police = { + color = 'blue', + icon = 'local_police' + }, + ambulance = { + color = 'red', + icon = 'fas fa-ambulance' + } +} \ No newline at end of file diff --git a/html/app.js b/html/app.js index dad371e..652e9dd 100644 --- a/html/app.js +++ b/html/app.js @@ -1,63 +1,66 @@ +import { registerWindowMethods } from "./testing.js"; +import { isEnvBrowser } from "./utils.js"; +import { + determineStyleFromVariant, + DEV_MODE, + fetchNotifyConfig, + NOTIFY_CONFIG, +} from "./config.js"; + const { useQuasar } = Quasar; + const { onMounted, onUnmounted } = Vue; + const app = Vue.createApp({ setup() { const $q = useQuasar(); - const showNotif = (e) => { - const text = e.data.text; - const length = e.data.length; - const type = e.data.type; - const caption = e.data.caption; - switch (type) { - case 'success': - color = 'green'; - icon = 'done'; - break; - case 'primary': - color = 'blue'; - icon = 'info'; - break; - case 'error': - color = 'red'; - icon = 'dangerous'; - break; - case 'police': - color = 'blue'; - icon = 'local_police'; - break; - case 'ambulance': - color = 'red'; - icon = 'fas fa-ambulance'; - break; - } + const showNotif = async ({ data }) => { + // Otherwise we process any old MessageEvent with a data property + if (data?.action !== "notify") return; - if (text.length > 100) { - multiline = true; - } else { - multiline = false; + const { text, length, type, caption } = data; + const { color, icon } = determineStyleFromVariant(type); + + // Make sure we have sucessfully fetched out config properly + if (!NOTIFY_CONFIG) { + console.error( + "The notification config did not load properly, trying again for next time" + ); + // Lets check again to see if it exists + await fetchNotifyConfig(); + // If we have a config lets re-run notification with same data, this + // isn't recursive though. + if (NOTIFY_CONFIG) return showNotif({ data }); } $q.notify({ message: text, - caption: caption, - multiLine: multiline, - color: color, - group: false, - progress: true, - position: 'right', + multiLine: text.length > 100, + // If our text is larger than a 100 characters, + // we should use multiline notifications + group: NOTIFY_CONFIG.NotificationStyling.group ?? false, + progress: NOTIFY_CONFIG.NotificationStyling.progress ?? true, + position: NOTIFY_CONFIG.NotificationStyling.position ?? "right", timeout: length, - icon: icon, + caption, + color, + icon, }); }; onMounted(() => { - window.addEventListener('message', showNotif); + window.addEventListener("message", showNotif); }); onUnmounted(() => { - window.removeEventListener('message', showNotif); + window.removeEventListener("message", showNotif); }); return {}; }, }); + app.use(Quasar, { config: {} }); -app.mount('#q-app'); +app.mount("#q-app"); + +if (DEV_MODE || isEnvBrowser()) { + registerWindowMethods(); +} diff --git a/html/config.js b/html/config.js new file mode 100644 index 0000000..d21352a --- /dev/null +++ b/html/config.js @@ -0,0 +1,46 @@ +import { fetchNui, isEnvBrowser } from "./utils.js"; +import { BrowserMockConfigData } from "./testing.js"; + +export const DEV_MODE = false; + +/** + * @typedef NotiVariantData + * @property {string} icon + * @property {string} color + **/ + +/** + * Will hold config statically outside of Vue state + * @property {Record} VariantDefinitions + * @property {Record} NotificationStyling + * */ +export let NOTIFY_CONFIG = null; + +/** + * Pure function taking a notification type and returning an object + * with style details + * @param variant {string} + * @returns NotiVariantData + **/ +export const determineStyleFromVariant = (variant) => { + const variantData = NOTIFY_CONFIG.VariantDefinitions[variant]; + if (!variantData) + throw new Error(`Style of type: ${variant}, does not exist in the config`); + return variantData; +}; + +// Fetch and set NOTIFY_CONFIG from client script callback +export const fetchNotifyConfig = async () => { + NOTIFY_CONFIG = await fetchNui("getNotifyConfig", {}, BrowserMockConfigData); + if (isEnvBrowser() || DEV_MODE) { + console.log("Fetched Config:"); + console.dir(NOTIFY_CONFIG); + } +}; + +// We specifically wait for all other files to load +// just in case of a race condition between client handlers +// and NUI fetch call +window.addEventListener("load", async () => { + await fetchNotifyConfig(); +}); diff --git a/html/index.html b/html/index.html index 53c31ae..97ef4b2 100644 --- a/html/index.html +++ b/html/index.html @@ -26,7 +26,7 @@ src="https://cdn.jsdelivr.net/npm/quasar@2.1.0/dist/quasar.umd.prod.js" defer > - +
{ + window.SendNotification = (data) => { + window.dispatchEvent( + new MessageEvent("message", { + data: { + action: "notify", + ...data, + }, + }) + ); + }; +}; + +// Used for browser env handling +export const BrowserMockConfigData = { + NotificationStyling: { + group: true, + position: "top-right", + progress: true, + }, + VariantDefinitions: { + success: { + color: "green", + icon: "done", + }, + primary: { + color: "blue", + icon: "info", + }, + error: { + color: "red", + icon: "dangerous", + }, + police: { + color: "blue", + icon: "local_police", + }, + ambulance: { + color: "red", + icon: "fas fa-ambulance", + }, + }, +}; diff --git a/html/utils.js b/html/utils.js new file mode 100644 index 0000000..e608831 --- /dev/null +++ b/html/utils.js @@ -0,0 +1,26 @@ +// Returns whether we are in browser or not +export const isEnvBrowser = () => !window.invokeNative; + +/** + * Real simple wrapper around fetch api for NUI focus + * it will return the mockData param if we are in browser. So we don't + * make a useless request to a hostname that doesn't exist. + * @param evName {string} - The callback event name/type + * @param data {any} - Optional `body` data JSON stringified & passed with the request + * @param mockData {any} - Mock data to return if this is running in browser + * @return Promise + **/ +export const fetchNui = async (evName, data, mockData = null) => { + if (isEnvBrowser()) return mockData; + + const resourceName = window.GetParentResourceName(); + + const rawResp = await fetch(`https://cfx-nui-${resourceName}/${evName}`, { + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json; charset=UTF8", + }, + }); + + return await rawResp.json(); +};