diff --git a/[esx_addons]/esx_weather/fxmanifest.lua b/[esx_addons]/esx_weather/fxmanifest.lua
index 8258739b..8e5979e7 100644
--- a/[esx_addons]/esx_weather/fxmanifest.lua
+++ b/[esx_addons]/esx_weather/fxmanifest.lua
@@ -2,7 +2,7 @@ fx_version "cerulean"
game "gta5"
author "Kenshin13"
description "Weather sync for your ESX server"
-version "1.0.0"
+version "1.1.0"
legacyversion "1.13.4"
lua54 "yes"
use_fxv2_oal "yes"
@@ -27,7 +27,6 @@ files {
"ui/css/*.css",
"ui/js/core/state.js",
"ui/js/core/constants.js",
- "ui/js/utils/sanitizer.js",
"ui/js/utils/helpers.js",
"ui/js/services/nui-bridge.js",
"ui/js/controllers/ui-controller.js",
diff --git a/[esx_addons]/esx_weather/ui/css/layout.css b/[esx_addons]/esx_weather/ui/css/layout.css
index 13a68dfa..d76bcde3 100644
--- a/[esx_addons]/esx_weather/ui/css/layout.css
+++ b/[esx_addons]/esx_weather/ui/css/layout.css
@@ -70,6 +70,53 @@
margin: 0;
}
+.header-controls {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.perf-toggle-btn {
+ background: transparent;
+ border: 0.125rem solid var(--mid-color);
+ color: var(--light-color);
+ height: 2.5rem;
+ padding: 0 var(--spacing-sm);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ transition: all var(--transition-fast);
+ font-size: 0.875rem;
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.perf-toggle-btn svg {
+ flex-shrink: 0;
+}
+
+.perf-toggle-btn:hover {
+ border-color: var(--brand-color);
+ color: var(--brand-color);
+ background: rgba(var(--brand-color-rgb), 0.1);
+ transform: scale(1.05);
+}
+
+.perf-toggle-btn:active {
+ transform: scale(0.95);
+}
+
+/* Performance mode active state */
+.perf-toggle-btn.active {
+ border-color: var(--brand-color);
+ color: var(--brand-color);
+ background: rgba(var(--brand-color-rgb), 0.2);
+ box-shadow: 0 0 0.5rem rgba(var(--brand-color-rgb), 0.3);
+}
+
.close-btn {
background: transparent;
border: 0.125rem solid var(--mid-color);
diff --git a/[esx_addons]/esx_weather/ui/css/performance.css b/[esx_addons]/esx_weather/ui/css/performance.css
new file mode 100644
index 00000000..2953241d
--- /dev/null
+++ b/[esx_addons]/esx_weather/ui/css/performance.css
@@ -0,0 +1,68 @@
+/**
+ * Performance Mode Styles
+ * Disables heavy animations and effects for improved FPS on weak hardware
+ */
+
+/* Disable all weather icon animations */
+.performance-mode .weather-icon.animate-sun,
+.performance-mode .weather-icon.animate-rain,
+.performance-mode .weather-icon.animate-cloud,
+.performance-mode .weather-icon.animate-lightning,
+.performance-mode .current-zone-icon.animate-sun,
+.performance-mode .current-zone-icon.animate-rain,
+.performance-mode .current-zone-icon.animate-cloud,
+.performance-mode .current-zone-icon.animate-lightning {
+ animation: none !important;
+}
+
+/* Remove backdrop filter blur (known CEF issue, replace with solid color) */
+.performance-mode .backdrop {
+ backdrop-filter: none !important;
+}
+
+/* Remove drop-shadow filters from icons */
+.performance-mode .current-zone-icon,
+.performance-mode .weather-icon {
+ filter: none !important;
+}
+
+/* Remove text shadows for reduced GPU load */
+.performance-mode .current-zone-name {
+ text-shadow: none !important;
+}
+
+.performance-mode .zone-card-title {
+ text-shadow: none !important;
+}
+
+/* Simplify modal entrance animation (fade only, no scale/translate) */
+.performance-mode .modal {
+ animation: fadeIn var(--transition-normal) ease-out !important;
+}
+
+/* Remove hover transform effects (keep color changes) */
+.performance-mode .zone-card:hover {
+ transform: none !important;
+}
+
+.performance-mode .perf-toggle-btn:hover,
+.performance-mode .close-btn:hover {
+ transform: none !important;
+}
+
+.performance-mode .perf-toggle-btn:active,
+.performance-mode .close-btn:active {
+ transform: none !important;
+}
+
+/* Simplify zone card hover background gradient */
+.performance-mode .zone-card::before {
+ display: none !important;
+}
+
+/* Reduce transition duration in performance mode */
+.performance-mode .zone-card,
+.performance-mode .perf-toggle-btn,
+.performance-mode .close-btn {
+ transition-duration: var(--transition-fast) !important;
+}
diff --git a/[esx_addons]/esx_weather/ui/index.html b/[esx_addons]/esx_weather/ui/index.html
index 0269bee6..14553016 100644
--- a/[esx_addons]/esx_weather/ui/index.html
+++ b/[esx_addons]/esx_weather/ui/index.html
@@ -8,6 +8,7 @@
+
@@ -20,12 +21,20 @@
Weather Control Panel
Control the forecast. Shape the atmosphere. Rule the skies.
-
+
@@ -45,7 +54,6 @@
Weather Control Panel
-
diff --git a/[esx_addons]/esx_weather/ui/js/controllers/ui-controller.js b/[esx_addons]/esx_weather/ui/js/controllers/ui-controller.js
index 65a10e9e..05f2cb01 100644
--- a/[esx_addons]/esx_weather/ui/js/controllers/ui-controller.js
+++ b/[esx_addons]/esx_weather/ui/js/controllers/ui-controller.js
@@ -22,7 +22,8 @@ const UIController = (() => {
app: document.getElementById('app'),
closeBtn: document.getElementById('closeBtn'),
currentZoneName: document.getElementById('currentZoneName'),
- zonesGrid: document.getElementById('zonesGrid')
+ zonesGrid: document.getElementById('zonesGrid'),
+ perfToggleBtn: document.getElementById('perfToggleBtn')
};
};
@@ -63,20 +64,7 @@ const UIController = (() => {
const container = document.querySelector('.current-zone-info');
if (!container) return;
- // Validate inputs
- if (!Sanitizer.isValidZoneName(zoneName)) {
- console.warn('[UIController] Invalid zone name:', zoneName);
- return;
- }
-
- if (!Sanitizer.isValidWeatherType(weatherType, State.weatherTypes)) {
- console.warn('[UIController] Invalid weather type:', weatherType);
- return;
- }
-
const iconConfig = getWeatherIcon(weatherType);
- const safeZoneName = Sanitizer.sanitizeHTML(zoneName);
- const safeWeatherType = Sanitizer.sanitizeHTML(weatherType);
container.className = `current-zone-info ${iconConfig.gradientClass}`;
@@ -119,12 +107,6 @@ const UIController = (() => {
* @returns {HTMLElement}
*/
const createZoneCard = (zoneName, weatherType, isCurrent) => {
- // Validate inputs
- if (!Sanitizer.isValidZoneName(zoneName) || !Sanitizer.isValidWeatherType(weatherType, State.weatherTypes)) {
- console.warn('[UIController] Invalid zone card data:', { zoneName, weatherType });
- return document.createElement('div');
- }
-
const card = document.createElement('div');
const iconConfig = getWeatherIcon(weatherType);
@@ -264,12 +246,6 @@ const UIController = (() => {
const card = elements.zonesGrid?.querySelector(`[data-zone="${zoneName}"]`);
if (!card) return;
- // Validate inputs
- if (!Sanitizer.isValidWeatherType(weatherType, State.weatherTypes)) {
- console.warn('[UIController] Invalid weather type for update:', weatherType);
- return;
- }
-
const iconConfig = getWeatherIcon(weatherType);
const isCurrent = card.classList.contains('current');
@@ -317,6 +293,61 @@ const UIController = (() => {
}
};
+ /**
+ * Toggle performance mode on/off
+ * Applies CSS class to root element and updates button state
+ * @returns {void}
+ */
+ const togglePerformanceMode = () => {
+ setPerformanceMode(!State.performanceMode);
+ applyPerformanceMode();
+ };
+
+ /**
+ * Apply performance mode settings to UI
+ * Adds/removes performance-mode class based on state
+ * @returns {void}
+ */
+ const applyPerformanceMode = () => {
+ const app = elements.app || document.getElementById('app');
+ if (!app) return;
+
+ if (State.performanceMode) {
+ app.classList.add('performance-mode');
+ updatePerformanceModeButtonState();
+ } else {
+ app.classList.remove('performance-mode');
+ updatePerformanceModeButtonState();
+ }
+ };
+
+ /**
+ * Update performance mode button visual state
+ * Shows active state when performance mode is enabled
+ * @returns {void}
+ */
+ const updatePerformanceModeButtonState = () => {
+ const btn = elements.perfToggleBtn || document.getElementById('perfToggleBtn');
+ if (!btn) return;
+
+ if (State.performanceMode) {
+ btn.classList.add('active');
+ btn.setAttribute('title', 'Performance Mode: ON (Click to disable)');
+ } else {
+ btn.classList.remove('active');
+ btn.setAttribute('title', 'Performance Mode: OFF (Click to enable)');
+ }
+ };
+
+ /**
+ * Initialize performance mode based on saved setting
+ * Called during app initialization
+ * @returns {void}
+ */
+ const initPerformanceMode = () => {
+ applyPerformanceMode();
+ };
+
return {
initElements,
show,
@@ -325,6 +356,9 @@ const UIController = (() => {
renderZones,
updateZoneWeather,
getWeatherIcon,
+ togglePerformanceMode,
+ applyPerformanceMode,
+ initPerformanceMode,
elements
};
})();
diff --git a/[esx_addons]/esx_weather/ui/js/core/state.js b/[esx_addons]/esx_weather/ui/js/core/state.js
index df23d7a8..f4ebf9f0 100644
--- a/[esx_addons]/esx_weather/ui/js/core/state.js
+++ b/[esx_addons]/esx_weather/ui/js/core/state.js
@@ -31,9 +31,28 @@
const State = {
isVisible: false,
data: null,
- weatherTypes: {}
+ weatherTypes: {},
+ performanceMode: getPerformanceModeSetting()
};
+/**
+ * Get performance mode setting from localStorage
+ * @returns {boolean} Performance mode enabled status
+ */
+function getPerformanceModeSetting() {
+ const stored = localStorage.getItem('weather_performanceMode');
+ return stored !== null ? JSON.parse(stored) : false;
+}
+
+/**
+ * Set performance mode and persist to localStorage
+ * @param {boolean} enabled - Whether performance mode is enabled
+ */
+function setPerformanceMode(enabled) {
+ State.performanceMode = enabled;
+ localStorage.setItem('weather_performanceMode', JSON.stringify(enabled));
+}
+
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = State;
diff --git a/[esx_addons]/esx_weather/ui/js/handlers/event-handlers.js b/[esx_addons]/esx_weather/ui/js/handlers/event-handlers.js
index 2df4e1ff..1b6a5d42 100644
--- a/[esx_addons]/esx_weather/ui/js/handlers/event-handlers.js
+++ b/[esx_addons]/esx_weather/ui/js/handlers/event-handlers.js
@@ -53,6 +53,7 @@ const EventHandlers = (() => {
/**
* Handle custom dropdown option selection
+ * In performance mode, skips icon animations and uses instant text updates
* @param {HTMLElement} option - Selected option element
* @returns {void}
*/
@@ -74,46 +75,66 @@ const EventHandlers = (() => {
const weatherName = Helpers.getWeatherDisplayName(value, State.weatherTypes);
const iconConfig = UIController.getWeatherIcon(value);
- if (textElement) {
- textElement.classList.add('slide-out');
-
- setTimeout(() => {
+ // In performance mode, use instant updates without animations
+ if (State.performanceMode) {
+ // Instant text update
+ if (textElement) {
textElement.textContent = weatherName;
- textElement.classList.remove('slide-out');
- textElement.classList.add('slide-in');
-
- setTimeout(() => {
- textElement.classList.remove('slide-in');
- }, AnimationTimings.SLIDE_ANIMATION);
- }, AnimationTimings.SLIDE_ANIMATION);
- }
-
- if (trigger) {
- const currentIcon = trigger.querySelector('svg') || trigger.querySelector('i');
+ }
- if (currentIcon) {
- currentIcon.classList.add('icon-fade');
+ // Instant icon update
+ if (trigger) {
+ const currentIcon = trigger.querySelector('svg') || trigger.querySelector('i');
+ if (currentIcon) {
+ const newIcon = document.createElement('i');
+ newIcon.setAttribute('data-lucide', iconConfig.icon);
+ trigger.replaceChild(newIcon, currentIcon);
+ Helpers.initLucideIcons(trigger);
+ }
+ }
+ } else {
+ // Normal mode with animations
+ if (textElement) {
+ textElement.classList.add('slide-out');
setTimeout(() => {
- currentIcon.remove();
+ textElement.textContent = weatherName;
+ textElement.classList.remove('slide-out');
+ textElement.classList.add('slide-in');
- const newIcon = document.createElement('i');
- newIcon.setAttribute('data-lucide', iconConfig.icon);
- newIcon.style.opacity = '0';
- newIcon.style.transform = 'scale(0.8)';
+ setTimeout(() => {
+ textElement.classList.remove('slide-in');
+ }, AnimationTimings.SLIDE_ANIMATION);
+ }, AnimationTimings.SLIDE_ANIMATION);
+ }
- trigger.insertBefore(newIcon, trigger.firstChild);
+ if (trigger) {
+ const currentIcon = trigger.querySelector('svg') || trigger.querySelector('i');
- Helpers.initLucideIcons(trigger);
+ if (currentIcon) {
+ currentIcon.classList.add('icon-fade');
setTimeout(() => {
- const renderedIcon = trigger.querySelector('svg') || trigger.querySelector('i');
- if (renderedIcon) {
- renderedIcon.style.opacity = '1';
- renderedIcon.style.transform = 'scale(1)';
- }
- }, AnimationTimings.ICON_FADE_IN);
- }, AnimationTimings.ICON_FADE_OUT);
+ currentIcon.remove();
+
+ const newIcon = document.createElement('i');
+ newIcon.setAttribute('data-lucide', iconConfig.icon);
+ newIcon.style.opacity = '0';
+ newIcon.style.transform = 'scale(0.8)';
+
+ trigger.insertBefore(newIcon, trigger.firstChild);
+
+ Helpers.initLucideIcons(trigger);
+
+ setTimeout(() => {
+ const renderedIcon = trigger.querySelector('svg') || trigger.querySelector('i');
+ if (renderedIcon) {
+ renderedIcon.style.opacity = '1';
+ renderedIcon.style.transform = 'scale(1)';
+ }
+ }, AnimationTimings.ICON_FADE_IN);
+ }, AnimationTimings.ICON_FADE_OUT);
+ }
}
}
@@ -164,6 +185,14 @@ const EventHandlers = (() => {
});
};
+ /**
+ * Handle performance mode toggle button click
+ * @returns {void}
+ */
+ const handlePerformanceModeToggle = () => {
+ UIController.togglePerformanceMode();
+ };
+
/**
* Global click handler with delegation
* @param {Event} event - Click event
@@ -172,6 +201,12 @@ const EventHandlers = (() => {
const handleGlobalClick = (event) => {
const target = event.target;
+ // Performance mode toggle button
+ if (target.matches('#perfToggleBtn') || target.closest('#perfToggleBtn')) {
+ handlePerformanceModeToggle();
+ return;
+ }
+
// Close button
if (target.matches('#closeBtn, .close-btn') || target.closest('#closeBtn, .close-btn')) {
handleClose();
diff --git a/[esx_addons]/esx_weather/ui/js/main.js b/[esx_addons]/esx_weather/ui/js/main.js
index 09b1c3b3..7e258410 100644
--- a/[esx_addons]/esx_weather/ui/js/main.js
+++ b/[esx_addons]/esx_weather/ui/js/main.js
@@ -59,6 +59,9 @@ const init = async () => {
return;
}
+ // Initialize performance mode based on saved setting
+ UIController.initPerformanceMode();
+
// Initialize event handlers
EventHandlers.init();
@@ -66,6 +69,7 @@ const init = async () => {
window.addEventListener('message', handleNUIMessage);
console.log('[Init] Weather Admin Panel initialized successfully');
+ console.log(`[Init] Performance Mode: ${State.performanceMode ? 'ON' : 'OFF'}`);
} catch (error) {
console.error('[Init] Initialization failed:', error);
}
diff --git a/[esx_addons]/esx_weather/ui/js/utils/sanitizer.js b/[esx_addons]/esx_weather/ui/js/utils/sanitizer.js
deleted file mode 100644
index ae82b3dd..00000000
--- a/[esx_addons]/esx_weather/ui/js/utils/sanitizer.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Sanitizer Utility Module
- * @module Sanitizer
- * @description XSS protection for user-generated content
- */
-
-/**
- * HTML Sanitizer
- * Prevents XSS attacks by escaping dangerous characters
- */
-const Sanitizer = (() => {
- /**
- * Escape HTML special characters to prevent XSS
- * @param {string} str - String to sanitize
- * @returns {string} Sanitized string
- * @example
- * sanitizeHTML('')
- * // Returns: '<script>alert("xss")</script>'
- */
- const sanitizeHTML = (str) => {
- if (typeof str !== 'string') {
- return '';
- }
-
- const htmlEscapeMap = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
- '/': '/'
- };
-
- return str.replace(/[&<>"'/]/g, (char) => htmlEscapeMap[char]);
- };
-
- /**
- * Sanitize attribute values for safe use in HTML attributes
- * @param {string} value - Attribute value to sanitize
- * @returns {string} Sanitized attribute value
- * @example
- * sanitizeAttribute('onclick="alert(1)"')
- * // Returns: 'onclick="alert(1)"'
- */
- const sanitizeAttribute = (value) => {
- if (typeof value !== 'string') {
- return '';
- }
-
- return value
- .replace(/"/g, '"')
- .replace(/'/g, ''')
- .replace(/`/g, '`');
- };
-
- /**
- * Create a safe text node (alternative to innerHTML)
- * @param {string} text - Text content
- * @returns {Text} DOM Text node
- */
- const createTextNode = (text) => {
- return document.createTextNode(String(text));
- };
-
- /**
- * Safely set element text content
- * @param {HTMLElement} element - Target element
- * @param {string} text - Text to set
- * @returns {void}
- */
- const setTextContent = (element, text) => {
- if (!element) return;
- element.textContent = String(text);
- };
-
- /**
- * Validate weather type against known types
- * @param {string} weatherType - Weather type to validate
- * @param {Object.} validTypes - Valid weather types
- * @returns {boolean} Is valid weather type
- */
- const isValidWeatherType = (weatherType, validTypes) => {
- if (typeof weatherType !== 'string') return false;
- return Object.values(validTypes).includes(weatherType);
- };
-
- /**
- * Validate zone name (alphanumeric + spaces only)
- * @param {string} zoneName - Zone name to validate
- * @returns {boolean} Is valid zone name
- */
- const isValidZoneName = (zoneName) => {
- if (typeof zoneName !== 'string') return false;
- // Allow alphanumeric, spaces, hyphens, underscores
- return /^[a-zA-Z0-9\s\-_]+$/.test(zoneName);
- };
-
- return {
- sanitizeHTML,
- sanitizeAttribute,
- createTextNode,
- setTextContent,
- isValidWeatherType,
- isValidZoneName
- };
-})();
-
-// Export for module usage
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = Sanitizer;
-}