Add a configurable position for the esx_notify

This commit is contained in:
alexistb2904
2025-08-06 10:46:29 +00:00
parent 830c567cfd
commit fdc7465d55
4 changed files with 413 additions and 266 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
Config = {}
Config.notificationSoundEnabled = true
Config.notificationSoundEnabled = true
Config.position = "middle-right" -- top-right, top-left, top-middle, bottom-right, bottom-left, bottom-middle, middle-left, middle-right
+2 -1
View File
@@ -37,7 +37,8 @@ local function Notify(notificatonType, length, message, title)
length = length or 5000,
message = message or "ESX-Notify",
title = title or "New Notification",
notificationSoundEnabled = Config.notificationSoundEnabled
position = Config.position or "middle-right",
notificationSoundEnabled = Config.notificationSoundEnabled or false
}))
end
+267 -126
View File
@@ -1,196 +1,337 @@
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap');
:root {
--background: rgba(20, 20, 20, 0.9);
--text-color: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--success-color: #2ecc71;
--success-border: #1f9c4d;
--error-color: #e74c3c;
--error-border: #c0392b;
--info-color: #3498db;
--info-border: #2980b9;
--warning-color: #f39c12;
--warning-border: #d35400;
--background: rgba(20, 20, 20, 0.9);
--text-color: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--success-color: #2ecc71;
--success-border: #1f9c4d;
--error-color: #e74c3c;
--error-border: #c0392b;
--info-color: #3498db;
--info-border: #2980b9;
--warning-color: #f39c12;
--warning-border: #d35400;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
font-family: "Poppins", sans-serif;
overflow: hidden;
width: 100vw;
height: 100vh;
font-family: 'Poppins', sans-serif;
overflow: hidden;
}
#root {
position: fixed;
right: 2rem;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
max-height: 80vh;
z-index: 1000;
position: fixed;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
max-height: 80vh;
z-index: 1000;
}
#root.middle-right {
right: 2rem;
top: 50%;
transform: translateY(-50%);
}
#root.middle-left {
left: 2rem;
top: 50%;
transform: translateY(-50%);
}
#root.top-right {
right: 2rem;
top: 2rem;
}
#root.top-left {
left: 2rem;
top: 2rem;
}
#root.top-middle {
left: 50%;
top: 2rem;
transform: translateX(-50%);
}
#root.bottom-right {
right: 2rem;
bottom: 2rem;
}
#root.bottom-left {
left: 2rem;
bottom: 2rem;
}
#root.bottom-middle {
left: 50%;
bottom: 2rem;
transform: translateX(-50%);
}
.notify {
display: flex;
width: 300px;
background: var(--background);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
position: relative;
padding: 12px;
align-items: center;
border-left: 4px solid #555;
display: flex;
width: 300px;
background: var(--background);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
position: relative;
padding: 12px;
align-items: center;
border-left: 4px solid #555;
}
.notify-success {
border-left-color: var(--success-border);
border-left-color: var(--success-border);
}
.notify-error {
border-left-color: var(--error-border);
border-left-color: var(--error-border);
}
.notify-info {
border-left-color: var(--info-border);
border-left-color: var(--info-border);
}
.notify-warning {
border-left-color: var(--warning-border);
border-left-color: var(--warning-border);
}
.notify-icon-container {
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.hexagon {
position: relative;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
clip-path: polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%);
background: linear-gradient(145deg, var(--icon-color), rgba(0, 0, 0, 0.7));
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
position: relative;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
clip-path: polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%);
background: linear-gradient(145deg, var(--icon-color), rgba(0, 0, 0, 0.7));
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
.hexagon::before {
content: "";
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: var(--background);
clip-path: polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%);
z-index: 1;
content: '';
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: var(--background);
clip-path: polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%);
z-index: 1;
}
.hexagon::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(145deg, var(--icon-color) 10%, transparent 70%);
opacity: 0.4;
clip-path: polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%);
z-index: 2;
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(145deg, var(--icon-color) 10%, transparent 70%);
opacity: 0.4;
clip-path: polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%);
z-index: 2;
}
.hexagon .material-symbols-outlined {
color: var(--icon-color);
font-size: 20px;
position: relative;
z-index: 3;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
display: flex;
align-items: center;
justify-content: center;
height: 24px;
width: 24px;
margin-top: -1px;
color: var(--icon-color);
font-size: 20px;
position: relative;
z-index: 3;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
display: flex;
align-items: center;
justify-content: center;
height: 24px;
width: 24px;
margin-top: -1px;
}
.notify-content {
flex: 1;
position: relative;
padding-bottom: 8px;
flex: 1;
position: relative;
padding-bottom: 8px;
}
.notify-title {
color: var(--text-color);
font-size: 14px;
font-weight: 600;
margin-bottom: 2px;
color: var(--text-color);
font-size: 14px;
font-weight: 600;
margin-bottom: 2px;
}
.notify-text {
color: var(--text-secondary);
font-size: 12px;
font-weight: 400;
color: var(--text-secondary);
font-size: 12px;
font-weight: 400;
}
.notify-progress {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 100%;
border-radius: 1px;
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 100%;
border-radius: 1px;
}
.material-symbols-outlined {
font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 48;
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 48;
}
.notify-warning .material-symbols-outlined,
.notify-error .material-symbols-outlined {
margin-top: -2px;
margin-top: -2px;
}
.fadeIn {
animation: fadeIn 0.3s ease-in-out;
#root.top-right .fadeIn,
#root.bottom-right .fadeIn,
#root.middle-right .fadeIn {
animation: fadeIn-right 0.3s ease-in-out;
}
#root.top-left .fadeIn,
#root.bottom-left .fadeIn,
#root.middle-left .fadeIn {
animation: fadeIn-left 0.3s ease-in-out;
}
#root.top-middle .fadeIn {
animation: fadeIn-top 0.3s ease-in-out;
}
#root.bottom-middle .fadeIn {
animation: fadeIn-bottom 0.3s ease-in-out;
}
.fadeOut {
animation: fadeOut 0.5s ease-in-out;
opacity: 0;
opacity: 0;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateX(50px);
}
to {
opacity: 1;
transform: translateX(0);
}
#root.top-right .fadeOut,
#root.bottom-right .fadeOut,
#root.middle-right .fadeOut {
animation: fadeOut-right 0.5s ease-in-out;
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(50px);
}
#root.top-left .fadeOut,
#root.bottom-left .fadeOut,
#root.middle-left .fadeOut {
animation: fadeOut-left 0.5s ease-in-out;
}
#root.top-middle .fadeOut {
animation: fadeOut-top 0.5s ease-in-out;
}
#root.bottom-middle .fadeOut {
animation: fadeOut-bottom 0.5s ease-in-out;
}
@keyframes fadeIn-right {
from {
opacity: 0;
transform: translateX(50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeIn-left {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeIn-top {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn-bottom {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOut-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(50px);
}
}
@keyframes fadeOut-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-50px);
}
}
@keyframes fadeOut-top {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-50px);
}
}
@keyframes fadeOut-bottom {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(50px);
}
}
+142 -138
View File
@@ -1,160 +1,165 @@
const w = window
const w = window;
const types = {
["success"]: {
["icon"]: "check_circle",
["frequency"]: 800,
["duration"]: 100,
["color"]: "#2ecc71",
["borderColor"]: "#1f9c4d",
},
["error"]: {
["icon"]: "error",
["frequency"]: 300,
["duration"]: 150,
["color"]: "#e74c3c",
["borderColor"]: "#c0392b",
},
["info"]: {
["icon"]: "info",
["frequency"]: 600,
["duration"]: 100,
["color"]: "#3498db",
["borderColor"]: "#2980b9",
},
["warning"]: {
["icon"]: "warning_amber",
["frequency"]: 450,
["duration"]: 125,
["color"]: "#f39c12",
["borderColor"]: "#d35400",
},
}
['success']: {
['icon']: 'check_circle',
['frequency']: 800,
['duration']: 100,
['color']: '#2ecc71',
['borderColor']: '#1f9c4d',
},
['error']: {
['icon']: 'error',
['frequency']: 300,
['duration']: 150,
['color']: '#e74c3c',
['borderColor']: '#c0392b',
},
['info']: {
['icon']: 'info',
['frequency']: 600,
['duration']: 100,
['color']: '#3498db',
['borderColor']: '#2980b9',
},
['warning']: {
['icon']: 'warning_amber',
['frequency']: 450,
['duration']: 125,
['color']: '#f39c12',
['borderColor']: '#d35400',
},
};
// the color codes example `i ~r~love~s~ donuts`
const codes = {
"~r~": "#c0392b",
"~b~": "#378cbf",
"~g~": "#2ecc71",
"~y~": "yellow",
"~p~": "purple",
"~c~": "grey",
"~m~": "#212121",
"~u~": "black",
"~o~": "#fb9b04",
}
'~r~': '#c0392b',
'~b~': '#378cbf',
'~g~': '#2ecc71',
'~y~': 'yellow',
'~p~': 'purple',
'~c~': 'grey',
'~m~': '#212121',
'~u~': 'black',
'~o~': '#fb9b04',
};
// Audio context for sound effects
let audioContext
let audioContext;
// Initialize audio context on user interaction
document.addEventListener("click", initAudioContext, { once: true })
document.addEventListener("keydown", initAudioContext, { once: true })
document.addEventListener('click', initAudioContext, { once: true });
document.addEventListener('keydown', initAudioContext, { once: true });
function initAudioContext() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)()
}
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
function playNotificationSound(type) {
if (!audioContext) initAudioContext()
if (!audioContext) initAudioContext();
const typeConfig = types[type] || types["info"]
const oscillator = audioContext.createOscillator()
const gainNode = audioContext.createGain()
const typeConfig = types[type] || types['info'];
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = "sine"
oscillator.frequency.setValueAtTime(typeConfig.frequency, audioContext.currentTime)
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(typeConfig.frequency, audioContext.currentTime);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + typeConfig.duration / 1000)
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + typeConfig.duration / 1000);
oscillator.connect(gainNode)
gainNode.connect(audioContext.destination)
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start()
oscillator.stop(audioContext.currentTime + typeConfig.duration / 1000)
oscillator.start();
oscillator.stop(audioContext.currentTime + typeConfig.duration / 1000);
}
w.addEventListener("message", (event) => {
notification({
type: event.data.type,
title: event.data.title || "New Notification",
message: event.data.message,
length: event.data.length,
notificationSoundEnabled: event.data.notificationSoundEnabled,
})
})
w.addEventListener('message', (event) => {
notification({
type: event.data.type,
title: event.data.title || 'New Notification',
message: event.data.message,
length: event.data.length,
position: event.data.position,
notificationSoundEnabled: event.data.notificationSoundEnabled,
});
});
const replaceColors = (str, obj) => {
let strToReplace = str
let strToReplace = str;
for (const id in obj) {
strToReplace = strToReplace.replace(new RegExp(id, "g"), obj[id])
}
for (const id in obj) {
strToReplace = strToReplace.replace(new RegExp(id, 'g'), obj[id]);
}
return strToReplace
}
return strToReplace;
};
const sanitizeHTML = (str) => {
const temp = document.createElement("div")
temp.textContent = str
return temp.innerHTML
}
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
};
const processLineBreaks = (str) => {
// Replace <br> tags with actual line breaks
return str.replace(/&lt;br&gt;/g, "<br>")
}
// Replace <br> tags with actual line breaks
return str.replace(/&lt;br&gt;/g, '<br>');
};
const notification = (data) => {
if (typeof $ === "undefined") {
console.error("jQuery is not loaded. Please ensure jQuery is included in your project.")
return
}
if (typeof $ === 'undefined') {
console.error('jQuery is not loaded. Please ensure jQuery is included in your project.');
return;
}
if (data.message) {
// Remove any standalone ~s~ tags (not preceded by a color code)
data.message = data.message.replace(/~s~/g, "")
}
if (data.message) {
// Remove any standalone ~s~ tags (not preceded by a color code)
data.message = data.message.replace(/~s~/g, '');
}
if (data.title) {
// Remove any standalone ~s~ tags (not preceded by a color code)
data.title = data.title.replace(/~s~/g, "")
}
if (data.title) {
// Remove any standalone ~s~ tags (not preceded by a color code)
data.title = data.title.replace(/~s~/g, '');
}
let sanitizedTitle = data.title ? sanitizeHTML(data.title) : ""
let sanitizedMessage = data.message ? sanitizeHTML(data.message) : ""
let sanitizedTitle = data.title ? sanitizeHTML(data.title) : '';
let sanitizedMessage = data.message ? sanitizeHTML(data.message) : '';
if (data.title) {
for (const color in codes) {
if (sanitizedTitle.includes(color)) {
const objArr = {}
objArr[color] = `<span style="color: ${codes[color]}">`
objArr["~s~"] = "</span>"
sanitizedTitle = replaceColors(sanitizedTitle, objArr)
}
}
}
if (data.title) {
for (const color in codes) {
if (sanitizedTitle.includes(color)) {
const objArr = {};
objArr[color] = `<span style="color: ${codes[color]}">`;
objArr['~s~'] = '</span>';
sanitizedTitle = replaceColors(sanitizedTitle, objArr);
}
}
}
for (const color in codes) {
if (sanitizedMessage.includes(color)) {
const objArr = {}
objArr[color] = `<span style="color: ${codes[color]}">`
objArr["~s~"] = "</span>"
sanitizedMessage = replaceColors(sanitizedMessage, objArr)
}
}
for (const color in codes) {
if (sanitizedMessage.includes(color)) {
const objArr = {};
objArr[color] = `<span style="color: ${codes[color]}">`;
objArr['~s~'] = '</span>';
sanitizedMessage = replaceColors(sanitizedMessage, objArr);
}
}
sanitizedMessage = processLineBreaks(sanitizedMessage)
sanitizedTitle = processLineBreaks(sanitizedTitle)
sanitizedMessage = processLineBreaks(sanitizedMessage);
sanitizedTitle = processLineBreaks(sanitizedTitle);
const id = Math.floor(Math.random() * 100000)
const typeInfo = types[data.type] || types["info"]
const duration = data.length || 3000
const id = Math.floor(Math.random() * 100000);
const typeInfo = types[data.type] || types['info'];
const duration = data.length || 3000;
const notificationElement = $(`
$('#root')
.removeClass()
.addClass(data.position || 'middle-right');
const notificationElement = $(`
<div id="${id}" class="notify notify-${data.type} fadeIn">
<div class="notify-icon-container">
<div class="hexagon" style="--icon-color: ${typeInfo.color}; --border-color: ${typeInfo.borderColor}">
@@ -167,28 +172,27 @@ const notification = (data) => {
<div class="notify-progress" style="background-color: ${typeInfo.color}"></div>
</div>
</div>
`).appendTo(`#root`)
`).appendTo(`#root`);
$(`#${id} .notify-progress`).css({
transition: `width ${duration}ms linear`,
width: "0%",
})
$(`#${id} .notify-progress`).css({
transition: `width ${duration}ms linear`,
width: '0%',
});
if (data.notificationSoundEnabled) {
playNotificationSound(data.type)
}
if (data.notificationSoundEnabled) {
playNotificationSound(data.type);
}
setTimeout(() => {
$(`#${id} .notify-progress`).css("width", "100%")
}, 10)
setTimeout(() => {
$(`#${id} .notify-progress`).css('width', '100%');
}, 10);
setTimeout(() => {
$(`#${id}`).removeClass("fadeIn").addClass("fadeOut")
setTimeout(() => {
$(`#${id}`).remove()
}, 500)
}, duration)
setTimeout(() => {
$(`#${id}`).removeClass('fadeIn').addClass('fadeOut');
setTimeout(() => {
$(`#${id}`).remove();
}, 500);
}, duration);
return notificationElement
}
return notificationElement;
};