This commit is contained in:
2026-04-13 16:32:30 +08:00
parent dad17dbadd
commit 1de35f21d7
14 changed files with 1081 additions and 63 deletions

View File

@@ -0,0 +1,87 @@
{% extends 'base.html' %}
{% block content %}
<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-arrows-move text-primary me-2"></i>{{ _('Printer Control') }}</h1>
</div>
{% if error %}
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
</div>
{% else %}
<div class="row row-cols-1 row-cols-lg-2 g-4">
<!-- Webcam Stream -->
<div class="col">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-light fw-bold rounded-top">
<i class="bi bi-camera-video me-1"></i>{{ _('Live Webcam') }}
</div>
<div class="card-body p-0 ratio ratio-16x9">
<img src="{{ webcam_url }}" alt="{{ _('Loading webcam stream...') }}" class="w-100 h-100 object-fit-cover">
</div>
</div>
</div>
<!-- Motion Control -->
<div class="col">
<div class="card shadow-sm h-100">
<div class="card-header bg-light fw-bold text-secondary">
<i class="bi bi-dpad me-1"></i>{{ _('Basic Control') }}
</div>
<div class="card-body text-center d-flex flex-column justify-content-center align-items-center">
<!-- Home button -->
<button class="btn btn-lg btn-primary rounded-circle mb-4 shadow" style="width: 80px; height: 80px;" onclick="sendCommand('home')" title="{{ _('Home All Axes') }}">
<i class="bi bi-house-door fs-2"></i>
</button>
<div class="text-muted mb-4">{{ _('Home All Axes') }} (G28)</div>
<!-- Quick macros -->
<div class="d-flex gap-3 justify-content-center flex-wrap w-100">
<button class="btn btn-outline-danger flex-fill shadow-sm py-3" onclick="sendCommand('pause')" title="{{ _('Pause/Resume Print') }}">
<i class="bi bi-pause-circle fs-4 d-block mb-1"></i>{{ _('Pause') }}
</button>
<button class="btn btn-outline-warning flex-fill shadow-sm py-3" onclick="sendCommand('cancel')" title="{{ _('Cancel Print') }}">
<i class="bi bi-stop-circle fs-4 d-block mb-1"></i>{{ _('Cancel') }}
</button>
</div>
</div>
</div>
</div>
</div>
<script>
function sendCommand(cmdName) {
if ((cmdName === 'cancel' || cmdName === 'home') && !confirm("Are you sure you want to perform this action?")) {
return;
}
fetch('{{ url_for("printer.api_command") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({command: cmdName})
})
.then(r => r.json())
.then(data => {
if(data.success) {
flashMessage("success", "Command " + cmdName + " sent.");
} else {
flashMessage("danger", "Control failed: " + data.error);
}
})
.catch(err => {
flashMessage("danger", "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 %}

View File

@@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% block content %}
<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-gear-wide-connected text-primary me-2"></i>{{ _('OctoPrint Configuration') }}</h1>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-light fw-bold text-secondary border-bottom-0">
<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') }}">
<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">
<span class="input-group-text bg-white text-muted" id="url-addon"><i class="bi bi-globe"></i></span>
<input type="url" class="form-control" id="octoprint_url" name="octoprint_url" aria-describedby="url-addon" placeholder="e.g. http://octopi.local" value="{{ configs.get('octoprint_url', '') }}" required>
</div>
<div class="form-text">
{{ _('The local IP address or hostname of your OctoPrint server.') }}
</div>
</div>
<div class="mb-4">
<label for="octoprint_apikey" class="form-label fw-bold">{{ _('API Key / Application Key') }}</label>
<div class="input-group shadow-sm">
<span class="input-group-text bg-white text-muted" id="key-addon"><i class="bi bi-key"></i></span>
<input type="password" class="form-control" id="octoprint_apikey" name="octoprint_apikey" aria-describedby="key-addon" placeholder="{{ _('Paste API Key here') }}" value="{{ configs.get('octoprint_apikey', '') }}">
</div>
<div class="form-text">
{{ _('Can be found in OctoPrint Settings -> Application Keys or API.') }}
</div>
</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>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends 'base.html' %}
{% block content %}
<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>
{% else %}
<div class="alert alert-warning shadow-sm border-0 d-flex align-items-center" role="alert">
<i class="bi bi-exclamation-triangle-fill fs-4 text-warning me-3"></i>
<div>
<strong>{{ _('Configuration Required:') }}</strong>
{{ _('The OctoPrint URL is not set. Please go to the ') }} <a href="{{ url_for('printer.octo_config') }}" class="alert-link text-decoration-underline">{{ _('System Configuration') }}</a> {{ _('page to set it up.') }}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,62 @@
{% extends 'base.html' %}
{% block content %}
<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>
{% if error %}
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
</div>
{% else %}
<div class="card shadow-sm">
<div class="card-header bg-light fw-bold text-secondary">
<i class="bi bi-card-text me-1"></i>{{ _('Available Files on Printer') }}
</div>
<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 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>
<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>
</div>
{% endif %}
{% else %}
<div class="list-group-item text-center py-5 text-muted">
<i class="bi bi-inbox display-4 d-block mb-3"></i>
<p>{{ _('No printable files found. Go slice some G-Code first!') }}</p>
</div>
{% endfor %}
</div>
</div>
<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));
}
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,105 @@
{% extends 'base.html' %}
{% block content %}
<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-activity text-primary me-2"></i>{{ _('Printer Status') }}</h1>
</div>
{% if error %}
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
{% if current_user.is_admin %}
<a href="{{ url_for('printer.octo_config') }}" class="alert-link">{{ _('Go to Configuration') }}</a>
{% endif %}
</div>
{% elif status %}
<div class="row row-cols-1 row-cols-md-2 g-4">
<!-- State Card -->
<div class="col">
<div class="card shadow-sm h-100">
<div class="card-header bg-light fw-bold text-secondary">
<i class="bi bi-info-circle me-1"></i>{{ _('Current State') }}
</div>
<div class="card-body text-center">
<h3 class="display-6 mt-3 text-primary">{{ status.get('state', {}).get('text', 'Unknown') }}</h3>
</div>
</div>
</div>
<!-- Temperature Card -->
<div class="col">
<div class="card shadow-sm h-100">
<div class="card-header bg-light fw-bold text-secondary">
<i class="bi bi-thermometer-half me-1"></i>{{ _('Temperatures') }}
</div>
<div class="card-body">
{% set temps = status.get('temperature', {}) %}
<h5 class="mb-1"><i class="bi bi-fire text-danger me-2"></i>{{ _('Tool/Nozzle') }}</h5>
<h4 class="ms-4 mb-4">
{{ temps.get('tool0', {}).get('actual', 0) }} °C
<small class="text-muted fs-6">/ {{ temps.get('tool0', {}).get('target', 0) }} °C</small>
</h4>
<h5 class="mb-1"><i class="bi bi-square-fill text-warning me-2"></i>{{ _('Bed') }}</h5>
<h4 class="ms-4">
{{ temps.get('bed', {}).get('actual', 0) }} °C
<small class="text-muted fs-6">/ {{ temps.get('bed', {}).get('target', 0) }} °C</small>
</h4>
</div>
</div>
</div>
</div>
{% if job and job.get('job', {}).get('file', {}).get('name') %}
<div class="card shadow-sm mt-4 border-success">
<div class="card-header bg-success text-white fw-bold">
<i class="bi bi-play-circle me-1"></i>{{ _('Active Print Job') }}
</div>
<div class="card-body">
<h5>{{ job.get('job', {}).get('file', {}).get('name') }}</h5>
{% set progress = job.get('progress', {}).get('completion', 0) %}
{% if progress == None %}{% set progress = 0 %}{% endif %}
<div class="progress mt-3 mb-2" style="height: 25px;">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: {{ progress }}%;" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100">
{{ "%.1f"|format(progress) }}%
</div>
</div>
<div class="d-flex justify-content-between text-muted small mt-2">
<span><strong>{{ _('Print Time:') }}</strong> {{ job.get('progress', {}).get('printTime', 0) }}s</span>
<span><strong>{{ _('Time Left:') }}</strong> {{ job.get('progress', {}).get('printTimeLeft', 0) }}s</span>
</div>
<div class="mt-4 gap-2 d-flex">
<button class="btn btn-warning" onclick="sendCmd('pause')"><i class="bi bi-pause-fill me-1"></i>{{ _('Pause/Resume') }}</button>
<button class="btn btn-danger" onclick="sendCmd('cancel')"><i class="bi bi-stop-fill me-1"></i>{{ _('Cancel') }}</button>
</div>
</div>
</div>
{% endif %}
{% endif %}
<script>
function sendCmd(cmd) {
if(cmd === 'cancel' && !confirm("{{ _('Are you sure you want to cancel the print?') }}")) return;
fetch('{{ url_for("printer.api_command") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({command: cmd})
})
.then(res => res.json())
.then(data => {
if(data.success) {
window.location.reload();
} else {
alert("Error: " + data.error);
}
});
}
setTimeout(() => { if (!window.pauseRefresh) window.location.reload(); }, 15000);
</script>
{% endblock %}