add option to select the /web page bg

This commit is contained in:
MohamedGad100 2026-01-22 09:07:47 +02:00
parent bf634fe91e
commit 4f812de200
8 changed files with 330 additions and 1 deletions

View File

@ -1 +1,3 @@
from .hooks import test_pre_init_hook, test_post_init_hook
from .hooks import test_pre_init_hook, test_post_init_hook
from . import models
from . import controllers

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import main

View File

@ -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
}

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import res_config_settings

View File

@ -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)

View File

@ -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);
}
};
});

View File

@ -10,6 +10,7 @@
<link rel="stylesheet" type="text/scss" href="/expert_std_backend_theme/static/src/scss/theme.scss"/>
<link rel="stylesheet" type="text/scss" href="/expert_std_backend_theme/static/src/scss/web.scss"/>
<script src="/expert_std_backend_theme/static/src/js/menu.js"></script>
<script src="/expert_std_backend_theme/static/src/js/background.js"></script>
</xpath>
</template>
</data>

View File

@ -42,6 +42,26 @@
</div>
</div>
</xpath>
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Theme" data-key="expert_std_backend_theme">
<h2>Theme Customization</h2>
<div class="row mt16 o_settings_container" name="theme_background_setting_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="home_menu_background_image" string="Home Menu Background Image"/>
<div class="text-muted mb16">
Upload a custom background image for the home menu page. If not set, the default image will be used.
</div>
<field name="home_menu_background_image" widget="image" class="o_field_image" options="{'preview_image': 'home_menu_background_image', 'size': [1024, 1024]}"/>
<div class="text-muted mt8">
<small>Recommended size: 1920x1080 or higher. Supported formats: PNG, JPG, JPEG</small>
</div>
</div>
</div>
</div>
</div>
</xpath>
</data>
</field>
</record>