1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-11-16 12:20:12 +01:00

HTML, CSS, JS and JAVA corrections (#810)

* CSS corrections

* HTML corrections

* JS corrections

* JAVA corrections

* remove tab

* CSS corrections 2

* JS corrections 2

* back to the roots

* max-linie 127

* add slash hr|br

* return bootstrap-icons.css

* return bootstrap-icons.min.css

* return bootstrap.min.css

* Update bootstrap-icons.css

* Update bootstrap-icons.min.css

* Update bootstrap-icons.min.css

* Update bootstrap.min.css

* CSS corrections

* HTML corrections

* JS corrections

* JAVA corrections

* remove tab

* CSS corrections 2

* JS corrections 2

* back to the roots

* max-linie 127

* add slash hr|br

* return bootstrap-icons.css

* Update bootstrap-icons.css

* Bootstrap CSS

* Update prism.css
This commit is contained in:
Ludy 2024-02-16 22:49:06 +01:00 committed by GitHub
parent 68f582bcb9
commit e4a76e96af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
124 changed files with 9025 additions and 9424 deletions

View File

@ -33,6 +33,8 @@ public class AccountWebController {
return "redirect:/"; return "redirect:/";
} }
model.addAttribute("currentPage", "login");
if (request.getParameter("error") != null) { if (request.getParameter("error") != null) {
model.addAttribute("error", request.getParameter("error")); model.addAttribute("error", request.getParameter("error"));
@ -112,6 +114,7 @@ public class AccountWebController {
model.addAttribute("role", user.get().getRolesAsString()); model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("settings", settingsJson); model.addAttribute("settings", settingsJson);
model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
model.addAttribute("currentPage", "account");
} }
} else { } else {
return "redirect:/"; return "redirect:/";

View File

@ -0,0 +1,4 @@
.buttons-container {
margin-top: 20px;
text-align: center;
}

View File

@ -0,0 +1,28 @@
#box-drag-container {
position: relative;
margin: 20px 0;
}
#pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%;
}
.draggable-buttons-box {
position: absolute;
top: 0;
padding: 10px;
width: 100%;
display: flex;
gap: 5px;
}
.draggable-buttons-box > button {
z-index: 10;
background-color: rgba(13, 110, 253, 0.1);
}
.draggable-canvas {
border: 1px solid red;
position: absolute;
touch-action: none;
user-select: none;
top: 0px;
left: 0;
}

View File

@ -1,9 +1,11 @@
/* Dark Mode Styles */ /* Dark Mode Styles */
body, select, textarea { body,
--body-background-color: 51, 51, 51; select,
--base-font-color: 255, 255, 255; textarea {
background-color: rgb(var(--body-background-color)) !important; --body-background-color: 51, 51, 51;
color: rgb(var(--base-font-color)) !important; --base-font-color: 255, 255, 255;
background-color: rgb(var(--body-background-color)) !important;
color: rgb(var(--base-font-color)) !important;
} }
.card { .card {
background-color: rgb(var(--body-background-color)) !important; background-color: rgb(var(--body-background-color)) !important;
@ -11,11 +13,11 @@ body, select, textarea {
color: rgb(var(--base-font-color)) !important; color: rgb(var(--base-font-color)) !important;
} }
a { a {
color: #add8e6; color: #add8e6;
} }
a:hover { a:hover {
color: #87ceeb; /* Slightly brighter blue on hover for accessibility */ color: #87ceeb; /* Slightly brighter blue on hover for accessibility */
} }
.dark-card { .dark-card {
@ -36,7 +38,7 @@ body, select, textarea {
color: rgb(var(--base-font-color)) !important; color: rgb(var(--base-font-color)) !important;
} }
#support-section { #support-section {
background-color: #444 !important; background-color: #444 !important;
} }
#pages-container-wrapper { #pages-container-wrapper {
@ -47,89 +49,93 @@ body, select, textarea {
} }
.favorite-icon img { .favorite-icon img {
filter: brightness(0) invert(1) !important; filter: brightness(0) invert(1) !important;
} }
table thead { table thead {
background-color: #333 !important; background-color: #333 !important;
border: 1px solid #444; border: 1px solid #444;
} }
table th, table td { table th,
border: 1px solid #444 !important; table td {
color: white; border: 1px solid #444 !important;
color: white;
} }
.btn { .btn {
background-color: #444 !important; background-color: #444 !important;
border: none; border: none;
color: #fff !important; color: #fff !important;
} }
.btn-primary { .btn-primary {
background-color: #007bff !important; background-color: #007bff !important;
border: none; border: none;
color: #fff !important; color: #fff !important;
} }
.btn-secondary { .btn-secondary {
background-color: #6c757d !important; background-color: #6c757d !important;
border: none; border: none;
color: #fff !important; color: #fff !important;
} }
.btn-info { .btn-info {
background-color: #17a2b8 !important; background-color: #17a2b8 !important;
border: none; border: none;
color: #fff !important; color: #fff !important;
} }
.btn-danger { .btn-danger {
background-color: #dc3545 !important; background-color: #dc3545 !important;
border: none; border: none;
color: #fff !important; color: #fff !important;
} }
.btn-warning { .btn-warning {
background-color: #ffc107 !important; background-color: #ffc107 !important;
border: none; border: none;
color: #000 !important; color: #000 !important;
} }
.btn-outline-secondary { .btn-outline-secondary {
color: #fff !important; color: #fff !important;
border-color: #fff; border-color: #fff;
} }
.btn-outline-secondary:hover { .btn-outline-secondary:hover {
background-color: #444 !important; background-color: #444 !important;
color: #007bff !important; color: #007bff !important;
border-color: #007bff; border-color: #007bff;
} }
.blackwhite-icon { .blackwhite-icon {
filter: brightness(0) invert(1); filter: brightness(0) invert(1);
} }
hr { hr {
border-color: rgba(255, 255, 255, 0.6); /* semi-transparent white */ border-color: rgba(255, 255, 255, 0.6); /* semi-transparent white */
background-color: rgba(255, 255, 255, 0.6); /* for some browsers that might use background instead of border for <hr> */ background-color: rgba(255, 255, 255, 0.6); /* for some browsers that might use background instead of border for <hr> */
} }
.modal-content { .modal-content {
color: #fff !important; color: #fff !important;
border-color: #fff; border-color: #fff;
} }
#global-buttons-container input { #global-buttons-container input {
background-color: #323948; background-color: #323948;
caret-color: #ffffff; caret-color: #ffffff;
color: #ffffff; color: #ffffff;
} }
#global-buttons-container input::placeholder { #global-buttons-container input::placeholder {
color: #ffffff; color: #ffffff;
} }
#global-buttons-container input:disabled::-webkit-input-placeholder { /* WebKit browsers */ #global-buttons-container input:disabled::-webkit-input-placeholder {
color: #6E6865; /* WebKit browsers */
color: #6e6865;
} }
#global-buttons-container input:disabled:-moz-placeholder { /* Mozilla Firefox 4 to 18 */ #global-buttons-container input:disabled:-moz-placeholder {
color: #6E6865; /* Mozilla Firefox 4 to 18 */
color: #6e6865;
} }
#global-buttons-container input:disabled::-moz-placeholder { /* Mozilla Firefox 19+ */ #global-buttons-container input:disabled::-moz-placeholder {
color: #6E6865; /* Mozilla Firefox 19+ */
color: #6e6865;
} }
#global-buttons-container input:disabled:-ms-input-placeholder { /* Internet Explorer 10+ */ #global-buttons-container input:disabled:-ms-input-placeholder {
color: #6E6865; /* Internet Explorer 10+ */
color: #6e6865;
} }

View File

@ -1,78 +1,78 @@
#drag-container { #drag-container {
position: fixed; position: fixed;
display:flex; display: flex;
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
z-index: 10000; z-index: 10000;
visibility: hidden; visibility: hidden;
} }
#drag-container:not(:empty) { #drag-container:not(:empty) {
visibility: visible; visibility: visible;
} }
#drag-container .dragged-img { #drag-container .dragged-img {
position: fixed; position: fixed;
max-width: 200px; max-width: 200px;
max-height: 200px; max-height: 200px;
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.58); box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.58);
transform-origin: top left; transform-origin: top left;
} }
.drag-manager_dragging { .drag-manager_dragging {
width: 0px; width: 0px;
visibility: hidden; visibility: hidden;
} }
.drag-manager_draghover { .drag-manager_draghover {
width: 375px !important; width: 375px !important;
} }
.drag-manager_draghover .insert-file-button-container { .drag-manager_draghover .insert-file-button-container {
display: none !important; display: none !important;
} }
.drag-manager_draghover .button-container { .drag-manager_draghover .button-container {
visibility: hidden !important; visibility: hidden !important;
} }
html[lang-direction=ltr] .drag-manager_draghover img { html[lang-direction="ltr"] .drag-manager_draghover img {
left: calc(50% + 62.5px) !important; left: calc(50% + 62.5px) !important;
} }
html[lang-direction=rtl] .drag-manager_draghover img { html[lang-direction="rtl"] .drag-manager_draghover img {
left: 125px left: 125px;
} }
.drag-manager_dragging-container .hide-on-drag { .drag-manager_dragging-container .hide-on-drag {
display: none !important; display: none !important;
} }
.drag-manager_endpoint { .drag-manager_endpoint {
width: 80px; width: 80px;
height: 100%; height: 100%;
background-color: #FFFFFF10; background-color: #ffffff10;
transition: width 0.1s; transition: width 0.1s;
animation: end-drop-expand .3s ease; animation: end-drop-expand 0.3s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.drag-manager_endpoint svg { .drag-manager_endpoint svg {
width: 50px; width: 50px;
height: 50px; height: 50px;
} }
.drag-manager_endpoint.drag-manager_draghover { .drag-manager_endpoint.drag-manager_draghover {
width: 150px !important; width: 150px !important;
} }
@keyframes end-drop-expand { @keyframes end-drop-expand {
from { from {
width: 0; width: 0;
} }
to { to {
width: 80px; width: 80px;
} }
} }

View File

@ -0,0 +1,88 @@
h1 {
text-align: center;
margin-top: 10%;
}
p {
text-align: center;
margin-top: 2em;
}
.button:hover {
background-color: #005b7f;
}
.features-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr));
gap: 25px 30px;
}
.feature-card {
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.feature-card .card-text {
flex: 1;
}
#support-section {
background-color: #f9f9f9;
padding: 4rem;
margin-top: 1rem;
text-align: center;
}
#support-section h1 {
margin-top: 0;
}
#support-section p {
margin-top: 0;
}
#button-group {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
#github-button,
#discord-button {
display: inline-block;
padding: 1rem 2rem;
margin: 1rem;
background-color: #008cba;
color: #fff;
font-size: 1.2rem;
text-align: center;
text-decoration: none;
border-radius: 3rem;
transition: all 0.3s ease-in-out;
}
#github-button:hover,
#discord-button:hover,
#home-button:hover {
background-color: #005b7f;
}
#home-button {
display: block;
width: 200px;
height: 50px;
margin: 2em auto;
background-color: #008cba;
color: white;
text-align: center;
line-height: 50px;
text-decoration: none;
font-weight: bold;
border-radius: 25px;
transition: all 0.3s ease-in-out;
}

View File

@ -1,94 +1,97 @@
#errorContainer { #errorContainer {
margin: 20px; /* adjust this value as needed */ margin: 20px; /* adjust this value as needed */
} }
#helpModalDialog { #helpModalDialog {
width: 90%; width: 90%;
max-width: 800px; max-width: 800px;
} }
#helpModal h1 { #helpModal h1 {
text-align: center; text-align: center;
margin-top: 10%; margin-top: 10%;
} }
#helpModal p { #helpModal p {
text-align: center; text-align: center;
margin-top: 2em; margin-top: 2em;
} }
#helpModal .button:hover { #helpModal .button:hover {
background-color: #005b7f; background-color: #005b7f;
} }
#helpModal .features-container { #helpModal .features-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr)); grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr));
gap: 25px 30px; gap: 25px 30px;
} }
#helpModal .feature-card { #helpModal .feature-card {
border: 1px solid rgba(0, 0, 0, .125); border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 1.25rem; padding: 1.25rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
#helpModal .feature-card .card-text { #helpModal .feature-card .card-text {
flex: 1; flex: 1;
} }
#support-section { #support-section {
background-color: #f9f9f9; background-color: #f9f9f9;
padding: 4rem; padding: 4rem;
margin-top: 1rem; margin-top: 1rem;
text-align: center; text-align: center;
} }
#support-section h1 { #support-section h1 {
margin-top: 0; margin-top: 0;
} }
#support-section p { #support-section p {
margin-top: 0; margin-top: 0;
} }
#button-group { #button-group {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
#github-button, #discord-button { #github-button,
display: inline-block; #discord-button {
padding: 1rem 2rem; display: inline-block;
margin: 1rem; padding: 1rem 2rem;
background-color: #008CBA; margin: 1rem;
color: #fff; background-color: #008cba;
font-size: 1.2rem; color: #fff;
text-align: center; font-size: 1.2rem;
text-decoration: none; text-align: center;
border-radius: 3rem; text-decoration: none;
transition: all 0.3s ease-in-out; border-radius: 3rem;
transition: all 0.3s ease-in-out;
} }
#github-button:hover, #discord-button:hover, #home-button:hover { #github-button:hover,
background-color: #005b7f; #discord-button:hover,
#home-button:hover {
background-color: #005b7f;
} }
#home-button { #home-button {
display: block; display: block;
width: 200px; width: 200px;
height: 50px; height: 50px;
margin: 2em auto; margin: 2em auto;
background-color: #008CBA; background-color: #008cba;
color: white; color: white;
text-align: center; text-align: center;
line-height: 50px; line-height: 50px;
text-decoration: none; text-decoration: none;
font-weight: bold; font-weight: bold;
border-radius: 25px; border-radius: 25px;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }

View File

@ -1,10 +1,10 @@
.custom-file-label { .custom-file-label {
padding-right: 90px; padding-right: 90px;
} }
.selected-files { .selected-files {
margin-top: 10px; margin-top: 10px;
max-height: 150px; max-height: 150px;
overflow-y: auto; overflow-y: auto;
white-space: pre-wrap; white-space: pre-wrap;
} }

View File

@ -0,0 +1,20 @@
#footer {
display: flex;
flex-direction: column; /* Stack children vertically */
justify-content: center;
align-items: center;
width: 100%;
}
.footer-center {
display: flex;
align-items: center; /* Center children horizontally */
flex-grow: 1;
}
.footer-powered-by {
margin-top: auto; /* Pushes the text to the bottom */
color: grey;
text-align: center; /* Centers the text inside the div */
width: 100%; /* Full width to center the text properly */
}

View File

@ -1,49 +1,54 @@
#game-container { #game-container {
position: relative; position: relative;
width: 100vh; width: 100vh;
height: 0; height: 0;
padding-bottom: 75%; /* 4:3 aspect ratio */ padding-bottom: 75%; /* 4:3 aspect ratio */
background-color: transparent; background-color: transparent;
margin: auto; margin: auto;
overflow: hidden; overflow: hidden;
border: 2px solid black; /* Add border */ border: 2px solid black; /* Add border */
} }
.pdf, .player, .projectile { .pdf,
position: absolute; .player,
.projectile {
position: absolute;
} }
.pdf { .pdf {
width: 50px; width: 50px;
height: 50px; height: 50px;
} }
.player { .player {
width: 50px; width: 50px;
height: 50px; height: 50px;
} }
.projectile { .projectile {
background-color: black !important; background-color: black !important;
width: 5px; width: 5px;
height: 10px; height: 10px;
} }
#score, #level, #lives, #high-score { #score,
color: black; #level,
font-family: sans-serif; #lives,
position: absolute; #high-score {
font-size: calc(14px + 0.25vw); /* Reduced font size */ color: black;
font-family: sans-serif;
position: absolute;
font-size: calc(14px + 0.25vw); /* Reduced font size */
} }
#score { #score {
top: 10px; top: 10px;
left: 10px; left: 10px;
} }
#lives { #lives {
top: 10px; top: 10px;
left: calc(7vw); /* Adjusted position */ left: calc(7vw); /* Adjusted position */
} }
#high-score { #high-score {
top: 10px; top: 10px;
left: calc(14vw); /* Adjusted position */ left: calc(14vw); /* Adjusted position */
} }
#level { #level {
top: 10px; top: 10px;
right: 10px; right: 10px;
} }

View File

@ -1,20 +1,20 @@
#page-container { #page-container {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#content-wrap { #content-wrap {
flex: 1; flex: 1;
} }
#footer { #footer {
bottom: 0; bottom: 0;
width: 100%; width: 100%;
} }
.navbar { .navbar {
height: auto; /* Adjusts height automatically based on content */ height: auto; /* Adjusts height automatically based on content */
white-space: nowrap; /* Prevents wrapping of navbar contents */ white-space: nowrap; /* Prevents wrapping of navbar contents */
} }
/* TODO enable later /* TODO enable later
.navbar .container { .navbar .container {
@ -25,70 +25,70 @@
margin-right: auto; margin-right: auto;
}*/ }*/
html[lang-direction=ltr] * { html[lang-direction="ltr"] * {
direction: ltr; direction: ltr;
} }
html[lang-direction=rtl] * { html[lang-direction="rtl"] * {
direction: rtl; direction: rtl;
text-align: right; text-align: right;
} }
.ignore-rtl { .ignore-rtl {
direction: ltr !important; direction: ltr !important;
text-align: left !important; text-align: left !important;
} }
.align-top { .align-top {
position: absolute; position: absolute;
top: 0; top: 0;
} }
.align-center-right { .align-center-right {
position: absolute; position: absolute;
right: 0; right: 0;
top: 50%; top: 50%;
} }
.align-center-left { .align-center-left {
position: absolute; position: absolute;
left: 0; left: 0;
top: 50%; top: 50%;
} }
.align-bottom { .align-bottom {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
} }
.btn-group > label:first-of-type { .btn-group > label:first-of-type {
border-top-left-radius: 0.25rem !important; border-top-left-radius: 0.25rem !important;
border-bottom-left-radius: 0.25rem !important; border-bottom-left-radius: 0.25rem !important;
} }
html[lang-direction="rtl"] input.form-check-input { html[lang-direction="rtl"] input.form-check-input {
position: relative; position: relative;
margin-left: 0px; margin-left: 0px;
} }
html[lang-direction="rtl"] label.form-check-label { html[lang-direction="rtl"] label.form-check-label {
display: inline; display: inline;
} }
.margin-auto-parent { .margin-auto-parent {
width: 100%; width: 100%;
display: flex; display: flex;
} }
.margin-center { .margin-center {
margin: 0 auto; margin: 0 auto;
} }
#pdf-canvas { #pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384); box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%; width: 100%;
} }
.fixed-shadow-canvas { .fixed-shadow-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384); box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%; width: 100%;
} }
.shadow-canvas { .shadow-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384); box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
} }
.hidden { .hidden {
display: none; display: none;
} }

View File

@ -1,64 +1,62 @@
#searchBar { #searchBar {
background-image: url('../images/search.svg'); background-image: url("../images/search.svg");
background-position: 16px 16px; background-position: 16px 16px;
background-repeat: no-repeat; background-repeat: no-repeat;
width: 100%; width: 100%;
font-size: 16px; font-size: 16px;
margin-bottom: 12px; margin-bottom: 12px;
padding: 12px 20px 12px 40px; padding: 12px 20px 12px 40px;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.dark-mode-search { .dark-mode-search {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' hei… 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'/%3E%3C/svg%3E") !important; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' hei… 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'/%3E%3C/svg%3E") !important;
color: #f8f9fa !important; color: #f8f9fa !important;
background-color: #212529 !important; background-color: #212529 !important;
border-color: #343a40 !important; border-color: #343a40 !important;
} }
.features-container { .features-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(15rem, 3fr)); grid-template-columns: repeat(auto-fill, minmax(15rem, 3fr));
gap: 25px 30px; gap: 25px 30px;
} }
.feature-card { .feature-card {
border: 2px solid rgba(0, 0, 0, .25); border: 2px solid rgba(0, 0, 0, 0.25);
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 1.25rem; padding: 1.25rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
background: rgba(13, 110, 253, 0.05); background: rgba(13, 110, 253, 0.05);
transition: transform 0.3s, border 0.3s; transition:
transform-origin: center center; transform 0.3s,
outline: 2px solid transparent; border 0.3s;
transform-origin: center center;
outline: 2px solid transparent;
} }
.feature-card a { .feature-card a {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.feature-card .card-text { .feature-card .card-text {
flex: 1; flex: 1;
} }
.feature-card:hover { .feature-card:hover {
outline: 1px solid rgba(0, 0, 0, .5); outline: 1px solid rgba(0, 0, 0, 0.5);
cursor: pointer; cursor: pointer;
transform: scale(1.1); transform: scale(1.1);
} }
.feature-card:hover .card-title { .feature-card:hover .card-title {
text-decoration: underline; text-decoration: underline;
} }
.card-title.text-primary { .card-title.text-primary {
color: #000; /* Replace with your desired shade of blue */ color: #000; /* Replace with your desired shade of blue */
@ -67,27 +65,27 @@
.home-card-icon { .home-card-icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
transform: translateY(-5px); transform: translateY(-5px);
} }
.home-card-icon-colour { .home-card-icon-colour {
filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg); filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
} }
.favorite-icon { .favorite-icon {
display: none; display: none;
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 10px;
} }
/* Only show the favorite icons when the parent card is being hovered over */ /* Only show the favorite icons when the parent card is being hovered over */
.feature-card:hover .favorite-icon { .feature-card:hover .favorite-icon {
display: block; display: block;
} }
.favorite-icon img { .favorite-icon img {
filter: brightness(0); filter: brightness(0);
} }
.jumbotron { .jumbotron {
padding: 3rem 3rem; /* Reduce vertical padding */ padding: 3rem 3rem; /* Reduce vertical padding */
} }

View File

@ -1,40 +1,43 @@
#image-highlighter { #image-highlighter {
position: fixed; position: fixed;
display:flex; display: flex;
inset: 0; inset: 0;
z-index: 10000; z-index: 10000;
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
visibility: hidden; visibility: hidden;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: visbility 0.1s linear, background-color 0.1s linear; transition:
visbility 0.1s linear,
background-color 0.1s linear;
} }
#image-highlighter > * { #image-highlighter > * {
max-width: 80vw; max-width: 80vw;
max-height: 80vh; max-height: 80vh;
animation: image-highlight .1s linear; animation: image-highlight 0.1s linear;
transition: transform .1s linear, opacity .1s linear; transition:
transform 0.1s linear,
opacity 0.1s linear;
} }
#image-highlighter > *.remove { #image-highlighter > *.remove {
transform: scale(0.8) !important; transform: scale(0.8) !important;
opacity: 0 !important; opacity: 0 !important;
} }
#image-highlighter:not(:empty) { #image-highlighter:not(:empty) {
background-color: rgba(0, 0, 0, 0.37); background-color: rgba(0, 0, 0, 0.37);
visibility: visible; visibility: visible;
} }
@keyframes image-highlight { @keyframes image-highlight {
from { from {
transform: scale(0.8); transform: scale(0.8);
opacity: 0; opacity: 0;
} }
to { to {
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
} }
} }

View File

@ -0,0 +1,9 @@
td a {
text-decoration: none;
}
td a:hover,
td a:focus {
text-decoration: underline;
/* Adds underline on hover/focus for clarity */
}

View File

@ -1,14 +1,13 @@
/* Dark Mode Styles */ /* Dark Mode Styles */
body { body {
--body-background-color: 255, 255, 255; --body-background-color: 255, 255, 255;
--base-font-color: 33, 37, 41; --base-font-color: 33, 37, 41;
} }
#global-buttons-container input { #global-buttons-container input {
background-color: #ffffff; background-color: #ffffff;
/*caret-color: #ffffff;*/ /*caret-color: #ffffff;*/
/*color: #ffffff;*/ /*color: #ffffff;*/
} }
/*#global-buttons-container input:disabled::-webkit-input-placeholder { !* WebKit browsers *!*/ /*#global-buttons-container input:disabled::-webkit-input-placeholder { !* WebKit browsers *!*/
/* color: #98A0AB;*/ /* color: #98A0AB;*/

View File

@ -0,0 +1,111 @@
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.container-flex {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%; /* Set width to 100% */
align-items: center; /* Center its children horizontally */
}
.footer-bottom {
margin-top: auto;
}
body.light-mode input:-webkit-autofill,
body.light-mode input:-webkit-autofill:hover,
body.light-mode input:-webkit-autofill:focus,
body.light-mode input:-webkit-autofill:active {
-webkit-text-fill-color: #212529; /* Dark font color */
-webkit-box-shadow: 0 0 0 1000px #f8f9fa inset; /* Light background color */
}
/* Dark Mode */
body.dark-mode input:-webkit-autofill,
body.dark-mode input:-webkit-autofill:hover,
body.dark-mode input:-webkit-autofill:focus,
body.dark-mode input:-webkit-autofill:active {
-webkit-text-fill-color: #f8f9fa; /* Light font color */
-webkit-box-shadow: 0 0 0 1000px #212529 inset; /* Dark background color */
}
/* Light Mode */
body.light-mode .form-floating > input:focus + label {
color: #212529 !important; /* Dark text for light background */
}
/* Dark Mode */
body.dark-mode .form-floating > input:focus + label {
color: #fff !important; /* Light text for dark background */
}
body.light-mode .form-floating > label {
color: #212529 !important; /* Dark text for light background */
}
body.dark-mode .form-floating > label {
color: #fff !important; /* Light text for dark background */
}
/* Removing default styles for ul and li */
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
list-style: none;
}
/* Positioning the container of these elements to the top right */
.your-container-class {
position: absolute;
top: 0;
right: 0;
display: flex;
}
/* Styling for the dropdown */
.dropdown-menu {
min-width: 200px; /* or whatever width you prefer */
}
.navbar-icon {
width: 40px;
height: 40px;
}

View File

@ -1,4 +1,4 @@
.list-group-item { .list-group-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -25,5 +25,4 @@
.move-down span { .move-down span {
font-weight: bold; font-weight: bold;
font-size: 1.2em; font-size: 1.2em;
} }

View File

@ -0,0 +1,119 @@
.multi-tool-container {
max-width: 95vw;
margin: 0 auto;
}
#global-buttons-container {
display: flex;
gap: 10px;
align-items: start;
background-color: rgba(13, 110, 253, 0.1);
border: 1px solid rgba(0, 0, 0, 0.25);
backdrop-filter: blur(2px);
top: 10px;
z-index: 10;
padding: 10px;
border-radius: 8px;
}
#global-buttons-container > * {
padding: 0.6rem 0.75rem;
}
#global-buttons-container svg {
width: 20px;
height: 20px;
}
#export-button {
margin-left: auto;
}
#pages-container-wrapper {
--background-color: rgba(0, 0, 0, 0.025);
--scroll-bar-color: #f1f1f1;
--scroll-bar-thumb: #888;
--scroll-bar-thumb-hover: #555;
background-color: var(--background-color);
width: 100%;
display: flex;
flex-direction: column;
padding: 10px 25px;
border-radius: 10px;
overflow-y: hidden;
overflow-x: auto;
min-height: 275px;
margin: 0 0 30px 0;
}
#pages-container {
margin: auto;
gap: 0px;
display: flex;
align-items: center;
justify-content: center;
}
/* width */
#pages-container-wrapper::-webkit-scrollbar {
width: 10px;
height: 10px;
}
/* Track */
#pages-container-wrapper::-webkit-scrollbar-track {
background: var(--scroll-bar-color);
}
/* Handle */
#pages-container-wrapper::-webkit-scrollbar-thumb {
border-radius: 10px;
background: var(--scroll-bar-thumb);
}
/* Handle on hover */
#pages-container-wrapper::-webkit-scrollbar-thumb:hover {
background: var(--scroll-bar-thumb-hover);
}
.page-container {
height: 250px;
display: flex;
align-items: center;
flex-direction: column-reverse;
aspect-ratio: 1;
text-align: center;
position: relative;
user-select: none;
transition: width 1s linear;
}
.page-container img {
/* max-width: calc(100% - 15px); */
max-height: calc(100% - 15px);
max-width: 237px;
display: block;
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
border-radius: 4px;
transition: rotate 0.3s;
}
#add-pdf-button {
margin: 0 auto;
}
.page-number {
position: absolute;
top: 5px;
right: 5px;
color: white;
background-color: #007bff; /* Primary blue color */
padding: 3px 6px;
border-radius: 4px;
font-size: 12px;
z-index: 2;
}

View File

@ -1,80 +1,116 @@
#navbarSearch { #navbarSearch {
top: 100%; top: 100%;
right: 0; right: 0;
transition: all 0.3s;
max-height: 0;
overflow: hidden;
}
#navbarSearch.show {
max-height: 300px; /* Adjust this to your desired max height */
} }
#searchForm { #searchForm {
width: 200px; /* Adjust this value as needed */ width: 200px; /* Adjust this value as needed */
} }
/* Style the search results to match the navbar */ /* Style the search results to match the navbar */
#searchResults { #searchResults {
max-height: 200px; /* Adjust this value as needed */ max-height: 200px; /* Adjust this value as needed */
overflow-y: auto; overflow-y: auto;
width: 100%; width: 100%;
max-width: 300px; /* Adjust to your preferred width */
transition: height 0.3s ease; /* Smooth height transition */
} }
#searchResults .dropdown-item { #searchResults .dropdown-item {
display: flex; display: flex;
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
height: 50px; /* Fixed height */ height: 50px; /* Fixed height */
overflow: hidden; /* Hide overflow */ overflow: hidden; /* Hide overflow */
} }
#searchResults .icon { #searchResults .icon {
margin-right: 10px; margin-right: 10px;
} }
#searchResults .icon-text { #searchResults .icon-text {
display: inline; display: inline;
overflow: hidden; /* Hide overflow */ overflow: hidden; /* Hide overflow */
text-overflow: ellipsis; /* Add ellipsis for long text */ text-overflow: ellipsis; /* Add ellipsis for long text */
} }
#search-icon i {
font-size: 24px; /* Adjust this to your desired size */
transition: color 0.3s;
}
#search-icon:hover i {
color: #666; /* Adjust this to your hover color */
}
.search-input {
transition:
border 0.3s,
box-shadow 0.3s;
}
.search-input:focus {
border-color: #666; /* Adjust this to your focus color */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adjust this to your desired shadow */
}
/* Set a fixed height and styling for each search result item */
.search-results a {
display: flex;
align-items: center;
gap: 10px; /* space between icon and text */
height: 40px; /* Adjust based on your design */
overflow: hidden; /* Prevent content from overflowing */
white-space: nowrap; /* Prevent text from wrapping to next line */
text-overflow: ellipsis; /* Truncate text if it's too long */
}
.main-icon { .main-icon {
width: 36px; width: 36px;
height: 36px; height: 36px;
vertical-align: middle; vertical-align: middle;
transform: translateY(-2px); transform: translateY(-2px);
} }
.icon { .icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
vertical-align: middle; vertical-align: middle;
transform: translateY(-2px); transform: translateY(-2px);
} }
.icon+.icon { .icon + .icon {
margin-left: -4px; margin-left: -4px;
} }
.icon-text { .icon-text {
margin-left: 4px; margin-left: 4px;
} }
.nav-item-separator { .nav-item-separator {
position: relative; position: relative;
margin: 0 4px; /* Adjust the margin as needed */ margin: 0 4px; /* Adjust the margin as needed */
} }
.nav-item-separator::before { .nav-item-separator::before {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
top: 10%; /* Adjust the top and bottom margins as needed */ top: 10%; /* Adjust the top and bottom margins as needed */
bottom: 10%; bottom: 10%;
width: 1px; width: 1px;
background-color: #ccc; /* Adjust the color as needed */ background-color: #ccc; /* Adjust the color as needed */
} }
.navbar-icon { .navbar-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
transform: translateY(-2px); transform: translateY(-2px);
} }

View File

@ -1,87 +1,85 @@
.pdf-actions_button-container { .pdf-actions_button-container {
z-index: 2; z-index: 2;
display:flex; display: flex;
opacity: 0; opacity: 0;
transition: opacity 0.1s linear; transition: opacity 0.1s linear;
} }
.pdf-actions_container:hover .pdf-actions_button-container { .pdf-actions_container:hover .pdf-actions_button-container {
opacity: 1; opacity: 1;
} }
.pdf-actions_button-container > * { .pdf-actions_button-container > * {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
margin: 3px; margin: 3px;
display: block; display: block;
} }
.pdf-actions_container svg { .pdf-actions_container svg {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
.pdf-actions_container:nth-child(1) .pdf-actions_move-left-button { .pdf-actions_container:nth-child(1) .pdf-actions_move-left-button {
display: none; display: none;
} }
.pdf-actions_container:last-child .pdf-actions_move-right-button { .pdf-actions_container:last-child .pdf-actions_move-right-button {
display: none; display: none;
} }
/* "insert pdf" buttons that appear on the right when hover */ /* "insert pdf" buttons that appear on the right when hover */
.pdf-actions_insert-file-button-container { .pdf-actions_insert-file-button-container {
translate: 0 -50%; translate: 0 -50%;
width: 80px; width: 80px;
height: 100%; height: 100%;
z-index: 1; z-index: 1;
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.pdf-actions_insert-file-button-container.left { .pdf-actions_insert-file-button-container.left {
left: -20px; left: -20px;
} }
.pdf-actions_insert-file-button-container.right { .pdf-actions_insert-file-button-container.right {
right: -20px; right: -20px;
} }
html[lang-direction=ltr] .pdf-actions_insert-file-button-container.right { html[lang-direction="ltr"] .pdf-actions_insert-file-button-container.right {
display:none; display: none;
} }
html[lang-direction=rtl] .pdf-actions_insert-file-button-container.left { html[lang-direction="rtl"] .pdf-actions_insert-file-button-container.left {
display:none; display: none;
} }
.pdf-actions_insert-file-button-container.left .pdf-actions_insert-file-button { .pdf-actions_insert-file-button-container.left .pdf-actions_insert-file-button {
left: 0; left: 0;
translate: 0 -50%; translate: 0 -50%;
} }
.pdf-actions_insert-file-button-container.right .pdf-actions_insert-file-button { .pdf-actions_insert-file-button-container.right .pdf-actions_insert-file-button {
right: 0; right: 0;
translate: 0 -50%; translate: 0 -50%;
} }
html[lang-direction=ltr] .pdf-actions_container:last-child > .pdf-actions_insert-file-button-container.right { html[lang-direction="ltr"] .pdf-actions_container:last-child > .pdf-actions_insert-file-button-container.right {
display: block; display: block;
} }
html[lang-direction="rtl"] .pdf-actions_container:last-child > .pdf-actions_insert-file-button-container.left {
html[lang-direction=rtl] .pdf-actions_container:last-child > .pdf-actions_insert-file-button-container.left { display: block;
display: block;
} }
.pdf-actions_insert-file-button-container:hover { .pdf-actions_insert-file-button-container:hover {
opacity: 1; opacity: 1;
transition: opacity 0.05s; transition: opacity 0.05s;
} }
.pdf-actions_insert-file-button { .pdf-actions_insert-file-button {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 50%; right: 50%;
translate: 50% -50%; translate: 50% -50%;
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 100px; border-radius: 100px;
} }

View File

@ -0,0 +1,21 @@
.btn-margin {
margin-right: 2px;
}
.bordered-box {
border: 1px solid #ddd;
padding: 20px;
margin: 20px;
width: 70%;
}
.center-element {
width: 80%;
text-align: center;
margin: auto;
}
.element-margin {
margin: 10px 0;
/* Adjust this value to increase/decrease the margin as needed */
}

View File

@ -1,37 +1,113 @@
/* Rainbow Mode Styles */ /* Rainbow Mode Styles */
body { body {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%); background: linear-gradient(
color: #fff !important; 90deg,
--body-background-color: 255, 255, 255; rgba(255, 0, 0, 1) 0%,
--base-font-color: 33, 37, 41; rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
);
color: #fff !important;
--body-background-color: 255, 255, 255;
--base-font-color: 33, 37, 41;
} }
.dark-card { .dark-card {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) !important; background: linear-gradient(
color: white !important; 90deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
) !important;
color: white !important;
} }
.jumbotron { .jumbotron {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%); background: linear-gradient(
color: #fff !important; 90deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
);
color: #fff !important;
} }
.list-group { .list-group {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) !important; background: linear-gradient(
color: fff !important; 90deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
) !important;
color: fff !important;
} }
.list-group-item { .list-group-item {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) !important; background: linear-gradient(
color: fff !important; 90deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
) !important;
color: fff !important;
} }
#support-section { #support-section {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) !important; background: linear-gradient(
90deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
) !important;
} }
#pages-container-wrapper { #pages-container-wrapper {
--background-color: rgba(255, 255, 255, 0.046) !important; --background-color: rgba(255, 255, 255, 0.046) !important;
--scroll-bar-color: #4c4c4c !important; --scroll-bar-color: #4c4c4c !important;
--scroll-bar-thumb: #d3d3d3 !important; --scroll-bar-thumb: #d3d3d3 !important;
--scroll-bar-thumb-hover: #ffffff !important; --scroll-bar-thumb-hover: #ffffff !important;
} }

View File

@ -0,0 +1,29 @@
#pdf-preview {
margin: 0 auto;
display: block;
max-width: calc(100% - 30px);
max-height: calc(100% - 30px);
box-shadow: 0 0 4px rgba(100, 100, 100, 0.25);
transition: rotate 0.3s;
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
}
.previewContainer {
aspect-ratio: 1;
width: 100%;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem;
margin: 1rem 0;
padding: 15px;
display: block;
overflow: hidden;
position: relative;
}
.buttonContainer {
display: flex;
justify-content: space-around;
}

View File

@ -0,0 +1,39 @@
select#font-select,
select#font-select option {
height: 60px; /* Adjust as needed */
font-size: 30px; /* Adjust as needed */
}
.drawing-pad-container {
text-align: center;
}
#drawing-pad-canvas {
background: rgba(125, 125, 125, 0.2);
width: 100%;
height: 300px;
}
#box-drag-container {
position: relative;
margin: 20px 0;
}
.draggable-buttons-box {
position: absolute;
top: 0;
padding: 10px;
width: 100%;
display: flex;
gap: 5px;
}
.draggable-buttons-box > button {
z-index: 10;
background-color: rgba(13, 110, 253, 0.1);
}
.draggable-canvas {
border: 1px solid red;
position: absolute;
touch-action: none;
user-select: none;
top: 0px;
left: 0;
}

View File

@ -0,0 +1,10 @@
.pdf-visual-aid {
width: 150px; /* Adjust as needed */
height: 200px; /* Adjust as needed */
border: 1px solid black; /* Represents the PDF page */
position: relative;
}
.line {
position: absolute;
background-color: red; /* Line color */
}

View File

@ -0,0 +1,41 @@
.a4container {
position: relative;
width: 50%;
aspect-ratio: 0.707;
border: 1px solid #ddd;
box-sizing: border-box;
background-color: white;
}
.pageNumber {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
font-size: 1em;
color: #333;
cursor: pointer;
background-color: #ccc;
width: 15%;
height: 15%;
transform: translate(-50%, -50%);
}
.pageNumber:hover {
background-color: #eee;
}
#myForm {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.selectedPosition {
background-color: #0a0;
}
.selectedPosition.selectedHovered {
background-color: #006600;
}

View File

@ -1,26 +1,24 @@
.tab-group { .tab-group {
} }
.tab-container { .tab-container {
display: none; display: none;
} }
.tab-container.active { .tab-container.active {
display: block; display: block;
border: 1px solid rgba(var(--base-font-color), 0.25); border: 1px solid rgba(var(--base-font-color), 0.25);
padding: 15px; padding: 15px;
} }
.tab-buttons > button { .tab-buttons > button {
margin-bottom: -1px; margin-bottom: -1px;
background: 0 0; background: 0 0;
border: 1px solid transparent; border: 1px solid transparent;
color: rgb(var(--base-font-color)); color: rgb(var(--base-font-color));
border-top-left-radius: 0.25rem; border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
} }
.tab-buttons > button.active { .tab-buttons > button.active {
background-color: rgb(var(--body-background-color)); background-color: rgb(var(--body-background-color));
border-color: rgba(var(--base-font-color), 0.25) rgba(var(--base-font-color), 0.25) rgb(var(--body-background-color)); border-color: rgba(var(--base-font-color), 0.25) rgba(var(--base-font-color), 0.25) rgb(var(--body-background-color));
} }

View File

@ -1,5 +1,5 @@
var toggleCount = 0 var toggleCount = 0;
var lastToggleTime = Date.now() var lastToggleTime = Date.now();
var elements = { var elements = {
lightModeStyles: null, lightModeStyles: null,
@ -11,18 +11,18 @@ var elements = {
navbar: null, navbar: null,
navIcons: null, navIcons: null,
navDropdownMenus: null, navDropdownMenus: null,
} };
function getElements() { function getElements() {
elements.lightModeStyles = document.getElementById("light-mode-styles") elements.lightModeStyles = document.getElementById("light-mode-styles");
elements.darkModeStyles = document.getElementById("dark-mode-styles") elements.darkModeStyles = document.getElementById("dark-mode-styles");
elements.rainbowModeStyles = document.getElementById("rainbow-mode-styles") elements.rainbowModeStyles = document.getElementById("rainbow-mode-styles");
elements.darkModeIcon = document.getElementById("dark-mode-icon") elements.darkModeIcon = document.getElementById("dark-mode-icon");
elements.searchBar = document.getElementById("searchBar") elements.searchBar = document.getElementById("searchBar");
elements.formControls = document.querySelectorAll(".form-control") elements.formControls = document.querySelectorAll(".form-control");
elements.navbar = document.querySelectorAll("nav.navbar") elements.navbar = document.querySelectorAll("nav.navbar");
elements.navIcons = document.querySelectorAll("nav .icon, .navbar-icon") elements.navIcons = document.querySelectorAll("nav .icon, .navbar-icon");
elements.navDropdownMenus = document.querySelectorAll("nav .dropdown-menu") elements.navDropdownMenus = document.querySelectorAll(".dropdown-menu");
} }
function setMode(mode) { function setMode(mode) {
var event = new CustomEvent("modeChanged", { detail: mode }); var event = new CustomEvent("modeChanged", { detail: mode });
@ -48,22 +48,22 @@ function setMode(mode) {
elements.searchBar.classList.add("dark-mode-search"); elements.searchBar.classList.add("dark-mode-search");
} }
if (elements && elements.formControls) { if (elements && elements.formControls) {
elements.formControls.forEach(input => input.classList.add("bg-dark", "text-white")); elements.formControls.forEach((input) => input.classList.add("bg-dark", "text-white"));
} }
if (elements && elements.navbar) { if (elements && elements.navbar) {
elements.navbar.forEach(navElement => { elements.navbar.forEach((navElement) => {
navElement.classList.remove("navbar-light", "bg-light"); navElement.classList.remove("navbar-light", "bg-light");
navElement.classList.add("navbar-dark", "bg-dark"); navElement.classList.add("navbar-dark", "bg-dark");
}); });
} }
if (elements && elements.navDropdownMenus) { if (elements && elements.navDropdownMenus) {
elements.navDropdownMenus.forEach(menu => menu.classList.add("dropdown-menu-dark")); elements.navDropdownMenus.forEach((menu) => menu.classList.add("dropdown-menu-dark"));
} }
if (elements && elements.navIcons) { if (elements && elements.navIcons) {
elements.navIcons.forEach(icon => (icon.style.filter = "invert(1)")); elements.navIcons.forEach((icon) => (icon.style.filter = "invert(1)"));
} }
var tables = document.querySelectorAll(".table"); var tables = document.querySelectorAll(".table");
tables.forEach(table => { tables.forEach((table) => {
table.classList.add("table-dark"); table.classList.add("table-dark");
}); });
if (jumbotron) { if (jumbotron) {
@ -78,22 +78,22 @@ function setMode(mode) {
elements.searchBar.classList.remove("dark-mode-search"); elements.searchBar.classList.remove("dark-mode-search");
} }
if (elements && elements.formControls) { if (elements && elements.formControls) {
elements.formControls.forEach(input => input.classList.remove("bg-dark", "text-white")); elements.formControls.forEach((input) => input.classList.remove("bg-dark", "text-white"));
} }
if (elements && elements.navbar) { if (elements && elements.navbar) {
elements.navbar.forEach(navElement => { elements.navbar.forEach((navElement) => {
navElement.classList.remove("navbar-dark", "bg-dark"); navElement.classList.remove("navbar-dark", "bg-dark");
navElement.classList.add("navbar-light", "bg-light"); navElement.classList.add("navbar-light", "bg-light");
}); });
} }
if (elements && elements.navDropdownMenus) { if (elements && elements.navDropdownMenus) {
elements.navDropdownMenus.forEach(menu => menu.classList.remove("dropdown-menu-dark")); elements.navDropdownMenus.forEach((menu) => menu.classList.remove("dropdown-menu-dark"));
} }
if (elements && elements.navIcons) { if (elements && elements.navIcons) {
elements.navIcons.forEach(icon => (icon.style.filter = "none")); elements.navIcons.forEach((icon) => (icon.style.filter = "none"));
} }
var tables = document.querySelectorAll(".table-dark"); var tables = document.querySelectorAll(".table-dark");
tables.forEach(table => { tables.forEach((table) => {
table.classList.remove("table-dark"); table.classList.remove("table-dark");
}); });
if (jumbotron) { if (jumbotron) {
@ -108,43 +108,43 @@ function setMode(mode) {
} }
function toggleDarkMode() { function toggleDarkMode() {
var currentTime = Date.now() var currentTime = Date.now();
if (currentTime - lastToggleTime < 1000) { if (currentTime - lastToggleTime < 1000) {
toggleCount++ toggleCount++;
} else { } else {
toggleCount = 1 toggleCount = 1;
} }
lastToggleTime = currentTime lastToggleTime = currentTime;
if (toggleCount >= 18) { if (toggleCount >= 18) {
localStorage.setItem("dark-mode", "rainbow") localStorage.setItem("dark-mode", "rainbow");
setMode("rainbow") setMode("rainbow");
} else if (localStorage.getItem("dark-mode") == "on") { } else if (localStorage.getItem("dark-mode") == "on") {
localStorage.setItem("dark-mode", "off") localStorage.setItem("dark-mode", "off");
setMode("off") setMode("off");
} else { } else {
localStorage.setItem("dark-mode", "on") localStorage.setItem("dark-mode", "on");
setMode("on") setMode("on");
} }
} }
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
getElements() getElements();
var currentMode = localStorage.getItem("dark-mode") var currentMode = localStorage.getItem("dark-mode");
if (currentMode === "on" || currentMode === "off" || currentMode === "rainbow") { if (currentMode === "on" || currentMode === "off" || currentMode === "rainbow") {
setMode(currentMode) setMode(currentMode);
} else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
setMode("on") setMode("on");
} else { } else {
setMode("off") setMode("off");
} }
var darkModeToggle = document.getElementById("dark-mode-toggle"); var darkModeToggle = document.getElementById("dark-mode-toggle");
if (darkModeToggle !== null) { if (darkModeToggle !== null) {
darkModeToggle.addEventListener("click", function (event) { darkModeToggle.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
toggleDarkMode(); toggleDarkMode();
}); });
} }
}) });

View File

@ -1,242 +1,235 @@
function showErrorBanner(message, stackTrace) { function showErrorBanner(message, stackTrace) {
const errorContainer = document.getElementById("errorContainer"); const errorContainer = document.getElementById("errorContainer");
errorContainer.style.display = "block"; // Display the banner errorContainer.style.display = "block"; // Display the banner
document.querySelector("#errorContainer .alert-heading").textContent = "Error"; document.querySelector("#errorContainer .alert-heading").textContent = "Error";
document.querySelector("#errorContainer p").textContent = message; document.querySelector("#errorContainer p").textContent = message;
document.querySelector("#traceContent").textContent = stackTrace; document.querySelector("#traceContent").textContent = stackTrace;
} }
let firstErrorOccurred = false; let firstErrorOccurred = false;
$(document).ready(function() { $(document).ready(function () {
$('form').submit(async function(event) { $("form").submit(async function (event) {
event.preventDefault(); event.preventDefault();
firstErrorOccurred = false; firstErrorOccurred = false;
const url = this.action; const url = this.action;
const files = $('#fileInput-input')[0].files; const files = $("#fileInput-input")[0].files;
const formData = new FormData(this); const formData = new FormData(this);
// Remove empty file entries // Remove empty file entries
for (let [key, value] of formData.entries()) { for (let [key, value] of formData.entries()) {
if (value instanceof File && !value.name) { if (value instanceof File && !value.name) {
formData.delete(key); formData.delete(key);
} }
}
const override = $("#override").val() || "";
const originalButtonText = $("#submitBtn").text();
$("#submitBtn").text("Processing...");
console.log(override);
try {
if (remoteCall === true) {
if (override === "multi" || (!multiple && files.length > 1 && override !== "single")) {
await submitMultiPdfForm(url, files);
} else {
await handleSingleDownload(url, formData);
} }
const override = $('#override').val() || ''; }
const originalButtonText = $('#submitBtn').text(); $("#submitBtn").text(originalButtonText);
$('#submitBtn').text('Processing...'); } catch (error) {
console.log(override); handleDownloadError(error);
try { $("#submitBtn").text(originalButtonText);
if(remoteCall === true) { console.error(error);
if (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) { }
await submitMultiPdfForm(url, files); });
} else {
await handleSingleDownload(url, formData);
}
}
$('#submitBtn').text(originalButtonText);
} catch (error) {
handleDownloadError(error);
$('#submitBtn').text(originalButtonText);
console.error(error);
}
});
}); });
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
try {
const response = await fetch(url, { method: "POST", body: formData });
const contentType = response.headers.get("content-type");
if (!response.ok) {
if (contentType && contentType.includes("application/json")) {
return handleJsonResponse(response);
console.error("Throwing error banner, response was not okay");
}
throw new Error(`HTTP error! status: ${response.status}`);
}
async function handleSingleDownload(url, formData, isMulti = false , isZip = false) { const contentDisposition = response.headers.get("Content-Disposition");
try { let filename = getFilenameFromContentDisposition(contentDisposition);
const response = await fetch(url, { method: 'POST', body: formData });
const contentType = response.headers.get('content-type');
if (!response.ok) { const blob = await response.blob();
if (contentType && contentType.includes('application/json')) { if (contentType.includes("application/pdf") || contentType.includes("image/")) {
return handleJsonResponse(response); return handleResponse(blob, filename, !isMulti, isZip);
console.error('Throwing error banner, response was not okay'); } else {
} return handleResponse(blob, filename, false, isZip);
throw new Error(`HTTP error! status: ${response.status}`); }
} } catch (error) {
console.error("Error in handleSingleDownload:", error);
const contentDisposition = response.headers.get('Content-Disposition'); throw error; // Re-throw the error if you want it to be handled higher up.
let filename = getFilenameFromContentDisposition(contentDisposition); }
const blob = await response.blob();
if (contentType.includes('application/pdf') || contentType.includes('image/')) {
return handleResponse(blob, filename, !isMulti, isZip);
} else {
return handleResponse(blob, filename, false, isZip);
}
} catch (error) {
console.error('Error in handleSingleDownload:', error);
throw error; // Re-throw the error if you want it to be handled higher up.
}
} }
function getFilenameFromContentDisposition(contentDisposition) { function getFilenameFromContentDisposition(contentDisposition) {
let filename; let filename;
if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) { if (contentDisposition && contentDisposition.indexOf("attachment") !== -1) {
filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim(); filename = decodeURIComponent(contentDisposition.split("filename=")[1].replace(/"/g, "")).trim();
} else { } else {
// If the Content-Disposition header is not present or does not contain the filename, use a default filename // If the Content-Disposition header is not present or does not contain the filename, use a default filename
filename = 'download'; filename = "download";
} }
return filename; return filename;
} }
async function handleJsonResponse(response) { async function handleJsonResponse(response) {
const json = await response.json(); const json = await response.json();
const errorMessage = JSON.stringify(json, null, 2); const errorMessage = JSON.stringify(json, null, 2);
if (errorMessage.toLowerCase().includes('the password is incorrect') || errorMessage.toLowerCase().includes('Password is not provided') || errorMessage.toLowerCase().includes('PDF contains an encryption dictionary')) { if (
if (!firstErrorOccurred) { errorMessage.toLowerCase().includes("the password is incorrect") ||
firstErrorOccurred = true; errorMessage.toLowerCase().includes("Password is not provided") ||
alert(pdfPasswordPrompt); errorMessage.toLowerCase().includes("PDF contains an encryption dictionary")
} ) {
} else { if (!firstErrorOccurred) {
showErrorBanner(json.error + ':' + json.message, json.trace); firstErrorOccurred = true;
} alert(pdfPasswordPrompt);
}
} else {
showErrorBanner(json.error + ":" + json.message, json.trace);
}
} }
async function handleResponse(blob, filename, considerViewOptions = false, isZip = false) { async function handleResponse(blob, filename, considerViewOptions = false, isZip = false) {
if (!blob) return; if (!blob) return;
const downloadOption = localStorage.getItem('downloadOption'); const downloadOption = localStorage.getItem("downloadOption");
if (considerViewOptions) { if (considerViewOptions) {
if (downloadOption === 'sameWindow') { if (downloadOption === "sameWindow") {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.location.href = url; window.location.href = url;
return; return;
} else if (downloadOption === 'newWindow') { } else if (downloadOption === "newWindow") {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, '_blank'); window.open(url, "_blank");
return; return;
} }
} }
if(!isZip){ if (!isZip) {
downloadFile(blob, filename); downloadFile(blob, filename);
} }
return { filename, blob }; return { filename, blob };
} }
function handleDownloadError(error) { function handleDownloadError(error) {
const errorMessage = error.message; const errorMessage = error.message;
showErrorBanner(errorMessage); showErrorBanner(errorMessage);
} }
let urls = []; // An array to hold all the URLs let urls = []; // An array to hold all the URLs
function downloadFile(blob, filename) { function downloadFile(blob, filename) {
if (!(blob instanceof Blob)) { if (!(blob instanceof Blob)) {
console.error('Invalid blob passed to downloadFile function'); console.error("Invalid blob passed to downloadFile function");
return; return;
} }
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = filename; a.download = filename;
a.click(); a.click();
urls.push(url); // Store the URL so it doesn't get garbage collected too soon urls.push(url); // Store the URL so it doesn't get garbage collected too soon
return { filename, blob }; return { filename, blob };
} }
async function submitMultiPdfForm(url, files) { async function submitMultiPdfForm(url, files) {
const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4; const zipThreshold = parseInt(localStorage.getItem("zipThreshold"), 10) || 4;
const zipFiles = files.length > zipThreshold; const zipFiles = files.length > zipThreshold;
let jszip = null; let jszip = null;
// Show the progress bar // Show the progress bar
$('#progressBarContainer').show(); $("#progressBarContainer").show();
// Initialize the progress bar // Initialize the progress bar
let progressBar = $('#progressBar'); let progressBar = $("#progressBar");
progressBar.css('width', '0%'); progressBar.css("width", "0%");
progressBar.attr('aria-valuenow', 0); progressBar.attr("aria-valuenow", 0);
progressBar.attr('aria-valuemax', files.length); progressBar.attr("aria-valuemax", files.length);
if (zipFiles) { if (zipFiles) {
jszip = new JSZip(); jszip = new JSZip();
} }
// Get the form with the method attribute set to POST
let postForm = document.querySelector('form[method="POST"]');
// Get the form with the method attribute set to POST // Get existing form data
let postForm = document.querySelector('form[method="POST"]'); let formData;
if (postForm) {
// Get existing form data formData = new FormData($(postForm)[0]); // Convert the form to a jQuery object and get the raw DOM element
let formData; } else {
if (postForm) { console.log("No form with POST method found.");
formData = new FormData($(postForm)[0]); // Convert the form to a jQuery object and get the raw DOM element }
} else { //Remove file to reuse parameters for other runs
console.log("No form with POST method found."); formData.delete("fileInput");
} // Remove empty file entries
//Remove file to reuse parameters for other runs for (let [key, value] of formData.entries()) {
formData.delete('fileInput'); if (value instanceof File && !value.name) {
// Remove empty file entries formData.delete(key);
for (let [key, value] of formData.entries()) {
if (value instanceof File && !value.name) {
formData.delete(key);
}
} }
const CONCURRENCY_LIMIT = 8; }
const chunks = []; const CONCURRENCY_LIMIT = 8;
for (let i = 0; i < Array.from(files).length; i += CONCURRENCY_LIMIT) { const chunks = [];
chunks.push(Array.from(files).slice(i, i + CONCURRENCY_LIMIT)); for (let i = 0; i < Array.from(files).length; i += CONCURRENCY_LIMIT) {
} chunks.push(Array.from(files).slice(i, i + CONCURRENCY_LIMIT));
}
for (const chunk of chunks) { for (const chunk of chunks) {
const promises = chunk.map(async file => { const promises = chunk.map(async (file) => {
let fileFormData = new FormData(); let fileFormData = new FormData();
fileFormData.append('fileInput', file); fileFormData.append("fileInput", file);
console.log(fileFormData); console.log(fileFormData);
// Add other form data // Add other form data
for (let pair of formData.entries()) { for (let pair of formData.entries()) {
fileFormData.append(pair[0], pair[1]); fileFormData.append(pair[0], pair[1]);
console.log(pair[0]+ ', ' + pair[1]); console.log(pair[0] + ", " + pair[1]);
} }
try { try {
const downloadDetails = await handleSingleDownload(url, fileFormData, true, zipFiles); const downloadDetails = await handleSingleDownload(url, fileFormData, true, zipFiles);
console.log(downloadDetails); console.log(downloadDetails);
if (zipFiles) { if (zipFiles) {
jszip.file(downloadDetails.filename, downloadDetails.blob); jszip.file(downloadDetails.filename, downloadDetails.blob);
} else { } else {
//downloadFile(downloadDetails.blob, downloadDetails.filename); //downloadFile(downloadDetails.blob, downloadDetails.filename);
} }
updateProgressBar(progressBar, Array.from(files).length); updateProgressBar(progressBar, Array.from(files).length);
} catch (error) { } catch (error) {
handleDownloadError(error); handleDownloadError(error);
console.error(error); console.error(error);
} }
}); });
await Promise.all(promises); await Promise.all(promises);
}
} if (zipFiles) {
try {
if (zipFiles) { const content = await jszip.generateAsync({ type: "blob" });
try { downloadFile(content, "files.zip");
const content = await jszip.generateAsync({ type: "blob" }); } catch (error) {
downloadFile(content, "files.zip"); console.error("Error generating ZIP file: " + error);
} catch (error) { }
console.error('Error generating ZIP file: ' + error); }
} progressBar.css("width", "100%");
} progressBar.attr("aria-valuenow", Array.from(files).length);
progressBar.css('width', '100%');
progressBar.attr('aria-valuenow', Array.from(files).length);
} }
function updateProgressBar(progressBar, files) { function updateProgressBar(progressBar, files) {
let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length); let progress = (progressBar.attr("aria-valuenow") / files.length) * 100 + 100 / files.length;
progressBar.css('width', progress + '%'); progressBar.css("width", progress + "%");
progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1); progressBar.attr("aria-valuenow", parseInt(progressBar.attr("aria-valuenow")) + 1);
} }
window.addEventListener('unload', () => { window.addEventListener("unload", () => {
for (const url of urls) { for (const url of urls) {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
}); });

View File

@ -1,282 +1,287 @@
const DraggableUtils = { const DraggableUtils = {
boxDragContainer: document.getElementById("box-drag-container"),
pdfCanvas: document.getElementById("pdf-canvas"),
nextId: 0,
pdfDoc: null,
pageIndex: 0,
documentsMap: new Map(),
boxDragContainer: document.getElementById('box-drag-container'), init() {
pdfCanvas: document.getElementById('pdf-canvas'), interact(".draggable-canvas")
nextId: 0, .draggable({
pdfDoc: null, listeners: {
pageIndex: 0, move: (event) => {
documentsMap: new Map(), const target = event.target;
const x = (parseFloat(target.getAttribute("data-bs-x")) || 0) + event.dx;
const y = (parseFloat(target.getAttribute("data-bs-y")) || 0) + event.dy;
init() { target.style.transform = `translate(${x}px, ${y}px)`;
interact('.draggable-canvas') target.setAttribute("data-bs-x", x);
.draggable({ target.setAttribute("data-bs-y", y);
listeners: {
move: (event) => {
const target = event.target;
const x = (parseFloat(target.getAttribute('data-bs-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-bs-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`; this.onInteraction(target);
target.setAttribute('data-bs-x', x); },
target.setAttribute('data-bs-y', y); },
})
this.onInteraction(target); .resizable({
}, edges: { left: true, right: true, bottom: true, top: true },
}, listeners: {
}) move: (event) => {
.resizable({ var target = event.target;
edges: { left: true, right: true, bottom: true, top: true }, var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
listeners: { var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
move: (event) => {
var target = event.target
var x = (parseFloat(target.getAttribute('data-bs-x')) || 0)
var y = (parseFloat(target.getAttribute('data-bs-y')) || 0)
// check if control key is pressed // check if control key is pressed
if (event.ctrlKey) { if (event.ctrlKey) {
const aspectRatio = target.offsetWidth / target.offsetHeight; const aspectRatio = target.offsetWidth / target.offsetHeight;
// preserve aspect ratio // preserve aspect ratio
let width = event.rect.width; let width = event.rect.width;
let height = event.rect.height; let height = event.rect.height;
if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) { if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
height = width / aspectRatio; height = width / aspectRatio;
} else { } else {
width = height * aspectRatio; width = height * aspectRatio;
} }
event.rect.width = width; event.rect.width = width;
event.rect.height = height; event.rect.height = height;
} }
target.style.width = event.rect.width + 'px' target.style.width = event.rect.width + "px";
target.style.height = event.rect.height + 'px' target.style.height = event.rect.height + "px";
// translate when resizing from top or left edges // translate when resizing from top or left edges
x += event.deltaRect.left x += event.deltaRect.left;
y += event.deltaRect.top y += event.deltaRect.top;
target.style.transform = 'translate(' + x + 'px,' + y + 'px)' target.style.transform = "translate(" + x + "px," + y + "px)";
target.setAttribute('data-bs-x', x) target.setAttribute("data-bs-x", x);
target.setAttribute('data-bs-y', y) target.setAttribute("data-bs-y", y);
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height) target.textContent = Math.round(event.rect.width) + "\u00D7" + Math.round(event.rect.height);
this.onInteraction(target); this.onInteraction(target);
},
}, },
},
modifiers: [ modifiers: [
interact.modifiers.restrictSize({ interact.modifiers.restrictSize({
min: { width: 5, height: 5 }, min: { width: 5, height: 5 },
}), }),
], ],
inertia: true, inertia: true,
}); });
}, },
onInteraction(target) { onInteraction(target) {
this.boxDragContainer.appendChild(target); this.boxDragContainer.appendChild(target);
}, },
createDraggableCanvas() { createDraggableCanvas() {
const createdCanvas = document.createElement('canvas'); const createdCanvas = document.createElement("canvas");
createdCanvas.id = `draggable-canvas-${this.nextId++}`; createdCanvas.id = `draggable-canvas-${this.nextId++}`;
createdCanvas.classList.add("draggable-canvas"); createdCanvas.classList.add("draggable-canvas");
const x = 0; const x = 0;
const y = 20; const y = 20;
createdCanvas.style.transform = `translate(${x}px, ${y}px)`; createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
createdCanvas.setAttribute('data-bs-x', x); createdCanvas.setAttribute("data-bs-x", x);
createdCanvas.setAttribute('data-bs-y', y); createdCanvas.setAttribute("data-bs-y", y);
createdCanvas.onclick = e => this.onInteraction(e.target); createdCanvas.onclick = (e) => this.onInteraction(e.target);
this.boxDragContainer.appendChild(createdCanvas); this.boxDragContainer.appendChild(createdCanvas);
return createdCanvas; return createdCanvas;
}, },
createDraggableCanvasFromUrl(dataUrl) { createDraggableCanvasFromUrl(dataUrl) {
return new Promise((resolve) => { return new Promise((resolve) => {
var myImage = new Image(); var myImage = new Image();
myImage.src = dataUrl; myImage.src = dataUrl;
myImage.onload = () => { myImage.onload = () => {
var createdCanvas = this.createDraggableCanvas(); var createdCanvas = this.createDraggableCanvas();
createdCanvas.width = myImage.width; createdCanvas.width = myImage.width;
createdCanvas.height = myImage.height; createdCanvas.height = myImage.height;
const imgAspect = myImage.width / myImage.height; const imgAspect = myImage.width / myImage.height;
const pdfAspect = this.boxDragContainer.offsetWidth / this.boxDragContainer.offsetHeight; const pdfAspect = this.boxDragContainer.offsetWidth / this.boxDragContainer.offsetHeight;
var scaleMultiplier; var scaleMultiplier;
if (imgAspect > pdfAspect) { if (imgAspect > pdfAspect) {
scaleMultiplier = this.boxDragContainer.offsetWidth / myImage.width; scaleMultiplier = this.boxDragContainer.offsetWidth / myImage.width;
} else {
scaleMultiplier = this.boxDragContainer.offsetHeight / myImage.height;
}
var newWidth = createdCanvas.width;
var newHeight = createdCanvas.height;
if (scaleMultiplier < 1) {
newWidth = newWidth * scaleMultiplier;
newHeight = newHeight * scaleMultiplier;
}
createdCanvas.style.width = newWidth+"px";
createdCanvas.style.height = newHeight+"px";
var myContext = createdCanvas.getContext("2d");
myContext.drawImage(myImage,0,0);
resolve(createdCanvas);
}
})
},
deleteAllDraggableCanvases() {
this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach(el => el.remove());
},
deleteDraggableCanvas(element) {
if (element) {
element.remove();
}
},
getLastInteracted() {
return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type");
},
storePageContents() {
var pagesMap = this.documentsMap.get(this.pdfDoc);
if (!pagesMap) {
pagesMap = {};
}
const elements = [...this.boxDragContainer.querySelectorAll(".draggable-canvas")];
const draggablesData = elements.map(el => {return{element:el, offsetWidth:el.offsetWidth, offsetHeight:el.offsetHeight}});
elements.forEach(el => this.boxDragContainer.removeChild(el));
pagesMap[this.pageIndex] = draggablesData;
pagesMap[this.pageIndex+"-offsetWidth"] = this.pdfCanvas.offsetWidth;
pagesMap[this.pageIndex+"-offsetHeight"] = this.pdfCanvas.offsetHeight;
this.documentsMap.set(this.pdfDoc, pagesMap);
},
loadPageContents() {
var pagesMap = this.documentsMap.get(this.pdfDoc);
this.deleteAllDraggableCanvases();
if (!pagesMap) {
return;
}
const draggablesData = pagesMap[this.pageIndex];
if (draggablesData) {
draggablesData.forEach(draggableData => this.boxDragContainer.appendChild(draggableData.element));
}
this.documentsMap.set(this.pdfDoc, pagesMap);
},
async renderPage(pdfDocument, pageIdx) {
this.pdfDoc = pdfDocument ? pdfDocument : this.pdfDoc;
this.pageIndex = pageIdx;
// persist
const page = await this.pdfDoc.getPage(this.pageIndex+1);
// set the canvas size to the size of the page
if (page.rotate == 90 || page.rotate == 270) {
this.pdfCanvas.width = page.view[3];
this.pdfCanvas.height = page.view[2];
} else { } else {
this.pdfCanvas.width = page.view[2]; scaleMultiplier = this.boxDragContainer.offsetHeight / myImage.height;
this.pdfCanvas.height = page.view[3];
} }
// render the page onto the canvas var newWidth = createdCanvas.width;
var renderContext = { var newHeight = createdCanvas.height;
canvasContext: this.pdfCanvas.getContext("2d"), if (scaleMultiplier < 1) {
viewport: page.getViewport({ scale: 1 }) newWidth = newWidth * scaleMultiplier;
newHeight = newHeight * scaleMultiplier;
}
createdCanvas.style.width = newWidth + "px";
createdCanvas.style.height = newHeight + "px";
var myContext = createdCanvas.getContext("2d");
myContext.drawImage(myImage, 0, 0);
resolve(createdCanvas);
};
});
},
deleteAllDraggableCanvases() {
this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach((el) => el.remove());
},
deleteDraggableCanvas(element) {
if (element) {
element.remove();
}
},
getLastInteracted() {
return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type");
},
storePageContents() {
var pagesMap = this.documentsMap.get(this.pdfDoc);
if (!pagesMap) {
pagesMap = {};
}
const elements = [...this.boxDragContainer.querySelectorAll(".draggable-canvas")];
const draggablesData = elements.map((el) => {
return {
element: el,
offsetWidth: el.offsetWidth,
offsetHeight: el.offsetHeight,
};
});
elements.forEach((el) => this.boxDragContainer.removeChild(el));
pagesMap[this.pageIndex] = draggablesData;
pagesMap[this.pageIndex + "-offsetWidth"] = this.pdfCanvas.offsetWidth;
pagesMap[this.pageIndex + "-offsetHeight"] = this.pdfCanvas.offsetHeight;
this.documentsMap.set(this.pdfDoc, pagesMap);
},
loadPageContents() {
var pagesMap = this.documentsMap.get(this.pdfDoc);
this.deleteAllDraggableCanvases();
if (!pagesMap) {
return;
}
const draggablesData = pagesMap[this.pageIndex];
if (draggablesData) {
draggablesData.forEach((draggableData) => this.boxDragContainer.appendChild(draggableData.element));
}
this.documentsMap.set(this.pdfDoc, pagesMap);
},
async renderPage(pdfDocument, pageIdx) {
this.pdfDoc = pdfDocument ? pdfDocument : this.pdfDoc;
this.pageIndex = pageIdx;
// persist
const page = await this.pdfDoc.getPage(this.pageIndex + 1);
// set the canvas size to the size of the page
if (page.rotate == 90 || page.rotate == 270) {
this.pdfCanvas.width = page.view[3];
this.pdfCanvas.height = page.view[2];
} else {
this.pdfCanvas.width = page.view[2];
this.pdfCanvas.height = page.view[3];
}
// render the page onto the canvas
var renderContext = {
canvasContext: this.pdfCanvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 }),
};
await page.render(renderContext).promise;
//return pdfCanvas.toDataURL();
},
async incrementPage() {
if (this.pageIndex < this.pdfDoc.numPages - 1) {
this.storePageContents();
await this.renderPage(this.pdfDoc, this.pageIndex + 1);
this.loadPageContents();
}
},
async decrementPage() {
if (this.pageIndex > 0) {
this.storePageContents();
await this.renderPage(this.pdfDoc, this.pageIndex - 1);
this.loadPageContents();
}
},
parseTransform(element) {},
async getOverlayedPdfDocument() {
const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
ignoreEncryption: true,
});
this.storePageContents();
const pagesMap = this.documentsMap.get(this.pdfDoc);
for (let pageIdx in pagesMap) {
if (pageIdx.includes("offset")) {
continue;
}
console.log(typeof pageIdx);
const page = pdfDocModified.getPage(parseInt(pageIdx));
const draggablesData = pagesMap[pageIdx];
const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
for (const draggableData of draggablesData) {
// embed the draggable canvas
const draggableElement = draggableData.element;
const response = await fetch(draggableElement.toDataURL());
const draggableImgBytes = await response.arrayBuffer();
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
// calculate the position in the pdf document
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, "");
const transformComponents = tansform.split(",");
const draggablePositionPixels = {
x: parseFloat(transformComponents[0]),
y: parseFloat(transformComponents[1]),
width: draggableData.offsetWidth,
height: draggableData.offsetHeight,
};
const draggablePositionRelative = {
x: draggablePositionPixels.x / offsetWidth,
y: draggablePositionPixels.y / offsetHeight,
width: draggablePositionPixels.width / offsetWidth,
height: draggablePositionPixels.height / offsetHeight,
};
const draggablePositionPdf = {
x: draggablePositionRelative.x * page.getWidth(),
y: draggablePositionRelative.y * page.getHeight(),
width: draggablePositionRelative.width * page.getWidth(),
height: draggablePositionRelative.height * page.getHeight(),
}; };
await page.render(renderContext).promise;
//return pdfCanvas.toDataURL(); // draw the image
}, page.drawImage(pdfImageObject, {
async incrementPage() { x: draggablePositionPdf.x,
if (this.pageIndex < this.pdfDoc.numPages-1) { y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
this.storePageContents(); width: draggablePositionPdf.width,
await this.renderPage(this.pdfDoc, this.pageIndex+1); height: draggablePositionPdf.height,
this.loadPageContents(); });
} }
}, }
async decrementPage() {
if (this.pageIndex > 0) {
this.storePageContents();
await this.renderPage(this.pdfDoc, this.pageIndex-1);
this.loadPageContents();
}
},
parseTransform(element) { this.loadPageContents();
return pdfDocModified;
}, },
async getOverlayedPdfDocument() { };
const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true });
this.storePageContents();
const pagesMap = this.documentsMap.get(this.pdfDoc);
for (let pageIdx in pagesMap) {
if (pageIdx.includes("offset")) {
continue;
}
console.log(typeof pageIdx);
const page = pdfDocModified.getPage(parseInt(pageIdx));
const draggablesData = pagesMap[pageIdx];
const offsetWidth = pagesMap[pageIdx+"-offsetWidth"];
const offsetHeight = pagesMap[pageIdx+"-offsetHeight"];
for (const draggableData of draggablesData) {
// embed the draggable canvas
const draggableElement = draggableData.element;
const response = await fetch(draggableElement.toDataURL());
const draggableImgBytes = await response.arrayBuffer();
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
// calculate the position in the pdf document
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
const transformComponents = tansform.split(",");
const draggablePositionPixels = {
x: parseFloat(transformComponents[0]),
y: parseFloat(transformComponents[1]),
width: draggableData.offsetWidth,
height: draggableData.offsetHeight,
};
const draggablePositionRelative = {
x: draggablePositionPixels.x / offsetWidth,
y: draggablePositionPixels.y / offsetHeight,
width: draggablePositionPixels.width / offsetWidth,
height: draggablePositionPixels.height / offsetHeight,
}
const draggablePositionPdf = {
x: draggablePositionRelative.x * page.getWidth(),
y: draggablePositionRelative.y * page.getHeight(),
width: draggablePositionRelative.width * page.getWidth(),
height: draggablePositionRelative.height * page.getHeight(),
}
// draw the image
page.drawImage(pdfImageObject, {
x: draggablePositionPdf.x,
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
width: draggablePositionPdf.width,
height: draggablePositionPdf.height,
});
}
}
this.loadPageContents();
return pdfDocModified;
},
}
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
DraggableUtils.init(); DraggableUtils.init();
}); });

View File

@ -1,50 +1,50 @@
var traceVisible = false; var traceVisible = false;
function toggletrace() { function toggletrace() {
var traceDiv = document.getElementById("trace"); var traceDiv = document.getElementById("trace");
if (!traceVisible) { if (!traceVisible) {
traceDiv.style.maxHeight = "500px"; traceDiv.style.maxHeight = "500px";
traceVisible = true; traceVisible = true;
} else { } else {
traceDiv.style.maxHeight = "0px"; traceDiv.style.maxHeight = "0px";
traceVisible = false; traceVisible = false;
} }
adjustContainerHeight(); adjustContainerHeight();
} }
function copytrace() { function copytrace() {
var flip = false var flip = false;
if (!traceVisible) { if (!traceVisible) {
toggletrace() toggletrace();
flip = true flip = true;
} }
var traceContent = document.getElementById("traceContent"); var traceContent = document.getElementById("traceContent");
var range = document.createRange(); var range = document.createRange();
range.selectNode(traceContent); range.selectNode(traceContent);
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
window.getSelection().addRange(range); window.getSelection().addRange(range);
document.execCommand("copy"); document.execCommand("copy");
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
if (flip) { if (flip) {
toggletrace() toggletrace();
} }
} }
function dismissError() { function dismissError() {
var errorContainer = document.getElementById("errorContainer"); var errorContainer = document.getElementById("errorContainer");
errorContainer.style.display = "none"; errorContainer.style.display = "none";
errorContainer.style.height = "0"; errorContainer.style.height = "0";
} }
function adjustContainerHeight() { function adjustContainerHeight() {
var errorContainer = document.getElementById("errorContainer"); var errorContainer = document.getElementById("errorContainer");
var traceDiv = document.getElementById("trace"); var traceDiv = document.getElementById("trace");
if (traceVisible) { if (traceVisible) {
errorContainer.style.height = errorContainer.scrollHeight - traceDiv.scrollHeight + traceDiv.offsetHeight + "px"; errorContainer.style.height = errorContainer.scrollHeight - traceDiv.scrollHeight + traceDiv.offsetHeight + "px";
} else { } else {
errorContainer.style.height = "auto"; errorContainer.style.height = "auto";
} }
} }
function showHelp() { function showHelp() {
$('#helpModal').modal('show'); $("#helpModal").modal("show");
} }

View File

@ -1,45 +1,45 @@
function updateFavoritesDropdown() { function updateFavoritesDropdown() {
var dropdown = document.querySelector('#favoritesDropdown'); var dropdown = document.querySelector("#favoritesDropdown");
// Check if dropdown exists // Check if dropdown exists
if (!dropdown) { if (!dropdown) {
console.error('Dropdown element with ID "favoritesDropdown" not found!'); console.error('Dropdown element with ID "favoritesDropdown" not found!');
return; // Exit the function return; // Exit the function
}
dropdown.innerHTML = ""; // Clear the current favorites
var hasFavorites = false;
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (localStorage.getItem(key) === "favorite") {
// Find the corresponding navbar entry
var navbarEntry = document.querySelector(`a[href='${key}']`);
if (navbarEntry) {
// Create a new dropdown entry
var dropdownItem = document.createElement("a");
dropdownItem.className = "dropdown-item";
dropdownItem.href = navbarEntry.href;
dropdownItem.innerHTML = navbarEntry.innerHTML;
dropdown.appendChild(dropdownItem);
hasFavorites = true;
} else {
console.warn(`Navbar entry not found for key: ${key}`);
}
} }
dropdown.innerHTML = ''; // Clear the current favorites }
var hasFavorites = false; // Show or hide the default item based on whether there are any favorites
if (!hasFavorites) {
for (var i = 0; i < localStorage.length; i++) { var defaultItem = document.createElement("a");
var key = localStorage.key(i); defaultItem.className = "dropdown-item";
if (localStorage.getItem(key) === 'favorite') { defaultItem.textContent = noFavourites;
// Find the corresponding navbar entry dropdown.appendChild(defaultItem);
var navbarEntry = document.querySelector(`a[href='${key}']`); }
if (navbarEntry) {
// Create a new dropdown entry
var dropdownItem = document.createElement('a');
dropdownItem.className = 'dropdown-item';
dropdownItem.href = navbarEntry.href;
dropdownItem.innerHTML = navbarEntry.innerHTML;
dropdown.appendChild(dropdownItem);
hasFavorites = true;
} else {
console.warn(`Navbar entry not found for key: ${key}`);
}
}
}
// Show or hide the default item based on whether there are any favorites
if (!hasFavorites) {
var defaultItem = document.createElement('a');
defaultItem.className = 'dropdown-item';
defaultItem.textContent = noFavourites;
dropdown.appendChild(defaultItem);
}
} }
// Ensure that the DOM content has been fully loaded before calling the function // Ensure that the DOM content has been fully loaded before calling the function
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
console.log('DOMContentLoaded event fired'); console.log("DOMContentLoaded event fired");
updateFavoritesDropdown(); updateFavoritesDropdown();
}); });

View File

@ -1,104 +1,107 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll('.custom-file-chooser').forEach(setupFileInput); document.querySelectorAll(".custom-file-chooser").forEach(setupFileInput);
}); });
function setupFileInput(chooser) { function setupFileInput(chooser) {
const elementId = chooser.getAttribute('data-bs-element-id'); const elementId = chooser.getAttribute("data-bs-element-id");
const filesSelected = chooser.getAttribute('data-bs-files-selected'); const filesSelected = chooser.getAttribute("data-bs-files-selected");
const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt'); const pdfPrompt = chooser.getAttribute("data-bs-pdf-prompt");
let allFiles = []; let allFiles = [];
let overlay; let overlay;
let dragCounter = 0; let dragCounter = 0;
const dragenterListener = function() { const dragenterListener = function () {
dragCounter++; dragCounter++;
if (!overlay) { if (!overlay) {
overlay = document.createElement('div'); overlay = document.createElement("div");
overlay.style.position = 'fixed'; overlay.style.position = "fixed";
overlay.style.top = 0; overlay.style.top = 0;
overlay.style.left = 0; overlay.style.left = 0;
overlay.style.width = '100%'; overlay.style.width = "100%";
overlay.style.height = '100%'; overlay.style.height = "100%";
overlay.style.background = 'rgba(0, 0, 0, 0.5)'; overlay.style.background = "rgba(0, 0, 0, 0.5)";
overlay.style.color = '#fff'; overlay.style.color = "#fff";
overlay.style.zIndex = '1000'; overlay.style.zIndex = "1000";
overlay.style.display = 'flex'; overlay.style.display = "flex";
overlay.style.alignItems = 'center'; overlay.style.alignItems = "center";
overlay.style.justifyContent = 'center'; overlay.style.justifyContent = "center";
overlay.style.pointerEvents = 'none'; overlay.style.pointerEvents = "none";
overlay.innerHTML = '<p>Drop files anywhere to upload</p>'; overlay.innerHTML = "<p>Drop files anywhere to upload</p>";
document.getElementById('content-wrap').appendChild(overlay); document.getElementById("content-wrap").appendChild(overlay);
} }
}; };
const dragleaveListener = function() { const dragleaveListener = function () {
dragCounter--; dragCounter--;
if (dragCounter === 0) { if (dragCounter === 0) {
if (overlay) { if (overlay) {
overlay.remove(); overlay.remove();
overlay = null; overlay = null;
} }
} }
}; };
const dropListener = function(e) { const dropListener = function (e) {
e.preventDefault(); e.preventDefault();
const dt = e.dataTransfer; const dt = e.dataTransfer;
const files = dt.files; const files = dt.files;
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
allFiles.push(files[i]); allFiles.push(files[i]);
} }
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();
allFiles.forEach(file => dataTransfer.items.add(file)); allFiles.forEach((file) => dataTransfer.items.add(file));
const fileInput = document.getElementById(elementId); const fileInput = document.getElementById(elementId);
fileInput.files = dataTransfer.files; fileInput.files = dataTransfer.files;
if (overlay) { if (overlay) {
overlay.remove(); overlay.remove();
overlay = null; overlay = null;
} }
dragCounter = 0; dragCounter = 0;
fileInput.dispatchEvent(new Event('change', { bubbles: true })); fileInput.dispatchEvent(new Event("change", { bubbles: true }));
}; };
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
document.body.addEventListener("dragenter", dragenterListener);
document.body.addEventListener("dragleave", dragleaveListener);
document.body.addEventListener("drop", dropListener);
$("#" + elementId).on("change", function (e) {
allFiles = Array.from(e.target.files);
handleFileInputChange(this);
});
function handleFileInputChange(inputElement) {
const files = allFiles;
const fileNames = files.map((f) => f.name);
const selectedFilesContainer = $(inputElement).siblings(".selected-files");
selectedFilesContainer.empty();
fileNames.forEach((fileName) => {
selectedFilesContainer.append("<div>" + fileName + "</div>");
}); });
if (fileNames.length === 1) {
function preventDefaults(e) { $(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames[0]);
e.preventDefault(); } else if (fileNames.length > 1) {
e.stopPropagation(); $(inputElement)
} .siblings(".custom-file-label")
.addClass("selected")
document.body.addEventListener('dragenter', dragenterListener); .html(fileNames.length + " " + filesSelected);
document.body.addEventListener('dragleave', dragleaveListener); } else {
document.body.addEventListener('drop', dropListener); $(inputElement).siblings(".custom-file-label").addClass("selected").html(pdfPrompt);
$("#" + elementId).on("change", function(e) {
allFiles = Array.from(e.target.files);
handleFileInputChange(this);
});
function handleFileInputChange(inputElement) {
const files = allFiles;
const fileNames = files.map(f => f.name);
const selectedFilesContainer = $(inputElement).siblings(".selected-files");
selectedFilesContainer.empty();
fileNames.forEach(fileName => {
selectedFilesContainer.append("<div>" + fileName + "</div>");
});
if (fileNames.length === 1) {
$(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames[0]);
} else if (fileNames.length > 1) {
$(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames.length + " " + filesSelected);
} else {
$(inputElement).siblings(".custom-file-label").addClass("selected").html(pdfPrompt);
}
} }
}
} }

View File

@ -1,292 +1,265 @@
function initializeGame() { function initializeGame() {
const gameContainer = document.getElementById('game-container'); const gameContainer = document.getElementById("game-container");
const player = document.getElementById('player'); const player = document.getElementById("player");
let playerSize = gameContainer.clientWidth * 0.0625; // 5% of container width let playerSize = gameContainer.clientWidth * 0.0625; // 5% of container width
player.style.width = playerSize + 'px'; player.style.width = playerSize + "px";
player.style.height = playerSize + 'px'; player.style.height = playerSize + "px";
let playerX = gameContainer.clientWidth / 2 - playerSize / 2; let playerX = gameContainer.clientWidth / 2 - playerSize / 2;
let playerY = gameContainer.clientHeight * 0.1; let playerY = gameContainer.clientHeight * 0.1;
const scoreElement = document.getElementById('score'); const scoreElement = document.getElementById("score");
const levelElement = document.getElementById('level'); const levelElement = document.getElementById("level");
const livesElement = document.getElementById('lives'); const livesElement = document.getElementById("lives");
const highScoreElement = document.getElementById('high-score'); const highScoreElement = document.getElementById("high-score");
let pdfSize = gameContainer.clientWidth * 0.0625; // 5% of container width let pdfSize = gameContainer.clientWidth * 0.0625; // 5% of container width
let projectileWidth = gameContainer.clientWidth * 0.00625;// 0.00625; // 0.5% of container width let projectileWidth = gameContainer.clientWidth * 0.00625; // 0.00625; // 0.5% of container width
let projectileHeight = gameContainer.clientHeight * 0.01667; // 1% of container height let projectileHeight = gameContainer.clientHeight * 0.01667; // 1% of container height
let paused = false; let paused = false;
const fireRate = 200; // Time between shots in milliseconds const fireRate = 200; // Time between shots in milliseconds
let lastProjectileTime = 0; let lastProjectileTime = 0;
let lives = 3; let lives = 3;
let highScore = localStorage.getItem("highScore") ? parseInt(localStorage.getItem("highScore")) : 0;
updateHighScore();
let highScore = localStorage.getItem('highScore') ? parseInt(localStorage.getItem('highScore')) : 0; const keysPressed = {};
updateHighScore(); const pdfs = [];
const projectiles = [];
let score = 0;
let level = 1;
let pdfSpeed = 0.5;
let gameOver = false;
function handleKeys() {
if (keysPressed["ArrowLeft"]) {
const keysPressed = {}; playerX -= 10;
const pdfs = [];
const projectiles = [];
let score = 0;
let level = 1;
let pdfSpeed = 0.5;
let gameOver = false;
function handleKeys() {
if (keysPressed['ArrowLeft']) {
playerX -= 10;
}
if (keysPressed['ArrowRight']) {
playerX += 10;
}
if (keysPressed[' '] && !gameOver) {
const currentTime = new Date().getTime();
if (currentTime - lastProjectileTime >= fireRate) {
shootProjectile();
lastProjectileTime = currentTime;
}
}
updatePlayerPosition();
}
document.addEventListener('keydown', (event) => {
if (event.key === ' ') {
event.preventDefault();
}
keysPressed[event.key] = true;
handleKeys();
});
document.addEventListener('keyup', (event) => {
keysPressed[event.key] = false;
});
function updatePlayerPosition() {
player.style.left = playerX + 'px';
player.style.bottom = playerY + 'px';
} }
if (keysPressed["ArrowRight"]) {
function updateLives() { playerX += 10;
livesElement.textContent = 'Lives: ' + lives;
} }
if (keysPressed[" "] && !gameOver) {
function updateHighScore() { const currentTime = new Date().getTime();
highScoreElement.textContent = 'High Score: ' + highScore; if (currentTime - lastProjectileTime >= fireRate) {
shootProjectile();
lastProjectileTime = currentTime;
}
} }
function shootProjectile() {
const projectile = document.createElement('div');
projectile.classList.add('projectile');
projectile.style.backgroundColor = 'black';
projectile.style.width = projectileWidth + 'px';
projectile.style.height = projectileHeight + 'px';
projectile.style.left = (playerX + playerSize / 2 - projectileWidth / 2) + 'px';
projectile.style.top = (gameContainer.clientHeight - playerY - playerSize) + 'px';
gameContainer.appendChild(projectile);
projectiles.push(projectile);
}
function spawnPdf() {
const pdf = document.createElement('img');
pdf.src = 'images/file-earmark-pdf.svg';
pdf.classList.add('pdf');
pdf.style.width = pdfSize + 'px';
pdf.style.height = pdfSize + 'px';
pdf.style.left = Math.floor(Math.random() * (gameContainer.clientWidth - pdfSize)) + 'px';
pdf.style.top = '0px';
gameContainer.appendChild(pdf);
pdfs.push(pdf);
}
function resetEnemies() {
pdfs.forEach((pdf) => gameContainer.removeChild(pdf));
pdfs.length = 0;
}
function updateGame() {
if (gameOver || paused) return;
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
const pdf = pdfs[pdfIndex];
const pdfY = parseFloat(pdf.style.top) + pdfSpeed;
if (pdfY + 50 > gameContainer.clientHeight) {
gameContainer.removeChild(pdf);
pdfs.splice(pdfIndex, 1);
// Deduct 2 points when a PDF gets past the player
score -= 0;
updateScore();
// Decrease lives and check if game over
lives--;
updateLives();
if (lives <= 0) {
endGame();
return;
}
} else {
pdf.style.top = pdfY + 'px';
// Check for collision with player
if (collisionDetected(player, pdf)) {
lives--;
updateLives();
resetEnemies();
if (lives <= 0) {
endGame();
return;
}
}
}
};
projectiles.forEach((projectile, projectileIndex) => {
const projectileY = parseInt(projectile.style.top) - 10;
if (projectileY < 0) {
gameContainer.removeChild(projectile);
projectiles.splice(projectileIndex, 1);
} else {
projectile.style.top = projectileY + 'px';
}
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
const pdf = pdfs[pdfIndex];
if (collisionDetected(projectile, pdf)) {
gameContainer.removeChild(pdf);
gameContainer.removeChild(projectile);
pdfs.splice(pdfIndex, 1);
projectiles.splice(projectileIndex, 1);
score = score + 10;
updateScore();
break;
}
}
});
setTimeout(updateGame, 1000 / 60);
}
function resetGame() {
playerX = gameContainer.clientWidth / 2;
playerY = 50;
updatePlayerPosition();
pdfs.forEach((pdf) => gameContainer.removeChild(pdf));
projectiles.forEach((projectile) => gameContainer.removeChild(projectile));
pdfs.length = 0;
projectiles.length = 0;
score = 0;
level = 1;
lives = 3;
gameOver = false;
updateScore();
updateLives();
levelElement.textContent = 'Level: ' + level;
pdfSpeed = 1;
clearTimeout(spawnPdfTimeout); // Clear the existing spawnPdfTimeout
setTimeout(updateGame, 1000 / 60);
spawnPdfInterval();
}
function updateScore() {
scoreElement.textContent = 'Score: ' + score;
checkLevelUp();
}
function checkLevelUp() {
const newLevel = Math.floor(score / 100) + 1;
if (newLevel > level) {
level = newLevel;
levelElement.textContent = 'Level: ' + level;
pdfSpeed += 0.2;
}
}
function collisionDetected(a, b) {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
return (
rectA.left < rectB.right &&
rectA.right > rectB.left &&
rectA.top < rectB.bottom &&
rectA.bottom > rectB.top
);
}
function endGame() {
gameOver = true;
if (score > highScore) {
highScore = score;
localStorage.setItem('highScore', highScore);
updateHighScore();
}
alert('Game Over! Your final score is: ' + score);
document.getElementById('game-container-wrapper').close();
}
let spawnPdfTimeout;
const BASE_SPAWN_INTERVAL_MS = 1250; // milliseconds before a new enemy spawns
const LEVEL_INCREASE_FACTOR_MS = 0; // milliseconds to decrease the spawn interval per level
const MAX_SPAWN_RATE_REDUCTION_MS = 800; // Max milliseconds from the base spawn interval
function spawnPdfInterval() {
console.log("spawnPdfInterval");
if (gameOver || paused) {
console.log("spawnPdfInterval 2");
clearTimeout(spawnPdfTimeout);
return;
}
console.log("spawnPdfInterval 3");
spawnPdf();
let spawnRateReduction = Math.min(level * LEVEL_INCREASE_FACTOR_MS, MAX_SPAWN_RATE_REDUCTION_MS);
let spawnRate = BASE_SPAWN_INTERVAL_MS - spawnRateReduction;
spawnPdfTimeout = setTimeout(spawnPdfInterval, spawnRate);
}
updatePlayerPosition(); updatePlayerPosition();
updateGame(); }
spawnPdfInterval();
document.addEventListener("keydown", (event) => {
if (event.key === " ") {
event.preventDefault();
}
keysPressed[event.key] = true;
handleKeys();
});
document.addEventListener('visibilitychange', function() { document.addEventListener("keyup", (event) => {
if (document.hidden) { keysPressed[event.key] = false;
paused = true; });
} else {
paused = false; function updatePlayerPosition() {
updateGame(); player.style.left = playerX + "px";
spawnPdfInterval(); player.style.bottom = playerY + "px";
}
function updateLives() {
livesElement.textContent = "Lives: " + lives;
}
function updateHighScore() {
highScoreElement.textContent = "High Score: " + highScore;
}
function shootProjectile() {
const projectile = document.createElement("div");
projectile.classList.add("projectile");
projectile.style.backgroundColor = "black";
projectile.style.width = projectileWidth + "px";
projectile.style.height = projectileHeight + "px";
projectile.style.left = playerX + playerSize / 2 - projectileWidth / 2 + "px";
projectile.style.top = gameContainer.clientHeight - playerY - playerSize + "px";
gameContainer.appendChild(projectile);
projectiles.push(projectile);
}
function spawnPdf() {
const pdf = document.createElement("img");
pdf.src = "images/file-earmark-pdf.svg";
pdf.classList.add("pdf");
pdf.style.width = pdfSize + "px";
pdf.style.height = pdfSize + "px";
pdf.style.left = Math.floor(Math.random() * (gameContainer.clientWidth - pdfSize)) + "px";
pdf.style.top = "0px";
gameContainer.appendChild(pdf);
pdfs.push(pdf);
}
function resetEnemies() {
pdfs.forEach((pdf) => gameContainer.removeChild(pdf));
pdfs.length = 0;
}
function updateGame() {
if (gameOver || paused) return;
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
const pdf = pdfs[pdfIndex];
const pdfY = parseFloat(pdf.style.top) + pdfSpeed;
if (pdfY + 50 > gameContainer.clientHeight) {
gameContainer.removeChild(pdf);
pdfs.splice(pdfIndex, 1);
// Deduct 2 points when a PDF gets past the player
score -= 0;
updateScore();
// Decrease lives and check if game over
lives--;
updateLives();
if (lives <= 0) {
endGame();
return;
} }
} else {
pdf.style.top = pdfY + "px";
// Check for collision with player
if (collisionDetected(player, pdf)) {
lives--;
updateLives();
resetEnemies();
if (lives <= 0) {
endGame();
return;
}
}
}
}
projectiles.forEach((projectile, projectileIndex) => {
const projectileY = parseInt(projectile.style.top) - 10;
if (projectileY < 0) {
gameContainer.removeChild(projectile);
projectiles.splice(projectileIndex, 1);
} else {
projectile.style.top = projectileY + "px";
}
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
const pdf = pdfs[pdfIndex];
if (collisionDetected(projectile, pdf)) {
gameContainer.removeChild(pdf);
gameContainer.removeChild(projectile);
pdfs.splice(pdfIndex, 1);
projectiles.splice(projectileIndex, 1);
score = score + 10;
updateScore();
break;
}
}
}); });
window.resetGame = resetGame; setTimeout(updateGame, 1000 / 60);
}
function resetGame() {
playerX = gameContainer.clientWidth / 2;
playerY = 50;
updatePlayerPosition();
pdfs.forEach((pdf) => gameContainer.removeChild(pdf));
projectiles.forEach((projectile) => gameContainer.removeChild(projectile));
pdfs.length = 0;
projectiles.length = 0;
score = 0;
level = 1;
lives = 3;
gameOver = false;
updateScore();
updateLives();
levelElement.textContent = "Level: " + level;
pdfSpeed = 1;
clearTimeout(spawnPdfTimeout); // Clear the existing spawnPdfTimeout
setTimeout(updateGame, 1000 / 60);
spawnPdfInterval();
}
function updateScore() {
scoreElement.textContent = "Score: " + score;
checkLevelUp();
}
function checkLevelUp() {
const newLevel = Math.floor(score / 100) + 1;
if (newLevel > level) {
level = newLevel;
levelElement.textContent = "Level: " + level;
pdfSpeed += 0.2;
}
}
function collisionDetected(a, b) {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
return rectA.left < rectB.right && rectA.right > rectB.left && rectA.top < rectB.bottom && rectA.bottom > rectB.top;
}
function endGame() {
gameOver = true;
if (score > highScore) {
highScore = score;
localStorage.setItem("highScore", highScore);
updateHighScore();
}
alert("Game Over! Your final score is: " + score);
document.getElementById("game-container-wrapper").close();
}
let spawnPdfTimeout;
const BASE_SPAWN_INTERVAL_MS = 1250; // milliseconds before a new enemy spawns
const LEVEL_INCREASE_FACTOR_MS = 0; // milliseconds to decrease the spawn interval per level
const MAX_SPAWN_RATE_REDUCTION_MS = 800; // Max milliseconds from the base spawn interval
function spawnPdfInterval() {
console.log("spawnPdfInterval");
if (gameOver || paused) {
console.log("spawnPdfInterval 2");
clearTimeout(spawnPdfTimeout);
return;
}
console.log("spawnPdfInterval 3");
spawnPdf();
let spawnRateReduction = Math.min(level * LEVEL_INCREASE_FACTOR_MS, MAX_SPAWN_RATE_REDUCTION_MS);
let spawnRate = BASE_SPAWN_INTERVAL_MS - spawnRateReduction;
spawnPdfTimeout = setTimeout(spawnPdfInterval, spawnRate);
}
updatePlayerPosition();
updateGame();
spawnPdfInterval();
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
paused = true;
} else {
paused = false;
updateGame();
spawnPdfInterval();
}
});
window.resetGame = resetGame;
} }
window.initializeGame = initializeGame; window.initializeGame = initializeGame;

View File

@ -1,55 +1,52 @@
function compareVersions(version1, version2) { function compareVersions(version1, version2) {
const v1 = version1.split('.'); const v1 = version1.split(".");
const v2 = version2.split('.'); const v2 = version2.split(".");
for (let i = 0; i < v1.length || i < v2.length; i++) { for (let i = 0; i < v1.length || i < v2.length; i++) {
const n1 = parseInt(v1[i]) || 0; const n1 = parseInt(v1[i]) || 0;
const n2 = parseInt(v2[i]) || 0; const n2 = parseInt(v2[i]) || 0;
if (n1 > n2) { if (n1 > n2) {
return 1; return 1;
} else if (n1 < n2) { } else if (n1 < n2) {
return -1; return -1;
} }
} }
return 0; return 0;
} }
async function getLatestReleaseVersion() { async function getLatestReleaseVersion() {
const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest"; const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest";
try { try {
const response = await fetch(url); const response = await fetch(url);
const data = await response.json(); const data = await response.json();
return data.tag_name ? data.tag_name.substring(1) : ""; return data.tag_name ? data.tag_name.substring(1) : "";
} catch (error) { } catch (error) {
console.error("Failed to fetch latest version:", error); console.error("Failed to fetch latest version:", error);
return ""; // Return an empty string if the fetch fails return ""; // Return an empty string if the fetch fails
} }
} }
async function checkForUpdate() { async function checkForUpdate() {
// Initialize the update button as hidden // Initialize the update button as hidden
var updateBtn = document.getElementById("update-btn"); var updateBtn = document.getElementById("update-btn");
if (updateBtn !== null) { if (updateBtn !== null) {
updateBtn.style.display = "none"; updateBtn.style.display = "none";
} }
const latestVersion = await getLatestReleaseVersion();
const latestVersion = await getLatestReleaseVersion(); console.log("latestVersion=" + latestVersion);
console.log("latestVersion=" + latestVersion) console.log("currentVersion=" + currentVersion);
console.log("currentVersion=" + currentVersion) console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion));
console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion)) if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) { document.getElementById("update-btn").style.display = "block";
document.getElementById("update-btn").style.display = "block"; console.log("visible");
console.log("visible") } else {
} else { console.log("hidden");
console.log("hidden") }
}
} }
document.addEventListener("DOMContentLoaded", (event) => {
document.addEventListener('DOMContentLoaded', (event) => { checkForUpdate();
checkForUpdate();
}); });

View File

@ -1,78 +1,76 @@
function filterCards() { function filterCards() {
var input = document.getElementById('searchBar'); var input = document.getElementById("searchBar");
var filter = input.value.toUpperCase(); var filter = input.value.toUpperCase();
var cards = document.querySelectorAll('.feature-card'); var cards = document.querySelectorAll(".feature-card");
for (var i = 0; i < cards.length; i++) { for (var i = 0; i < cards.length; i++) {
var card = cards[i]; var card = cards[i];
var title = card.querySelector('h5.card-title').innerText; var title = card.querySelector("h5.card-title").innerText;
var text = card.querySelector('p.card-text').innerText; var text = card.querySelector("p.card-text").innerText;
// Get the navbar tags associated with the card // Get the navbar tags associated with the card
var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`); var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`);
var navbarTags = navbarItem ? navbarItem.getAttribute('data-bs-tags') : ''; var navbarTags = navbarItem ? navbarItem.getAttribute("data-bs-tags") : "";
var content = title + ' ' + text + ' ' + navbarTags; var content = title + " " + text + " " + navbarTags;
if (content.toUpperCase().indexOf(filter) > -1) { if (content.toUpperCase().indexOf(filter) > -1) {
card.style.display = ""; card.style.display = "";
} else { } else {
card.style.display = "none"; card.style.display = "none";
}
} }
}
} }
function toggleFavorite(element) { function toggleFavorite(element) {
var img = element.querySelector('img'); var img = element.querySelector("img");
var card = element.closest('.feature-card'); var card = element.closest(".feature-card");
var cardId = card.id; var cardId = card.id;
if (img.src.endsWith('star.svg')) { if (img.src.endsWith("star.svg")) {
img.src = 'images/star-fill.svg'; img.src = "images/star-fill.svg";
card.classList.add('favorite'); card.classList.add("favorite");
localStorage.setItem(cardId, 'favorite'); localStorage.setItem(cardId, "favorite");
} else { } else {
img.src = 'images/star.svg'; img.src = "images/star.svg";
card.classList.remove('favorite'); card.classList.remove("favorite");
localStorage.removeItem(cardId); localStorage.removeItem(cardId);
} }
reorderCards(); reorderCards();
updateFavoritesDropdown(); updateFavoritesDropdown();
filterCards(); filterCards();
} }
function reorderCards() { function reorderCards() {
var container = document.querySelector('.features-container'); var container = document.querySelector(".features-container");
var cards = Array.from(container.getElementsByClassName('feature-card')); var cards = Array.from(container.getElementsByClassName("feature-card"));
cards.sort(function(a, b) { cards.sort(function (a, b) {
var aIsFavorite = localStorage.getItem(a.id) === 'favorite'; var aIsFavorite = localStorage.getItem(a.id) === "favorite";
var bIsFavorite = localStorage.getItem(b.id) === 'favorite'; var bIsFavorite = localStorage.getItem(b.id) === "favorite";
if (aIsFavorite && !bIsFavorite) { if (aIsFavorite && !bIsFavorite) {
return -1; return -1;
} }
if (!aIsFavorite && bIsFavorite) { if (!aIsFavorite && bIsFavorite) {
return 1; return 1;
} }
return 0; return 0;
}); });
cards.forEach(function(card) { cards.forEach(function (card) {
container.appendChild(card); container.appendChild(card);
}); });
} }
function initializeCards() { function initializeCards() {
var cards = document.querySelectorAll('.feature-card'); var cards = document.querySelectorAll(".feature-card");
cards.forEach(function(card) { cards.forEach(function (card) {
var cardId = card.id; var cardId = card.id;
var img = card.querySelector('.favorite-icon img'); var img = card.querySelector(".favorite-icon img");
if (localStorage.getItem(cardId) === 'favorite') { if (localStorage.getItem(cardId) === "favorite") {
img.src = 'images/star-fill.svg'; img.src = "images/star-fill.svg";
card.classList.add('favorite'); card.classList.add("favorite");
} }
}); });
reorderCards(); reorderCards();
updateFavoritesDropdown(); updateFavoritesDropdown();
filterCards(); filterCards();
} }
window.onload = initializeCards; window.onload = initializeCards;

View File

@ -1,80 +1,88 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
setLanguageForDropdown('.lang_dropdown-item'); setLanguageForDropdown(".lang_dropdown-item");
// Detect the browser's preferred language // Detect the browser's preferred language
let browserLang = navigator.language || navigator.userLanguage; let browserLang = navigator.language || navigator.userLanguage;
// Convert to a format consistent with your language codes (e.g., en-GB, fr-FR) // Convert to a format consistent with your language codes (e.g., en-GB, fr-FR)
browserLang = browserLang.replace('-', '_'); browserLang = browserLang.replace("-", "_");
// Check if the dropdown contains the browser's language // Check if the dropdown contains the browser's language
const dropdownLangExists = document.querySelector(`.lang_dropdown-item[data-language-code="${browserLang}"]`); const dropdownLangExists = document.querySelector(`.lang_dropdown-item[data-language-code="${browserLang}"]`);
// Set the default language to browser's language or 'en_GB' if not found in the dropdown // Set the default language to browser's language or 'en_GB' if not found in the dropdown
const defaultLocale = dropdownLangExists ? browserLang : 'en_GB'; const defaultLocale = dropdownLangExists ? browserLang : "en_GB";
const storedLocale = localStorage.getItem('languageCode') || defaultLocale; const storedLocale = localStorage.getItem("languageCode") || defaultLocale;
const dropdownItems = document.querySelectorAll(".lang_dropdown-item");
for (let i = 0; i < dropdownItems.length; i++) {
const dropdownItems = document.querySelectorAll('.lang_dropdown-item'); const item = dropdownItems[i];
item.classList.remove("active");
for (let i = 0; i < dropdownItems.length; i++) { if (item.dataset.languageCode === storedLocale) {
const item = dropdownItems[i]; item.classList.add("active");
item.classList.remove('active'); }
if (item.dataset.languageCode === storedLocale) { item.addEventListener("click", handleDropdownItemClick);
item.classList.add('active'); }
}
item.addEventListener('click', handleDropdownItemClick);
}
}); });
function setLanguageForDropdown(dropdownClass) { function setLanguageForDropdown(dropdownClass) {
const defaultLocale = document.documentElement.lang || 'en_GB'; const defaultLocale = document.documentElement.lang || "en_GB";
const storedLocale = localStorage.getItem('languageCode') || defaultLocale; const storedLocale = localStorage.getItem("languageCode") || defaultLocale;
const dropdownItems = document.querySelectorAll(dropdownClass); const dropdownItems = document.querySelectorAll(dropdownClass);
for (let i = 0; i < dropdownItems.length; i++) { for (let i = 0; i < dropdownItems.length; i++) {
const item = dropdownItems[i]; const item = dropdownItems[i];
item.classList.remove('active'); item.classList.remove("active");
if (item.dataset.languageCode === storedLocale) { if (item.dataset.languageCode === storedLocale) {
item.classList.add('active'); item.classList.add("active");
}
item.addEventListener('click', handleDropdownItemClick);
} }
item.addEventListener("click", handleDropdownItemClick);
}
} }
function handleDropdownItemClick(event) { function handleDropdownItemClick(event) {
event.preventDefault(); event.preventDefault();
const languageCode = event.currentTarget.dataset.bsLanguageCode; // change this to event.currentTarget const languageCode = event.currentTarget.dataset.bsLanguageCode; // change this to event.currentTarget
if (languageCode) { if (languageCode) {
localStorage.setItem('languageCode', languageCode); localStorage.setItem("languageCode", languageCode);
const currentUrl = window.location.href; const currentUrl = window.location.href;
if (currentUrl.indexOf('?lang=') === -1) { if (currentUrl.indexOf("?lang=") === -1) {
window.location.href = currentUrl + '?lang=' + languageCode; window.location.href = currentUrl + "?lang=" + languageCode;
} else {
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
}
} else { } else {
console.error("Language code is not set for this item."); // for debugging window.location.href = currentUrl.replace(/\?lang=\w{2,}/, "?lang=" + languageCode);
} }
} else {
console.error("Language code is not set for this item."); // for debugging
}
} }
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".nav-item.dropdown").forEach((element) => {
const dropdownMenu = element.querySelector(".dropdown-menu");
if (
dropdownMenu.id !== "favoritesDropdown" &&
dropdownMenu.children.length <= 2 &&
dropdownMenu.querySelectorAll("hr.dropdown-divider").length === dropdownMenu.children.length
) {
if (
element.previousElementSibling &&
element.previousElementSibling.classList.contains("nav-item") &&
element.previousElementSibling.classList.contains("nav-item-separator")
) {
element.previousElementSibling.remove();
}
element.remove();
}
});
document.addEventListener('DOMContentLoaded', function() { //Sort languages by alphabet
document.querySelectorAll('.nav-item.dropdown').forEach((element) => { const list = Array.from(document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').children).filter(
const dropdownMenu = element.querySelector(".dropdown-menu"); (child) => child.matches("a"),
if (dropdownMenu.id !== 'favoritesDropdown' && dropdownMenu.children.length <= 2 && dropdownMenu.querySelectorAll("hr.dropdown-divider").length === dropdownMenu.children.length) { );
if (element.previousElementSibling && element.previousElementSibling.classList.contains('nav-item') && element.previousElementSibling.classList.contains('nav-item-separator')) { list
element.previousElementSibling.remove(); .sort(function (a, b) {
} return a.textContent.toUpperCase().localeCompare(b.textContent.toUpperCase());
element.remove(); })
} .forEach((node) => document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').appendChild(node));
}); });
//Sort languages by alphabet
const list = Array.from(document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').children).filter(child => child.matches('a'));
list.sort(function(a, b) {
return a.textContent.toUpperCase().localeCompare(b.textContent.toUpperCase());
}).forEach(node => document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').appendChild(node));
});

View File

@ -1,47 +1,47 @@
async function downloadFilesWithCallback(processFileCallback) { async function downloadFilesWithCallback(processFileCallback) {
const fileInput = document.querySelector('input[type="file"]'); const fileInput = document.querySelector('input[type="file"]');
const files = fileInput.files; const files = fileInput.files;
const zipThreshold = 4; const zipThreshold = 4;
const zipFiles = files.length > zipThreshold; const zipFiles = files.length > zipThreshold;
let jszip = null; let jszip = null;
if (zipFiles) { if (zipFiles) {
jszip = new JSZip(); jszip = new JSZip();
} }
const promises = Array.from(files).map(async file => { const promises = Array.from(files).map(async (file) => {
const { processedData, fileName } = await processFileCallback(file); const { processedData, fileName } = await processFileCallback(file);
if (zipFiles) {
jszip.file(fileName, processedData);
} else {
const url = URL.createObjectURL(processedData);
const downloadOption = localStorage.getItem('downloadOption');
if (downloadOption === 'sameWindow') {
window.location.href = url;
} else if (downloadOption === 'newWindow') {
window.open(url, '_blank');
} else {
const downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.download = fileName;
downloadLink.click();
}
}
});
await Promise.all(promises);
if (zipFiles) { if (zipFiles) {
const content = await jszip.generateAsync({ type: "blob" }); jszip.file(fileName, processedData);
const url = URL.createObjectURL(content); } else {
const a = document.createElement('a'); const url = URL.createObjectURL(processedData);
a.href = url; const downloadOption = localStorage.getItem("downloadOption");
a.download = "files.zip";
document.body.appendChild(a); if (downloadOption === "sameWindow") {
a.click(); window.location.href = url;
a.remove(); } else if (downloadOption === "newWindow") {
window.open(url, "_blank");
} else {
const downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = fileName;
downloadLink.click();
}
} }
});
await Promise.all(promises);
if (zipFiles) {
const content = await jszip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(content);
const a = document.createElement("a");
a.href = url;
a.download = "files.zip";
document.body.appendChild(a);
a.click();
a.remove();
}
} }

View File

@ -1,27 +1,27 @@
let currentSort = { let currentSort = {
field: null, field: null,
descending: false descending: false,
}; };
document.getElementById("fileInput-input").addEventListener("change", function() { document.getElementById("fileInput-input").addEventListener("change", function () {
var files = this.files; var files = this.files;
displayFiles(files); displayFiles(files);
}); });
/** /**
* @param {FileList} files * @param {FileList} files
*/ */
function displayFiles(files) { function displayFiles(files) {
const list = document.getElementById("selectedFiles"); const list = document.getElementById("selectedFiles");
while (list.firstChild) { while (list.firstChild) {
list.removeChild(list.firstChild); list.removeChild(list.firstChild);
} }
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const item = document.createElement("li"); const item = document.createElement("li");
item.className = "list-group-item"; item.className = "list-group-item";
item.innerHTML = ` item.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100"> <div class="d-flex justify-content-between align-items-center w-100">
<div class="filename">${files[i].name}</div> <div class="filename">${files[i].name}</div>
<div class="arrows d-flex"> <div class="arrows d-flex">
@ -31,100 +31,100 @@ function displayFiles(files) {
</div> </div>
</div> </div>
`; `;
list.appendChild(item); list.appendChild(item);
} }
attachMoveButtons(); attachMoveButtons();
} }
function attachMoveButtons() { function attachMoveButtons() {
var moveUpButtons = document.querySelectorAll(".move-up"); var moveUpButtons = document.querySelectorAll(".move-up");
for (var i = 0; i < moveUpButtons.length; i++) { for (var i = 0; i < moveUpButtons.length; i++) {
moveUpButtons[i].addEventListener("click", function(event) { moveUpButtons[i].addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
var parent = this.closest(".list-group-item"); var parent = this.closest(".list-group-item");
var grandParent = parent.parentNode; var grandParent = parent.parentNode;
if (parent.previousElementSibling) { if (parent.previousElementSibling) {
grandParent.insertBefore(parent, parent.previousElementSibling); grandParent.insertBefore(parent, parent.previousElementSibling);
updateFiles(); updateFiles();
} }
}); });
} }
var moveDownButtons = document.querySelectorAll(".move-down"); var moveDownButtons = document.querySelectorAll(".move-down");
for (var i = 0; i < moveDownButtons.length; i++) { for (var i = 0; i < moveDownButtons.length; i++) {
moveDownButtons[i].addEventListener("click", function(event) { moveDownButtons[i].addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
var parent = this.closest(".list-group-item"); var parent = this.closest(".list-group-item");
var grandParent = parent.parentNode; var grandParent = parent.parentNode;
if (parent.nextElementSibling) { if (parent.nextElementSibling) {
grandParent.insertBefore(parent.nextElementSibling, parent); grandParent.insertBefore(parent.nextElementSibling, parent);
updateFiles(); updateFiles();
} }
}); });
} }
var removeButtons = document.querySelectorAll(".remove-file"); var removeButtons = document.querySelectorAll(".remove-file");
for (var i = 0; i < removeButtons.length; i++) { for (var i = 0; i < removeButtons.length; i++) {
removeButtons[i].addEventListener("click", function (event) { removeButtons[i].addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
var parent = this.closest(".list-group-item"); var parent = this.closest(".list-group-item");
parent.remove(); parent.remove();
updateFiles(); updateFiles();
}); });
} }
} }
document.getElementById("sortByNameBtn").addEventListener("click", function() { document.getElementById("sortByNameBtn").addEventListener("click", function () {
if (currentSort.field === "name" && !currentSort.descending) { if (currentSort.field === "name" && !currentSort.descending) {
currentSort.descending = true; currentSort.descending = true;
sortFiles((a, b) => b.name.localeCompare(a.name)); sortFiles((a, b) => b.name.localeCompare(a.name));
} else { } else {
currentSort.field = "name"; currentSort.field = "name";
currentSort.descending = false; currentSort.descending = false;
sortFiles((a, b) => a.name.localeCompare(b.name)); sortFiles((a, b) => a.name.localeCompare(b.name));
} }
}); });
document.getElementById("sortByDateBtn").addEventListener("click", function() { document.getElementById("sortByDateBtn").addEventListener("click", function () {
if (currentSort.field === "lastModified" && !currentSort.descending) { if (currentSort.field === "lastModified" && !currentSort.descending) {
currentSort.descending = true; currentSort.descending = true;
sortFiles((a, b) => b.lastModified - a.lastModified); sortFiles((a, b) => b.lastModified - a.lastModified);
} else { } else {
currentSort.field = "lastModified"; currentSort.field = "lastModified";
currentSort.descending = false; currentSort.descending = false;
sortFiles((a, b) => a.lastModified - b.lastModified); sortFiles((a, b) => a.lastModified - b.lastModified);
} }
}); });
function sortFiles(comparator) { function sortFiles(comparator) {
// Convert FileList to array and sort // Convert FileList to array and sort
const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator); const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator);
// Refresh displayed list // Refresh displayed list
displayFiles(sortedFilesArray); displayFiles(sortedFilesArray);
// Update the files property // Update the files property
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();
sortedFilesArray.forEach(file => dataTransfer.items.add(file)); sortedFilesArray.forEach((file) => dataTransfer.items.add(file));
document.getElementById("fileInput-input").files = dataTransfer.files; document.getElementById("fileInput-input").files = dataTransfer.files;
} }
function updateFiles() { function updateFiles() {
var dataTransfer = new DataTransfer(); var dataTransfer = new DataTransfer();
var liElements = document.querySelectorAll("#selectedFiles li"); var liElements = document.querySelectorAll("#selectedFiles li");
const files = document.getElementById("fileInput-input").files; const files = document.getElementById("fileInput-input").files;
for (var i = 0; i < liElements.length; i++) { for (var i = 0; i < liElements.length; i++) {
var fileNameFromList = liElements[i].querySelector(".filename").innerText; var fileNameFromList = liElements[i].querySelector(".filename").innerText;
var fileFromFiles; var fileFromFiles;
for (var j = 0; j < files.length; j++) { for (var j = 0; j < files.length; j++) {
var file = files[j]; var file = files[j];
if (file.name === fileNameFromList) { if (file.name === fileNameFromList) {
dataTransfer.items.add(file); dataTransfer.items.add(file);
break; break;
} }
}
} }
document.getElementById("fileInput-input").files = dataTransfer.files; }
document.getElementById("fileInput-input").files = dataTransfer.files;
} }

View File

@ -1,125 +1,123 @@
class DragDropManager { class DragDropManager {
dragContainer; dragContainer;
wrapper; wrapper;
pageDirection; pageDirection;
movePageTo; movePageTo;
pageDragging; pageDragging;
draggelEl; draggelEl;
draggedImageEl; draggedImageEl;
hoveredEl; hoveredEl;
endInsertionElement; endInsertionElement;
constructor(id, wrapperId) { constructor(id, wrapperId) {
this.dragContainer = document.getElementById(id); this.dragContainer = document.getElementById(id);
this.pageDirection = document.documentElement.getAttribute("lang-direction"); this.pageDirection = document.documentElement.getAttribute("lang-direction");
this.wrapper = document.getElementById(wrapperId); this.wrapper = document.getElementById(wrapperId);
this.pageDragging = false; this.pageDragging = false;
this.hoveredEl = undefined; this.hoveredEl = undefined;
this.draggelEl = undefined this.draggelEl = undefined;
this.draggedImageEl = undefined; this.draggedImageEl = undefined;
var styleElement = document.createElement('link'); var styleElement = document.createElement("link");
styleElement.rel = 'stylesheet'; styleElement.rel = "stylesheet";
styleElement.href = 'css/dragdrop.css' styleElement.href = "css/dragdrop.css";
document.head.appendChild(styleElement); document.head.appendChild(styleElement);
const div = document.createElement('div'); const div = document.createElement("div");
div.classList.add('drag-manager_endpoint'); div.classList.add("drag-manager_endpoint");
div.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16"> div.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293V6.5z"/> <path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293V6.5z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/> <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg>` </svg>`;
this.endInsertionElement = div; this.endInsertionElement = div;
this.startDraggingPage = this.startDraggingPage.bind(this); this.startDraggingPage = this.startDraggingPage.bind(this);
this.onDragEl = this.onDragEl.bind(this); this.onDragEl = this.onDragEl.bind(this);
this.stopDraggingPage = this.stopDraggingPage.bind(this); this.stopDraggingPage = this.stopDraggingPage.bind(this);
this.adapt(div); 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.visibility = "hidden";
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.visibility = "visible";
this.draggedImageEl.style.left = `${clientX}px`;
this.draggedImageEl.style.top = `${clientY}px`;
} }
}
startDraggingPage(div,) { stopDraggingPage() {
this.pageDragging = true; window.removeEventListener("mousemove", this.onDragEl);
this.draggedEl = div; this.wrapper.classList.remove("drag-manager_dragging-container");
const img = div.querySelector('img'); this.wrapper.removeChild(this.endInsertionElement);
div.classList.add('drag-manager_dragging'); window.removeEventListener("mouseup", this.stopDraggingPage);
const imageSrc = img.src; this.draggedImageEl = undefined;
this.pageDragging = false;
const imgEl = document.createElement('img'); this.draggedEl.classList.remove("drag-manager_dragging");
imgEl.classList.add('dragged-img'); this.hoveredEl?.classList.remove("drag-manager_draghover");
imgEl.src = imageSrc; this.dragContainer.childNodes.forEach((dragChild) => {
this.draggedImageEl = imgEl; this.dragContainer.removeChild(dragChild);
imgEl.style.visibility = 'hidden'; });
imgEl.style.transform = `rotate(${img.style.rotate === '' ? '0deg' : img.style.rotate}) translate(-50%, -50%)`; if (!this.hoveredEl) {
this.dragContainer.appendChild(imgEl); return;
window.addEventListener('mouseup', this.stopDraggingPage)
window.addEventListener('mousemove', this.onDragEl)
this.wrapper.classList.add('drag-manager_dragging-container');
this.wrapper.appendChild(this.endInsertionElement);
} }
if (this.hoveredEl === this.endInsertionElement) {
onDragEl(mouseEvent) { this.movePageTo(this.draggedEl);
const { clientX, clientY } = mouseEvent; return;
if(this.draggedImageEl) {
this.draggedImageEl.style.visibility = 'visible';
this.draggedImageEl.style.left = `${clientX}px`;
this.draggedImageEl.style.top = `${clientY}px`;
}
} }
this.movePageTo(this.draggedEl, this.hoveredEl);
}
setActions({ movePageTo }) {
this.movePageTo = movePageTo;
}
stopDraggingPage() { adapt(div) {
window.removeEventListener('mousemove', this.onDragEl); const onDragStart = () => {
this.wrapper.classList.remove('drag-manager_dragging-container'); this.startDraggingPage(div);
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 }) { const onMouseEnter = () => {
this.movePageTo = movePageTo; if (this.pageDragging) {
} this.hoveredEl = div;
div.classList.add("drag-manager_draghover");
}
};
const onMouseLeave = () => {
this.hoveredEl = undefined;
div.classList.remove("drag-manager_draghover");
};
adapt(div) { div.addEventListener("dragstart", onDragStart);
const onDragStart = () => { div.addEventListener("mouseenter", onMouseEnter);
this.startDraggingPage(div); div.addEventListener("mouseleave", onMouseLeave);
}
const onMouseEnter = () => { return div;
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; export default DragDropManager;

View File

@ -1,46 +1,46 @@
class ImageHiglighter { class ImageHiglighter {
imageHighlighter; imageHighlighter;
constructor(id) { constructor(id) {
this.imageHighlighter = document.getElementById(id); this.imageHighlighter = document.getElementById(id);
this.imageHighlightCallback = this.imageHighlightCallback.bind(this); this.imageHighlightCallback = this.imageHighlightCallback.bind(this);
var styleElement = document.createElement('link'); var styleElement = document.createElement("link");
styleElement.rel = 'stylesheet'; styleElement.rel = "stylesheet";
styleElement.href = 'css/imageHighlighter.css' styleElement.href = "css/imageHighlighter.css";
document.head.appendChild(styleElement); document.head.appendChild(styleElement);
this.imageHighlighter.onclick = () => { this.imageHighlighter.onclick = () => {
this.imageHighlighter.childNodes.forEach((child) => { this.imageHighlighter.childNodes.forEach((child) => {
child.classList.add('remove'); child.classList.add("remove");
setTimeout(() => { setTimeout(() => {
this.imageHighlighter.removeChild(child); this.imageHighlighter.removeChild(child);
}, 100) }, 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() { imageHighlightCallback(highlightEvent) {
// not needed in this case 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);
}
adapt(div) { setActions() {
const img = div.querySelector('.page-image'); // not needed in this case
img.addEventListener('click', this.imageHighlightCallback) }
return div;
} adapt(div) {
const img = div.querySelector(".page-image");
img.addEventListener("click", this.imageHighlightCallback);
return div;
}
} }
export default ImageHiglighter; export default ImageHiglighter;

View File

@ -1,198 +1,200 @@
class PdfActionsManager { class PdfActionsManager {
pageDirection; pageDirection;
pagesContainer; pagesContainer;
constructor(id) { constructor(id) {
this.pagesContainer = document.getElementById(id); this.pagesContainer = document.getElementById(id);
this.pageDirection = document.documentElement.getAttribute("lang-direction"); this.pageDirection = document.documentElement.getAttribute("lang-direction");
var styleElement = document.createElement('link'); var styleElement = document.createElement("link");
styleElement.rel = 'stylesheet'; styleElement.rel = "stylesheet";
styleElement.href = 'css/pdfActions.css' styleElement.href = "css/pdfActions.css";
document.head.appendChild(styleElement); document.head.appendChild(styleElement);
}
getPageContainer(element) {
var container = element;
while (!container.classList.contains("page-container")) {
container = container.parentNode;
} }
return container;
}
getPageContainer(element) { moveUpButtonCallback(e) {
var container = element var imgContainer = this.getPageContainer(e.target);
while (!container.classList.contains('page-container')) {
container = container.parentNode; const sibling = imgContainer.previousSibling;
} if (sibling) {
return container; this.movePageTo(imgContainer, sibling, true);
} }
}
moveUpButtonCallback(e) { moveDownButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target); var imgContainer = this.getPageContainer(e.target);
const sibling = imgContainer.nextSibling;
const sibling = imgContainer.previousSibling; if (sibling) {
if (sibling) { this.movePageTo(imgContainer, sibling.nextSibling, true);
this.movePageTo(imgContainer, sibling, true);
}
} }
}
moveDownButtonCallback(e) { rotateCCWButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target); var imgContainer = this.getPageContainer(e.target);
const sibling = imgContainer.nextSibling; const img = imgContainer.querySelector("img");
if (sibling) {
this.movePageTo(imgContainer, sibling.nextSibling, true);
}
};
rotateCCWButtonCallback(e) { this.rotateElement(img, -90);
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");
rotateCWButtonCallback(e) { this.rotateElement(img, 90);
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);
if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
deletePageButtonCallback(e) { filenameInput.disabled = true;
var imgContainer = this.getPageContainer(e.target); filenameInput.value = "";
this.pagesContainer.removeChild(imgContainer); filenameParagraph.innerText = "";
if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById('filename-input');
const filenameParagraph = document.getElementById('filename');
const downloadBtn = document.getElementById('export-button');
filenameInput.disabled = true; downloadBtn.disabled = true;
filenameInput.value = "";
filenameParagraph.innerText = "";
downloadBtn.disabled = true;
}
};
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) { insertFileButtonCallback(e) {
div.classList.add('pdf-actions_container'); var imgContainer = this.getPageContainer(e.target);
const leftDirection = this.pageDirection === 'rtl' ? 'right' : 'left' this.addPdfs(imgContainer);
const rightDirection = this.pageDirection === 'rtl' ? 'left' : 'right' }
const buttonContainer = document.createElement('div');
buttonContainer.classList.add("pdf-actions_button-container", "hide-on-drag"); setActions({ movePageTo, addPdfs, rotateElement }) {
this.movePageTo = movePageTo;
this.addPdfs = addPdfs;
this.rotateElement = rotateElement;
const moveUp = document.createElement('button'); this.moveUpButtonCallback = this.moveUpButtonCallback.bind(this);
moveUp.classList.add("pdf-actions_move-left-button","btn", "btn-secondary"); this.moveDownButtonCallback = this.moveDownButtonCallback.bind(this);
moveUp.innerHTML = `<i class="bi bi-arrow-${leftDirection}-short"></i>`; this.rotateCCWButtonCallback = this.rotateCCWButtonCallback.bind(this);
moveUp.onclick = this.moveUpButtonCallback; this.rotateCWButtonCallback = this.rotateCWButtonCallback.bind(this);
buttonContainer.appendChild(moveUp); this.deletePageButtonCallback = this.deletePageButtonCallback.bind(this);
this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this);
}
const moveDown = document.createElement('button'); adapt(div) {
moveDown.classList.add("pdf-actions_move-right-button","btn", "btn-secondary"); div.classList.add("pdf-actions_container");
moveDown.innerHTML = `<i class="bi bi-arrow-${rightDirection}-short"></i>`; const leftDirection = this.pageDirection === "rtl" ? "right" : "left";
moveDown.onclick = this.moveDownButtonCallback; const rightDirection = this.pageDirection === "rtl" ? "left" : "right";
buttonContainer.appendChild(moveDown); const buttonContainer = document.createElement("div");
const rotateCCW = document.createElement('button'); buttonContainer.classList.add("pdf-actions_button-container", "hide-on-drag");
rotateCCW.classList.add("btn", "btn-secondary");
rotateCCW.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16"> const moveUp = document.createElement("button");
moveUp.classList.add("pdf-actions_move-left-button", "btn", "btn-secondary");
moveUp.innerHTML = `<i class="bi bi-arrow-${leftDirection}-short"></i>`;
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 = `<i class="bi bi-arrow-${rightDirection}-short"></i>`;
moveDown.onclick = this.moveDownButtonCallback;
buttonContainer.appendChild(moveDown);
const rotateCCW = document.createElement("button");
rotateCCW.classList.add("btn", "btn-secondary");
rotateCCW.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" /> <path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" /> <path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
</svg>`; </svg>`;
rotateCCW.onclick = this.rotateCCWButtonCallback; rotateCCW.onclick = this.rotateCCWButtonCallback;
buttonContainer.appendChild(rotateCCW); buttonContainer.appendChild(rotateCCW);
const rotateCW = document.createElement('button'); const rotateCW = document.createElement("button");
rotateCW.classList.add("btn", "btn-secondary"); rotateCW.classList.add("btn", "btn-secondary");
rotateCW.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> rotateCW.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" /> <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" /> <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
</svg>`; </svg>`;
rotateCW.onclick = this.rotateCWButtonCallback; rotateCW.onclick = this.rotateCWButtonCallback;
buttonContainer.appendChild(rotateCW); buttonContainer.appendChild(rotateCW);
const deletePage = document.createElement('button'); const deletePage = document.createElement("button");
deletePage.classList.add("btn", "btn-danger"); deletePage.classList.add("btn", "btn-danger");
deletePage.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> deletePage.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/> <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/> <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
</svg>`; </svg>`;
deletePage.onclick = this.deletePageButtonCallback; deletePage.onclick = this.deletePageButtonCallback;
buttonContainer.appendChild(deletePage); buttonContainer.appendChild(deletePage);
div.appendChild(buttonContainer); div.appendChild(buttonContainer);
const insertFileButtonContainer = document.createElement('div'); const insertFileButtonContainer = document.createElement("div");
insertFileButtonContainer.classList.add( insertFileButtonContainer.classList.add(
"pdf-actions_insert-file-button-container", "pdf-actions_insert-file-button-container",
leftDirection, leftDirection,
`align-center-${leftDirection}`); `align-center-${leftDirection}`,
);
const insertFileButton = document.createElement('button'); const insertFileButton = document.createElement("button");
insertFileButton.classList.add("btn", "btn-primary", "pdf-actions_insert-file-button"); insertFileButton.classList.add("btn", "btn-primary", "pdf-actions_insert-file-button");
insertFileButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16"> insertFileButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16">
<path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/> <path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/>
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
</svg>`; </svg>`;
insertFileButton.onclick = this.insertFileButtonCallback; insertFileButton.onclick = this.insertFileButtonCallback;
insertFileButtonContainer.appendChild(insertFileButton); insertFileButtonContainer.appendChild(insertFileButton);
div.appendChild(insertFileButtonContainer); div.appendChild(insertFileButtonContainer);
// add this button to every element, but only show it on the last one :D // add this button to every element, but only show it on the last one :D
const insertFileButtonRightContainer = document.createElement('div'); const insertFileButtonRightContainer = document.createElement("div");
insertFileButtonRightContainer.classList.add( insertFileButtonRightContainer.classList.add(
"pdf-actions_insert-file-button-container", "pdf-actions_insert-file-button-container",
rightDirection, rightDirection,
`align-center-${rightDirection}`); `align-center-${rightDirection}`,
);
const insertFileButtonRight = document.createElement('button'); const insertFileButtonRight = document.createElement("button");
insertFileButtonRight.classList.add("btn", "btn-primary", "pdf-actions_insert-file-button"); insertFileButtonRight.classList.add("btn", "btn-primary", "pdf-actions_insert-file-button");
insertFileButtonRight.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16"> insertFileButtonRight.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-plus" viewBox="0 0 16 16">
<path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/> <path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/>
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
insertFileButtonRight</svg>`; insertFileButtonRight</svg>`;
insertFileButtonRight.onclick = () => addPdfs(); insertFileButtonRight.onclick = () => addPdfs();
insertFileButtonRightContainer.appendChild(insertFileButtonRight); insertFileButtonRightContainer.appendChild(insertFileButtonRight);
div.appendChild(insertFileButtonRightContainer); div.appendChild(insertFileButtonRightContainer);
const adaptPageNumber = (pageNumber, div) => { const adaptPageNumber = (pageNumber, div) => {
const pageNumberElement = document.createElement('span'); const pageNumberElement = document.createElement("span");
pageNumberElement.classList.add('page-number'); pageNumberElement.classList.add("page-number");
pageNumberElement.textContent = pageNumber; pageNumberElement.textContent = pageNumber;
div.insertBefore(pageNumberElement, div.firstChild); div.insertBefore(pageNumberElement, div.firstChild);
}; };
div.addEventListener('mouseenter', () => { div.addEventListener("mouseenter", () => {
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1; const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
adaptPageNumber(pageNumber, div); adaptPageNumber(pageNumber, div);
}); });
div.addEventListener('mouseleave', () => { div.addEventListener("mouseleave", () => {
const pageNumberElement = div.querySelector('.page-number'); const pageNumberElement = div.querySelector(".page-number");
if (pageNumberElement) { if (pageNumberElement) {
div.removeChild(pageNumberElement); div.removeChild(pageNumberElement);
} }
}); });
return div; return div;
} }
} }
export default PdfActionsManager; export default PdfActionsManager;

View File

@ -1,285 +1,282 @@
class PdfContainer { class PdfContainer {
fileName; fileName;
pagesContainer; pagesContainer;
pagesContainerWrapper; pagesContainerWrapper;
pdfAdapters; pdfAdapters;
downloadLink; downloadLink;
constructor(id, wrapperId, pdfAdapters) { constructor(id, wrapperId, pdfAdapters) {
this.pagesContainer = document.getElementById(id) this.pagesContainer = document.getElementById(id);
this.pagesContainerWrapper = document.getElementById(wrapperId); this.pagesContainerWrapper = document.getElementById(wrapperId);
this.downloadLink = null; this.downloadLink = null;
this.movePageTo = this.movePageTo.bind(this); this.movePageTo = this.movePageTo.bind(this);
this.addPdfs = this.addPdfs.bind(this); this.addPdfs = this.addPdfs.bind(this);
this.addPdfsFromFiles = this.addPdfsFromFiles.bind(this); this.addPdfsFromFiles = this.addPdfsFromFiles.bind(this);
this.rotateElement = this.rotateElement.bind(this); this.rotateElement = this.rotateElement.bind(this);
this.rotateAll = this.rotateAll.bind(this); this.rotateAll = this.rotateAll.bind(this);
this.exportPdf = this.exportPdf.bind(this); this.exportPdf = this.exportPdf.bind(this);
this.updateFilename = this.updateFilename.bind(this); this.updateFilename = this.updateFilename.bind(this);
this.setDownloadAttribute = this.setDownloadAttribute.bind(this); this.setDownloadAttribute = this.setDownloadAttribute.bind(this);
this.preventIllegalChars = this.preventIllegalChars.bind(this); this.preventIllegalChars = this.preventIllegalChars.bind(this);
this.pdfAdapters = pdfAdapters; this.pdfAdapters = pdfAdapters;
this.pdfAdapters.forEach(adapter => { this.pdfAdapters.forEach((adapter) => {
adapter.setActions({ adapter.setActions({
movePageTo: this.movePageTo, movePageTo: this.movePageTo,
addPdfs: this.addPdfs, addPdfs: this.addPdfs,
rotateElement: this.rotateElement, rotateElement: this.rotateElement,
updateFilename: this.updateFilename updateFilename: this.updateFilename,
}) });
}) });
window.addPdfs = this.addPdfs; window.addPdfs = this.addPdfs;
window.exportPdf = this.exportPdf; window.exportPdf = this.exportPdf;
window.rotateAll = this.rotateAll; window.rotateAll = this.rotateAll;
const filenameInput = document.getElementById('filename-input'); const filenameInput = document.getElementById("filename-input");
const downloadBtn = document.getElementById('export-button'); const downloadBtn = document.getElementById("export-button");
filenameInput.onkeyup = this.updateFilename; filenameInput.onkeyup = this.updateFilename;
filenameInput.onkeydown = this.preventIllegalChars; filenameInput.onkeydown = this.preventIllegalChars;
filenameInput.disabled = false; filenameInput.disabled = false;
filenameInput.innerText = ""; filenameInput.innerText = "";
downloadBtn.disabled = true; downloadBtn.disabled = true;
}
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);
} }
movePageTo(startElement, endElement, scrollTo = false) { if (scrollTo) {
const childArray = Array.from(this.pagesContainer.childNodes); const { width } = startElement.getBoundingClientRect();
const startIndex = childArray.indexOf(startElement); const vector = endIndex !== -1 && startIndex > endIndex ? 0 - width : width;
const endIndex = childArray.indexOf(endElement);
this.pagesContainer.removeChild(startElement); this.pagesContainerWrapper.scroll({
if(!endElement) { left: this.pagesContainerWrapper.scrollLeft + vector,
this.pagesContainer.append(startElement); });
}
}
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.addPdfsFromFiles(files, nextSiblingElement);
this.updateFilename(files ? files[0].name : "");
};
input.click();
}
async addPdfsFromFiles(files, nextSiblingElement) {
this.fileName = files[0].name;
for (var i = 0; i < files.length; i++) {
await this.addPdfFile(files[i], nextSiblingElement);
}
document.querySelectorAll(".enable-on-file").forEach((element) => {
element.disabled = false;
});
}
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) {
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs/pdf.worker.js";
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 { } else {
this.pagesContainer.insertBefore(startElement, endElement); canvas.width = page.view[2];
canvas.height = page.view[3];
} }
if(scrollTo) { // render the page onto the canvas
const { width } = startElement.getBoundingClientRect(); var renderContext = {
const vector = (endIndex !== -1 && startIndex > endIndex) canvasContext: canvas.getContext("2d"),
? 0-width viewport: page.getViewport({ scale: 1 }),
: 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.addPdfsFromFiles(files, nextSiblingElement);
this.updateFilename(files ? files[0].name : "");
}
input.click();
}
async addPdfsFromFiles(files, nextSiblingElement) {
this.fileName = files[0].name;
for (var i=0; i < files.length; i++) {
await this.addPdfFile(files[i], nextSiblingElement);
}
document.querySelectorAll(".enable-on-file").forEach(element => {
element.disabled = false;
});
}
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) {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
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();
}
}; };
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, {
ignoreEncryption: true,
});
return pdfDoc;
}
rotateAll(deg) {
for (var i = 0; i < this.pagesContainer.childNodes.length; i++) {
const img = this.pagesContainer.childNodes[i].querySelector("img");
if (!img) continue;
this.rotateElement(img, deg);
}
}
async exportPdf() {
const pdfDoc = await PDFLib.PDFDocument.create();
const pageContainers = this.pagesContainer.querySelectorAll(".page-container"); // Select all .page-container elements
for (var i = 0; i < pageContainers.length; i++) {
const img = pageContainers[i].querySelector("img"); // Find the img element within each .page-container
if (!img) continue;
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]);
const page = pages[0];
const rotation = img.style.rotate;
if (rotation) {
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ""));
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle));
}
pdfDoc.addPage(page);
}
const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" });
const url = URL.createObjectURL(pdfBlob);
const downloadOption = localStorage.getItem("downloadOption");
const filenameInput = document.getElementById("filename-input");
let inputArr = filenameInput.value.split(".");
if (inputArr !== null && inputArr !== undefined && inputArr.length > 0) {
inputArr = inputArr.filter((n) => n); // remove all empty strings, nulls or undefined
if (inputArr.length > 1) {
inputArr.pop(); // remove right part after last dot
}
filenameInput.value = inputArr.join("");
this.fileName = filenameInput.value;
} }
async toPdfLib(objectUrl) { if (!filenameInput.value.includes(".pdf")) {
const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer()); filenameInput.value = filenameInput.value + ".pdf";
const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes, { ignoreEncryption: true }); this.fileName = filenameInput.value;
return pdfDoc;
} }
if (downloadOption === "sameWindow") {
// Open the file in the same window
window.location.href = url;
} else if (downloadOption === "newWindow") {
// Open the file in a new window
window.open(url, "_blank");
} else {
// Download the file
this.downloadLink = document.createElement("a");
this.downloadLink.id = "download-link";
this.downloadLink.href = url;
// downloadLink.download = this.fileName ? this.fileName : 'managed.pdf';
// downloadLink.download = this.fileName;
this.downloadLink.setAttribute("download", this.fileName ? this.fileName : "managed.pdf");
this.downloadLink.setAttribute("target", "_blank");
this.downloadLink.onclick = this.setDownloadAttribute;
this.downloadLink.click();
}
}
setDownloadAttribute() {
this.downloadLink.setAttribute("download", this.fileName ? this.fileName : "managed.pdf");
}
rotateAll(deg) { updateFilename(fileName = "") {
for (var i=0; i<this.pagesContainer.childNodes.length; i++) { const filenameInput = document.getElementById("filename-input");
const img = this.pagesContainer.childNodes[i].querySelector("img"); const pagesContainer = document.getElementById("pages-container");
if (!img) continue; const downloadBtn = document.getElementById("export-button");
this.rotateElement(img, deg)
} downloadBtn.disabled = pagesContainer.childElementCount === 0;
if (!this.fileName) {
this.fileName = fileName;
} }
async exportPdf() { if (!filenameInput.value) {
const pdfDoc = await PDFLib.PDFDocument.create(); filenameInput.value = this.fileName;
const pageContainers = this.pagesContainer.querySelectorAll('.page-container'); // Select all .page-container elements
for (var i = 0; i < pageContainers.length; i++) {
const img = pageContainers[i].querySelector("img"); // Find the img element within each .page-container
if (!img) continue;
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx])
const page = pages[0];
const rotation = img.style.rotate;
if (rotation) {
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ''));
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle))
}
pdfDoc.addPage(page);
}
const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(pdfBlob);
const downloadOption = localStorage.getItem('downloadOption');
const filenameInput = document.getElementById('filename-input');
let inputArr = filenameInput.value.split('.');
if (inputArr !== null && inputArr !== undefined && inputArr.length > 0) {
inputArr = inputArr.filter(n => n); // remove all empty strings, nulls or undefined
if (inputArr.length > 1) {
inputArr.pop(); // remove right part after last dot
}
filenameInput.value = inputArr.join('');
this.fileName = filenameInput.value;
}
if (!filenameInput.value.includes('.pdf')) {
filenameInput.value = filenameInput.value + '.pdf';
this.fileName = filenameInput.value;
}
if (downloadOption === 'sameWindow') {
// Open the file in the same window
window.location.href = url;
} else if (downloadOption === 'newWindow') {
// Open the file in a new window
window.open(url, '_blank');
} else {
// Download the file
this.downloadLink = document.createElement('a');
this.downloadLink.id = 'download-link';
this.downloadLink.href = url;
// downloadLink.download = this.fileName ? this.fileName : 'managed.pdf';
// downloadLink.download = this.fileName;
this.downloadLink.setAttribute('download', this.fileName ? this.fileName : 'managed.pdf');
this.downloadLink.setAttribute('target', '_blank');
this.downloadLink.onclick = this.setDownloadAttribute;
this.downloadLink.click();
}
} }
}
setDownloadAttribute() { preventIllegalChars(e) {
this.downloadLink.setAttribute("download", this.fileName ? this.fileName : 'managed.pdf'); // const filenameInput = document.getElementById('filename-input');
} //
// filenameInput.value = filenameInput.value.replace('.pdf', '');
updateFilename(fileName = "") { //
const filenameInput = document.getElementById('filename-input'); // // prevent .
const pagesContainer = document.getElementById('pages-container'); // if (filenameInput.value.includes('.')) {
const downloadBtn = document.getElementById('export-button'); // filenameInput.value.replace('.','');
// }
downloadBtn.disabled = pagesContainer.childElementCount === 0 }
if (!this.fileName) {
this.fileName = fileName;
}
if (!filenameInput.value) {
filenameInput.value = this.fileName;
}
}
preventIllegalChars(e) {
// const filenameInput = document.getElementById('filename-input');
//
// filenameInput.value = filenameInput.value.replace('.pdf', '');
//
// // prevent .
// if (filenameInput.value.includes('.')) {
// filenameInput.value.replace('.','');
// }
}
} }
export default PdfContainer; export default PdfContainer;

View File

@ -1,94 +1,95 @@
class FileDragManager { class FileDragManager {
overlay; overlay;
dragCounter; dragCounter;
updateFilename; updateFilename;
constructor(cb = null) { constructor(cb = null) {
this.dragCounter = 0; this.dragCounter = 0;
this.setCallback(cb); this.setCallback(cb);
// Prevent default behavior for drag events // Prevent default behavior for drag events
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false);
}); });
function preventDefaults(e) { function preventDefaults(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}
this.dragenterListener = this.dragenterListener.bind(this);
this.dragleaveListener = this.dragleaveListener.bind(this);
this.dropListener = this.dropListener.bind(this);
document.body.addEventListener('dragenter', this.dragenterListener);
document.body.addEventListener('dragleave', this.dragleaveListener);
// Add drop event listener
document.body.addEventListener('drop', this.dropListener);
} }
setActions({ updateFilename }) { this.dragenterListener = this.dragenterListener.bind(this);
this.updateFilename = updateFilename; this.dragleaveListener = this.dragleaveListener.bind(this);
this.dropListener = this.dropListener.bind(this);
document.body.addEventListener("dragenter", this.dragenterListener);
document.body.addEventListener("dragleave", this.dragleaveListener);
// Add drop event listener
document.body.addEventListener("drop", this.dropListener);
}
setActions({ updateFilename }) {
this.updateFilename = updateFilename;
}
setCallback(cb) {
if (cb) {
this.callback = cb;
} else {
this.callback = (files) => console.warn("FileDragManager not set");
} }
}
setCallback(cb) { dragenterListener() {
if (cb) { this.dragCounter++;
this.callback = cb; if (!this.overlay) {
} else { // Create and show the overlay
this.callback = (files) => console.warn("FileDragManager not set"); this.overlay = document.createElement("div");
} this.overlay.style.position = "fixed";
this.overlay.style.top = 0;
this.overlay.style.left = 0;
this.overlay.style.width = "100%";
this.overlay.style.height = "100%";
this.overlay.style.background = "rgba(0, 0, 0, 0.5)";
this.overlay.style.color = "#fff";
this.overlay.style.zIndex = "1000";
this.overlay.style.display = "flex";
this.overlay.style.alignItems = "center";
this.overlay.style.justifyContent = "center";
this.overlay.style.pointerEvents = "none";
this.overlay.innerHTML = "<p>Drop files anywhere to upload</p>";
document.getElementById("content-wrap").appendChild(this.overlay);
} }
}
dragenterListener() { dragleaveListener() {
this.dragCounter++; this.dragCounter--;
if (!this.overlay) { if (this.dragCounter === 0) {
// Create and show the overlay // Hide and remove the overlay
this.overlay = document.createElement('div'); if (this.overlay) {
this.overlay.style.position = 'fixed'; this.overlay.remove();
this.overlay.style.top = 0; this.overlay = null;
this.overlay.style.left = 0; }
this.overlay.style.width = '100%'; }
this.overlay.style.height = '100%'; }
this.overlay.style.background = 'rgba(0, 0, 0, 0.5)';
this.overlay.style.color = '#fff'; dropListener(e) {
this.overlay.style.zIndex = '1000'; const dt = e.dataTransfer;
this.overlay.style.display = 'flex'; const files = dt.files;
this.overlay.style.alignItems = 'center'; this.callback(files)
this.overlay.style.justifyContent = 'center'; .catch((err) => {
this.overlay.style.pointerEvents = 'none'; console.error(err);
this.overlay.innerHTML = '<p>Drop files anywhere to upload</p>'; //maybe
document.getElementById('content-wrap').appendChild(this.overlay); })
.finally(() => {
// Hide and remove the overlay
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
} }
};
dragleaveListener() { this.updateFilename(files ? files[0].name : "");
this.dragCounter--; });
if (this.dragCounter === 0) { }
// Hide and remove the overlay
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
}
}
};
dropListener(e) {
const dt = e.dataTransfer;
const files = dt.files;
this.callback(files).catch((err) => {
console.error(err);
//maybe
}).finally(() => {
// Hide and remove the overlay
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
}
this.updateFilename(files ? files[0].name : "");
});
};
} }
export default FileDragManager; export default FileDragManager;

View File

@ -1,35 +1,34 @@
const scrollDivHorizontally = (id) => { const scrollDivHorizontally = (id) => {
var scrollDelta = 0; // variable to store the accumulated scroll delta var scrollDelta = 0; // variable to store the accumulated scroll delta
var isScrolling = false; // variable to track if scroll is already in progress var isScrolling = false; // variable to track if scroll is already in progress
const divToScrollHorizontally = document.getElementById(id) const divToScrollHorizontally = document.getElementById(id);
function scrollLoop() { function scrollLoop() {
// Scroll the div horizontally by a fraction of the accumulated scroll delta // Scroll the div horizontally by a fraction of the accumulated scroll delta
divToScrollHorizontally.scrollLeft += scrollDelta * 0.1; divToScrollHorizontally.scrollLeft += scrollDelta * 0.1;
// Reduce the accumulated scroll delta by a fraction // Reduce the accumulated scroll delta by a fraction
scrollDelta *= 0.9; scrollDelta *= 0.9;
// If scroll delta is still significant, continue the scroll loop // If scroll delta is still significant, continue the scroll loop
if (Math.abs(scrollDelta) > 0.1) { if (Math.abs(scrollDelta) > 0.1) {
requestAnimationFrame(scrollLoop); requestAnimationFrame(scrollLoop);
} else { } else {
isScrolling = false; // Reset scroll in progress flag isScrolling = false; // Reset scroll in progress flag
}
} }
}
divToScrollHorizontally.addEventListener("wheel", function (e) {
e.preventDefault(); // prevent default mousewheel behavior
divToScrollHorizontally.addEventListener("wheel", function(e) { // Accumulate the horizontal scroll delta
e.preventDefault(); // prevent default mousewheel behavior scrollDelta -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY;
// Accumulate the horizontal scroll delta // If scroll is not already in progress, start the scroll loop
scrollDelta -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY; if (!isScrolling) {
isScrolling = true;
// If scroll is not already in progress, start the scroll loop requestAnimationFrame(scrollLoop);
if (!isScrolling) { }
isScrolling = true; });
requestAnimationFrame(scrollLoop); };
}
});
}
export default scrollDivHorizontally; export default scrollDivHorizontally;

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +1,76 @@
// Toggle search bar when the search icon is clicked // Toggle search bar when the search icon is clicked
document.querySelector('#search-icon').addEventListener('click', function(e) { document.querySelector("#search-icon").addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
var searchBar = document.querySelector('#navbarSearch'); var searchBar = document.querySelector("#navbarSearch");
searchBar.classList.toggle('show'); searchBar.classList.toggle("show");
}); });
window.onload = function() { window.onload = function () {
var items = document.querySelectorAll('.dropdown-item, .nav-link'); var items = document.querySelectorAll(".dropdown-item, .nav-link");
var dummyContainer = document.createElement('div'); var dummyContainer = document.createElement("div");
dummyContainer.style.position = 'absolute'; dummyContainer.style.position = "absolute";
dummyContainer.style.visibility = 'hidden'; dummyContainer.style.visibility = "hidden";
dummyContainer.style.whiteSpace = 'nowrap'; // Ensure we measure full width dummyContainer.style.whiteSpace = "nowrap"; // Ensure we measure full width
document.body.appendChild(dummyContainer); document.body.appendChild(dummyContainer);
var maxWidth = 0; var maxWidth = 0;
items.forEach(function(item) { items.forEach(function (item) {
var clone = item.cloneNode(true); var clone = item.cloneNode(true);
dummyContainer.appendChild(clone); dummyContainer.appendChild(clone);
var width = clone.offsetWidth; var width = clone.offsetWidth;
if (width > maxWidth) { if (width > maxWidth) {
maxWidth = width; maxWidth = width;
} }
dummyContainer.removeChild(clone); dummyContainer.removeChild(clone);
}); });
document.body.removeChild(dummyContainer); document.body.removeChild(dummyContainer);
// Store max width for later use // Store max width for later use
window.navItemMaxWidth = maxWidth; window.navItemMaxWidth = maxWidth;
}; };
// Show search results as user types in search box // Show search results as user types in search box
document.querySelector('#navbarSearchInput').addEventListener('input', function(e) { document.querySelector("#navbarSearchInput").addEventListener("input", function (e) {
var searchText = e.target.value.toLowerCase(); var searchText = e.target.value.toLowerCase();
var items = document.querySelectorAll('.dropdown-item, .nav-link'); var items = document.querySelectorAll(".dropdown-item, .nav-link");
var resultsBox = document.querySelector('#searchResults'); var resultsBox = document.querySelector("#searchResults");
// Clear any previous results // Clear any previous results
resultsBox.innerHTML = ''; resultsBox.innerHTML = "";
items.forEach(function(item) { items.forEach(function (item) {
var titleElement = item.querySelector('.icon-text'); var titleElement = item.querySelector(".icon-text");
var iconElement = item.querySelector('.icon'); var iconElement = item.querySelector(".icon");
var itemHref = item.getAttribute('href'); var itemHref = item.getAttribute("href");
var tags = item.getAttribute('data-bs-tags') || ""; // If no tags, default to empty string var tags = item.getAttribute("data-bs-tags") || ""; // If no tags, default to empty string
if (titleElement && iconElement && itemHref !== '#') { if (titleElement && iconElement && itemHref !== "#") {
var title = titleElement.innerText; var title = titleElement.innerText;
if ((title.toLowerCase().indexOf(searchText) !== -1 || tags.toLowerCase().indexOf(searchText) !== -1) && !resultsBox.querySelector(`a[href="${item.getAttribute('href')}"]`)) { if (
var result = document.createElement('a'); (title.toLowerCase().indexOf(searchText) !== -1 || tags.toLowerCase().indexOf(searchText) !== -1) &&
result.href = itemHref; !resultsBox.querySelector(`a[href="${item.getAttribute("href")}"]`)
result.classList.add('dropdown-item'); ) {
var result = document.createElement("a");
result.href = itemHref;
result.classList.add("dropdown-item");
var resultIcon = document.createElement('img'); var resultIcon = document.createElement("img");
resultIcon.src = iconElement.src; resultIcon.src = iconElement.src;
resultIcon.alt = 'icon'; resultIcon.alt = "icon";
resultIcon.classList.add('icon'); resultIcon.classList.add("icon");
result.appendChild(resultIcon); result.appendChild(resultIcon);
var resultText = document.createElement('span'); var resultText = document.createElement("span");
resultText.textContent = title; resultText.textContent = title;
resultText.classList.add('icon-text'); resultText.classList.add("icon-text");
result.appendChild(resultText); result.appendChild(resultText);
resultsBox.appendChild(result); resultsBox.appendChild(result);
} }
} }
}); });
// Set the width of the search results box to the maximum width // Set the width of the search results box to the maximum width
resultsBox.style.width = window.navItemMaxWidth + 'px'; resultsBox.style.width = window.navItemMaxWidth + "px";
}); });

View File

@ -1,42 +1,33 @@
// Get the download option from local storage, or set it to 'sameWindow' if it doesn't exist // Get the download option from local storage, or set it to 'sameWindow' if it doesn't exist
var downloadOption = localStorage.getItem('downloadOption') var downloadOption = localStorage.getItem("downloadOption") || "sameWindow";
|| 'sameWindow';
// Set the selected option in the dropdown // Set the selected option in the dropdown
document.getElementById('downloadOption').value = downloadOption; document.getElementById("downloadOption").value = downloadOption;
// Save the selected option to local storage when the dropdown value changes // Save the selected option to local storage when the dropdown value changes
document.getElementById('downloadOption').addEventListener( document.getElementById("downloadOption").addEventListener("change", function () {
'change', downloadOption = this.value;
function() { localStorage.setItem("downloadOption", downloadOption);
downloadOption = this.value;
localStorage.setItem('downloadOption',
downloadOption);
});
// Get the zipThreshold value from local storage, or set it to 0 if it doesn't exist
var zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4;
// Set the value of the slider and the display span
document.getElementById('zipThreshold').value = zipThreshold;
document.getElementById('zipThresholdValue').textContent = zipThreshold;
// Save the selected value to local storage when the slider value changes
document.getElementById('zipThreshold').addEventListener('input', function() {
zipThreshold = this.value;
document.getElementById('zipThresholdValue').textContent = zipThreshold;
localStorage.setItem('zipThreshold', zipThreshold);
}); });
// Get the zipThreshold value from local storage, or set it to 0 if it doesn't exist
var zipThreshold = parseInt(localStorage.getItem("zipThreshold"), 10) || 4;
var boredWaiting = localStorage.getItem('boredWaiting') || 'disabled'; // Set the value of the slider and the display span
document.getElementById('boredWaiting').checked = boredWaiting === 'enabled'; document.getElementById("zipThreshold").value = zipThreshold;
document.getElementById("zipThresholdValue").textContent = zipThreshold;
document.getElementById('boredWaiting').addEventListener('change', function() { // Save the selected value to local storage when the slider value changes
boredWaiting = this.checked ? 'enabled' : 'disabled'; document.getElementById("zipThreshold").addEventListener("input", function () {
localStorage.setItem('boredWaiting', boredWaiting); zipThreshold = this.value;
}); document.getElementById("zipThresholdValue").textContent = zipThreshold;
localStorage.setItem("zipThreshold", zipThreshold);
});
var boredWaiting = localStorage.getItem("boredWaiting") || "disabled";
document.getElementById("boredWaiting").checked = boredWaiting === "enabled";
document.getElementById("boredWaiting").addEventListener("change", function () {
boredWaiting = this.checked ? "enabled" : "disabled";
localStorage.setItem("boredWaiting", boredWaiting);
});

View File

@ -1,39 +1,38 @@
TabContainer = { TabContainer = {
initTabGroups() { initTabGroups() {
const groups = document.querySelectorAll(".tab-group"); const groups = document.querySelectorAll(".tab-group");
const unloadedGroups = [...groups].filter(g => !g.initialised); const unloadedGroups = [...groups].filter((g) => !g.initialised);
unloadedGroups.forEach(group => { unloadedGroups.forEach((group) => {
const containers = group.querySelectorAll(".tab-container"); const containers = group.querySelectorAll(".tab-container");
const tabTitles = [...containers].map(c => c.getAttribute("title")); const tabTitles = [...containers].map((c) => c.getAttribute("title"));
const tabList = document.createElement("div"); const tabList = document.createElement("div");
tabList.classList.add("tab-buttons"); tabList.classList.add("tab-buttons");
tabTitles.forEach(title => { tabTitles.forEach((title) => {
const tabButton = document.createElement("button"); const tabButton = document.createElement("button");
tabButton.innerHTML = title; tabButton.innerHTML = title;
tabButton.onclick = e => { tabButton.onclick = (e) => {
this.setActiveTab(e.target); this.setActiveTab(e.target);
} };
tabList.appendChild(tabButton); tabList.appendChild(tabButton);
}); });
group.prepend(tabList); group.prepend(tabList);
this.setActiveTab(tabList.firstChild); this.setActiveTab(tabList.firstChild);
group.initialised = true; group.initialised = true;
}); });
}, },
setActiveTab(tabButton) { setActiveTab(tabButton) {
const group = tabButton.closest(".tab-group") const group = tabButton.closest(".tab-group");
group.querySelectorAll(".active").forEach(el => el.classList.remove("active")); group.querySelectorAll(".active").forEach((el) => el.classList.remove("active"));
tabButton.classList.add("active"); tabButton.classList.add("active");
group.querySelector(`[title="${tabButton.innerHTML}"]`).classList.add("active"); group.querySelector(`[title="${tabButton.innerHTML}"]`).classList.add("active");
}, },
} };
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
TabContainer.initTabGroups(); TabContainer.initTabGroups();
}) });

View File

@ -116,7 +116,6 @@
top: 0; top: 0;
} }
:root { :root {
--annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>"); --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
--input-focus-border-color: Highlight; --input-focus-border-color: Highlight;
@ -139,9 +138,7 @@
.annotationLayer .textWidgetAnnotation :is(input, textarea):required, .annotationLayer .textWidgetAnnotation :is(input, textarea):required,
.annotationLayer .choiceWidgetAnnotation select:required, .annotationLayer .choiceWidgetAnnotation select:required,
.annotationLayer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required {
.buttonWidgetAnnotation:is(.checkBox, .radioButton)
input:required {
outline: 1.5px solid selectedItem; outline: 1.5px solid selectedItem;
} }
@ -228,9 +225,7 @@
height: 100%; height: 100%;
} }
.annotationLayer .annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) > a:hover {
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
> a:hover {
opacity: 0.2; opacity: 0.2;
background-color: rgba(255, 255, 0, 1); background-color: rgba(255, 255, 0, 1);
box-shadow: 0 2px 10px rgba(255, 255, 0, 1); box-shadow: 0 2px 10px rgba(255, 255, 0, 1);
@ -268,9 +263,7 @@
.annotationLayer .textWidgetAnnotation :is(input, textarea):required, .annotationLayer .textWidgetAnnotation :is(input, textarea):required,
.annotationLayer .choiceWidgetAnnotation select:required, .annotationLayer .choiceWidgetAnnotation select:required,
.annotationLayer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required {
.buttonWidgetAnnotation:is(.checkBox, .radioButton)
input:required {
outline: 1.5px solid red; outline: 1.5px solid red;
} }
@ -288,9 +281,7 @@ input:required {
.annotationLayer .textWidgetAnnotation :is(input, textarea)[disabled], .annotationLayer .textWidgetAnnotation :is(input, textarea)[disabled],
.annotationLayer .choiceWidgetAnnotation select[disabled], .annotationLayer .choiceWidgetAnnotation select[disabled],
.annotationLayer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input[disabled] {
.buttonWidgetAnnotation:is(.checkBox, .radioButton)
input[disabled] {
background: none; background: none;
border: 2px solid var(--input-disabled-border-color); border: 2px solid var(--input-disabled-border-color);
cursor: not-allowed; cursor: not-allowed;
@ -298,9 +289,7 @@ input[disabled] {
.annotationLayer .textWidgetAnnotation :is(input, textarea):hover, .annotationLayer .textWidgetAnnotation :is(input, textarea):hover,
.annotationLayer .choiceWidgetAnnotation select:hover, .annotationLayer .choiceWidgetAnnotation select:hover,
.annotationLayer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:hover {
.buttonWidgetAnnotation:is(.checkBox, .radioButton)
input:hover {
border: 2px solid var(--input-hover-border-color); border: 2px solid var(--input-hover-border-color);
} }
@ -489,7 +478,6 @@ input:hover {
z-index: -1; z-index: -1;
} }
:root { :root {
--xfa-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>"); --xfa-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
--xfa-focus-outline: auto; --xfa-focus-outline: auto;
@ -827,10 +815,7 @@ input:hover {
--freetext-padding: 2px; --freetext-padding: 2px;
--resizer-bg-color: var(--outline-color); --resizer-bg-color: var(--outline-color);
--resizer-size: 6px; --resizer-size: 6px;
--resizer-shift: calc( --resizer-shift: calc(0px - (var(--outline-width) + var(--resizer-size)) / 2 - var(--outline-around-width));
0px - (var(--outline-width) + var(--resizer-size)) / 2 -
var(--outline-around-width)
);
--editorFreeText-editing-cursor: text; --editorFreeText-editing-cursor: text;
--editorInk-editing-cursor: url(../images/cursor-editorInk.svg) 0 16, pointer; --editorInk-editing-cursor: url(../images/cursor-editorInk.svg) 0 16, pointer;
@ -853,8 +838,7 @@ input:hover {
@media (-webkit-min-device-pixel-ratio: 1.1), (min-resolution: 1.1dppx) { @media (-webkit-min-device-pixel-ratio: 1.1), (min-resolution: 1.1dppx) {
:root { :root {
--editorFreeText-editing-cursor: url(../images/cursor-editorFreeText.svg) 0 16, --editorFreeText-editing-cursor: url(../images/cursor-editorFreeText.svg) 0 16, text;
text;
} }
} }
@ -1109,216 +1093,354 @@ input:hover {
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomRight, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.bottomRight,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomRight, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.bottomRight,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomRight, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.bottomRight,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomRight { :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.bottomRight {
cursor: nwse-resize; cursor: nwse-resize;
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomMiddle, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.bottomMiddle,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomMiddle, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.bottomMiddle,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomMiddle, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.bottomMiddle,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomMiddle { :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.bottomMiddle {
cursor: ns-resize; cursor: ns-resize;
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomLeft, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.bottomLeft,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomLeft, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.bottomLeft,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomLeft, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.bottomLeft,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomLeft { :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.bottomLeft {
cursor: nesw-resize; cursor: nesw-resize;
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleLeft, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.middleLeft,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleLeft, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.middleLeft,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleLeft, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.middleLeft,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleLeft { :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.middleLeft {
cursor: ew-resize; cursor: ew-resize;
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topLeft, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.topLeft,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomRight, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.bottomRight,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomRight, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.bottomRight,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomRight, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.bottomRight,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomRight { :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.bottomRight {
cursor: nesw-resize; cursor: nesw-resize;
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topMiddle, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.topMiddle,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomMiddle, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.bottomMiddle,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomMiddle, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.bottomMiddle,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomMiddle, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.bottomMiddle,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomMiddle { :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.bottomMiddle {
cursor: ew-resize; cursor: ew-resize;
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topRight, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.topRight,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomLeft, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.bottomLeft,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomLeft, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.bottomLeft,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomLeft, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.bottomLeft,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomLeft { :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.bottomLeft {
cursor: nwse-resize; cursor: nwse-resize;
} }
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleRight, :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.middleRight,
.annotationEditorLayer[data-main-rotation="0"] .annotationEditorLayer[data-main-rotation="0"]
:is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleLeft, :is([data-editor-rotation="90"], [data-editor-rotation="270"])
> .resizers
> .resizer.middleLeft,
.annotationEditorLayer[data-main-rotation="90"] .annotationEditorLayer[data-main-rotation="90"]
:is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleLeft, :is([data-editor-rotation="0"], [data-editor-rotation="180"])
> .resizers
> .resizer.middleLeft,
.annotationEditorLayer[data-main-rotation="180"] .annotationEditorLayer[data-main-rotation="180"]
:is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleLeft, :is([data-editor-rotation="270"], [data-editor-rotation="90"])
> .resizers
> .resizer.middleLeft,
.annotationEditorLayer[data-main-rotation="270"] .annotationEditorLayer[data-main-rotation="270"]
:is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleLeft { :is([data-editor-rotation="180"], [data-editor-rotation="0"])
> .resizers
> .resizer.middleLeft {
cursor: ns-resize; cursor: ns-resize;
} }
.annotationEditorLayer .annotationEditorLayer
:is( :is(
[data-main-rotation="0"] [data-editor-rotation="90"], [data-main-rotation="0"] [data-editor-rotation="90"],
[data-main-rotation="90"] [data-editor-rotation="0"], [data-main-rotation="90"] [data-editor-rotation="0"],
[data-main-rotation="180"] [data-editor-rotation="270"], [data-main-rotation="180"] [data-editor-rotation="270"],
[data-main-rotation="270"] [data-editor-rotation="180"] [data-main-rotation="270"] [data-editor-rotation="180"]
) .altText { )
.altText {
rotate: 270deg; rotate: 270deg;
} }
[dir="ltr"] .annotationEditorLayer [dir="ltr"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="90"], :is(
[data-main-rotation="90"] [data-editor-rotation="0"], [data-main-rotation="0"] [data-editor-rotation="90"],
[data-main-rotation="180"] [data-editor-rotation="270"], [data-main-rotation="90"] [data-editor-rotation="0"],
[data-main-rotation="270"] [data-editor-rotation="180"] [data-main-rotation="180"] [data-editor-rotation="270"],
) .altText { [data-main-rotation="270"] [data-editor-rotation="180"]
)
.altText {
inset-inline-start: calc(100% - 8px); inset-inline-start: calc(100% - 8px);
} }
[dir="ltr"] .annotationEditorLayer [dir="ltr"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="90"], :is(
[data-main-rotation="90"] [data-editor-rotation="0"], [data-main-rotation="0"] [data-editor-rotation="90"],
[data-main-rotation="180"] [data-editor-rotation="270"], [data-main-rotation="90"] [data-editor-rotation="0"],
[data-main-rotation="270"] [data-editor-rotation="180"] [data-main-rotation="180"] [data-editor-rotation="270"],
) .altText.small { [data-main-rotation="270"] [data-editor-rotation="180"]
)
.altText.small {
inset-inline-start: calc(100% + 8px); inset-inline-start: calc(100% + 8px);
inset-block-start: 100%; inset-block-start: 100%;
} }
[dir="rtl"] .annotationEditorLayer [dir="rtl"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="90"], :is(
[data-main-rotation="90"] [data-editor-rotation="0"], [data-main-rotation="0"] [data-editor-rotation="90"],
[data-main-rotation="180"] [data-editor-rotation="270"], [data-main-rotation="90"] [data-editor-rotation="0"],
[data-main-rotation="270"] [data-editor-rotation="180"] [data-main-rotation="180"] [data-editor-rotation="270"],
) .altText { [data-main-rotation="270"] [data-editor-rotation="180"]
)
.altText {
inset-block-end: calc(100% - 8px); inset-block-end: calc(100% - 8px);
} }
[dir="rtl"] .annotationEditorLayer [dir="rtl"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="90"], :is(
[data-main-rotation="90"] [data-editor-rotation="0"], [data-main-rotation="0"] [data-editor-rotation="90"],
[data-main-rotation="180"] [data-editor-rotation="270"], [data-main-rotation="90"] [data-editor-rotation="0"],
[data-main-rotation="270"] [data-editor-rotation="180"] [data-main-rotation="180"] [data-editor-rotation="270"],
) .altText.small { [data-main-rotation="270"] [data-editor-rotation="180"]
)
.altText.small {
inset-inline-start: -8px; inset-inline-start: -8px;
inset-block-start: 0; inset-block-start: 0;
} }
.annotationEditorLayer .annotationEditorLayer
:is( :is(
[data-main-rotation="0"] [data-editor-rotation="180"], [data-main-rotation="0"] [data-editor-rotation="180"],
[data-main-rotation="90"] [data-editor-rotation="90"], [data-main-rotation="90"] [data-editor-rotation="90"],
[data-main-rotation="180"] [data-editor-rotation="0"], [data-main-rotation="180"] [data-editor-rotation="0"],
[data-main-rotation="270"] [data-editor-rotation="270"] [data-main-rotation="270"] [data-editor-rotation="270"]
) .altText { )
.altText {
rotate: 180deg; rotate: 180deg;
inset-block-end: calc(100% - 8px); inset-block-end: calc(100% - 8px);
@ -1326,64 +1448,74 @@ input:hover {
} }
.annotationEditorLayer .annotationEditorLayer
:is( :is(
[data-main-rotation="0"] [data-editor-rotation="180"], [data-main-rotation="0"] [data-editor-rotation="180"],
[data-main-rotation="90"] [data-editor-rotation="90"], [data-main-rotation="90"] [data-editor-rotation="90"],
[data-main-rotation="180"] [data-editor-rotation="0"], [data-main-rotation="180"] [data-editor-rotation="0"],
[data-main-rotation="270"] [data-editor-rotation="270"] [data-main-rotation="270"] [data-editor-rotation="270"]
) .altText.small { )
.altText.small {
inset-inline-start: 100%; inset-inline-start: 100%;
inset-block-start: -8px; inset-block-start: -8px;
} }
.annotationEditorLayer .annotationEditorLayer
:is( :is(
[data-main-rotation="0"] [data-editor-rotation="270"], [data-main-rotation="0"] [data-editor-rotation="270"],
[data-main-rotation="90"] [data-editor-rotation="180"], [data-main-rotation="90"] [data-editor-rotation="180"],
[data-main-rotation="180"] [data-editor-rotation="90"], [data-main-rotation="180"] [data-editor-rotation="90"],
[data-main-rotation="270"] [data-editor-rotation="0"] [data-main-rotation="270"] [data-editor-rotation="0"]
) .altText { )
.altText {
rotate: 90deg; rotate: 90deg;
} }
[dir="ltr"] .annotationEditorLayer [dir="ltr"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="270"], :is(
[data-main-rotation="90"] [data-editor-rotation="180"], [data-main-rotation="0"] [data-editor-rotation="270"],
[data-main-rotation="180"] [data-editor-rotation="90"], [data-main-rotation="90"] [data-editor-rotation="180"],
[data-main-rotation="270"] [data-editor-rotation="0"] [data-main-rotation="180"] [data-editor-rotation="90"],
) .altText { [data-main-rotation="270"] [data-editor-rotation="0"]
)
.altText {
inset-block-end: calc(100% - 8px); inset-block-end: calc(100% - 8px);
} }
[dir="ltr"] .annotationEditorLayer [dir="ltr"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="270"], :is(
[data-main-rotation="90"] [data-editor-rotation="180"], [data-main-rotation="0"] [data-editor-rotation="270"],
[data-main-rotation="180"] [data-editor-rotation="90"], [data-main-rotation="90"] [data-editor-rotation="180"],
[data-main-rotation="270"] [data-editor-rotation="0"] [data-main-rotation="180"] [data-editor-rotation="90"],
) .altText.small { [data-main-rotation="270"] [data-editor-rotation="0"]
)
.altText.small {
inset-inline-start: -8px; inset-inline-start: -8px;
inset-block-start: 0; inset-block-start: 0;
} }
[dir="rtl"] .annotationEditorLayer [dir="rtl"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="270"], :is(
[data-main-rotation="90"] [data-editor-rotation="180"], [data-main-rotation="0"] [data-editor-rotation="270"],
[data-main-rotation="180"] [data-editor-rotation="90"], [data-main-rotation="90"] [data-editor-rotation="180"],
[data-main-rotation="270"] [data-editor-rotation="0"] [data-main-rotation="180"] [data-editor-rotation="90"],
) .altText { [data-main-rotation="270"] [data-editor-rotation="0"]
)
.altText {
inset-inline-start: calc(100% - 8px); inset-inline-start: calc(100% - 8px);
} }
[dir="rtl"] .annotationEditorLayer [dir="rtl"]
:is( .annotationEditorLayer
[data-main-rotation="0"] [data-editor-rotation="270"], :is(
[data-main-rotation="90"] [data-editor-rotation="180"], [data-main-rotation="0"] [data-editor-rotation="270"],
[data-main-rotation="180"] [data-editor-rotation="90"], [data-main-rotation="90"] [data-editor-rotation="180"],
[data-main-rotation="270"] [data-editor-rotation="0"] [data-main-rotation="180"] [data-editor-rotation="90"],
) .altText.small { [data-main-rotation="270"] [data-editor-rotation="0"]
)
.altText.small {
inset-inline-start: calc(100% + 8px); inset-inline-start: calc(100% + 8px);
inset-block-start: 100%; inset-block-start: 100%;
} }
@ -1421,7 +1553,6 @@ input:hover {
} }
.altText.small { .altText.small {
inset-block-end: unset; inset-block-end: unset;
inset-inline-start: 0; inset-inline-start: 0;
inset-block-start: calc(100% + 8px); inset-block-start: calc(100% + 8px);
@ -1515,7 +1646,6 @@ input:hover {
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.altText .tooltip.show { .altText .tooltip.show {
--alt-text-tooltip-bg: #1c1b22; --alt-text-tooltip-bg: #1c1b22;
--alt-text-tooltip-fg: #fbfbfe; --alt-text-tooltip-fg: #fbfbfe;
@ -1524,7 +1654,6 @@ input:hover {
} }
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
.altText .tooltip.show { .altText .tooltip.show {
--alt-text-tooltip-bg: Canvas; --alt-text-tooltip-bg: Canvas;
--alt-text-tooltip-fg: CanvasText; --alt-text-tooltip-fg: CanvasText;
@ -1581,7 +1710,6 @@ input:hover {
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
#altTextDialog { #altTextDialog {
--dialog-bg-color: #1c1b22; --dialog-bg-color: #1c1b22;
--dialog-border-color: #1c1b22; --dialog-border-color: #1c1b22;
@ -1604,7 +1732,6 @@ input:hover {
} }
@media screen and (forced-colors: active) { @media screen and (forced-colors: active) {
#altTextDialog { #altTextDialog {
--dialog-bg-color: Canvas; --dialog-bg-color: Canvas;
--dialog-border-color: CanvasText; --dialog-border-color: CanvasText;
@ -2022,8 +2149,8 @@ input:hover {
--toolbar-border-color: rgba(184, 184, 184, 1); --toolbar-border-color: rgba(184, 184, 184, 1);
--toolbar-box-shadow: 0 1px 0 var(--toolbar-border-color); --toolbar-box-shadow: 0 1px 0 var(--toolbar-border-color);
--toolbar-border-bottom: none; --toolbar-border-bottom: none;
--toolbarSidebar-box-shadow: inset calc(-1px * var(--dir-factor)) 0 0 rgba(0, 0, 0, 0.25), --toolbarSidebar-box-shadow: inset calc(-1px * var(--dir-factor)) 0 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(0, 0, 0, 0.15),
0 1px 0 rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.1); 0 0 1px rgba(0, 0, 0, 0.1);
--toolbarSidebar-border-bottom: none; --toolbarSidebar-border-bottom: none;
--button-hover-color: rgba(221, 222, 223, 1); --button-hover-color: rgba(221, 222, 223, 1);
--toggled-btn-color: rgba(0, 0, 0, 1); --toggled-btn-color: rgba(0, 0, 0, 1);
@ -2319,8 +2446,7 @@ body {
font: message-box; font: message-box;
} }
:is(.toolbar, .editorParamsToolbar, .findbar, #sidebarContainer) :is(.toolbar, .editorParamsToolbar, .findbar, #sidebarContainer) :is(input, button, select),
:is(input, button, select),
.secondaryToolbar :is(input, button, a, select) { .secondaryToolbar :is(input, button, a, select) {
outline: none; outline: none;
font: message-box; font: message-box;
@ -2417,19 +2543,18 @@ body {
height: 100%; height: 100%;
width: calc(100% + 150px); width: calc(100% + 150px);
background: repeating-linear-gradient( background: repeating-linear-gradient(
135deg, 135deg,
var(--progressBar-blend-color) 0, var(--progressBar-blend-color) 0,
var(--progressBar-bg-color) 5px, var(--progressBar-bg-color) 5px,
var(--progressBar-bg-color) 45px, var(--progressBar-bg-color) 45px,
var(--progressBar-color) 55px, var(--progressBar-color) 55px,
var(--progressBar-color) 95px, var(--progressBar-color) 95px,
var(--progressBar-blend-color) 100px var(--progressBar-blend-color) 100px
); );
animation: progressIndeterminate 1s linear infinite; animation: progressIndeterminate 1s linear infinite;
} }
#outerContainer.sidebarResizing #outerContainer.sidebarResizing :is(#sidebarContainer, #viewerContainer, #loadingBar) {
:is(#sidebarContainer, #viewerContainer, #loadingBar) {
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */ /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
transition-duration: 0s; transition-duration: 0s;
} }
@ -2600,8 +2725,9 @@ body {
.doorHanger, .doorHanger,
.doorHangerRight { .doorHangerRight {
border-radius: 2px; border-radius: 2px;
box-shadow: 0 1px 5px var(--doorhanger-border-color), box-shadow:
0 0 0 1px var(--doorhanger-border-color); 0 1px 5px var(--doorhanger-border-color),
0 0 0 1px var(--doorhanger-border-color);
border: var(--doorhanger-border-color-whcm); border: var(--doorhanger-border-color-whcm);
} }
@ -3455,8 +3581,7 @@ dialog :link {
cursor: grab !important; cursor: grab !important;
} }
.grab-to-pan-grab .grab-to-pan-grab *:not(input):not(textarea):not(button):not(select):not(:link) {
*:not(input):not(textarea):not(button):not(select):not(:link) {
cursor: inherit !important; cursor: inherit !important;
} }

View File

@ -1,22 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title='<3')}"></th:block> <th:block th:insert="~{fragments/common :: head(title='<3')}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br /><br />
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6"></div>
</div>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </body>
</div> </html>
</body>
</html>

View File

@ -1,326 +1,286 @@
<!doctype html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{account.title}, header=#{account.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-9"> <div class="col-md-9">
<!-- User Settings Title --> <!-- User Settings Title -->
<h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2> <h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2>
<hr> <hr />
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'notAuthenticated'}" class="alert alert-danger"> <th:block th:if="${param.messageType != null and param.messageType.size() > 0}">
<span th:text="#{notAuthenticatedMessage}">Default message if not found</span> <div th:if="${param.messageType[0] == 'notAuthenticated'}" class="alert alert-danger">
</div> <span th:text="#{notAuthenticatedMessage}">Default message if not found</span>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'userNotFound'}" class="alert alert-danger"> </div>
<span th:text="#{userNotFoundMessage}">Default message if not found</span> <div th:if="${param.messageType[0] == 'userNotFound'}" class="alert alert-danger">
</div> <span th:text="#{userNotFoundMessage}">Default message if not found</span>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'incorrectPassword'}" class="alert alert-danger"> </div>
<span th:text="#{incorrectPasswordMessage}">Default message if not found</span> <div th:if="${param.messageType[0] == 'incorrectPassword'}" class="alert alert-danger">
</div> <span th:text="#{incorrectPasswordMessage}">Default message if not found</span>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger"> </div>
<span th:text="#{usernameExistsMessage}">Default message if not found</span> <div th:if="${param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
</div> <span th:text="#{usernameExistsMessage}">Default message if not found</span>
</div>
</th:block>
<!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<th:block th:if="${error}">
<div class="alert alert-danger" role="alert">
<!-- At the top of the user settings --> <span th:text="${error}">Error Message</span>
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3> </div>
</th:block>
<div th:if="${error}" class="alert alert-danger" role="alert"> <!-- Change Username Form -->
<span th:text="${error}">Error Message</span> <h4></h4>
</div> <form action="api/v1/user/change-username" method="post">
<!-- Change Username Form --> <div class="mb-3">
<h4></h4> <label for="newUsername" th:text="#{account.changeUsername}">Change Username</label>
<form action="api/v1/user/change-username" method="post"> <input type="text" class="form-control" name="newUsername" id="newUsername" th:placeholder="#{account.newUsername}">
<div class="mb-3">
<label for="newUsername" th:text="#{account.changeUsername}">Change Username</label>
<input type="text" class="form-control" name="newUsername" id="newUsername" th:placeholder="#{account.newUsername}">
</div>
<div class="mb-3">
<label for="currentPassword" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPasswordUsername" th:placeholder="#{password}">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{account.changeUsername}">Change Username</button>
</div>
</form>
<hr> <!-- Separator Line -->
<!-- Change Password Form -->
<h4 th:text="#{account.changePassword}">Change Password?</h4>
<form action="api/v1/user/change-password" method="post">
<div class="mb-3">
<label for="currentPassword" th:text="#{account.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPasswordPassword" th:placeholder="#{account.oldPassword}">
</div>
<div class="mb-3">
<label for="newPassword" th:text="#{account.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
</div>
<div class="mb-3">
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{account.changePassword}">Change Password</button>
</div>
</form>
<hr>
<div class="card">
<div class="card-header" th:text="#{account.yourApiKey}">
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="password" class="form-control" id="apiKey" th:placeholder="#{account.yourApiKey}" readonly>
<div class="input-group-append">
<button class="btn btn-outline-secondary" id="copyBtn" type="button" onclick="copyToClipboard()">
<img class="blackwhite-icon" src="images/clipboard.svg" alt="Copy" style="height:20px;">
</button>
<button class="btn btn-outline-secondary" id="showBtn" type="button" onclick="showApiKey()">
<img class="blackwhite-icon" id="eyeIcon" src="images/eye.svg" alt="Toggle API Key Visibility" style="height:20px;">
</button>
<button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">
<img class="blackwhite-icon" id="eyeIcon" src="images/arrow-clockwise.svg" alt="Refresh API-Key" style="height:20px;">
</button>
</div>
</div>
</div>
</div>
<script>
function copyToClipboard() {
const apiKeyElement = document.getElementById("apiKey");
apiKeyElement.select();
document.execCommand("copy");
}
function showApiKey() {
const apiKeyElement = document.getElementById("apiKey");
const copyBtn = document.getElementById("copyBtn");
const eyeIcon = document.getElementById("eyeIcon");
if (apiKeyElement.type === "password") {
apiKeyElement.type = "text";
eyeIcon.src = "images/eye-slash.svg";
copyBtn.disabled = false; // Enable copy button when API key is visible
} else {
apiKeyElement.type = "password";
eyeIcon.src = "images/eye.svg";
copyBtn.disabled = true; // Disable copy button when API key is hidden
}
}
document.addEventListener("DOMContentLoaded", async function() {
try {
let response = await fetch('/api/v1/user/get-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
} else {
manageUIState(null);
}
} catch (error) {
console.error('There was an error:', error);
}
});
async function refreshApiKey() {
try {
let response = await fetch('/api/v1/user/update-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
document.getElementById("apiKey").type = 'text';
document.getElementById("copyBtn").disabled = false;
} else {
alert('Error refreshing API key.');
}
} catch (error) {
console.error('There was an error:', error);
}
}
function manageUIState(apiKey) {
const apiKeyElement = document.getElementById("apiKey");
const showBtn = document.getElementById("showBtn");
const copyBtn = document.getElementById("copyBtn");
if (apiKey && apiKey.trim().length > 0) {
apiKeyElement.value = apiKey;
showBtn.disabled = false;
copyBtn.disabled = true;
} else {
apiKeyElement.value = "";
showBtn.disabled = true;
copyBtn.disabled = true;
}
}
document.addEventListener("DOMContentLoaded", function() {
const form = document.querySelector('form[action="api/v1/user/change-password"]');
form.addEventListener('submit', function(event) {
const newPassword = document.getElementById('newPassword').value;
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmNewPassword) {
alert('New Password and Confirm New Password must match.');
event.preventDefault(); // Prevent form submission
}
});
});
</script>
<hr> <!-- Separator Line -->
<h4 th:text="#{account.syncTitle}">Sync browser settings with Account</h4>
<div class="container mt-4">
<h3 th:text="#{account.settingsCompare}">Settings Comparison:</h3>
<table id="settingsTable" class="table table-bordered table-sm table-striped">
<thead>
<tr>
<th th:text="#{account.property}">Property</th>
<th th:text="#{account.accountSettings}">Account Setting</th>
<th th:text="#{account.webBrowserSettings}">Web Browser Setting</th>
</tr>
</thead>
<tbody>
<!-- This will be dynamically populated by JavaScript -->
</tbody>
</table>
<div class="buttons-container mt-3 text-center">
<button id="syncToBrowser" class="btn btn-primary btn-sm" th:text="#{account.syncToBrowser}">Sync Account -> Browser</button>
<button id="syncToAccount" class="btn btn-secondary btn-sm" th:text="#{account.syncToAccount}">Sync Account <- Browser</button>
</div>
</div>
<style>
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.buttons-container {
margin-top: 20px;
text-align: center;
}
</style>
<script th:inline="javascript">
document.addEventListener("DOMContentLoaded", function() {
const settingsTableBody = document.querySelector("#settingsTable tbody");
/*<![CDATA[*/
var accountSettingsString = /*[[${settings}]]*/ {};
/*]]>*/
var accountSettings = JSON.parse(accountSettingsString);
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
allKeys.forEach(key => {
if(key === 'debug' || key === '0' || key === '1') return; // Ignoring specific keys
const accountValue = accountSettings[key] || '-';
const browserValue = localStorage.getItem(key) || '-';
const row = settingsTableBody.insertRow();
const propertyCell = row.insertCell(0);
const accountCell = row.insertCell(1);
const browserCell = row.insertCell(2);
propertyCell.textContent = key;
accountCell.textContent = accountValue;
browserCell.textContent = browserValue;
});
document.getElementById('syncToBrowser').addEventListener('click', function() {
// First, clear the local storage
localStorage.clear();
// Then, set the account settings to local storage
for (let key in accountSettings) {
if(key !== 'debug' && key !== '0' && key !== '1') { // Only sync non-ignored keys
localStorage.setItem(key, accountSettings[key]);
}
}
location.reload(); // Refresh the page after sync
});
document.getElementById('syncToAccount').addEventListener('click', function() {
let form = document.createElement("form");
form.method = "POST";
form.action = "api/v1/user/updateUserSettings"; // Your endpoint URL
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if(key !== 'debug' && key !== '0' && key !== '1') { // Only send non-ignored keys
let hiddenField = document.createElement("input");
hiddenField.type = "hidden";
hiddenField.name = key;
hiddenField.value = localStorage.getItem(key);
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
});
});
</script>
<div class="mb-3 mt-4">
<a href="logout">
<button type="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</button>
</a>
<a th:if="${role == 'ROLE_ADMIN'}" href="addUsers" target="_blank">
<button type="button" class="btn btn-info" th:text="#{account.adminSettings}">Admin Settings</button>
</a>
</div>
</div>
</div> </div>
</div> <div class="mb-3">
<label for="currentPassword" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPasswordUsername" th:placeholder="#{password}">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{account.changeUsername}">Change Username</button>
</div>
</form>
<hr /> <!-- Separator Line -->
<!-- Change Password Form -->
<h4 th:text="#{account.changePassword}">Change Password?</h4>
<form action="api/v1/user/change-password" method="post">
<div class="mb-3">
<label for="currentPassword" th:text="#{account.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPasswordPassword" th:placeholder="#{account.oldPassword}">
</div>
<div class="mb-3">
<label for="newPassword" th:text="#{account.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
</div>
<div class="mb-3">
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{account.changePassword}">Change Password</button>
</div>
</form>
<hr />
<div class="card">
<div class="card-header" th:text="#{account.yourApiKey}"></div>
<div class="card-body">
<div class="input-group mb-3">
<input type="password" class="form-control" id="apiKey" th:placeholder="#{account.yourApiKey}" readonly>
<div class="input-group-append">
<button class="btn btn-outline-secondary" id="copyBtn" type="button" onclick="copyToClipboard()">
<img class="blackwhite-icon" src="images/clipboard.svg" alt="Copy" style="height:20px;">
</button>
<button class="btn btn-outline-secondary" id="showBtn" type="button" onclick="showApiKey()">
<img class="blackwhite-icon" id="eyeIcon" src="images/eye.svg" alt="Toggle API Key Visibility" style="height:20px;">
</button>
<button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">
<img class="blackwhite-icon" id="eyeIcon" src="images/arrow-clockwise.svg" alt="Refresh API-Key" style="height:20px;">
</button>
</div>
</div>
</div>
</div>
<script>
function copyToClipboard() {
const apiKeyElement = document.getElementById("apiKey");
apiKeyElement.select();
document.execCommand("copy");
}
function showApiKey() {
const apiKeyElement = document.getElementById("apiKey");
const copyBtn = document.getElementById("copyBtn");
const eyeIcon = document.getElementById("eyeIcon");
if (apiKeyElement.type === "password") {
apiKeyElement.type = "text";
eyeIcon.src = "images/eye-slash.svg";
copyBtn.disabled = false; // Enable copy button when API key is visible
} else {
apiKeyElement.type = "password";
eyeIcon.src = "images/eye.svg";
copyBtn.disabled = true; // Disable copy button when API key is hidden
}
}
document.addEventListener("DOMContentLoaded", async function() {
try {
let response = await fetch('/api/v1/user/get-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
} else {
manageUIState(null);
}
} catch (error) {
console.error('There was an error:', error);
}
});
async function refreshApiKey() {
try {
let response = await fetch('/api/v1/user/update-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
document.getElementById("apiKey").type = 'text';
document.getElementById("copyBtn").disabled = false;
} else {
alert('Error refreshing API key.');
}
} catch (error) {
console.error('There was an error:', error);
}
}
function manageUIState(apiKey) {
const apiKeyElement = document.getElementById("apiKey");
const showBtn = document.getElementById("showBtn");
const copyBtn = document.getElementById("copyBtn");
if (apiKey && apiKey.trim().length > 0) {
apiKeyElement.value = apiKey;
showBtn.disabled = false;
copyBtn.disabled = true;
} else {
apiKeyElement.value = "";
showBtn.disabled = true;
copyBtn.disabled = true;
}
}
document.addEventListener("DOMContentLoaded", function() {
const form = document.querySelector('form[action="api/v1/user/change-password"]');
form.addEventListener('submit', function(event) {
const newPassword = document.getElementById('newPassword').value;
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmNewPassword) {
alert('New Password and Confirm New Password must match.');
event.preventDefault(); // Prevent form submission
}
});
});
</script>
<hr /> <!-- Separator Line -->
<h4 th:text="#{account.syncTitle}">Sync browser settings with Account</h4>
<div class="container mt-4">
<h3 th:text="#{account.settingsCompare}">Settings Comparison:</h3>
<table id="settingsTable" class="table table-bordered table-sm table-striped">
<thead>
<tr>
<th th:text="#{account.property}">Property</th>
<th th:text="#{account.accountSettings}">Account Setting</th>
<th th:text="#{account.webBrowserSettings}">Web Browser Setting</th>
</tr>
</thead>
<tbody>
<!-- This will be dynamically populated by JavaScript -->
</tbody>
</table>
<div class="buttons-container mt-3 text-center">
<button id="syncToBrowser" class="btn btn-primary btn-sm" th:text="#{account.syncToBrowser}">Sync Account -> Browser</button>
<button id="syncToAccount" class="btn btn-secondary btn-sm" th:text="#{account.syncToAccount}">Sync Account <- Browser</button>
</div>
</div>
<script th:inline="javascript">
document.addEventListener("DOMContentLoaded", function() {
const settingsTableBody = document.querySelector("#settingsTable tbody");
/*<![CDATA[*/
var accountSettingsString = /*[[${settings}]]*/ {};
/*]]>*/
var accountSettings = JSON.parse(accountSettingsString);
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
allKeys.forEach(key => {
if(key === 'debug' || key === '0' || key === '1') return; // Ignoring specific keys
const accountValue = accountSettings[key] || '-';
const browserValue = localStorage.getItem(key) || '-';
const row = settingsTableBody.insertRow();
const propertyCell = row.insertCell(0);
const accountCell = row.insertCell(1);
const browserCell = row.insertCell(2);
propertyCell.textContent = key;
accountCell.textContent = accountValue;
browserCell.textContent = browserValue;
});
document.getElementById('syncToBrowser').addEventListener('click', function() {
// First, clear the local storage
localStorage.clear();
// Then, set the account settings to local storage
for (let key in accountSettings) {
if(key !== 'debug' && key !== '0' && key !== '1') { // Only sync non-ignored keys
localStorage.setItem(key, accountSettings[key]);
}
}
location.reload(); // Refresh the page after sync
});
document.getElementById('syncToAccount').addEventListener('click', function() {
let form = document.createElement("form");
form.method = "POST";
form.action = "api/v1/user/updateUserSettings"; // Your endpoint URL
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if(key !== 'debug' && key !== '0' && key !== '1') { // Only send non-ignored keys
let hiddenField = document.createElement("input");
hiddenField.type = "hidden";
hiddenField.name = key;
hiddenField.value = localStorage.getItem(key);
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
});
});
</script>
<div class="mb-3 mt-4">
<a href="logout">
<button type="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</button>
</a>
<a th:if="${role == 'ROLE_ADMIN'}" href="addUsers" target="_blank">
<button type="button" class="btn btn-info" th:text="#{account.adminSettings}">Admin Settings</button>
</a>
</div>
</div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,84 +1,78 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<!-- User Settings Title --> <!-- User Settings Title -->
<h2 class="text-center" th:text="#{adminUserSettings.header}">Admin User Control Settings</h2> <h2 class="text-center" th:text="#{adminUserSettings.header}">Admin User Control Settings</h2>
<table class="table">
<thead>
<tr>
<th th:text="#{username}">Username</th>
<th th:text="#{adminUserSettings.roles}">Roles</th>
<th th:text="#{adminUserSettings.actions}">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.username}"></td>
<td th:text="${user.getRolesAsString()}"></td>
<td>
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post">
<button type="submit" th:text="#{delete}">Delete</button>
</form>
</td>
</tr>
</tbody>
</table>
<h2 th:text="#{adminUserSettings.addUser}">Add New User</h2>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
<table class="table"> </div>
<thead> <form action="/api/v1/user/admin/saveUser" method="post">
<tr> <div class="mb-3">
<th th:text="#{username}">Username</th> <label for="username" th:text="#{username}">Username</label>
<th th:text="#{adminUserSettings.roles}">Roles</th> <input type="text" class="form-control" name="username" required>
<th th:text="#{adminUserSettings.actions}">Actions</th> </div>
</tr> <div class="mb-3">
</thead> <label for="password" th:text="#{password}">Password</label>
<tbody> <input type="password" class="form-control" name="password" required>
<tr th:each="user : ${users}"> </div>
<td th:text="${user.username}"></td> <div class="mb-3">
<td th:text="${user.getRolesAsString()}"></td> <label for="role" th:text="#{adminUserSettings.role}">Role</label>
<td> <select name="role" class="form-control" required>
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post"> <option value="ROLE_ADMIN" th:text="#{adminUserSettings.admin}">Admin</option>
<button type="submit" th:text="#{delete}">Delete</button> <option value="ROLE_USER" th:text="#{adminUserSettings.user}">User</option>
</form> <option value="ROLE_LIMITED_API_USER" th:text="#{adminUserSettings.apiUser}">Limited API User</option>
</td> <option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option>
</tr> <option value="ROLE_DEMO_USER" th:text="#{adminUserSettings.demoUser}">Demo User</option>
</tbody> </select>
</table> </div>
<div class="mb-3">
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
<h2 th:text="#{adminUserSettings.addUser}">Add New User</h2>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
</div>
<form action="/api/v1/user/admin/saveUser" method="post">
<div class="mb-3">
<label for="username" th:text="#{username}">Username</label>
<input type="text" class="form-control" name="username" required>
</div>
<div class="mb-3">
<label for="password" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="password" required>
</div>
<div class="mb-3">
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" required>
<option value="ROLE_ADMIN" th:text="#{adminUserSettings.admin}">Admin</option>
<option value="ROLE_USER" th:text="#{adminUserSettings.user}">User</option>
<option value="ROLE_LIMITED_API_USER" th:text="#{adminUserSettings.apiUser}">Limited API User</option>
<option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option>
<option value="ROLE_DEMO_USER" th:text="#{adminUserSettings.demoUser}">Demo User</option>
</select>
</div>
<div class="mb-3">
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
</div>
<!-- Add other fields as required -->
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
</form>
</div>
</div> </div>
</div>
<!-- Add other fields as required -->
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
</form>
</div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,44 +1,43 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{autoSplitPDF.title}, header=#{autoSplitPDF.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{autoSplitPDF.title}, header=#{autoSplitPDF.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{autoSplitPDF.header}"></h2> <h2 th:text="#{autoSplitPDF.header}"></h2>
<!-- Added a brief description --> <!-- Added a brief description -->
<p th:text="#{autoSplitPDF.description}"></p> <p th:text="#{autoSplitPDF.description}"></p>
<ul> <ul>
<li th:text="#{autoSplitPDF.selectText.1}"></li> <li th:text="#{autoSplitPDF.selectText.1}"></li>
<li th:text="#{autoSplitPDF.selectText.2}"></li> <li th:text="#{autoSplitPDF.selectText.2}"></li>
<li th:text="#{autoSplitPDF.selectText.3}"></li> <li th:text="#{autoSplitPDF.selectText.3}"></li>
<li th:text="#{autoSplitPDF.selectText.4}"></li> <li th:text="#{autoSplitPDF.selectText.4}"></li>
</ul> </ul>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/misc/auto-split-pdf}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/misc/auto-split-pdf}">
<p th:text="#{autoSplitPDF.formPrompt}"></p> <p th:text="#{autoSplitPDF.formPrompt}"></p>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="duplexMode" id="duplexMode"> <input type="checkbox" class="form-check-input" name="duplexMode" id="duplexMode">
<label class="ms-3" for="duplexMode" th:text=#{autoSplitPDF.duplexMode}></label> <label class="ms-3" for="duplexMode" th:text=#{autoSplitPDF.duplexMode}></label>
</div>
<p><a th:href="@{files/Auto Splitter Divider (minimal).pdf}" download th:text="#{autoSplitPDF.dividerDownload1}"></a></p>
<p><a th:href="@{files/Auto Splitter Divider (with instructions).pdf}" download th:text="#{autoSplitPDF.dividerDownload2}"></a></p>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoSplitPDF.submit}"></button>
</form>
</div>
</div> </div>
<p><a th:href="@{files/Auto Splitter Divider (minimal).pdf}" download th:text="#{autoSplitPDF.dividerDownload1}"></a></p>
<p><a th:href="@{files/Auto Splitter Divider (with instructions).pdf}" download th:text="#{autoSplitPDF.dividerDownload2}"></a></p>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoSplitPDF.submit}"></button>
</form>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,90 +1,83 @@
<!doctype html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{changeCreds.title}, header=#{changeCreds.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{changeCreds.title}, header=#{changeCreds.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-9"> <div class="col-md-9">
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{changeCreds.header}">User Settings</h2>
<hr>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'notAuthenticated'}" class="alert alert-danger">
<span th:text="#{notAuthenticatedMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'userNotFound'}" class="alert alert-danger">
<span th:text="#{userNotFoundMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'incorrectPassword'}" class="alert alert-danger">
<span th:text="#{incorrectPasswordMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
</div>
<!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<!-- Change Username Form -->
<h4></h4>
<h4 th:text="#{changeCreds.changeUserAndPassword}">Change Username and password</h4>
<form action="api/v1/user/change-username-and-password" method="post">
<div class="mb-3">
<label for="username" th:text="#{changeCreds.newUsername}">New Username</label>
<input type="text" class="form-control" name="username" id="username" th:placeholder="${username}">
</div>
<div class="mb-3">
<label for="password" th:text="#{changeCreds.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="password" id="password" th:placeholder="#{changeCreds.oldPassword}">
</div>
<div class="mb-3">
<label for="newPassword" th:text="#{changeCreds.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{changeCreds.newPassword}">
</div>
<div class="mb-3">
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{changeCreds.submit}">Change credentials!</button>
</div>
</form>
<script>
document.addEventListener("DOMContentLoaded", function() {
const form = document.querySelector('form[action="api/v1/user/change-username-and-password"]');
form.addEventListener('submit', function(event) {
const newPassword = document.getElementById('newPassword').value;
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmNewPassword) {
alert('New Password and Confirm New Password must match.');
event.preventDefault(); // Prevent form submission
}
});
});
</script>
</div>
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{changeCreds.header}">User Settings</h2>
<hr />
<th:block th:if="${param.messageType != null and param.messageType.size() > 0}">
<div th:if="${param.messageType[0] == 'notAuthenticated'}" class="alert alert-danger">
<span th:text="#{notAuthenticatedMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType[0] == 'userNotFound'}" class="alert alert-danger">
<span th:text="#{userNotFoundMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType[0] == 'incorrectPassword'}" class="alert alert-danger">
<span th:text="#{incorrectPasswordMessage}">Default message if not found</span>
</div>
<div th:if="${param.messageType[0] == 'usernameExists'}" class="alert alert-danger">
<span th:text="#{usernameExistsMessage}">Default message if not found</span>
</div>
</th:block>
<!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<!-- Change Username Form -->
<h4></h4>
<h4 th:text="#{changeCreds.changeUserAndPassword}">Change Username and password</h4>
<form action="api/v1/user/change-username-and-password" method="post">
<div class="mb-3">
<label for="username" th:text="#{changeCreds.newUsername}">New Username</label>
<input type="text" class="form-control" name="username" id="username" th:placeholder="${username}">
</div> </div>
</div> <div class="mb-3">
<label for="password" th:text="#{changeCreds.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="password" id="password" th:placeholder="#{changeCreds.oldPassword}">
</div>
<div class="mb-3">
<label for="newPassword" th:text="#{changeCreds.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{changeCreds.newPassword}">
</div>
<div class="mb-3">
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{changeCreds.submit}">Change credentials!</button>
</div>
</form>
<script>
document.addEventListener("DOMContentLoaded", function() {
const form = document.querySelector('form[action="api/v1/user/change-username-and-password"]');
form.addEventListener('submit', function(event) {
const newPassword = document.getElementById('newPassword').value;
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmNewPassword) {
alert('New Password and Confirm New Password must match.');
event.preventDefault(); // Prevent form submission
}
});
});
</script>
</div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,29 +1,31 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{BookToPDF.title}, header=#{BookToPDF.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{BookToPDF.title}, header=#{BookToPDF.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{BookToPDF.header}"></h2> <h2 th:text="#{BookToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/book/pdf}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/book/pdf}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{BookToPDF.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{BookToPDF.submit}"></button>
</form> </form>
<p class="mt-3" th:text="#{BookToPDF.help}"></p> <p class="mt-3" th:text="#{BookToPDF.help}"></p>
<p class="mt-3" th:text="#{BookToPDF.credit}"></p> <p class="mt-3" th:text="#{BookToPDF.credit}"></p>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,37 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{fileToPDF.title}, header=#{fileToPDF.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{fileToPDF.title}, header=#{fileToPDF.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{fileToPDF.header}"></h2> <h2 th:text="#{fileToPDF.header}"></h2>
<p th:text="#{processTimeWarning}"> <p th:text="#{processTimeWarning}"></p>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/file/pdf}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/file/pdf}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{fileToPDF.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{fileToPDF.submit}"></button>
</form>
</form> <p class="mt-3" th:text="#{fileToPDF.credit}"></p>
<p class="mt-3" th:text="#{fileToPDF.credit}"></p> <p class="mt-3" th:text="#{fileToPDF.supportedFileTypes}"></p>
<p class="mt-3" th:text="#{fileToPDF.supportedFileTypes}"></p> <p th:utext="#{fileToPDF.fileTypesList}"></p>
<p th:utext="#{fileToPDF.fileTypesList}"></p> <a href="https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html">https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html</a>
<a href="https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html">https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html</a>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,36 +1,35 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{HTMLToPDF.title}, header=#{HTMLToPDF.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{HTMLToPDF.title}, header=#{HTMLToPDF.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="mb-3"> <div class="mb-3">
<h2 th:text="#{HTMLToPDF.header}"></h2> <h2 th:text="#{HTMLToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/html/pdf}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/html/pdf}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='text/html,application/zip' )}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='text/html,application/zip' )}"></div>
<div class="mb-3">
<div class="mb-3"> <label for="zoom" th:text="#{HTMLToPDF.zoom}" class="form-label"></label>
<label for="zoom" th:text="#{HTMLToPDF.zoom}" class="form-label"></label> <input type="number" step="0.1" class="form-control" id="zoom" name="zoom" value="1" />
<input type="number" step="0.1" class="form-control" id="zoom" name="zoom" value="1" />
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{HTMLToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{HTMLToPDF.help}"></p>
<p class="mt-3" th:text="#{HTMLToPDF.credit}"></p>
</div>
</div> </div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{HTMLToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{HTMLToPDF.help}"></p>
<p class="mt-3" th:text="#{HTMLToPDF.credit}"></p>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,88 +1,82 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{imageToPDF.title}, header=#{imageToPDF.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{imageToPDF.title}, header=#{imageToPDF.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{imageToPDF.header}"></h2> <h2 th:text="#{imageToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/img/pdf}">
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/img/pdf}"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='image/*', inputText=#{imgPrompt})}"></div>
<div class="mb-3">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='image/*', inputText=#{imgPrompt})}"></div> <label for="fitOption" th:text="#{imageToPDF.selectLabel}">Fit Options</label>
<select class="form-control" id="fitOption" name="fitOption">
<div class="mb-3"> <option value="fillPage" th:text="#{imageToPDF.fillPage}">Fill Page</option>
<label for="fitOption" th:text="#{imageToPDF.selectLabel}">Fit Options</label> <option value="fitDocumentToImage" th:text="#{imageToPDF.fitDocumentToImage}">Fit Document to Image</option>
<select class="form-control" id="fitOption" name="fitOption"> <option value="maintainAspectRatio" th:text="#{imageToPDF.maintainAspectRatio}">Maintain Aspect Ratio</option>
<option value="fillPage" th:text="#{imageToPDF.fillPage}">Fill Page</option> </select>
<option value="fitDocumentToImage" th:text="#{imageToPDF.fitDocumentToImage}">Fit Document to Image</option>
<option value="maintainAspectRatio" th:text="#{imageToPDF.maintainAspectRatio}">Maintain Aspect Ratio</option>
</select>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="autoRotate" id="autoRotate">
<label class="ms-3" for="autoRotate" th:text=#{imageToPDF.selectText.2}></label>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" id="colorType" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option>
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
</select>
</div>
<br>
<input type="hidden" id="override" name="override" value="multi">
<div class="mb-3">
<label th:text=#{imageToPDF.selectText.3}></label>
<select class="form-control" id="conversionType" name="conversionType" disabled>
<option value="merge" th:text=#{imageToPDF.selectText.4}></option>
<option value="convert" th:text=#{imageToPDF.selectText.5} selected></option>
</select>
</div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{imageToPDF.submit}"></button>
<script>
$('#fileInput-input').on('change', function() {
var files = document.getElementById("fileInput-input").files;
var conversionType = document.getElementById("conversionType");
console.log("files.length=" + files.length)
if (files.length > 1) {
conversionType.disabled = false;
} else {
conversionType.disabled = true;
}
});
$('#conversionType').change(function() {
var selectedValue = $(this).val();
var override = document.getElementById("override");
console.log("selectedValue=" + selectedValue)
if (selectedValue === 'merge') {
override.value = "single";
} else if (selectedValue === 'convert') {
override.value = "multi";
}
});
</script>
</form>
</div>
</div> </div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="autoRotate" id="autoRotate">
<label class="ms-3" for="autoRotate" th:text=#{imageToPDF.selectText.2}></label>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" id="colorType" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option>
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
</select>
</div>
<br>
<input type="hidden" id="override" name="override" value="multi">
<div class="mb-3">
<label th:text=#{imageToPDF.selectText.3}></label>
<select class="form-control" id="conversionType" name="conversionType" disabled>
<option value="merge" th:text=#{imageToPDF.selectText.4}></option>
<option value="convert" th:text=#{imageToPDF.selectText.5} selected></option>
</select>
</div>
<br /><br />
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{imageToPDF.submit}"></button>
<script>
$('#fileInput-input').on('change', function() {
var files = document.getElementById("fileInput-input").files;
var conversionType = document.getElementById("conversionType");
console.log("files.length=" + files.length)
if (files.length > 1) {
conversionType.disabled = false;
} else {
conversionType.disabled = true;
}
});
$('#conversionType').change(function() {
var selectedValue = $(this).val();
var override = document.getElementById("override");
console.log("selectedValue=" + selectedValue)
if (selectedValue === 'merge') {
override.value = "single";
} else if (selectedValue === 'convert') {
override.value = "multi";
}
});
</script>
</form>
</div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,30 +1,31 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{MarkdownToPDF.title}, header=#{MarkdownToPDF.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{MarkdownToPDF.title}, header=#{MarkdownToPDF.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{MarkdownToPDF.header}"></h2> <h2 th:text="#{MarkdownToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/markdown/pdf}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/markdown/pdf}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='text/markdown')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='text/markdown')}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{MarkdownToPDF.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{MarkdownToPDF.submit}"></button>
</form>
</form> <p class="mt-3" th:text="#{MarkdownToPDF.help}"></p>
<p class="mt-3" th:text="#{MarkdownToPDF.help}"></p> <p class="mt-3" th:text="#{MarkdownToPDF.credit}"></p>
<p class="mt-3" th:text="#{MarkdownToPDF.credit}"></p>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,55 +1,47 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
th:lang-direction="#{language.direction}" <head>
xmlns:th="http://www.thymeleaf.org"> <th:block th:insert="~{fragments/common :: head(title=#{PDFToBook.title}, header=#{PDFToBook.header})}"></th:block>
</head>
<th:block <body>
th:insert="~{fragments/common :: head(title=#{PDFToBook.title}, header=#{PDFToBook.header})}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<body> <div id="content-wrap">
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div id="page-container"> <br /><br />
<div id="content-wrap"> <div class="container">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div class="row justify-content-center">
<br> <br> <div class="col-md-6">
<div class="container"> <h2 th:text="#{PDFToBook.header}"></h2>
<div class="row justify-content-center"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/book}">
<div class="col-md-6"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<h2 th:text="#{PDFToBook.header}"></h2> <div class="mb-3">
<form method="post" enctype="multipart/form-data" <label th:text="#{PDFToBook.selectText.1}"></label>
th:action="@{api/v1/convert/pdf/book}"> <select class="form-control" name="outputFormat">
<div <option value="epub">EPUB</option>
th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <option value="mobi">MOBI</option>
<option value="azw3">AZW3</option>
<div class="mb-3"> <option value="docx">DOCX</option>
<label th:text="#{PDFToBook.selectText.1}"></label> <select <option value="rtf">RTF</option>
class="form-control" name="outputFormat"> <option value="txt">TXT</option>
<option value="epub">EPUB</option> <option value="html">HTML</option>
<option value="mobi">MOBI</option> <option value="lit">LIT</option>
<option value="azw3">AZW3</option> <option value="fb2">FB2</option>
<option value="docx">DOCX</option> <option value="pdb">PDB</option>
<option value="rtf">RTF</option> <option value="lrf">LRF</option>
<option value="txt">TXT</option> </select>
<option value="html">HTML</option> </div>
<option value="lit">LIT</option> <br>
<option value="fb2">FB2</option> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToBook.submit}"></button>
<option value="pdb">PDB</option> </form>
<option value="lrf">LRF</option> <p class="mt-3" th:text="#{PDFToBook.help}"></p>
</select> <p class="mt-3" th:text="#{PDFToBook.credit}"></p>
</div>
</div> </div>
<br> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" </div>
th:text="#{PDFToBook.submit}"></button> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</form> </body>
<p class="mt-3" th:text="#{PDFToBook.help}"></p>
<p class="mt-3" th:text="#{PDFToBook.credit}"></p>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html> </html>

View File

@ -1,159 +1,143 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToCSV.title}, header=#{PDFToCSV.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToCSV.title}, header=#{PDFToCSV.header})}"></th:block> <body>
<div id="page-container">
<div id="content-wrap">
<body> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div id="page-container"> <br /><br />
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
</br></br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToCSV.header}"></h2> <h2 th:text="#{PDFToCSV.header}"></h2>
<form id="PDFToCSVForm" th:action="@{api/v1/convert/pdf/csv}" method="post" enctype="multipart/form-data"> <form id="PDFToCSVForm" th:action="@{api/v1/convert/pdf/csv}" method="post" enctype="multipart/form-data">
<input id="pageId" type="hidden" name="pageId" /> <input id="pageId" type="hidden" name="pageId" />
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<button type="submit" class="btn btn-primary" th:text="#{PDFToCSV.submit}"></button> <button type="submit" class="btn btn-primary" th:text="#{PDFToCSV.submit}"></button>
</form> </form>
<p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p> <p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p>
<div style="position: relative; display: inline-block;">
<div>
<div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container">
<button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> < </button>
<button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> > </button>
</div>
<canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
</div>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
</div>
<script>
let pdfCanvas = document.getElementById('crop-pdf-canvas');
let overlayCanvas = document.getElementById('overlayCanvas');
// let paginationBtnContainer = ;
let context = pdfCanvas.getContext('2d');
let btn1Object = document.getElementById('previous-page-btn');
let btn2Object = document.getElementById('next-page-btn');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let fileInput = document.getElementById('fileInput-input');
let file;
let pdfDoc = null;
let pageId = document.getElementById('pageId');
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
btn1Object.addEventListener('click',function (e){
if (currentPage !== 1) {
currentPage = currentPage - 1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
btn2Object.addEventListener('click',function (e){
if (currentPage !== totalPages){
currentPage=currentPage+1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
fileInput.addEventListener('change', function(e) {
file = e.target.files[0];
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pageId.value = currentPage;
};
reader.readAsArrayBuffer(file);
document.getElementById("pagination-button-container").style.display="flex";
document.getElementById("instruction-text").style.display="block";
}
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function(page) {
let viewport = page.getViewport({ scale: 1.0 });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
pdfCanvas.classList.add("shadow-canvas");
});
}
</script>
<div style="position: relative; display: inline-block;">
<div>
<div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container">
<button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> < </button>
<button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> > </button>
</div>
<canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
</div> </div>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
</div>
<script>
let pdfCanvas = document.getElementById('crop-pdf-canvas');
let overlayCanvas = document.getElementById('overlayCanvas');
// let paginationBtnContainer = ;
let context = pdfCanvas.getContext('2d');
let btn1Object = document.getElementById('previous-page-btn');
let btn2Object = document.getElementById('next-page-btn');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let fileInput = document.getElementById('fileInput-input');
let file;
let pdfDoc = null;
let pageId = document.getElementById('pageId');
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
btn1Object.addEventListener('click',function (e){
if (currentPage !== 1) {
currentPage = currentPage - 1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
btn2Object.addEventListener('click',function (e){
if (currentPage !== totalPages){
currentPage=currentPage+1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
fileInput.addEventListener('change', function(e) {
file = e.target.files[0];
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js';
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pageId.value = currentPage;
};
reader.readAsArrayBuffer(file);
document.getElementById("pagination-button-container").style.display="flex";
document.getElementById("instruction-text").style.display="block";
}
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function(page) {
let viewport = page.getViewport({ scale: 1.0 });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
pdfCanvas.classList.add("shadow-canvas");
});
}
</script>
</div> </div>
</div>
</div> </div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </body>
</div>
</body>
</html> </html>

View File

@ -1,29 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToHTML.title}, header=#{PDFToHTML.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToHTML.title}, header=#{PDFToHTML.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToHTML.header}"></h2> <h2 th:text="#{PDFToHTML.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/html}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/html}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToHTML.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToHTML.submit}"></button>
</form>
</form> <p class="mt-3" th:text="#{PDFToHTML.credit}"></p>
<p class="mt-3" th:text="#{PDFToHTML.credit}"></p>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,61 +1,58 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToImage.title}, header=#{pdfToImage.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToImage.title}, header=#{pdfToImage.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br /><br />
<br> <br> <div class="container">
<div class="container"> <div class="row justify-content-center">
<div class="row justify-content-center"> <div class="col-md-6">
<div class="col-md-6"> <h2 th:text="#{pdfToImage.header}"></h2>
<h2 th:text="#{pdfToImage.header}"></h2> <p th:text="#{processTimeWarning}"></p>
<p th:text="#{processTimeWarning}"></p> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/img}">
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/img}"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div class="mb-3">
<div class="mb-3"> <label th:text="#{pdfToImage.selectText}"></label>
<label th:text="#{pdfToImage.selectText}"></label> <select class="form-control" name="imageFormat">
<select class="form-control" name="imageFormat"> <option value="png">PNG</option>
<option value="png">PNG</option> <option value="jpg">JPG</option>
<option value="jpg">JPG</option> <option value="gif">GIF</option>
<option value="gif">GIF</option> <option value="tiff">TIFF</option>
<option value="tiff">TIFF</option> <option value="bmp">BMP</option>
<option value="bmp">BMP</option> </select>
</select>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.singleOrMultiple}"></label>
<select class="form-control" name="singleOrMultiple">
<option value="multiple" th:text="#{pdfToImage.multi}"></option>
<option value="single" th:text="#{pdfToImage.single}"></option>
</select>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option>
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
</select>
</div>
<div class="mb-3">
<label for="dpi">DPI:</label>
<input type="number" name="dpi" class="form-control" id="dpi" min="1" step="1" value="300" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToImage.submit}"></button>
</form>
</div>
</div> </div>
<div class="mb-3">
<label th:text="#{pdfToImage.singleOrMultiple}"></label>
<select class="form-control" name="singleOrMultiple">
<option value="multiple" th:text="#{pdfToImage.multi}"></option>
<option value="single" th:text="#{pdfToImage.single}"></option>
</select>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option>
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
</select>
</div>
<div class="mb-3">
<label for="dpi">DPI:</label>
<input type="number" name="dpi" class="form-control" id="dpi" min="1" step="1" value="300" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToImage.submit}"></button>
</form>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,32 +1,31 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToPDFA.title}, header=#{pdfToPDFA.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToPDFA.title}, header=#{pdfToPDFA.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{pdfToPDFA.header}"></h2> <h2 th:text="#{pdfToPDFA.header}"></h2>
<p>Currently doesn't work for multiple inputs at once</p> <p>Currently doesn't work for multiple inputs at once</p>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/pdfa}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/pdfa}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToPDFA.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToPDFA.submit}"></button>
</form> </form>
<p class="mt-3" th:text="#{pdfToPDFA.credit}"></p> <p class="mt-3" th:text="#{pdfToPDFA.credit}"></p>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,38 +1,38 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToPresentation.title}, header=#{PDFToPresentation.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToPresentation.title}, header=#{PDFToPresentation.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToPresentation.header}"></h2> <h2 th:text="#{PDFToPresentation.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/presentation}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/presentation}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="mb-3">
<div class="mb-3"> <label th:text="#{PDFToPresentation.selectText.1}"></label>
<label th:text="#{PDFToPresentation.selectText.1}"></label> <select class="form-control" name="outputFormat">
<select class="form-control" name="outputFormat"> <option value="ppt">PPT</option>
<option value="ppt">PPT</option> <option value="pptx">PPTX</option>
<option value="pptx">PPTX</option> <option value="odp">ODP</option>
<option value="odp">ODP</option> </select>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToPresentation.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToPresentation.credit}"></p>
</div>
</div> </div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToPresentation.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToPresentation.credit}"></p>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,35 +1,38 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToText.title}, header=#{PDFToText.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToText.title}, header=#{PDFToText.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToText.header}"></h2> <h2 th:text="#{PDFToText.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/text}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/text}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="mb-3">
<div class="mb-3"> <label th:text="#{PDFToText.selectText.1}"></label>
<label th:text="#{PDFToText.selectText.1}"></label> <select class="form-control" name="outputFormat">
<select class="form-control" name="outputFormat"> <option value="rtf">RTF</option>
<option value="rtf">RTF</option> <option value="txt">TXT</option>
<option value="txt">TXT</option> </select>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToText.credit}"></p>
</div>
</div> </div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToText.credit}"></p>
</div>
</div>
</div> </div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </body>
</div> </html>

View File

@ -1,40 +1,40 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToWord.title}, header=#{PDFToWord.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToWord.title}, header=#{PDFToWord.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToWord.header}"></h2> <h2 th:text="#{PDFToWord.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/word}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/word}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="mb-3">
<div class="mb-3"> <label th:text="#{PDFToWord.selectText.1}"></label>
<label th:text="#{PDFToWord.selectText.1}"></label> <select class="form-control" name="outputFormat">
<select class="form-control" name="outputFormat"> <option value="doc">Doc</option>
<option value="doc">Doc</option> <option value="docx">DocX</option>
<option value="docx">DocX</option> <option value="odt">Odt</option>
<option value="odt">Odt</option> </select>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToWord.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToWord.credit}"></p>
</div>
</div> </div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToWord.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToWord.credit}"></p>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,29 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToXML.title}, header=#{PDFToXML.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToXML.title}, header=#{PDFToXML.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToXML.header}"></h2> <h2 th:text="#{PDFToXML.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/xml}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/pdf/xml}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToXML.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToXML.submit}"></button>
</form>
</form> <p class="mt-3" th:text="#{PDFToXML.credit}"></p>
<p class="mt-3" th:text="#{PDFToXML.credit}"></p>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,29 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{URLToPDF.title}, header=#{URLToPDF.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{URLToPDF.title}, header=#{URLToPDF.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{URLToPDF.header}"></h2> <h2 th:text="#{URLToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/url/pdf}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/url/pdf}">
<input type="text" class="form-control" id="urlInput" name="urlInput"> <input type="text" class="form-control" id="urlInput" name="urlInput">
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{URLToPDF.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{URLToPDF.submit}"></button>
</form>
</form> <p class="mt-3" th:text="#{URLToPDF.credit}"></p>
<p class="mt-3" th:text="#{URLToPDF.credit}"></p>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,147 +1,135 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{crop.title}, header=#{crop.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{crop.title}, header=#{crop.header})}"></th:block> <body>
<div id="page-container">
<div id="content-wrap">
<body> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div id="page-container"> <br /><br />
<div id="content-wrap"> <div class="container">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div class="row justify-content-center">
<br> <br> <div class="col-md-6">
<div class="container"> <h2 th:text="#{crop.header}"></h2>
<div class="row justify-content-center"> <form id="cropForm" action="/api/v1/general/crop" method="post" enctype="multipart/form-data">
<div class="col-md-6"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<h2 th:text="#{crop.header}"></h2> <input id="x" type="hidden" name="x">
<form id="cropForm" action="/api/v1/general/crop" method="post" enctype="multipart/form-data"> <input id="y" type="hidden" name="y">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <input id="width" type="hidden" name="width">
<input id="x" type="hidden" name="x"> <input id="height" type="hidden" name="height">
<input id="y" type="hidden" name="y"> <button type="submit" class="btn btn-primary" th:text="#{crop.submit}"></button>
<input id="width" type="hidden" name="width"> </form>
<input id="height" type="hidden" name="height"> <div style="position: relative; display: inline-block;">
<button type="submit" class="btn btn-primary" th:text="#{crop.submit}"></button> <canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
</form> <canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
<div style="position: relative; display: inline-block;">
<canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
</div>
<script>
let pdfCanvas = document.getElementById('crop-pdf-canvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let isDrawing = false; // New flag to check if drawing is ongoing
let cropForm = document.getElementById('cropForm');
let fileInput = document.getElementById('fileInput-input');
let xInput = document.getElementById('x');
let yInput = document.getElementById('y');
let widthInput = document.getElementById('width');
let heightInput = document.getElementById('height');
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
fileInput.addEventListener('change', function(e) {
let file = e.target.files[0];
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
});
overlayCanvas.addEventListener('mousedown', function(e) {
// Clear previously drawn rectangle on the main canvas
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
renderPage(currentPage); // Re-render the PDF
// Clear the overlay canvas to ensure old drawings are removed
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
startX = e.offsetX;
startY = e.offsetY;
isDrawing = true;
});
overlayCanvas.addEventListener('mousemove', function(e) {
if (!isDrawing) return;
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
overlayContext.strokeStyle = 'red';
overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
});
overlayCanvas.addEventListener('mouseup', function(e) {
isDrawing = false;
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
let flippedY = pdfCanvas.height - e.offsetY;
xInput.value = startX;
yInput.value = flippedY;
widthInput.value = rectWidth;
heightInput.value = rectHeight;
// Draw the final rectangle on the main canvas
context.strokeStyle = 'red';
context.strokeRect(startX, startY, rectWidth, rectHeight);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function(page) {
let viewport = page.getViewport({ scale: 1.0 });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
pdfCanvas.classList.add("shadow-canvas");
});
}
</script>
</div>
</div> </div>
<script>
let pdfCanvas = document.getElementById('crop-pdf-canvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let isDrawing = false; // New flag to check if drawing is ongoing
let cropForm = document.getElementById('cropForm');
let fileInput = document.getElementById('fileInput-input');
let xInput = document.getElementById('x');
let yInput = document.getElementById('y');
let widthInput = document.getElementById('width');
let heightInput = document.getElementById('height');
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
fileInput.addEventListener('change', function(e) {
let file = e.target.files[0];
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
});
overlayCanvas.addEventListener('mousedown', function(e) {
// Clear previously drawn rectangle on the main canvas
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
renderPage(currentPage); // Re-render the PDF
// Clear the overlay canvas to ensure old drawings are removed
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
startX = e.offsetX;
startY = e.offsetY;
isDrawing = true;
});
overlayCanvas.addEventListener('mousemove', function(e) {
if (!isDrawing) return;
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
overlayContext.strokeStyle = 'red';
overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
});
overlayCanvas.addEventListener('mouseup', function(e) {
isDrawing = false;
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
let flippedY = pdfCanvas.height - e.offsetY;
xInput.value = startX;
yInput.value = flippedY;
widthInput.value = rectWidth;
heightInput.value = rectHeight;
// Draw the final rectangle on the main canvas
context.strokeStyle = 'red';
context.strokeRect(startX, startY, rectWidth, rectHeight);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function(page) {
let viewport = page.getViewport({ scale: 1.0 });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
pdfCanvas.classList.add("shadow-canvas");
});
}
</script>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </body>
</div>
</body>
</html> </html>

View File

@ -1,131 +1,39 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head> <head>
<title>Error! :(</title> <th:block th:insert="~{fragments/common :: head(title='404 - Page Not Found | Oops, we tripped in the code!')}"></th:block>
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block>
<style>
h1 {
text-align: center;
margin-top: 10%;
}
p {
text-align: center;
margin-top: 2em;
}
.button:hover {
background-color: #005b7f;
}
.features-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr));
gap: 25px 30px;
}
.feature-card {
border: 1px solid rgba(0, 0, 0, .125);
border-radius: 0.25rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.feature-card .card-text {
flex: 1;
}
#support-section {
background-color: #f9f9f9;
padding: 4rem;
margin-top: 1rem;
text-align: center;
}
#support-section h1 {
margin-top: 0;
}
#support-section p {
margin-top: 0;
}
#button-group {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
#github-button, #discord-button {
display: inline-block;
padding: 1rem 2rem;
margin: 1rem;
background-color: #008CBA;
color: #fff;
font-size: 1.2rem;
text-align: center;
text-decoration: none;
border-radius: 3rem;
transition: all 0.3s ease-in-out;
}
#github-button:hover, #discord-button:hover, #home-button:hover {
background-color: #005b7f;
}
#home-button {
display: block;
width: 200px;
height: 50px;
margin: 2em auto;
background-color: #008CBA;
color: white;
text-align: center;
line-height: 50px;
text-decoration: none;
font-weight: bold;
border-radius: 25px;
transition: all 0.3s ease-in-out;
}
</style>
</head> </head>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div> <div class="container">
<div class="container"> <div id="support-section">
<h1 class="display-2">Oops!</h1>
<div id="support-section"> <p class="lead" th:if="${param.status == '404'}">
<h1 class="display-2">Oops!</h1> We can't seem to find the page you're looking for.
<p class="lead" th:if="${param.status == '404'}">We can't seem to find the page you're looking for.</p> </p>
<p class="lead" th:unless="${param.status == '404'}">Something went wrong</p> <p class="lead" th:unless="${param.status == '404'}">
Something went wrong
<br> </p>
<h2>Need help / Found a issue?</h2> <br>
<p>If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:</p> <h2>Need help / Found a issue?</h2>
<div id="button-group"> <p>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank">Submit a ticket on GitHub</a> If you're still having trouble, don't hesitate to reach out to us
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Join our Discord server</a> for help. You can submit a ticket on our GitHub page or contact us
through Discord:
</p>
<div id="button-group">
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank">Submit a ticket on GitHub</a>
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Join our Discord server</a>
</div>
<a href="/" id="home-button">Go back to homepage</a>
</div> </div>
<a href="/" id="home-button">Go back to homepage</a>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body> </body>
</html> </html>

View File

@ -1,33 +1,33 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block> <body>
<body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{pageExtracter.header}"></h2> <h2 th:text="#{pageExtracter.header}"></h2>
<form th:action="@{api/v1/general/rearrange-pages}" method="post" enctype="multipart/form-data"> <form th:action="@{api/v1/general/rearrange-pages}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<input type="hidden" id="customMode" name="customMode" value=""> <input type="hidden" id="customMode" name="customMode" value="">
<div class="mb-3"> <div class="mb-3">
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label> <label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageNumbers" placeholder="(e.g. 1,2,8 or 4,7,12-16 or 2n-1)" required> <input type="text" class="form-control" id="pageOrder" name="pageNumbers" placeholder="(e.g. 1,2,8 or 4,7,12-16 or 2n-1)" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageExtracter.submit}"></button>
</form>
</div>
</div> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageExtracter.submit}"></button>
</form>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,12 +1,12 @@
<div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}" data-bs-tags="${tags}"> <div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}" th:data-bs-tags="${tags}">
<a th:href="${cardLink}"> <a th:href="${cardLink}">
<div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title --> <div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title -->
<img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30"> <img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30">
<h5 class="card-title ms-2" th:text="${cardTitle}"></h5> <!-- Add some margin-left (ms-2) for spacing between SVG and title --> <h5 class="card-title ms-2" th:text="${cardTitle}"></h5> <!-- Add some margin-left (ms-2) for spacing between SVG and title -->
</div> </div>
<p class="card-text" th:text="${cardText}"></p> <p class="card-text" th:text="${cardText}"></p>
</a> </a>
<div class="favorite-icon" onclick="toggleFavorite(this)"> <div class="favorite-icon" onclick="toggleFavorite(this)">
<img src="images/star.svg" alt="Favorite"> <img src="images/star.svg" alt="Favorite">
</div> </div>
</div> </div>

View File

@ -1,137 +1,144 @@
<head th:fragment="head"> <th:block th:fragment="head">
<!-- Title -->
<title th:text="${@appName} + (${title} != null and ${title} != '' ? ' - ' + ${title} : '')"></title>
<!-- Title --> <!-- Metadata -->
<title th:text="${@appName} + (${title} != null and ${title} != '' ? ' - ' + ${title} : '')"></title> <meta charset="UTF-8">
<meta name="description" th:content="${@appName} + (${header} != null and ${header} != '' ? ' - ' + ${header} : '')"/>
<meta name="msapplication-TileColor" content="#2d89ef">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Metadata --> <!-- Icons -->
<meta charset="UTF-8"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=2">
<meta name="description" th:content="${@appName} + (${header} != null and ${header} != '' ? ' - ' + ${header} : '')"/> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=2">
<meta name="msapplication-TileColor" content="#2d89ef"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=2">
<meta name="theme-color" content="#ffffff"> <link rel="manifest" href="/site.webmanifest?v=2">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="mask-icon" href="/safari-pinned-tab.svg?v=2" color="#ca2b2a">
<link rel="shortcut icon" href="/favicon.ico?v=2">
<meta name="apple-mobile-web-app-title" content="Stirling PDF">
<meta name="application-name" content="Stirling PDF">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#ffffff">
<!-- Icons --> <!-- jQuery -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=2"> <script src="js/thirdParty/jquery.min.js"></script>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=2"> <script src="js/thirdParty/jszip.min.js"></script>
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=2">
<link rel="manifest" href="/site.webmanifest?v=2">
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=2" color="#ca2b2a">
<link rel="shortcut icon" href="/favicon.ico?v=2">
<meta name="apple-mobile-web-app-title" content="Stirling PDF">
<meta name="application-name" content="Stirling PDF">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#ffffff">
<!-- jQuery --> <!-- Bootstrap -->
<script src="js/thirdParty/jquery.min.js"></script> <script src="js/thirdParty/popper.min.js"></script>
<script src="js/thirdParty/jszip.min.js"></script> <script src="js/thirdParty/bootstrap.min.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css">
<!-- Bootstrap --> <!-- Bootstrap Icons -->
<script src="js/thirdParty/popper.min.js"></script> <link rel="stylesheet" href="css/bootstrap-icons.min.css">
<script src="js/thirdParty/bootstrap.min.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css">
<!-- Bootstrap Icons --> <!-- PDF.js -->
<link rel="stylesheet" href="css/bootstrap-icons.min.css"> <script src="pdfjs/pdf.js"></script>
<!-- PDF.js --> <!-- PDF-Lib -->
<script src="pdfjs/pdf.js"></script> <script src="js/thirdParty/pdf-lib.min.js"></script>
<!-- PDF-Lib --> <!-- Custom -->
<script src="js/thirdParty/pdf-lib.min.js"></script> <link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" th:href="@{css/light-mode.css}" id="light-mode-styles">
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles">
<link rel="stylesheet" th:href="@{css/rainbow-mode.css}" id="rainbow-mode-styles" disabled="true">
<link rel="stylesheet" href="css/tab-container.css">
<link rel="stylesheet" href="css/navbar.css">
<!-- Custom --> <link rel="stylesheet" th:href="@{/css/error.css}" th:if="${error}">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" th:href="@{css/light-mode.css}" id="light-mode-styles">
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles">
<link rel="stylesheet" th:href="@{css/rainbow-mode.css}" id="rainbow-mode-styles" disabled="true">
<link rel="stylesheet" href="css/tab-container.css">
<script src="js/tab-container.js"></script>
<link rel="stylesheet" href="css/home.css" th:if="${currentPage == 'home'}">
<link rel="stylesheet" href="css/account.css" th:if="${currentPage == 'account'}">
<link rel="stylesheet" href="css/licenses.css" th:if="${currentPage == 'licenses'}">
<link rel="stylesheet" href="css/multi-tool.css" th:if="${currentPage == 'multi-tool'}">
<link rel="stylesheet" href="css/rotate-pdf.css" th:if="${currentPage == 'rotate-pdf'}">
<link rel="stylesheet" href="css/stamp.css" th:if="${currentPage == 'stamp'}">
<link rel="stylesheet" href="css/fileSelect.css">
<link rel="stylesheet" href="css/footer.css">
</head> <!-- Help Modal -->
<link rel="stylesheet" href="css/errorBanner.css">
<script src="js/tab-container.js"></script>
<script src="js/darkmode.js"></script>
</th:block>
<th:block th:fragment="game"> <th:block th:fragment="game">
<dialog id="game-container-wrapper" class="game-container-wrapper" data-bs-modal> <dialog id="game-container-wrapper" class="game-container-wrapper" data-bs-modal>
<script> <script>
console.log("loaded game") console.log("loaded game");
$(document).ready(function() { $(document).ready(function() {
function loadGameScript(callback) { function loadGameScript(callback) {
console.log('loadGameScript called'); console.log('loadGameScript called');
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'js/game.js'; script.src = 'js/game.js';
script.onload = callback; script.onload = callback;
document.body.appendChild(script); document.body.appendChild(script);
} }
let gameScriptLoaded = false; let gameScriptLoaded = false;
const gameDialog = document.getElementById('game-container-wrapper') const gameDialog = document.getElementById('game-container-wrapper');
$('#show-game-btn').on('click', function() { $('#show-game-btn').on('click', function() {
console.log('Show game button clicked'); console.log('Show game button clicked');
if (!gameScriptLoaded) { if (!gameScriptLoaded) {
console.log('Show game button load'); console.log('Show game button load');
loadGameScript(function() { loadGameScript(function() {
console.log('Game script loaded'); console.log('Game script loaded');
window.initializeGame(); window.initializeGame();
gameScriptLoaded = true; gameScriptLoaded = true;
}); });
} else { } else {
window.resetGame(); window.resetGame();
} }
gameDialog.showModal(); gameDialog.showModal();
}); });
gameDialog.addEventListener("click", e => { gameDialog.addEventListener("click", e => {
const dialogDimensions = gameDialog.getBoundingClientRect() const dialogDimensions = gameDialog.getBoundingClientRect()
if ( if (
e.clientX < dialogDimensions.left || e.clientX < dialogDimensions.left ||
e.clientX > dialogDimensions.right || e.clientX > dialogDimensions.right ||
e.clientY < dialogDimensions.top || e.clientY < dialogDimensions.top ||
e.clientY > dialogDimensions.bottom e.clientY > dialogDimensions.bottom
) { ) {
gameDialog.close() gameDialog.close();
} }
}) })
}) })
</script> </script>
<div id="game-container"> <div id="game-container">
<div id="lives">Lives: 3</div> <div id="lives">Lives: 3</div>
<div id="score">Score: 0</div> <div id="score">Score: 0</div>
<div id="high-score">High Score: 0</div> <div id="high-score">High Score: 0</div>
<div id="level">Level: 1</div> <div id="level">Level: 1</div>
<img src="favicon.svg" class="player" id="player"> <img src="favicon.svg" class="player" id="player">
</div> </div>
<link rel="stylesheet" href="css/game.css"> <link rel="stylesheet" href="css/game.css">
</dialog> </dialog>
</th:block> </th:block>
<th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, notRequired=${notRequired} ?: false"> <th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, notRequired=${notRequired} ?: false">
<script th:inline="javascript"> <script th:inline="javascript">
const pdfPasswordPrompt =/*[[#{error.pdfPassword}]]*/ ''; const pdfPasswordPrompt = /*[[#{error.pdfPassword}]]*/ '';
const multiple = [[${multiple}]] || false; const multiple = [[${multiple}]] || false;
const remoteCall = [[${remoteCall}]] || true; const remoteCall = [[${remoteCall}]] || true;
</script> </script>
<script src="js/downloader.js"></script> <script src="js/downloader.js"></script>
<div class="custom-file-chooser" th:attr="data-bs-unique-id=${name}, <div class="custom-file-chooser" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
data-bs-element-id=${name+'-input'}, <div class="mb-3">
data-bs-files-selected=#{filesSelected}, <input type="file" class="form-control" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple th:required="${notRequired} ? null : 'required'">
data-bs-pdf-prompt=#{pdfPrompt}"> </div>
<div class="mb-3"> <div class="selected-files"></div>
<input type="file" class="form-control" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple th:required="${notRequired} ? null : 'required'"> </div>
</div>
<div class="selected-files"></div>
</div>
<div id="progressBarContainer" style="display: none; position: relative;"> <div id="progressBarContainer" style="display: none; position: relative;">
<div class="progress" style="height: 1rem;"> <div class="progress" style="height: 1rem;">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"> <div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">Bored waiting?</button> <button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">Bored waiting?</button>
<script src="js/fileInput.js"></script>
<script src="js/fileInput.js"></script>
<link rel="stylesheet" href="css/fileSelect.css">
</th:block> </th:block>

View File

@ -1,23 +1,5 @@
<th:block th:fragment="errorBanner"> <th:block th:fragment="errorBanner">
<style>
#github-button,
#discord-button {
display: inline-block;
padding: 1rem 2rem;
background-color: #008CBA;
color: #fff;
text-align: center;
text-decoration: none;
border-radius: 3rem;
transition: all 0.3s ease-in-out;
}
#github-button:hover,
#discord-button:hover {
background-color: #005b7f;
}
</style>
<br th:if="${message}"> <br th:if="${message}">
<div id="errorContainer" th:if="${message}" class="alert alert-danger alert-dismissible fade show" role="alert"> <div id="errorContainer" th:if="${message}" class="alert alert-danger alert-dismissible fade show" role="alert">
<h4 class="alert-heading" th:text="'Error: ' + ${status} + ' ' + ${error}"></h4> <h4 class="alert-heading" th:text="'Error: ' + ${status} + ' ' + ${error}"></h4>
@ -52,10 +34,10 @@
function copytrace() { function copytrace() {
var flip = false var flip = false
if(!traceVisible) { if(!traceVisible) {
toggletrace() toggletrace()
flip = true flip = true
} }
var traceContent = document.getElementById("traceContent"); var traceContent = document.getElementById("traceContent");
var range = document.createRange(); var range = document.createRange();
range.selectNode(traceContent); range.selectNode(traceContent);
@ -64,7 +46,7 @@
document.execCommand("copy"); document.execCommand("copy");
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
if(flip){ if(flip){
toggletrace() toggletrace()
} }
} }

View File

@ -1,58 +1,49 @@
<th:block th:fragment="errorBannerPerPage"> <th:block th:fragment="errorBannerPerPage">
<div id="errorContainer" class="alert alert-danger alert-dismissible fade show" role="alert" style="display: none;">
<h4 class="alert-heading">Error</h4>
<p></p>
<button type="button" class="btn btn-danger" onclick="toggletrace()">Show Stack Trace</button>
<button type="button" class="btn btn-secondary" onclick="copytrace()">Copy Stack Trace</button>
<button type="button" class="btn btn-info" onclick="showHelp()">Help</button>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span aria-hidden="true">&times;</span>
</button>
<!-- Stack trace section -->
<div id="trace" style="max-height: 0; overflow: hidden;">
<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 3px; padding: 10px; margin-top: 5px;">
<pre id="traceContent"></pre>
</div>
</div>
</div>
<div class="modal fade" id="helpModal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true">
<div id="errorContainer" class="alert alert-danger alert-dismissible fade show" role="alert" style="display: none;"> <div class="modal-dialog modal-dialog-centered" role="document" id="helpModalDialog">
<h4 class="alert-heading">Error</h4> <div class="modal-content">
<p></p> <div class="modal-header">
<button type="button" class="btn btn-danger" onclick="toggletrace()">Show Stack Trace</button> <h5 class="modal-title" id="helpModalLabel">Help</h5>
<button type="button" class="btn btn-secondary" onclick="copytrace()">Copy Stack Trace</button> <button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<button type="button" class="btn btn-info" onclick="showHelp()">Help</button> <span aria-hidden="true">&times;</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()"> </button>
<span aria-hidden="true">&times;</span> </div>
</button> <div class="modal-body">
<!-- Stack trace section --> <div class="container">
<div id="trace" style="max-height: 0; overflow: hidden;"> <div id="support-section">
<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 3px; padding: 10px; margin-top: 5px;"> <h1 class="display-2">Oops!</h1>
<pre id="traceContent"></pre> <p class="lead">Sorry for the issue!.</p>
</div> <br>
</div> <h2>Need help / Found an issue?</h2>
<p>If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:</p>
<div id="button-group">
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank">GitHub - Submit a ticket</a>
</div> <a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Discord - Submit Support post</a>
</div>
<!-- Help Modal --> <a href="/" id="home-button">Go to Homepage</a>
<link rel="stylesheet" href="css/errorBanner.css"> <a data-bs-dismiss="modal" id="home-button">Close</a>
</div>
<div class="modal fade" id="helpModal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true"> </div>
<div class="modal-dialog modal-dialog-centered" role="document" id="helpModalDialog"> </div>
<div class="modal-content"> </div>
<div class="modal-header"> </div>
<h5 class="modal-title" id="helpModalLabel">Help</h5> </div>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close"> <script src="js/errorBanner.js"></script>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container">
<div id="support-section">
<h1 class="display-2">Oops!</h1>
<p class="lead">Sorry for the issue!.</p>
<br>
<h2>Need help / Found an issue?</h2>
<p>If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:</p>
<div id="button-group">
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank">GitHub - Submit a ticket</a>
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Discord - Submit Support post</a>
</div>
<a href="/" id="home-button">Go to Homepage</a>
<a data-bs-dismiss="modal" id="home-button">Close</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/errorBanner.js"></script>
</th:block> </th:block>

View File

@ -1,58 +1,19 @@
<div th:fragment="footer"> <footer th:fragment="footer" id="footer" class="text-center py-3">
<footer id="footer" class="text-center py-3">
<div class="footer-center"> <div class="footer-center">
<a href="https://github.com/Stirling-Tools/Stirling-PDF" target="_blank" class="mx-1" title="Visit Github Repository">
<a href="https://github.com/Stirling-Tools/Stirling-PDF" <img src="images/github.svg">
target="_blank" class="mx-1" title="Visit Github Repository"><img </a>
src="images/github.svg"></img></a> <a <a href="https://hub.docker.com/r/frooodle/s-pdf" target="_blank" class="mx-1" title="See Docker Hub">
href="https://hub.docker.com/r/frooodle/s-pdf" target="_blank" <img src="images/docker.svg">
class="mx-1" title="See Docker Hub"><img src="images/docker.svg"></img></a> </a>
<a href="https://discord.gg/Cn8pWhQRxZ" target="_blank" class="mx-1" <a href="https://discord.gg/Cn8pWhQRxZ" target="_blank" class="mx-1" title="Join Discord Channel">
title="Join Discord Channel"><img src="images/discord.svg"></img></a> <img src="images/discord.svg">
<a href="https://github.com/sponsors/Frooodle" target="_blank" </a>
class="mx-1" title="Donate"><img <a href="https://github.com/sponsors/Frooodle" target="_blank" class="mx-1" title="Donate">
src="images/suit-heart-fill.svg"></img></a> <img src="images/suit-heart-fill.svg">
</a>
<div style="color: grey;" th:if="${@appName} != 'Stirling PDF'" class="footer-powered-by" th:text="#{poweredBy} + ' Stirling PDF'"></div> <div style="color: grey;" th:if="${@appName} != 'Stirling PDF'" class="footer-powered-by" th:text="#{poweredBy} + ' Stirling PDF'"></div>
</div> </div>
<a href="licenses" id="licenses" target="_blank" class="mx-1" title="" th:text="#{licenses.nav}">Licenses</a>
</footer>
<a href="licenses" id="licenses" target="_blank" class="mx-1" title="" th:text="#{licenses.nav}">Licenses</a>
</footer>
</div>
<style>
#footer {
display: flex;
flex-direction: column; /* Stack children vertically */
justify-content: center;
align-items: center;
width: 100%;
}
.footer-center {
display: flex;
flex-direction: column; /* Stack items vertically */
justify-content: center; /* Center children vertically */
align-items: center; /* Center children horizontally */
flex-grow: 1;
}
.footer-powered-by {
margin-top: auto; /* Pushes the text to the bottom */
color: grey;
text-align: center; /* Centers the text inside the div */
width: 100%; /* Full width to center the text properly */
}
.right-link-container {
align-self: flex-end; /* Align to the end of the flex container */
padding-right: 20px;
}
</style>

View File

@ -1,107 +1,28 @@
<th:block th:fragment="langs"> <th:block th:fragment="langs">
<a class="dropdown-item lang_dropdown-item" href="" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="bg_BG"> <img src="images/flags/bg.svg" alt="icon" width="20" height="15"> Български</a>
data-bs-language-code="bg_BG"> <img src="images/flags/bg.svg" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ar_AR"> <img src="images/flags/sa.svg" alt="icon" width="20" height="15"> العربية</a>
alt="icon" width="20" height="15"> Български <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ca_CA"> <img src="images/flags/es-ct.svg" alt="icon" width="20" height="15"> Català</a>
</a> <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="zh_CN"> <img src="images/flags/cn.svg" alt="icon" width="20" height="15"> 简体中文</a>
<a class="dropdown-item lang_dropdown-item" href="" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="zh_TW"> <img src="images/flags/tw.svg" alt="icon" width="20" height="15"> 正體中文</a>
data-bs-language-code="ar_AR"> <img src="images/flags/sa.svg" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="de_DE"> <img src="images/flags/de.svg" alt="icon" width="20" height="15"> Deutsch</a>
alt="icon" width="20" height="15"> العربية <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="en_GB"> <img src="images/flags/gb.svg" alt="icon" width="20" height="15"> English (GB)</a>
</a> <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="en_US"> <img src="images/flags/us.svg" alt="icon" width="20" height="15"> English (US)</a>
<a class="dropdown-item lang_dropdown-item" href="" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="eu_ES"> <img src="images/flags/eu.svg" alt="icon" width="20" height="15"> Euskara</a>
data-bs-language-code="ca_CA"> <img src="images/flags/es-ct.svg" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="es_ES"> <img src="images/flags/es.svg" alt="icon" width="20" height="15"> Español</a>
alt="icon" width="20" height="15"> Català <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="fr_FR"> <img src="images/flags/fr.svg" alt="icon" width="20" height="15"> Français</a>
</a> <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="id_ID"> <img src="images/flags/id.svg" alt="icon" width="20" height="15"> Indonesia</a>
<a class="dropdown-item lang_dropdown-item" href="" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="it_IT"> <img src="images/flags/it.svg" alt="icon" width="20" height="15"> Italiano</a>
data-bs-language-code="zh_CN"> <img src="images/flags/cn.svg" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="nl_NL"> <img src="images/flags/nl.svg" alt="icon" width="20" height="15"> Nederlands</a>
alt="icon" width="20" height="15"> 简体中文 <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pl_PL"> <img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski</a>
</a> <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pt_BR"> <img src="images/flags/pt_br.svg" alt="icon" width="20" height="15"> Português (BR)</a>
<a class="dropdown-item lang_dropdown-item" href="" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ro_RO"> <img src="images/flags/ro.svg" alt="icon" width="20" height="15"> Romanian</a>
data-bs-language-code="zh_TW"> <img src="images/flags/tw.svg" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="sv_SE"> <img src="images/flags/se.svg" alt="icon" width="20" height="15"> Svenska</a>
alt="icon" width="20" height="15"> 正體中文 <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="tr_TR"> <img src="images/flags/tr.svg" alt="icon" width="20" height="15"> Türkçe</a>
</a> <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ru_RU"> <img src="images/flags/ru.svg" alt="icon" width="20" height="15"> Русский</a>
<a class="dropdown-item lang_dropdown-item" href="" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ko_KR"> <img src="images/flags/kr.svg" alt="icon" width="20" height="15"> 한국어</a>
data-bs-language-code="de_DE"> <img src="images/flags/de.svg" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ja_JP"> <img src="images/flags/jp.svg" alt="icon" width="20" height="15"> 日本語</a>
alt="icon" width="20" height="15"> Deutsch <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="el_GR"> <img src="images/flags/gr.svg" alt="icon" width="20" height="15"> Ελληνικά</a>
</a> <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="hu_HU"> <img src="images/flags/hu.svg" alt="icon" width="20" height="15"> Hungarian</a>
<a class="dropdown-item lang_dropdown-item" href="" <a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="hi_IN"> <img src="images/flags/in.svg" alt="icon" width="20" height="15"> हिन्दी</a>
data-bs-language-code="en_GB"> <img src="images/flags/gb.svg" <a class="dropdown-item lang_dropdown-item" href="" data-language-code="sr-Latn-RS"> <img src="images/flags/rs.svg" alt="icon" width="20" height="15"> Srpski</a>
alt="icon" width="20" height="15"> English (GB)
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="en_US"> <img src="images/flags/us.svg"
alt="icon" width="20" height="15"> English (US)
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="eu_ES"> <img src="images/flags/eu.svg"
alt="icon" width="20" height="15"> Euskara
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="es_ES"> <img src="images/flags/es.svg"
alt="icon" width="20" height="15"> Español
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="fr_FR"> <img src="images/flags/fr.svg"
alt="icon" width="20" height="15"> Français
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="id_ID"> <img src="images/flags/id.svg"
alt="icon" width="20" height="15"> Indonesia
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="it_IT"> <img src="images/flags/it.svg"
alt="icon" width="20" height="15"> Italiano
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="nl_NL"> <img src="images/flags/nl.svg"
alt="icon" width="20" height="15"> Nederlands
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="pl_PL"> <img src="images/flags/pl.svg"
alt="icon" width="20" height="15"> Polski
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="pt_BR"> <img src="images/flags/pt_br.svg"
alt="icon" width="20" height="15"> Português (BR)
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="ro_RO"> <img src="images/flags/ro.svg"
alt="icon" width="20" height="15"> Romanian
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="sv_SE"> <img src="images/flags/se.svg"
alt="icon" width="20" height="15"> Svenska
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="tr_TR"> <img src="images/flags/tr.svg"
alt="icon" width="20" height="15"> Türkçe
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="ru_RU"> <img src="images/flags/ru.svg"
alt="icon" width="20" height="15"> Русский
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="ko_KR"> <img src="images/flags/kr.svg"
alt="icon" width="20" height="15"> 한국어
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="ja_JP"> <img src="images/flags/jp.svg"
alt="icon" width="20" height="15"> 日本語
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="el_GR"> <img src="images/flags/gr.svg"
alt="icon" width="20" height="15"> Ελληνικά
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="hu_HU"> <img src="images/flags/hu.svg"
alt="icon" width="20" height="15"> Hungarian
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-bs-language-code="hi_IN"> <img src="images/flags/in.svg"
alt="icon" width="20" height="15"> हिन्दी
</a>
<a class="dropdown-item lang_dropdown-item" href=""
data-language-code="sr-Latn-RS"> <img src="images/flags/rs.svg"
alt="icon" width="20" height="15"> Srpski
</a>
</th:block> </th:block>

View File

@ -1,315 +1,237 @@
<div th:fragment="navbar" class="mx-auto"> <div th:fragment="navbar" class="mx-auto">
<script src="js/languageSelection.js"></script> <script src="js/languageSelection.js"></script>
<script th:inline="javascript">
<script th:inline="javascript"> const currentVersion = /*[[${@appVersion}]]*/ '';
const currentVersion = /*[[${@appVersion}]]*/ ''; const noFavourites = /*[[#{noFavourites}]]*/ '';
const noFavourites = /*[[#{noFavourites}]]*/ ''; </script>
</script> <script th:src="@{js/githubVersion.js}"></script>
<script th:src="@{js/githubVersion.js}"></script> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container ">
<link rel="stylesheet" href="css/navbar.css"> <a class="navbar-brand" href="#" th:href="@{/}" style="display: flex;">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container ">
<a class="navbar-brand" href="#" th:href="@{/}" >
<img class="main-icon" src="favicon.svg?v=2" alt="icon"> <img class="main-icon" src="favicon.svg?v=2" alt="icon">
<span class="icon-text" th:text="${@navBarText}"></span> <span class="icon-text" th:text="${@navBarText}"></span>
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav">
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto flex-nowrap"> <ul class="navbar-nav me-auto flex-nowrap">
<li class="nav-item">
<li class="nav-item"> <a class="nav-link" href="#" th:href="@{multi-tool}" th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
<a class="nav-link" href="#" th:href="@{multi-tool}" th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}"> <img class="icon" src="images/tools.svg" alt="icon">
<img class="icon" src="images/tools.svg" alt="icon"> <span class="icon-text" th:text="#{home.multiTool.title}"></span>
<span class="icon-text" th:text="#{home.multiTool.title}"></span>
</a>
</li>
<li th:if="${@enableAlphaFunctionality}" class="nav-item nav-item-separator"></li>
<li th:if="${@enableAlphaFunctionality}" class="nav-item">
<a class="nav-link" href="#" th:href="@{pipeline}" th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
<img class="icon" src="images/pipeline.svg" alt="icon">
<span class="icon-text" th:text="#{home.pipeline.title}"></span>
</a>
</li>
<li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
<span class="icon-text" th:text="#{navbar.pageOps}"></span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<!-- Existing menu items -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'images/union.svg', 'home.merge.title', 'home.merge.desc', 'merge.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'images/layout-split.svg', 'home.split.title', 'home.split.desc', 'split.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc', 'pdfOrganiser.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc', 'rotate.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc', 'removePages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc', 'pageLayout.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'scale-pages', 'images/scale-pages.svg', 'home.scalePages.title', 'home.scalePages.desc', 'scalePages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'auto-split-pdf', 'images/layout-split.svg', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc', 'autoSplitPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('adjust-contrast', 'images/adjust-contrast.svg', 'home.adjust-contrast.title', 'home.adjust-contrast.desc', 'adjust-contrast.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('crop', 'images/crop.svg', 'home.crop.title', 'home.crop.desc', 'crop.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-page', 'images/extract.svg', 'home.extractPage.title', 'home.extractPage.desc', 'extractPage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-single-page', 'images/single-page.svg', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'split-by-size-or-count', 'images/layout-split.svg', 'home.autoSizeSplitPDF.title', 'home.autoSizeSplitPDF.desc', 'autoSizeSplitPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'overlay-pdf', 'images/overlay.svg', 'home.overlay-pdfs.title', 'home.overlay-pdfs.desc', 'overlay-pdfs.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'split-pdf-by-sections', 'images/layout-split.svg', 'home.split-by-sections.title', 'home.split-by-sections.desc', 'split-by-sections.tags')}"></div>
</div>
</li>
<li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='pdf-to-img' OR ${currentPage}=='img-to-pdf' OR ${currentPage}=='pdf-to-pdfa' OR ${currentPage}=='file-to-pdf' OR ${currentPage}=='xlsx-to-pdf' OR ${currentPage}=='pdf-to-word' OR ${currentPage}=='pdf-to-presentation' OR ${currentPage}=='pdf-to-text' OR ${currentPage}=='pdf-to-html' OR ${currentPage}=='pdf-to-xml' ? 'active' : ''"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/arrow-left-right.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{navbar.convert}"></span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> </li>
<!-- Existing menu items --> <li th:if="${@enableAlphaFunctionality}" class="nav-item nav-item-separator"></li>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('img-to-pdf', 'images/image.svg', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags')}"></div> <li th:if="${@enableAlphaFunctionality}" class="nav-item">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'images/file.svg', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags')}"></div> <a class="nav-link" href="#" th:href="@{pipeline}" th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('html-to-pdf', 'images/html.svg', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags')}"></div> <img class="icon" src="images/pipeline.svg" alt="icon">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('url-to-pdf', 'images/url.svg', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags')}"></div> <span class="icon-text" th:text="#{home.pipeline.title}"></span>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('markdown-to-pdf', 'images/markdown.svg', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags')}"></div> </a>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('book-to-pdf', 'images/book.svg', 'home.BookToPDF.title', 'home.BookToPDF.desc', 'BookToPDF.tags')}"></div> </li>
<hr class="dropdown-divider"> <li class="nav-item nav-item-separator"></li>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'images/image.svg', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags')}"></div> <li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'images/file-earmark-word.svg', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags')}"></div> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-presentation', 'images/file-earmark-ppt.svg', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags')}"></div> <img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-text', 'images/filetype-txt.svg', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags')}"></div> <span class="icon-text" th:text="#{navbar.pageOps}"></span>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-html', 'images/filetype-html.svg', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags')}"></div> </a>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'images/filetype-xml.svg', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags')}"></div> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'images/file-earmark-pdf.svg', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags')}"></div> <!-- Existing menu items -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-csv', 'images/pdf-csv.svg', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'pdfToPDFA.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'images/union.svg', 'home.merge.title', 'home.merge.desc', 'merge.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-book', 'images/book.svg', 'home.PDFToBook.title', 'home.PDFToBook.desc', 'PDFToBook.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'images/layout-split.svg', 'home.split.title', 'home.split.desc', 'split.tags')}"></div>
</div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc', 'pdfOrganiser.tags')}"></div>
</li> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc', 'rotate.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc', 'removePages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc', 'pageLayout.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('scale-pages', 'images/scale-pages.svg', 'home.scalePages.title', 'home.scalePages.desc', 'scalePages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-split-pdf', 'images/layout-split.svg', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc', 'autoSplitPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('adjust-contrast', 'images/adjust-contrast.svg', 'home.adjust-contrast.title', 'home.adjust-contrast.desc', 'adjust-contrast.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('crop', 'images/crop.svg', 'home.crop.title', 'home.crop.desc', 'crop.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-page', 'images/extract.svg', 'home.extractPage.title', 'home.extractPage.desc', 'extractPage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-single-page', 'images/single-page.svg', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-by-size-or-count', 'images/layout-split.svg', 'home.autoSizeSplitPDF.title', 'home.autoSizeSplitPDF.desc', 'autoSizeSplitPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('overlay-pdf', 'images/overlay.svg', 'home.overlay-pdfs.title', 'home.overlay-pdfs.desc', 'overlay-pdfs.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdf-by-sections', 'images/layout-split.svg', 'home.split-by-sections.title', 'home.split-by-sections.desc', 'split-by-sections.tags')}"></div>
</div>
</li>
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='add-watermark' OR ${currentPage}=='cert-sign' OR ${currentPage}=='sanitize-pdf' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='pdf-to-img' OR ${currentPage}=='img-to-pdf' OR ${currentPage}=='pdf-to-pdfa' OR ${currentPage}=='file-to-pdf' OR ${currentPage}=='xlsx-to-pdf' OR ${currentPage}=='pdf-to-word' OR ${currentPage}=='pdf-to-presentation' OR ${currentPage}=='pdf-to-text' OR ${currentPage}=='pdf-to-html' OR ${currentPage}=='pdf-to-xml' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span> <img class="icon" src="images/arrow-left-right.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
</a> <span class="icon-text" th:text="#{navbar.convert}"></span>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> </a>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-password', 'images/lock.svg', 'home.addPassword.title', 'home.addPassword.desc', 'addPassword.tags')}"></div> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-password', 'images/unlock.svg', 'home.removePassword.title', 'home.removePassword.desc', 'removePassword.tags')}"></div> <!-- Existing menu items -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-permissions', 'images/shield-lock.svg', 'home.permissions.title', 'home.permissions.desc', 'permissions.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('img-to-pdf', 'images/image.svg', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'images/file.svg', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('html-to-pdf', 'images/html.svg', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('url-to-pdf', 'images/url.svg', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-redact', 'images/eraser-fill.svg', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('markdown-to-pdf', 'images/markdown.svg', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags')}"></div>
</div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('book-to-pdf', 'images/book.svg', 'home.BookToPDF.title', 'home.BookToPDF.desc', 'BookToPDF.tags')}"></div>
</li> <hr class="dropdown-divider">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'images/image.svg', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'images/file-earmark-word.svg', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-presentation', 'images/file-earmark-ppt.svg', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-text', 'images/filetype-txt.svg', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-html', 'images/filetype-html.svg', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'images/filetype-xml.svg', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'images/file-earmark-pdf.svg', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-csv', 'images/pdf-csv.svg', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'pdfToPDFA.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-book', 'images/book.svg', 'home.PDFToBook.title', 'home.PDFToBook.desc', 'PDFToBook.tags')}"></div>
</div>
</li>
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='show-javascript' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='remove-annotations' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' OR ${currentPage}=='add-page-numbers' OR ${currentPage}=='auto-rename' OR ${currentPage}=='get-info-on-pdf' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{navbar.other}"></span>
</a> <li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='add-watermark' OR ${currentPage}=='cert-sign' OR ${currentPage}=='sanitize-pdf' ? 'active' : ''">
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<!--<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'images/pipeline.svg', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags')}"></div> --> <img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc', 'ocr.tags')}"></div> </a>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags')}"></div> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPdfs.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-password', 'images/lock.svg', 'home.addPassword.title', 'home.addPassword.desc', 'addPassword.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-images', 'images/images.svg', 'home.extractImages.title', 'home.extractImages.desc', 'extractImages.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-password', 'images/unlock.svg', 'home.removePassword.title', 'home.removePassword.desc', 'removePassword.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-metadata', 'images/clipboard-data.svg', 'home.changeMetadata.title', 'home.changeMetadata.desc', 'changeMetadata.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-permissions', 'images/shield-lock.svg', 'home.permissions.title', 'home.permissions.desc', 'permissions.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-image-scans', 'images/scanner.svg', 'home.ScannerImageSplit.title', 'home.ScannerImageSplit.desc', 'ScannerImageSplit.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sign', 'images/sign.svg', 'home.sign.title', 'home.sign.desc', 'sign.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('flatten', 'images/flatten.svg', 'home.flatten.title', 'home.flatten.desc', 'flatten.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'images/wrench.svg', 'home.repair.title', 'home.repair.desc', 'repair.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-redact', 'images/eraser-fill.svg', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-blanks', 'images/blank-file.svg', 'home.removeBlanks.title', 'home.removeBlanks.desc', 'removeBlanks.tags')}"></div> </div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-annotations', 'images/no-chat.svg', 'home.removeAnnotations.title', 'home.removeAnnotations.desc', 'removeAnnotations.tags')}"></div> </li>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc', 'compare.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-page-numbers', 'images/add-page-numbers.svg', 'home.add-page-numbers.title', 'home.add-page-numbers.desc', 'add-page-numbers.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'images/fonts.svg', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('get-info-on-pdf', 'images/info.svg', 'home.getPdfInfo.title', 'home.getPdfInfo.desc', 'getPdfInfo.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('show-javascript', 'images/js.svg', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('stamp', 'images/stamp.svg', 'home.AddStampRequest.title', 'home.AddStampRequest.desc', 'AddStampRequest.tags')}"></div>
</div>
</li>
<li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='show-javascript' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='remove-annotations' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' OR ${currentPage}=='add-page-numbers' OR ${currentPage}=='auto-rename' OR ${currentPage}=='get-info-on-pdf' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{navbar.other}"></span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<!--<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'images/pipeline.svg', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags')}"></div> -->
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('view-pdf', 'images/book-opened.svg', 'home.viewPdf.title', 'home.viewPdf.desc', 'viewPdf.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc', 'ocr.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPdfs.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-images', 'images/images.svg', 'home.extractImages.title', 'home.extractImages.desc', 'extractImages.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-metadata', 'images/clipboard-data.svg', 'home.changeMetadata.title', 'home.changeMetadata.desc', 'changeMetadata.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-image-scans', 'images/scanner.svg', 'home.ScannerImageSplit.title', 'home.ScannerImageSplit.desc', 'ScannerImageSplit.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sign', 'images/sign.svg', 'home.sign.title', 'home.sign.desc', 'sign.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('flatten', 'images/flatten.svg', 'home.flatten.title', 'home.flatten.desc', 'flatten.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'images/wrench.svg', 'home.repair.title', 'home.repair.desc', 'repair.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-blanks', 'images/blank-file.svg', 'home.removeBlanks.title', 'home.removeBlanks.desc', 'removeBlanks.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-annotations', 'images/no-chat.svg', 'home.removeAnnotations.title', 'home.removeAnnotations.desc', 'removeAnnotations.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc', 'compare.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-page-numbers', 'images/add-page-numbers.svg', 'home.add-page-numbers.title', 'home.add-page-numbers.desc', 'add-page-numbers.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'images/fonts.svg', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('get-info-on-pdf', 'images/info.svg', 'home.getPdfInfo.title', 'home.getPdfInfo.desc', 'getPdfInfo.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('show-javascript', 'images/js.svg', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('stamp', 'images/stamp.svg', 'home.AddStampRequest.title', 'home.AddStampRequest.desc', 'AddStampRequest.tags')}"></div>
</div>
</li>
</ul> </ul>
<ul class="navbar-nav flex-nowrap"> <ul class="navbar-nav flex-nowrap">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="navbar-icon" src="images/star.svg" alt="icon" width="24" height="24"> <img class="navbar-icon" src="images/star.svg" alt="icon" width="24" height="24">
</a> </a>
<div class="dropdown-menu" id="favoritesDropdown" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" id="favoritesDropdown" aria-labelledby="navbarDropdown">
<!-- Dropdown items will be added here by JavaScript --> <!-- Dropdown items will be added here by JavaScript -->
</div> </div>
</li> </li>
<script src="js/languageSelection.js"></script>
<li class="nav-item">
<a class="nav-link" id="dark-mode-toggle" href="#">
<script src="js/languageSelection.js"></script> <img class="navbar-icon" id="dark-mode-icon" src="moon.svg" alt="icon" />
<script src="js/darkmode.js"></script> </a>
</li>
<li class="nav-item"> <li class="nav-item dropdown">
<a class="nav-link" id="dark-mode-toggle" href="#"> <a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="navbar-icon" id="dark-mode-icon" src="moon.svg" alt="icon" /> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2 globe-icon" viewBox="0 0 20 20">
</a> <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855-.143.268-.276.56-.395.872.705.157 1.472.257 2.282.287V1.077zM4.249 3.539c.142-.384.304-.744.481-1.078a6.7 6.7 0 0 1 .597-.933A7.01 7.01 0 0 0 3.051 3.05c.362.184.763.349 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9.124 9.124 0 0 1-1.565-.667A6.964 6.964 0 0 0 1.018 7.5h2.49zm1.4-2.741a12.344 12.344 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332zM8.5 5.09V7.5h2.99a12.342 12.342 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.612 13.612 0 0 1 7.5 10.91V8.5H4.51zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741H8.5zm-3.282 3.696c.12.312.252.604.395.872.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a6.696 6.696 0 0 1-.598-.933 8.853 8.853 0 0 1-.481-1.079 8.38 8.38 0 0 0-1.198.49 7.01 7.01 0 0 0 2.276 1.522zm-1.383-2.964A13.36 13.36 0 0 1 3.508 8.5h-2.49a6.963 6.963 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667zm6.728 2.964a7.009 7.009 0 0 0 2.275-1.521 8.376 8.376 0 0 0-1.197-.49 8.853 8.853 0 0 1-.481 1.078 6.688 6.688 0 0 1-.597.933zM8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855.143-.268.276-.56.395-.872A12.63 12.63 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.963 6.963 0 0 0 14.982 8.5h-2.49a13.36 13.36 0 0 1-.437 3.008zM14.982 7.5a6.963 6.963 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008h2.49zM11.27 2.461c.177.334.339.694.482 1.078a8.368 8.368 0 0 0 1.196-.49 7.01 7.01 0 0 0-2.275-1.52c.218.283.418.597.597.932zm-.488 1.343a7.765 7.765 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/>
</li> </svg>
</a>
<li class="nav-item dropdown"> <div class="dropdown-menu" aria-labelledby="languageDropdown">
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <th:block th:insert="~{fragments/languages :: langs}"></th:block>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2 globe-icon" viewBox="0 0 20 20"> </div>
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855-.143.268-.276.56-.395.872.705.157 1.472.257 2.282.287V1.077zM4.249 3.539c.142-.384.304-.744.481-1.078a6.7 6.7 0 0 1 .597-.933A7.01 7.01 0 0 0 3.051 3.05c.362.184.763.349 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9.124 9.124 0 0 1-1.565-.667A6.964 6.964 0 0 0 1.018 7.5h2.49zm1.4-2.741a12.344 12.344 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332zM8.5 5.09V7.5h2.99a12.342 12.342 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.612 13.612 0 0 1 7.5 10.91V8.5H4.51zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741H8.5zm-3.282 3.696c.12.312.252.604.395.872.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a6.696 6.696 0 0 1-.598-.933 8.853 8.853 0 0 1-.481-1.079 8.38 8.38 0 0 0-1.198.49 7.01 7.01 0 0 0 2.276 1.522zm-1.383-2.964A13.36 13.36 0 0 1 3.508 8.5h-2.49a6.963 6.963 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667zm6.728 2.964a7.009 7.009 0 0 0 2.275-1.521 8.376 8.376 0 0 0-1.197-.49 8.853 8.853 0 0 1-.481 1.078 6.688 6.688 0 0 1-.597.933zM8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855.143-.268.276-.56.395-.872A12.63 12.63 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.963 6.963 0 0 0 14.982 8.5h-2.49a13.36 13.36 0 0 1-.437 3.008zM14.982 7.5a6.963 6.963 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008h2.49zM11.27 2.461c.177.334.339.694.482 1.078a8.368 8.368 0 0 0 1.196-.49 7.01 7.01 0 0 0-2.275-1.52c.218.283.418.597.597.932zm-.488 1.343a7.765 7.765 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/> </li>
</svg> <li class="nav-item">
</a> <!-- Settings Button -->
<div class="dropdown-menu" aria-labelledby="languageDropdown"> <a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal">
<th:block th:insert="~{fragments/languages :: langs}"></th:block> <img class="navbar-icon" src="images/gear.svg" alt="icon" width="24" height="24">
</div> </a>
</li> </li>
<!-- Search Button and Search Bar -->
<li class="nav-item position-relative">
<li class="nav-item"> <a href="#" class="nav-link" id="search-icon">
<!-- Settings Button --> <img class="navbar-icon" src="images/search.svg" alt="icon" width="24" height="24">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal"> </a>
<img class="navbar-icon" src="images/gear.svg" alt="icon" width="24" height="24"> <!-- Search Bar -->
</a> <div class="collapse position-absolute" id="navbarSearch">
<form class="d-flex p-2 bg-white border search-form" id="searchForm">
</li> <input class="form-control search-input" type="search" th:placeholder="#{home.searchBar}" aria-label="Search" id="navbarSearchInput">
</form>
<!-- Search Button and Search Bar --> <!-- Search Results -->
<li class="nav-item position-relative"> <div id="searchResults" class="border p-2 bg-white search-results"></div>
<a href="#" class="nav-link" id="search-icon"> </div>
<img class="navbar-icon" src="images/search.svg" alt="icon" width="24" height="24"> </li>
</a>
<!-- Search Bar -->
<div class="collapse position-absolute" id="navbarSearch">
<form class="d-flex p-2 bg-white border search-form" id="searchForm">
<input class="form-control search-input" type="search" th:placeholder="#{home.searchBar}" aria-label="Search" id="navbarSearchInput">
</form>
<!-- Search Results -->
<div id="searchResults" class="border p-2 bg-white search-results"></div>
</div>
</li>
<style>
#search-icon i {
font-size: 24px; /* Adjust this to your desired size */
transition: color 0.3s;
}
#search-icon:hover i {
color: #666; /* Adjust this to your hover color */
}
#navbarSearch {
transition: all 0.3s;
max-height: 0;
overflow: hidden;
}
#navbarSearch.show {
max-height: 300px; /* Adjust this to your desired max height */
}
.search-input {
transition: border 0.3s, box-shadow 0.3s;
}
.search-input:focus {
border-color: #666; /* Adjust this to your focus color */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adjust this to your desired shadow */
}
#searchResults {
max-width: 300px; /* Adjust to your preferred width */
transition: height 0.3s ease; /* Smooth height transition */
}
/* Set a fixed height and styling for each search result item */
.search-results a {
display: flex;
align-items: center;
gap: 10px; /* space between icon and text */
height: 40px; /* Adjust based on your design */
overflow: hidden; /* Prevent content from overflowing */
white-space: nowrap; /* Prevent text from wrapping to next line */
text-overflow: ellipsis; /* Truncate text if it's too long */
}
</style>
</ul> </ul>
</div>
</div> </div>
<script src="js/favourites.js"></script>
<script src="js/search.js"></script>
</nav>
</div> <th:block th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></th:block>
<script src="js/favourites.js"></script> <div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel" aria-hidden="true">
<script src="js/search.js"></script> <div class="modal-dialog modal-dialog-centered" role="document">
</nav> <div class="modal-content dark-card">
<div th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></div>
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content dark-card">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5> <h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<p class="mb-0" th:utext="#{settings.appVersion} + ' ' + ${@appVersion}"></p> <p class="mb-0" th:utext="#{settings.appVersion} + ' ' + ${@appVersion}"></p>
<a href="https://github.com/sponsors/Frooodle" target="_blank"> <a href="https://github.com/sponsors/Frooodle" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary">Sponsor Stirling-PDF</button> <button type="button" class="btn btn-sm btn-outline-primary">Sponsor Stirling-PDF</button>
</a>
<a href="swagger-ui/index.html" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary">API</button>
</a>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:utext="#{settings.update}"></button>
</a>
</div>
<div class="mb-3">
<label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label>
<select class="form-control" id="downloadOption">
<option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option>
<option value="newWindow" th:utext="#{settings.downloadOption.2}"></option>
<option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option>
</select>
</div>
<div class="mb-3">
<label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label>
<input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4">
<span id="zipThresholdValue" class="ms-2"></span>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="boredWaiting">
<label class="form-check-label" for="boredWaiting" th:text="#{bored}"></label>
</div>
<a th:if="${@loginEnabled}" href="account" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary" th:text="#{settings.accountSettings}">Account Settings</button>
</a> </a>
<a href="swagger-ui/index.html" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary">API</button>
</a>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:utext="#{settings.update}"></button>
</a>
</div>
<div class="mb-3">
<label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label>
<select class="form-control" id="downloadOption">
<option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option>
<option value="newWindow" th:utext="#{settings.downloadOption.2}"></option>
<option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option>
</select>
</div>
<div class="mb-3">
<label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label>
<input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4">
<span id="zipThresholdValue" class="ms-2"></span>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="boredWaiting">
<label class="form-check-label" for="boredWaiting" th:text="#{bored}"></label>
</div>
<a th:if="${@loginEnabled}" href="account" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary" th:text="#{settings.accountSettings}">Account Settings</button>
</a>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a th:if="${@loginEnabled}" href="logout"> <a th:if="${@loginEnabled}" href="logout">
<button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button> <button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button>
</a> </a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div> </div>
</div>
</div> </div>
</div>
<script src="js/settings.js"></script>
</div> </div>
</div>
<script src="js/settings.js"></script>
</div>

View File

@ -1,6 +1,6 @@
<div th:fragment="navbarEntry (endpoint, imgSrc, titleKey, descKey, tagKey)" th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}"> <th:block th:fragment="navbarEntry(endpoint, imgSrc, titleKey, descKey, tagKey)" th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
<a class="dropdown-item" href="#" th:href="@{${endpoint}}" th:classappend="${endpoint.equals(currentPage)} ? 'active' : ''" th:title="#{${descKey}}" th:data-bs-tags="#{${tagKey}}"> <a class="dropdown-item" href="#" th:href="@{${endpoint}}" th:classappend="${endpoint.equals(currentPage)} ? 'active' : ''" th:title="#{${descKey}}" th:data-bs-tags="#{${tagKey}}">
<img class="icon" th:src="@{${imgSrc}}" alt="icon"> <img class="icon" th:src="@{${imgSrc}}" alt="icon">
<span class="icon-text" th:text="#{${titleKey}}"></span> <span class="icon-text" th:text="#{${titleKey}}"></span>
</a> </a>
</div> </th:block>

View File

@ -1,115 +1,106 @@
<!doctype html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block> <div id="page-container">
<div id="content-wrap">
<link rel="stylesheet" href="css/home.css"> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<!-- Jumbotron --> <!-- Jumbotron -->
<div class="bg-light p-5 rounded d-none d-md-block" id="jumbotron"> <div class="bg-light p-5 rounded d-none d-md-block" id="jumbotron">
<div class="container"> <div class="container">
<h1 class="display-4" th:text="${@appName}"></h1> <h1 class="display-4" th:text="${@appName}"></h1>
<p class="lead" th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}"></p> <p class="lead" th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}"></p>
</div> </div>
</div> </div>
<br class="d-md-none"> <br class="d-md-none">
<!-- Features --> <!-- Features -->
<script src="js/homecard.js"></script> <script src="js/homecard.js"></script>
<div class=" container"> <div class=" container">
<br> <br>
<input type="text" id="searchBar" onkeyup="filterCards()" th:placeholder="#{home.searchBar}"> <input type="text" id="searchBar" onkeyup="filterCards()" th:placeholder="#{home.searchBar}">
<div class="features-container "> <div class="features-container">
<th:block th:if="${@enableAlphaFunctionality}">
<div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg', tags=#{pipeline.tags})}"></div>
</th:block>
<div th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', svgPath='images/book-opened.svg', tags=#{viewPdf.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg', tags=#{multiTool.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg', tags=#{merge.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg', tags=#{split.tags})}"></div>
<th:block th:if="${@enableAlphaFunctionality}"> <div th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', svgPath='images/arrow-clockwise.svg', tags=#{rotate.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div> <div th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', svgPath='images/crop.svg', tags=#{crop.tags})}"></div>
</th:block> <div th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', svgPath='images/add-page-numbers.svg', tags=#{add-page-numbers.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', svgPath='images/book-opened.svg')}"></div> <div th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', svgPath='images/adjust-contrast.svg', tags=#{adjust-contrast.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div> <div th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', svgPath='images/image.svg', tags=#{imageToPdf.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', svgPath='images/image.svg', tags=#{pdfToImage.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', svgPath='images/arrow-clockwise.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-organizer', cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', svgPath='images/sort-numeric-down.svg', tags=#{pdfOrganiser.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', svgPath='images/crop.svg')}"></div> <div th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', svgPath='images/file-earmark-richtext.svg', tags=#{addImage.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', svgPath='images/add-page-numbers.svg')}"></div> <div th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', svgPath='images/droplet.svg', tags=#{watermark.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', svgPath='images/adjust-contrast.svg')}"></div> <div th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', svgPath='images/file.svg', tags=#{fileToPDF.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', svgPath='images/image.svg')}"></div> <div th:replace="~{fragments/card :: card(id='remove-pages', cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', svgPath='images/file-earmark-x.svg', tags=#{removePages.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', svgPath='images/image.svg')}"></div> <div th:replace="~{fragments/card :: card(id='add-password', cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', svgPath='images/lock.svg', tags=#{addPassword.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-organizer', cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', svgPath='images/sort-numeric-down.svg')}"></div> <div th:replace="~{fragments/card :: card(id='remove-password', cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', svgPath='images/unlock.svg', tags=#{removePassword.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', svgPath='images/file-earmark-richtext.svg')}"></div> <div th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', svgPath='images/file-zip.svg', tags=#{compressPdfs.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', svgPath='images/droplet.svg')}"></div> <div th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', svgPath='images/clipboard-data.svg', tags=#{changeMetadata.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', svgPath='images/file.svg')}"></div> <div th:replace="~{fragments/card :: card(id='change-permissions', cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', svgPath='images/shield-lock.svg', tags=#{permissions.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='remove-pages', cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', svgPath='images/file-earmark-x.svg')}"></div> <div th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', svgPath='images/search.svg', tags=#{ocr.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='add-password', cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', svgPath='images/lock.svg')}"></div> <div th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', svgPath='images/images.svg', tags=#{extractImages.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='remove-password', cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', svgPath='images/unlock.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', svgPath='images/file-earmark-pdf.svg', tags=#{pdfToPDFA.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', svgPath='images/file-zip.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-word', cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', svgPath='images/file-earmark-word.svg', tags=#{PDFToWord.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', svgPath='images/clipboard-data.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-presentation', cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', svgPath='images/file-earmark-ppt.svg', tags=#{PDFToPresentation.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='change-permissions', cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', svgPath='images/shield-lock.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-text', cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', svgPath='images/filetype-txt.svg', tags=#{PDFToText.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', svgPath='images/search.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-html', cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', svgPath='images/filetype-html.svg', tags=#{PDFToHTML.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', svgPath='images/images.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-xml', cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', svgPath='images/filetype-xml.svg', tags=#{PDFToXML.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', svgPath='images/file-earmark-pdf.svg')}"></div> <div th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', svgPath='images/scanner.svg', tags=#{ScannerImageSplit.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-word', cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', svgPath='images/file-earmark-word.svg')}"></div> <div th:replace="~{fragments/card :: card(id='sign', cardTitle=#{home.sign.title}, cardText=#{home.sign.desc}, cardLink='sign', svgPath='images/sign.svg', tags=#{sign.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-presentation', cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', svgPath='images/file-earmark-ppt.svg')}"></div> <div th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', svgPath='images/flatten.svg', tags=#{flatten.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-text', cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', svgPath='images/filetype-txt.svg')}"></div> <div th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', svgPath='images/wrench.svg', tags=#{repair.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-html', cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', svgPath='images/filetype-html.svg')}"></div> <div th:replace="~{fragments/card :: card(id='remove-blanks', cardTitle=#{home.removeBlanks.title}, cardText=#{home.removeBlanks.desc}, cardLink='remove-blanks', svgPath='images/blank-file.svg', tags=#{removeBlanks.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-xml', cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', svgPath='images/filetype-xml.svg')}"></div> <div th:replace="~{fragments/card :: card(id='remove-annotations', cardTitle=#{home.removeAnnotations.title}, cardText=#{home.removeAnnotations.desc}, cardLink='remove-annotations', svgPath='images/no-chat.svg', tags=#{removeAnnotations.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', svgPath='images/scales.svg', tags=#{compare.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', svgPath='images/scanner.svg')}"></div> <div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg', tags=#{certSign.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='sign', cardTitle=#{home.sign.title}, cardText=#{home.sign.desc}, cardLink='sign', svgPath='images/sign.svg')}"></div> <div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg', tags=#{pageLayout.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', svgPath='images/flatten.svg')}"></div> <div th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', svgPath='images/scale-pages.svg', tags=#{scalePages.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', svgPath='images/wrench.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='remove-blanks', cardTitle=#{home.removeBlanks.title}, cardText=#{home.removeBlanks.desc}, cardLink='remove-blanks', svgPath='images/blank-file.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='remove-annotations', cardTitle=#{home.removeAnnotations.title}, cardText=#{home.removeAnnotations.desc}, cardLink='remove-annotations', svgPath='images/no-chat.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', svgPath='images/scales.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', svgPath='images/scale-pages.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', svgPath='images/fonts.svg')}"></div> <div th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', svgPath='images/fonts.svg', tags=#{auto-rename.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='auto-split-pdf', cardTitle=#{home.autoSplitPDF.title}, cardText=#{home.autoSplitPDF.desc}, cardLink='auto-split-pdf', svgPath='images/layout-split.svg')}"></div> <div th:replace="~{fragments/card :: card(id='auto-split-pdf', cardTitle=#{home.autoSplitPDF.title}, cardText=#{home.autoSplitPDF.desc}, cardLink='auto-split-pdf', svgPath='images/layout-split.svg', tags=#{autoSplitPDF.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', svgPath='images/sanitize.svg')}"></div> <div th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', svgPath='images/sanitize.svg', tags=#{sanitizePdf.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', svgPath='images/url.svg')}"></div> <div th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', svgPath='images/url.svg', tags=#{URLToPDF.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', svgPath='images/html.svg')}"></div> <div th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', svgPath='images/html.svg', tags=#{HTMLToPDF.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='markdown-to-pdf', cardTitle=#{home.MarkdownToPDF.title}, cardText=#{home.MarkdownToPDF.desc}, cardLink='markdown-to-pdf', svgPath='images/markdown.svg')}"></div> <div th:replace="~{fragments/card :: card(id='markdown-to-pdf', cardTitle=#{home.MarkdownToPDF.title}, cardText=#{home.MarkdownToPDF.desc}, cardLink='markdown-to-pdf', svgPath='images/markdown.svg', tags=#{MarkdownToPDF.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='get-info-on-pdf', cardTitle=#{home.getPdfInfo.title}, cardText=#{home.getPdfInfo.desc}, cardLink='get-info-on-pdf', svgPath='images/info.svg')}"></div> <div th:replace="~{fragments/card :: card(id='get-info-on-pdf', cardTitle=#{home.getPdfInfo.title}, cardText=#{home.getPdfInfo.desc}, cardLink='get-info-on-pdf', svgPath='images/info.svg', tags=#{getPdfInfo.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', svgPath='images/extract.svg')}"></div> <div th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', svgPath='images/extract.svg', tags=#{extractPage.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', svgPath='images/single-page.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', svgPath='images/single-page.svg', tags=#{PdfToSinglePage.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', svgPath='images/js.svg')}"></div> <div th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', svgPath='images/js.svg', tags=#{showJS.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', svgPath='images/eraser-fill.svg')}"></div> <div th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', svgPath='images/eraser-fill.svg', tags=#{autoRedact.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-csv', cardTitle=#{home.tableExtraxt.title}, cardText=#{home.tableExtraxt.desc}, cardLink='pdf-to-csv', svgPath='images/pdf-csv.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='split-by-size-or-count', cardTitle=#{home.autoSizeSplitPDF.title}, cardText=#{home.autoSizeSplitPDF.desc}, cardLink='split-by-size-or-count', svgPath='images/layout-split.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='overlay-pdf', cardTitle=#{home.overlay-pdfs.title}, cardText=#{home.overlay-pdfs.desc}, cardLink='overlay-pdf', svgPath='images/overlay.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='split-pdf-by-sections', cardTitle=#{home.split-by-sections.title}, cardText=#{home.split-by-sections.desc}, cardLink='split-pdf-by-sections', svgPath='images/layout-split.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='book-to-pdf', cardTitle=#{home.BookToPDF.title}, cardText=#{home.BookToPDF.desc}, cardLink='book-to-pdf', svgPath='images/book.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-book', cardTitle=#{home.PDFToBook.title}, cardText=#{home.PDFToBook.desc}, cardLink='pdf-to-book', svgPath='images/book.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', svgPath='images/stamp.svg')}"></div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
<div th:replace="~{fragments/card :: card(id='pdf-to-csv', cardTitle=#{home.tableExtraxt.title}, cardText=#{home.tableExtraxt.desc}, cardLink='pdf-to-csv', svgPath='images/pdf-csv.svg', tags=#{tableExtraxt.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='split-by-size-or-count', cardTitle=#{home.autoSizeSplitPDF.title}, cardText=#{home.autoSizeSplitPDF.desc}, cardLink='split-by-size-or-count', svgPath='images/layout-split.svg', tags=#{autoSizeSplitPDF.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='overlay-pdf', cardTitle=#{home.overlay-pdfs.title}, cardText=#{home.overlay-pdfs.desc}, cardLink='overlay-pdf', svgPath='images/overlay.svg', tags=#{overlay-pdfs.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='split-pdf-by-sections', cardTitle=#{home.split-by-sections.title}, cardText=#{home.split-by-sections.desc}, cardLink='split-pdf-by-sections', svgPath='images/layout-split.svg', tags=#{split-by-sections.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='book-to-pdf', cardTitle=#{home.BookToPDF.title}, cardText=#{home.BookToPDF.desc}, cardLink='book-to-pdf', svgPath='images/book.svg', tags=#{BookToPDF.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-book', cardTitle=#{home.PDFToBook.title}, cardText=#{home.PDFToBook.desc}, cardLink='pdf-to-book', svgPath='images/book.svg', tags=#{PDFToBook.tags})}"></div>
<div th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', svgPath='images/stamp.svg', tags=#{AddStampRequest.tags})}"></div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html> </html>

View File

@ -1,56 +1,42 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
th:lang-direction="#{language.direction}" <head>
xmlns:th="http://www.thymeleaf.org"> <th:block th:insert="~{fragments/common :: head(title=#{licenses.title}, header=#{licenses.title})}"></th:block>
</head>
<th:block <body>
th:insert="~{fragments/common :: head(title=#{licenses.title}, header=#{licenses.title})}"></th:block> <div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br /><br />
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{licenses.header}">3rd Party licenses</h2>
<table class="table table-striped">
<thead>
<tr>
<th th:text="#{licenses.module}">Module</th>
<th th:text="#{licenses.version}">Version</th>
<th th:text="#{licenses.license}">License</th>
</tr>
</thead>
<tbody>
<tr th:each="dep : ${dependencies}">
<td><a th:href="${dep.moduleUrl}"
th:text="${dep.moduleName}"></a></td>
<td th:text="${dep.moduleVersion}"></td>
<td><a th:href="${dep.moduleLicenseUrl}"
th:text="${dep.moduleLicense}"></a></td>
</tr>
</tbody>
</table>
</div>
<style> </div>
td a { </div>
text-decoration: none; </div>
} <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
td a:hover, td a:focus { </body>
text-decoration: underline;
/* Adds underline on hover/focus for clarity */
}
</style>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{licenses.header}">3rd Party licenses</h2>
<table class="table table-striped">
<thead>
<tr>
<th th:text="#{licenses.module}">Module</th>
<th th:text="#{licenses.version}">Version</th>
<th th:text="#{licenses.license}">License</th>
</tr>
</thead>
<tbody>
<tr th:each="dep : ${dependencies}">
<td><a th:href="${dep.moduleUrl}"
th:text="${dep.moduleName}"></a></td>
<td th:text="${dep.moduleVersion}"></td>
<td><a th:href="${dep.moduleLicenseUrl}"
th:text="${dep.moduleLicense}"></a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html> </html>

View File

@ -1,313 +1,175 @@
<!doctype html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{login.title}, header=#{login.header})}"></th:block>
<link rel="stylesheet" href="css/login.css">
</head>
<th:block th:insert="~{fragments/common :: head(title=#{login.title}, header=#{login.header})}"></th:block> <body>
<script src="js/darkmode.js"></script> <div class="your-container-class"></div>
<style> <div class="container-flex">
html, body { <main class="form-signin text-center">
height: 100%; <script>
} function setInputMode(elementId, mode) {
var inputElement = document.getElementById(elementId);
body { if (!inputElement) return; // If the element doesn't exist, exit the function
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
} switch (mode) {
case "on":
.form-signin { inputElement.classList.add("bg-dark", "text-light");
width: 100%; inputElement.classList.remove("bg-light", "text-dark");
max-width: 330px; break;
padding: 15px; case "off":
margin: auto; inputElement.classList.add("bg-light", "text-dark");
} inputElement.classList.remove("bg-dark", "text-light");
break;
.form-signin .checkbox { case "rainbow":
font-weight: 400; // Assuming you have defined some classes for rainbow mode
} break;
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.container-flex {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%; /* Set width to 100% */
align-items: center; /* Center its children horizontally */
}
.footer-bottom {
margin-top: auto;
}
body.light-mode input:-webkit-autofill,
body.light-mode input:-webkit-autofill:hover,
body.light-mode input:-webkit-autofill:focus,
body.light-mode input:-webkit-autofill:active {
-webkit-text-fill-color: #212529; /* Dark font color */
-webkit-box-shadow: 0 0 0 1000px #f8f9fa inset; /* Light background color */
}
/* Dark Mode */
body.dark-mode input:-webkit-autofill,
body.dark-mode input:-webkit-autofill:hover,
body.dark-mode input:-webkit-autofill:focus,
body.dark-mode input:-webkit-autofill:active {
-webkit-text-fill-color: #f8f9fa; /* Light font color */
-webkit-box-shadow: 0 0 0 1000px #212529 inset; /* Dark background color */
}
/* Light Mode */
body.light-mode .form-floating > input:focus + label {
color: #212529 !important; /* Dark text for light background */
}
/* Dark Mode */
body.dark-mode .form-floating > input:focus + label {
color: #fff !important; /* Light text for dark background */
}
body.light-mode .form-floating > label {
color: #212529 !important; /* Dark text for light background */
}
body.dark-mode .form-floating > label {
color: #fff !important; /* Light text for dark background */
}
/* Removing default styles for ul and li */
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
list-style: none;
}
/* Positioning the container of these elements to the top right */
.your-container-class {
position: absolute;
top: 0;
right: 0;
display: flex;
}
/* Styling for the dropdown */
.dropdown-menu {
min-width: 200px; /* or whatever width you prefer */
}
.navbar-icon {
width: 40px;
height: 40px;
}
</style>
<body>
<div class="your-container-class">
</div>
<div class="container-flex">
<main class="form-signin text-center">
<script>
function setInputMode(elementId, mode) {
var inputElement = document.getElementById(elementId);
if (!inputElement) return; // If the element doesn't exist, exit the function
switch (mode) {
case "on":
inputElement.classList.add("bg-dark", "text-light");
inputElement.classList.remove("bg-light", "text-dark");
break;
case "off":
inputElement.classList.add("bg-light", "text-dark");
inputElement.classList.remove("bg-dark", "text-light");
break;
case "rainbow":
// Assuming you have defined some classes for rainbow mode
break;
}
}
document.addEventListener('modeChanged', function(e) {
var mode = e.detail;
setInputMode("username", mode);
setInputMode("password", mode);
document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
switch (mode) {
case "on":
document.body.classList.add("dark-mode");
break;
case "off":
document.body.classList.add("light-mode");
break;
case "rainbow":
document.body.classList.add("rainbow-mode");
break;
}
});
document.addEventListener('DOMContentLoaded', function() {
const defaultLocale = document.documentElement.lang || 'en_GB';
const storedLocale = localStorage.getItem('languageCode') || defaultLocale;
const currentURL = new URL(window.location.href);
const urlParams = currentURL.searchParams;
const currentLangParam = urlParams.get('lang') || defaultLocale;
if (defaultLocale !== storedLocale && currentLangParam !== storedLocale) {
urlParams.set('lang', storedLocale);
currentURL.search = urlParams.toString();
window.location.href = currentURL.toString();
return;
}
const dropdown = document.getElementById('languageDropdown');
const dropdownItems = document.querySelectorAll('.lang_dropdown-item');
let activeItem;
for (let i = 0; i < dropdownItems.length; i++) {
const item = dropdownItems[i];
item.classList.remove('active');
if (item.dataset.bsLanguageCode === storedLocale) {
item.classList.add('active');
activeItem = item;
}
item.addEventListener('click', handleDropdownItemClick);
}
if (activeItem) {
dropdown.innerHTML = activeItem.innerHTML; // This will set the dropdown button's content to the active language's flag and name
}
// Additional functionality that was in your provided code:
document.querySelectorAll('.nav-item.dropdown').forEach((element) => {
const dropdownMenu = element.querySelector(".dropdown-menu");
if (dropdownMenu.id !== 'favoritesDropdown' && dropdownMenu.children.length <= 2 && dropdownMenu.querySelectorAll("hr.dropdown-divider").length === dropdownMenu.children.length) {
if (element.previousElementSibling && element.previousElementSibling.classList.contains('nav-item') && element.previousElementSibling.classList.contains('nav-item-separator')) {
element.previousElementSibling.remove();
} }
element.remove(); }
}
});
// Sort languages by alphabet document.addEventListener('modeChanged', function(e) {
const list = Array.from(document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').children).filter(child => child.matches('a')); var mode = e.detail;
list.sort(function(a, b) {
var A = a.textContent.toUpperCase();
var B = b.textContent.toUpperCase();
return (A < B) ? -1 : (A > B) ? 1 : 0;
}).forEach(node => document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').appendChild(node));
});
function handleDropdownItemClick(event) { setInputMode("username", mode);
event.preventDefault(); setInputMode("password", mode);
const languageCode = event.currentTarget.dataset.bsLanguageCode; document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
const dropdown = document.getElementById('languageDropdown');
if (languageCode) { switch (mode) {
localStorage.setItem('languageCode', languageCode); case "on":
const currentLang = document.documentElement.getAttribute('lang'); document.body.classList.add("dark-mode");
if (currentLang !== languageCode) { break;
console.log("currentLang", currentLang) case "off":
console.log("languageCode", languageCode) document.body.classList.add("light-mode");
const currentUrl = window.location.href; break;
if (currentUrl.indexOf('?lang=') === -1) { case "rainbow":
window.location.href = currentUrl + '?lang=' + languageCode; document.body.classList.add("rainbow-mode");
} else { break;
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode); }
} });
}
dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content document.addEventListener('DOMContentLoaded', function() {
} else {
console.error("Language code is not set for this item."); const defaultLocale = document.documentElement.lang || 'en_GB';
} const storedLocale = localStorage.getItem('languageCode') || defaultLocale;
}
const currentURL = new URL(window.location.href);
const urlParams = currentURL.searchParams;
const currentLangParam = urlParams.get('lang') || defaultLocale;
if (defaultLocale !== storedLocale && currentLangParam !== storedLocale) {
urlParams.set('lang', storedLocale);
currentURL.search = urlParams.toString();
window.location.href = currentURL.toString();
return;
}
</script> const dropdown = document.getElementById('languageDropdown');
<div th:if="${logoutMessage}" class="alert alert-success" const dropdownItems = document.querySelectorAll('.lang_dropdown-item');
th:text="${logoutMessage}"></div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'credsUpdated'}" class="alert alert-success"> let activeItem;
<span th:text="#{changedCredsMessage}">Default message if not found</span> for (let i = 0; i < dropdownItems.length; i++) {
</div> const item = dropdownItems[i];
<form th:action="@{login}" method="post"> item.classList.remove('active');
<img class="mb-4" src="favicon.svg?v=2" alt="" width="144" height="144"> if (item.dataset.bsLanguageCode === storedLocale) {
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1> item.classList.add('active');
<h2 class="h5 mb-3 fw-normal" th:text="#{login.signinTitle}">Please sign in</h2> activeItem = item;
}
item.addEventListener('click', handleDropdownItemClick);
}
<div class="form-floating"> if (activeItem) {
<input type="text" class="form-control bg-dark text-light" id="username" name="username" dropdown.innerHTML = activeItem.innerHTML; // This will set the dropdown button's content to the active language's flag and name
placeholder="admin"> <label for="username" th:text="#{username}">Username</label> }
</div>
<div class="form-floating">
<input type="password" class="form-control bg-dark text-light" id="password" name="password"
placeholder="Password"> <label for="password" th:text="#{password}">Password</label>
</div>
<div class="checkbox mb-3"> // Additional functionality that was in your provided code:
<label > <input type="checkbox" value="remember-me">
<span th:text="#{login.rememberme}"></span>
</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit" th:text="#{login.signin}">Sign
in</button>
</form>
<div class="mt-3"> <!-- Added a margin-top for spacing -->
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
English (GB) <!-- Default language placeholder -->
</button>
<div class="dropdown-menu" aria-labelledby="languageDropdown">
<!-- Here's where the fragment will be included -->
<th:block th:replace="~{fragments/languages :: langs}"></th:block>
</div>
</div>
</div>
document.querySelectorAll('.nav-item.dropdown').forEach((element) => {
const dropdownMenu = element.querySelector(".dropdown-menu");
if (dropdownMenu.id !== 'favoritesDropdown' && dropdownMenu.children.length <= 2 && dropdownMenu.querySelectorAll("hr.dropdown-divider").length === dropdownMenu.children.length) {
if (element.previousElementSibling && element.previousElementSibling.classList.contains('nav-item') && element.previousElementSibling.classList.contains('nav-item-separator')) {
element.previousElementSibling.remove();
}
element.remove();
}
});
// Sort languages by alphabet
const list = Array.from(document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').children).filter(child => child.matches('a'));
list.sort(function(a, b) {
var A = a.textContent.toUpperCase();
var B = b.textContent.toUpperCase();
return (A < B) ? -1 : (A > B) ? 1 : 0;
}).forEach(node => document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').appendChild(node));
});
<div class="text-danger text-center"> function handleDropdownItemClick(event) {
<div th:if="${error == 'badcredentials'}" th:text="#{login.invalid}">Invalid username or event.preventDefault();
password.</div> const languageCode = event.currentTarget.dataset.bsLanguageCode;
<div th:if="${error == 'locked'}" th:text="#{login.locked}">Your account has been locked. const dropdown = document.getElementById('languageDropdown');
</div>
</div>
</main> if (languageCode) {
localStorage.setItem('languageCode', languageCode);
const currentLang = document.documentElement.getAttribute('lang');
if (currentLang !== languageCode) {
console.log("currentLang", currentLang)
console.log("languageCode", languageCode)
const currentUrl = window.location.href;
if (currentUrl.indexOf('?lang=') === -1) {
window.location.href = currentUrl + '?lang=' + languageCode;
} else {
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
}
}
dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content
} else {
console.error("Language code is not set for this item.");
}
}
</script>
<div th:if="${logoutMessage}" class="alert alert-success" th:text="${logoutMessage}"></div>
<div th:if="${param.messageType != null and param.messageType.size() > 0 and param.messageType[0] == 'credsUpdated'}" class="alert alert-success">
<span th:text="#{changedCredsMessage}">Default message if not found</span>
</div>
<form th:action="@{login}" method="post">
<img class="mb-4" src="favicon.svg?v=2" alt="" width="144" height="144">
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
<h2 class="h5 mb-3 fw-normal" th:text="#{login.signinTitle}">Please sign in</h2>
<div class="form-floating">
<input type="text" class="form-control bg-dark text-light" id="username" name="username" placeholder="admin"> <label for="username" th:text="#{username}">Username</label>
</div>
<div class="form-floating">
<input type="password" class="form-control bg-dark text-light" id="password" name="password" placeholder="Password"> <label for="password" th:text="#{password}">Password</label>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div> <div class="checkbox mb-3">
</div> <label > <input type="checkbox" value="remember-me">
<span th:text="#{login.rememberme}"></span>
</label>
</body> </div>
<button class="w-100 btn btn-lg btn-primary" type="submit" th:text="#{login.signin}">Sign in</button>
</form>
<div class="mt-3"> <!-- Added a margin-top for spacing -->
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
English (GB) <!-- Default language placeholder -->
</button>
<div class="dropdown-menu" aria-labelledby="languageDropdown">
<!-- Here's where the fragment will be included -->
<th:block th:replace="~{fragments/languages :: langs}"></th:block>
</div>
</div>
</div>
<div class="text-danger text-center">
<div th:if="${error == 'badcredentials'}" th:text="#{login.invalid}">Invalid username or password.</div>
<div th:if="${error == 'locked'}" th:text="#{login.locked}">Your account has been locked. </div>
</div>
</main>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html> </html>

View File

@ -1,40 +1,39 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{merge.title}, header=#{merge.header})}"></th:block>
<link rel="stylesheet" href="css/merge.css">
</head>
<th:block th:insert="~{fragments/common :: head(title=#{merge.title}, header=#{merge.header})}"></th:block> <body>
<body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container" id="dropContainer"> <div class="container" id="dropContainer">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{merge.header}"></h2> <h2 th:text="#{merge.header}"></h2>
<form action="api/v1/general/merge-pdfs" method="post" enctype="multipart/form-data"> <form action="api/v1/general/merge-pdfs" method="post" enctype="multipart/form-data">
<div class="mb-3"> <div class="mb-3">
<label th:text="#{multiPdfDropPrompt}"></label> <label th:text="#{multiPdfDropPrompt}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=true, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=true, accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<ul id="selectedFiles" class="list-group"></ul>
</div>
<div class="mb-3 text-center">
<button type="button" id="sortByNameBtn" class="btn btn-info" th:text="#{merge.sortByName}"></button>
<button type="button" id="sortByDateBtn" class="btn btn-info" th:text="#{merge.sortByDate}"></button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{merge.submit}"></button>
</div>
</form>
<link rel="stylesheet" href="css/merge.css">
<script src="js/merge.js"></script>
</div>
</div> </div>
<div class="mb-3">
<ul id="selectedFiles" class="list-group"></ul>
</div>
<div class="mb-3 text-center">
<button type="button" id="sortByNameBtn" class="btn btn-info" th:text="#{merge.sortByName}"></button>
<button type="button" id="sortByDateBtn" class="btn btn-info" th:text="#{merge.sortByDate}"></button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{merge.submit}"></button>
</div>
</form>
<script src="js/merge.js"></script>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,141 +1,112 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<head> <th:block th:insert="~{fragments/common :: head(title=#{addImage.title}, header=#{addImage.header})}"></th:block>
<th:block th:insert="~{fragments/common :: head(title=#{addImage.title}, header=#{addImage.header})}"></th:block>
<script src="js/thirdParty/interact.min.js"></script> <script src="js/thirdParty/interact.min.js"></script>
<link rel="stylesheet" href="css/add-image.css">
</head>
</head> <body>
<body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{addImage.header}"></h2> <h2 th:text="#{addImage.header}"></h2>
<!-- pdf selector --> <!-- pdf selector -->
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
<script> <script>
let originalFileName = ''; let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => { document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (file) { if (file) {
originalFileName = file.name.replace(/\.[^/.]+$/, ""); originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer(); const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js' pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0); await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach(el => { document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = ''; el.style.cssText = '';
}) });
} }
}); });
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".show-on-file-selected").forEach(el => { document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = "display:none !important"; el.style.cssText = "display:none !important";
}) })
}); });
</script> </script>
<div class="tab-group show-on-file-selected"> <div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{addImage.upload}"> <div class="tab-container" th:title="#{addImage.upload}">
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multiple=true, accept='image/*', inputText=#{imgPrompt})}"></div> <div th:replace="~{fragments/common :: fileSelector(name='image-upload', multiple=true, accept='image/*', inputText=#{imgPrompt})}"></div>
<script> <script>
const imageUpload = document.querySelector('input[name=image-upload]'); const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', e => { imageUpload.addEventListener('change', e => {
if(!e.target.files) { if(!e.target.files) {
return; return;
} }
for (const imageFile of e.target.files) { for (const imageFile of e.target.files) {
var reader = new FileReader(); var reader = new FileReader();
reader.readAsDataURL(imageFile); reader.readAsDataURL(imageFile);
reader.onloadend = function (e) { reader.onloadend = function (e) {
DraggableUtils.createDraggableCanvasFromUrl(e.target.result); DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
}; };
} }
}); });
</script> </script>
</div>
</div>
<!-- draggables box -->
<div id="box-drag-container" class="show-on-file-selected">
<canvas id="pdf-canvas"></canvas>
<script src="js/draggable-utils.js"></script>
<div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
</svg>
</button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
</button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
<style>
#box-drag-container {
position: relative;
margin: 20px 0;
}
#pdf-canvas {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
width: 100%;
}
.draggable-buttons-box {
position: absolute;
top: 0;
padding: 10px;
width: 100%;
display: flex;
gap: 5px;
}
.draggable-buttons-box > button {
z-index: 10;
background-color: rgba(13, 110, 253, 0.1);
}
.draggable-canvas {
border: 1px solid red;
position: absolute;
touch-action: none;
user-select: none;
top: 0px;
left: 0;
}
</style>
</div>
<!-- download button -->
<div class="margin-auto-parent">
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button>
</div>
<script>
document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_addedImage.pdf';
link.click();
});
</script>
</div>
</div> </div>
</div>
<!-- draggables box -->
<div id="box-drag-container" class="show-on-file-selected">
<canvas id="pdf-canvas"></canvas>
<script src="js/draggable-utils.js"></script>
<div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
</svg>
</button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
</button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
</div>
<!-- download button -->
<div class="margin-auto-parent">
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button>
</div>
<script>
document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_addedImage.pdf';
link.click();
});
</script>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,154 +1,137 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
th:lang-direction="#{language.direction}" <head>
xmlns:th="http://www.thymeleaf.org"> <th:block th:insert="~{fragments/common :: head(title=#{addPageNumbers.title}, header=#{addPageNumbers.header})}"></th:block>
<style>
.a4container {
position: relative;
width: 50%;
aspect-ratio: 0.707;
border: 1px solid #ddd;
box-sizing: border-box;
background-color: white;
}
<th:block .pageNumber {
th:insert="~{fragments/common :: head(title=#{addPageNumbers.title}, header=#{addPageNumbers.header})}"></th:block> position: absolute;
display: flex;
justify-content: center;
align-items: center;
font-size: 1em;
color: #333;
cursor: pointer;
background-color: #ccc;
width: 15%;
height: 15%;
transform: translate(-50%, -50%);
}
.pageNumber:hover {
background-color: #eee;
}
<body> #myForm {
<th:block th:insert="~{fragments/common :: game}"></th:block> display: flex;
<div id="page-container"> justify-content: center;
<div id="content-wrap"> align-items: center;
<div th:insert="~{fragments/navbar.html :: navbar}"></div> margin-top: 20px;
<br> <br> }
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{addPageNumbers.header}"></h2>
<form method="post" enctype="multipart/form-data"
th:action="@{api/v1/misc/add-page-numbers}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br>
<div class="mb-3">
<label for="customMargin" th:text="#{addPageNumbers.selectText.2}"></label> <select
class="form-control" id="customMargin" name="customMargin"
required>
<option value="small" th:text="#{sizes.small}"></option>
<option value="medium" selected th:text="#{sizes.medium}"></option>
<option value="large" th:text="#{sizes.large}"></option>
<option value="x-large" th:text="#{sizes.x-large}"></option>
</select>
</div>
<style>
.a4container {
position: relative;
width: 50%;
aspect-ratio: 0.707;
border: 1px solid #ddd;
box-sizing: border-box;
background-color: white;
}
.pageNumber { .selectedPosition {
position: absolute; background-color: #0a0;
display: flex; }
justify-content: center;
align-items: center;
font-size: 1em;
color: #333;
cursor: pointer;
background-color: #ccc;
width: 15%;
height: 15%;
transform: translate(-50%, -50%);
}
.pageNumber:hover { .selectedPosition.selectedHovered {
background-color: #eee; background-color: #006600;
} }
</style>
</head>
#myForm { <body>
display: flex; <th:block th:insert="~{fragments/common :: game}"></th:block>
justify-content: center; <div id="page-container">
align-items: center; <div id="content-wrap">
margin-top: 20px; <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
} <br /><br />
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{addPageNumbers.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/misc/add-page-numbers}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br>
<div class="mb-3">
<label for="customMargin" th:text="#{addPageNumbers.selectText.2}"></label>
<select class="form-control" id="customMargin" name="customMargin" required>
<option value="small" th:text="#{sizes.small}"></option>
<option value="medium" selected th:text="#{sizes.medium}"></option>
<option value="large" th:text="#{sizes.large}"></option>
<option value="x-large" th:text="#{sizes.x-large}"></option>
</select>
</div>
<div class="mb-3">
<label for="position" th:text="#{addPageNumbers.selectText.3}"></label>
<div class="a4container">
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div>
<div class="pageNumber" id="2" style="top: 10%; left: 50%;">2</div>
<div class="pageNumber" id="3" style="top: 10%; left: 90%;">3</div>
<div class="pageNumber" id="4" style="top: 50%; left: 10%;">4</div>
<div class="pageNumber" id="5" style="top: 50%; left: 50%;">5</div>
<div class="pageNumber" id="6" style="top: 50%; left: 90%;">6</div>
<div class="pageNumber" id="7" style="top: 90%; left: 10%;">7</div>
<div class="pageNumber selectedPosition" id="8" style="top: 90%; left: 50%;">8</div>
<div class="pageNumber" id="9" style="top: 90%; left: 90%;">9</div>
</div>
</div>
<input type="hidden" id="numberInput" name="position" min="1" max="9" value="8" required />
<div class="mb-3">
<label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label>
<input type="number" class="form-control" id="startingNumber" name="startingNumber" min="1" required value="1" />
</div>
<div class="mb-3">
<label for="pagesToNumber" th:text="#{addPageNumbers.selectText.5}"></label>
<input type="text" class="form-control" id="pagesToNumber" name="pagesToNumber" th:placeholder="#{addPageNumbers.numberPagesDesc}" />
</div>
<div class="mb-3">
<label for="customText" th:text="#{addPageNumbers.selectText.6}"></label>
<input type="text" class="form-control" id="customText" name="customText" th:placeholder="#{addPageNumbers.customNumberDesc}" />
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPageNumbers.submit}"></button>
</form>
</div>
</div>
</div>
<script>
let cells = document.querySelectorAll('.pageNumber');
let inputField = document.getElementById('numberInput');
.selectedPosition { cells.forEach(cell => {
background-color: #0a0; cell.addEventListener('click', function(e) {
} cells.forEach(cell => {
cell.classList.remove('selectedPosition'); // Remove selected class from all cells
cell.classList.remove('selectedHovered'); // Also remove selectedHovered class
});
let selectedLocation = e.target.id;
inputField.value = selectedLocation;
e.target.classList.add('selectedPosition'); // Add selected class to clicked cell
e.target.classList.add('selectedHovered'); // Add selectedHovered class
});
.selectedPosition.selectedHovered { cell.addEventListener('mouseenter', function(e) {
background-color: #006600; if(e.target.classList.contains('selectedPosition')) {
} e.target.classList.add('selectedHovered');
</style> }
});
cell.addEventListener('mouseleave', function(e) {
<div class="mb-3"> if(e.target.classList.contains('selectedPosition')) {
<label for="position" th:text="#{addPageNumbers.selectText.3}"></label> e.target.classList.remove('selectedHovered');
<div class="a4container"> }
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div> });
<div class="pageNumber" id="2" style="top: 10%; left: 50%;">2</div> });
<div class="pageNumber" id="3" style="top: 10%; left: 90%;">3</div> </script>
<div class="pageNumber" id="4" style="top: 50%; left: 10%;">4</div> </div>
<div class="pageNumber" id="5" style="top: 50%; left: 50%;">5</div> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
<div class="pageNumber" id="6" style="top: 50%; left: 90%;">6</div> </div>
<div class="pageNumber" id="7" style="top: 90%; left: 10%;">7</div> </body>
<div class="pageNumber selectedPosition" id="8" style="top: 90%; left: 50%;">8</div>
<div class="pageNumber" id="9" style="top: 90%; left: 90%;">9</div>
</div>
</div>
<input type="hidden" id="numberInput" name="position" min="1"
max="9" value="8" required />
<div class="mb-3">
<label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label> <input
type="number" class="form-control" id="startingNumber"
name="startingNumber" min="1" required value="1" />
</div>
<div class="mb-3">
<label for="pagesToNumber" th:text="#{addPageNumbers.selectText.5}"></label> <input
type="text" class="form-control" id="pagesToNumber"
name="pagesToNumber"
th:placeholder="#{addPageNumbers.numberPagesDesc}" />
</div>
<div class="mb-3">
<label for="customText" th:text="#{addPageNumbers.selectText.6}"></label> <input type="text"
class="form-control" id="customText" name="customText"
th:placeholder="#{addPageNumbers.customNumberDesc}" />
</div>
<button type="submit" id="submitBtn" class="btn btn-primary"
th:text="#{addPageNumbers.submit}"></button>
</form>
</div>
</div>
</div>
<script>
let cells = document.querySelectorAll('.pageNumber');
let inputField = document.getElementById('numberInput');
cells.forEach(cell => {
cell.addEventListener('click', function(e) {
cells.forEach(cell => {
cell.classList.remove('selectedPosition'); // Remove selected class from all cells
cell.classList.remove('selectedHovered'); // Also remove selectedHovered class
});
let selectedLocation = e.target.id;
inputField.value = selectedLocation;
e.target.classList.add('selectedPosition'); // Add selected class to clicked cell
e.target.classList.add('selectedHovered'); // Add selectedHovered class
});
cell.addEventListener('mouseenter', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.add('selectedHovered');
}
});
cell.addEventListener('mouseleave', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.remove('selectedHovered');
}
});
});
</script>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html> </html>

View File

@ -1,20 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
th:lang-direction="#{language.direction}" <head>
xmlns:th="http://www.thymeleaf.org"> <th:block th:insert="~{fragments/common :: head(title=#{adjustContrast.title}, header=#{adjustContrast.header})}"></th:block>
</head>
<th:block
th:insert="~{fragments/common :: head(title=#{adjustContrast.title}, header=#{adjustContrast.header})}"></th:block>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-12"> <div class="col-md-12">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-3"> <div class="col-md-3">
<div id="sliders-container" style="display:none;"> <div id="sliders-container" style="display:none;">
@ -46,265 +43,265 @@
</div> </div>
<style> <style>
#flex-container { #flex-container {
display: flex; display: flex;
align-items: center; align-items: center;
} }
#sliders-container { #sliders-container {
padding: 0 20px; /* Add some padding to separate sliders from canvas */ padding: 0 20px; /* Add some padding to separate sliders from canvas */
} }
</style> </style>
<script src="pdfjs/pdf.js"></script> <script src="pdfjs/pdf.js"></script>
<script> <script>
var canvas = document.getElementById('contrast-pdf-canvas'); var canvas = document.getElementById('contrast-pdf-canvas');
var context = canvas.getContext('2d'); var context = canvas.getContext('2d');
var originalImageData = null; var originalImageData = null;
var allPages = []; var allPages = [];
var pdfDoc = null; var pdfDoc = null;
var pdf = null; // This is the current PDF document var pdf = null; // This is the current PDF document
async function renderPDFAndSaveOriginalImageData(file) { async function renderPDFAndSaveOriginalImageData(file) {
var fileReader = new FileReader(); var fileReader = new FileReader();
fileReader.onload = async function() { fileReader.onload = async function() {
var data = new Uint8Array(this.result); var data = new Uint8Array(this.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js' pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
pdf = await pdfjsLib.getDocument({data: data}).promise; pdf = await pdfjsLib.getDocument({data: data}).promise;
// Get the number of pages in the PDF // Get the number of pages in the PDF
var numPages = pdf.numPages; var numPages = pdf.numPages;
allPages = Array.from({length: numPages}, (_, i) => i + 1); allPages = Array.from({length: numPages}, (_, i) => i + 1);
// Create a new PDF document // Create a new PDF document
pdfDoc = await PDFLib.PDFDocument.create(); pdfDoc = await PDFLib.PDFDocument.create();
// Render the first page in the viewer // Render the first page in the viewer
await renderPageAndAdjustImageProperties(1); await renderPageAndAdjustImageProperties(1);
document.getElementById("sliders-container").style.display = "block"; document.getElementById("sliders-container").style.display = "block";
}; };
fileReader.readAsArrayBuffer(file); fileReader.readAsArrayBuffer(file);
} }
// This function is now async and returns a promise // This function is now async and returns a promise
function renderPageAndAdjustImageProperties(pageNum) { function renderPageAndAdjustImageProperties(pageNum) {
return new Promise(async function(resolve, reject) { return new Promise(async function(resolve, reject) {
var page = await pdf.getPage(pageNum); var page = await pdf.getPage(pageNum);
var scale = 1.5; var scale = 1.5;
var viewport = page.getViewport({ scale: scale }); var viewport = page.getViewport({ scale: scale });
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
var renderContext = { var renderContext = {
canvasContext: context, canvasContext: context,
viewport: viewport viewport: viewport
}; };
var renderTask = page.render(renderContext); var renderTask = page.render(renderContext);
renderTask.promise.then(function () { renderTask.promise.then(function () {
originalImageData = context.getImageData(0, 0, canvas.width, canvas.height); originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
adjustImageProperties(); adjustImageProperties();
resolve(); resolve();
}); });
canvas.classList.add("fixed-shadow-canvas"); canvas.classList.add("fixed-shadow-canvas");
}); });
} }
function adjustImageProperties() { function adjustImageProperties() {
var contrast = parseFloat(document.getElementById('contrast-slider').value); var contrast = parseFloat(document.getElementById('contrast-slider').value);
var brightness = parseFloat(document.getElementById('brightness-slider').value); var brightness = parseFloat(document.getElementById('brightness-slider').value);
var saturation = parseFloat(document.getElementById('saturation-slider').value); var saturation = parseFloat(document.getElementById('saturation-slider').value);
contrast /= 100; // normalize to range [0, 2] contrast /= 100; // normalize to range [0, 2]
brightness /= 100; // normalize to range [0, 2] brightness /= 100; // normalize to range [0, 2]
saturation /= 100; // normalize to range [0, 2] saturation /= 100; // normalize to range [0, 2]
if (originalImageData) { if (originalImageData) {
var newImageData = context.createImageData(originalImageData.width, originalImageData.height); var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
newImageData.data.set(originalImageData.data); newImageData.data.set(originalImageData.data);
for(var i=0; i<newImageData.data.length; i+=4) for(var i=0; i<newImageData.data.length; i+=4)
{ {
var r = newImageData.data[i]; var r = newImageData.data[i];
var g = newImageData.data[i+1]; var g = newImageData.data[i+1];
var b = newImageData.data[i+2]; var b = newImageData.data[i+2];
// Adjust contrast // Adjust contrast
r = adjustContrastForPixel(r, contrast); r = adjustContrastForPixel(r, contrast);
g = adjustContrastForPixel(g, contrast); g = adjustContrastForPixel(g, contrast);
b = adjustContrastForPixel(b, contrast); b = adjustContrastForPixel(b, contrast);
// Adjust brightness // Adjust brightness
r = adjustBrightnessForPixel(r, brightness); r = adjustBrightnessForPixel(r, brightness);
g = adjustBrightnessForPixel(g, brightness); g = adjustBrightnessForPixel(g, brightness);
b = adjustBrightnessForPixel(b, brightness); b = adjustBrightnessForPixel(b, brightness);
// Adjust saturation // Adjust saturation
var rgb = adjustSaturationForPixel(r, g, b, saturation); var rgb = adjustSaturationForPixel(r, g, b, saturation);
newImageData.data[i] = rgb[0]; newImageData.data[i] = rgb[0];
newImageData.data[i+1] = rgb[1]; newImageData.data[i+1] = rgb[1];
newImageData.data[i+2] = rgb[2]; newImageData.data[i+2] = rgb[2];
} }
context.putImageData(newImageData, 0, 0); context.putImageData(newImageData, 0, 0);
} }
} }
function rgbToHsl(r, g, b) { function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255; r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b); var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2; var h, s, l = (max + min) / 2;
if (max === min) { if (max === min) {
h = s = 0; // achromatic h = s = 0; // achromatic
} else { } else {
var d = max - min; var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break; case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break; case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break; case b: h = (r - g) / d + 4; break;
} }
h /= 6; h /= 6;
} }
return [h, s, l]; return [h, s, l];
} }
function hslToRgb(h, s, l) { function hslToRgb(h, s, l) {
var r, g, b; var r, g, b;
if (s === 0) { if (s === 0) {
r = g = b = l; // achromatic r = g = b = l; // achromatic
} else { } else {
var hue2rgb = function hue2rgb(p, q, t) { var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1; if (t < 0) t += 1;
if (t > 1) t -= 1; if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q; if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p; return p;
}; };
var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q; var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3); r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h); g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3); b = hue2rgb(p, q, h - 1 / 3);
} }
return [r * 255, g * 255, b * 255]; return [r * 255, g * 255, b * 255];
} }
function adjustContrastForPixel(pixel, contrast) { function adjustContrastForPixel(pixel, contrast) {
// Normalize to range [-0.5, 0.5] // Normalize to range [-0.5, 0.5]
var normalized = pixel / 255 - 0.5; var normalized = pixel / 255 - 0.5;
// Apply contrast // Apply contrast
normalized *= contrast; normalized *= contrast;
// Denormalize back to [0, 255] // Denormalize back to [0, 255]
return (normalized + 0.5) * 255; return (normalized + 0.5) * 255;
} }
function clamp(value, min, max) { function clamp(value, min, max) {
return Math.min(Math.max(value, min), max); return Math.min(Math.max(value, min), max);
} }
function adjustSaturationForPixel(r, g, b, saturation) { function adjustSaturationForPixel(r, g, b, saturation) {
var hsl = rgbToHsl(r, g, b); var hsl = rgbToHsl(r, g, b);
// Adjust saturation // Adjust saturation
hsl[1] = clamp(hsl[1] * saturation, 0, 1); hsl[1] = clamp(hsl[1] * saturation, 0, 1);
// Convert back to RGB // Convert back to RGB
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Return adjusted RGB values // Return adjusted RGB values
return rgb; return rgb;
} }
function adjustBrightnessForPixel(pixel, brightness) { function adjustBrightnessForPixel(pixel, brightness) {
return Math.max(0, Math.min(255, pixel * brightness)); return Math.max(0, Math.min(255, pixel * brightness));
} }
async function downloadPDF() { async function downloadPDF() {
for (var i = 0; i < allPages.length; i++) { for (var i = 0; i < allPages.length; i++) {
await renderPageAndAdjustImageProperties(allPages[i]); await renderPageAndAdjustImageProperties(allPages[i]);
const pngImageBytes = canvas.toDataURL('image/png'); const pngImageBytes = canvas.toDataURL('image/png');
const pngImage = await pdfDoc.embedPng(pngImageBytes); const pngImage = await pdfDoc.embedPng(pngImageBytes);
const pngDims = pngImage.scale(1); const pngDims = pngImage.scale(1);
// Create a blank page matching the dimensions of the image // Create a blank page matching the dimensions of the image
const page = pdfDoc.addPage([pngDims.width, pngDims.height]); const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
// Draw the PNG image // Draw the PNG image
page.drawImage(pngImage, { page.drawImage(pngImage, {
x: 0, x: 0,
y: 0, y: 0,
width: pngDims.width, width: pngDims.width,
height: pngDims.height height: pngDims.height
}); });
} }
// Serialize the PDFDocument to bytes (a Uint8Array) // Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
// Create a Blob // Create a Blob
const blob = new Blob([pdfBytes.buffer], {type: "application/pdf"}); const blob = new Blob([pdfBytes.buffer], {type: "application/pdf"});
// Create download link // Create download link
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob); downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = "download.pdf"; downloadLink.download = "download.pdf";
downloadLink.click(); downloadLink.click();
// After download, reset the viewer and clear stored data // After download, reset the viewer and clear stored data
allPages = []; // Clear the pages allPages = []; // Clear the pages
originalImageData = null; // Clear the image data originalImageData = null; // Clear the image data
// Go back to page 1 and render it in the viewer // Go back to page 1 and render it in the viewer
if (pdf !== null) { if (pdf !== null) {
renderPageAndAdjustImageProperties(1); renderPageAndAdjustImageProperties(1);
} }
} }
// Event listeners // Event listeners
document.getElementById('fileInput-input').addEventListener('change', function(e) { document.getElementById('fileInput-input').addEventListener('change', function(e) {
if (e.target.files.length > 0) { if (e.target.files.length > 0) {
renderPDFAndSaveOriginalImageData(e.target.files[0]); renderPDFAndSaveOriginalImageData(e.target.files[0]);
} }
}); });
document.getElementById('contrast-slider').addEventListener('input', function() { document.getElementById('contrast-slider').addEventListener('input', function() {
document.getElementById('contrast-val').textContent = this.value; document.getElementById('contrast-val').textContent = this.value;
adjustImageProperties(); adjustImageProperties();
}); });
document.getElementById('brightness-slider').addEventListener('input', function() { document.getElementById('brightness-slider').addEventListener('input', function() {
document.getElementById('brightness-val').textContent = this.value; document.getElementById('brightness-val').textContent = this.value;
adjustImageProperties(); adjustImageProperties();
}); });
document.getElementById('saturation-slider').addEventListener('input', function() { document.getElementById('saturation-slider').addEventListener('input', function() {
document.getElementById('saturation-val').textContent = this.value; document.getElementById('saturation-val').textContent = this.value;
adjustImageProperties(); adjustImageProperties();
}); });
document.getElementById('download-button').addEventListener('click', function() { document.getElementById('download-button').addEventListener('click', function() {
downloadPDF(); downloadPDF();
}); });
</script> </script>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,31 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{autoCrop.title}, header=#{autoCrop.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{autoCrop.title}, header=#{autoCrop.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{autoCrop.header}"></h2> <h2 th:text="#{autoCrop.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/misc/auto-crop}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/misc/auto-crop}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoCrop.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoCrop.submit}"></button>
</form> </form>
<p class="mt-3" th:text="#{autoCrop.credit}"></p> <p class="mt-3" th:text="#{autoCrop.credit}"></p>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,30 +1,29 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{auto-rename.title}, header=#{auto-rename.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{auto-rename.title}, header=#{auto-rename.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{auto-rename.header}"></h2> <h2 th:text="#{auto-rename.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/misc/auto-rename}"> <form method="post" enctype="multipart/form-data" th:action="@{api/v1/misc/auto-rename}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{auto-rename.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{auto-rename.submit}"></button>
</form> </form>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,261 +1,227 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{changeMetadata.title}, header=#{changeMetadata.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{changeMetadata.title}, header=#{changeMetadata.header})}"></th:block> <body>
<body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{changeMetadata.header}"></h2> <h2 th:text="#{changeMetadata.header}"></h2>
<form method="post" id="form1" enctype="multipart/form-data" th:action="@{api/v1/misc/update-metadata}">
<form method="post" id="form1" enctype="multipart/form-data" th:action="@{api/v1/misc/update-metadata}"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p>
<p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p> <div class="mb-3-inline form-check">
<input type="checkbox" class="form-check-input" id="deleteAll" name="deleteAll">
<div class="mb-3-inline form-check"> <label class="ms-3" for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label>
<input type="checkbox" class="form-check-input" id="deleteAll" name="deleteAll">
<label class="ms-3" for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label>
</div>
<div class="mb-3-inline form-check">
<input type="checkbox" class="form-check-input" id="customModeCheckbox">
<label class="ms-3" for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label>
</div>
<div class="mb-3">
<label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label>
<input type="text" class="form-control" id="author" name="author">
</div>
<div class="mb-3">
<label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label>
<input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59">
</div>
<div class="mb-3">
<label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label>
<input type="text" class="form-control" id="creator" name="creator">
</div>
<div class="mb-3">
<label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label>
<input type="text" class="form-control" id="keywords" name="keywords">
</div>
<div class="mb-3">
<label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label>
<input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59">
</div>
<div class="mb-3">
<label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label>
<input type="text" class="form-control" id="producer" name="producer">
</div>
<div class="mb-3">
<label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label>
<input type="text" class="form-control" id="subject" name="subject">
</div>
<div class="mb-3">
<label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label>
<input type="text" class="form-control" id="title" name="title">
</div>
<div class="mb-3">
<label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label>
<select class="form-control" id="trapped" name="trapped">
<option value="True" th:text="#{true}"></option>
<option value="False" th:text="#{false}" selected></option>
<option value="Unknown" th:text="#{unknown}"></option>
</select>
</div>
<div id="customMetadata" style="display: none;">
<h3 th:text="#{changeMetadata.selectText.4}"></h3>
<div class="mb-3" id="otherMetadataEntries"></div>
</div>
<div id="customMetadataEntries"></div>
<button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button>
<br>
<br>
<button class="btn btn-primary" type="submit" id="submitBtn" th:text="#{changeMetadata.submit}"></button>
<script>
const deleteAllCheckbox = document.querySelector("#deleteAll");
let inputs = document.querySelectorAll("input");
const customMetadataDiv = document.getElementById('customMetadata');
const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
deleteAllCheckbox.addEventListener("change", function(event) {
inputs.forEach(input => {
// If it's the deleteAllCheckbox or any file input, skip
if (input === deleteAllCheckbox || input.type === "file") {
return;
}
// Disable or enable based on the checkbox state
input.disabled = deleteAllCheckbox.checked;
});
});
const customModeCheckbox = document.getElementById('customModeCheckbox');
const addMetadataBtn = document.getElementById("addMetadataBtn");
const customMetadataFormContainer = document.getElementById("customMetadataEntries");
var count = 1;
const fileInput = document.querySelector("#fileInput-input");
const authorInput = document.querySelector("#author");
const creationDateInput = document.querySelector("#creationDate");
const creatorInput = document.querySelector("#creator");
const keywordsInput = document.querySelector("#keywords");
const modificationDateInput = document.querySelector("#modificationDate");
const producerInput = document.querySelector("#producer");
const subjectInput = document.querySelector("#subject");
const titleInput = document.querySelector("#title");
const trappedInput = document.querySelector("#trapped");
var lastPDFFileMeta = null;
fileInput.addEventListener("change", async function() {
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
while (customMetadataFormContainer.firstChild) {
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
}
const file = this.files[0];
var url = URL.createObjectURL(file)
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const pdf = await pdfjsLib.getDocument(url).promise;
const pdfMetadata = await pdf.getMetadata();
lastPDFFile = pdfMetadata?.info
console.log(pdfMetadata);
if(!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
customModeCheckbox.disabled = true;
customModeCheckbox.checked = false;
} else {
customModeCheckbox.disabled = false;
}
authorInput.value = pdfMetadata?.info?.Author;
creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
creatorInput.value = pdfMetadata?.info?.Creator;
keywordsInput.value = pdfMetadata?.info?.Keywords;
modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
producerInput.value = pdfMetadata?.info?.Producer;
subjectInput.value = pdfMetadata?.info?.Subject;
titleInput.value = pdfMetadata?.info?.Title;
console.log(pdfMetadata?.info);
const trappedValue = pdfMetadata?.info?.Trapped;
// Get all options in the select element
const options = trappedInput.options;
// Loop through all options to find the one with a matching value
for (let i = 0; i < options.length; i++) {
if (options[i].value === trappedValue) {
options[i].selected = true;
break;
}
}
addExtra();
});
addMetadataBtn.addEventListener("click", () => {
const keyInput = document.createElement("input");
keyInput.type = "text";
keyInput.placeholder = 'Key';
keyInput.className = "form-control";
keyInput.name = "customKey" + count;
const valueInput = document.createElement("input");
valueInput.type = "text";
valueInput.placeholder = 'Value';
valueInput.className = "form-control";
valueInput.name = "customValue" + count;
count = count + 1;
const formGroup = document.createElement("div");
formGroup.className = "mb-3";
formGroup.appendChild(keyInput);
formGroup.appendChild(valueInput);
customMetadataFormContainer.appendChild(formGroup);
});
function convertDateFormat(dateTimeString) {
if (!dateTimeString || dateTimeString.length < 17) {
return dateTimeString;
}
const year = dateTimeString.substring(2, 6);
const month = dateTimeString.substring(6, 8);
const day = dateTimeString.substring(8, 10);
const hour = dateTimeString.substring(10, 12);
const minute = dateTimeString.substring(12, 14);
const second = dateTimeString.substring(14, 16);
return year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second;
}
function addExtra() {
const event = document.getElementById("customModeCheckbox");
if (event.checked && lastPDFFile.Custom != null) {
customMetadataDiv.style.display = 'block';
for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
if (key === 'Author' || key === 'CreationDate' || key === 'Creator' || key === 'Keywords' || key === 'ModDate' || key === 'Producer' || key === 'Subject' || key === 'Title' || key === 'Trapped') {
continue;
}
const entryDiv = document.createElement('div');
entryDiv.className = 'mb-3';
entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
otherMetadataEntriesDiv.appendChild(entryDiv);
}
} else {
customMetadataDiv.style.display = 'none';
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
}
}
customModeCheckbox.addEventListener('change', (event) => {
addExtra();
});
</script>
</form>
</div>
</div> </div>
<div class="mb-3-inline form-check">
<input type="checkbox" class="form-check-input" id="customModeCheckbox">
<label class="ms-3" for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label>
</div>
<div class="mb-3">
<label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label>
<input type="text" class="form-control" id="author" name="author">
</div>
<div class="mb-3">
<label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label>
<input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59">
</div>
<div class="mb-3">
<label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label>
<input type="text" class="form-control" id="creator" name="creator">
</div>
<div class="mb-3">
<label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label>
<input type="text" class="form-control" id="keywords" name="keywords">
</div>
<div class="mb-3">
<label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label>
<input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59">
</div>
<div class="mb-3">
<label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label>
<input type="text" class="form-control" id="producer" name="producer">
</div>
<div class="mb-3">
<label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label>
<input type="text" class="form-control" id="subject" name="subject">
</div>
<div class="mb-3">
<label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label>
<input type="text" class="form-control" id="title" name="title">
</div>
<div class="mb-3">
<label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label>
<select class="form-control" id="trapped" name="trapped">
<option value="True" th:text="#{true}"></option>
<option value="False" th:text="#{false}" selected></option>
<option value="Unknown" th:text="#{unknown}"></option>
</select>
</div>
<div id="customMetadata" style="display: none;">
<h3 th:text="#{changeMetadata.selectText.4}"></h3>
<div class="mb-3" id="otherMetadataEntries"></div>
</div>
<div id="customMetadataEntries"></div>
<button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button>
<br>
<br>
<button class="btn btn-primary" type="submit" id="submitBtn" th:text="#{changeMetadata.submit}"></button>
<script>
const deleteAllCheckbox = document.querySelector("#deleteAll");
let inputs = document.querySelectorAll("input");
const customMetadataDiv = document.getElementById('customMetadata');
const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
deleteAllCheckbox.addEventListener("change", function(event) {
inputs.forEach(input => {
// If it's the deleteAllCheckbox or any file input, skip
if (input === deleteAllCheckbox || input.type === "file") {
return;
}
// Disable or enable based on the checkbox state
input.disabled = deleteAllCheckbox.checked;
});
});
const customModeCheckbox = document.getElementById('customModeCheckbox');
const addMetadataBtn = document.getElementById("addMetadataBtn");
const customMetadataFormContainer = document.getElementById("customMetadataEntries");
var count = 1;
const fileInput = document.querySelector("#fileInput-input");
const authorInput = document.querySelector("#author");
const creationDateInput = document.querySelector("#creationDate");
const creatorInput = document.querySelector("#creator");
const keywordsInput = document.querySelector("#keywords");
const modificationDateInput = document.querySelector("#modificationDate");
const producerInput = document.querySelector("#producer");
const subjectInput = document.querySelector("#subject");
const titleInput = document.querySelector("#title");
const trappedInput = document.querySelector("#trapped");
var lastPDFFileMeta = null;
fileInput.addEventListener("change", async function() {
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
while (customMetadataFormContainer.firstChild) {
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
}
const file = this.files[0];
var url = URL.createObjectURL(file)
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const pdf = await pdfjsLib.getDocument(url).promise;
const pdfMetadata = await pdf.getMetadata();
lastPDFFile = pdfMetadata?.info
console.log(pdfMetadata);
if(!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
customModeCheckbox.disabled = true;
customModeCheckbox.checked = false;
} else {
customModeCheckbox.disabled = false;
}
authorInput.value = pdfMetadata?.info?.Author;
creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
creatorInput.value = pdfMetadata?.info?.Creator;
keywordsInput.value = pdfMetadata?.info?.Keywords;
modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
producerInput.value = pdfMetadata?.info?.Producer;
subjectInput.value = pdfMetadata?.info?.Subject;
titleInput.value = pdfMetadata?.info?.Title;
console.log(pdfMetadata?.info);
const trappedValue = pdfMetadata?.info?.Trapped;
// Get all options in the select element
const options = trappedInput.options;
// Loop through all options to find the one with a matching value
for (let i = 0; i < options.length; i++) {
if (options[i].value === trappedValue) {
options[i].selected = true;
break;
}
}
addExtra();
});
addMetadataBtn.addEventListener("click", () => {
const keyInput = document.createElement("input");
keyInput.type = "text";
keyInput.placeholder = 'Key';
keyInput.className = "form-control";
keyInput.name = "customKey" + count;
const valueInput = document.createElement("input");
valueInput.type = "text";
valueInput.placeholder = 'Value';
valueInput.className = "form-control";
valueInput.name = "customValue" + count;
count = count + 1;
const formGroup = document.createElement("div");
formGroup.className = "mb-3";
formGroup.appendChild(keyInput);
formGroup.appendChild(valueInput);
customMetadataFormContainer.appendChild(formGroup);
});
function convertDateFormat(dateTimeString) {
if (!dateTimeString || dateTimeString.length < 17) {
return dateTimeString;
}
const year = dateTimeString.substring(2, 6);
const month = dateTimeString.substring(6, 8);
const day = dateTimeString.substring(8, 10);
const hour = dateTimeString.substring(10, 12);
const minute = dateTimeString.substring(12, 14);
const second = dateTimeString.substring(14, 16);
return year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second;
}
function addExtra() {
const event = document.getElementById("customModeCheckbox");
if (event.checked && lastPDFFile.Custom != null) {
customMetadataDiv.style.display = 'block';
for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
if (key === 'Author' || key === 'CreationDate' || key === 'Creator' || key === 'Keywords' || key === 'ModDate' || key === 'Producer' || key === 'Subject' || key === 'Title' || key === 'Trapped') {
continue;
}
const entryDiv = document.createElement('div');
entryDiv.className = 'mb-3';
entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
otherMetadataEntriesDiv.appendChild(entryDiv);
}
} else {
customMetadataDiv.style.display = 'none';
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
}
}
customModeCheckbox.addEventListener('change', (event) => {
addExtra();
});
</script>
</form>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,190 +1,187 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{compare.title}, header=#{compare.header})}"></th:block>
<style>
.result-column {
border: 1px solid #ccc;
padding: 15px;
overflow-y: auto;
height: calc(100vh - 400px);
white-space: pre-wrap;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: head(title=#{compare.title}, header=#{compare.header})}"></th:block>
<body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-9"> <div class="col-md-9">
<h2 th:text="#{compare.header}"></h2> <h2 th:text="#{compare.header}"></h2>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput2', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput2', multiple=false, accept='application/pdf', remoteCall='false')}"></div> <button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button>
<button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button> <div class="row">
<div class="col-md-6">
<h3 th:text="#{compare.document.1}"></h3>
<div class="row"> <div id="result1" class="result-column"></div>
<div class="col-md-6">
<h3 th:text="#{compare.document.1}"></h3>
<div id="result1" class="result-column"></div>
</div>
<div class="col-md-6">
<h3 th:text="#{compare.document.2}"></h3>
<div id="result2" class="result-column"></div>
</div>
</div>
<style>
.result-column {
border: 1px solid #ccc;
padding: 15px;
overflow-y: auto;
height: calc(100vh - 400px);
white-space: pre-wrap;
}
</style>
<script>
// get the elements
var result1 = document.getElementById('result1');
var result2 = document.getElementById('result2');
// add event listeners
result1.addEventListener('scroll', function() {
result2.scrollTop = result1.scrollTop;
});
result2.addEventListener('scroll', function() {
result1.scrollTop = result2.scrollTop;
});
async function comparePDFs() {
const file1 = document.getElementById("fileInput-input").files[0];
const file2 = document.getElementById("fileInput2-input").files[0];
if (!file1 || !file2) {
console.error("Please select two PDF files to compare");
return;
}
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const [pdf1, pdf2] = await Promise.all([
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
]);
const extractText = async (pdf) => {
const pages = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const strings = content.items.map(item => item.str);
pages.push(strings.join(" "));
}
return pages.join(" ");
};
const [text1, text2] = await Promise.all([
extractText(pdf1),
extractText(pdf2)
]);
if (text1.trim() === "" || text2.trim() === "") {
alert("One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.");
return;
}
const diff = (text1, text2) => {
const words1 = text1.split(' ');
const words2 = text2.split(' ');
// Create a 2D array to hold our "matrix"
const matrix = Array(words1.length + 1).fill(null).map(() => Array(words2.length + 1).fill(0));
// Perform standard LCS algorithm
for (let i = 1; i <= words1.length; i++) {
for (let j = 1; j <= words2.length; j++) {
if (words1[i - 1] === words2[j - 1]) {
matrix[i][j] = matrix[i - 1][j - 1] + 1;
} else {
matrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]);
}
}
}
let i = words1.length;
let j = words2.length;
const differences = [];
// Backtrack through the matrix to create the diff
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && words1[i - 1] === words2[j - 1]) {
differences.unshift(['black', words1[i - 1]]);
i--;
j--;
} else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
differences.unshift(['green', words2[j - 1]]);
j--;
} else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) {
differences.unshift(['red', words1[i - 1]]);
i--;
}
}
return differences;
};
const differences = diff(text1, text2);
const displayDifferences = (differences) => {
const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2");
resultDiv1.innerHTML = "";
resultDiv2.innerHTML = "";
differences.forEach(([color, word]) => {
const span1 = document.createElement("span");
const span2 = document.createElement("span");
// If it's an addition, show it in green in the second document and transparent in the first
if (color === "green") {
span1.style.color = "transparent";
span1.style.userSelect = "none";
span2.style.color = color;
}
// If it's a deletion, show it in red in the first document and transparent in the second
else if (color === "red") {
span1.style.color = color;
span2.style.color = "transparent";
span2.style.userSelect = "none";
}
// If it's unchanged, show it in black in both
else {
span1.style.color = color;
span2.style.color = color;
}
span1.textContent = word;
span2.textContent = word;
resultDiv1.appendChild(span1);
resultDiv2.appendChild(span2);
// Add space after each word, or a new line if the word ends with a full stop
const spaceOrNewline1 = document.createElement("span");
const spaceOrNewline2 = document.createElement("span");
if (word.endsWith(".")) {
spaceOrNewline1.innerHTML = "<br>";
spaceOrNewline2.innerHTML = "<br>";
} else {
spaceOrNewline1.textContent = " ";
spaceOrNewline2.textContent = " ";
}
resultDiv1.appendChild(spaceOrNewline1);
resultDiv2.appendChild(spaceOrNewline2);
});
};
console.log('Differences:', differences);
displayDifferences(differences);
}
</script>
</div>
</div> </div>
<div class="col-md-6">
<h3 th:text="#{compare.document.2}"></h3>
<div id="result2" class="result-column"></div>
</div>
</div>
<script>
// get the elements
var result1 = document.getElementById('result1');
var result2 = document.getElementById('result2');
// add event listeners
result1.addEventListener('scroll', function() {
result2.scrollTop = result1.scrollTop;
});
result2.addEventListener('scroll', function() {
result1.scrollTop = result2.scrollTop;
});
async function comparePDFs() {
const file1 = document.getElementById("fileInput-input").files[0];
const file2 = document.getElementById("fileInput2-input").files[0];
if (!file1 || !file2) {
console.error("Please select two PDF files to compare");
return;
}
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js'
const [pdf1, pdf2] = await Promise.all([
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
]);
const extractText = async (pdf) => {
const pages = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const strings = content.items.map(item => item.str);
pages.push(strings.join(" "));
}
return pages.join(" ");
};
const [text1, text2] = await Promise.all([
extractText(pdf1),
extractText(pdf2)
]);
if (text1.trim() === "" || text2.trim() === "") {
alert("One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.");
return;
}
const diff = (text1, text2) => {
const words1 = text1.split(' ');
const words2 = text2.split(' ');
// Create a 2D array to hold our "matrix"
const matrix = Array(words1.length + 1).fill(null).map(() => Array(words2.length + 1).fill(0));
// Perform standard LCS algorithm
for (let i = 1; i <= words1.length; i++) {
for (let j = 1; j <= words2.length; j++) {
if (words1[i - 1] === words2[j - 1]) {
matrix[i][j] = matrix[i - 1][j - 1] + 1;
} else {
matrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]);
}
}
}
let i = words1.length;
let j = words2.length;
const differences = [];
// Backtrack through the matrix to create the diff
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && words1[i - 1] === words2[j - 1]) {
differences.unshift(['black', words1[i - 1]]);
i--;
j--;
} else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
differences.unshift(['green', words2[j - 1]]);
j--;
} else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) {
differences.unshift(['red', words1[i - 1]]);
i--;
}
}
return differences;
};
const differences = diff(text1, text2);
const displayDifferences = (differences) => {
const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2");
resultDiv1.innerHTML = "";
resultDiv2.innerHTML = "";
differences.forEach(([color, word]) => {
const span1 = document.createElement("span");
const span2 = document.createElement("span");
// If it's an addition, show it in green in the second document and transparent in the first
if (color === "green") {
span1.style.color = "transparent";
span1.style.userSelect = "none";
span2.style.color = color;
}
// If it's a deletion, show it in red in the first document and transparent in the second
else if (color === "red") {
span1.style.color = color;
span2.style.color = "transparent";
span2.style.userSelect = "none";
}
// If it's unchanged, show it in black in both
else {
span1.style.color = color;
span2.style.color = color;
}
span1.textContent = word;
span2.textContent = word;
resultDiv1.appendChild(span1);
resultDiv2.appendChild(span2);
// Add space after each word, or a new line if the word ends with a full stop
const spaceOrNewline1 = document.createElement("span");
const spaceOrNewline2 = document.createElement("span");
if (word.endsWith(".")) {
spaceOrNewline1.innerHTML = "<br>";
spaceOrNewline2.innerHTML = "<br>";
} else {
spaceOrNewline1.textContent = " ";
spaceOrNewline2.textContent = " ";
}
resultDiv1.appendChild(spaceOrNewline1);
resultDiv2.appendChild(spaceOrNewline2);
});
};
console.log('Differences:', differences);
displayDifferences(differences);
}
</script>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body>
</html>

View File

@ -1,50 +1,48 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{compress.title}, header=#{compress.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{compress.title}, header=#{compress.header})}"></th:block> <body>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{compress.header}"></h2> <h2 th:text="#{compress.header}"></h2>
<form action="#" th:action="@{api/v1/misc/compress-pdf}" method="post" enctype="multipart/form-data"> <form action="#" th:action="@{api/v1/misc/compress-pdf}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h4 th:text="#{compress.selectText.1}"></h4> <h4 th:text="#{compress.selectText.1}"></h4>
<label for="optimizeLevel" th:text="#{compress.selectText.2}"></label> <label for="optimizeLevel" th:text="#{compress.selectText.2}"></label>
<select name="optimizeLevel" id="optimizeLevel" class="form-control"> <select name="optimizeLevel" id="optimizeLevel" class="form-control">
<option value="1">1</option> <option value="1">1</option>
<option value="2" selected>2</option> <option value="2" selected>2</option>
<option value="3">3</option> <option value="3">3</option>
<option value="4" th:text="#{compress.selectText.3}"></option> <option value="4" th:text="#{compress.selectText.3}"></option>
</select> </select>
</div> </div>
</div>
<div class="card mb-3">
<div class="card-body">
<h4 th:text="#{compress.selectText.4}"></h4>
<label for="expectedOutputSize" th:text="#{compress.selectText.5}"></label>
<input type="text" name="expectedOutputSize" id="expectedOutputSize" min="1" class="form-control">
</div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
</form>
</div>
</div> </div>
<div class="card mb-3">
<div class="card-body">
<h4 th:text="#{compress.selectText.4}"></h4>
<label for="expectedOutputSize" th:text="#{compress.selectText.5}"></label>
<input type="text" name="expectedOutputSize" id="expectedOutputSize" min="1" class="form-control">
</div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
</form>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,54 +1,53 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{home.ScannerImageSplit.title}, header=#{home.ScannerImageSplit.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{home.ScannerImageSplit.title}, header=#{home.ScannerImageSplit.header})}"></th:block> <body>
<div id="page-container">
<div id="content-wrap">
<body> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div id="page-container"> <br /><br />
<div id="content-wrap"> <div class="container">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div class="row justify-content-center">
<br> <br> <div class="col-md-6">
<div class="container"> <h2 th:text="#{home.ScannerImageSplit.title}"></h2>
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{home.ScannerImageSplit.title}"></h2>
<form id="multiPdfForm" th:action="@{api/v1/misc/extract-image-scans}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='image/*, application/pdf')}"></div>
<div class="mb-3">
<label for="angleThreshold" th:text="#{ScannerImageSplit.selectText.1}"></label>
<input type="number" class="form-control" id="angleThreshold" name="angle_threshold" value="10">
<small id="angleThresholdHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.2}"></small>
</div>
<div class="mb-3">
<label for="tolerance" th:text="#{ScannerImageSplit.selectText.3}"></label>
<input type="number" class="form-control" id="tolerance" name="tolerance" value="20">
<small id="toleranceHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.4}"></small>
</div>
<div class="mb-3">
<label for="minArea" th:text="#{ScannerImageSplit.selectText.5}"></label>
<input type="number" class="form-control" id="minArea" name="min_area" value="8000">
<small id="minAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.6}"></small>
</div>
<div class="mb-3">
<label for="minContourArea" th:text="#{ScannerImageSplit.selectText.7}"></label>
<input type="number" class="form-control" id="minContourArea" name="min_contour_area" value="500">
<small id="minContourAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.8}"></small>
</div>
<div class="mb-3">
<label for="borderSize" th:text="#{ScannerImageSplit.selectText.9}"></label>
<input type="number" class="form-control" id="borderSize" name="border_size" value="1">
<small id="borderSizeHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.10}"></small>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{genericSubmit}"></button>
</form>
<form id="multiPdfForm" th:action="@{api/v1/misc/extract-image-scans}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='image/*, application/pdf')}"></div>
<div class="mb-3">
<label for="angleThreshold" th:text="#{ScannerImageSplit.selectText.1}"></label>
<input type="number" class="form-control" id="angleThreshold" name="angle_threshold" value="10">
<small id="angleThresholdHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.2}"></small>
</div>
<div class="mb-3">
<label for="tolerance" th:text="#{ScannerImageSplit.selectText.3}"></label>
<input type="number" class="form-control" id="tolerance" name="tolerance" value="20">
<small id="toleranceHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.4}"></small>
</div>
<div class="mb-3">
<label for="minArea" th:text="#{ScannerImageSplit.selectText.5}"></label>
<input type="number" class="form-control" id="minArea" name="min_area" value="8000">
<small id="minAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.6}"></small>
</div>
<div class="mb-3">
<label for="minContourArea" th:text="#{ScannerImageSplit.selectText.7}"></label>
<input type="number" class="form-control" id="minContourArea" name="min_contour_area" value="500">
<small id="minContourAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.8}"></small>
</div>
<div class="mb-3">
<label for="borderSize" th:text="#{ScannerImageSplit.selectText.9}"></label>
<input type="number" class="form-control" id="borderSize" name="border_size" value="1">
<small id="borderSizeHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.10}"></small>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{genericSubmit}"></button>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </body>
</div>
</body>
</html> </html>

View File

@ -1,36 +1,35 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{extractImages.title}, header=#{extractImages.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{extractImages.title}, header=#{extractImages.header})}"></th:block> <body>
<div id="page-container">
<div id="content-wrap">
<body> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div id="page-container"> <br /><br />
<div id="content-wrap"> <div class="container">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div class="row justify-content-center">
<br> <br> <div class="col-md-6">
<div class="container"> <h2 th:text="#{extractImages.header}"></h2>
<div class="row justify-content-center"> <form id="multiPdfForm" th:action="@{api/v1/misc/extract-images}" method="post" enctype="multipart/form-data">
<div class="col-md-6"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<h2 th:text="#{extractImages.header}"></h2> <div class="mb-3">
<label th:text="#{extractImages.selectText}"></label>
<form id="multiPdfForm" th:action="@{api/v1/misc/extract-images}" method="post" enctype="multipart/form-data"> <select class="form-control" name="format">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label th:text="#{extractImages.selectText}"></label>
<select class="form-control" name="format">
<option value="png">PNG</option> <option value="png">PNG</option>
<option value="jpg">JPG</option> <option value="jpg">JPG</option>
<option value="gif">GIF</option> <option value="gif">GIF</option>
</select> </select>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{extractImages.submit}"></button>
</form>
</div> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{extractImages.submit}"></button>
</form>
</div> </div>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </body>
</div>
</body>
</html> </html>

View File

@ -1,57 +1,55 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{flatten.title}, header=#{flatten.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: head(title=#{flatten.title}, header=#{flatten.header})}"></th:block>
<body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{flatten.header}"></h2> <h2 th:text="#{flatten.header}"></h2>
<form id="pdfForm" class="mb-3"> <form id="pdfForm" class="mb-3">
<div class="custom-file"> <div class="custom-file">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button>
<script src="js/local-pdf-input-download.js"></script>
<script>
document.getElementById('pdfForm').addEventListener('submit', async (e) => {
e.preventDefault();
const { PDFDocument } = PDFLib;
const processFile = async (file) => {
const origFileUrl = URL.createObjectURL(file);
const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true });
const form = pdfDoc.getForm();
form.flatten();
const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_flattened.pdf';
return { processedData: pdfBlob, fileName };
};
await downloadFilesWithCallback(processFile);
});
</script>
</form>
</div>
</div> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button>
<script src="js/local-pdf-input-download.js"></script>
<script>
document.getElementById('pdfForm').addEventListener('submit', async (e) => {
e.preventDefault();
const { PDFDocument } = PDFLib;
const processFile = async (file) => {
const origFileUrl = URL.createObjectURL(file);
const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true });
const form = pdfDoc.getForm();
form.flatten();
const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_flattened.pdf';
return { processedData: pdfBlob, fileName };
};
await downloadFilesWithCallback(processFile);
});
</script>
</form>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,254 +1,249 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{ocr.title}, header=#{ocr.header})}"></th:block>
<th:block th:insert="~{fragments/common :: head(title=#{ocr.title}, header=#{ocr.header})}"></th:block>
<head>
<script> <script>
function handleLangSelection() { function handleLangSelection() {
let checkboxes = document.getElementsByName("languages"); let checkboxes = document.getElementsByName("languages");
let selected = false; let selected = false;
for (let i = 0; i < checkboxes.length; i++) { for (let i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].checked) { if (checkboxes[i].checked) {
selected = true; selected = true;
checkboxes[i].setAttribute('required', 'false'); checkboxes[i].setAttribute('required', 'false');
} }
}
if (selected) {
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].removeAttribute('required');
}
}
else {
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].setAttribute('required', 'true');
}
}
} }
if (selected) {
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].removeAttribute('required');
}
}
else {
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].setAttribute('required', 'true');
}
}
}
</script> </script>
</head> </head>
<body> <body>
<th:block th:insert="~{fragments/common :: game}"></th:block> <th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{ocr.header}"></h2> <h2 th:text="#{ocr.header}"></h2>
<form th:if="${#lists.size(languages) > 0}" action="#" th:action="@{api/v1/misc/ocr-pdf}" method="post" enctype="multipart/form-data" class="mb-3"> <form th:if="${#lists.size(languages) > 0}" action="#" th:action="@{api/v1/misc/ocr-pdf}" method="post" enctype="multipart/form-data" class="mb-3">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="mb-3"> <div class="mb-3">
<label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label> <label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label>
<hr> <hr />
<div id="languages"> <div id="languages">
<div th:each="language, iterStat : ${languages}"> <div th:each="language, iterStat : ${languages}">
<input type="checkbox" th:name="languages" th:value="${language}" required th:id="${'language-' + language}" onchange="handleLangSelection()" /> <input type="checkbox" th:name="languages" th:value="${language}" required th:id="${'language-' + language}" onchange="handleLangSelection()" />
<label class="form-check-label" th:for="${'language-' + language}" th:text="${language}"></label> <label class="form-check-label" th:for="${'language-' + language}" th:text="${language}"></label>
</div>
</div>
<hr>
</div>
<div class="mb-3">
<label th:text="#{ocr.selectText.10}"></label>
<select class="form-control" name="ocrType">
<option value="skip-text" th:text="#{ocr.selectText.6}"></option>
<option value="force-ocr" th:text="#{ocr.selectText.7}"></option>
<option value="Normal" th:text="#{ocr.selectText.8}"></option>
</select>
</div>
<br>
<label for="languages" class="form-label" th:text="#{ocr.selectText.9}"></label>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="sidecar" id="sidecar" />
<label class="form-check-label" for="sidecar" th:text="#{ocr.selectText.2}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="deskew" id="deskew" />
<label class="form-check-label" for="deskew" th:text="#{ocr.selectText.3}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="clean" id="clean" />
<label class="form-check-label" for="clean" th:text="#{ocr.selectText.4}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="clean-final" id="clean-final" />
<label class="form-check-label" for="clean-final" th:text="#{ocr.selectText.5}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="removeImagesAfter" id="removeImagesAfter" />
<label class="form-check-label" for="removeImagesAfter" th:text="#{ocr.selectText.11}"></label>
</div>
<div class="mb-3">
<label th:text="#{ocr.selectText.12}"></label>
<select class="form-control" name="ocrRenderType">
<option value="hocr">HOCR (Latin/Roman alphabet only)</option>
<option value="sandwich">Sandwich</option>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{ocr.submit}"></button>
</form>
<script>
const languageMap = {
'afr': 'Afrikaans',
'amh': 'Amharic',
'ara': 'Arabic',
'asm': 'Assamese',
'aze': 'Azerbaijani',
'aze_cyrl': 'Azerbaijani (Cyrillic)',
'bel': 'Belarusian',
'ben': 'Bengali',
'bod': 'Tibetan',
'bos': 'Bosnian',
'bre': 'Breton',
'bul': 'Bulgarian',
'cat': 'Catalan',
'ceb': 'Cebuano',
'ces': 'Czech',
'chi_sim': 'Chinese (Simplified)',
'chi_sim_vert': 'Chinese (Simplified, Vertical)',
'chi_tra': 'Chinese (Traditional)',
'chi_tra_vert': 'Chinese (Traditional, Vertical)',
'chr': 'Cherokee',
'cos': 'Corsican',
'cym': 'Welsh',
'dan': 'Danish',
'dan_frak': 'Danish (Fraktur)',
'deu': 'German',
'deu_frak': 'German (Fraktur)',
'div': 'Divehi',
'dzo': 'Dzongkha',
'ell': 'Greek',
'eng': 'English',
'enm': 'English, Middle (1100-1500)',
'epo': 'Esperanto',
'equ': 'Math / equation detection module',
'est': 'Estonian',
'eus': 'Basque',
'fao': 'Faroese',
'fas': 'Persian',
'fil': 'Filipino',
'fin': 'Finnish',
'fra': 'French',
'frk': 'Frankish',
'frm': 'French, Middle (ca.1400-1600)',
'fry': 'Western Frisian',
'gla': 'Scottish Gaelic',
'gle': 'Irish',
'glg': 'Galician',
'grc': 'Ancient Greek',
'guj': 'Gujarati',
'hat': 'Haitian, Haitian Creole',
'heb': 'Hebrew',
'hin': 'Hindi',
'hrv': 'Croatian',
'hun': 'Hungarian',
'hye': 'Armenian',
'iku': 'Inuktitut',
'ind': 'Indonesian',
'isl': 'Icelandic',
'ita': 'Italian',
'ita_old': 'Italian (Old)',
'jav': 'Javanese',
'jpn': 'Japanese',
'jpn_vert': 'Japanese (Vertical)',
'kan': 'Kannada',
'kat': 'Georgian',
'kat_old': 'Georgian (Old)',
'kaz': 'Kazakh',
'khm': 'Central Khmer',
'kir': 'Kirghiz, Kyrgyz',
'kmr': 'Northern Kurdish',
'kor': 'Korean',
'kor_vert': 'Korean (Vertical)',
'lao': 'Lao',
'lat': 'Latin',
'lav': 'Latvian',
'lit': 'Lithuanian',
'ltz': 'Luxembourgish',
'mal': 'Malayalam',
'mar': 'Marathi',
'mkd': 'Macedonian',
'mlt': 'Maltese',
'mon': 'Mongolian',
'mri': 'Maori',
'msa': 'Malay',
'mya': 'Burmese',
'nep': 'Nepali',
'nld': 'Dutch; Flemish',
'nor': 'Norwegian',
'oci': 'Occitan (post 1500)',
'ori': 'Oriya',
'osd': 'Orientation and script detection module',
'pan': 'Panjabi, Punjabi',
'pol': 'Polish',
'por': 'Portuguese',
'pus': 'Pushto, Pashto',
'que': 'Quechua',
'ron': 'Romanian, Moldavian, Moldovan',
'rus': 'Russian',
'san': 'Sanskrit',
'sin': 'Sinhala, Sinhalese',
'slk': 'Slovak',
'slk_frak': 'Slovak (Fraktur)',
'slv': 'Slovenian',
'snd': 'Sindhi',
'spa': 'Spanish',
'spa_old': 'Spanish (Old)',
'sqi': 'Albanian',
'srp': 'Serbian',
'srp_latn': 'Serbian (Latin)',
'sun': 'Sundanese',
'swa': 'Swahili',
'swe': 'Swedish',
'syr': 'Syriac',
'tam': 'Tamil',
'tat': 'Tatar',
'tel': 'Telugu',
'tgk': 'Tajik',
'tgl': 'Tagalog',
'tha': 'Thai',
'tir': 'Tigrinya',
'ton': 'Tonga (Tonga Islands)',
'tur': 'Turkish',
'uig': 'Uighur, Uyghur',
'ukr': 'Ukrainian',
'urd': 'Urdu',
'uzb': 'Uzbek',
'uzb_cyrl': 'Uzbek (Cyrillic)',
'vie': 'Vietnamese',
'yid': 'Yiddish',
'yor': 'Yoruba'
};
// Step 2: Function to get the full language name
function getFullLanguageName(shortCode) {
return languageMap[shortCode] || shortCode;
}
// Step 3: Apply the function to your labels
document.addEventListener('DOMContentLoaded', () => {
const labels = document.querySelectorAll('#languages .form-check-label');
labels.forEach(label => {
const languageCode = label.getAttribute('for').split('-')[1];
label.textContent = getFullLanguageName(languageCode);
});
});
</script>
<p th:text="#{ocr.credit}"></p>
<p th:text="#{ocr.help}"></p>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md">https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md</a>
</div> </div>
</div>
<hr />
</div> </div>
<div class="mb-3">
<label th:text="#{ocr.selectText.10}"></label>
<select class="form-control" name="ocrType">
<option value="skip-text" th:text="#{ocr.selectText.6}"></option>
<option value="force-ocr" th:text="#{ocr.selectText.7}"></option>
<option value="Normal" th:text="#{ocr.selectText.8}"></option>
</select>
</div>
<br>
<label for="languages" class="form-label" th:text="#{ocr.selectText.9}"></label>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="sidecar" id="sidecar" />
<label class="form-check-label" for="sidecar" th:text="#{ocr.selectText.2}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="deskew" id="deskew" />
<label class="form-check-label" for="deskew" th:text="#{ocr.selectText.3}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="clean" id="clean" />
<label class="form-check-label" for="clean" th:text="#{ocr.selectText.4}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="clean-final" id="clean-final" />
<label class="form-check-label" for="clean-final" th:text="#{ocr.selectText.5}"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="removeImagesAfter" id="removeImagesAfter" />
<label class="form-check-label" for="removeImagesAfter" th:text="#{ocr.selectText.11}"></label>
</div>
<div class="mb-3">
<label th:text="#{ocr.selectText.12}"></label>
<select class="form-control" name="ocrRenderType">
<option value="hocr">HOCR (Latin/Roman alphabet only)</option>
<option value="sandwich">Sandwich</option>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{ocr.submit}"></button>
</form>
<script>
const languageMap = {
'afr': 'Afrikaans',
'amh': 'Amharic',
'ara': 'Arabic',
'asm': 'Assamese',
'aze': 'Azerbaijani',
'aze_cyrl': 'Azerbaijani (Cyrillic)',
'bel': 'Belarusian',
'ben': 'Bengali',
'bod': 'Tibetan',
'bos': 'Bosnian',
'bre': 'Breton',
'bul': 'Bulgarian',
'cat': 'Catalan',
'ceb': 'Cebuano',
'ces': 'Czech',
'chi_sim': 'Chinese (Simplified)',
'chi_sim_vert': 'Chinese (Simplified, Vertical)',
'chi_tra': 'Chinese (Traditional)',
'chi_tra_vert': 'Chinese (Traditional, Vertical)',
'chr': 'Cherokee',
'cos': 'Corsican',
'cym': 'Welsh',
'dan': 'Danish',
'dan_frak': 'Danish (Fraktur)',
'deu': 'German',
'deu_frak': 'German (Fraktur)',
'div': 'Divehi',
'dzo': 'Dzongkha',
'ell': 'Greek',
'eng': 'English',
'enm': 'English, Middle (1100-1500)',
'epo': 'Esperanto',
'equ': 'Math / equation detection module',
'est': 'Estonian',
'eus': 'Basque',
'fao': 'Faroese',
'fas': 'Persian',
'fil': 'Filipino',
'fin': 'Finnish',
'fra': 'French',
'frk': 'Frankish',
'frm': 'French, Middle (ca.1400-1600)',
'fry': 'Western Frisian',
'gla': 'Scottish Gaelic',
'gle': 'Irish',
'glg': 'Galician',
'grc': 'Ancient Greek',
'guj': 'Gujarati',
'hat': 'Haitian, Haitian Creole',
'heb': 'Hebrew',
'hin': 'Hindi',
'hrv': 'Croatian',
'hun': 'Hungarian',
'hye': 'Armenian',
'iku': 'Inuktitut',
'ind': 'Indonesian',
'isl': 'Icelandic',
'ita': 'Italian',
'ita_old': 'Italian (Old)',
'jav': 'Javanese',
'jpn': 'Japanese',
'jpn_vert': 'Japanese (Vertical)',
'kan': 'Kannada',
'kat': 'Georgian',
'kat_old': 'Georgian (Old)',
'kaz': 'Kazakh',
'khm': 'Central Khmer',
'kir': 'Kirghiz, Kyrgyz',
'kmr': 'Northern Kurdish',
'kor': 'Korean',
'kor_vert': 'Korean (Vertical)',
'lao': 'Lao',
'lat': 'Latin',
'lav': 'Latvian',
'lit': 'Lithuanian',
'ltz': 'Luxembourgish',
'mal': 'Malayalam',
'mar': 'Marathi',
'mkd': 'Macedonian',
'mlt': 'Maltese',
'mon': 'Mongolian',
'mri': 'Maori',
'msa': 'Malay',
'mya': 'Burmese',
'nep': 'Nepali',
'nld': 'Dutch; Flemish',
'nor': 'Norwegian',
'oci': 'Occitan (post 1500)',
'ori': 'Oriya',
'osd': 'Orientation and script detection module',
'pan': 'Panjabi, Punjabi',
'pol': 'Polish',
'por': 'Portuguese',
'pus': 'Pushto, Pashto',
'que': 'Quechua',
'ron': 'Romanian, Moldavian, Moldovan',
'rus': 'Russian',
'san': 'Sanskrit',
'sin': 'Sinhala, Sinhalese',
'slk': 'Slovak',
'slk_frak': 'Slovak (Fraktur)',
'slv': 'Slovenian',
'snd': 'Sindhi',
'spa': 'Spanish',
'spa_old': 'Spanish (Old)',
'sqi': 'Albanian',
'srp': 'Serbian',
'srp_latn': 'Serbian (Latin)',
'sun': 'Sundanese',
'swa': 'Swahili',
'swe': 'Swedish',
'syr': 'Syriac',
'tam': 'Tamil',
'tat': 'Tatar',
'tel': 'Telugu',
'tgk': 'Tajik',
'tgl': 'Tagalog',
'tha': 'Thai',
'tir': 'Tigrinya',
'ton': 'Tonga (Tonga Islands)',
'tur': 'Turkish',
'uig': 'Uighur, Uyghur',
'ukr': 'Ukrainian',
'urd': 'Urdu',
'uzb': 'Uzbek',
'uzb_cyrl': 'Uzbek (Cyrillic)',
'vie': 'Vietnamese',
'yid': 'Yiddish',
'yor': 'Yoruba'
};
// Step 2: Function to get the full language name
function getFullLanguageName(shortCode) {
return languageMap[shortCode] || shortCode;
}
// Step 3: Apply the function to your labels
document.addEventListener('DOMContentLoaded', () => {
const labels = document.querySelectorAll('#languages .form-check-label');
labels.forEach(label => {
const languageCode = label.getAttribute('for').split('-')[1];
label.textContent = getFullLanguageName(languageCode);
});
});
</script>
<p th:text="#{ocr.credit}"></p>
<p th:text="#{ocr.help}"></p>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md">https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md</a>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,64 +1,65 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{removeAnnotations.title}, header=#{removeAnnotations.header})}"></th:block> <head>
<th:block th:insert="~{fragments/common :: head(title=#{removeAnnotations.title}, header=#{removeAnnotations.header})}"></th:block>
</head>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{removeAnnotations.header}"></h2> <h2 th:text="#{removeAnnotations.header}"></h2>
<form id="pdfForm" class="mb-3"> <form id="pdfForm" class="mb-3">
<div class="custom-file"> <div class="custom-file">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeAnnotations.submit}"></button>
</form>
</div>
</div> </div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeAnnotations.submit}"></button>
</form>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<script src="js/local-pdf-input-download.js"></script> <script src="js/local-pdf-input-download.js"></script>
<script> <script>
document.getElementById('pdfForm').addEventListener('submit', async (e) => { document.getElementById('pdfForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const { PDFDocument } = PDFLib; const { PDFDocument } = PDFLib;
const processFile = async (file) => { const processFile = async (file) => {
const origFileUrl = URL.createObjectURL(file); const origFileUrl = URL.createObjectURL(file);
const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer()); const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true }); const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true });
const pages = pdfDoc.getPages(); const pages = pdfDoc.getPages();
for (let i = 0; i < pages.length; ++i) { for (let i = 0; i < pages.length; ++i) {
const page = pages[i]; const page = pages[i];
const annotations = page.node.Annots(); const annotations = page.node.Annots();
if (!annotations) continue; if (!annotations) continue;
const ctx = annotations.context; const ctx = annotations.context;
for (let j = 0; j < annotations.size(); ++j) { for (let j = 0; j < annotations.size(); ++j) {
const annotation = annotations.get(j); const annotation = annotations.get(j);
ctx.delete(annotation); ctx.delete(annotation);
}
} }
}
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_removed_annotations.pdf'; const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_removed_annotations.pdf';
return { processedData: pdfBlob, fileName }; return { processedData: pdfBlob, fileName };
}; };
await downloadFilesWithCallback(processFile); await downloadFilesWithCallback(processFile);
}); });
</script> </script>
</body> </body>
</html>
</html>

View File

@ -1,39 +1,38 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{removeBlanks.title}, header=#{removeBlanks.header})}"></th:block>
</head>
<th:block th:insert="~{fragments/common :: head(title=#{removeBlanks.title}, header=#{removeBlanks.header})}"></th:block> <body>
<div id="page-container">
<div id="content-wrap">
<body> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div id="page-container"> <br /><br />
<div id="content-wrap"> <div class="container">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div class="row justify-content-center">
<br> <br> <div class="col-md-6">
<div class="container"> <h2 th:text="#{removeBlanks.header}"></h2>
<div class="row justify-content-center"> <form id="multiPdfForm" th:action="@{api/v1/misc/remove-blanks}" method="post" enctype="multipart/form-data">
<div class="col-md-6"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<h2 th:text="#{removeBlanks.header}"></h2> <div class="mb-3">
<label for="threshold" th:text="#{removeBlanks.threshold}"></label>
<form id="multiPdfForm" th:action="@{api/v1/misc/remove-blanks}" method="post" enctype="multipart/form-data"> <input type="number" class="form-control" id="threshold" name="threshold" value="10">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <small id="thresholdHelp" class="form-text text-muted" th:text="#{removeBlanks.thresholdDesc}"></small>
<div class="mb-3"> </div>
<label for="threshold" th:text="#{removeBlanks.threshold}"></label> <div class="mb-3">
<input type="number" class="form-control" id="threshold" name="threshold" value="10"> <label for="whitePercent" th:text="#{removeBlanks.whitePercent}"></label>
<small id="thresholdHelp" class="form-text text-muted" th:text="#{removeBlanks.thresholdDesc}"></small> <input type="number" class="form-control" id="whitePercent" name="whitePercent" value="99.9" step="0.1">
</div> <small id="whitePercentHelp" class="form-text text-muted" th:text="#{removeBlanks.whitePercentDesc}"></small>
<div class="mb-3"> </div>
<label for="whitePercent" th:text="#{removeBlanks.whitePercent}"></label> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeBlanks.submit}"></button>
<input type="number" class="form-control" id="whitePercent" name="whitePercent" value="99.9" step="0.1"> </form>
<small id="whitePercentHelp" class="form-text text-muted" th:text="#{removeBlanks.whitePercentDesc}"></small> </div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeBlanks.submit}"></button>
</form>
</div> </div>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </body>
</div>
</body>
</html> </html>

View File

@ -1,29 +1,27 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{repair.title}, header=#{repair.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: head(title=#{repair.title}, header=#{repair.header})}"></th:block>
<body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br> <br> <br /><br />
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{repair.header}"></h2> <h2 th:text="#{repair.header}"></h2>
<form id="multiPdfForm" th:action="@{api/v1/misc/repair}" method="post" enctype="multipart/form-data"> <form id="multiPdfForm" th:action="@{api/v1/misc/repair}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{repair.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{repair.submit}"></button>
</form> </form>
</div>
</div>
</div> </div>
</div>
</div> </div>
<div th:insert="~{fragments/footer.html :: footer}"></div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body> </body>
</html> </html>

Some files were not shown because too many files have changed in this diff Show More