From 7a88a79c4bb6cffd9053cfa67f69e69f0d2d9c6e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Oct 2026 13:03:47 +0700 Subject: [PATCH 1/4] test: rename QML test doubles that shadow component properties qmllint in Qt 6.12 reports an id that has the same name as a property of the object it is assigned to (id-shadows-member). quick-check runs qmllint with -W 0, so the Arch package build, which now gets Qt 6.12, failed on five test doubles such as `controller: controller`. Name them with the Mock suffix the other QML tests already use. --- tests/quick/qml/tst_mainlifecycleguard.qml | 8 +- tests/quick/qml/tst_mediaeditorlayout.qml | 226 +++++++++--------- .../quick/qml/tst_mediaexportpickerlayout.qml | 22 +- .../tst_supportbundleexportpickerlayout.qml | 26 +- 4 files changed, 141 insertions(+), 141 deletions(-) diff --git a/tests/quick/qml/tst_mainlifecycleguard.qml b/tests/quick/qml/tst_mainlifecycleguard.qml index ab3913e..b3c112a 100644 --- a/tests/quick/qml/tst_mainlifecycleguard.qml +++ b/tests/quick/qml/tst_mainlifecycleguard.qml @@ -14,7 +14,7 @@ TestCase { height: 760 ListModel { - id: mediaModel + id: mediaModelMock ListElement { mediaName: "one.mp4.h264_2240x1080" mediaId: "media-one" @@ -37,7 +37,7 @@ TestCase { } ListModel { - id: operationModel + id: operationModelMock } QtObject { @@ -115,8 +115,8 @@ TestCase { property var savedLayoutModel: [] property string metricsAlignment: "Left" property string metricsColor: "#dcdcdc" - property var mediaModel: mediaModel - property var operationModel: operationModel + property var mediaModel: mediaModelMock + property var operationModel: operationModelMock property int fullSubmitCount: 0 property int splitSubmitCount: 0 property string lastSubmissionId: "" diff --git a/tests/quick/qml/tst_mediaeditorlayout.qml b/tests/quick/qml/tst_mediaeditorlayout.qml index b6f6a21..c00955b 100644 --- a/tests/quick/qml/tst_mediaeditorlayout.qml +++ b/tests/quick/qml/tst_mediaeditorlayout.qml @@ -18,7 +18,7 @@ TestCase { visible: true QtObject { - id: controller + id: controllerMock property bool open: false property bool busy: false @@ -62,59 +62,59 @@ TestCase { Components.MediaEditor { id: editor - controller: controller + controller: controllerMock } } function init() { host.width = 1060 host.height = 700 - controller.recoveredDeviceCopy = false - controller.sourceKind = "LocalMedia" - controller.originalMediaName = "" - controller.replaceAllowed = false - controller.replaceBlockReason = "" - controller.submissionPending = false - controller.submissionAction = "" - controller.targetWidth = 2240 - controller.targetHeight = 1080 - controller.mode = "Fit" - controller.zoomPercent = 100 - controller.focusX = 5000 - controller.focusY = 5000 - controller.rotation = 0 - controller.backgroundColor = "#000000" - controller.preparationTarget = "FullFrame" - controller.splitTargetAvailable = true - controller.deviceCopyMetadataStatus = "NotSupported" - controller.deviceCopyDimensionsAvailable = false - controller.deviceCopyWidth = 0 - controller.deviceCopyHeight = 0 - controller.deviceCopyDurationAvailable = false - controller.deviceCopyDurationMilliseconds = 0 - controller.deviceCopyFrameRateAvailable = false - controller.deviceCopyFrameRateNumerator = 0 - controller.deviceCopyFrameRateDenominator = 0 - controller.open = false + controllerMock.recoveredDeviceCopy = false + controllerMock.sourceKind = "LocalMedia" + controllerMock.originalMediaName = "" + controllerMock.replaceAllowed = false + controllerMock.replaceBlockReason = "" + controllerMock.submissionPending = false + controllerMock.submissionAction = "" + controllerMock.targetWidth = 2240 + controllerMock.targetHeight = 1080 + controllerMock.mode = "Fit" + controllerMock.zoomPercent = 100 + controllerMock.focusX = 5000 + controllerMock.focusY = 5000 + controllerMock.rotation = 0 + controllerMock.backgroundColor = "#000000" + controllerMock.preparationTarget = "FullFrame" + controllerMock.splitTargetAvailable = true + controllerMock.deviceCopyMetadataStatus = "NotSupported" + controllerMock.deviceCopyDimensionsAvailable = false + controllerMock.deviceCopyWidth = 0 + controllerMock.deviceCopyHeight = 0 + controllerMock.deviceCopyDurationAvailable = false + controllerMock.deviceCopyDurationMilliseconds = 0 + controllerMock.deviceCopyFrameRateAvailable = false + controllerMock.deviceCopyFrameRateNumerator = 0 + controllerMock.deviceCopyFrameRateDenominator = 0 + controllerMock.open = false wait(0) - controller.open = true + controllerMock.open = true tryVerify(() => editor.opened) } function setReadyDeviceCopyMetadata() { - controller.deviceCopyMetadataStatus = "Ready" - controller.deviceCopyDimensionsAvailable = true - controller.deviceCopyWidth = 2240 - controller.deviceCopyHeight = 1080 - controller.deviceCopyDurationAvailable = true - controller.deviceCopyDurationMilliseconds = 60000 - controller.deviceCopyFrameRateAvailable = true - controller.deviceCopyFrameRateNumerator = 30 - controller.deviceCopyFrameRateDenominator = 1 + controllerMock.deviceCopyMetadataStatus = "Ready" + controllerMock.deviceCopyDimensionsAvailable = true + controllerMock.deviceCopyWidth = 2240 + controllerMock.deviceCopyHeight = 1080 + controllerMock.deviceCopyDurationAvailable = true + controllerMock.deviceCopyDurationMilliseconds = 60000 + controllerMock.deviceCopyFrameRateAvailable = true + controllerMock.deviceCopyFrameRateNumerator = 30 + controllerMock.deviceCopyFrameRateDenominator = 1 } function cleanup() { - controller.open = false + controllerMock.open = false wait(0) } @@ -141,8 +141,8 @@ TestCase { {"width": 1280, "height": 720} ] for (const profile of profiles) { - controller.targetWidth = profile.width - controller.targetHeight = profile.height + controllerMock.targetWidth = profile.width + controllerMock.targetHeight = profile.height wait(0) const expectedRatio = profile.width / profile.height @@ -209,9 +209,9 @@ TestCase { split.forceActiveFocus() tryVerify(() => split.activeFocus) keyClick(Qt.Key_Space) - compare(controller.preparationTarget, "SplitArea") - controller.targetWidth = 1120 - controller.targetHeight = 1080 + compare(controllerMock.preparationTarget, "SplitArea") + controllerMock.targetWidth = 1120 + controllerMock.targetHeight = 1080 wait(0) verify(!full.checked) @@ -225,14 +225,14 @@ TestCase { verify(resolution.text.indexOf("Split area") >= 0) verify(resolution.text.indexOf("1120 × 1080") >= 0) - controller.submissionPending = true + controllerMock.submissionPending = true wait(0) verify(!full.enabled) verify(!split.enabled) } function test_unavailableSplitTargetHasVisibleAccessibleReason() { - controller.splitTargetAvailable = false + controllerMock.splitTargetAvailable = false wait(0) const full = findChild(editor, "fullFramePreparationTarget") @@ -252,7 +252,7 @@ TestCase { } function test_preparationTargetSelectorReflowsAtNarrowWidth() { - controller.splitTargetAvailable = false + controllerMock.splitTargetAvailable = false host.width = 360 host.height = 640 wait(0) @@ -309,36 +309,36 @@ TestCase { verify(horizontal !== null) verify(vertical !== null) - controller.mode = "Stretch" + controllerMock.mode = "Stretch" wait(0) verify(description.text.indexOf("distorted") >= 0) - controller.mode = "Crop" - controller.zoomPercent = 100 - controller.focusX = 2500 - controller.focusY = 7500 + controllerMock.mode = "Crop" + controllerMock.zoomPercent = 100 + controllerMock.focusX = 2500 + controllerMock.focusY = 7500 wait(0) compare(horizontal.value, 25) compare(vertical.value, 75) verify(!horizontal.enabled) verify(!vertical.enabled) - controller.zoomPercent = 160 + controllerMock.zoomPercent = 160 wait(0) verify(horizontal.enabled) verify(vertical.enabled) - controller.mode = "Fit" + controllerMock.mode = "Fit" wait(0) verify(!horizontal.enabled) verify(!vertical.enabled) } function test_recoveredCopyUsesExplicitActions() { - controller.recoveredDeviceCopy = true - controller.sourceKind = "RecoveredDeviceCopy" - controller.originalMediaName = "device-video.h264_2240x1080" - controller.replaceAllowed = true + controllerMock.recoveredDeviceCopy = true + controllerMock.sourceKind = "RecoveredDeviceCopy" + controllerMock.originalMediaName = "device-video.h264_2240x1080" + controllerMock.replaceAllowed = true setReadyDeviceCopyMetadata() wait(0) @@ -372,15 +372,15 @@ TestCase { verify(replace.visible) verify(replace.enabled) - controller.targetWidth = 1280 - controller.targetHeight = 720 + controllerMock.targetWidth = 1280 + controllerMock.targetHeight = 720 wait(0) verify(noticeText.text.indexOf("2240") >= 0) verify(noticeText.text.indexOf("1080") >= 0) verify(noticeText.text.indexOf("1280") < 0) - controller.submissionPending = true - controller.submissionAction = "Replace" + controllerMock.submissionPending = true + controllerMock.submissionAction = "Replace" wait(0) verify(!saveAsNew.enabled) verify(!replace.enabled) @@ -388,8 +388,8 @@ TestCase { } function test_actionRowRemainsVisibleInShortWindow() { - controller.recoveredDeviceCopy = true - controller.replaceAllowed = true + controllerMock.recoveredDeviceCopy = true + controllerMock.replaceAllowed = true wait(0) const content = @@ -412,10 +412,10 @@ TestCase { } function test_recoveredCopyWarnsOnlyForNeutralGeometry() { - controller.recoveredDeviceCopy = true - controller.mode = "Crop" - controller.zoomPercent = 100 - controller.rotation = 0 + controllerMock.recoveredDeviceCopy = true + controllerMock.mode = "Crop" + controllerMock.zoomPercent = 100 + controllerMock.rotation = 0 wait(0) const noticeText = @@ -425,15 +425,15 @@ TestCase { verify(noticeText.text.indexOf( "will not visibly change") >= 0) - controller.zoomPercent = 160 + controllerMock.zoomPercent = 160 wait(0) verify(!editor.recoveredTransformIsGeometryNeutral) verify(noticeText.text.indexOf( "Save as new stores") >= 0) - controller.mode = "Stretch" - controller.zoomPercent = 100 - controller.rotation = 180 + controllerMock.mode = "Stretch" + controllerMock.zoomPercent = 100 + controllerMock.rotation = 180 wait(0) verify(!editor.recoveredTransformIsGeometryNeutral) } @@ -474,21 +474,21 @@ TestCase { } function test_deviceCopyMetadataStates(data) { - controller.recoveredDeviceCopy = true - controller.sourceKind = "RecoveredDeviceCopy" - controller.originalMediaName = + controllerMock.recoveredDeviceCopy = true + controllerMock.sourceKind = "RecoveredDeviceCopy" + controllerMock.originalMediaName = "device-copy.mp4.h264_1280x720" - controller.deviceCopyMetadataStatus = data.status - controller.deviceCopyDimensionsAvailable = data.dimensions - controller.deviceCopyWidth = data.dimensions ? 2240 : 0 - controller.deviceCopyHeight = data.dimensions ? 1080 : 0 - controller.deviceCopyDurationAvailable = data.duration - controller.deviceCopyDurationMilliseconds = + controllerMock.deviceCopyMetadataStatus = data.status + controllerMock.deviceCopyDimensionsAvailable = data.dimensions + controllerMock.deviceCopyWidth = data.dimensions ? 2240 : 0 + controllerMock.deviceCopyHeight = data.dimensions ? 1080 : 0 + controllerMock.deviceCopyDurationAvailable = data.duration + controllerMock.deviceCopyDurationMilliseconds = data.duration ? 60000 : 0 - controller.deviceCopyFrameRateAvailable = data.frameRate - controller.deviceCopyFrameRateNumerator = + controllerMock.deviceCopyFrameRateAvailable = data.frameRate + controllerMock.deviceCopyFrameRateNumerator = data.frameRate ? 30 : 0 - controller.deviceCopyFrameRateDenominator = + controllerMock.deviceCopyFrameRateDenominator = data.frameRate ? 1 : 0 wait(0) @@ -535,8 +535,8 @@ TestCase { } function test_deviceCopyMetadataIsHiddenForLocalMedia() { - controller.recoveredDeviceCopy = false - controller.sourceKind = "LocalMedia" + controllerMock.recoveredDeviceCopy = false + controllerMock.sourceKind = "LocalMedia" setReadyDeviceCopyMetadata() wait(0) @@ -546,10 +546,10 @@ TestCase { } function test_deviceCopyMetadataFormatsLongDuration() { - controller.recoveredDeviceCopy = true - controller.sourceKind = "RecoveredDeviceCopy" + controllerMock.recoveredDeviceCopy = true + controllerMock.sourceKind = "RecoveredDeviceCopy" setReadyDeviceCopyMetadata() - controller.deviceCopyDurationMilliseconds = 3723000 + controllerMock.deviceCopyDurationMilliseconds = 3723000 wait(0) const duration = findChild(editor, "deviceCopyDurationValue") @@ -562,12 +562,12 @@ TestCase { } function test_recoveredResolutionUsesProbeNotFilenameOrTarget() { - controller.recoveredDeviceCopy = true - controller.sourceKind = "RecoveredDeviceCopy" - controller.originalMediaName = + controllerMock.recoveredDeviceCopy = true + controllerMock.sourceKind = "RecoveredDeviceCopy" + controllerMock.originalMediaName = "misleading-name.mp4.h264_1280x720" - controller.targetWidth = 800 - controller.targetHeight = 480 + controllerMock.targetWidth = 800 + controllerMock.targetHeight = 480 setReadyDeviceCopyMetadata() wait(0) @@ -581,10 +581,10 @@ TestCase { verify(noticeText.text.indexOf("800") < 0) verify(noticeText.text.indexOf("480") < 0) - controller.deviceCopyMetadataStatus = "Partial" - controller.deviceCopyDimensionsAvailable = false - controller.deviceCopyWidth = 0 - controller.deviceCopyHeight = 0 + controllerMock.deviceCopyMetadataStatus = "Partial" + controllerMock.deviceCopyDimensionsAvailable = false + controllerMock.deviceCopyWidth = 0 + controllerMock.deviceCopyHeight = 0 wait(0) verify(noticeText.text.indexOf("1280") < 0) verify(noticeText.text.indexOf("720") < 0) @@ -593,9 +593,9 @@ TestCase { } function test_deviceCopyMetadataIsNarrowAndAccessible() { - controller.recoveredDeviceCopy = true - controller.sourceKind = "RecoveredDeviceCopy" - controller.originalMediaName = "device-copy.h264" + controllerMock.recoveredDeviceCopy = true + controllerMock.sourceKind = "RecoveredDeviceCopy" + controllerMock.originalMediaName = "device-copy.h264" setReadyDeviceCopyMetadata() host.width = 360 host.height = 640 @@ -635,9 +635,9 @@ TestCase { } function test_deviceCopyMetadataRussianTranslation() { - controller.recoveredDeviceCopy = true - controller.sourceKind = "RecoveredDeviceCopy" - controller.originalMediaName = "device-copy.h264" + controllerMock.recoveredDeviceCopy = true + controllerMock.sourceKind = "RecoveredDeviceCopy" + controllerMock.originalMediaName = "device-copy.h264" setReadyDeviceCopyMetadata() wait(0) @@ -671,24 +671,24 @@ TestCase { compare(frameRateLabel.text, "Частота кадров") compare(frameRate.text, "30 FPS") - controller.deviceCopyMetadataStatus = "Loading" + controllerMock.deviceCopyMetadataStatus = "Loading" wait(0) compare(state.text, "Загрузка") - controller.deviceCopyMetadataStatus = "Partial" + controllerMock.deviceCopyMetadataStatus = "Partial" wait(0) compare(state.text, "Частично") - controller.deviceCopyMetadataStatus = "Unavailable" + controllerMock.deviceCopyMetadataStatus = "Unavailable" wait(0) compare(state.text, "Недоступно") - controller.deviceCopyMetadataStatus = "NotSupported" + controllerMock.deviceCopyMetadataStatus = "NotSupported" wait(0) compare(state.text, "Не поддерживается") } function test_preparationTargetRussianTranslation() { - controller.preparationTarget = "SplitArea" - controller.targetWidth = 1120 - controller.targetHeight = 1080 + controllerMock.preparationTarget = "SplitArea" + controllerMock.targetWidth = 1120 + controllerMock.targetHeight = 1080 wait(0) const group = findChild(editor, "mediaPreparationTargetGroup") @@ -724,7 +724,7 @@ TestCase { resolution.text, "Область разделения: 1120 × 1080") - controller.splitTargetAvailable = false + controllerMock.splitTargetAvailable = false wait(0) verify(unavailable.visible) compare( diff --git a/tests/quick/qml/tst_mediaexportpickerlayout.qml b/tests/quick/qml/tst_mediaexportpickerlayout.qml index b211562..923ab01 100644 --- a/tests/quick/qml/tst_mediaexportpickerlayout.qml +++ b/tests/quick/qml/tst_mediaexportpickerlayout.qml @@ -18,7 +18,7 @@ TestCase { visible: true QtObject { - id: workflow + id: workflowMock property string exportedMediaId: "" property string exportedMediaName: "" @@ -38,7 +38,7 @@ TestCase { Components.MediaExportPicker { id: picker - workflow: workflow + workflow: workflowMock homeFolder: Qt.resolvedUrl(".") } QtObject { @@ -55,9 +55,9 @@ TestCase { function init() { chooser.cancel() picker.portalChooser = null - workflow.exportedMediaId = "" - workflow.exportedMediaName = "" - workflow.exportedFileName = "" + workflowMock.exportedMediaId = "" + workflowMock.exportedMediaName = "" + workflowMock.exportedFileName = "" picker.close() wait(0) } @@ -71,10 +71,10 @@ TestCase { chooser.busy = false chooser.selected(Qt.resolvedUrl(".")) tryVerify(() => picker.opened) - compare(workflow.exportedMediaId, "") + compare(workflowMock.exportedMediaId, "") picker.exportCopy() - tryCompare(workflow, "exportedMediaId", "original-id") - compare(workflow.exportedMediaName, "original-media") + tryCompare(workflowMock, "exportedMediaId", "original-id") + compare(workflowMock.exportedMediaName, "original-media") } function test_suggestedNameAndExplicitExport() { @@ -91,9 +91,9 @@ TestCase { verify(exportButton.enabled) mouseClick(exportButton) - tryCompare(workflow, "exportedMediaId", "media-id") - compare(workflow.exportedMediaName, "device-media") - compare(workflow.exportedFileName, + tryCompare(workflowMock, "exportedMediaId", "media-id") + compare(workflowMock.exportedMediaName, "device-media") + compare(workflowMock.exportedFileName, "suggested-device-copy.h264") } } diff --git a/tests/quick/qml/tst_supportbundleexportpickerlayout.qml b/tests/quick/qml/tst_supportbundleexportpickerlayout.qml index dadf30c..652c59a 100644 --- a/tests/quick/qml/tst_supportbundleexportpickerlayout.qml +++ b/tests/quick/qml/tst_supportbundleexportpickerlayout.qml @@ -23,7 +23,7 @@ TestCase { } QtObject { - id: controller + id: controllerMock property bool busy: false property int exportCount: 0 property url exportedFolder @@ -45,7 +45,7 @@ TestCase { Components.SupportBundleExportPicker { id: picker - controller: controller + controller: controllerMock homeFolder: Qt.resolvedUrl("..") focusReturnItem: launcher } @@ -56,9 +56,9 @@ TestCase { picker.portalChooser = null host.requestActivate() tryVerify(() => host.active) - controller.busy = false - controller.exportCount = 0 - controller.exportedFolder = "" + controllerMock.busy = false + controllerMock.exportCount = 0 + controllerMock.exportedFolder = "" picker.close() picker.currentFolder = picker.homeFolder wait(0) @@ -71,10 +71,10 @@ TestCase { const selectedFolder = Qt.resolvedUrl(".") chooser.busy = false chooser.selected(selectedFolder) - compare(controller.exportCount, 0) + compare(controllerMock.exportCount, 0) picker.currentFolder = picker.homeFolder - tryCompare(controller, "exportCount", 1) - compare(controller.exportedFolder, selectedFolder) + tryCompare(controllerMock, "exportCount", 1) + compare(controllerMock.exportedFolder, selectedFolder) } function test_folderOnlyExportAndAccessibility() { @@ -116,8 +116,8 @@ TestCase { saveButton.forceActiveFocus() tryVerify(() => saveButton.activeFocus) keyClick(Qt.Key_Space) - tryCompare(controller, "exportCount", 1) - compare(String(controller.exportedFolder), + tryCompare(controllerMock, "exportCount", 1) + compare(String(controllerMock.exportedFolder), String(picker.currentFolder)) tryVerify(() => launcher.activeFocus) } @@ -135,16 +135,16 @@ TestCase { tryVerify(() => !saveButton.enabled) saveButton.forceActiveFocus() keyClick(Qt.Key_Space) - compare(controller.exportCount, 0) + compare(controllerMock.exportCount, 0) picker.navigate(picker.homeFolder) tryVerify(() => saveButton.enabled) - controller.busy = true + controllerMock.busy = true tryVerify(() => !saveButton.enabled) verify(!saveButton.enabled) keyClick(Qt.Key_Escape) tryVerify(() => !picker.opened) tryVerify(() => launcher.activeFocus) - compare(controller.exportCount, 0) + compare(controllerMock.exportCount, 0) } } From d4800a352d6ee394490898834aca8185e426e575 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Oct 2026 13:03:47 +0700 Subject: [PATCH 2/4] ci: build the Nix flake on every change Build the flake added in #33 with a pinned install-nix-action and check that its output has both binaries at the release version, the runtime, the udev rules, the user unit pointing at the store path with KillMode=mixed, and an ffmpeg with libx264 on the runtime's PATH. Tagged releases skip the job, so a change in nixpkgs cannot block a release build. --- .github/workflows/package-linux.yml | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml index 9e6ea64..a83ea7e 100644 --- a/.github/workflows/package-linux.yml +++ b/.github/workflows/package-linux.yml @@ -689,3 +689,46 @@ jobs: ffmpeg -hide_banner -encoders \ > "$RUNNER_TEMP/ffmpeg-encoders.txt" 2>&1 grep -qw libx264 "$RUNNER_TEMP/ffmpeg-encoders.txt" + + nix: + name: Nix flake + # Tagged releases build only the published package formats; a change in + # nixpkgs must not block them. + if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Nix + uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Build the flake and check its output + shell: bash + run: | + set -euo pipefail + nix build .#default --print-build-logs + version=$(tr -d '\r\n' < VERSION) + test "$(result/bin/tryx --version)" = "tryx $version" + test "$(result/bin/tryx-panorama-manager --version)" = \ + "tryx-panorama-manager $version" + test -x result/lib/tryx-panorama-manager/tryx-panorama-runtime + test -f result/lib/udev/rules.d/70-tryx-pase-access.rules + test -f result/lib/udev/rules.d/99-tryx-pase-printer.rules + unit=result/share/systemd/user/tryx-panorama.service + grep -qx "ExecStart=$(readlink -f result)/lib/tryx-panorama-manager/tryx-panorama-runtime" "$unit" + grep -qx 'KillMode=mixed' "$unit" + # The runtime finds ffmpeg through PATH; its wrapper must provide + # one with the libx264 encoder that media preparation needs. + ffmpeg_bin=$(grep -a -o '/nix/store/[^:]*-ffmpeg-[^:]*/bin' \ + result/lib/tryx-panorama-manager/tryx-panorama-runtime | sort -u) + test "$(printf '%s\n' "$ffmpeg_bin" | wc -l)" -eq 1 + "$ffmpeg_bin/ffmpeg" -hide_banner -encoders \ + > "$RUNNER_TEMP/ffmpeg-encoders.txt" 2>&1 + grep -qw libx264 "$RUNNER_TEMP/ffmpeg-encoders.txt" From 4a74d453756edcb785d95a26cb9847fd31ec9c77 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Oct 2026 13:03:47 +0700 Subject: [PATCH 3/4] docs: describe installing the Nix flake on NixOS Add the flake input and the NixOS configuration that enables the package, its udev rules and the user service, based on the configuration the flake contributor tested with a display. --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 98c86e0..6687b2b 100644 --- a/README.md +++ b/README.md @@ -651,6 +651,36 @@ makepkg --cleanbuild --check popd ``` +## NixOS + +The repository is a Nix flake, contributed by @Yuna404. It builds the same +files as the native packages: the GUI, the `tryx` client, the runtime with +ffmpeg on its `PATH`, the udev rules and the user service. CI builds it on +every change; NixOS itself is community-tested. The flake ships with releases +after 2.5.3. `github:DXVSI/Tryx-Linux-GUI` follows the latest release; add a +release tag to the URL to pin one. + +Add the flake as an input: + +```nix +tryx.url = "github:DXVSI/Tryx-Linux-GUI"; +``` + +Then enable the package, its udev rules and the user service in the NixOS +configuration. Without the udev rules the application cannot open the display. + +```nix +{ inputs, pkgs, ... }: +{ + nixpkgs.overlays = [ inputs.tryx.overlays.default ]; + + environment.systemPackages = [ pkgs.tryx-panorama-manager ]; + services.udev.packages = [ pkgs.tryx-panorama-manager ]; + systemd.packages = [ pkgs.tryx-panorama-manager ]; + systemd.user.services.tryx-panorama.wantedBy = [ "graphical-session.target" ]; +} +``` + ## Requirements **Build:** From a29a9e46052f72d757532670d6dc4da6d61f90ed Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Oct 2026 13:25:18 +0700 Subject: [PATCH 4/4] test: rename the Panorama layout test doubles as well The Panorama layout test has the same shadowing ids. Qt 6.12 qmllint did not print its warnings when it linted all files together and only exited with 255, so the first fix missed it. All QML files now lint clean with Qt 6.12 qmllint -W 0, and the QML tests pass under Qt 6.12. --- tests/quick/qml/tst_panoramalayout.qml | 48 +++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/quick/qml/tst_panoramalayout.qml b/tests/quick/qml/tst_panoramalayout.qml index fb95dc0..6066ea9 100644 --- a/tests/quick/qml/tst_panoramalayout.qml +++ b/tests/quick/qml/tst_panoramalayout.qml @@ -14,7 +14,7 @@ TestCase { height: 760 ListModel { - id: mediaModel + id: mediaModelMock function canDelete(mediaName) { return mediaName.length > 0 @@ -65,7 +65,7 @@ TestCase { } ListModel { - id: operationModel + id: operationModelMock ListElement { operationId: "one" subject: "Upload" @@ -97,8 +97,8 @@ TestCase { property string activeOperationId: "" property string operationSummary: "" property real operationProgress: 0 - property var mediaModel: mediaModel - property var operationModel: operationModel + property var mediaModel: mediaModelMock + property var operationModel: operationModelMock property var availableMetrics: [ "CPU Temperature", "GPU Temperature" ] @@ -427,19 +427,19 @@ TestCase { } function init() { - runtimeMock.mediaModel = mediaModel - mediaModel.setProperty(0, "mediaName", + runtimeMock.mediaModel = mediaModelMock + mediaModelMock.setProperty(0, "mediaName", "one.mp4.h264_2240x1080") - mediaModel.setProperty(0, "mediaSize", 1024) - mediaModel.setProperty(0, "mediaSource", 1) - mediaModel.setProperty(0, "managedOrigin", true) - mediaModel.setProperty(0, "readOnly", false) - mediaModel.setProperty(1, "mediaName", + mediaModelMock.setProperty(0, "mediaSize", 1024) + mediaModelMock.setProperty(0, "mediaSource", 1) + mediaModelMock.setProperty(0, "managedOrigin", true) + mediaModelMock.setProperty(0, "readOnly", false) + mediaModelMock.setProperty(1, "mediaName", "two.mp4.h264_2240x1080") - mediaModel.setProperty(1, "mediaSize", 2048) - mediaModel.setProperty(1, "mediaSource", 2) - mediaModel.setProperty(1, "managedOrigin", false) - mediaModel.setProperty(1, "readOnly", true) + mediaModelMock.setProperty(1, "mediaSize", 2048) + mediaModelMock.setProperty(1, "mediaSource", 2) + mediaModelMock.setProperty(1, "managedOrigin", false) + mediaModelMock.setProperty(1, "readOnly", true) runtimeMock.legacyConnected = false runtimeMock.currentScreenMode = "Full Screen" runtimeMock.currentPlayMode = "Single" @@ -901,26 +901,26 @@ TestCase { verify(origin !== null) verify(size !== null) - mediaModel.setProperty(0, "mediaSource", 0) - mediaModel.setProperty(0, "mediaSize", 0) + mediaModelMock.setProperty(0, "mediaSource", 0) + mediaModelMock.setProperty(0, "mediaSize", 0) wait(0) compare(origin.text, "Unknown origin") compare(size.text, "Unknown size") - mediaModel.setProperty(0, "mediaSource", 99) + mediaModelMock.setProperty(0, "mediaSource", 99) wait(0) compare(origin.text, "Unknown origin") - mediaModel.setProperty(0, "mediaSource", 1) + mediaModelMock.setProperty(0, "mediaSource", 1) runtimeMock.legacyConnected = true wait(0) compare(origin.text, "Unknown origin") runtimeMock.legacyConnected = false - mediaModel.setProperty(0, "readOnly", true) - mediaModel.setProperty(0, "managedOrigin", false) - mediaModel.setProperty(1, "readOnly", false) - mediaModel.setProperty(1, "managedOrigin", true) + mediaModelMock.setProperty(0, "readOnly", true) + mediaModelMock.setProperty(0, "managedOrigin", false) + mediaModelMock.setProperty(1, "readOnly", false) + mediaModelMock.setProperty(1, "managedOrigin", true) wait(0) compare(origin.text, "User media") compare(findChild(page, "mediaOriginLabel-media-two").text, @@ -931,7 +931,7 @@ TestCase { const longName = "a-very-long-user-media-name-that-must-remain-accessible" + ".mp4.h264_2240x1080" - mediaModel.setProperty(0, "mediaName", longName) + mediaModelMock.setProperty(0, "mediaName", longName) const page = createTemporaryObject( panoramaComponent, testCase,