Initial Commit
This commit is contained in:
Executable
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2025-2026 AIIrondev
|
||||
*
|
||||
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||||
* See Legal/LICENSE for the full license text.
|
||||
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
* For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
*/
|
||||
/* Styles for planned appointments in item details */
|
||||
.planned-appointments-panel {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background-color: #e1f5fe;
|
||||
border-left: 4px solid #03a9f4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.planned-appointments-panel h4 {
|
||||
color: #0277bd;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.appointment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.appointment-item {
|
||||
padding: 10px;
|
||||
background-color: #f9fdff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #b3e5fc;
|
||||
}
|
||||
|
||||
.appointment-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.appointment-date {
|
||||
font-weight: bold;
|
||||
color: #0288d1;
|
||||
}
|
||||
|
||||
.appointment-period {
|
||||
background-color: #03a9f4;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.appointment-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.status-planned {
|
||||
background-color: #03a9f4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #9E9E9E;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background-color: #F44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.appointment-user {
|
||||
margin-left: auto;
|
||||
font-style: italic;
|
||||
color: #546e7a;
|
||||
}
|
||||
|
||||
.appointment-notes {
|
||||
font-size: 0.9em;
|
||||
color: #455a64;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid #e1f5fe;
|
||||
font-style: italic;
|
||||
}
|
||||
Executable
+806
@@ -0,0 +1,806 @@
|
||||
/*
|
||||
* Copyright 2025-2026 AIIrondev
|
||||
*
|
||||
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||||
* See Legal/LICENSE for the full license text.
|
||||
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
* For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
*/
|
||||
/* Global visual system for all pages */
|
||||
:root {
|
||||
--ui-bg: #f2f5f8;
|
||||
--ui-bg-accent: #e7edf4;
|
||||
--ui-surface: #ffffff;
|
||||
--ui-surface-soft: #f8fafc;
|
||||
--ui-border: #d8e0e8;
|
||||
--ui-text: #1f2d3d;
|
||||
--ui-text-muted: #5d6d7e;
|
||||
--ui-title: #1a2e44;
|
||||
--ui-shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||
--ui-shadow-md: 0 10px 24px rgba(15, 23, 42, 0.1);
|
||||
--ui-radius: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
|
||||
color: var(--ui-text);
|
||||
background: var(--ui-bg);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--ui-surface);
|
||||
border: 0;
|
||||
padding: 22px;
|
||||
border-radius: var(--ui-radius);
|
||||
box-shadow: none;
|
||||
width: min(1200px, calc(100% - 28px));
|
||||
margin: 16px auto;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: var(--ui-title);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p,
|
||||
small,
|
||||
label,
|
||||
span {
|
||||
color: var(--ui-text-muted);
|
||||
}
|
||||
|
||||
.card,
|
||||
.content,
|
||||
.form-card,
|
||||
.modal-content,
|
||||
.list-group-item,
|
||||
.dropdown-menu,
|
||||
.table,
|
||||
.alert {
|
||||
border-radius: var(--ui-radius);
|
||||
}
|
||||
|
||||
.card,
|
||||
.content,
|
||||
.form-card,
|
||||
.modal-content,
|
||||
.list-group-item,
|
||||
.dropdown-menu,
|
||||
.table {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
background: var(--ui-surface);
|
||||
}
|
||||
|
||||
.card-header,
|
||||
.modal-header,
|
||||
thead th {
|
||||
background: linear-gradient(180deg, #f7fafd, #edf3f8) !important;
|
||||
border-bottom: 1px solid var(--ui-border) !important;
|
||||
color: var(--ui-title) !important;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select,
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
input[type="email"],
|
||||
input[type="date"],
|
||||
select {
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid #cad5e0 !important;
|
||||
min-height: 40px;
|
||||
color: var(--ui-text) !important;
|
||||
background-color: #fff !important;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus,
|
||||
textarea:focus,
|
||||
input:focus,
|
||||
select:focus {
|
||||
border-color: #7ea3c8 !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(70, 130, 180, 0.14) !important;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.table {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
border-color: #dee6ee !important;
|
||||
}
|
||||
|
||||
.table-striped > tbody > tr:nth-of-type(odd) {
|
||||
background-color: #f8fbff;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-width: 1px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-bottom: 1px solid #273341;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.container {
|
||||
width: calc(100% - 18px);
|
||||
padding: 14px;
|
||||
margin: 10px auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit and generate buttons with black text for better visibility */
|
||||
.edit-button {
|
||||
color: black !important; /* Override any existing color */
|
||||
}
|
||||
|
||||
.duplicate-button {
|
||||
color: black !important; /* Override any existing color */
|
||||
}
|
||||
|
||||
.generate-qr-button {
|
||||
color: black !important; /* Override any existing color */
|
||||
}
|
||||
|
||||
.ausleihen {
|
||||
color: black !important; /* Override any existing color */
|
||||
}
|
||||
|
||||
/* Modal image display fix */
|
||||
.modal-image {
|
||||
display: none;
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.modal-image.active-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Active image display classes */
|
||||
.item-image.active-image {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.item-image:not(.active-image) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* QR Reader styling */
|
||||
#qr-reader {
|
||||
width: 500px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Edit new location container */
|
||||
.edit-new-location-container {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Modal dialog styling */
|
||||
.modal-dialog-white {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-content-margin {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Unified list/card view switch styling for main and main_admin */
|
||||
.view-switch-group {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.view-toggle-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid #c7ced6;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
font-size: 1.15rem;
|
||||
cursor: pointer;
|
||||
color: #2f3e4e;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.12s ease, box-shadow 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.view-toggle-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #8fa2b7;
|
||||
background: #f8fbff;
|
||||
box-shadow: 0 3px 8px rgba(33, 37, 41, 0.12);
|
||||
}
|
||||
|
||||
.view-toggle-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.view-toggle-btn[aria-pressed='true'] {
|
||||
background: #e9f2ff;
|
||||
border-color: #7ea1c8;
|
||||
color: #1f4f7a;
|
||||
}
|
||||
|
||||
.table-view-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.table-view .table-view-header {
|
||||
display: grid;
|
||||
grid-template-columns: var(--table-columns, minmax(180px, 2fr) repeat(5, minmax(110px, 1fr)));
|
||||
gap: 10px 14px;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 8px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: #3f4e5d;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
body.table-view .items-container {
|
||||
display: block !important;
|
||||
padding: 0;
|
||||
overflow-x: visible;
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
|
||||
body.table-view .item-card {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e1e6ed;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: none;
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content {
|
||||
display: grid;
|
||||
grid-template-columns: var(--table-columns, minmax(180px, 2fr) repeat(5, minmax(110px, 1fr)));
|
||||
gap: 10px 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content h3,
|
||||
body.table-view .item-card .card-content p {
|
||||
margin: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content p strong {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content .item-name-cell,
|
||||
body.table-view .item-card .card-content .item-col-name {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
color: #1f2d3d;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content .item-ort-cell,
|
||||
body.table-view .item-card .card-content .item-col-location,
|
||||
body.table-view .item-card .card-content .item-filter1-cell,
|
||||
body.table-view .item-card .card-content .item-col-filter1,
|
||||
body.table-view .item-card .card-content .item-filter2-cell,
|
||||
body.table-view .item-card .card-content .item-col-filter2,
|
||||
body.table-view .item-card .card-content .item-filter3-cell,
|
||||
body.table-view .item-card .card-content .item-col-filter3,
|
||||
body.table-view .item-card .card-content .item-code-cell,
|
||||
body.table-view .item-card .card-content .item-col-code,
|
||||
body.table-view .item-card .card-content .item-col-count {
|
||||
color: #445465;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
body.table-view .item-card .image-container,
|
||||
body.table-view .item-card .borrower-badge,
|
||||
body.table-view .item-card .appointment-badge,
|
||||
body.table-view .item-card .damage-badge {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.table-view .item-card .actions {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #d9e0e8;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.view-switch-group {
|
||||
position: static;
|
||||
margin: 8px auto 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
body.table-view .table-view-header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content p strong {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
/* Unified action button styling for main and main_admin */
|
||||
:root {
|
||||
--action-radius: 10px;
|
||||
--action-shadow: none;
|
||||
--action-shadow-hover: none;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.actions form,
|
||||
.modal-actions form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions .ausleihen,
|
||||
.actions .schedule-button,
|
||||
.actions .quick-details-button,
|
||||
.actions .edit-button,
|
||||
.actions .duplicate-button,
|
||||
.actions .delete-button,
|
||||
.actions .damage-button,
|
||||
.modal-actions .ausleihen,
|
||||
.modal-actions .schedule-button,
|
||||
.modal-actions .quick-details-button,
|
||||
.modal-actions .edit-button,
|
||||
.modal-actions .duplicate-button,
|
||||
.modal-actions .delete-button,
|
||||
.modal-actions .damage-button {
|
||||
min-height: 38px !important;
|
||||
padding: 8px 14px !important;
|
||||
border-radius: var(--action-radius) !important;
|
||||
border-width: 1px !important;
|
||||
border-style: solid !important;
|
||||
font-size: 0.88rem !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: var(--action-shadow);
|
||||
transition: transform 0.14s ease, box-shadow 0.14s ease, filter 0.14s ease;
|
||||
}
|
||||
|
||||
.actions .ausleihen,
|
||||
.modal-actions .ausleihen {
|
||||
background: #198754 !important;
|
||||
border-color: #157347 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.actions .schedule-button,
|
||||
.modal-actions .schedule-button {
|
||||
background: #fd7e14 !important;
|
||||
border-color: #e46f10 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.actions .quick-details-button,
|
||||
.modal-actions .quick-details-button {
|
||||
background: #6c757d !important;
|
||||
border-color: #616971 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.actions .edit-button,
|
||||
.modal-actions .edit-button {
|
||||
background: #0d6efd !important;
|
||||
border-color: #0b5ed7 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.actions .duplicate-button,
|
||||
.modal-actions .duplicate-button {
|
||||
background: #6f42c1 !important;
|
||||
border-color: #6439af !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.actions .delete-button,
|
||||
.modal-actions .delete-button {
|
||||
background: #dc3545 !important;
|
||||
border-color: #bb2d3b !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.actions .damage-button,
|
||||
.modal-actions .damage-button {
|
||||
background: #0dcaf0 !important;
|
||||
border-color: #0aa5c0 !important;
|
||||
color: #0b1f2a !important;
|
||||
}
|
||||
|
||||
.actions .disabled-button,
|
||||
.modal-actions .disabled-button,
|
||||
.actions button:disabled,
|
||||
.modal-actions button:disabled {
|
||||
opacity: 0.72;
|
||||
filter: grayscale(0.28);
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.actions .ausleihen:hover,
|
||||
.actions .schedule-button:hover,
|
||||
.actions .quick-details-button:hover,
|
||||
.actions .edit-button:hover,
|
||||
.actions .duplicate-button:hover,
|
||||
.actions .delete-button:hover,
|
||||
.actions .damage-button:hover,
|
||||
.modal-actions .ausleihen:hover,
|
||||
.modal-actions .schedule-button:hover,
|
||||
.modal-actions .quick-details-button:hover,
|
||||
.modal-actions .edit-button:hover,
|
||||
.modal-actions .duplicate-button:hover,
|
||||
.modal-actions .delete-button:hover,
|
||||
.modal-actions .damage-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--action-shadow-hover);
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
|
||||
.actions button:active,
|
||||
.modal-actions button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--action-shadow);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.actions,
|
||||
.modal-actions {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.actions .ausleihen,
|
||||
.actions .schedule-button,
|
||||
.actions .quick-details-button,
|
||||
.actions .edit-button,
|
||||
.actions .duplicate-button,
|
||||
.actions .delete-button,
|
||||
.actions .damage-button,
|
||||
.modal-actions .ausleihen,
|
||||
.modal-actions .schedule-button,
|
||||
.modal-actions .quick-details-button,
|
||||
.modal-actions .edit-button,
|
||||
.modal-actions .duplicate-button,
|
||||
.modal-actions .delete-button,
|
||||
.modal-actions .damage-button {
|
||||
min-height: 36px !important;
|
||||
padding: 7px 12px !important;
|
||||
font-size: 0.84rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Global uniform button system for all pages */
|
||||
:root {
|
||||
--ui-btn-radius: 10px;
|
||||
--ui-btn-min-height: 38px;
|
||||
--ui-btn-padding-y: 8px;
|
||||
--ui-btn-padding-x: 14px;
|
||||
--ui-btn-font-size: 0.9rem;
|
||||
--ui-btn-font-weight: 700;
|
||||
--ui-btn-shadow: none;
|
||||
--ui-btn-shadow-hover: none;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.action-button,
|
||||
.button,
|
||||
.export-button,
|
||||
.search-button,
|
||||
.scan-button,
|
||||
.save-button,
|
||||
.cancel-button,
|
||||
.primary-button,
|
||||
.danger-button,
|
||||
.submit-button,
|
||||
.nav-back-button,
|
||||
.back-button,
|
||||
.add-new-btn,
|
||||
.fetch-isbn-button,
|
||||
.import-book-button,
|
||||
.return-button,
|
||||
.item-reset-button,
|
||||
.admin-only-button,
|
||||
.reset-cancel-btn,
|
||||
.reset-confirm-btn,
|
||||
.btn-accept,
|
||||
.btn-decline,
|
||||
.popup-close-button,
|
||||
.delete-image-button,
|
||||
.remove-book-cover-button,
|
||||
.remove-duplicate-image-button {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 6px;
|
||||
min-height: var(--ui-btn-min-height) !important;
|
||||
padding: var(--ui-btn-padding-y) var(--ui-btn-padding-x) !important;
|
||||
border-radius: var(--ui-btn-radius) !important;
|
||||
border: 1px solid transparent !important;
|
||||
font-size: var(--ui-btn-font-size) !important;
|
||||
font-weight: var(--ui-btn-font-weight) !important;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
text-decoration: none !important;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--ui-btn-shadow);
|
||||
transition: transform 0.14s ease, box-shadow 0.14s ease, filter 0.14s ease;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
.action-button:hover,
|
||||
.button:hover,
|
||||
.export-button:hover,
|
||||
.search-button:hover,
|
||||
.scan-button:hover,
|
||||
.save-button:hover,
|
||||
.cancel-button:hover,
|
||||
.primary-button:hover,
|
||||
.danger-button:hover,
|
||||
.submit-button:hover,
|
||||
.nav-back-button:hover,
|
||||
.back-button:hover,
|
||||
.add-new-btn:hover,
|
||||
.fetch-isbn-button:hover,
|
||||
.import-book-button:hover,
|
||||
.return-button:hover,
|
||||
.item-reset-button:hover,
|
||||
.admin-only-button:hover,
|
||||
.reset-cancel-btn:hover,
|
||||
.reset-confirm-btn:hover,
|
||||
.btn-accept:hover,
|
||||
.btn-decline:hover,
|
||||
.popup-close-button:hover,
|
||||
.delete-image-button:hover,
|
||||
.remove-book-cover-button:hover,
|
||||
.remove-duplicate-image-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--ui-btn-shadow-hover);
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
|
||||
.btn:active,
|
||||
.action-button:active,
|
||||
.button:active,
|
||||
.export-button:active,
|
||||
.search-button:active,
|
||||
.scan-button:active,
|
||||
.save-button:active,
|
||||
.cancel-button:active,
|
||||
.primary-button:active,
|
||||
.danger-button:active,
|
||||
.submit-button:active,
|
||||
.nav-back-button:active,
|
||||
.back-button:active,
|
||||
.add-new-btn:active,
|
||||
.fetch-isbn-button:active,
|
||||
.import-book-button:active,
|
||||
.return-button:active,
|
||||
.item-reset-button:active,
|
||||
.admin-only-button:active,
|
||||
.reset-cancel-btn:active,
|
||||
.reset-confirm-btn:active,
|
||||
.btn-accept:active,
|
||||
.btn-decline:active,
|
||||
.popup-close-button:active,
|
||||
.delete-image-button:active,
|
||||
.remove-book-cover-button:active,
|
||||
.remove-duplicate-image-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--ui-btn-shadow);
|
||||
}
|
||||
|
||||
.btn:disabled,
|
||||
.action-button:disabled,
|
||||
.button:disabled,
|
||||
.export-button:disabled,
|
||||
.search-button:disabled,
|
||||
.scan-button:disabled,
|
||||
.save-button:disabled,
|
||||
.cancel-button:disabled,
|
||||
.primary-button:disabled,
|
||||
.danger-button:disabled,
|
||||
.submit-button:disabled,
|
||||
.add-new-btn:disabled,
|
||||
.fetch-isbn-button:disabled,
|
||||
.import-book-button:disabled,
|
||||
.return-button:disabled,
|
||||
.item-reset-button:disabled,
|
||||
.admin-only-button:disabled,
|
||||
.reset-cancel-btn:disabled,
|
||||
.reset-confirm-btn:disabled,
|
||||
.btn-accept:disabled,
|
||||
.btn-decline:disabled,
|
||||
.popup-close-button:disabled,
|
||||
.delete-image-button:disabled,
|
||||
.remove-book-cover-button:disabled,
|
||||
.remove-duplicate-image-button:disabled {
|
||||
opacity: 0.72;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(0.25);
|
||||
}
|
||||
|
||||
/* Preserve consistent role colors */
|
||||
.btn-primary,
|
||||
.save-button,
|
||||
.submit-button,
|
||||
.primary-button,
|
||||
.register-button {
|
||||
background: #0d6efd !important;
|
||||
border-color: #0b5ed7 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.cancel-button,
|
||||
.button,
|
||||
.nav-back-button,
|
||||
.back-button,
|
||||
.reset-cancel-btn {
|
||||
background: #6c757d !important;
|
||||
border-color: #616971 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-success,
|
||||
.btn-accept,
|
||||
.return-button,
|
||||
.item-reset-button,
|
||||
.import-book-button,
|
||||
.fetch-isbn-button,
|
||||
.scan-button {
|
||||
background: #198754 !important;
|
||||
border-color: #157347 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-warning,
|
||||
.export-button,
|
||||
.search-button,
|
||||
.add-new-btn {
|
||||
background: #fd7e14 !important;
|
||||
border-color: #e46f10 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-danger,
|
||||
.danger-button,
|
||||
.btn-decline,
|
||||
.reset-confirm-btn,
|
||||
.delete-image-button,
|
||||
.remove-book-cover-button,
|
||||
.remove-duplicate-image-button {
|
||||
background: #dc3545 !important;
|
||||
border-color: #bb2d3b !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
min-height: 32px !important;
|
||||
padding: 6px 10px !important;
|
||||
font-size: 0.82rem !important;
|
||||
}
|
||||
|
||||
.w-100.btn,
|
||||
.w-100.action-button,
|
||||
.w-100.button,
|
||||
.w-100.export-button,
|
||||
.w-100.save-button,
|
||||
.w-100.submit-button,
|
||||
.w-100.primary-button,
|
||||
.w-100.danger-button,
|
||||
.w-100.cancel-button {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.btn,
|
||||
.action-button,
|
||||
.button,
|
||||
.export-button,
|
||||
.search-button,
|
||||
.scan-button,
|
||||
.save-button,
|
||||
.cancel-button,
|
||||
.primary-button,
|
||||
.danger-button,
|
||||
.submit-button,
|
||||
.nav-back-button,
|
||||
.back-button,
|
||||
.add-new-btn,
|
||||
.fetch-isbn-button,
|
||||
.import-book-button,
|
||||
.return-button,
|
||||
.item-reset-button,
|
||||
.admin-only-button,
|
||||
.reset-cancel-btn,
|
||||
.reset-confirm-btn,
|
||||
.btn-accept,
|
||||
.btn-decline,
|
||||
.popup-close-button,
|
||||
.delete-image-button,
|
||||
.remove-book-cover-button,
|
||||
.remove-duplicate-image-button {
|
||||
min-height: 36px !important;
|
||||
padding: 7px 12px !important;
|
||||
font-size: 0.84rem !important;
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><rect width="200" height="200" fill="#f0f0f0"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="20" fill="#666">No Image</text></svg>
|
||||
|
After Width: | Height: | Size: 273 B |
Executable
+400
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* Copyright 2025-2026 AIIrondev
|
||||
*
|
||||
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||||
* See Legal/LICENSE for the full license text.
|
||||
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
* For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
*/
|
||||
|
||||
// Initialize when document is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (isIOSDevice()) {
|
||||
console.log('iOS device detected, applying iOS-specific enhancements');
|
||||
applyIOSFixes();
|
||||
}
|
||||
});
|
||||
|
||||
// Apply all iOS-specific fixes
|
||||
function applyIOSFixes() {
|
||||
// Fix file input issues
|
||||
fixIOSFileInputs();
|
||||
|
||||
// Fix form submission
|
||||
fixIOSFormSubmission();
|
||||
|
||||
// Fix image handling
|
||||
fixIOSImageHandling();
|
||||
|
||||
// Fix duplication
|
||||
fixIOSDuplication();
|
||||
|
||||
// Apply CSS fixes
|
||||
applyIOSCSSFixes();
|
||||
}
|
||||
|
||||
// Fix iOS file input issues
|
||||
function fixIOSFileInputs() {
|
||||
// Find all file inputs
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||
|
||||
fileInputs.forEach(input => {
|
||||
// Create a touch-friendly wrapper
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'ios-file-input-wrapper';
|
||||
wrapper.style.position = 'relative';
|
||||
|
||||
// Style the original input to be more touch-friendly
|
||||
input.style.padding = '20px 0';
|
||||
|
||||
// Wrap the input
|
||||
if (input.parentNode) {
|
||||
input.parentNode.insertBefore(wrapper, input);
|
||||
wrapper.appendChild(input);
|
||||
|
||||
// Add a visible button for better touch target
|
||||
const fakeButton = document.createElement('div');
|
||||
fakeButton.className = 'ios-file-button';
|
||||
fakeButton.textContent = 'Wählen Sie Bilder/Videos aus';
|
||||
fakeButton.style.display = 'inline-block';
|
||||
fakeButton.style.padding = '10px 15px';
|
||||
fakeButton.style.backgroundColor = '#4CAF50';
|
||||
fakeButton.style.color = 'white';
|
||||
fakeButton.style.borderRadius = '4px';
|
||||
fakeButton.style.textAlign = 'center';
|
||||
fakeButton.style.margin = '10px 0';
|
||||
fakeButton.style.fontWeight = 'bold';
|
||||
wrapper.appendChild(fakeButton);
|
||||
|
||||
// Make sure the real input covers the fake button
|
||||
input.style.position = 'absolute';
|
||||
input.style.top = '0';
|
||||
input.style.left = '0';
|
||||
input.style.width = '100%';
|
||||
input.style.height = '100%';
|
||||
input.style.opacity = '0';
|
||||
input.style.zIndex = '10';
|
||||
}
|
||||
|
||||
// Add custom event handling
|
||||
input.addEventListener('change', function() {
|
||||
// Update visual feedback
|
||||
const fileCount = this.files ? this.files.length : 0;
|
||||
let fileText = fakeButton.textContent;
|
||||
|
||||
if (fileCount > 0) {
|
||||
fileText = `${fileCount} Datei${fileCount !== 1 ? 'en' : ''} ausgewählt`;
|
||||
fakeButton.style.backgroundColor = '#2E7D32';
|
||||
} else {
|
||||
fileText = 'Wählen Sie Bilder/Videos aus';
|
||||
fakeButton.style.backgroundColor = '#4CAF50';
|
||||
}
|
||||
|
||||
fakeButton.textContent = fileText;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fix iOS form submission issues
|
||||
function fixIOSFormSubmission() {
|
||||
// Find upload forms
|
||||
const uploadForms = document.querySelectorAll('form[action*="upload_item"]');
|
||||
|
||||
uploadForms.forEach(form => {
|
||||
// Add an iOS submission handler
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Only intercept on iOS
|
||||
if (!isIOSDevice()) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Show that we're processing
|
||||
const loadingMessage = document.createElement('div');
|
||||
loadingMessage.className = 'ios-loading-message';
|
||||
loadingMessage.textContent = 'Wird verarbeitet...';
|
||||
loadingMessage.style.padding = '10px';
|
||||
loadingMessage.style.backgroundColor = '#f0f0f0';
|
||||
loadingMessage.style.borderRadius = '5px';
|
||||
loadingMessage.style.margin = '10px 0';
|
||||
loadingMessage.style.textAlign = 'center';
|
||||
form.appendChild(loadingMessage);
|
||||
|
||||
// Disable the submit button
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
}
|
||||
|
||||
// Use timeout to allow UI to update before heavy processing
|
||||
setTimeout(() => {
|
||||
// Gather form data with special handling for iOS
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Add iOS flag
|
||||
formData.append('is_ios', 'true');
|
||||
|
||||
// Submit with fetch API which works better on iOS
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Remove loading message
|
||||
if (loadingMessage.parentNode) {
|
||||
loadingMessage.parentNode.removeChild(loadingMessage);
|
||||
}
|
||||
|
||||
// Re-enable submit button
|
||||
if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
// Show success and redirect
|
||||
alert(data.message || 'Element erfolgreich hinzugefügt');
|
||||
|
||||
// Redirect to the appropriate page
|
||||
if (data.itemId) {
|
||||
window.location.href = `/home_admin?highlight_item=${data.itemId}`;
|
||||
} else {
|
||||
window.location.href = '/home_admin';
|
||||
}
|
||||
} else {
|
||||
// Show error
|
||||
alert(data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('iOS form submission error:', error);
|
||||
|
||||
// Remove loading message
|
||||
if (loadingMessage.parentNode) {
|
||||
loadingMessage.parentNode.removeChild(loadingMessage);
|
||||
}
|
||||
|
||||
// Re-enable submit button
|
||||
if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
|
||||
// Show error
|
||||
alert('Ein Netzwerkfehler ist aufgetreten. Bitte versuchen Sie es erneut.');
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fix iOS image handling issues
|
||||
function fixIOSImageHandling() {
|
||||
// Reduce image preview quality on iOS
|
||||
if (typeof ImagePreviewGenerator !== 'undefined') {
|
||||
// Override the compression quality for iOS
|
||||
const originalCompressImage = ImagePreviewGenerator.prototype._compressImage;
|
||||
|
||||
if (originalCompressImage) {
|
||||
ImagePreviewGenerator.prototype._compressImage = function(dataUrl) {
|
||||
// Lower quality for iOS
|
||||
this.quality = 0.5;
|
||||
this.maxWidth = 600;
|
||||
this.maxHeight = 600;
|
||||
|
||||
return originalCompressImage.call(this, dataUrl);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fix image preview display
|
||||
document.querySelectorAll('.image-preview-container').forEach(container => {
|
||||
// Add iOS-specific styling
|
||||
container.style.webkitOverflowScrolling = 'touch';
|
||||
container.style.maxHeight = '150px';
|
||||
container.style.overflow = 'auto';
|
||||
});
|
||||
}
|
||||
|
||||
// Fix iOS duplication issues
|
||||
function fixIOSDuplication() {
|
||||
// Override duplicateItem function
|
||||
if (typeof duplicateItem !== 'undefined') {
|
||||
const originalDuplicateItem = duplicateItem;
|
||||
|
||||
duplicateItem = function(itemId) {
|
||||
// Use localStorage on iOS instead of sessionStorage
|
||||
const useFunction = originalDuplicateItem;
|
||||
|
||||
useFunction(itemId);
|
||||
|
||||
// Additional iOS-specific handling
|
||||
const checkForSessionData = setInterval(() => {
|
||||
const sessionData = sessionStorage.getItem('duplicateItemData');
|
||||
if (sessionData) {
|
||||
// Copy from sessionStorage to localStorage
|
||||
try {
|
||||
localStorage.setItem('duplicateItemData', sessionData);
|
||||
localStorage.setItem('duplicateItemTimestamp', Date.now().toString());
|
||||
console.log('Copied duplicate data to localStorage for iOS reliability');
|
||||
} catch (e) {
|
||||
console.warn('Failed to copy to localStorage:', e);
|
||||
}
|
||||
|
||||
clearInterval(checkForSessionData);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Clear interval after 3 seconds regardless
|
||||
setTimeout(() => clearInterval(checkForSessionData), 3000);
|
||||
};
|
||||
}
|
||||
|
||||
// Fix prefill function
|
||||
if (typeof prefillFormWithDuplicateData !== 'undefined') {
|
||||
const originalPrefill = prefillFormWithDuplicateData;
|
||||
|
||||
prefillFormWithDuplicateData = function() {
|
||||
// Try to get data from localStorage first
|
||||
let data = null;
|
||||
try {
|
||||
const localData = localStorage.getItem('duplicateItemData');
|
||||
if (localData) {
|
||||
data = JSON.parse(localData);
|
||||
localStorage.removeItem('duplicateItemData');
|
||||
localStorage.removeItem('duplicateItemTimestamp');
|
||||
console.log('Using duplicate data from localStorage');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error accessing localStorage:', e);
|
||||
}
|
||||
|
||||
// If we got data from localStorage, manually populate the form
|
||||
if (data) {
|
||||
console.log('Manually populating form with iOS optimization');
|
||||
|
||||
// Basic fields
|
||||
document.getElementById('name').value = data.name || '';
|
||||
document.getElementById('beschreibung').value = data.description || '';
|
||||
|
||||
// Handle filters with a delay
|
||||
setTimeout(() => {
|
||||
// Filter 1
|
||||
if (data.filter1 && Array.isArray(data.filter1)) {
|
||||
data.filter1.forEach((val, idx) => {
|
||||
const field = document.getElementById(`filter1-${idx+1}`);
|
||||
if (field && val) field.value = val;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter 2
|
||||
if (data.filter2 && Array.isArray(data.filter2)) {
|
||||
data.filter2.forEach((val, idx) => {
|
||||
const field = document.getElementById(`filter2-${idx+1}`);
|
||||
if (field && val) field.value = val;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter 3
|
||||
if (data.filter3 && Array.isArray(data.filter3)) {
|
||||
data.filter3.forEach((val, idx) => {
|
||||
const field = document.getElementById(`filter3-${idx+1}`);
|
||||
if (field && val) field.value = val;
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// Location
|
||||
setTimeout(() => {
|
||||
const location = document.getElementById('ort');
|
||||
if (location && data.location) location.value = data.location;
|
||||
}, 400);
|
||||
|
||||
// Year and cost
|
||||
const year = document.getElementById('anschaffungsjahr');
|
||||
if (year && data.year) year.value = data.year;
|
||||
|
||||
const cost = document.getElementById('anschaffungskosten');
|
||||
if (cost && data.cost) cost.value = data.cost;
|
||||
|
||||
// Handle images - limited for iOS
|
||||
const form = document.querySelector('form');
|
||||
if (form && data.images && Array.isArray(data.images)) {
|
||||
// Add hidden field to indicate duplication
|
||||
const isDuplicatingField = document.createElement('input');
|
||||
isDuplicatingField.type = 'hidden';
|
||||
isDuplicatingField.name = 'is_duplicating';
|
||||
isDuplicatingField.value = 'true';
|
||||
form.appendChild(isDuplicatingField);
|
||||
|
||||
// Only use first 3 images on iOS for performance
|
||||
const limitedImages = data.images.slice(0, 3);
|
||||
|
||||
limitedImages.forEach(imageName => {
|
||||
const imageField = document.createElement('input');
|
||||
imageField.type = 'hidden';
|
||||
imageField.name = 'duplicate_images';
|
||||
imageField.value = imageName;
|
||||
form.appendChild(imageField);
|
||||
});
|
||||
|
||||
// Add a message about limited images
|
||||
if (data.images.length > 3) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.style.margin = '10px 0';
|
||||
messageDiv.style.padding = '10px';
|
||||
messageDiv.style.backgroundColor = '#fff3cd';
|
||||
messageDiv.style.borderRadius = '5px';
|
||||
messageDiv.style.color = '#856404';
|
||||
messageDiv.textContent = `Aus Leistungsgründen werden nur ${limitedImages.length} von ${data.images.length} Bildern verwendet`;
|
||||
|
||||
const fileInput = document.getElementById('images');
|
||||
if (fileInput && fileInput.parentNode) {
|
||||
fileInput.parentNode.appendChild(messageDiv);
|
||||
} else {
|
||||
form.appendChild(messageDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use original function as fallback
|
||||
originalPrefill();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Apply iOS-specific CSS fixes
|
||||
function applyIOSCSSFixes() {
|
||||
// Create a style element
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
/* Improve touch targets for iOS */
|
||||
button, select, input[type="submit"], .ausleihen, .edit-button,
|
||||
.delete-button, .duplicate-button, .details-button {
|
||||
min-height: 44px !important;
|
||||
padding: 10px 15px !important;
|
||||
}
|
||||
|
||||
/* Improve scrolling on iOS */
|
||||
.items-container, .filter-options, .modal-content {
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
/* Prevent zoom on inputs */
|
||||
input, select, textarea {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* Fix iOS image display */
|
||||
.item-image {
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Improve form fields on iOS */
|
||||
.form-group {
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
Executable
+285
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Copyright 2025-2026 AIIrondev
|
||||
*
|
||||
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||||
* See Legal/LICENSE for the full license text.
|
||||
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
* For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
*/
|
||||
|
||||
// Detect mobile devices including tablets
|
||||
function isMobileDevice() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
// Specifically detect iOS devices
|
||||
function isIOSDevice() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
}
|
||||
|
||||
// Mobile-compatible file tracker to replace DataTransfer API
|
||||
class MobileCompatFileTracker {
|
||||
constructor() {
|
||||
this.files = [];
|
||||
this.removedIndices = new Set();
|
||||
}
|
||||
|
||||
// Add files from a FileList
|
||||
addFiles(fileList) {
|
||||
// Convert FileList to Array for easier handling
|
||||
Array.from(fileList).forEach(file => {
|
||||
this.files.push(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove a file by index
|
||||
removeFile(index) {
|
||||
if (index >= 0 && index < this.files.length) {
|
||||
this.removedIndices.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all non-removed files
|
||||
getFiles() {
|
||||
return this.files.filter((_, index) => !this.removedIndices.has(index));
|
||||
}
|
||||
|
||||
// Convert to FormData for upload
|
||||
appendToFormData(formData, fieldName) {
|
||||
this.getFiles().forEach(file => {
|
||||
formData.append(fieldName, file);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear all files
|
||||
clear() {
|
||||
this.files = [];
|
||||
this.removedIndices.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile optimized image preview generator
|
||||
class ImagePreviewGenerator {
|
||||
constructor(options = {}) {
|
||||
this.maxWidth = options.maxWidth || 800;
|
||||
this.maxHeight = options.maxHeight || 600;
|
||||
this.quality = options.quality || 0.8;
|
||||
this.maxPreviewsOnMobile = options.maxPreviewsOnMobile || 3;
|
||||
}
|
||||
|
||||
// Create optimized image preview element
|
||||
createPreview(file, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file) {
|
||||
reject(new Error('No file provided'));
|
||||
return;
|
||||
}
|
||||
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
|
||||
if (isVideo) {
|
||||
this._createVideoPreview(file, index).then(resolve).catch(reject);
|
||||
} else if (file.type.startsWith('image/')) {
|
||||
this._createImagePreview(file, index).then(resolve).catch(reject);
|
||||
} else {
|
||||
reject(new Error('Unsupported file type'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create image preview with optimization for mobile
|
||||
_createImagePreview(file, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
// For mobile, we'll compress the image before creating the preview
|
||||
if (isMobileDevice()) {
|
||||
this._compressImage(e.target.result)
|
||||
.then(compressedDataUrl => {
|
||||
const previewElement = this._createPreviewElement(compressedDataUrl, file.name, index, false);
|
||||
resolve(previewElement);
|
||||
})
|
||||
.catch(err => {
|
||||
// Fallback to original if compression fails
|
||||
console.warn('Image compression failed, using original:', err);
|
||||
const previewElement = this._createPreviewElement(e.target.result, file.name, index, false);
|
||||
resolve(previewElement);
|
||||
});
|
||||
} else {
|
||||
// For desktop, use the original image
|
||||
const previewElement = this._createPreviewElement(e.target.result, file.name, index, false);
|
||||
resolve(previewElement);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Create video preview with thumbnail generation
|
||||
_createVideoPreview(file, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const videoUrl = URL.createObjectURL(file);
|
||||
const video = document.createElement('video');
|
||||
|
||||
video.src = videoUrl;
|
||||
video.onloadedmetadata = () => {
|
||||
// Create preview element
|
||||
const previewElement = this._createPreviewElement(videoUrl, file.name, index, true);
|
||||
resolve(previewElement);
|
||||
|
||||
// Revoke object URL after a delay to ensure it's loaded
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(videoUrl);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
URL.revokeObjectURL(videoUrl);
|
||||
reject(new Error('Failed to load video'));
|
||||
};
|
||||
|
||||
// Set a timeout in case the video never loads
|
||||
setTimeout(() => {
|
||||
if (!video.videoWidth) {
|
||||
URL.revokeObjectURL(videoUrl);
|
||||
reject(new Error('Video load timeout'));
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// Create HTML element for preview
|
||||
_createPreviewElement(src, fileName, index, isVideo) {
|
||||
const previewDiv = document.createElement('div');
|
||||
previewDiv.className = 'image-preview-item';
|
||||
|
||||
if (isVideo) {
|
||||
const video = document.createElement('video');
|
||||
video.src = src;
|
||||
video.controls = true;
|
||||
video.preload = 'metadata';
|
||||
video.style.maxWidth = '150px';
|
||||
video.style.maxHeight = '150px';
|
||||
video.style.objectFit = 'cover';
|
||||
previewDiv.appendChild(video);
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
img.alt = fileName;
|
||||
img.style.maxWidth = '150px';
|
||||
img.style.maxHeight = '150px';
|
||||
img.style.objectFit = 'cover';
|
||||
previewDiv.appendChild(img);
|
||||
}
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'image-controls';
|
||||
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.textContent = 'Entfernen';
|
||||
removeButton.style.background = '#dc3545';
|
||||
removeButton.style.color = 'white';
|
||||
removeButton.style.border = 'none';
|
||||
removeButton.style.padding = '5px 10px';
|
||||
removeButton.style.borderRadius = '3px';
|
||||
removeButton.style.cursor = 'pointer';
|
||||
removeButton.style.marginLeft = '10px';
|
||||
removeButton.dataset.index = index;
|
||||
removeButton.addEventListener('click', function() {
|
||||
const indexToRemove = parseInt(this.dataset.index, 10);
|
||||
// We'll define removeImagePreview elsewhere
|
||||
if (typeof window.removeImagePreview === 'function') {
|
||||
window.removeImagePreview(indexToRemove);
|
||||
}
|
||||
});
|
||||
|
||||
controlsDiv.appendChild(removeButton);
|
||||
previewDiv.appendChild(controlsDiv);
|
||||
|
||||
return previewDiv;
|
||||
}
|
||||
|
||||
// Compress image for mobile devices
|
||||
_compressImage(dataUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
// Scale down if needed
|
||||
if (width > this.maxWidth || height > this.maxHeight) {
|
||||
const ratio = Math.min(this.maxWidth / width, this.maxHeight / height);
|
||||
width *= ratio;
|
||||
height *= ratio;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Get compressed data URL
|
||||
const compressedDataUrl = canvas.toDataURL('image/jpeg', this.quality);
|
||||
|
||||
// Clean up
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
|
||||
resolve(compressedDataUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => reject(new Error('Failed to load image for compression'));
|
||||
img.src = dataUrl;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile-optimized debounce function for event handlers
|
||||
function debounce(func, wait = 300) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Mobile-safe promise-based setTimeout
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Log mobile-specific issues to console or server
|
||||
function logMobileIssue(action, error) {
|
||||
if (isMobileDevice()) {
|
||||
const diagnosticInfo = {
|
||||
action: action,
|
||||
error: error.message,
|
||||
browser: navigator.userAgent,
|
||||
timestamp: new Date().toISOString(),
|
||||
memoryInfo: performance?.memory ? {
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize
|
||||
} : 'Not available'
|
||||
};
|
||||
|
||||
console.error('Mobile Issue:', diagnosticInfo);
|
||||
|
||||
// Optionally send to server
|
||||
fetch('/log_mobile_issue', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(diagnosticInfo)
|
||||
}).catch(() => {
|
||||
// Silently fail if logging fails
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright 2025-2026 AIIrondev
|
||||
*
|
||||
* Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||||
* See Legal/LICENSE for the full license text.
|
||||
* Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
* For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
*/
|
||||
chatBox = document.getElementById('chatBox');
|
||||
|
||||
function fetchMessages() {
|
||||
fetch('/messages')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
chatBox.innerHTML = '';
|
||||
data.messages.forEach(msg => {
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.textContent = msg;
|
||||
chatBox.appendChild(messageElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(fetchMessages, 1000);
|
||||
Reference in New Issue
Block a user