diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7c1c4c75..55500892 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: # with: # languages: java - - uses: gradle/gradle-build-action@v2.3.3 + - uses: gradle/gradle-build-action@v2.4.2 with: gradle-version: 7.6 arguments: assemble --no-build-cache diff --git a/src/main/resources/static/css/dragdrop.css b/src/main/resources/static/css/dragdrop.css new file mode 100644 index 00000000..e75a0d06 --- /dev/null +++ b/src/main/resources/static/css/dragdrop.css @@ -0,0 +1,78 @@ +#drag-container { + position: fixed; + display:flex; + inset: 0; + pointer-events: none; + z-index: 10000; + visibility: hidden; +} + +#drag-container:not(:empty) { + visibility: visible; +} + +#drag-container .dragged-img { + position: fixed; + max-width: 200px; + max-height: 200px; + box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.58); + transform-origin: top left; +} + +.drag-manager_dragging { + width: 0px; + visibility: hidden; +} + +.drag-manager_draghover { + width: 375px !important; +} + +.drag-manager_draghover .insert-file-button-container { + display: none !important; +} + +.drag-manager_draghover .button-container { + visibility: hidden !important; +} + +html[lang-direction=ltr] .drag-manager_draghover img { + left: calc(50% + 62.5px) !important; +} + +html[lang-direction=rtl] .drag-manager_draghover img { + left: 125px +} + +.drag-manager_dragging-container .hide-on-drag { + display: none !important; +} + +.drag-manager_endpoint { + width: 80px; + height: 100%; + background-color: #FFFFFF10; + transition: width 0.1s; + animation: end-drop-expand .3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.drag-manager_endpoint svg { + width: 50px; + height: 50px; +} + +.drag-manager_endpoint.drag-manager_draghover { + width: 150px !important; +} + +@keyframes end-drop-expand { + from { + width: 0; + } + to { + width: 80px; + } +} \ No newline at end of file diff --git a/src/main/resources/static/css/imageHighlighter.css b/src/main/resources/static/css/imageHighlighter.css new file mode 100644 index 00000000..231895d6 --- /dev/null +++ b/src/main/resources/static/css/imageHighlighter.css @@ -0,0 +1,40 @@ + +#image-highlighter { + position: fixed; + display:flex; + inset: 0; + z-index: 10000; + background-color: rgba(0, 0, 0, 0); + visibility: hidden; + align-items: center; + justify-content: center; + transition: visbility 0.1s linear, background-color 0.1s linear; +} + +#image-highlighter > * { + max-width: 80vw; + max-height: 80vh; + animation: image-highlight .1s linear; + transition: transform .1s linear, opacity .1s linear; +} + +#image-highlighter > *.remove { + transform: scale(0.8) !important; + opacity: 0 !important; +} + +#image-highlighter:not(:empty) { + background-color: rgba(0, 0, 0, 0.37); + visibility: visible; +} + +@keyframes image-highlight { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/main/resources/static/css/pdfActions.css b/src/main/resources/static/css/pdfActions.css new file mode 100644 index 00000000..152e3ebc --- /dev/null +++ b/src/main/resources/static/css/pdfActions.css @@ -0,0 +1,87 @@ + +.pdf-actions_button-container { + z-index: 2; + display:flex; + opacity: 0; + transition: opacity 0.1s linear; +} + +.pdf-actions_container:hover .pdf-actions_button-container { + opacity: 1; +} + +.pdf-actions_button-container > * { + padding: 0.25rem 0.5rem; + margin: 3px; + display: block; +} + +.pdf-actions_container svg { + width: 16px; + height: 16px; +} +.pdf-actions_container:nth-child(1) .pdf-actions_move-left-button { + display: none; +} +.pdf-actions_container:last-child .pdf-actions_move-right-button { + display: none; +} + +/* "insert pdf" buttons that appear on the right when hover */ +.pdf-actions_insert-file-button-container { + translate: 0 -50%; + width: 80px; + height: 100%; + + z-index: 1; + opacity: 0; + transition: opacity 0.2s; +} + +.pdf-actions_insert-file-button-container.left { + left: -20px; +} + +.pdf-actions_insert-file-button-container.right { + right: -20px; +} + +html[lang-direction=ltr] .pdf-actions_insert-file-button-container.right { + display:none; +} + +html[lang-direction=rtl] .pdf-actions_insert-file-button-container.left { + display:none; +} + +.pdf-actions_insert-file-button-container.left .pdf-actions_insert-file-button { + left: 0; + translate: 0 -50%; +} + +.pdf-actions_insert-file-button-container.right .pdf-actions_insert-file-button { + right: 0; + translate: 0 -50%; +} + +html[lang-direction=ltr] .pdf-actions_container:last-child > .pdf-actions_insert-file-button-container.right { + display: block; +} + + +html[lang-direction=rtl] .pdf-actions_container:last-child > .pdf-actions_insert-file-button-container.left { + display: block; +} + +.pdf-actions_insert-file-button-container:hover { + opacity: 1; + transition: opacity 0.05s; +} +.pdf-actions_insert-file-button { + position: absolute; + top: 50%; + right: 50%; + translate: 50% -50%; + aspect-ratio: 1; + border-radius: 100px; +} \ No newline at end of file diff --git a/src/main/resources/static/js/multitool/DragDropManager.js b/src/main/resources/static/js/multitool/DragDropManager.js new file mode 100644 index 00000000..93fbbdf1 --- /dev/null +++ b/src/main/resources/static/js/multitool/DragDropManager.js @@ -0,0 +1,127 @@ + + +class DragDropManager { + dragContainer; + wrapper; + pageDirection; + movePageTo; + pageDragging; + draggelEl; + draggedImageEl; + hoveredEl; + endInsertionElement; + + constructor(id, wrapperId) { + this.dragContainer = document.getElementById(id); + this.pageDirection = document.documentElement.getAttribute("lang-direction"); + this.wrapper = document.getElementById(wrapperId); + this.pageDragging = false; + this.hoveredEl = undefined; + this.draggelEl = undefined + this.draggedImageEl = undefined; + + var styleElement = document.createElement('link'); + styleElement.rel = 'stylesheet'; + styleElement.href = 'css/dragdrop.css' + + document.head.appendChild(styleElement); + + const div = document.createElement('div'); + div.classList.add('drag-manager_endpoint'); + div.innerHTML = ` + + + ` + this.endInsertionElement = div; + + this.startDraggingPage = this.startDraggingPage.bind(this); + this.onDragEl = this.onDragEl.bind(this); + this.stopDraggingPage = this.stopDraggingPage.bind(this); + + this.adapt(div); + } + + startDraggingPage(div,) { + this.pageDragging = true; + this.draggedEl = div; + const img = div.querySelector('img'); + div.classList.add('drag-manager_dragging'); + const imageSrc = img.src; + + const imgEl = document.createElement('img'); + imgEl.classList.add('dragged-img'); + imgEl.src = imageSrc; + this.draggedImageEl = imgEl; + imgEl.style.left = screenX; + imgEl.style.right = screenY; + imgEl.style.transform = `rotate(${img.style.rotate === '' ? '0deg' : img.style.rotate}) translate(-50%, -50%)`; + this.dragContainer.appendChild(imgEl); + + window.addEventListener('mouseup', this.stopDraggingPage) + window.addEventListener('mousemove', this.onDragEl) + this.wrapper.classList.add('drag-manager_dragging-container'); + this.wrapper.appendChild(this.endInsertionElement); + } + + onDragEl(mouseEvent) { + const { clientX, clientY } = mouseEvent; + if(this.draggedImageEl) { + this.draggedImageEl.style.left = `${clientX}px`; + this.draggedImageEl.style.top = `${clientY}px`; + } + } + + + stopDraggingPage() { + window.removeEventListener('mousemove', this.onDragEl); + this.wrapper.classList.remove('drag-manager_dragging-container'); + this.wrapper.removeChild(this.endInsertionElement); + window.removeEventListener('mouseup', this.stopDraggingPage) + this.draggedImageEl = undefined; + this.pageDragging = false; + this.draggedEl.classList.remove('drag-manager_dragging'); + this.hoveredEl?.classList.remove('drag-manager_draghover'); + this.dragContainer.childNodes.forEach((dragChild) => { + this.dragContainer.removeChild(dragChild); + }) + if(!this.hoveredEl) { + return; + } + if(this.hoveredEl === this.endInsertionElement) { + this.movePageTo(this.draggedEl); + return; + } + this.movePageTo(this.draggedEl, this.hoveredEl); + } + + setActions({ movePageTo }) { + this.movePageTo = movePageTo; + } + + + adapt(div) { + const onDragStart = () => { + this.startDraggingPage(div); + } + + const onMouseEnter = () => { + if (this.pageDragging) { + this.hoveredEl = div; + div.classList.add('drag-manager_draghover'); + } + } + + const onMouseLeave = () => { + this.hoveredEl = undefined + div.classList.remove('drag-manager_draghover'); + } + + div.addEventListener('dragstart', onDragStart); + div.addEventListener('mouseenter', onMouseEnter); + div.addEventListener('mouseleave', onMouseLeave); + + return div; + } +} + +export default DragDropManager; \ No newline at end of file diff --git a/src/main/resources/static/js/multitool/ImageHighlighter.js b/src/main/resources/static/js/multitool/ImageHighlighter.js new file mode 100644 index 00000000..6f7cd22e --- /dev/null +++ b/src/main/resources/static/js/multitool/ImageHighlighter.js @@ -0,0 +1,46 @@ +class ImageHiglighter { + imageHighlighter; + constructor(id) { + this.imageHighlighter = document.getElementById(id); + this.imageHighlightCallback = this.imageHighlightCallback.bind(this); + + var styleElement = document.createElement('link'); + styleElement.rel = 'stylesheet'; + styleElement.href = 'css/imageHighlighter.css' + + document.head.appendChild(styleElement); + + this.imageHighlighter.onclick = () => { + this.imageHighlighter.childNodes.forEach((child) => { + child.classList.add('remove'); + setTimeout(() => { + this.imageHighlighter.removeChild(child); + }, 100) + }) + } + } + + imageHighlightCallback(highlightEvent) { + var bigImg = document.createElement('img'); + bigImg.onclick = (imageClickEvent) => { + // This prevents the highlighter's onClick from closing the image when clicking + // on the image instead of next to it. + imageClickEvent.preventDefault(); + imageClickEvent.stopPropagation(); + }; + bigImg.src = highlightEvent.target.src; + this.imageHighlighter.appendChild(bigImg); + }; + + setActions() { + // not needed in this case + } + + adapt(div) { + const img = div.querySelector('.page-image'); + img.addEventListener('click', this.imageHighlightCallback) + return div; + } +} + +export default ImageHiglighter; \ No newline at end of file diff --git a/src/main/resources/static/js/multitool/PdfActionsManager.js b/src/main/resources/static/js/multitool/PdfActionsManager.js new file mode 100644 index 00000000..4bff39e3 --- /dev/null +++ b/src/main/resources/static/js/multitool/PdfActionsManager.js @@ -0,0 +1,167 @@ +class PdfActionsManager { + pageDirection; + pagesContainer; + + constructor(id) { + this.pagesContainer = document.getElementById(id); + this.pageDirection = document.documentElement.getAttribute("lang-direction"); + + var styleElement = document.createElement('link'); + styleElement.rel = 'stylesheet'; + styleElement.href = 'css/pdfActions.css' + + document.head.appendChild(styleElement); + } + + getPageContainer(element) { + var container = element + while (!container.classList.contains('page-container')) { + container = container.parentNode; + } + return container; + } + + moveUpButtonCallback(e) { + var imgContainer = this.getPageContainer(e.target); + + const sibling = imgContainer.previousSibling; + if (sibling) { + this.movePageTo(imgContainer, sibling, true); + } + } + + moveDownButtonCallback(e) { + var imgContainer = this.getPageContainer(e.target); + const sibling = imgContainer.nextSibling; + if (sibling) { + this.movePageTo(imgContainer, sibling.nextSibling, true); + } + }; + + rotateCCWButtonCallback(e) { + var imgContainer = this.getPageContainer(e.target); + const img = imgContainer.querySelector("img"); + + this.rotateElement(img, -90) + }; + + rotateCWButtonCallback(e) { + var imgContainer = this.getPageContainer(e.target); + const img = imgContainer.querySelector("img"); + + this.rotateElement(img, 90) + }; + + deletePageButtonCallback(e) { + var imgContainer = this.getPageContainer(e.target); + this.pagesContainer.removeChild(imgContainer); + }; + + insertFileButtonCallback(e) { + var imgContainer = this.getPageContainer(e.target); + this.addPdfs(imgContainer) + }; + + setActions({ movePageTo, addPdfs, rotateElement }) { + this.movePageTo = movePageTo; + this.addPdfs = addPdfs; + this.rotateElement = rotateElement; + + this.moveUpButtonCallback = this.moveUpButtonCallback.bind(this); + this.moveDownButtonCallback = this.moveDownButtonCallback.bind(this); + this.rotateCCWButtonCallback = this.rotateCCWButtonCallback.bind(this); + this.rotateCWButtonCallback = this.rotateCWButtonCallback.bind(this); + this.deletePageButtonCallback = this.deletePageButtonCallback.bind(this); + this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this); + } + + adapt(div) { + div.classList.add('pdf-actions_container'); + const leftDirection = this.pageDirection === 'rtl' ? 'right' : 'left' + const rightDirection = this.pageDirection === 'rtl' ? 'left' : 'right' + const buttonContainer = document.createElement('div'); + + buttonContainer.classList.add("pdf-actions_button-container", "hide-on-drag"); + + const moveUp = document.createElement('button'); + moveUp.classList.add("pdf-actions_move-left-button","btn", "btn-secondary"); + moveUp.innerHTML = ``; + moveUp.onclick = this.moveUpButtonCallback; + buttonContainer.appendChild(moveUp); + + const moveDown = document.createElement('button'); + moveDown.classList.add("pdf-actions_move-right-button","btn", "btn-secondary"); + moveDown.innerHTML = ``; + moveDown.onclick = this.moveDownButtonCallback; + buttonContainer.appendChild(moveDown); + + const rotateCCW = document.createElement('button'); + rotateCCW.classList.add("btn", "btn-secondary"); + rotateCCW.innerHTML = ` + + + `; + rotateCCW.onclick = this.rotateCCWButtonCallback; + buttonContainer.appendChild(rotateCCW); + + const rotateCW = document.createElement('button'); + rotateCW.classList.add("btn", "btn-secondary"); + rotateCW.innerHTML = ` + + + `; + rotateCW.onclick = this.rotateCWButtonCallback; + buttonContainer.appendChild(rotateCW); + + const deletePage = document.createElement('button'); + deletePage.classList.add("btn", "btn-danger"); + deletePage.innerHTML = ` + + + `; + deletePage.onclick = this.deletePageButtonCallback; + buttonContainer.appendChild(deletePage); + + div.appendChild(buttonContainer); + + const insertFileButtonContainer = document.createElement('div'); + + insertFileButtonContainer.classList.add( + "pdf-actions_insert-file-button-container", + leftDirection, + `align-center-${leftDirection}`); + + const insertFileButton = document.createElement('button'); + insertFileButton.classList.add("btn", "btn-primary", "pdf-actions_insert-file-button"); + insertFileButton.innerHTML = ` + + + `; + insertFileButton.onclick = this.insertFileButtonCallback; + insertFileButtonContainer.appendChild(insertFileButton); + + div.appendChild(insertFileButtonContainer); + + // add this button to every element, but only show it on the last one :D + const insertFileButtonRightContainer = document.createElement('div'); + insertFileButtonRightContainer.classList.add( + "pdf-actions_insert-file-button-container", + rightDirection, + `align-center-${rightDirection}`); + + const insertFileButtonRight = document.createElement('button'); + insertFileButtonRight.classList.add("btn", "btn-primary", "pdf-actions_insert-file-button"); + insertFileButtonRight.innerHTML = ` + + + insertFileButtonRight`; + insertFileButtonRight.onclick = () => addPdfs(); + insertFileButtonRightContainer.appendChild(insertFileButtonRight); + + div.appendChild(insertFileButtonRightContainer); + + return div; + } +} + +export default PdfActionsManager; \ No newline at end of file diff --git a/src/main/resources/static/js/multitool/PdfContainer.js b/src/main/resources/static/js/multitool/PdfContainer.js new file mode 100644 index 00000000..f1cc1061 --- /dev/null +++ b/src/main/resources/static/js/multitool/PdfContainer.js @@ -0,0 +1,206 @@ +class PdfContainer { + fileName; + pagesContainer; + pagesContainerWrapper; + pdfAdapters; + + constructor(id, wrapperId, pdfAdapters) { + this.fileName = null; + this.pagesContainer = document.getElementById(id) + this.pagesContainerWrapper = document.getElementById(wrapperId); + this.movePageTo = this.movePageTo.bind(this); + this.addPdfs = this.addPdfs.bind(this); + this.rotateElement = this.rotateElement.bind(this); + this.rotateAll = this.rotateAll.bind(this); + this.exportPdf = this.exportPdf.bind(this); + + this.pdfAdapters = pdfAdapters; + + this.pdfAdapters.forEach(adapter => { + adapter.setActions({ + movePageTo: this.movePageTo, + addPdfs: this.addPdfs, + rotateElement: this.rotateElement, + }) + }) + + window.addPdfs = this.addPdfs; + window.exportPdf = this.exportPdf; + window.rotateAll = this.rotateAll; + } + + movePageTo(startElement, endElement, scrollTo = false) { + const childArray = Array.from(this.pagesContainer.childNodes); + const startIndex = childArray.indexOf(startElement); + const endIndex = childArray.indexOf(endElement); + this.pagesContainer.removeChild(startElement); + if(!endElement) { + this.pagesContainer.append(startElement); + } else { + this.pagesContainer.insertBefore(startElement, endElement); + } + + if(scrollTo) { + const { width } = startElement.getBoundingClientRect(); + const vector = (endIndex !== -1 && startIndex > endIndex) + ? 0-width + : width; + + this.pagesContainerWrapper.scroll({ + left: this.pagesContainerWrapper.scrollLeft + vector, + }) + } + } + + addPdfs(nextSiblingElement) { + var input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.setAttribute("accept", "application/pdf"); + + input.onchange = async(e) => { + const files = e.target.files; + this.fileName = files[0].name; + for (var i=0; i < files.length; i++) { + this.addPdfFile(files[i], nextSiblingElement); + } + + document.querySelectorAll(".enable-on-file").forEach(element => { + element.disabled = false; + }); + } + + input.click(); + } + + rotateElement(element, deg) { + var lastTransform = element.style.rotate; + if (!lastTransform) { + lastTransform = "0"; + } + const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, '')); + const newAngle = lastAngle + deg; + + element.style.rotate = newAngle + "deg"; + } + + async addPdfFile(file, nextSiblingElement) { + const { renderer, pdfDocument } = await this.loadFile(file); + + for (var i=0; i < renderer.pageCount; i++) { + const div = document.createElement('div'); + + div.classList.add("page-container"); + + var img = document.createElement('img'); + img.classList.add('page-image') + const imageSrc = await renderer.renderPage(i) + img.src = imageSrc; + img.pageIdx = i; + img.rend = renderer; + img.doc = pdfDocument; + div.appendChild(img); + + this.pdfAdapters.forEach((adapter) => { + adapter.adapt?.(div) + }) + if (nextSiblingElement) { + this.pagesContainer.insertBefore(div, nextSiblingElement); + } else { + this.pagesContainer.appendChild(div); + } + } + } + + async loadFile(file) { + var objectUrl = URL.createObjectURL(file); + var pdfDocument = await this.toPdfLib(objectUrl); + var renderer = await this.toRenderer(objectUrl); + return { renderer, pdfDocument }; + } + + async toRenderer(objectUrl) { + const pdf = await pdfjsLib.getDocument(objectUrl).promise; + return { + document: pdf, + pageCount: pdf.numPages, + renderPage: async function(pageIdx) { + const page = await this.document.getPage(pageIdx+1); + + const canvas = document.createElement("canvas"); + + // set the canvas size to the size of the page + if (page.rotate == 90 || page.rotate == 270) { + canvas.width = page.view[3]; + canvas.height = page.view[2]; + } else { + canvas.width = page.view[2]; + canvas.height = page.view[3]; + } + + // render the page onto the canvas + var renderContext = { + canvasContext: canvas.getContext("2d"), + viewport: page.getViewport({ scale: 1 }) + }; + + await page.render(renderContext).promise; + return canvas.toDataURL(); + } + }; + } + + async toPdfLib(objectUrl) { + const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer()); + const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes); + return pdfDoc; + } + + + + rotateAll(deg) { + for (var i=0; i { + var scrollDelta = 0; // variable to store the accumulated scroll delta + var isScrolling = false; // variable to track if scroll is already in progress + const divToScrollHorizontally = document.getElementById(id) + function scrollLoop() { + // Scroll the div horizontally by a fraction of the accumulated scroll delta + divToScrollHorizontally.scrollLeft += scrollDelta * 0.1; + + // Reduce the accumulated scroll delta by a fraction + scrollDelta *= 0.9; + + // If scroll delta is still significant, continue the scroll loop + if (Math.abs(scrollDelta) > 0.1) { + requestAnimationFrame(scrollLoop); + } else { + isScrolling = false; // Reset scroll in progress flag + } + } + + + divToScrollHorizontally.addEventListener("wheel", function(e) { + e.preventDefault(); // prevent default mousewheel behavior + + // Accumulate the horizontal scroll delta + scrollDelta -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY; + + // If scroll is not already in progress, start the scroll loop + if (!isScrolling) { + isScrolling = true; + requestAnimationFrame(scrollLoop); + } + }); +} + +export default scrollDivHorizontally; diff --git a/src/main/resources/templates/multi-tool.html b/src/main/resources/templates/multi-tool.html index 5d911403..2c6944b0 100644 --- a/src/main/resources/templates/multi-tool.html +++ b/src/main/resources/templates/multi-tool.html @@ -3,7 +3,6 @@ -
@@ -53,390 +52,40 @@
-
+
+ +