diff --git a/odex25_base/expert_std_backend_theme/__init__.py b/odex25_base/expert_std_backend_theme/__init__.py index 5d65c3bdb..65615124f 100644 --- a/odex25_base/expert_std_backend_theme/__init__.py +++ b/odex25_base/expert_std_backend_theme/__init__.py @@ -1 +1,3 @@ -from .hooks import test_pre_init_hook, test_post_init_hook \ No newline at end of file +from .hooks import test_pre_init_hook, test_post_init_hook +from . import models +from . import controllers \ No newline at end of file diff --git a/odex25_base/expert_std_backend_theme/controllers/__init__.py b/odex25_base/expert_std_backend_theme/controllers/__init__.py new file mode 100644 index 000000000..757b12a1f --- /dev/null +++ b/odex25_base/expert_std_backend_theme/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/odex25_base/expert_std_backend_theme/controllers/main.py b/odex25_base/expert_std_backend_theme/controllers/main.py new file mode 100644 index 000000000..8aaa7f827 --- /dev/null +++ b/odex25_base/expert_std_backend_theme/controllers/main.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +import base64 +from odoo import http +from odoo.http import request + + +class BackgroundImageController(http.Controller): + + @http.route('/expert_std_backend_theme/get_background_image', type='json', auth='user') + def get_background_image(self): + """Return the background image URL or False if using default""" + try: + attachment_id = request.env['ir.config_parameter'].sudo().get_param( + 'expert_std_backend_theme.home_menu_background_image_id', False + ) + if attachment_id: + attachment = request.env['ir.attachment'].sudo().browse(int(attachment_id)) + if attachment.exists() and attachment.datas: + # Add write_date as cache buster to force reload when image changes + timestamp = attachment.write_date.timestamp() if attachment.write_date else 0 + return { + 'url': '/web/image/%s?t=%s' % (attachment_id, int(timestamp)), + 'has_custom': True + } + except Exception: + pass + return { + 'url': '/expert_std_backend_theme/static/src/img/bg_app_drawer.png', + 'has_custom': False + } diff --git a/odex25_base/expert_std_backend_theme/models/__init__.py b/odex25_base/expert_std_backend_theme/models/__init__.py new file mode 100644 index 000000000..6084d2cae --- /dev/null +++ b/odex25_base/expert_std_backend_theme/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import res_config_settings diff --git a/odex25_base/expert_std_backend_theme/models/res_config_settings.py b/odex25_base/expert_std_backend_theme/models/res_config_settings.py new file mode 100644 index 000000000..a29a39475 --- /dev/null +++ b/odex25_base/expert_std_backend_theme/models/res_config_settings.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + home_menu_background_image = fields.Binary( + string='Home Menu Background Image', + help='Upload a custom background image for the home menu page. If not set, the default image will be used.' + ) + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + attachment_id = self.env['ir.config_parameter'].sudo().get_param( + 'expert_std_backend_theme.home_menu_background_image_id', False + ) + if attachment_id: + attachment = self.env['ir.attachment'].sudo().browse(int(attachment_id)) + if attachment.exists(): + res.update(home_menu_background_image=attachment.datas) + return res + + def set_values(self): + super(ResConfigSettings, self).set_values() + param = self.env['ir.config_parameter'].sudo() + + if self.home_menu_background_image: + # Detect MIME type from image data + import base64 + try: + image_data = base64.b64decode(self.home_menu_background_image) + # Detect image type from magic bytes + if image_data.startswith(b'\x89PNG'): + mimetype = 'image/png' + extension = 'png' + elif image_data.startswith(b'\xFF\xD8\xFF'): + mimetype = 'image/jpeg' + extension = 'jpg' + elif image_data.startswith(b'GIF'): + mimetype = 'image/gif' + extension = 'gif' + else: + mimetype = 'image/png' # Default fallback + extension = 'png' + except Exception: + mimetype = 'image/png' + extension = 'png' + + # Create or update attachment + attachment_id = param.get_param('expert_std_backend_theme.home_menu_background_image_id', False) + if attachment_id: + attachment = self.env['ir.attachment'].sudo().browse(int(attachment_id)) + if attachment.exists(): + attachment.write({ + 'datas': self.home_menu_background_image, + 'name': 'home_menu_background_image.%s' % extension, + 'mimetype': mimetype, + }) + else: + attachment_id = False + else: + attachment_id = False + + if not attachment_id: + # Create new attachment + attachment = self.env['ir.attachment'].sudo().create({ + 'name': 'home_menu_background_image.%s' % extension, + 'type': 'binary', + 'datas': self.home_menu_background_image, + 'mimetype': mimetype, + 'public': True, # Make it accessible without authentication + 'res_model': 'res.config.settings', + 'res_field': 'home_menu_background_image', + }) + param.set_param('expert_std_backend_theme.home_menu_background_image_id', attachment.id) + else: + # Remove the image - delete attachment and parameter + attachment_id = param.get_param('expert_std_backend_theme.home_menu_background_image_id', False) + if attachment_id: + attachment = self.env['ir.attachment'].sudo().browse(int(attachment_id)) + # Only delete if it exists and is owned by this module + if attachment.exists() and attachment.res_model == 'res.config.settings': + attachment.unlink() + param.set_param('expert_std_backend_theme.home_menu_background_image_id', False) diff --git a/odex25_base/expert_std_backend_theme/static/src/js/background.js b/odex25_base/expert_std_backend_theme/static/src/js/background.js new file mode 100644 index 000000000..44bc17985 --- /dev/null +++ b/odex25_base/expert_std_backend_theme/static/src/js/background.js @@ -0,0 +1,186 @@ +odoo.define('expert_std_backend_theme.background', function (require) { + "use strict"; + + var rpc = require('web.rpc'); + var core = require('web.core'); + + // Cache for background image URL to prevent flash + var cachedBackgroundUrl = null; + var isFetching = false; + + /** + * Check if we're on the home menu page + * Home menu is shown when URL hash contains 'home' or is empty/just '#' + */ + function isHomeMenuPage() { + var hash = window.location.hash || ''; + // Check if hash contains 'home' or is empty/just '#' + var isHome = hash.indexOf('home') !== -1 || hash === '' || hash === '#'; + // Also check if home menu element exists + var hasHomeMenu = document.querySelector('.o_home_menu') !== null; + return isHome && hasHomeMenu; + } + + /** + * Fetch and cache the background image URL + */ + function fetchBackgroundUrl() { + if (cachedBackgroundUrl !== null) { + return Promise.resolve(cachedBackgroundUrl); + } + if (isFetching) { + // If already fetching, wait for it + return new Promise(function(resolve) { + var checkInterval = setInterval(function() { + if (cachedBackgroundUrl !== null) { + clearInterval(checkInterval); + resolve(cachedBackgroundUrl); + } + }, 50); + }); + } + isFetching = true; + return rpc.query({ + route: '/expert_std_backend_theme/get_background_image', + }).then(function (result) { + isFetching = false; + if (result && result.url) { + cachedBackgroundUrl = result.url; + } else { + cachedBackgroundUrl = '/expert_std_backend_theme/static/src/img/bg_app_drawer.png'; + } + return cachedBackgroundUrl; + }).catch(function () { + isFetching = false; + cachedBackgroundUrl = '/expert_std_backend_theme/static/src/img/bg_app_drawer.png'; + return cachedBackgroundUrl; + }); + } + + /** + * Set the background image for the home menu + * Checks for custom uploaded image, falls back to default + * Only applies on the actual home menu page + */ + function setHomeMenuBackground() { + var body = document.body; + if (!body) { + return; + } + + // Only apply background if we're on the home menu page + if (!isHomeMenuPage()) { + // Remove background if we're not on home menu + if (body.classList.contains('o_home_menu_background')) { + body.style.backgroundImage = ''; + body.style.backgroundPosition = ''; + body.style.backgroundRepeat = ''; + body.style.backgroundSize = ''; + } + return; + } + + // Only proceed if body has the home menu background class + if (!body.classList.contains('o_home_menu_background')) { + return; + } + + // Prevent CSS default from showing - clear it first + body.style.backgroundImage = 'none'; + + // Use cached URL if available, otherwise fetch + if (cachedBackgroundUrl !== null) { + // Apply immediately if cached + body.style.backgroundImage = 'url(' + cachedBackgroundUrl + ')'; + body.style.backgroundPosition = 'center'; + body.style.backgroundRepeat = 'no-repeat'; + body.style.backgroundSize = 'cover'; + } else { + // Fetch and apply (background is already cleared to prevent flash) + fetchBackgroundUrl().then(function(url) { + if (isHomeMenuPage() && body.classList.contains('o_home_menu_background')) { + body.style.backgroundImage = 'url(' + url + ')'; + body.style.backgroundPosition = 'center'; + body.style.backgroundRepeat = 'no-repeat'; + body.style.backgroundSize = 'cover'; + } + }); + } + } + + // Pre-fetch background URL early to prevent flash + fetchBackgroundUrl(); + + // Listen for home menu events - pre-fetch BEFORE showing to prevent flash + core.bus.on('will_show_home_menu', null, function () { + // Pre-fetch the background URL before home menu is shown + fetchBackgroundUrl(); + }); + + core.bus.on('show_home_menu', null, function () { + // Apply background immediately when home menu shows + setTimeout(setHomeMenuBackground, 10); + }); + + // Set background when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setHomeMenuBackground); + } else { + setHomeMenuBackground(); + } + + core.bus.on('hide_home_menu', null, function () { + // Remove background when leaving home menu + var body = document.body; + if (body) { + body.style.backgroundImage = ''; + body.style.backgroundPosition = ''; + body.style.backgroundRepeat = ''; + body.style.backgroundSize = ''; + } + }); + + // Also set it when navigating (for SPA behavior) + core.bus.on('web_client_ready', null, function () { + setTimeout(setHomeMenuBackground, 100); + }); + + // Watch for URL hash changes (Odoo navigation) - using hashchange event instead of interval + window.addEventListener('hashchange', function() { + setTimeout(setHomeMenuBackground, 100); + }); + + // Watch for body class changes (Odoo navigation) + var bodyObserver = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + setTimeout(setHomeMenuBackground, 100); + } + }); + }); + + if (document.body) { + bodyObserver.observe(document.body, { + attributes: true, + attributeFilter: ['class'] + }); + } + + // Cleanup observer and clear cache when page unloads + window.addEventListener('beforeunload', function() { + if (bodyObserver) { + bodyObserver.disconnect(); + } + cachedBackgroundUrl = null; + isFetching = false; + }); + + return { + setHomeMenuBackground: setHomeMenuBackground, + clearCache: function() { + cachedBackgroundUrl = null; + isFetching = false; + fetchBackgroundUrl().then(setHomeMenuBackground); + } + }; +}); diff --git a/odex25_base/expert_std_backend_theme/views/assets.xml b/odex25_base/expert_std_backend_theme/views/assets.xml index b9dc9ef4a..c4e034e43 100644 --- a/odex25_base/expert_std_backend_theme/views/assets.xml +++ b/odex25_base/expert_std_backend_theme/views/assets.xml @@ -10,6 +10,7 @@ + diff --git a/odex25_base/expert_std_backend_theme/views/settings.xml b/odex25_base/expert_std_backend_theme/views/settings.xml index 675d88745..8f7622b9a 100644 --- a/odex25_base/expert_std_backend_theme/views/settings.xml +++ b/odex25_base/expert_std_backend_theme/views/settings.xml @@ -42,6 +42,26 @@ + + + Theme Customization + + + + + + + Upload a custom background image for the home menu page. If not set, the default image will be used. + + + + Recommended size: 1920x1080 or higher. Supported formats: PNG, JPG, JPEG + + + + + +