mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
feat(branding): implement background image library and selection functionality
Added a new API endpoint to list uploaded background images, enhancing the branding settings page with a background image library. Implemented UI components for selecting and displaying background images, including improved handling of background size and position. Updated tests to validate the new functionality and ensure proper integration with existing branding features.
This commit is contained in:
@@ -474,6 +474,10 @@ func (s *Server) processIDChange(msg *pb.RegisterPk) *pb.RendezvousMessage {
|
||||
log.Printf("[signal] Rejected invalid old peer ID in ID change: %q", oldID)
|
||||
return registerPkResponse(pb.RegisterPkResponse_NOT_SUPPORT)
|
||||
}
|
||||
if !isValidPeerID(newID) {
|
||||
log.Printf("[signal] Rejected invalid new peer ID in ID change: %q", newID)
|
||||
return registerPkResponse(pb.RegisterPkResponse_NOT_SUPPORT)
|
||||
}
|
||||
|
||||
oldPeer, err := s.db.GetPeer(oldID)
|
||||
if err != nil {
|
||||
|
||||
@@ -256,6 +256,30 @@ func TestProcessIDChangeSoftDeletedTargetReturnsIDExists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessIDChangeRejectsInvalidNewID(t *testing.T) {
|
||||
srv, database := newTestSignalServer(t, config.EnrollmentModeOpen)
|
||||
|
||||
if err := database.UpsertPeer(&db.Peer{ID: "OLD213", Status: "ONLINE"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
msg := newRegisterPk("bad/id")
|
||||
msg.OldId = "OLD213"
|
||||
resp := srv.processIDChange(msg)
|
||||
if got := registerPkResult(resp); got != pb.RegisterPkResponse_NOT_SUPPORT {
|
||||
t.Fatalf("ID change result = %v, want %v", got, pb.RegisterPkResponse_NOT_SUPPORT)
|
||||
}
|
||||
|
||||
if peer, err := database.GetPeer("OLD213"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if peer == nil {
|
||||
t.Fatal("OLD213 should remain active after rejected invalid target ID")
|
||||
}
|
||||
if entry := srv.peers.Get("bad/id"); entry != nil {
|
||||
t.Fatalf("invalid target ID must not enter memory map: %+v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessIDChangeRejectsSoftDeletedSource(t *testing.T) {
|
||||
srv, database := newTestSignalServer(t, config.EnrollmentModeOpen)
|
||||
|
||||
|
||||
@@ -56,6 +56,14 @@
|
||||
return /^#[0-9a-fA-F]{6}$/.test(v) ? v : fallback;
|
||||
}
|
||||
|
||||
function applyBackgroundSizeMode(target, size) {
|
||||
var mode = String(size || 'cover').trim();
|
||||
if (['cover', 'contain', 'auto', 'center', 'repeat'].indexOf(mode) === -1) mode = 'cover';
|
||||
target.style.backgroundSize = (mode === 'cover' || mode === 'contain') ? mode : 'auto';
|
||||
target.style.backgroundRepeat = mode === 'repeat' ? 'repeat' : 'no-repeat';
|
||||
target.style.backgroundPosition = mode === 'repeat' ? 'top left' : 'center';
|
||||
}
|
||||
|
||||
function applyAppearancePayload(payload, baseUrl) {
|
||||
var data = payload && (payload.data || payload.appearance || payload);
|
||||
if (!data || typeof data !== 'object') return;
|
||||
@@ -79,16 +87,23 @@
|
||||
if (bg.type === 'image' && bg.imageUrl && baseUrl) {
|
||||
var absolute = new URL(bg.imageUrl, baseUrl).toString();
|
||||
document.body.style.backgroundImage = 'linear-gradient(rgba(0,0,0,.55), rgba(0,0,0,.55)), url("' + absolute.replace(/"/g, '%22') + '")';
|
||||
document.body.style.backgroundSize = bg.size === 'contain' ? 'contain' : 'cover';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
document.body.style.backgroundColor = '';
|
||||
applyBackgroundSizeMode(document.body, bg.size);
|
||||
document.querySelectorAll('.card').forEach(function (card) {
|
||||
card.style.backdropFilter = 'blur(18px)';
|
||||
});
|
||||
} else if (bg.type === 'gradient' && bg.gradient) {
|
||||
document.body.style.backgroundImage = bg.gradient;
|
||||
document.body.style.backgroundColor = '';
|
||||
document.body.style.backgroundSize = 'cover';
|
||||
document.body.style.backgroundRepeat = 'no-repeat';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
} else if (bg.type === 'color' && bg.color) {
|
||||
document.body.style.backgroundImage = 'none';
|
||||
document.body.style.backgroundColor = safeColor(bg.color, 'var(--bg)');
|
||||
document.body.style.backgroundSize = '';
|
||||
document.body.style.backgroundRepeat = '';
|
||||
document.body.style.backgroundPosition = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
return /^#[0-9a-fA-F]{6}$/.test(v) ? v : fallback;
|
||||
}
|
||||
|
||||
function applyBackgroundSizeMode(target, size) {
|
||||
var mode = String(size || 'cover').trim();
|
||||
if (['cover', 'contain', 'auto', 'center', 'repeat'].indexOf(mode) === -1) mode = 'cover';
|
||||
target.style.backgroundSize = (mode === 'cover' || mode === 'contain') ? mode : 'auto';
|
||||
target.style.backgroundRepeat = mode === 'repeat' ? 'repeat' : 'no-repeat';
|
||||
target.style.backgroundPosition = mode === 'repeat' ? 'top left' : 'center';
|
||||
}
|
||||
|
||||
function applyAppearancePayload(payload, baseUrl) {
|
||||
var data = payload && (payload.data || payload.appearance || payload);
|
||||
if (!data || typeof data !== 'object') return;
|
||||
@@ -63,14 +71,21 @@
|
||||
if (bg.type === 'image' && bg.imageUrl && baseUrl) {
|
||||
var absolute = new URL(bg.imageUrl, baseUrl).toString();
|
||||
document.body.style.backgroundImage = 'linear-gradient(rgba(0,0,0,.55), rgba(0,0,0,.55)), url("' + absolute.replace(/"/g, '%22') + '")';
|
||||
document.body.style.backgroundSize = bg.size === 'contain' ? 'contain' : 'cover';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
document.body.style.backgroundColor = '';
|
||||
applyBackgroundSizeMode(document.body, bg.size);
|
||||
if (card) card.style.backdropFilter = 'blur(18px)';
|
||||
} else if (bg.type === 'gradient' && bg.gradient) {
|
||||
document.body.style.backgroundImage = bg.gradient;
|
||||
document.body.style.backgroundColor = '';
|
||||
document.body.style.backgroundSize = 'cover';
|
||||
document.body.style.backgroundRepeat = 'no-repeat';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
} else if (bg.type === 'color' && bg.color) {
|
||||
document.body.style.backgroundImage = 'none';
|
||||
document.body.style.backgroundColor = safeColor(bg.color, 'var(--bg)');
|
||||
document.body.style.backgroundSize = '';
|
||||
document.body.style.backgroundRepeat = '';
|
||||
document.body.style.backgroundPosition = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1033,6 +1033,96 @@
|
||||
background: var(--accent-red);
|
||||
}
|
||||
|
||||
.background-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.background-picker-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.background-library {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.background-library-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.background-library-item:hover {
|
||||
border-color: var(--accent-blue);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.background-library-item.active {
|
||||
border-color: var(--accent-blue);
|
||||
box-shadow: 0 0 0 2px var(--accent-blue-muted);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.background-library-thumb {
|
||||
display: block;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--bg-secondary);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.background-library-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.background-library-empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: var(--space-md);
|
||||
border: 1px dashed var(--border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.background-current-file {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.background-advanced-url {
|
||||
border-top: 1px solid var(--border-primary);
|
||||
padding-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.background-advanced-url summary {
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.background-advanced-url .form-group {
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
/* Color grid */
|
||||
.color-grid {
|
||||
display: grid;
|
||||
@@ -1216,9 +1306,6 @@
|
||||
background: linear-gradient(135deg, var(--bg-secondary), var(--bg-tertiary));
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@
|
||||
el.style.backgroundImage = String(branding.bgGradient);
|
||||
el.style.backgroundColor = '';
|
||||
el.style.backgroundSize = 'cover';
|
||||
el.style.backgroundRepeat = 'no-repeat';
|
||||
el.style.backgroundPosition = 'center';
|
||||
return true;
|
||||
}
|
||||
@@ -234,13 +235,20 @@
|
||||
_wallpaperPath = branding.bgImageUrl;
|
||||
el.style.backgroundImage = 'url("' + String(branding.bgImageUrl).replace(/"/g, '%22') + '")';
|
||||
el.style.backgroundColor = '';
|
||||
el.style.backgroundSize = (branding.bgSize === 'contain') ? 'contain' : 'cover';
|
||||
el.style.backgroundPosition = 'center';
|
||||
applyBackgroundSizeMode(el, branding.bgSize);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function applyBackgroundSizeMode(el, size) {
|
||||
var mode = String(size || 'cover').trim();
|
||||
if (['cover', 'contain', 'auto', 'center', 'repeat'].indexOf(mode) === -1) mode = 'cover';
|
||||
el.style.backgroundSize = (mode === 'cover' || mode === 'contain') ? mode : 'auto';
|
||||
el.style.backgroundRepeat = mode === 'repeat' ? 'repeat' : 'no-repeat';
|
||||
el.style.backgroundPosition = mode === 'repeat' ? 'top left' : 'center';
|
||||
}
|
||||
|
||||
/** Check if wallpaper files are available on server */
|
||||
function probeWallpapers(cb) {
|
||||
if (_wallpapersAvailable !== null) { cb(_wallpapersAvailable); return; }
|
||||
@@ -258,6 +266,7 @@
|
||||
el.style.backgroundImage = DEFAULT_GRADIENT;
|
||||
el.style.backgroundColor = '#0d1117';
|
||||
el.style.backgroundSize = 'cover';
|
||||
el.style.backgroundRepeat = 'no-repeat';
|
||||
el.style.backgroundPosition = 'center';
|
||||
}
|
||||
|
||||
@@ -281,6 +290,7 @@
|
||||
el.style.backgroundImage = 'none';
|
||||
el.style.backgroundColor = color;
|
||||
el.style.backgroundSize = '';
|
||||
el.style.backgroundRepeat = '';
|
||||
el.style.backgroundPosition = '';
|
||||
localStorage.setItem(STORAGE_WALL, url);
|
||||
localStorage.setItem(STORAGE_WALL_FIT, fit);
|
||||
@@ -296,6 +306,7 @@
|
||||
el.style.backgroundColor = '';
|
||||
el.style.backgroundImage = 'url("' + url + '")';
|
||||
el.style.backgroundSize = bgSize;
|
||||
el.style.backgroundRepeat = 'no-repeat';
|
||||
el.style.backgroundPosition = bgPos;
|
||||
localStorage.setItem(STORAGE_WALL, url);
|
||||
localStorage.setItem(STORAGE_WALL_FIT, fit);
|
||||
@@ -308,6 +319,7 @@
|
||||
newLayer.className = 'desktop-wallpaper-new';
|
||||
newLayer.style.backgroundImage = 'url("' + url + '")';
|
||||
newLayer.style.backgroundSize = bgSize;
|
||||
newLayer.style.backgroundRepeat = 'no-repeat';
|
||||
newLayer.style.backgroundPosition = bgPos;
|
||||
el.appendChild(newLayer);
|
||||
|
||||
@@ -319,6 +331,7 @@
|
||||
el.style.backgroundColor = '';
|
||||
el.style.backgroundImage = 'url("' + url + '")';
|
||||
el.style.backgroundSize = bgSize;
|
||||
el.style.backgroundRepeat = 'no-repeat';
|
||||
el.style.backgroundPosition = bgPos;
|
||||
if (newLayer.parentElement) newLayer.remove();
|
||||
}, 600);
|
||||
|
||||
@@ -1197,6 +1197,7 @@
|
||||
if (rdclientOverlayVal) rdclientOverlayVal.textContent = data.rdclientBgOverlay || '0';
|
||||
showBackgroundPanel('rdclient-bg', data.rdclientBgType || 'inherit');
|
||||
syncExistingBackgroundUploadStatuses(data);
|
||||
markSelectedBackgroundImage(data.bgImageUrl || '');
|
||||
|
||||
// Footer & custom CSS
|
||||
setVal('footer-text', data.footerText || '');
|
||||
@@ -1262,6 +1263,69 @@
|
||||
input.click();
|
||||
});
|
||||
});
|
||||
initBackgroundLibrary();
|
||||
}
|
||||
|
||||
async function initBackgroundLibrary() {
|
||||
const library = document.getElementById('bg-image-library');
|
||||
if (!library) return;
|
||||
library.addEventListener('click', (event) => {
|
||||
const item = event.target.closest('.background-library-item');
|
||||
if (!item) return;
|
||||
selectBackgroundLibraryImage(item.dataset.url || '');
|
||||
});
|
||||
await loadBackgroundLibrary(document.getElementById('bg-image-url')?.value || '');
|
||||
}
|
||||
|
||||
async function loadBackgroundLibrary(selectedUrl = '') {
|
||||
const library = document.getElementById('bg-image-library');
|
||||
if (!library) return;
|
||||
library.innerHTML = `<div class="background-library-empty">${_('common.loading')}</div>`;
|
||||
try {
|
||||
const resp = await Utils.api('/api/settings/branding/backgrounds');
|
||||
const items = Array.isArray(resp.data) ? resp.data : [];
|
||||
renderBackgroundLibrary(items, selectedUrl || document.getElementById('bg-image-url')?.value || '');
|
||||
} catch (err) {
|
||||
library.innerHTML = `<div class="background-library-empty">${Utils.escapeHtml(err.message || _('errors.server_error'))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderBackgroundLibrary(items, selectedUrl = '') {
|
||||
const library = document.getElementById('bg-image-library');
|
||||
if (!library) return;
|
||||
if (!items.length) {
|
||||
library.innerHTML = `<div class="background-library-empty">${_('desktop.wp_unavailable')}</div>`;
|
||||
return;
|
||||
}
|
||||
library.innerHTML = items.map(item => {
|
||||
const url = String(item.url || '');
|
||||
const name = String(item.name || url.split('/').pop() || '');
|
||||
const isActive = url === selectedUrl;
|
||||
return `
|
||||
<button type="button" class="background-library-item${isActive ? ' active' : ''}" data-url="${Utils.escapeHtml(url)}" title="${Utils.escapeHtml(name)}">
|
||||
<span class="background-library-thumb" style="background-image:url('${Utils.escapeHtml(url).replace(/'/g, '%27')}')"></span>
|
||||
<span class="background-library-name">${Utils.escapeHtml(name)}</span>
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function selectBackgroundLibraryImage(url) {
|
||||
if (!url) return;
|
||||
const input = document.getElementById('bg-image-url');
|
||||
if (input) input.value = url;
|
||||
selectBackgroundImageType('bg-image-url');
|
||||
setBackgroundUploadStatus(document.getElementById('bg-file-name'), url.split('/').pop() || url, url);
|
||||
markSelectedBackgroundImage(url);
|
||||
onBrandingFieldChange();
|
||||
}
|
||||
|
||||
function markSelectedBackgroundImage(url) {
|
||||
const library = document.getElementById('bg-image-library');
|
||||
if (!library) return;
|
||||
library.querySelectorAll('.background-library-item').forEach(item => {
|
||||
item.classList.toggle('active', item.dataset.url === url);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1359,6 +1423,8 @@
|
||||
|
||||
if (uploadedUrl) {
|
||||
clearTimeout(_autosaveDebounce);
|
||||
await loadBackgroundLibrary(uploadedUrl);
|
||||
markSelectedBackgroundImage(uploadedUrl);
|
||||
updateBackgroundUploadPanel(statusEl, {
|
||||
state: 'success',
|
||||
icon: 'check_circle',
|
||||
|
||||
@@ -313,6 +313,37 @@ const bgUpload = multer({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/settings/branding/backgrounds - List uploaded background images.
|
||||
*/
|
||||
router.get('/api/settings/branding/backgrounds', requireAuth, (req, res) => {
|
||||
try {
|
||||
const uploadsRoot = path.resolve(UPLOADS_DIR);
|
||||
const managedPattern = /^bg-[0-9a-f]{16}\.(png|jpg|jpeg|gif|webp)$/i;
|
||||
const files = fs.readdirSync(uploadsRoot)
|
||||
.filter((name) => managedPattern.test(name))
|
||||
.map((name) => {
|
||||
const fullPath = path.resolve(uploadsRoot, name);
|
||||
if (!fullPath.startsWith(uploadsRoot + path.sep)) return null;
|
||||
const stat = fs.lstatSync(fullPath);
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
||||
return {
|
||||
name,
|
||||
url: `/uploads/${name}`,
|
||||
size: stat.size,
|
||||
updatedAt: stat.mtime.toISOString()
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
|
||||
res.json({ success: true, data: files });
|
||||
} catch (err) {
|
||||
console.error('List branding backgrounds error:', err);
|
||||
res.status(500).json({ success: false, error: req.t('errors.server_error') });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/settings/branding/upload-background - Upload a background image.
|
||||
* Used by the console / login / agent-portal wallpaper pickers. Returns the
|
||||
|
||||
@@ -117,6 +117,29 @@ describe('Branding routes', () => {
|
||||
});
|
||||
|
||||
describe('POST /api/settings/branding/upload-background', () => {
|
||||
it('lists uploaded managed background images', async () => {
|
||||
const uploadsDir = path.join(config.dataDir, 'uploads');
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
const fileName = 'bg-0123456789abcdef.png';
|
||||
const filePath = path.join(uploadsDir, fileName);
|
||||
fs.writeFileSync(filePath, Buffer.from('managed-background'));
|
||||
|
||||
try {
|
||||
const res = await request(app).get('/api/settings/branding/backgrounds');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: fileName,
|
||||
url: `/uploads/${fileName}`
|
||||
})
|
||||
]));
|
||||
} finally {
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts a multipart background image upload', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/settings/branding/upload-background')
|
||||
|
||||
@@ -811,16 +811,20 @@
|
||||
</div>
|
||||
|
||||
<div class="logo-config-panel hidden" id="bg-image-panel">
|
||||
<div class="form-group">
|
||||
<label class="form-label">${_('branding.bg_image_upload')}</label>
|
||||
<div class="logo-upload-area">
|
||||
<div class="background-picker">
|
||||
<div class="background-picker-head">
|
||||
<div>
|
||||
<label class="form-label">${_('desktop.wp_images')}</label>
|
||||
<small class="form-hint">${_('branding.bg_image_hint')}</small>
|
||||
</div>
|
||||
<input type="file" id="bg-image-file" data-target="bg-image-url" accept="image/png,image/jpeg,image/webp,image/gif" class="logo-file-input branding-bg-file">
|
||||
<label for="bg-image-file" class="logo-upload-btn">
|
||||
<span class="material-icons">upload_file</span>
|
||||
<span>${_('branding.bg_image_choose')}</span>
|
||||
</label>
|
||||
<span id="bg-file-name" class="logo-file-name"></span>
|
||||
</div>
|
||||
<div class="background-library" id="bg-image-library" aria-label="${_('desktop.wp_images')}"></div>
|
||||
<span id="bg-file-name" class="logo-file-name background-current-file"></span>
|
||||
<div class="branding-upload-status" id="bg-upload-status" hidden>
|
||||
<div class="branding-upload-status-row">
|
||||
<span class="material-icons branding-upload-status-icon">sync</span>
|
||||
@@ -830,12 +834,15 @@
|
||||
<div class="branding-upload-progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bg-image-url">${_('branding.bg_image_url')}</label>
|
||||
<input type="text" id="bg-image-url" class="form-input"
|
||||
placeholder="https://example.com/wallpaper.jpg">
|
||||
<small class="form-hint">${_('branding.bg_image_hint')}</small>
|
||||
<details class="background-advanced-url">
|
||||
<summary>${_('settings.tab_advanced')}</summary>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bg-image-url">${_('branding.bg_image_url')}</label>
|
||||
<input type="text" id="bg-image-url" class="form-input"
|
||||
placeholder="https://example.com/wallpaper.jpg">
|
||||
<small class="form-hint">${_('branding.bg_image_hint')}</small>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="bg-size">${_('branding.bg_size')}</label>
|
||||
|
||||
Reference in New Issue
Block a user