From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../quickshell/Modules/Settings/PrinterTab.qml | 1702 ++++++++++++++++++++ 1 file changed, 1702 insertions(+) create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml') diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml new file mode 100644 index 0000000..437dc47 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Settings/PrinterTab.qml @@ -0,0 +1,1702 @@ +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 + } + } + } + } + } + } + } + } +} -- cgit v1.3