mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 04:42:05 -04:00
Add a new "Add by Address" flow in the printer settings that allows users to manually add printers by IP address or hostname, enabling printing to devices not visible via mDNS/Avahi discovery (e.g., printers behind Tailscale subnet routers, VPNs, or across network boundaries). Go backend: - New cups.testConnection IPC method that probes remote printers via IPP Get-Printer-Attributes with /ipp/print then / fallback - Input validation with host sanitization and protocol allowlist - Auth-aware probing (HTTP 401/403 reported as reachable) - lpadmin CLI fallback for CreatePrinter/DeletePrinter when cups-pk-helper polkit authorization fails QML frontend: - "Add by Address" toggle alongside existing device discovery - Manual entry form with host, port, protocol fields - Test Connection button with loading state and result display - Smart PPD auto-selection by probed makeModel with driverless fallback - All strings use I18n.tr() with translator context Includes 20+ unit tests covering validation, probe delegation, TLS flag propagation, auth error detection, and handler routing.
1703 lines
92 KiB
QML
1703 lines
92 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.Common
|
|
import qs.Modals.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: printerTab
|
|
|
|
LayoutMirroring.enabled: I18n.isRtl
|
|
LayoutMirroring.childrenInherit: true
|
|
|
|
property bool showAddPrinter: false
|
|
property bool manualEntryMode: false
|
|
property string manualHost: ""
|
|
property string manualPort: "631"
|
|
property string manualProtocol: "ipp"
|
|
property bool testingConnection: false
|
|
property var testConnectionResult: null
|
|
property string newPrinterName: ""
|
|
property string selectedDeviceUri: ""
|
|
property var selectedDevice: null
|
|
property string selectedPpd: ""
|
|
property string newPrinterLocation: ""
|
|
property string newPrinterInfo: ""
|
|
property var suggestedPPDs: []
|
|
|
|
function resetAddPrinterForm() {
|
|
manualEntryMode = false;
|
|
manualHost = "";
|
|
manualPort = "631";
|
|
manualProtocol = "ipp";
|
|
testingConnection = false;
|
|
testConnectionResult = null;
|
|
newPrinterName = "";
|
|
selectedDeviceUri = "";
|
|
selectedDevice = null;
|
|
selectedPpd = "";
|
|
newPrinterLocation = "";
|
|
newPrinterInfo = "";
|
|
suggestedPPDs = [];
|
|
}
|
|
|
|
Connections {
|
|
target: CupsService
|
|
function onPpdsChanged() {
|
|
if (printerTab.manualEntryMode && printerTab.testConnectionResult?.success)
|
|
printerTab.selectDriverlessPPD();
|
|
}
|
|
}
|
|
|
|
function selectDriverlessPPD() {
|
|
if (printerTab.selectedPpd || CupsService.ppds.length === 0)
|
|
return;
|
|
|
|
const probeModel = printerTab.testConnectionResult?.data?.makeModel || "";
|
|
let suggested = [];
|
|
|
|
// Try to find a model-specific PPD match
|
|
if (probeModel) {
|
|
const normalizedModel = probeModel.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
const modelMatches = CupsService.ppds.filter(p => {
|
|
const normalizedPPD = (p.makeModel || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
return normalizedPPD.includes(normalizedModel) || normalizedModel.includes(normalizedPPD);
|
|
});
|
|
if (modelMatches.length > 0)
|
|
suggested = suggested.concat(modelMatches);
|
|
}
|
|
|
|
// Always include driverless as an option
|
|
const driverless = CupsService.ppds.filter(p => p.name === "driverless" || p.name === "everywhere");
|
|
for (const d of driverless) {
|
|
if (!suggested.find(s => s.name === d.name))
|
|
suggested.push(d);
|
|
}
|
|
|
|
if (suggested.length > 0) {
|
|
printerTab.selectedPpd = suggested[0].name;
|
|
printerTab.suggestedPPDs = suggested;
|
|
}
|
|
}
|
|
|
|
function selectDevice(device) {
|
|
if (!device)
|
|
return;
|
|
selectedDevice = device;
|
|
selectedDeviceUri = device.uri;
|
|
if (!newPrinterName) {
|
|
newPrinterName = CupsService.suggestPrinterName(device);
|
|
}
|
|
if (device.location && !newPrinterLocation) {
|
|
newPrinterLocation = CupsService.decodeUri(device.location);
|
|
}
|
|
suggestedPPDs = CupsService.getMatchingPPDs(device);
|
|
if (suggestedPPDs.length > 0 && !selectedPpd) {
|
|
selectedPpd = suggestedPPDs[0].name;
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
CupsService.getClasses();
|
|
}
|
|
|
|
ConfirmModal {
|
|
id: deletePrinterConfirm
|
|
}
|
|
|
|
ConfirmModal {
|
|
id: purgeJobsConfirm
|
|
}
|
|
|
|
ConfirmModal {
|
|
id: deleteClassConfirm
|
|
}
|
|
|
|
DankFlickable {
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentHeight: mainColumn.height + Theme.spacingXL
|
|
contentWidth: width
|
|
|
|
Column {
|
|
id: mainColumn
|
|
topPadding: 4
|
|
|
|
width: Math.min(600, parent.width - Theme.spacingL * 2)
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: Theme.spacingL
|
|
|
|
StyledRect {
|
|
width: parent.width
|
|
height: overviewSection.implicitHeight + Theme.spacingL * 2
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainerHigh
|
|
|
|
Column {
|
|
id: overviewSection
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "print"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - Theme.iconSize - Theme.spacingM
|
|
spacing: Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
StyledText {
|
|
text: I18n.tr("CUPS Print Server")
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
}
|
|
|
|
Grid {
|
|
columns: 2
|
|
columnSpacing: Theme.spacingL
|
|
rowSpacing: Theme.spacingS
|
|
width: parent.width
|
|
|
|
StyledText {
|
|
text: I18n.tr("Status")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
Row {
|
|
spacing: Theme.spacingS
|
|
|
|
Rectangle {
|
|
width: 8
|
|
height: 8
|
|
radius: 4
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: CupsService.cupsAvailable ? Theme.success : Theme.error
|
|
}
|
|
|
|
StyledText {
|
|
text: CupsService.cupsAvailable ? I18n.tr("Available") : I18n.tr("Unavailable")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Printers")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
StyledText {
|
|
text: CupsService.printerNames.length.toString()
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Total Jobs")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
StyledText {
|
|
text: CupsService.getTotalJobsNum().toString()
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
width: parent.width
|
|
height: addPrinterSection.implicitHeight + Theme.spacingL * 2
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainerHigh
|
|
visible: CupsService.cupsAvailable
|
|
|
|
Column {
|
|
id: addPrinterSection
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "add_circle"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - Theme.iconSize - Theme.spacingM - addPrinterToggleBtn.width - Theme.spacingM
|
|
spacing: Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
StyledText {
|
|
text: I18n.tr("Add Printer")
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Configure a new printer")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: addPrinterToggleBtn
|
|
width: 28
|
|
height: 28
|
|
radius: 14
|
|
color: addPrinterToggleArea.containsMouse ? Theme.surfacePressed : "transparent"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: printerTab.showAddPrinter ? "expand_less" : "expand_more"
|
|
size: 18
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
MouseArea {
|
|
id: addPrinterToggleArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
printerTab.showAddPrinter = !printerTab.showAddPrinter;
|
|
if (printerTab.showAddPrinter) {
|
|
if (CupsService.devices.length === 0) {
|
|
CupsService.getDevices();
|
|
CupsService.getPPDs();
|
|
}
|
|
} else {
|
|
printerTab.resetAddPrinterForm();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
visible: printerTab.showAddPrinter
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Rectangle {
|
|
width: discoverRow.width + Theme.spacingM * 2
|
|
height: 32
|
|
radius: Theme.cornerRadius
|
|
color: !printerTab.manualEntryMode ? Theme.primary : (discoverArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight)
|
|
|
|
Row {
|
|
id: discoverRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: "search"
|
|
size: 16
|
|
color: !printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Discover Devices", "Toggle button to scan for printers via mDNS/Avahi")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: !printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: discoverArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
printerTab.manualEntryMode = false;
|
|
printerTab.testConnectionResult = null;
|
|
printerTab.testingConnection = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: manualRow.width + Theme.spacingM * 2
|
|
height: 32
|
|
radius: Theme.cornerRadius
|
|
color: printerTab.manualEntryMode ? Theme.primary : (manualArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight)
|
|
|
|
Row {
|
|
id: manualRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: "edit"
|
|
size: 16
|
|
color: printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Add by Address", "Toggle button to manually add a printer by IP or hostname")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: manualArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
printerTab.manualEntryMode = true;
|
|
printerTab.selectedDevice = null;
|
|
printerTab.selectedDeviceUri = "";
|
|
if (CupsService.ppds.length === 0)
|
|
CupsService.getPPDs();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
visible: !printerTab.manualEntryMode
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Device")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankDropdown {
|
|
id: deviceDropdown
|
|
dropdownWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2
|
|
popupWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2
|
|
enableFuzzySearch: true
|
|
emptyText: I18n.tr("No devices found")
|
|
currentValue: {
|
|
if (CupsService.loadingDevices)
|
|
return I18n.tr("Scanning...");
|
|
if (printerTab.selectedDevice)
|
|
return CupsService.getDeviceDisplayName(printerTab.selectedDevice);
|
|
return I18n.tr("Select device...");
|
|
}
|
|
options: CupsService.filteredDevices.map(d => CupsService.getDeviceDisplayName(d))
|
|
onValueChanged: value => {
|
|
const filtered = CupsService.filteredDevices;
|
|
const device = filtered.find(d => CupsService.getDeviceDisplayName(d) === value);
|
|
if (device)
|
|
printerTab.selectDevice(device);
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
id: scanDevicesBtn
|
|
iconName: "refresh"
|
|
buttonSize: 32
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
enabled: !CupsService.loadingDevices
|
|
onClicked: CupsService.getDevices()
|
|
|
|
RotationAnimation on rotation {
|
|
running: CupsService.loadingDevices
|
|
loops: Animation.Infinite
|
|
from: 0
|
|
to: 360
|
|
duration: 1000
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
visible: printerTab.selectedDevice !== null
|
|
|
|
Item {
|
|
width: 80
|
|
height: 1
|
|
}
|
|
|
|
StyledText {
|
|
text: CupsService.getDeviceSubtitle(printerTab.selectedDevice)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.width - 80 - Theme.spacingS
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
visible: printerTab.manualEntryMode
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Host", "Label for printer IP address or hostname input field")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankTextField {
|
|
width: parent.width - 80 - Theme.spacingS
|
|
placeholderText: I18n.tr("IP address or hostname", "Placeholder text for manual printer address input")
|
|
text: printerTab.manualHost
|
|
onTextEdited: {
|
|
printerTab.manualHost = text;
|
|
printerTab.testConnectionResult = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Port", "Label for printer port number input field")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankTextField {
|
|
width: 80
|
|
placeholderText: "631"
|
|
text: printerTab.manualPort
|
|
onTextEdited: {
|
|
printerTab.manualPort = text;
|
|
printerTab.testConnectionResult = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Protocol", "Label for printer protocol selector, e.g. ipp, ipps, lpd, socket")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankDropdown {
|
|
id: protocolDropdown
|
|
dropdownWidth: 120
|
|
popupWidth: 120
|
|
currentValue: printerTab.manualProtocol
|
|
options: ["ipp", "ipps", "lpd", "socket"]
|
|
onValueChanged: value => {
|
|
printerTab.manualProtocol = value;
|
|
printerTab.testConnectionResult = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Item {
|
|
width: 80
|
|
height: 1
|
|
}
|
|
|
|
DankButton {
|
|
text: printerTab.testingConnection ? I18n.tr("Testing...", "Button state while testing printer connection") : I18n.tr("Test Connection", "Button to test connection to a printer by IP address")
|
|
iconName: printerTab.testingConnection ? "sync" : "lan"
|
|
buttonHeight: 36
|
|
enabled: printerTab.manualHost.length > 0 && !printerTab.testingConnection
|
|
onClicked: {
|
|
printerTab.testingConnection = true;
|
|
printerTab.testConnectionResult = null;
|
|
const port = parseInt(printerTab.manualPort) || 631;
|
|
CupsService.testConnection(printerTab.manualHost, port, printerTab.manualProtocol, response => {
|
|
printerTab.testingConnection = false;
|
|
if (response.error) {
|
|
printerTab.testConnectionResult = {
|
|
"success": false,
|
|
"error": response.error
|
|
};
|
|
} else if (response.result) {
|
|
printerTab.testConnectionResult = {
|
|
"success": response.result.reachable === true,
|
|
"data": response.result
|
|
};
|
|
if (response.result.reachable) {
|
|
if (response.result.uri)
|
|
printerTab.selectedDeviceUri = response.result.uri;
|
|
if (response.result.name && !printerTab.newPrinterName)
|
|
printerTab.newPrinterName = response.result.name.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 32) || "Printer";
|
|
// Load PPDs if not loaded yet, then select driverless
|
|
if (CupsService.ppds.length === 0) {
|
|
CupsService.getPPDs();
|
|
}
|
|
selectDriverlessPPD();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
visible: printerTab.testConnectionResult !== null
|
|
|
|
Row {
|
|
spacing: Theme.spacingS
|
|
|
|
Item {
|
|
width: 80
|
|
height: 1
|
|
}
|
|
|
|
Rectangle {
|
|
width: 8
|
|
height: 8
|
|
radius: 4
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: printerTab.testConnectionResult?.success ? Theme.success : Theme.error
|
|
}
|
|
|
|
StyledText {
|
|
text: printerTab.testConnectionResult?.success ? I18n.tr("Printer reachable", "Status message when test connection to printer succeeds") : I18n.tr("Connection failed", "Status message when test connection to printer fails")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: printerTab.testConnectionResult?.success ? Theme.success : Theme.error
|
|
}
|
|
}
|
|
|
|
Row {
|
|
spacing: Theme.spacingS
|
|
visible: printerTab.testConnectionResult?.success && (printerTab.testConnectionResult?.data?.makeModel || printerTab.testConnectionResult?.data?.info)
|
|
|
|
Item {
|
|
width: 80
|
|
height: 1
|
|
}
|
|
|
|
StyledText {
|
|
text: printerTab.testConnectionResult?.data?.makeModel || printerTab.testConnectionResult?.data?.info || ""
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
|
|
Row {
|
|
spacing: Theme.spacingS
|
|
visible: !printerTab.testConnectionResult?.success && printerTab.testConnectionResult?.data?.error
|
|
|
|
Item {
|
|
width: 80
|
|
height: 1
|
|
}
|
|
|
|
StyledText {
|
|
text: printerTab.testConnectionResult?.data?.error || printerTab.testConnectionResult?.error || ""
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.parent.width - 80 - Theme.spacingS
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Driver")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankDropdown {
|
|
id: ppdDropdown
|
|
dropdownWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2
|
|
popupWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2
|
|
enableFuzzySearch: true
|
|
emptyText: I18n.tr("No drivers found")
|
|
currentValue: {
|
|
if (CupsService.loadingPPDs)
|
|
return I18n.tr("Loading...");
|
|
if (printerTab.selectedPpd) {
|
|
const ppd = CupsService.ppds.find(p => p.name === printerTab.selectedPpd);
|
|
if (ppd) {
|
|
const isSuggested = printerTab.suggestedPPDs.some(s => s.name === ppd.name);
|
|
return (isSuggested ? "★ " : "") + (ppd.makeModel || ppd.name);
|
|
}
|
|
return printerTab.selectedPpd;
|
|
}
|
|
return printerTab.suggestedPPDs.length > 0 ? I18n.tr("Recommended available") : I18n.tr("Select driver...");
|
|
}
|
|
options: {
|
|
const suggested = printerTab.suggestedPPDs.map(p => "★ " + (p.makeModel || p.name));
|
|
const others = CupsService.ppds.filter(p => !printerTab.suggestedPPDs.some(s => s.name === p.name)).map(p => p.makeModel || p.name);
|
|
return suggested.concat(others);
|
|
}
|
|
onValueChanged: value => {
|
|
const cleanValue = value.replace(/^★ /, "");
|
|
const ppd = CupsService.ppds.find(p => (p.makeModel || p.name) === cleanValue);
|
|
if (ppd)
|
|
printerTab.selectedPpd = ppd.name;
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
id: refreshPpdsBtn
|
|
iconName: "refresh"
|
|
buttonSize: 32
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
enabled: !CupsService.loadingPPDs
|
|
onClicked: CupsService.getPPDs()
|
|
|
|
RotationAnimation on rotation {
|
|
running: CupsService.loadingPPDs
|
|
loops: Animation.Infinite
|
|
from: 0
|
|
to: 360
|
|
duration: 1000
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Name")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankTextField {
|
|
width: parent.width - 80 - Theme.spacingS
|
|
placeholderText: I18n.tr("Printer name (no spaces)")
|
|
text: printerTab.newPrinterName
|
|
onTextEdited: printerTab.newPrinterName = text.replace(/\s/g, "-")
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Location")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankTextField {
|
|
width: parent.width - 80 - Theme.spacingS
|
|
placeholderText: I18n.tr("Optional location")
|
|
text: printerTab.newPrinterLocation
|
|
onTextEdited: printerTab.newPrinterLocation = text
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Description")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: 80
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankTextField {
|
|
width: parent.width - 80 - Theme.spacingS
|
|
placeholderText: I18n.tr("Optional description")
|
|
text: printerTab.newPrinterInfo
|
|
onTextEdited: printerTab.newPrinterInfo = text
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
LayoutMirroring.enabled: false
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
layoutDirection: Qt.RightToLeft
|
|
|
|
DankButton {
|
|
text: CupsService.creatingPrinter ? I18n.tr("Creating...") : I18n.tr("Create Printer")
|
|
iconName: CupsService.creatingPrinter ? "sync" : "add"
|
|
buttonHeight: 36
|
|
enabled: printerTab.newPrinterName.length > 0 && printerTab.selectedDeviceUri.length > 0 && printerTab.selectedPpd.length > 0 && !CupsService.creatingPrinter
|
|
onClicked: {
|
|
CupsService.createPrinter(printerTab.newPrinterName, printerTab.selectedDeviceUri, printerTab.selectedPpd, {
|
|
location: printerTab.newPrinterLocation,
|
|
information: printerTab.newPrinterInfo
|
|
});
|
|
printerTab.resetAddPrinterForm();
|
|
printerTab.showAddPrinter = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
width: parent.width
|
|
height: printersSection.implicitHeight + Theme.spacingL * 2
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainerHigh
|
|
visible: CupsService.cupsAvailable
|
|
|
|
Column {
|
|
id: printersSection
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "print"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - Theme.iconSize - Theme.spacingM - refreshBtn.width - Theme.spacingM
|
|
spacing: Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
StyledText {
|
|
text: I18n.tr("Printers")
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
|
|
StyledText {
|
|
text: {
|
|
const count = CupsService.printerNames.length;
|
|
if (count === 0)
|
|
return I18n.tr("No printers configured");
|
|
return I18n.ntr("%1 printer", "%1 printers", count).arg(count);
|
|
}
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
id: refreshBtn
|
|
iconName: "refresh"
|
|
buttonSize: 32
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: CupsService.getState()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: 80
|
|
visible: CupsService.printerNames.length === 0
|
|
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingS
|
|
|
|
DankIcon {
|
|
name: "print_disabled"
|
|
size: 32
|
|
color: Theme.surfaceVariantText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("No printers found")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: 4
|
|
visible: CupsService.printerNames.length > 0
|
|
|
|
Repeater {
|
|
model: CupsService.printerNames
|
|
|
|
delegate: Rectangle {
|
|
id: printerDelegate
|
|
required property string modelData
|
|
required property int index
|
|
|
|
readonly property var printerData: CupsService.getPrinterData(modelData)
|
|
readonly property bool isExpanded: CupsService.expandedPrinter === modelData || hasJobs
|
|
readonly property bool hasJobs: (printerData?.jobs?.length ?? 0) > 0
|
|
readonly property bool isIdle: printerData?.state === "idle"
|
|
readonly property bool isStopped: printerData?.state === "stopped"
|
|
|
|
width: parent.width
|
|
height: isExpanded ? 56 + expandedContent.height : 56
|
|
radius: Theme.cornerRadius
|
|
color: printerMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
|
border.width: CupsService.selectedPrinter === modelData ? 2 : 0
|
|
border.color: Theme.primary
|
|
clip: true
|
|
|
|
Behavior on height {
|
|
NumberAnimation {
|
|
duration: 150
|
|
easing.type: Easing.OutQuad
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: 56
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: printerActions.left
|
|
anchors.rightMargin: Theme.spacingS
|
|
spacing: Theme.spacingS
|
|
|
|
DankIcon {
|
|
name: isStopped ? "print_disabled" : "print"
|
|
size: 20
|
|
color: isStopped ? Theme.error : (isIdle ? Theme.primary : Theme.warning)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
width: parent.width - 20 - Theme.spacingS
|
|
|
|
StyledText {
|
|
text: modelData
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
font.weight: CupsService.selectedPrinter === modelData ? Font.Medium : Font.Normal
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: CupsService.getPrinterStateTranslation(printerData?.state || "")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: {
|
|
switch (printerData?.state) {
|
|
case "idle":
|
|
return Theme.primary;
|
|
case "stopped":
|
|
return Theme.error;
|
|
case "processing":
|
|
return Theme.warning;
|
|
default:
|
|
return Theme.surfaceVariantText;
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: "•"
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
visible: (printerData?.jobs?.length ?? 0) > 0
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.ntr("%1 job", "%1 jobs", printerData?.jobs?.length ?? 0).arg(printerData?.jobs?.length ?? 0)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
visible: (printerData?.jobs?.length ?? 0) > 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: printerActions
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXS
|
|
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
radius: 14
|
|
color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: isExpanded ? "expand_less" : "expand_more"
|
|
size: 18
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
MouseArea {
|
|
id: expandBtn
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
CupsService.expandedPrinter = isExpanded ? "" : modelData;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
radius: 14
|
|
color: deleteBtn.containsMouse ? Theme.errorHover : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "delete"
|
|
size: 18
|
|
color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
|
|
}
|
|
|
|
MouseArea {
|
|
id: deleteBtn
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
deletePrinterConfirm.showWithOptions({
|
|
title: I18n.tr("Delete Printer"),
|
|
message: I18n.tr("Delete \"%1\"?").arg(modelData),
|
|
confirmText: I18n.tr("Delete"),
|
|
confirmColor: Theme.error,
|
|
onConfirm: () => CupsService.deletePrinter(modelData)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: printerMouseArea
|
|
anchors.fill: parent
|
|
anchors.rightMargin: printerActions.width + Theme.spacingM
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
CupsService.setSelectedPrinter(modelData);
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: expandedContent
|
|
width: parent.width
|
|
visible: isExpanded
|
|
|
|
Rectangle {
|
|
width: parent.width - Theme.spacingM * 2
|
|
height: 1
|
|
x: Theme.spacingM
|
|
color: Theme.outlineLight
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: detailsColumn.implicitHeight + Theme.spacingM * 2
|
|
|
|
Column {
|
|
id: detailsColumn
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingM
|
|
spacing: Theme.spacingS
|
|
|
|
Flow {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
|
|
Repeater {
|
|
model: {
|
|
const fields = [];
|
|
const p = printerData;
|
|
if (!p)
|
|
return fields;
|
|
|
|
fields.push({
|
|
label: I18n.tr("State"),
|
|
value: CupsService.getPrinterStateTranslation(p.state)
|
|
});
|
|
if (p.stateReason && p.stateReason !== "none")
|
|
fields.push({
|
|
label: I18n.tr("Reason"),
|
|
value: CupsService.getPrinterStateReasonTranslation(p.stateReason)
|
|
});
|
|
if (p.makeModel)
|
|
fields.push({
|
|
label: I18n.tr("Model"),
|
|
value: p.makeModel
|
|
});
|
|
if (p.location)
|
|
fields.push({
|
|
label: I18n.tr("Location"),
|
|
value: p.location
|
|
});
|
|
fields.push({
|
|
label: I18n.tr("Accepting"),
|
|
value: p.accepting ? I18n.tr("Yes") : I18n.tr("No")
|
|
});
|
|
|
|
return fields;
|
|
}
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: fieldContent.width + Theme.spacingM * 2
|
|
height: 32
|
|
radius: Theme.cornerRadius - 2
|
|
color: Theme.surfaceContainerHigh
|
|
border.width: 1
|
|
border.color: Theme.outlineLight
|
|
|
|
Row {
|
|
id: fieldContent
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledText {
|
|
text: modelData.label + ":"
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: modelData.value
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
Rectangle {
|
|
height: 28
|
|
width: pauseResumeRow.width + Theme.spacingM * 2
|
|
radius: 14
|
|
color: pauseResumeArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
|
|
|
Row {
|
|
id: pauseResumeRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: isStopped ? "play_arrow" : "pause"
|
|
size: 16
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: isStopped ? I18n.tr("Resume") : I18n.tr("Pause")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: pauseResumeArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (isStopped) {
|
|
CupsService.resumePrinter(modelData);
|
|
} else {
|
|
CupsService.pausePrinter(modelData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
height: 28
|
|
width: testPageRow.width + Theme.spacingM * 2
|
|
radius: 14
|
|
color: testPageArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
|
|
|
Row {
|
|
id: testPageRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: "description"
|
|
size: 16
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Test Page")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: testPageArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: CupsService.printTestPage(modelData)
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
height: 28
|
|
width: acceptRejectRow.width + Theme.spacingM * 2
|
|
radius: 14
|
|
color: acceptRejectArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
|
|
|
Row {
|
|
id: acceptRejectRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: printerData?.accepting ? "block" : "check_circle"
|
|
size: 16
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: printerData?.accepting ? I18n.tr("Reject Jobs") : I18n.tr("Accept Jobs")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: acceptRejectArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (printerData?.accepting) {
|
|
CupsService.rejectJobs(modelData);
|
|
} else {
|
|
CupsService.acceptJobs(modelData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
visible: (printerData?.jobs?.length ?? 0) > 0
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingS
|
|
|
|
StyledText {
|
|
text: I18n.tr("Jobs")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Item {
|
|
width: 1
|
|
height: 1
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Rectangle {
|
|
height: 24
|
|
width: purgeRow.width + Theme.spacingM * 2
|
|
radius: 12
|
|
color: purgeArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
|
|
|
|
Row {
|
|
id: purgeRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: "delete_sweep"
|
|
size: 14
|
|
color: purgeArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Clear All")
|
|
font.pixelSize: Theme.fontSizeSmall - 1
|
|
color: purgeArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: purgeArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
purgeJobsConfirm.showWithOptions({
|
|
title: I18n.tr("Clear All Jobs"),
|
|
message: I18n.tr("Cancel all jobs for \"%1\"?").arg(modelData),
|
|
confirmText: I18n.tr("Clear"),
|
|
confirmColor: Theme.error,
|
|
onConfirm: () => CupsService.purgeJobs(modelData)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: printerData?.jobs ?? []
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: parent.width
|
|
height: 44
|
|
radius: Theme.cornerRadius - 2
|
|
color: Theme.surfaceContainerHighest
|
|
border.width: 1
|
|
border.color: Theme.outlineLight
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.right: jobActions.left
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingS
|
|
|
|
DankIcon {
|
|
name: "description"
|
|
size: 18
|
|
color: Theme.surfaceVariantText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 1
|
|
width: parent.width - 18 - Theme.spacingS
|
|
|
|
StyledText {
|
|
text: "[" + modelData.id + "] " + CupsService.getJobStateTranslation(modelData.state)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceText
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
|
|
StyledText {
|
|
text: {
|
|
const size = Math.round((modelData.size || 0) / 1024);
|
|
const date = new Date(modelData.timeCreated);
|
|
return size + " KB • " + date.toLocaleString(Qt.locale(), Locale.ShortFormat);
|
|
}
|
|
font.pixelSize: Theme.fontSizeSmall - 1
|
|
color: Theme.surfaceVariantText
|
|
anchors.left: parent.left
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: jobActions
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 4
|
|
|
|
Rectangle {
|
|
width: 24
|
|
height: 24
|
|
radius: 12
|
|
color: holdJobBtn.containsMouse ? Theme.surfacePressed : "transparent"
|
|
visible: modelData.state === "pending"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "pause"
|
|
size: 14
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
|
|
MouseArea {
|
|
id: holdJobBtn
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: CupsService.holdJob(modelData.id)
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 24
|
|
height: 24
|
|
radius: 12
|
|
color: restartJobBtn.containsMouse ? Theme.surfacePressed : "transparent"
|
|
visible: modelData.state === "pending-held" || modelData.state === "completed" || modelData.state === "aborted"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "replay"
|
|
size: 14
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
|
|
MouseArea {
|
|
id: restartJobBtn
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: CupsService.restartJob(modelData.id)
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 24
|
|
height: 24
|
|
radius: 12
|
|
color: cancelJobBtn.containsMouse ? Theme.errorHover : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "close"
|
|
size: 14
|
|
color: cancelJobBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
|
|
}
|
|
|
|
MouseArea {
|
|
id: cancelJobBtn
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: CupsService.cancelJob(printerDelegate.modelData, modelData.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
width: parent.width
|
|
height: classesSection.implicitHeight + Theme.spacingL * 2
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainerHigh
|
|
visible: CupsService.cupsAvailable && CupsService.printerClasses.length > 0
|
|
|
|
Column {
|
|
id: classesSection
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "workspaces"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - Theme.iconSize - Theme.spacingM - refreshClassesBtn.width - Theme.spacingM
|
|
spacing: Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
StyledText {
|
|
text: I18n.tr("Printer Classes")
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.ntr("%1 class", "%1 classes", CupsService.printerClasses.length).arg(CupsService.printerClasses.length)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
id: refreshClassesBtn
|
|
iconName: "refresh"
|
|
buttonSize: 32
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: CupsService.getClasses()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: 4
|
|
|
|
Repeater {
|
|
model: CupsService.printerClasses
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: parent.width
|
|
height: 48
|
|
radius: Theme.cornerRadius
|
|
color: classMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.right: classActions.left
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingS
|
|
|
|
DankIcon {
|
|
name: "workspaces"
|
|
size: 20
|
|
color: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
|
|
StyledText {
|
|
text: modelData.name || I18n.tr("Unknown")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.ntr("%1 printer", "%1 printers", modelData.members?.length ?? 0).arg(modelData.members?.length ?? 0)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: classActions
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXS
|
|
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
radius: 14
|
|
color: deleteClassBtn.containsMouse ? Theme.errorHover : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "delete"
|
|
size: 18
|
|
color: deleteClassBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
|
|
}
|
|
|
|
MouseArea {
|
|
id: deleteClassBtn
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
deleteClassConfirm.showWithOptions({
|
|
title: I18n.tr("Delete Class"),
|
|
message: I18n.tr("Delete class \"%1\"?").arg(modelData.name),
|
|
confirmText: I18n.tr("Delete"),
|
|
confirmColor: Theme.error,
|
|
onConfirm: () => CupsService.deleteClass(modelData.name)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: classMouseArea
|
|
anchors.fill: parent
|
|
anchors.rightMargin: classActions.width + Theme.spacingM
|
|
hoverEnabled: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|