整理文件夹及架构,加入打印机页面,octo反代有问题
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
<div class="card-body">
|
||||
<h5>{{ _('CuraEngine Configurations') }}</h5>
|
||||
<hr>
|
||||
<form method="POST" action="{{ url_for('admin.settings') }}">
|
||||
<form id="settingsForm" onsubmit="submitSettings(event)">
|
||||
<div class="mb-3">
|
||||
<label for="offset_x" class="form-label">{{ _('Plater Origin Offset X (mm)') }}</label>
|
||||
<input type="number" class="form-control" name="offset_x" id="offset_x" value="{{ configs.get('offset_x', '0') }}">
|
||||
@@ -70,8 +70,44 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">{{ _('Save Settings') }}</button>
|
||||
<button type="submit" class="btn btn-primary" id="btn-save-settings">{{ _('Save Settings') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function submitSettings(event) {
|
||||
event.preventDefault();
|
||||
const form = document.getElementById('settingsForm');
|
||||
const formData = new FormData(form);
|
||||
const btn = document.getElementById('btn-save-settings');
|
||||
const originalText = btn.innerHTML;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Saving...';
|
||||
|
||||
fetch("{{ url_for('admin.settings') }}", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.showToast("{{ _('Settings updated successfully') }}", "success");
|
||||
} else {
|
||||
window.showToast("{{ _('Error updating settings') }}", "danger");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
window.showToast("{{ _('Network error') }}", "danger");
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -32,7 +32,7 @@
|
||||
</td>
|
||||
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="POST" class="d-inline" onsubmit="return confirm('{{ _('WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?') }}');">
|
||||
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="POST" class="d-inline" onsubmit="event.preventDefault(); window.customConfirm('{{ _('WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?') }}', () => { this.submit(); });">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" {% if user.id == current_user.id %}disabled{% endif %}>{{ _('Delete') }}</button>
|
||||
</form>
|
||||
</td>
|
||||
@@ -21,8 +21,18 @@
|
||||
.card { border: none; border-radius: 0.75rem; overflow: hidden; }
|
||||
.card-header { border-bottom: 1px solid rgba(0,0,0,.05); background-color: transparent; }
|
||||
|
||||
.toast-container { margin-bottom: 20px; margin-right: 20px; }
|
||||
.toast { border-radius: 0.5rem; box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15); opacity: 0.95; }
|
||||
.toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1055; width: auto; max-width: 90%; pointer-events: none; }
|
||||
.toast { border-radius: 0.5rem; box-shadow: 0 0.5rem 1rem rgba(0,0,0,.25); opacity: 1 !important; pointer-events: auto; }
|
||||
|
||||
/* 页面切换动画 Page Transition */
|
||||
@keyframes pageFadeInSlide {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
main { animation: pageFadeInSlide 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||
|
||||
/* 提升 Accordion 折叠栏动画更平滑 */
|
||||
.collapsing { transition: height 0.35s cubic-bezier(0.25, 0.8, 0.25, 1) !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -150,12 +160,12 @@
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 mt-4 bg-light min-vh-100 pb-5">
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1055;">
|
||||
<div class="toast-container" id="global-toast-container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
{% set toast_class = 'bg-success text-white' if category == 'success' else 'bg-danger text-white' if category == 'danger' else 'bg-warning text-dark' if category == 'warning' else 'bg-primary text-white' %}
|
||||
<div class="toast align-items-center border-0 {{ toast_class }}" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast align-items-center border-0 {{ toast_class }} mb-2" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body fw-medium">
|
||||
{{ message }}
|
||||
@@ -173,6 +183,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Custom Alert Modal -->
|
||||
<div class="modal fade" id="globalAlertModal" tabindex="-1" aria-hidden="true" style="z-index: 1060;">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-warning text-dark py-2">
|
||||
<h6 class="modal-title fw-bold" id="globalAlertTitle"><i class="bi bi-exclamation-triangle-fill me-2"></i>{{ _('Notice') }}</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body fs-6 text-center py-4 text-break" id="globalAlertMessage">
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-2 justify-content-center bg-light">
|
||||
<button type="button" class="btn btn-warning px-4 rounded-pill fw-bold" data-bs-dismiss="modal">{{ _('OK') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Custom Confirm Modal -->
|
||||
<div class="modal fade" id="globalConfirmModal" tabindex="-1" aria-hidden="true" style="z-index: 1060;">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white py-2">
|
||||
<h6 class="modal-title fw-bold"><i class="bi bi-question-circle-fill me-2"></i>{{ _('Confirm') }}</h6>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body fs-6 text-center py-4 text-break" id="globalConfirmMessage">
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-2 justify-content-center bg-light">
|
||||
<button type="button" class="btn btn-outline-secondary px-4 rounded-pill" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
|
||||
<button type="button" class="btn btn-primary px-4 rounded-pill fw-bold" id="globalConfirmBtn">{{ _('Yes') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||
<script>
|
||||
// Initialize Toasts automatically
|
||||
@@ -182,6 +227,53 @@
|
||||
return new bootstrap.Toast(toastEl, { delay: 3000 }).show()
|
||||
});
|
||||
});
|
||||
|
||||
// Global Utility: Show Toast dynamically
|
||||
window.showToast = function(msg, type='success', duration=3000) {
|
||||
const container = document.getElementById('global-toast-container');
|
||||
const toastClass = type === 'success' ? 'bg-success text-white' :
|
||||
type === 'danger' ? 'bg-danger text-white' :
|
||||
type === 'warning' ? 'bg-warning text-dark' : 'bg-primary text-white';
|
||||
|
||||
const html = `
|
||||
<div class="toast align-items-center border-0 ${toastClass} mb-2" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body fw-medium">${msg}</div>
|
||||
<button type="button" class="btn-close ${type==='warning'?'':'btn-close-white'} me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
const ts = container.lastElementChild;
|
||||
new bootstrap.Toast(ts, { autohide: true, delay: duration }).show();
|
||||
ts.addEventListener('hidden.bs.toast', () => { ts.remove(); });
|
||||
};
|
||||
|
||||
// Override default alert
|
||||
window.customAlert = function(msg, title) {
|
||||
document.getElementById('globalAlertMessage').innerHTML = String(msg).replace(/\n/g, '<br>');
|
||||
if(title) document.getElementById('globalAlertTitle').innerHTML = '<i class="bi bi-info-circle-fill me-2"></i>' + title;
|
||||
else document.getElementById('globalAlertTitle').innerHTML = '<i class="bi bi-exclamation-triangle-fill me-2"></i>Notice';
|
||||
new bootstrap.Modal(document.getElementById('globalAlertModal')).show();
|
||||
};
|
||||
|
||||
// Override default confirm
|
||||
window.customConfirm = function(msg, onConfirm) {
|
||||
document.getElementById('globalConfirmMessage').innerHTML = String(msg).replace(/\n/g, '<br>');
|
||||
const modalEl = document.getElementById('globalConfirmModal');
|
||||
const modal = new bootstrap.Modal(modalEl);
|
||||
|
||||
// Clear previous event listener bindings
|
||||
const elClone = document.getElementById('globalConfirmBtn').cloneNode(true);
|
||||
document.getElementById('globalConfirmBtn').parentNode.replaceChild(elClone, document.getElementById('globalConfirmBtn'));
|
||||
|
||||
document.getElementById('globalConfirmBtn').addEventListener('click', function() {
|
||||
modal.hide();
|
||||
if(onConfirm) onConfirm();
|
||||
});
|
||||
|
||||
modal.show();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -52,10 +52,13 @@
|
||||
|
||||
<script>
|
||||
function sendCommand(cmdName) {
|
||||
if ((cmdName === 'cancel' || cmdName === 'home') && !confirm("Are you sure you want to perform this action?")) {
|
||||
return;
|
||||
if (cmdName === 'cancel' || cmdName === 'home') {
|
||||
window.customConfirm("{{ _('Are you sure you want to perform this action?') }}", () => doSendCommand(cmdName));
|
||||
} else {
|
||||
doSendCommand(cmdName);
|
||||
}
|
||||
|
||||
}
|
||||
function doSendCommand(cmdName) {
|
||||
fetch('{{ url_for("printer.api_command") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -64,24 +67,15 @@ function sendCommand(cmdName) {
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
flashMessage("success", "Command " + cmdName + " sent.");
|
||||
window.showToast("{{ _('Command') }} " + cmdName + " {{ _('sent.') }}", "success");
|
||||
} else {
|
||||
flashMessage("danger", "Control failed: " + data.error);
|
||||
window.customAlert("{{ _('Control failed: ') }}" + data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
flashMessage("danger", "Network Error: " + err);
|
||||
window.customAlert("{{ _('Network Error: ') }}" + err);
|
||||
});
|
||||
}
|
||||
function flashMessage(type, text) {
|
||||
const container = document.querySelector('.toast-container');
|
||||
if(!container) return alert(text);
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center border-0 bg-${type} text-white show`;
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body fw-medium">${text}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 4000);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -10,7 +10,7 @@
|
||||
<i class="bi bi-link-45deg me-1"></i>{{ _('Connection Settings') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('printer.octo_config') }}">
|
||||
<form id="octoConfigForm" onsubmit="submitConfig(event)">
|
||||
<div class="mb-3">
|
||||
<label for="octoprint_url" class="form-label fw-bold">{{ _('OctoPrint Base URL') }}</label>
|
||||
<div class="input-group mb-3 shadow-sm">
|
||||
@@ -34,9 +34,45 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary px-4 rounded-pill shadow-sm"><i class="bi bi-save2 me-2"></i>{{ _('Save Connection Settings') }}</button>
|
||||
<button type="submit" class="btn btn-primary px-4 rounded-pill shadow-sm" id="btn-save-octo"><i class="bi bi-save2 me-2"></i>{{ _('Save Connection Settings') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function submitConfig(event) {
|
||||
event.preventDefault();
|
||||
const form = document.getElementById('octoConfigForm');
|
||||
const formData = new FormData(form);
|
||||
const btn = document.getElementById('btn-save-octo');
|
||||
const originalText = btn.innerHTML;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Saving...';
|
||||
|
||||
fetch("{{ url_for('printer.octo_config') }}", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.showToast("{{ _('OctoPrint settings updated') }}", "success");
|
||||
} else {
|
||||
window.showToast("{{ _('Error saving settings') }}", "danger");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
window.showToast("{{ _('Network error') }}", "danger");
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -39,23 +39,25 @@
|
||||
|
||||
<script>
|
||||
function printFile(origin, path) {
|
||||
if(!confirm("{{ _('Send this file to print immediately?') }}\n\n" + path)) return;
|
||||
|
||||
fetch('{{ url_for("printer.api_print_file") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ origin: origin, path: path })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
alert("{{ _('Print starting! Going to dashboard...') }}");
|
||||
window.location.href = "{{ url_for('printer.status') }}";
|
||||
} else {
|
||||
alert("Error: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => alert("Error: " + err));
|
||||
window.customConfirm("{{ _('Send this file to print immediately?') }}<br><small>" + path + "</small>", () => {
|
||||
fetch('{{ url_for("printer.api_print_file") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ origin: origin, path: path })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
window.showToast("{{ _('Print starting! Going to dashboard...') }}", "success");
|
||||
setTimeout(() => {
|
||||
window.location.href = "{{ url_for('printer.status') }}";
|
||||
}, 1500);
|
||||
} else {
|
||||
window.customAlert("Error: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => window.customAlert("Error: " + err));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
@@ -84,8 +84,13 @@
|
||||
|
||||
<script>
|
||||
function sendCmd(cmd) {
|
||||
if(cmd === 'cancel' && !confirm("{{ _('Are you sure you want to cancel the print?') }}")) return;
|
||||
|
||||
if(cmd === 'cancel') {
|
||||
window.customConfirm("{{ _('Are you sure you want to cancel the print?') }}", () => doSendCmd(cmd));
|
||||
} else {
|
||||
doSendCmd(cmd);
|
||||
}
|
||||
}
|
||||
function doSendCmd(cmd) {
|
||||
fetch('{{ url_for("printer.api_command") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -96,7 +101,7 @@ function sendCmd(cmd) {
|
||||
if(data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("Error: " + data.error);
|
||||
window.customAlert("Error: " + data.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<div class="card-body">
|
||||
<div id="drop-zone" class="border border-2 border-primary rounded p-4 text-center bg-white" style="border-style: dashed !important; cursor: pointer; transition: all 0.3s ease;">
|
||||
<i class="bi bi-cloud-arrow-up display-4 text-primary mb-2"></i>
|
||||
<h5 class="text-secondary fw-normal">{{ _('Drag & Drop STL file here or Click to Select') }}</h5>
|
||||
<input type="file" id="file" name="file" accept=".stl" class="d-none">
|
||||
<h5 class="text-secondary fw-normal">{{ _('Drag & Drop STL files here or Click to Select') }}</h5>
|
||||
<input type="file" id="file" name="file" accept=".stl" class="d-none" multiple>
|
||||
</div>
|
||||
|
||||
<div id="upload-progress-container" class="mt-3 d-none">
|
||||
@@ -69,7 +69,7 @@
|
||||
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-sm btn-outline-primary shadow-sm" title="{{ _('Download GCode') }}"><i class="bi bi-download"></i></a>
|
||||
<a href="{{ url_for('main.preview_gcode', file_id=file.id) }}" class="btn btn-sm btn-outline-info shadow-sm" title="{{ _('GCode Preview') }}"><i class="bi bi-eye"></i></a>
|
||||
{% endif %}
|
||||
<form action="{{ url_for('main.delete_file', file_id=file.id) }}" method="POST" onsubmit="return confirm('{{ _('Are you sure you want to delete this file?') }}');">
|
||||
<form action="{{ url_for('main.delete_file', file_id=file.id) }}" method="POST" onsubmit="event.preventDefault(); window.customConfirm('{{ _('Are you sure you want to delete this file?') }}', () => { this.submit(); });">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger shadow-sm" title="{{ _('Delete') }}"><i class="bi bi-trash3"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -147,7 +147,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
actionsHtml += `<a href="${previewUrl}" class="btn btn-sm btn-outline-info shadow-sm" title="{{ _('GCode Preview') }}"><i class="bi bi-eye"></i></a>\n`;
|
||||
}
|
||||
const deleteUrl = `{{ url_for('main.delete_file', file_id=999999999) }}`.replace('999999999', id);
|
||||
actionsHtml += `<form action="${deleteUrl}" method="POST" onsubmit="return confirm('{{ _('Are you sure you want to delete this file?') }}');">
|
||||
actionsHtml += `<form action="${deleteUrl}" method="POST" onsubmit="event.preventDefault(); window.customConfirm('{{ _('Are you sure you want to delete this file?') }}', () => { this.submit(); });">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger shadow-sm" title="{{ _('Delete') }}"><i class="bi bi-trash3"></i></button>
|
||||
</form>`;
|
||||
actionsTd.innerHTML = actionsHtml;
|
||||
@@ -199,25 +199,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
dropZone.addEventListener('drop', e => {
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length) {
|
||||
handleFileUpload(files[0]);
|
||||
handleFileUpload(files);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (fileInput.files.length) {
|
||||
handleFileUpload(fileInput.files[0]);
|
||||
handleFileUpload(fileInput.files);
|
||||
}
|
||||
});
|
||||
|
||||
function handleFileUpload(file) {
|
||||
if (!file.name.toLowerCase().endsWith('.stl')) {
|
||||
alert('{{ _("Please upload a valid .stl file!") }}');
|
||||
function handleFileUpload(files) {
|
||||
const formData = new FormData();
|
||||
let hasValidFile = false;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i].name.toLowerCase().endsWith('.stl')) {
|
||||
formData.append('file', files[i]);
|
||||
hasValidFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidFile) {
|
||||
window.customAlert('{{ _("Please upload valid .stl files!") }}');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
progressContainer.classList.remove('d-none');
|
||||
dropZone.classList.add('d-none');
|
||||
progressBar.style.width = '0%';
|
||||
@@ -244,7 +251,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
try {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
if (response.error) {
|
||||
alert('{{ _("Validation Failed") }}:\n' + response.error);
|
||||
window.customAlert('{{ _("Validation Failed") }}:\n' + response.error);
|
||||
progressContainer.classList.add('d-none');
|
||||
dropZone.classList.remove('d-none');
|
||||
return;
|
||||
@@ -252,14 +259,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
} catch(e) {
|
||||
console.log('No JSON error response');
|
||||
}
|
||||
alert('{{ _("Upload failed.") }}');
|
||||
window.customAlert('{{ _("Upload failed.") }}');
|
||||
progressContainer.classList.add('d-none');
|
||||
dropZone.classList.remove('d-none');
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
alert('{{ _("Upload error.") }}');
|
||||
window.customAlert('{{ _("Upload error.") }}');
|
||||
progressContainer.classList.add('d-none');
|
||||
dropZone.classList.remove('d-none');
|
||||
};
|
||||
@@ -52,13 +52,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 h-100 d-flex flex-column pb-3" style="overflow-y: auto; overflow-x: hidden;">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer; z-index: 10;" data-bs-toggle="collapse" data-bs-target="#collapseModels" aria-expanded="true">
|
||||
<span><i class="bi bi-layers-fill me-2"></i>{{ _('Available Models') }}</span>
|
||||
<i class="bi bi-chevron-bar-contract"></i>
|
||||
</div>
|
||||
<div id="collapseModels" class="collapse show">
|
||||
<div class="col-md-3 h-100 d-flex flex-column pb-3">
|
||||
<!-- Accordion wrapper for options -->
|
||||
<div class="accordion flex-grow-1" id="platerSidebarAccordion" style="overflow-y: auto; overflow-x: hidden; padding-right: 5px;">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer; z-index: 10;" data-bs-toggle="collapse" data-bs-target="#collapseModels" aria-expanded="true">
|
||||
<span><i class="bi bi-layers-fill me-2"></i>{{ _('Available Models') }}</span>
|
||||
<i class="bi bi-chevron-bar-contract"></i>
|
||||
</div>
|
||||
<div id="collapseModels" class="collapse show" data-bs-parent="#platerSidebarAccordion">
|
||||
<div class="list-group list-group-flush" id="model-list" style="min-height: 160px; max-height: max(250px, 35vh); overflow-y: auto;">
|
||||
{% for model in models %}
|
||||
<button id="add-model-btn-{{ model.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-matrix="{{ model.transform_matrix or '' }}" onclick="addModelToPlate(this, {{ model.id }}, '{{ model.url }}', '{{ model.name }}', '{{ model.status }}')">
|
||||
@@ -73,11 +75,11 @@
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3 flex-shrink-0">
|
||||
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseSettings" aria-expanded="true">
|
||||
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center collapsed" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseSettings" aria-expanded="false">
|
||||
<span><i class="bi bi-sliders me-2"></i>{{ _('Other Settings') }}</span>
|
||||
<i class="bi bi-chevron-bar-contract"></i>
|
||||
</div>
|
||||
<div id="collapseSettings" class="collapse show">
|
||||
<div id="collapseSettings" class="collapse" data-bs-parent="#platerSidebarAccordion">
|
||||
<div class="card-body py-2">
|
||||
<div class="mb-2">
|
||||
<label for="infill-density" class="form-label text-secondary small mb-1">{{ _('Infill Density') }} (%)</label>
|
||||
@@ -110,12 +112,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm flex-shrink-0">
|
||||
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseQuality" aria-expanded="true">
|
||||
<div class="card shadow-sm flex-shrink-0 mb-3">
|
||||
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center collapsed" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseQuality" aria-expanded="false">
|
||||
<span><i class="bi bi-gear-wide-connected me-2"></i>{{ _('Quality Profile') }}</span>
|
||||
<i class="bi bi-chevron-bar-contract"></i>
|
||||
</div>
|
||||
<div id="collapseQuality" class="collapse show">
|
||||
<div id="collapseQuality" class="collapse" data-bs-parent="#platerSidebarAccordion">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<select class="form-select bg-light" id="quality">
|
||||
@@ -124,16 +126,16 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="clearPlate()"><i class="bi bi-trash me-1"></i>{{ _('Clear Board') }}</button>
|
||||
<button class="btn btn-primary" onclick="mergeAndSlice()" id="btn-merge"><i class="bi bi-gear-fill me-2" id="merge-icon"></i><span id="merge-text">{{ _('Merge & Slice') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- End of accordion wrapper -->
|
||||
|
||||
<div class="mt-auto pt-3 border-top d-flex flex-column gap-2 mb-1">
|
||||
<button class="btn btn-outline-danger w-100" onclick="clearPlate()"><i class="bi bi-trash me-2"></i>{{ _('Clear Board') }}</button>
|
||||
<button class="btn btn-primary w-100 py-2 fs-5 shadow-sm" onclick="mergeAndSlice()" id="btn-merge"><i class="bi bi-gear-fill me-2" id="merge-icon"></i><span id="merge-text">{{ _('Merge & Slice') }}</span></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -764,7 +766,7 @@ function loadSTL(fileId, url, name, status, matrixData, callback) {
|
||||
}, undefined, function (error) {
|
||||
console.error(error);
|
||||
if (callback) callback();
|
||||
alert("{{ _('Error loading STL model file.') }}");
|
||||
window.customAlert("{{ _('Error loading STL model file.') }}");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -819,12 +821,12 @@ function mergeAndSlice() {
|
||||
selectModels([]); // Detach any active model to bake transformProxy world coordinates into its local matrix properties
|
||||
|
||||
if (loadedModels.length === 0) {
|
||||
alert("{{ _('Please add at least one model to the build plate.') }}");
|
||||
window.customAlert("{{ _('Please add at least one model to the build plate.') }}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkBounds()) {
|
||||
alert("{{ _('One or more models are outside the print area. Please adjust them before slicing.') }}");
|
||||
window.customAlert("{{ _('One or more models are outside the print area. Please adjust them before slicing.') }}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -842,17 +844,18 @@ function mergeAndSlice() {
|
||||
if (isEdit) {
|
||||
// Just checking if we want to warn
|
||||
if (loadedModels.length === 1 && loadedModels[0].userData.status === 'sliced') {
|
||||
if (!confirm("{{ _('This model has already been sliced. The existing GCode will be overwritten. Continue?') }}")) {
|
||||
return;
|
||||
}
|
||||
window.customConfirm("{{ _('This model has already been sliced. The existing GCode will be overwritten. Continue?') }}", doMergeAndSlice);
|
||||
return;
|
||||
} else if (window.isCompositeEdit) {
|
||||
if (!confirm("{{ _('You are editing a composite model. The existing composite will be updated and re-sliced. Continue?') }}")) {
|
||||
return;
|
||||
}
|
||||
window.customConfirm("{{ _('You are editing a composite model. The existing composite will be updated and re-sliced. Continue?') }}", doMergeAndSlice);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const pieces = loadedModels.map(m => {
|
||||
doMergeAndSlice();
|
||||
|
||||
function doMergeAndSlice() {
|
||||
const pieces = loadedModels.map(m => {
|
||||
m.updateMatrixWorld(true);
|
||||
const mat = m.matrixWorld.clone();
|
||||
if (m.userData.geomTrans) {
|
||||
@@ -893,18 +896,19 @@ function mergeAndSlice() {
|
||||
if(data.success) {
|
||||
window.location.href = "{{ url_for('main.files') }}";
|
||||
} else {
|
||||
alert("{{ _('Error:') }} " + data.error);
|
||||
window.customAlert("{{ _('Error:') }} " + data.error);
|
||||
btn.disabled = false;
|
||||
icon.className = 'bi bi-gear-fill me-2';
|
||||
text.innerText = '{{ _("Merge & Slice") }}';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert("{{ _('Error:') }} " + String(err));
|
||||
window.customAlert("{{ _('Error:') }} " + String(err));
|
||||
btn.disabled = false;
|
||||
icon.className = 'bi bi-gear-fill me-2';
|
||||
text.innerText = '{{ _("Merge & Slice") }}';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Reference in New Issue
Block a user