diff --git a/html/css/drawtext.css b/html/css/drawtext.css index 36832d5..f6c920b 100644 --- a/html/css/drawtext.css +++ b/html/css/drawtext.css @@ -1,9 +1,36 @@ -@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;500&display=swap"); - :root { + /* Typography */ + --font-primary: "Exo 2", sans-serif; + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-bold: 700; + --font-weight-light: 300; + /* Colors */ + --md-primary: #1c75d2; + --md-primary-container: #6facec; + --md-error: #c10114; + --md-error-container: #fe4255; + --md-success: #20bb44; + --md-success-container: #6ae587; + --md-warning: #ff9800; + --md-warning-container: #ffc107; + --md-info: #2197f2; + --md-info-container: #7ac1f7; + --md-surface: #fffbff; + --md-on-surface: #1c1b1f; + + /* Custom Variables */ --primary-bg: rgba(23, 23, 23, 90%); - --active-bg: #dc143c; + --active-bg: var(--md-error); --font-color: white; + + /* Elevation */ + --md-elevation-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15); + + /* Border Radius */ + --md-radius-small: 5px; + --md-radius-medium: 10px; + --md-radius-extra-small: 0.15rem; } #drawtext-container { @@ -13,8 +40,8 @@ overflow: hidden; padding: 0; margin: 0; - font-family: "Poppins", sans-serif !important; - font-weight: 300; + font-family: var(--font-primary) !important; + font-weight: var(--font-weight-light); } .text { @@ -23,8 +50,8 @@ color: var(--font-color); margin-top: 0.5rem; padding: 0.45rem; - border-radius: 0.15rem; - box-shadow: 0rem 0rem 0.1rem 0.05rem #000000; + border-radius: var(--md-radius-extra-small); + box-shadow: var(--md-elevation-1); } @media (width: 3840px) and (height: 2160px) { @@ -35,8 +62,8 @@ overflow: hidden; padding: 0; margin: 0; - font-family: "Poppins", sans-serif !important; - font-weight: 300; + font-family: var(--font-primary) !important; + font-weight: var(--font-weight-light); font-size: 1.5vh; } } @@ -94,4 +121,4 @@ transition: 0.5s; left: -100px; opacity: 0; -} \ No newline at end of file +} diff --git a/html/css/style.css b/html/css/style.css index 1211654..62ae7d2 100644 --- a/html/css/style.css +++ b/html/css/style.css @@ -1,3 +1,56 @@ +:root { + /* Typography */ + --font-primary: "Exo 2", sans-serif; + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-bold: 700; + --font-weight-light: 300; + + /* Colors */ + --md-primary: #0061a4; + --md-on-primary: #ffffff; + --md-primary-container: #d1e4ff; + --md-on-primary-container: #001d36; + + --md-error: #ba1a1a; + --md-on-error: #ffffff; + --md-error-container: #ffdad6; + --md-on-error-container: #410002; + + --md-success: #006e1c; + --md-on-success: #ffffff; + --md-success-container: #9df996; + --md-on-success-container: #002106; + + --md-warning: #7d5800; + --md-on-warning: #ffffff; + --md-warning-container: #ffdf93; + --md-on-warning-container: #271900; + + --md-info: #0062a1; + --md-on-info: #ffffff; + --md-info-container: #d0e4ff; + --md-on-info-container: #001d35; + + /* Surface colors */ + --md-surface: #fcfcff; + --md-on-surface: #1a1c1e; + --md-surface-variant: #dfe2eb; + --md-on-surface-variant: #43474e; + + /* Elevation */ + --md-elevation-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15); + + /* Border Radius */ + --md-radius-small: 4px; + --md-radius-medium: 8px; +} + +* { + font-family: var(--font-primary); + font-weight: var(--font-weight-regular); +} + html::-webkit-scrollbar { display: none; } @@ -5,77 +58,91 @@ html::-webkit-scrollbar { @media (width: 3840px) and (height: 2160px) { .success { background-color: rgba(23, 23, 23, 90%); - border-radius: 10px; - box-shadow: 0rem 0rem 0.1rem 0.05rem #000000; - border-left: 0.5rem solid #20bb44; + color: var(--md-on-surface); + box-shadow: var(--md-elevation-1); + border-left: 0.5rem solid var(--md-success); font-size: 1.5vh; } .primary { background-color: rgba(23, 23, 23, 90%); - border-radius: 10px; - box-shadow: 0rem 0rem 0.1rem 0.05rem #000000; - border-left: 5px solid #1c75d2; + color: var(--md-on-surface); + box-shadow: var(--md-elevation-1); + border-left: 5px solid var(--md-primary); font-size: 1.5vh; } .error { background-color: rgba(23, 23, 23, 90%); - border-radius: 10px; - box-shadow: 0rem 0rem 0.1rem 0.05rem #000000; - border-left: 5px solid #c10114; + color: var(--md-on-surface); + box-shadow: var(--md-elevation-1); + border-left: 5px solid var(--md-error); + font-size: 1.5vh; + } + + .warning { + background-color: rgba(23, 23, 23, 90%); + color: var(--md-on-surface); + box-shadow: var(--md-elevation-1); + border-left: 5px solid var(--md-warning); font-size: 1.5vh; } .police { background-color: rgba(23, 23, 23, 90%); - border-radius: 10px; - box-shadow: 0rem 0rem 0.1rem 0.05rem #000000; - border-left: 5px solid #2197f2; + color: var(--md-on-surface); + box-shadow: var(--md-elevation-1); + border-left: 5px solid var(--md-info); font-size: 1.5vh; } .ambulance { background-color: rgba(23, 23, 23, 90%); - border-radius: 10px; - box-shadow: 0rem 0rem 0.1rem 0.05rem #000000; - border-left: 5px solid #f44236; + color: var(--md-on-surface); + box-shadow: var(--md-elevation-1); + border-left: 5px solid var(--md-error); font-size: 1.5vh; } } .success { - background-color: #20bb44; - border-radius: 5px; - border: 1px solid #6ae587; + background-color: var(--md-success-container); + color: var(--md-on-success-container); + padding: 12px 16px; + font-weight: var(--font-weight-medium); } .primary { - background-color: #1c75d2; - border-radius: 5px; - border: 1px solid #6facec; + background-color: var(--md-primary-container); + color: var(--md-on-primary-container); + padding: 12px 16px; + font-weight: var(--font-weight-medium); } .warning { - background-color: #ff9800; - border-radius: 5px; - border: 1px solid #ffc107; + background-color: var(--md-warning-container); + color: var(--md-on-warning-container); + padding: 12px 16px; + font-weight: var(--font-weight-medium); } .error { - background-color: #c10114; - border-radius: 5px; - border: 1px solid #fe4255; + background-color: var(--md-error-container); + color: var(--md-on-error-container); + padding: 12px 16px; + font-weight: var(--font-weight-medium); } .police { - background-color: #2197f2; - border-radius: 5px; - border: 1px solid #7ac1f7; + background-color: var(--md-info-container); + color: var(--md-on-info-container); + padding: 12px 16px; + font-weight: var(--font-weight-medium); } .ambulance { - background-color: #f44236; - border-radius: 5px; - border: 1px solid #f88e86; + background-color: var(--md-error-container); + color: var(--md-on-error-container); + padding: 12px 16px; + font-weight: var(--font-weight-medium); } diff --git a/html/index.html b/html/index.html index 13c31e3..9d5286f 100644 --- a/html/index.html +++ b/html/index.html @@ -1,6 +1,6 @@ - + @@ -11,7 +11,7 @@ - +
diff --git a/html/js/app.js b/html/js/app.js index 7a9b374..007f725 100644 --- a/html/js/app.js +++ b/html/js/app.js @@ -1,16 +1,29 @@ -import { registerWindowMethods } from "./testing.js"; -import { isEnvBrowser } from "./utils.js"; -import { determineStyleFromVariant, DEV_MODE, fetchNotifyConfig, NOTIFY_CONFIG } from "./config.js"; +import { determineStyleFromVariant, fetchNotifyConfig, NOTIFY_CONFIG } from "./config.js"; const { useQuasar } = Quasar; const { onMounted, onUnmounted } = Vue; +const fetchNui = async (evName, data) => { + const resourceName = window.GetParentResourceName(); + + const rawResp = await fetch(`https://${resourceName}/${evName}`, { + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json; charset=UTF8", + }, + method: "POST", + }); + + return await rawResp.json(); +}; + +window.fetchNui = fetchNui; + const app = Vue.createApp({ setup() { const $q = useQuasar(); const showNotif = async ({ data }) => { - // Otherwise we process any old MessageEvent with a data property if (data?.action !== "notify") return; const { text, length, type, caption, icon: dataIcon } = data; @@ -20,21 +33,15 @@ const app = Vue.createApp({ icon = dataIcon; } - // 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, 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", @@ -56,7 +63,3 @@ const app = Vue.createApp({ app.use(Quasar, { config: {} }); app.mount("#q-app"); - -if (DEV_MODE || isEnvBrowser()) { - registerWindowMethods(); -} diff --git a/html/js/config.js b/html/js/config.js index 3757238..5ff6925 100644 --- a/html/js/config.js +++ b/html/js/config.js @@ -1,45 +1,53 @@ -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 {string} variant - * @returns NotiVariantData - **/ +const defaultConfig = { + NotificationStyling: { + group: true, + position: "top-right", + progress: true, + }, + VariantDefinitions: { + success: { + classes: "success", + icon: "done", + }, + primary: { + classes: "primary", + icon: "info", + }, + error: { + classes: "error", + icon: "dangerous", + }, + police: { + classes: "police", + icon: "local_police", + }, + ambulance: { + classes: "ambulance", + icon: "fas fa-ambulance", + }, + }, +}; + 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); + try { + NOTIFY_CONFIG = await window.fetchNui("getNotifyConfig", {}); + if (!NOTIFY_CONFIG) { + NOTIFY_CONFIG = defaultConfig; + } + } catch (error) { + console.error("Failed to fetch notification config, using default", error); + NOTIFY_CONFIG = defaultConfig; } }; -// 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/js/drawtext.js b/html/js/drawtext.js index 975d6e1..e93dad0 100644 --- a/html/js/drawtext.js +++ b/html/js/drawtext.js @@ -1,124 +1,124 @@ let direction = null; const drawText = async (textData) => { - const text = document.getElementById("text"); - let {position} = textData; - switch (textData.position) { - case "left": - addClass(text, position); - direction = "left"; - break; - case "top": - addClass(text, position); - direction = "top"; - break; - case "right": - addClass(text, position); - direction = "right"; - break; - default: - addClass(text, "left"); - direction = "left"; - break; - } + const text = document.getElementById("text"); + let { position } = textData; + switch (textData.position) { + case "left": + addClass(text, position); + direction = "left"; + break; + case "top": + addClass(text, position); + direction = "top"; + break; + case "right": + addClass(text, position); + direction = "right"; + break; + default: + addClass(text, "left"); + direction = "left"; + break; + } - text.innerHTML = textData.text; - document.getElementById("drawtext-container").style.display = "block"; - await sleep(100); - addClass(text, "show"); + text.innerHTML = textData.text; + document.getElementById("drawtext-container").style.display = "block"; + await sleep(100); + addClass(text, "show"); }; const changeText = async (textData) => { - const text = document.getElementById("text"); - let {position} = textData; + const text = document.getElementById("text"); + let { position } = textData; - removeClass(text, "show"); - addClass(text, "pressed"); - addClass(text, "hide"); + removeClass(text, "show"); + addClass(text, "pressed"); + addClass(text, "hide"); - await sleep(500); - removeClass(text, "left"); - removeClass(text, "right"); - removeClass(text, "top"); - removeClass(text, "bottom"); - removeClass(text, "hide"); - removeClass(text, "pressed"); - - switch (textData.position) { - case "left": - addClass(text, position); - direction = "left"; - break; - case "top": - addClass(text, position); - direction = "top"; - break; - case "right": - addClass(text, position); - direction = "right"; - break; - default: - addClass(text, "left"); - direction = "left"; - break; - } - text.innerHTML = textData.text; - - await sleep(100); - text.classList.add("show"); -}; - -const hideText = async () => { - const text = document.getElementById("text"); - removeClass(text, "show"); - addClass(text, "hide"); - - setTimeout(() => { + await sleep(500); removeClass(text, "left"); removeClass(text, "right"); removeClass(text, "top"); removeClass(text, "bottom"); removeClass(text, "hide"); removeClass(text, "pressed"); - document.getElementById("drawtext-container").style.display = "none"; - }, 1000); + + switch (textData.position) { + case "left": + addClass(text, position); + direction = "left"; + break; + case "top": + addClass(text, position); + direction = "top"; + break; + case "right": + addClass(text, position); + direction = "right"; + break; + default: + addClass(text, "left"); + direction = "left"; + break; + } + text.innerHTML = textData.text; + + await sleep(100); + text.classList.add("show"); +}; + +const hideText = async () => { + const text = document.getElementById("text"); + removeClass(text, "show"); + addClass(text, "hide"); + + setTimeout(() => { + removeClass(text, "left"); + removeClass(text, "right"); + removeClass(text, "top"); + removeClass(text, "bottom"); + removeClass(text, "hide"); + removeClass(text, "pressed"); + document.getElementById("drawtext-container").style.display = "none"; + }, 1000); }; const keyPressed = () => { - const text = document.getElementById("text"); - addClass(text, "pressed"); + const text = document.getElementById("text"); + addClass(text, "pressed"); }; window.addEventListener("message", (event) => { - const data = event.data; - const action = data.action; - const textData = data.data; - switch (action) { - case "DRAW_TEXT": - return drawText(textData); - case "CHANGE_TEXT": - return changeText(textData); - case "HIDE_TEXT": - return hideText(); - case "KEY_PRESSED": - return keyPressed(); - default: - return; - } + const data = event.data; + const action = data.action; + const textData = data.data; + switch (action) { + case "DRAW_TEXT": + return drawText(textData); + case "CHANGE_TEXT": + return changeText(textData); + case "HIDE_TEXT": + return hideText(); + case "KEY_PRESSED": + return keyPressed(); + default: + return; + } }); const sleep = (ms) => { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); }; const removeClass = (element, name) => { - if (element.classList.contains(name)) { - element.classList.remove(name); - } + if (element.classList.contains(name)) { + element.classList.remove(name); + } }; const addClass = (element, name) => { - if (!element.classList.contains(name)) { - element.classList.add(name); - } + if (!element.classList.contains(name)) { + element.classList.add(name); + } }; diff --git a/html/js/testing.js b/html/js/testing.js deleted file mode 100644 index 5181f57..0000000 --- a/html/js/testing.js +++ /dev/null @@ -1,44 +0,0 @@ -// Will register dev utilities on window -export const registerWindowMethods = () => { - 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: { - classes: "success", - icon: "done", - }, - primary: { - classes: "primary", - icon: "info", - }, - error: { - classes: "error", - icon: "dangerous", - }, - police: { - classes: "police", - icon: "local_police", - }, - ambulance: { - classes: "ambulance", - icon: "fas fa-ambulance", - }, - }, -}; diff --git a/html/js/utils.js b/html/js/utils.js deleted file mode 100644 index bf2fb8c..0000000 --- a/html/js/utils.js +++ /dev/null @@ -1,27 +0,0 @@ -// 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://${resourceName}/${evName}`, { - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json; charset=UTF8", - }, - method: "POST", - }); - - return await rawResp.json(); -};