diff --git a/betterdesk-server/signal/handler.go b/betterdesk-server/signal/handler.go index 24ce97cf..f2281e69 100644 --- a/betterdesk-server/signal/handler.go +++ b/betterdesk-server/signal/handler.go @@ -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 { diff --git a/betterdesk-server/signal/handler_test.go b/betterdesk-server/signal/handler_test.go index d02c8755..a5584b54 100644 --- a/betterdesk-server/signal/handler_test.go +++ b/betterdesk-server/signal/handler_test.go @@ -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) diff --git a/rdclient-desktop/src/settings.js b/rdclient-desktop/src/settings.js index eb9b3b7e..6447523a 100644 --- a/rdclient-desktop/src/settings.js +++ b/rdclient-desktop/src/settings.js @@ -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 = ''; } } diff --git a/rdclient-desktop/src/setup.js b/rdclient-desktop/src/setup.js index 2c94cf94..99c0048b 100644 --- a/rdclient-desktop/src/setup.js +++ b/rdclient-desktop/src/setup.js @@ -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 = ''; } } diff --git a/web-nodejs/public/css/pages.css b/web-nodejs/public/css/pages.css index d246ada8..e61cbbd9 100644 --- a/web-nodejs/public/css/pages.css +++ b/web-nodejs/public/css/pages.css @@ -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); } diff --git a/web-nodejs/public/js/desktop-widgets.js b/web-nodejs/public/js/desktop-widgets.js index 2a30ed20..68d1b2cc 100644 --- a/web-nodejs/public/js/desktop-widgets.js +++ b/web-nodejs/public/js/desktop-widgets.js @@ -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); diff --git a/web-nodejs/public/js/settings.js b/web-nodejs/public/js/settings.js index 1f68cf50..97264b81 100644 --- a/web-nodejs/public/js/settings.js +++ b/web-nodejs/public/js/settings.js @@ -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 = `