diff --git a/.gitignore b/.gitignore index bbc43feb1..105ddec3c 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,9 @@ SwaggerDoc.json /app/core/build /app/common/build /app/proprietary/build +common/build +proprietary/build +stirling-pdf/build # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java b/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java index 9f01c4558..2ee10ebcd 100644 --- a/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java +++ b/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java @@ -43,7 +43,11 @@ public class AutoJobAspect { // This aspect will run before any audit aspects due to @Order(0) // Extract parameters from the request and annotation boolean async = Boolean.parseBoolean(request.getParameter("async")); - log.debug("AutoJobAspect: Processing {} {} with async={}", request.getMethod(), request.getRequestURI(), async); + log.debug( + "AutoJobAspect: Processing {} {} with async={}", + request.getMethod(), + request.getRequestURI(), + async); long timeout = autoJobPostMapping.timeout(); int retryCount = autoJobPostMapping.retryCount(); boolean trackProgress = autoJobPostMapping.trackProgress(); @@ -219,10 +223,9 @@ public class AutoJobAspect { resourceWeight); } - /** - * Processes arguments in-place to handle file resolution and async file persistence. - * This approach avoids type mismatch issues by modifying the original objects directly. + * Processes arguments in-place to handle file resolution and async file persistence. This + * approach avoids type mismatch issues by modifying the original objects directly. * * @param originalArgs The original arguments * @param async Whether this is an async operation diff --git a/app/common/src/main/java/stirling/software/common/model/job/JobResult.java b/app/common/src/main/java/stirling/software/common/model/job/JobResult.java index e4eb456fd..52c0826e2 100644 --- a/app/common/src/main/java/stirling/software/common/model/job/JobResult.java +++ b/app/common/src/main/java/stirling/software/common/model/job/JobResult.java @@ -30,8 +30,7 @@ public class JobResult { private String error; /** List of result files for jobs that produce files */ - @JsonIgnore - private List resultFiles; + @JsonIgnore private List resultFiles; /** Time when the job was created */ private LocalDateTime createdAt; diff --git a/app/core/src/main/resources/static/css/navbar.css b/app/core/src/main/resources/static/css/navbar.css index cf5bba667..047957b6d 100644 --- a/app/core/src/main/resources/static/css/navbar.css +++ b/app/core/src/main/resources/static/css/navbar.css @@ -159,41 +159,36 @@ .scalable-languages-container { display: grid; - /* Auto-fill columns, with a minimum width of 180px */ - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + grid-template-columns: repeat(1, 1fr); +} + +@media (min-width: 400px) { + #languageSelection.scalable-languages-container { + grid-template-columns: repeat(2, 1fr); + } +} +@media (min-width: 600px) { + #languageSelection.scalable-languages-container { + grid-template-columns: repeat(3, 1fr); + } +} +@media (min-width: 900px) { + #languageSelection.scalable-languages-container { + grid-template-columns: repeat(4, 1fr) !important; + } } .scalable-languages-container:not(:has(> :nth-child(4))) .lang-dropdown-item-wrapper:last-child { border: 0px !important } -.scalable-languages-container:has(> *:nth-child(1)) { - --count: 1; +html[dir="ltr"] #languageSelection .lang-dropdown-item-wrapper:last-child { + border-right: none !important; } -.scalable-languages-container:has(> *:nth-child(2)) { - --count: 2; +#languageSelection .lang-dropdown-item-wrapper:last-child { + border: 0px !important; } - -.scalable-languages-container:has(> *:nth-child(3)) { - --count: 3; -} - -html[dir="ltr"] .lang-dropdown-item-wrapper { - border-right: 2px solid var(--md-nav-color-on-separator); -} - -html[dir="rtl"] .lang-dropdown-item-wrapper { - border-left: 2px solid var(--md-nav-color-on-separator); -} - -/* Responsive adjustments */ -@media (min-width: 1200px) { - .lang-dropdown-item-wrapper .dropdown-item { - min-width: 200px - } -} - @media (max-width: 600px) { .scalable-languages-container { grid-template-columns: repeat(2, 1fr); @@ -236,6 +231,41 @@ html[dir="rtl"] .lang-dropdown-item-wrapper { } } +.scalable-languages-container:has(> *:nth-child(1)) { + --count: 1; +} + +.scalable-languages-container:has(> *:nth-child(2)) { + --count: 2; +} + +.scalable-languages-container:has(> *:nth-child(3)) { + --count: 3; +} + +html[dir="ltr"] .lang-dropdown-item-wrapper { + border-right: 2px solid var(--md-nav-color-on-separator); +} + +html[dir="rtl"] .lang-dropdown-item-wrapper { + border-left: 2px solid var(--md-nav-color-on-separator); +} + +/* Responsive adjustments */ +@media (min-width: 1200px) { + .lang-dropdown-item-wrapper .dropdown-item { + min-width: 200px +} + +.scroll-lock-y { + overflow-y: auto; + max-height: 80vh; + overscroll-behavior-y: contain; + -webkit-overflow-scrolling: touch; +} +} + + .dropdown-item .icon-text { text-wrap: wrap; word-break: break-word; @@ -290,6 +320,21 @@ span.icon-text::after { color: var(--md-sys-color-on-surface-variant); } +.nav-link { + display: flex; + align-items: center; + max-width: 98vw; +} + +.chevron-icon { + margin-left: auto; + transition: transform 0.3s ease; +} + +[aria-expanded="true"] > .chevron-icon { + transform: rotate(180deg); +} + .nav-item { position: relative; } @@ -473,6 +518,7 @@ html[dir="rtl"] .dropdown-menu { box-shadow: var(--md-sys-elevation-2); } + .dropdown-menu-tp { color: transparent; background-color: transparent; @@ -484,27 +530,119 @@ html[dir="rtl"] .dropdown-menu { display: inline-flex; } -@media (min-width:992px) { +@media (max-width:1199.98px) { + .navbar-collapse .dropdown-menu { + width: 100%; + } + .navbar-collapse .dropdown-menu-wrapper { + width: 100%; + box-sizing: border-box; + } + .navbar-collapse .dropdown-mw-28 { + min-width: 0; + } +} + +@media (min-width:1200px) { + /* This CSS-based hover is disabled because it conflicts with Bootstrap's JavaScript. + Hover functionality is now handled in navbar.js and search.js */ + /* .dropdown:hover .dropdown-menu { display: block; margin-top: 0; } + */ - /* .icon-hide { - display: none; - } */ -} - -@media (max-width:1199px) { - .icon-hide { - display: inline-flex; - } -} - -@media (min-width:1200px) { .icon-hide { display: none; } + .chevron-icon { + display: none !important; + } +} + +@media (max-width: 1199.98px) { + .navbar-collapse .dropdown-menu { + width: 100vw !important; + max-width: 100vw !important; + left: 0 !important; + right: 0 !important; + transform: none !important; + transform-origin: none !important; + } + + .navbar .navbar-expand-xl .dropdown-menu, + .navbar-expand .dropdown-menu { + transform: none !important; + transform-origin: none !important; + left: 0 !important; + right: 0 !important; + } + + .navbar-collapse .dropdown-mega .dropdown-menu { + max-height: 60vh; + overflow-y: auto; + } + .navbar-collapse .dropdown-mega .dropdown-menu-wrapper { + border-radius: 0; + } + + #favoritesDropdown, + #languageDropdown + .dropdown-menu .dropdown-menu-wrapper, + #searchDropdown + .dropdown-menu .dropdown-menu-wrapper { + padding: 1.5rem 1rem; + } + + #favoritesDropdown, #languageSelection, #searchResults { + width: 100%; + box-sizing: border-box; + } + + .navbar-collapse .dropdown-menu-wrapper { + width: 95vw; + box-sizing: border-box; + margin-left: 0 !important; + padding: 0 !important; + } + .navbar-collapse .dropdown-mw-28 { + min-width: 0; + } + .icon-hide { + display: inline-flex; + } + + .navbar-collapse .dropdown-item { + margin-left: 0 !important; + } + + .container { + margin-left: auto !important; + margin-right: auto !important; + padding-left: 4px !important; + } + + #mainNavbarDropdownMenu { + transform: none !important; + transform-origin: none !important; + left: 0 !important; + right: 0 !important; + width: 100vw !important; + margin-bottom: 0 !important; + } + + .dropdown-menu, + .dropdown-menu-tp, + .dropdown-menu-tp.show { + transform: none !important; + transform-origin: none !important; + max-width: 95vw !important; + width: 100vw !important; + left: 0 !important; + right: 0 !important; + margin-bottom: 0 !important; + } + + } .go-pro-link { @@ -558,6 +696,12 @@ html[dir="rtl"] .dropdown-menu { box-sizing: border-box; } +@media (max-width: 768px) { + .feature-group { + min-width: 10rem; + } +} + .feature-rows { display: flex; flex-wrap: wrap; diff --git a/app/core/src/main/resources/static/css/theme/componentes.css b/app/core/src/main/resources/static/css/theme/componentes.css index c10806ff7..a113015eb 100644 --- a/app/core/src/main/resources/static/css/theme/componentes.css +++ b/app/core/src/main/resources/static/css/theme/componentes.css @@ -40,7 +40,7 @@ textarea { } *::-webkit-scrollbar-corner { - background-color: var(--md-sys-color-surface); + background-color: var(--md-sys-color-surface); } /* Alerts */ @@ -66,6 +66,9 @@ td { background-color: var(--md-sys-color-surface-5); border-radius: 3rem; padding: 2.5rem; + + max-width: 95vw; + margin-left: 2vw; } .card { diff --git a/app/core/src/main/resources/static/js/favourites.js b/app/core/src/main/resources/static/js/favourites.js index 913c656b2..169cf53d9 100644 --- a/app/core/src/main/resources/static/js/favourites.js +++ b/app/core/src/main/resources/static/js/favourites.js @@ -44,8 +44,16 @@ function updateFavoritesDropdown() { contentWrapper.style.color = 'inherit'; // Clone the original content - var originalContent = navbarEntry.querySelector('div').cloneNode(true); - contentWrapper.appendChild(originalContent); + var divElement = navbarEntry.querySelector('div'); + if (divElement) { + var originalContent = divElement.cloneNode(true); + contentWrapper.appendChild(originalContent); + } else { + // Fallback: create content manually if div is not found + var fallbackContent = document.createElement('div'); + fallbackContent.innerHTML = navbarEntry.innerHTML; + contentWrapper.appendChild(fallbackContent); + } // Create the remove button var removeButton = document.createElement('button'); diff --git a/app/core/src/main/resources/static/js/navbar.js b/app/core/src/main/resources/static/js/navbar.js index 1d8c0dcce..a95ff1639 100644 --- a/app/core/src/main/resources/static/js/navbar.js +++ b/app/core/src/main/resources/static/js/navbar.js @@ -42,6 +42,39 @@ function toolsManager() { }); } +function setupDropdowns() { + const dropdowns = document.querySelectorAll('.navbar-nav > .nav-item.dropdown'); + + dropdowns.forEach((dropdown) => { + const toggle = dropdown.querySelector('[data-bs-toggle="dropdown"]'); + if (!toggle) return; + + // Skip search dropdown, it has its own logic + if (toggle.id === 'searchDropdown') { + return; + } + + dropdown.addEventListener('show.bs.dropdown', () => { + // Find all other open dropdowns and hide them + const openDropdowns = document.querySelectorAll('.navbar-nav .dropdown-menu.show'); + openDropdowns.forEach((menu) => { + const parentDropdown = menu.closest('.dropdown'); + if (parentDropdown && parentDropdown !== dropdown) { + const parentToggle = parentDropdown.querySelector('[data-bs-toggle="dropdown"]'); + if (parentToggle) { + // Get or create Bootstrap dropdown instance + let instance = bootstrap.Dropdown.getInstance(parentToggle); + if (!instance) { + instance = new bootstrap.Dropdown(parentToggle); + } + instance.hide(); + } + } + }); + }); + }); +} + window.tooltipSetup = () => { const tooltipElements = document.querySelectorAll('[title]'); @@ -56,23 +89,54 @@ window.tooltipSetup = () => { document.body.appendChild(customTooltip); element.addEventListener('mouseenter', (event) => { - customTooltip.style.display = 'block'; - customTooltip.style.left = `${event.pageX + 10}px`; // Position tooltip slightly away from the cursor - customTooltip.style.top = `${event.pageY + 10}px`; + if (window.innerWidth >= 1200) { + customTooltip.style.display = 'block'; + customTooltip.style.left = `${event.pageX + 10}px`; + customTooltip.style.top = `${event.pageY + 10}px`; + } }); - // Update the position of the tooltip as the user moves the mouse element.addEventListener('mousemove', (event) => { - customTooltip.style.left = `${event.pageX + 10}px`; - customTooltip.style.top = `${event.pageY + 10}px`; + if (window.innerWidth >= 1200) { + customTooltip.style.left = `${event.pageX + 10}px`; + customTooltip.style.top = `${event.pageY + 10}px`; + } }); - // Hide the tooltip when the mouse leaves element.addEventListener('mouseleave', () => { customTooltip.style.display = 'none'; }); }); }; + +// Override the bootstrap dropdown styles for mobile +function fixNavbarDropdownStyles() { + if (window.innerWidth < 1200) { + document.querySelectorAll('.navbar .dropdown-menu').forEach(function(menu) { + menu.style.transform = 'none'; + menu.style.transformOrigin = 'none'; + menu.style.left = '0'; + menu.style.right = '0'; + menu.style.maxWidth = '95vw'; + menu.style.width = '100vw'; + menu.style.marginBottom = '0'; + }); + } else { + document.querySelectorAll('.navbar .dropdown-menu').forEach(function(menu) { + menu.style.transform = ''; + menu.style.transformOrigin = ''; + menu.style.left = ''; + menu.style.right = ''; + menu.style.maxWidth = ''; + menu.style.width = ''; + menu.style.marginBottom = ''; + }); + } +} + document.addEventListener('DOMContentLoaded', () => { tooltipSetup(); + setupDropdowns(); + fixNavbarDropdownStyles(); }); +window.addEventListener('resize', fixNavbarDropdownStyles); diff --git a/app/core/src/main/resources/static/js/search.js b/app/core/src/main/resources/static/js/search.js index c7932965c..277d722a9 100644 --- a/app/core/src/main/resources/static/js/search.js +++ b/app/core/src/main/resources/static/js/search.js @@ -56,8 +56,16 @@ document.querySelector("#navbarSearchInput").addEventListener("input", function contentWrapper.style.textDecoration = "none"; contentWrapper.style.color = "inherit"; - var originalContent = item.querySelector("div").cloneNode(true); - contentWrapper.appendChild(originalContent); + var divElement = item.querySelector("div"); + if (divElement) { + var originalContent = divElement.cloneNode(true); + contentWrapper.appendChild(originalContent); + } else { + // Fallback: create content manually if div is not found + var fallbackContent = document.createElement("div"); + fallbackContent.innerHTML = item.innerHTML; + contentWrapper.appendChild(fallbackContent); + } contentWrapper.onclick = function () { window.location.href = itemHref; @@ -77,35 +85,52 @@ document.querySelector("#navbarSearchInput").addEventListener("input", function const searchDropdown = document.getElementById('searchDropdown'); const searchInput = document.getElementById('navbarSearchInput'); -const dropdownMenu = searchDropdown.querySelector('.dropdown-menu'); -// Handle dropdown shown event -searchDropdown.addEventListener('shown.bs.dropdown', function () { - searchInput.focus(); -}); +// Check if elements exist before proceeding +if (searchDropdown && searchInput) { + const dropdownMenu = searchDropdown.querySelector('.dropdown-menu'); -// Handle hover opening -searchDropdown.addEventListener('mouseenter', function () { + // Create a single dropdown instance const dropdownInstance = new bootstrap.Dropdown(searchDropdown); - dropdownInstance.show(); - setTimeout(() => { - searchInput.focus(); - }, 100); -}); +// Handle click for mobile + searchDropdown.addEventListener('click', function (e) { + e.preventDefault(); + const isOpen = dropdownMenu.classList.contains('show'); + // Close all other open dropdowns + document.querySelectorAll('.navbar-nav .dropdown-menu.show').forEach((menu) => { + if (menu !== dropdownMenu) { + const parentDropdown = menu.closest('.dropdown'); + if (parentDropdown) { + const parentToggle = parentDropdown.querySelector('[data-bs-toggle="dropdown"]'); + if (parentToggle) { + let instance = bootstrap.Dropdown.getInstance(parentToggle); + if (!instance) { + instance = new bootstrap.Dropdown(parentToggle); + } + instance.hide(); + } + } + } + }); + if (!isOpen) { + dropdownInstance.show(); + setTimeout(() => searchInput.focus(), 150); + } else { + dropdownInstance.hide(); + } + }); -// Handle mouse leave -searchDropdown.addEventListener('mouseleave', function () { - // Check if current value is empty (including if user typed and then deleted) - if (searchInput.value.trim().length === 0) { - searchInput.blur(); - const dropdownInstance = new bootstrap.Dropdown(searchDropdown); - dropdownInstance.hide(); - } -}); + // Hide dropdown if it's open and user clicks outside + document.addEventListener('click', function(e) { + if (!searchDropdown.contains(e.target) && dropdownMenu.classList.contains('show')) { + dropdownInstance.hide(); + } + }); -searchDropdown.addEventListener('hidden.bs.dropdown', function () { - if (searchInput.value.trim().length === 0) { - searchInput.blur(); - } -}); + // Keep dropdown open if search input is clicked + searchInput.addEventListener('click', function (e) { + e.stopPropagation(); + }); + +} diff --git a/app/core/src/main/resources/templates/fragments/common.html b/app/core/src/main/resources/templates/fragments/common.html index d873fd7a1..78f0d5662 100644 --- a/app/core/src/main/resources/templates/fragments/common.html +++ b/app/core/src/main/resources/templates/fragments/common.html @@ -29,8 +29,40 @@ // Determine if this is actually a high DPI screen at page load const isHighDPI = systemDPR > 1.4; + + // Reset all navbar and dropdown scaling styles + function resetNavScaling() { + const navbarElement = document.querySelector('.navbar'); + if (navbarElement) { + navbarElement.style.transform = ''; + navbarElement.style.transformOrigin = ''; + navbarElement.style.width = ''; + navbarElement.style.left = ''; + navbarElement.style.right = ''; + navbarElement.style.marginBottom = ''; + navbarElement.classList.remove('navbar-expand-lg'); + navbarElement.classList.remove('navbar-expand-xl'); + } + // Reset dropdown scaling + const dropdowns = document.querySelectorAll('.dropdown-menu'); + dropdowns.forEach(dropdown => { + dropdown.style.transform = ''; + dropdown.style.transformOrigin = ''; + }); + // Reset CSS custom property + document.documentElement.style.setProperty('--navbar-height', ''); + } function scaleNav() { + resetNavScaling(); + if (window.innerWidth < 1200) { + const navbarElement = document.querySelector('.navbar'); + if (navbarElement) { + navbarElement.classList.remove('navbar-expand-lg'); + navbarElement.classList.add('navbar-expand-xl'); + } + return; + } const currentDPR = window.devicePixelRatio || 1; const browserZoom = currentDPR / systemDPR; diff --git a/app/core/src/main/resources/templates/fragments/navbar.html b/app/core/src/main/resources/templates/fragments/navbar.html index f6f707211..773810a5a 100644 --- a/app/core/src/main/resources/templates/fragments/navbar.html +++ b/app/core/src/main/resources/templates/fragments/navbar.html @@ -45,9 +45,10 @@ apps + expand_more