feat(print): integrate native raw printing engine into Stage 2 UI (#28) #31
@@ -346,7 +346,7 @@ pub async fn run_profcheck(
|
||||
pub async fn get_windows_printers() -> Result<Vec<String>, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
crate::print::windows::get_printers()
|
||||
crate::print::windows::get_printers().map(|list| list.into_iter().map(|p| p.name).collect())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
@@ -400,6 +400,48 @@ pub async fn print_target_cups(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_printers() -> Result<Vec<crate::print::Printer>, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
crate::print::windows::get_printers()
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
crate::print::unix::get_printers()
|
||||
}
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
{
|
||||
Err("Native printer enumeration is not supported on this platform.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn print_target_native(
|
||||
printer_name: String,
|
||||
tiff_path: String,
|
||||
ppd_uncorrected_passthrough: Option<bool>,
|
||||
) -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = ppd_uncorrected_passthrough;
|
||||
crate::print::windows::print_target(&printer_name, &tiff_path)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
crate::print::unix::print_target(
|
||||
&printer_name,
|
||||
&tiff_path,
|
||||
ppd_uncorrected_passthrough.unwrap_or(false),
|
||||
)
|
||||
}
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
{
|
||||
let _ = (printer_name, tiff_path, ppd_uncorrected_passthrough);
|
||||
Err("Native raw printing is not supported on this platform.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -28,6 +28,8 @@ pub fn run() {
|
||||
commands::print_target_windows,
|
||||
commands::get_cups_printers,
|
||||
commands::print_target_cups,
|
||||
commands::get_printers,
|
||||
commands::print_target_native,
|
||||
settings::load_settings,
|
||||
settings::save_settings,
|
||||
])
|
||||
|
||||
@@ -15,12 +15,14 @@ use windows::Win32::Graphics::Printing::{
|
||||
PRINTER_ENUM_LOCAL, PRINTER_INFO_2W, PRINTER_INFO_4W,
|
||||
};
|
||||
|
||||
use crate::print::Printer;
|
||||
|
||||
fn to_wide(s: &str) -> Vec<u16> {
|
||||
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
/// Enumerate available printers on the system
|
||||
pub fn get_printers() -> Result<Vec<String>, String> {
|
||||
pub fn get_printers() -> Result<Vec<Printer>, String> {
|
||||
unsafe {
|
||||
let flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
|
||||
let mut bytes_needed = 0u32;
|
||||
@@ -74,7 +76,11 @@ pub fn get_printers() -> Result<Vec<String>, String> {
|
||||
let info = *p_info.add(i);
|
||||
if !info.pPrinterName.is_null() {
|
||||
let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?;
|
||||
printers.push(name);
|
||||
printers.push(Printer {
|
||||
name,
|
||||
status: "Ready".to_string(),
|
||||
is_default: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Ok(printers);
|
||||
@@ -101,7 +107,11 @@ pub fn get_printers() -> Result<Vec<String>, String> {
|
||||
let info = *p_info.add(i);
|
||||
if !info.pPrinterName.is_null() {
|
||||
let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?;
|
||||
printers.push(name);
|
||||
printers.push(Printer {
|
||||
name,
|
||||
status: "Ready".to_string(),
|
||||
is_default: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(printers)
|
||||
|
||||
@@ -190,6 +190,56 @@
|
||||
<div class="gallery-info" id="galleryInfo"></div>
|
||||
<div class="gallery-grid" id="galleryGrid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Native Raw Printing Engine (Bypass Colour Management) -->
|
||||
<div class="print-panel hidden" id="rawPrintPanel">
|
||||
<div class="print-panel-header">
|
||||
<div class="print-panel-title">
|
||||
<span class="print-icon">🖨️</span>
|
||||
<h3>Direct Native Raw Printing</h3>
|
||||
</div>
|
||||
<span class="acpu-badge" title="Bypasses OS ICM / ColorSync & CUPS color management, replicating Adobe Color Print Utility">ACPU-Equivalent Mode</span>
|
||||
</div>
|
||||
<p class="print-panel-desc">
|
||||
Send the generated profiling target directly to the printer nozzles with all OS and driver color management strictly bypassed.
|
||||
</p>
|
||||
|
||||
<!-- Notification / Feedback Banner -->
|
||||
<div id="printNotification" class="notification-banner hidden">
|
||||
<span class="notification-icon" id="printNotificationIcon">ℹ️</span>
|
||||
<div class="notification-text" id="printNotificationText"></div>
|
||||
</div>
|
||||
|
||||
<div class="printer-controls-row">
|
||||
<div class="form-group printer-select-group">
|
||||
<label for="printerSelect">Destination Printer</label>
|
||||
<div class="printer-select-wrapper">
|
||||
<select id="printerSelect">
|
||||
<option value="" disabled selected>Detecting printers...</option>
|
||||
</select>
|
||||
<button type="button" class="btn-icon" id="btnRefreshPrinters" title="Refresh printer list">↻</button>
|
||||
<span id="printerStatusBadge" class="status-badge badge-idle hidden">Idle</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group print-options-group" id="cupsOptionsGroup">
|
||||
<label class="checkbox-label" title="Enable for printers that do not accept raw TIFF streams (uses uncorrected CUPS passthrough)">
|
||||
<input type="checkbox" id="chkPpdFallback">
|
||||
<span>PPD Uncorrected Passthrough (Fallback)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="print-actions-row">
|
||||
<button class="primary btn-print" id="btnPrintAll">
|
||||
<span class="btn-text">🖨️ Print All Pages (Bypass CM)</span>
|
||||
<span class="btn-spinner hidden"></span>
|
||||
</button>
|
||||
<button class="secondary" id="btnAdvanceToStage3">
|
||||
<span>Proceed to Measurement (Stage 3) →</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stage 3: chartread -->
|
||||
|
||||
+261
-12
@@ -5,6 +5,8 @@ import { setStage2Result } from './chartread.js';
|
||||
// Module-level state: set by Stage 1 when it completes
|
||||
let stage1Basename = "";
|
||||
let stage1Cwd = "";
|
||||
let currentManifest = null;
|
||||
let discoveredPrinters = [];
|
||||
|
||||
/**
|
||||
* Called by targen.js (or app.js) after Stage 1 completes.
|
||||
@@ -30,6 +32,18 @@ export function initPrinttarg() {
|
||||
const galleryInfo = document.getElementById("galleryInfo");
|
||||
const galleryGrid = document.getElementById("galleryGrid");
|
||||
|
||||
// Raw Printing UI elements
|
||||
const rawPrintPanel = document.getElementById("rawPrintPanel");
|
||||
const printerSelect = document.getElementById("printerSelect");
|
||||
const btnRefreshPrinters = document.getElementById("btnRefreshPrinters");
|
||||
const printerStatusBadge = document.getElementById("printerStatusBadge");
|
||||
const chkPpdFallback = document.getElementById("chkPpdFallback");
|
||||
const btnPrintAll = document.getElementById("btnPrintAll");
|
||||
const btnAdvanceToStage3 = document.getElementById("btnAdvanceToStage3");
|
||||
const printNotification = document.getElementById("printNotification");
|
||||
const printNotificationIcon = document.getElementById("printNotificationIcon");
|
||||
const printNotificationText = document.getElementById("printNotificationText");
|
||||
|
||||
// Show/hide custom page size inputs
|
||||
pageSizeSelect.addEventListener("change", (e) => {
|
||||
if (e.target.value === "custom") {
|
||||
@@ -39,7 +53,126 @@ export function initPrinttarg() {
|
||||
}
|
||||
});
|
||||
|
||||
// Create Layout button
|
||||
/**
|
||||
* Display status feedback banners in the raw printing panel.
|
||||
*/
|
||||
function showNotification(type, message) {
|
||||
if (!printNotification) return;
|
||||
printNotification.className = `notification-banner ${type}`;
|
||||
printNotification.classList.remove("hidden");
|
||||
|
||||
if (printNotificationIcon) {
|
||||
if (type === "success") printNotificationIcon.textContent = "✓";
|
||||
else if (type === "error") printNotificationIcon.textContent = "✕";
|
||||
else printNotificationIcon.textContent = "ℹ️";
|
||||
}
|
||||
|
||||
if (printNotificationText) {
|
||||
printNotificationText.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
function hideNotification() {
|
||||
if (printNotification) {
|
||||
printNotification.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the badge reflecting current printer's status.
|
||||
*/
|
||||
function updatePrinterStatusBadge() {
|
||||
const selectedName = printerSelect.value;
|
||||
const printer = discoveredPrinters.find(p => p.name === selectedName);
|
||||
|
||||
if (!printer || !printerStatusBadge) {
|
||||
if (printerStatusBadge) printerStatusBadge.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
printerStatusBadge.classList.remove("hidden", "badge-idle", "badge-ready", "badge-printing", "badge-stopped", "badge-error", "badge-default");
|
||||
|
||||
const statusLower = (printer.status || "").toLowerCase();
|
||||
printerStatusBadge.textContent = printer.status || "Ready";
|
||||
|
||||
if (statusLower.includes("print")) {
|
||||
printerStatusBadge.classList.add("badge-printing");
|
||||
} else if (statusLower.includes("stop") || statusLower.includes("disable") || statusLower.includes("error")) {
|
||||
printerStatusBadge.classList.add("badge-stopped");
|
||||
} else if (statusLower.includes("idle") || statusLower.includes("ready")) {
|
||||
printerStatusBadge.classList.add("badge-idle");
|
||||
} else {
|
||||
printerStatusBadge.classList.add("badge-default");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover and enumerate all OS-installed printers via Tauri IPC.
|
||||
*/
|
||||
async function loadPrinters() {
|
||||
if (!printerSelect) return;
|
||||
printerSelect.disabled = true;
|
||||
printerSelect.innerHTML = '<option value="" disabled selected>Enumerating printers...</option>';
|
||||
if (printerStatusBadge) printerStatusBadge.classList.add("hidden");
|
||||
|
||||
try {
|
||||
const printers = await invoke("get_printers");
|
||||
discoveredPrinters = Array.isArray(printers) ? printers : [];
|
||||
printerSelect.innerHTML = "";
|
||||
|
||||
if (discoveredPrinters.length === 0) {
|
||||
printerSelect.innerHTML = '<option value="" disabled selected>No printers found</option>';
|
||||
printerSelect.disabled = true;
|
||||
if (btnPrintAll) btnPrintAll.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
printerSelect.disabled = false;
|
||||
let defaultSelected = false;
|
||||
|
||||
discoveredPrinters.forEach((p, idx) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = p.name;
|
||||
opt.textContent = `${p.name}${p.is_default ? ' (Default)' : ''} [${p.status || 'Ready'}]`;
|
||||
if (p.is_default && !defaultSelected) {
|
||||
opt.selected = true;
|
||||
defaultSelected = true;
|
||||
}
|
||||
printerSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
if (!defaultSelected && discoveredPrinters.length > 0) {
|
||||
printerSelect.selectedIndex = 0;
|
||||
}
|
||||
|
||||
if (btnPrintAll && currentManifest) btnPrintAll.disabled = false;
|
||||
updatePrinterStatusBadge();
|
||||
|
||||
} catch (err) {
|
||||
console.error("[ICCery Print] Failed to query printers:", err);
|
||||
printerSelect.innerHTML = `<option value="" disabled selected>Failed to load printers</option>`;
|
||||
printerSelect.disabled = true;
|
||||
if (btnPrintAll) btnPrintAll.disabled = true;
|
||||
showNotification("error", `Could not enumerate system printers: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (printerSelect) {
|
||||
printerSelect.addEventListener("change", () => {
|
||||
updatePrinterStatusBadge();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnRefreshPrinters) {
|
||||
btnRefreshPrinters.addEventListener("click", () => {
|
||||
loadPrinters();
|
||||
});
|
||||
}
|
||||
|
||||
// Load initial printer list on startup
|
||||
loadPrinters();
|
||||
|
||||
// Create Layout button handler
|
||||
btnCreateLayout.addEventListener("click", async () => {
|
||||
// Validate that Stage 1 has been completed
|
||||
if (!stage1Basename) {
|
||||
@@ -51,6 +184,8 @@ export function initPrinttarg() {
|
||||
logPre.textContent = "";
|
||||
logContainer.classList.remove("hidden");
|
||||
tiffGallery.classList.add("hidden");
|
||||
if (rawPrintPanel) rawPrintPanel.classList.add("hidden");
|
||||
hideNotification();
|
||||
galleryGrid.innerHTML = "";
|
||||
btnCreateLayout.disabled = true;
|
||||
|
||||
@@ -83,8 +218,6 @@ export function initPrinttarg() {
|
||||
};
|
||||
|
||||
const processId = `printtarg_${stage1Basename}`;
|
||||
|
||||
// Accumulate all stdout lines to extract JSON manifest at the end
|
||||
let stdoutAccumulator = "";
|
||||
|
||||
try {
|
||||
@@ -116,11 +249,14 @@ export function initPrinttarg() {
|
||||
// Parse the JSON manifest from stdout
|
||||
const manifest = extractManifest(stdoutAccumulator);
|
||||
if (manifest && manifest.pages && manifest.pages.length > 0) {
|
||||
currentManifest = manifest;
|
||||
renderTiffGallery(manifest, config.cwd);
|
||||
if (rawPrintPanel) rawPrintPanel.classList.remove("hidden");
|
||||
if (discoveredPrinters.length > 0 && btnPrintAll) btnPrintAll.disabled = false;
|
||||
showNotification("info", "Target pages generated. Select your destination printer below and print with color management strictly bypassed.");
|
||||
}
|
||||
|
||||
setStage2Result(stage1Basename, stage1Cwd);
|
||||
advanceToStage3();
|
||||
} else {
|
||||
logPre.textContent += `\n[ERROR] printtarg exited with code ${event.payload.code}.\n`;
|
||||
btnCreateLayout.disabled = false;
|
||||
@@ -138,13 +274,115 @@ export function initPrinttarg() {
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract the JSON manifest object from the accumulated stdout.
|
||||
* The manifest is emitted by printtarg -u as a JSON block starting with { and ending with }.
|
||||
* It always appears at the end of stdout, after all "Creating file..." lines.
|
||||
* Spool a single TIFF target file with native color management bypass.
|
||||
*/
|
||||
async function printTargetFile(filePath, label, triggeringButton) {
|
||||
const printerName = printerSelect ? printerSelect.value : "";
|
||||
if (!printerName) {
|
||||
showNotification("error", "Please select a destination printer first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
|
||||
const origBtnContent = triggeringButton ? triggeringButton.innerHTML : "";
|
||||
|
||||
if (triggeringButton) {
|
||||
triggeringButton.disabled = true;
|
||||
triggeringButton.innerHTML = '<span class="btn-spinner"></span> Spooling...';
|
||||
}
|
||||
|
||||
showNotification("info", `Sending ${label} to '${printerName}' (bypassing color management)...`);
|
||||
|
||||
try {
|
||||
await invoke("print_target_native", {
|
||||
printerName,
|
||||
tiffPath: filePath,
|
||||
ppdUncorrectedPassthrough: ppdFallback,
|
||||
});
|
||||
|
||||
showNotification("success", `✓ Successfully spooled ${label} to '${printerName}' with raw color management bypass.`);
|
||||
} catch (err) {
|
||||
console.error("[ICCery Print Error]:", err);
|
||||
showNotification("error", `Print job failed for ${label}: ${err}`);
|
||||
} finally {
|
||||
if (triggeringButton) {
|
||||
triggeringButton.disabled = false;
|
||||
triggeringButton.innerHTML = origBtnContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print all generated TIFF target pages in sequence.
|
||||
*/
|
||||
if (btnPrintAll) {
|
||||
btnPrintAll.addEventListener("click", async () => {
|
||||
if (!currentManifest || !currentManifest.pages || currentManifest.pages.length === 0) {
|
||||
showNotification("error", "No generated target pages available to print.");
|
||||
return;
|
||||
}
|
||||
|
||||
const printerName = printerSelect ? printerSelect.value : "";
|
||||
if (!printerName) {
|
||||
showNotification("error", "Please select a destination printer first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
|
||||
const cwd = stage1Cwd;
|
||||
const sep = cwd.includes('\\') ? '\\' : '/';
|
||||
const pages = currentManifest.pages;
|
||||
|
||||
btnPrintAll.disabled = true;
|
||||
const btnText = btnPrintAll.querySelector(".btn-text");
|
||||
const btnSpinner = btnPrintAll.querySelector(".btn-spinner");
|
||||
if (btnSpinner) btnSpinner.classList.remove("hidden");
|
||||
if (btnText) btnText.textContent = "Spooling Targets...";
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const filePath = cwd ? `${cwd}${sep}${page.filename}` : page.filename;
|
||||
showNotification("info", `Spooling page ${i + 1} of ${pages.length} (${page.filename}) to '${printerName}'...`);
|
||||
|
||||
try {
|
||||
await invoke("print_target_native", {
|
||||
printerName,
|
||||
tiffPath: filePath,
|
||||
ppdUncorrectedPassthrough: ppdFallback,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[ICCery Print Error] Page ${page.filename}:`, err);
|
||||
showNotification("error", `Failed on page ${page.filename}: ${err}`);
|
||||
errorCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount === 0) {
|
||||
showNotification("success", `✓ All ${pages.length} target page(s) successfully spooled to '${printerName}'!`);
|
||||
}
|
||||
|
||||
btnPrintAll.disabled = false;
|
||||
if (btnSpinner) btnSpinner.classList.add("hidden");
|
||||
if (btnText) btnText.textContent = "🖨️ Print All Pages (Bypass CM)";
|
||||
});
|
||||
}
|
||||
|
||||
// Proceed to Stage 3
|
||||
if (btnAdvanceToStage3) {
|
||||
btnAdvanceToStage3.addEventListener("click", () => {
|
||||
setStage2Result(stage1Basename, stage1Cwd);
|
||||
advanceToStage3();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract JSON manifest emitted by printtarg -u.
|
||||
*/
|
||||
function extractManifest(stdout) {
|
||||
try {
|
||||
// Find the last JSON object in the output
|
||||
const jsonStart = stdout.lastIndexOf('{\n "event": "manifest"');
|
||||
if (jsonStart === -1) return null;
|
||||
const jsonEnd = stdout.indexOf('\n}', jsonStart);
|
||||
@@ -158,9 +396,7 @@ export function initPrinttarg() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render TIFF preview gallery using base64-encoded images from the backend.
|
||||
* @param {Object} manifest - The parsed JSON manifest from printtarg -u
|
||||
* @param {string} cwd - The working directory where TIFFs were generated
|
||||
* Render TIFF preview gallery with individual card print actions.
|
||||
*/
|
||||
async function renderTiffGallery(manifest, cwd) {
|
||||
tiffGallery.classList.remove("hidden");
|
||||
@@ -169,6 +405,7 @@ export function initPrinttarg() {
|
||||
const totalPatches = manifest.pages.reduce((sum, p) => sum + p.patches, 0);
|
||||
const dims = manifest.pages[0];
|
||||
galleryInfo.textContent = `${pageCount} page(s) · ${totalPatches} patches · ${dims.width_mm} × ${dims.height_mm} mm per page`;
|
||||
galleryGrid.innerHTML = "";
|
||||
|
||||
for (const page of manifest.pages) {
|
||||
const sep = cwd.includes('\\') ? '\\' : '/';
|
||||
@@ -187,7 +424,6 @@ export function initPrinttarg() {
|
||||
const img = document.createElement("img");
|
||||
img.src = `data:image/tiff;base64,${base64Data}`;
|
||||
img.alt = page.filename;
|
||||
// TIFF may not render natively in all browsers — provide a fallback
|
||||
img.onerror = () => {
|
||||
img.remove();
|
||||
const fallback = document.createElement("div");
|
||||
@@ -203,6 +439,19 @@ export function initPrinttarg() {
|
||||
card.appendChild(fallback);
|
||||
}
|
||||
|
||||
// Individual Card Print Button
|
||||
const cardActions = document.createElement("div");
|
||||
cardActions.className = "gallery-card-actions";
|
||||
const btnPrintCard = document.createElement("button");
|
||||
btnPrintCard.type = "button";
|
||||
btnPrintCard.className = "btn-print-page";
|
||||
btnPrintCard.innerHTML = "<span>🖨️ Print Page</span>";
|
||||
btnPrintCard.addEventListener("click", () => {
|
||||
printTargetFile(filePath, page.filename, btnPrintCard);
|
||||
});
|
||||
cardActions.appendChild(btnPrintCard);
|
||||
card.appendChild(cardActions);
|
||||
|
||||
galleryGrid.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,3 +847,229 @@ button.danger:hover {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Stage 2: Native Raw Printing Engine Panel
|
||||
======================================== */
|
||||
.print-panel {
|
||||
margin-top: 24px;
|
||||
background: var(--panel-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 18px 20px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.print-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.print-panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.print-panel-title h3 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.acpu-badge {
|
||||
background: rgba(0, 122, 204, 0.15);
|
||||
border: 1px solid rgba(0, 122, 204, 0.4);
|
||||
color: #4fc3f7;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.print-panel-desc {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 0.85rem;
|
||||
color: #aaa;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.printer-controls-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.printer-select-group {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.printer-select-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.printer-select-wrapper select {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-idle, .badge-ready {
|
||||
background: rgba(76, 175, 80, 0.15);
|
||||
color: #81c784;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.badge-printing {
|
||||
background: rgba(33, 150, 243, 0.15);
|
||||
color: #64b5f6;
|
||||
border: 1px solid rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.badge-stopped, .badge-error {
|
||||
background: rgba(244, 67, 54, 0.15);
|
||||
color: #e57373;
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.badge-default {
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
color: #ffd54f;
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.print-options-group {
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.notification-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 16px;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-banner.info {
|
||||
background: rgba(33, 150, 243, 0.12);
|
||||
border: 1px solid rgba(33, 150, 243, 0.3);
|
||||
color: #90caf9;
|
||||
}
|
||||
|
||||
.notification-banner.success {
|
||||
background: rgba(76, 175, 80, 0.12);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
color: #a5d6a7;
|
||||
}
|
||||
|
||||
.notification-banner.error {
|
||||
background: rgba(244, 67, 54, 0.12);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
color: #ef9a9a;
|
||||
}
|
||||
|
||||
.print-actions-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.gallery-card-actions {
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-print-page {
|
||||
font-size: 0.8rem;
|
||||
padding: 5px 10px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-print-page:hover {
|
||||
background: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user