import QtQuick import QtQuick.Controls.Basic import QtQuick.Layouts import qEmby Item { id: page Rectangle { anchors.fill: parent color: Theme.bgBase } ColumnLayout { anchors.fill: parent anchors.leftMargin: Theme.spacingLg anchors.rightMargin: Theme.spacingLg anchors.topMargin: Theme.spacingLg anchors.bottomMargin: Theme.spacingXl spacing: Theme.spacingLg RowLayout { Layout.fillWidth: true spacing: Theme.spacingMd Text { text: qsTr("Logs") color: Theme.textPrimary font.pixelSize: Theme.fontTitle font.weight: Font.ExtraBold } Item { Layout.fillWidth: true } Text { text: logsPage.statsText color: Theme.textMuted font.pixelSize: Theme.fontBody } ThemedButton { text: logsPage.refreshing ? qsTr("Refreshing…") : qsTr("Refresh") enabled: !logsPage.refreshing Accessible.name: qsTr("Refresh log files") onClicked: logsPage.refresh() } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: Math.min(190, Math.max(118, page.height * 0.25)) radius: Theme.radiusLg color: Theme.alpha(Theme.bgSurface, Theme.dark ? 0.64 : 0.94) border.width: 1 border.color: Theme.alpha(Theme.borderSoft, 0.7) ListView { id: fileList anchors.fill: parent anchors.margins: Theme.spacingXs clip: true model: logsPage.model spacing: 2 boundsBehavior: Flickable.StopAtBounds currentIndex: -1 ScrollBar.vertical: ThemedScrollBar { } delegate: Rectangle { id: logRow required property string name required property string modified required property string modifiedCompact required property string sizeText width: fileList.width height: 42 radius: Theme.radiusMd color: logsPage.selectedName === name ? Theme.alpha(Theme.accent, Theme.dark ? 0.2 : 0.12) : rowMouse.containsMouse ? Theme.alpha(Theme.bgRaised, 0.76) : "transparent" border.width: logsPage.selectedName === name ? 1 : 0 border.color: Theme.alpha(Theme.accent, 0.42) RowLayout { anchors.fill: parent anchors.leftMargin: Theme.spacingMd anchors.rightMargin: Theme.spacingSm spacing: Theme.spacingMd Text { text: logRow.name color: Theme.textPrimary font.pixelSize: Theme.fontBody font.weight: Font.Medium elide: Text.ElideRight Layout.fillWidth: true } Text { text: logRow.sizeText + " · " + logRow.modifiedCompact color: Theme.textMuted font.pixelSize: Theme.fontSmall } ThemedButton { implicitWidth: 74 text: qsTr("Save…") enabled: !logsPage.saving Accessible.name: qsTr("Save %1").arg(logRow.name) onClicked: logsPage.saveLog(logRow.name) } } MouseArea { id: rowMouse anchors.fill: parent anchors.rightMargin: 80 hoverEnabled: true cursorShape: Qt.PointingHandCursor Accessible.role: Accessible.ListItem Accessible.name: logRow.name onClicked: logsPage.selectLog(logRow.name) } } } EmptyState { anchors.centerIn: parent visible: logsPage.hasLoaded && !logsPage.loading && logsPage.totalCount === 0 text: qsTr("No log files found") hint: qsTr("The server did not report any downloadable logs.") } } Rectangle { Layout.fillWidth: true Layout.fillHeight: true radius: Theme.radiusLg color: Theme.alpha(Theme.bgSurface, Theme.dark ? 0.64 : 0.94) border.width: 1 border.color: Theme.alpha(Theme.borderSoft, 0.7) ColumnLayout { anchors.fill: parent spacing: 0 RowLayout { Layout.fillWidth: true Layout.leftMargin: Theme.spacingLg Layout.rightMargin: Theme.spacingMd Layout.topMargin: Theme.spacingMd Layout.bottomMargin: Theme.spacingMd spacing: Theme.spacingMd ColumnLayout { Layout.fillWidth: true spacing: Theme.spacingXs Text { text: logsPage.selectedName.length > 0 ? logsPage.selectedName : qsTr("No log file selected") color: Theme.textPrimary font.pixelSize: Theme.fontHeading font.weight: Font.DemiBold elide: Text.ElideMiddle Layout.fillWidth: true } Text { text: logsPage.selectedMeta color: Theme.textMuted font.pixelSize: Theme.fontSmall elide: Text.ElideRight Layout.fillWidth: true } } ThemedButton { visible: logsPage.selectedName.length > 0 text: logsPage.saving ? qsTr("Saving…") : qsTr("Save as…") enabled: !logsPage.saving && !logsPage.contentLoading onClicked: logsPage.saveLog(logsPage.selectedName) } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 color: Theme.borderSoft } ScrollView { id: contentScroll visible: logsPage.selectedName.length > 0 && !logsPage.contentLoading && logsPage.contentError.length === 0 Layout.fillWidth: true Layout.fillHeight: true clip: true TextArea { width: contentScroll.availableWidth text: logsPage.content readOnly: true selectByMouse: true wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere color: Theme.textBody selectionColor: Theme.accent selectedTextColor: "white" font.family: "monospace" font.pixelSize: Theme.fontSmall padding: Theme.spacingLg Accessible.name: qsTr("Log contents") background: Rectangle { color: "transparent" } } ScrollBar.vertical: ThemedScrollBar { } } LoadingState { visible: logsPage.contentLoading Layout.fillWidth: true Layout.fillHeight: true title: qsTr("Loading log") message: logsPage.selectedName } ErrorState { visible: !logsPage.contentLoading && logsPage.contentError.length > 0 Layout.fillWidth: true Layout.fillHeight: true title: qsTr("Could not load log") message: logsPage.contentError retryText: qsTr("Try again") onRetryRequested: logsPage.selectLog(logsPage.selectedName) } EmptyState { visible: logsPage.selectedName.length === 0 && logsPage.totalCount > 0 Layout.fillWidth: true Layout.fillHeight: true text: qsTr("Select a log file") hint: qsTr("Choose a file above to inspect its contents.") } } } } LoadingState { anchors.fill: parent visible: logsPage.loading && !logsPage.hasLoaded title: qsTr("Loading logs") message: qsTr("Fetching available server log files…") } ErrorState { anchors.fill: parent visible: !logsPage.loading && !logsPage.hasLoaded && logsPage.errorMessage.length > 0 title: qsTr("Could not load logs") message: logsPage.errorMessage onRetryRequested: logsPage.refresh() } ToastHost { id: toastHost anchors.fill: parent } Connections { function onToastRequested(message, kind) { toastHost.show(message, kind); } target: logsPage } }