修复偏移问题,修复代理问题

This commit is contained in:
2026-04-15 00:22:12 +08:00
parent 570af7c225
commit f0f9d658eb
33 changed files with 1621 additions and 1763 deletions

View File

@@ -28,6 +28,12 @@
<div class="form-text">{{ _('Files smaller than this will not generate a simplified proxy.') }}</div>
</div>
<div class="mb-3">
<label for="gcode_upload_folder" class="form-label"><i class="bi bi-folder2-open me-2"></i>{{ _('Custom GCode Output Folder') }}</label>
<input type="text" class="form-control" name="gcode_upload_folder" id="gcode_upload_folder" value="{{ configs.get('gcode_upload_folder', '') }}">
<div class="form-text">{{ _('Absolute path to save locally sliced GCode files (e.g. OctoPrint uploads folder like "/home/pi/.octoprint/uploads"). Leave empty to use system default.') }}</div>
</div>
<h5 class="mt-4">{{ _('Default Plater Settings') }}</h5>
<hr>
@@ -70,6 +76,31 @@
</select>
</div>
<h5 class="card-title text-primary border-bottom pb-2 mt-4 mb-3"><i class="bi bi-hdd-network me-2"></i>{{ _('Default Storage Quotas (MB)') }}</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">{{ _('Guest STL Quota') }} <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<input type="number" class="form-control" name="default_guest_stl_quota_mb" value="{{ configs.get('default_guest_stl_quota_mb', '0') }}" min="0">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">{{ _('Guest GCode Quota') }} <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<input type="number" class="form-control" name="default_guest_gcode_quota_mb" value="{{ configs.get('default_guest_gcode_quota_mb', '0') }}" min="0">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">{{ _('New User STL Quota') }} <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<input type="number" class="form-control" name="default_user_stl_quota_mb" value="{{ configs.get('default_user_stl_quota_mb', '0') }}" min="0">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">{{ _('New User GCode Quota') }} <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<input type="number" class="form-control" name="default_user_gcode_quota_mb" value="{{ configs.get('default_user_gcode_quota_mb', '0') }}" min="0">
</div>
</div>
<button type="submit" class="btn btn-primary" id="btn-save-settings">{{ _('Save Settings') }}</button>
</form>
</div>

View File

@@ -5,6 +5,9 @@
<h1 class="h2">{{ _('User Management') }}</h1>
</div>
<div class="mb-3 text-end">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addUserModal"><i class="bi bi-person-plus me-1"></i>{{ _('Add User') }}</button>
</div>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
@@ -12,6 +15,7 @@
<th>{{ _('ID') }}</th>
<th>{{ _('Username') }}</th>
<th>{{ _('Role') }}</th>
<th>{{ _('Quotas') }}</th>
<th>{{ _('Created At') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
@@ -30,8 +34,16 @@
<span class="badge bg-primary">{{ _('User') }}</span>
{% endif %}
</td>
<td>
<small class="text-muted d-block">STL: {{ user_quotas[user.id]['stl'] if user_quotas[user.id]['stl'] != '0' else _('Unlimited') }} MB</small>
<small class="text-muted d-block">GCode: {{ user_quotas[user.id]['gcode'] if user_quotas[user.id]['gcode'] != '0' else _('Unlimited') }} MB</small>
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editQuotaModal{{ user.id }}">{{ _('Edit Quota') }}</button>
{% if not user.is_guest %}
<button class="btn btn-sm btn-outline-info" data-bs-toggle="modal" data-bs-target="#resetPwdModal{{ user.id }}">{{ _('Reset Password') }}</button>
{% endif %}
<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>
@@ -41,4 +53,110 @@
</tbody>
</table>
</div>
{% for user in users %}
<!-- Edit Quota Modal -->
<div class="modal fade" id="editQuotaModal{{ user.id }}" tabindex="-1" aria-labelledby="editQuotaModalLabel{{ user.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('admin.update_quota', user_id=user.id) }}" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="editQuotaModalLabel{{ user.id }}">{{ _('Edit Quota for') }} {{ user.username }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">{{ _('STL Quota') }} (MB) <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<input type="number" class="form-control" name="stl_quota_mb" value="{{ user_quotas[user.id]['stl'] }}" min="0">
</div>
<div class="mb-3">
<label class="form-label">{{ _('GCode Quota') }} (MB) <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<input type="number" class="form-control" name="gcode_quota_mb" value="{{ user_quotas[user.id]['gcode'] }}" min="0">
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{ _('Save') }}</button>
</div>
</form>
</div>
</div>
</div>
{% if not user.is_guest %}
<!-- Reset Password Modal -->
<div class="modal fade" id="resetPwdModal{{ user.id }}" tabindex="-1" aria-labelledby="resetPwdModalLabel{{ user.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('admin.reset_password', user_id=user.id) }}" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="resetPwdModalLabel{{ user.id }}">{{ _('Reset Password for') }} {{ user.username }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">{{ _('New Password') }}</label>
<input type="password" class="form-control" name="password" required>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{ _('Save') }}</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
{% endfor %}
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('admin.add_user') }}" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="addUserModalLabel">{{ _('Add User') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">{{ _('Username') }}</label>
<input type="text" class="form-control" name="username" required>
</div>
<div class="mb-3">
<label class="form-label">{{ _('Password') }}</label>
<input type="password" class="form-control" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" name="is_admin" id="isAdminCheck">
<label class="form-check-label" for="isAdminCheck">{{ _('Is Admin') }}</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{ _('Create User') }}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Bootstrap Modal Z-Index Fix -->
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.modal').forEach(function(modal) {
document.body.appendChild(modal);
});
});
</script>
<!-- Bootstrap Modal Z-Index Fix -->
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.modal').forEach(function(modal) {
document.body.appendChild(modal);
});
});
</script>
{% endblock %}

View File

@@ -22,7 +22,17 @@
.card-header { border-bottom: 1px solid rgba(0,0,0,.05); background-color: transparent; }
.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; }
.toast {
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.25);
pointer-events: auto;
border: none;
transform: translateY(-20px);
transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
}
.toast.showing, .toast.show {
transform: translateY(0);
}
/* 页面切换动画 Page Transition */
@keyframes pageFadeInSlide {
@@ -117,6 +127,9 @@
{% endif %}
{% else %}
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mb-2 text-muted fw-bold text-uppercase" style="font-size: 0.75rem;">
<span><i class="bi bi-list-task me-1"></i>{{ _('General Operations') }}</span>
</h6>
<ul class="nav flex-column nav-pills gap-1">
<li class="nav-item">
<a class="nav-link text-dark {% if request.endpoint == 'main.index' %}active text-white shadow-sm{% endif %}" href="{{ url_for('main.index') }}">
@@ -158,31 +171,30 @@
</nav>
<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" 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 }} mb-2" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body fw-medium">
{{ message }}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% block content %}{% endblock %}
</main>
</div>
</div>
<!-- Toast Notification Container -->
<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 }} mb-2" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body fw-medium">
{{ _(message) if _ else message }}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</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">

View File

@@ -1,20 +1,38 @@
{% extends 'base.html' %}
{% block content %}
<style>
/* Prevent the outer page from scrolling */
body {
overflow: hidden;
}
/* Make the main container take exactly the viewport height and act as a flex column */
main {
height: 100vh;
padding-bottom: 0 !important;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Let the iframe container fill all the remaining height automatically */
.octo-panel-container {
flex-grow: 1;
margin-bottom: 0 !important;
border-radius: 0px !important;
}
</style>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="bi bi-window-sidebar text-info me-2"></i>{{ _('OctoPrint Panel (Embedded)') }}</h1>
</div>
{% if embed_url %}
<div class="card shadow rounded overflow-hidden" style="height: calc(100vh - 180px); min-height: 500px;">
<!-- iFrame wrapper for responsivness -->
<div class="w-100 h-100 position-relative">
<iframe src="{{ embed_url }}"
class="position-absolute border-0 w-100 h-100"
style="top: 0; left: 0;"
allowfullscreen>
</iframe>
</div>
<div class="card shadow overflow-hidden octo-panel-container position-relative">
<iframe src="{{ embed_url }}"
class="position-absolute border-0 w-100 h-100"
style="top: 0; left: 0;"
allowfullscreen>
</iframe>
</div>
{% else %}
<div class="alert alert-warning shadow-sm border-0 d-flex align-items-center" role="alert">

View File

@@ -1,8 +1,29 @@
{% extends 'base.html' %}
{% block content %}
<style>
@keyframes blink-fade {
0% { background-color: rgba(255, 193, 7, 0.4); box-shadow: 0 0 15px rgba(255, 193, 7, 0.6); }
50% { background-color: rgba(255, 193, 7, 0); box-shadow: 0 0 0 rgba(255, 193, 7, 0); }
100% { background-color: rgba(255, 193, 7, 0.4); box-shadow: 0 0 15px rgba(255, 193, 7, 0.6); }
}
.animate-blink {
animation: blink-fade 0.8s ease-in-out 4;
border: 2px solid #ffc107 !important;
border-radius: 8px;
}
.transition-style {
transition: all 0.3s ease;
}
</style>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="bi bi-file-earmark-plus text-primary me-2"></i>{{ _('Prepare Print') }}</h1>
<div>
<button type="button" class="btn btn-outline-primary btn-sm rounded shadow-sm fw-bold" onclick="document.getElementById('gcodeUploadInput').click();">
<i class="bi bi-upload me-1"></i>{{ _('Upload External GCode') }}
</button>
<input type="file" id="gcodeUploadInput" style="display: none;" accept=".gcode,.gco,.g" onchange="uploadExternalGcode(this)">
</div>
</div>
{% if error %}
@@ -17,12 +38,13 @@
<div class="list-group list-group-flush">
{% for f in files %}
{% if f.type == 'machinecode' %}
<div class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3">
<div id="file-{{ f.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3 transition-style">
<div class="me-auto text-truncate" style="max-width: 80%;">
<h6 class="mb-1"><i class="bi bi-file-earmark-code text-primary me-2"></i>{{ f.name }}</h6>
<small class="text-muted d-block">{{ _('Size:') }} {{ f.size }} bytes, {{ _('Time:') }} {{ f.gcodeAnalysis.estimatedPrintTime if f.gcodeAnalysis else 'Unknown' }}s</small>
</div>
<div>
<a href="{{ url_for('main.preview_gcode', file_id=f.id) }}" class="btn btn-sm btn-outline-info rounded-pill px-3 shadow-sm me-2"><i class="bi bi-eye me-1"></i>{{ _('Preview') }}</a>
<button class="btn btn-sm btn-outline-success rounded-pill px-3 shadow-sm" onclick="printFile('{{ f.origin }}', '{{ f.path }}')"><i class="bi bi-play-fill me-1"></i>{{ _('Print Now') }}</button>
<!-- <button class="btn btn-sm btn-outline-secondary rounded-pill ms-2" onclick="selectFile('{{ f.origin }}', '{{ f.path }}')">{{ _('Select') }}</button> -->
</div>
@@ -59,6 +81,54 @@ function printFile(origin, path) {
.catch(err => window.customAlert("Error: " + err));
});
}
function uploadExternalGcode(input) {
if (!input.files || input.files.length === 0) return;
let file = input.files[0];
let formData = new FormData();
formData.append('file', file);
window.showToast("{{ _('Uploading and linking GCode...') }}", "info");
let btn = document.querySelector('button[onclick="document.getElementById(\'gcodeUploadInput\').click();"]');
let oldBtnHtml = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Upload/Sync...';
btn.disabled = true;
fetch("{{ url_for('printer.upload_gcode') }}", {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if(data.success) {
window.location.reload();
} else {
window.customAlert("Upload failed: " + data.error);
btn.innerHTML = oldBtnHtml;
btn.disabled = false;
}
}).catch(e => {
window.customAlert("Error: " + e);
btn.innerHTML = oldBtnHtml;
btn.disabled = false;
});
}
window.addEventListener('DOMContentLoaded', (event) => {
if(window.location.hash) {
let el = document.querySelector(window.location.hash);
if(el) {
setTimeout(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('animate-blink');
// fallback to remove class later
setTimeout(() => el.classList.remove('animate-blink'), 3500);
}, 300);
}
}
});
</script>
{% endif %}
{% endblock %}

View File

@@ -39,12 +39,15 @@
</thead>
<tbody>
{% for file in files %}
<tr id="file-row-{{ file.id }}" data-status="{{ file.status }}">
{% set is_gcode = file.original_filename.lower().endswith('.gcode') or file.original_filename.lower().endswith('.gco') or file.original_filename.lower().endswith('.g') %}
<tr id="file-row-{{ file.id }}" data-status="{{ file.status }}" data-is-gcode="{{ 'true' if is_gcode else 'false' }}">
<td class="ps-4 text-muted">
<i class="bi bi-clock me-1"></i>
<span class="local-time" data-utc="{{ file.created_at.isoformat() }}">{{ file.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</td>
<td class="fw-medium">{{ file.original_filename }}</td>
<td class="fw-medium">
<i class="bi {{ 'bi-file-earmark-code text-success' if is_gcode else 'bi-box text-primary' }} me-2"></i>{{ file.original_filename }}
</td>
<td id="status-{{ file.id }}">
{% if file.status == 'waiting' %}
<span class="badge bg-info text-dark rounded-pill fw-normal px-2" title="{{ _('Waiting in queue for slicing') }}"><i class="bi bi-hourglass-split me-1"></i>{{ _('Waiting') }}...</span>
@@ -64,7 +67,9 @@
</td>
<td class="pe-4">
<div class="d-flex gap-2" id="actions-container-{{ file.id }}">
{% if not is_gcode %}
<a href="{{ url_for('main.plater') }}?add={{ file.id }}" class="btn btn-sm btn-outline-warning shadow-sm" title="{{ _('Slice') }}"><i class="bi bi-braces"></i> {{ _('Slice') }}</a>
{% endif %}
{% if file.status == 'sliced' %}
<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>
@@ -138,7 +143,10 @@ document.addEventListener('DOMContentLoaded', function() {
let actionsHtml = '';
const platerUrl = `{{ url_for('main.plater') }}?add=${id}`;
actionsHtml += `<a href="${platerUrl}" class="btn btn-sm btn-outline-warning shadow-sm" title="{{ _('Slice') }}"><i class="bi bi-braces"></i> {{ _('Slice') }}</a>\n`;
const isGcode = tr.getAttribute('data-is-gcode') === 'true';
if (!isGcode) {
actionsHtml += `<a href="${platerUrl}" class="btn btn-sm btn-outline-warning shadow-sm" title="{{ _('Slice') }}"><i class="bi bi-braces"></i> {{ _('Slice') }}</a>\n`;
}
if (status === 'sliced') {
const downloadUrl = `{{ url_for('main.download_gcode', file_id=999999999) }}`.replace('999999999', id);

View File

@@ -4,7 +4,8 @@
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="bi bi-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
<div>
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm"><i class="bi bi-download"></i> {{ _('Download GCode') }}</a>
<a href="{{ url_for('printer.prepare') }}#file-{{ file.id }}" class="btn btn-warning btn-sm rounded shadow-sm fw-bold"><i class="bi bi-printer"></i> {{ _('Go to Print') }}</a>
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm ms-2"><i class="bi bi-download"></i> {{ _('Download GCode') }}</a>
<a href="{{ url_for('main.files') }}" class="btn btn-outline-secondary btn-sm rounded ms-2 shadow-sm"><i class="bi bi-arrow-left"></i> {{ _('Back') }}</a>
</div>
</div>

View File

@@ -6,11 +6,50 @@
</div>
<div class="row">
<div class="col-md-4">
<!-- STL Files Stats -->
<div class="col-md-6">
<div class="card text-white bg-primary mb-3 shadow-sm border-0">
<div class="card-header border-0 fs-5 fw-medium"><i class="bi bi-bar-chart-fill me-2"></i>{{ _('Total Prints') }}</div>
<div class="card-header border-0 fs-5 fw-medium">
<i class="bi bi-box me-2"></i>{{ _('3D Model Files (STL)') }}
</div>
<div class="card-body mt-2">
<h5 class="card-title">{{ _('You have sliced') }} <b class="fs-1 mx-2">{{ current_user.print_files|length }}</b> {{ _('files') }}</h5>
<h5 class="card-title mb-3">
{{ _('You have uploaded') }} <b class="fs-1 mx-2">{{ stl_count }}</b> {{ _('files') }}
</h5>
<p class="card-text mb-2">
<i class="bi bi-hdd-fill me-1"></i>{{ _('Total Space Used') }}: <strong>{{ format_size(stl_used_bytes) }}</strong>
{% if stl_quota_mb > 0 %} / <strong>{{ stl_quota_mb }} MB</strong> <small class="opacity-75">({{ _('Quota') }})</small>{% else %} <small class="opacity-75">({{ _('Unlimited') }})</small>{% endif %}
</p>
{% if stl_quota_mb > 0 %}
{% set stl_percent = (stl_used_bytes / (stl_quota_mb * 1024 * 1024) * 100)|round(1) %}
<div class="progress bg-white bg-opacity-25" style="height: 8px;">
<div class="progress-bar {% if stl_percent > 90 %}bg-danger{% elif stl_percent > 75 %}bg-warning{% else %}bg-info{% endif %}" role="progressbar" style="width: {{ stl_percent if stl_percent <= 100 else 100 }}%"></div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- GCode Files Stats -->
<div class="col-md-6">
<div class="card text-white bg-success mb-3 shadow-sm border-0">
<div class="card-header border-0 fs-5 fw-medium">
<i class="bi bi-file-earmark-code me-2"></i>{{ _('Sliced Files (GCode)') }}
</div>
<div class="card-body mt-2">
<h5 class="card-title mb-3">
{{ _('You have sliced or uploaded') }} <b class="fs-1 mx-2">{{ gcode_count }}</b> {{ _('files') }}
</h5>
<p class="card-text mb-2">
<i class="bi bi-hdd-network-fill me-1"></i>{{ _('Total Space Used') }}: <strong>{{ format_size(gcode_used_bytes) }}</strong>
{% if gcode_quota_mb > 0 %} / <strong>{{ gcode_quota_mb }} MB</strong> <small class="opacity-75">({{ _('Quota') }})</small>{% else %} <small class="opacity-75">({{ _('Unlimited') }})</small>{% endif %}
</p>
{% if gcode_quota_mb > 0 %}
{% set gc_percent = (gcode_used_bytes / (gcode_quota_mb * 1024 * 1024) * 100)|round(1) %}
<div class="progress bg-white bg-opacity-25" style="height: 8px;">
<div class="progress-bar {% if gc_percent > 90 %}bg-danger{% elif gc_percent > 75 %}bg-warning{% else %}bg-info{% endif %}" role="progressbar" style="width: {{ gc_percent if gc_percent <= 100 else 100 }}%"></div>
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -148,6 +148,11 @@
<script>
// Toggle icons on collapse
document.addEventListener('DOMContentLoaded', function() {
{% if quota_exceeded %}
window.customConfirm("{{ _('GCode Storage Quota Exceeded. Please delete some files first.') }}",()=>{window.location.href = "{{ url_for('main.files') }}"});
return;
{% endif %}
const cards = document.querySelectorAll('.collapse');
cards.forEach(card => {
card.addEventListener('show.bs.collapse', function () {
@@ -896,7 +901,11 @@ function mergeAndSlice() {
if(data.success) {
window.location.href = "{{ url_for('main.files') }}";
} else {
window.customAlert("{{ _('Error:') }} " + data.error);
let errorMsg = data.error;
if (errorMsg === 'GCode Storage Quota Exceeded. Please delete some files first.') {
errorMsg = "{{ _('GCode Storage Quota Exceeded. Please delete some files first.') }}";
}
window.customAlert("{{ _('Error:') }} " + errorMsg);
btn.disabled = false;
icon.className = 'bi bi-gear-fill me-2';
text.innerText = '{{ _("Merge & Slice") }}';