Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package vermouth for openSUSE:Factory checked in at 2026-08-15 22:40:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/vermouth (Old) and /work/SRC/openSUSE:Factory/.vermouth.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "vermouth" Sat Aug 15 22:40:55 2026 rev:22 rq:1371264 version:2.0.2 Changes: -------- --- /work/SRC/openSUSE:Factory/vermouth/vermouth.changes 2026-08-09 21:37:54.523491343 +0200 +++ /work/SRC/openSUSE:Factory/.vermouth.new.1258/vermouth.changes 2026-08-15 22:41:24.088223887 +0200 @@ -1,0 +2,9 @@ +Fri Aug 14 21:46:23 UTC 2026 - Jehu Marcos Herrera Puentes <[email protected]> + +- Bump to version 2.0.2: + * fix(runtime): Fixes for missing runtimes and download + * feat: Playtime + * Update KDE runtime on the flatpack + * Update sha/commits + +------------------------------------------------------------------- Old: ---- vermouth-2.0.1.tar.xz New: ---- vermouth-2.0.2.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ vermouth.spec ++++++ --- /var/tmp/diff_new_pack.knV2Gu/_old 2026-08-15 22:41:24.829249972 +0200 +++ /var/tmp/diff_new_pack.knV2Gu/_new 2026-08-15 22:41:24.831250042 +0200 @@ -17,7 +17,7 @@ Name: vermouth -Version: 2.0.1 +Version: 2.0.2 Release: 0 Summary: A Wine/Proton game launcher for KDE License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.knV2Gu/_old 2026-08-15 22:41:24.869251380 +0200 +++ /var/tmp/diff_new_pack.knV2Gu/_new 2026-08-15 22:41:24.873251520 +0200 @@ -3,7 +3,7 @@ <service name="tar_scm" mode="manual"> <param name="scm">git</param> <param name="url">https://github.com/dekomote/vermouth.git</param> - <param name="revision">v2.0.1</param> + <param name="revision">v2.0.2</param> <param name="match-tag">v[0-9]*</param> <param name="filename">vermouth</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.knV2Gu/_old 2026-08-15 22:41:24.905252647 +0200 +++ /var/tmp/diff_new_pack.knV2Gu/_new 2026-08-15 22:41:24.908252753 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/dekomote/vermouth.git</param> - <param name="changesrevision">2b13fc2e4eecac239df0f2ab4bba9b5749ddecbc</param></service></servicedata> + <param name="changesrevision">bd9d0f8d76ac1a966bb912afa027cdd4e06f645d</param></service></servicedata> (No newline at EOF) ++++++ vermouth-2.0.1.tar.xz -> vermouth-2.0.2.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/CMakeLists.txt new/vermouth-2.0.2/CMakeLists.txt --- old/vermouth-2.0.1/CMakeLists.txt 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/CMakeLists.txt 2026-08-11 12:07:42.000000000 +0200 @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(vermouth VERSION 2.0.1 LANGUAGES CXX) +project(vermouth VERSION 2.0.2 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -95,6 +95,7 @@ qml/GogView.qml qml/DownloaderProgress.qml qml/FlatpakHostHint.qml + qml/TimeUtils.js NO_RESOURCE_TARGET_PATH ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/assets/com.dekomote.vermouth.metainfo.xml.in new/vermouth-2.0.2/assets/com.dekomote.vermouth.metainfo.xml.in --- old/vermouth-2.0.1/assets/com.dekomote.vermouth.metainfo.xml.in 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/assets/com.dekomote.vermouth.metainfo.xml.in 2026-08-11 12:07:42.000000000 +0200 @@ -40,6 +40,11 @@ </screenshots> <content_rating type="oars-1.1" /> <releases> + <release version="2.0.2" date="2026-08-11"> + <description> + <p>This release adds the playtime feature, fixes GE Proton download url and adds a check when runtimes are missing.</p> + </description> + </release> <release version="2.0.1" date="2026-08-05"> <description> <p>This release adds the UZDOOM runtime, the Default Runtime feature, the Sort feature and several minor fixes/QoL addons.</p> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/com.dekomote.vermouth.flathub.yml new/vermouth-2.0.2/com.dekomote.vermouth.flathub.yml --- old/vermouth-2.0.1/com.dekomote.vermouth.flathub.yml 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/com.dekomote.vermouth.flathub.yml 2026-08-11 12:07:42.000000000 +0200 @@ -1,6 +1,6 @@ app-id: com.dekomote.vermouth runtime: org.kde.Platform -runtime-version: '6.10' +runtime-version: '6.11' sdk: org.kde.Sdk command: vermouth @@ -98,5 +98,5 @@ sources: - type: git url: https://github.com/dekomote/vermouth.git - tag: v2.0.1 - commit: 90b29349531f4294a18659b11c95d49cdfe421fb + tag: v2.0.2 + commit: 2b13fc2e4eecac239df0f2ab4bba9b5749ddecbc diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/com.dekomote.vermouth.yml new/vermouth-2.0.2/com.dekomote.vermouth.yml --- old/vermouth-2.0.1/com.dekomote.vermouth.yml 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/com.dekomote.vermouth.yml 2026-08-11 12:07:42.000000000 +0200 @@ -1,6 +1,6 @@ app-id: com.dekomote.vermouth runtime: org.kde.Platform -runtime-version: '6.10' +runtime-version: '6.11' sdk: org.kde.Sdk command: vermouth diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/flake.nix new/vermouth-2.0.2/flake.nix --- old/vermouth-2.0.1/flake.nix 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/flake.nix 2026-08-11 12:07:42.000000000 +0200 @@ -16,7 +16,7 @@ default = pkgs.stdenv.mkDerivation { pname = "vermouth"; dontWrapQtApps = true; - version = "2.0.1"; + version = "2.0.2"; src = ./.; nativeBuildInputs = with pkgs; [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/packaging/.SRCINFO new/vermouth-2.0.2/packaging/.SRCINFO --- old/vermouth-2.0.1/packaging/.SRCINFO 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/packaging/.SRCINFO 2026-08-11 12:07:42.000000000 +0200 @@ -1,6 +1,6 @@ pkgbase = vermouth pkgdesc = A game and app launcher for Linux - native, Windows, and retro - pkgver = 2.0.1 + pkgver = 2.0.2 pkgrel = 1 url = https://github.com/dekomote/vermouth arch = x86_64 @@ -19,7 +19,7 @@ optdepends = sdl2: gamepad support provides = vermouth conflicts = vermouth - source = vermouth-2.0.1.tar.gz::https://github.com/dekomote/vermouth/archive/refs/tags/v2.0.1.tar.gz - sha256sums = 6b4e90b986dddc4086f746801aab3e1fbe564af77fcbbaaffd4408a49143cc7d + source = vermouth-2.0.2.tar.gz::https://github.com/dekomote/vermouth/archive/refs/tags/v2.0.2.tar.gz + sha256sums = 60269ae19bc12f6004250899580a70afbce4fbc6c310bf3c634b3e29925ffb05 pkgname = vermouth diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/packaging/PKGBUILD new/vermouth-2.0.2/packaging/PKGBUILD --- old/vermouth-2.0.1/packaging/PKGBUILD 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/packaging/PKGBUILD 2026-08-11 12:07:42.000000000 +0200 @@ -1,6 +1,6 @@ # Maintainer: Dejan Noveski <[email protected]> pkgname=vermouth -pkgver=2.0.1 +pkgver=2.0.2 pkgrel=1 pkgdesc="A game and app launcher for Linux - native, Windows, and retro" arch=('x86_64') @@ -27,7 +27,7 @@ 'sdl2: gamepad support' ) source=("${pkgname}-${pkgver}.tar.gz::https://github.com/dekomote/vermouth/archive/refs/tags/v${pkgver}.tar.gz") -sha256sums=('6b4e90b986dddc4086f746801aab3e1fbe564af77fcbbaaffd4408a49143cc7d') +sha256sums=('60269ae19bc12f6004250899580a70afbce4fbc6c310bf3c634b3e29925ffb05') build() { cd "${pkgname}-${pkgver}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/packaging/vermouth.spec new/vermouth-2.0.2/packaging/vermouth.spec --- old/vermouth-2.0.1/packaging/vermouth.spec 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/packaging/vermouth.spec 2026-08-11 12:07:42.000000000 +0200 @@ -1,5 +1,5 @@ Name: vermouth -Version: 2.0.1 +Version: 2.0.2 Release: 1%{?dist} Summary: A no-frills Wine/Proton game launcher for KDE License: MIT @@ -74,6 +74,8 @@ %{_datadir}/locale/*/LC_MESSAGES/vermouth.mo %changelog +* Tue Aug 11 2026 Dejan Noveski <[email protected]> - 2.0.2-1 +- Play time support, runtime fixes * Wed Aug 5 2026 Dejan Noveski <[email protected]> - 2.0.1-1 - UZDOOM runtime, Default runtime, Sort * Wed Jul 15 2026 Dejan Noveski <[email protected]> - 1.9.7-1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/qml/AddAppDialog.qml new/vermouth-2.0.2/qml/AddAppDialog.qml --- old/vermouth-2.0.1/qml/AddAppDialog.qml 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/qml/AddAppDialog.qml 2026-08-11 12:07:42.000000000 +0200 @@ -3,6 +3,7 @@ import QtQuick.Layouts import QtQuick.Dialogs import org.kde.kirigami as Kirigami +import "TimeUtils.js" as TimeUtils Kirigami.Dialog { id: dialog @@ -58,6 +59,7 @@ logoField.text = ""; steamGridDbIdField.text = ""; steamIdField.text = ""; + playTimeField.text = ""; platformCombo.currentIndex = -1; artSection.expanded = false; pendingAutoDownload = false; @@ -152,6 +154,7 @@ logoField.text = app.logoPath || ""; steamGridDbIdField.text = app.steamGridDbId > 0 ? app.steamGridDbId.toString() : ""; steamIdField.text = app.steamAppId > 0 ? app.steamAppId.toString() : ""; + playTimeField.text = TimeUtils.formatPlayTime(app.playTime) || "0:00:00"; platformCombo.currentIndex = platformCombo.find(app.platformSlug); artSection.expanded = gridField.text !== "" || heroField.text !== "" || logoField.text !== ""; prefixBasePath = protonScanner.prefixBasePath(); @@ -196,6 +199,10 @@ validationError = i18n("Please select a platform."); return false; } + if (isNaN(TimeUtils.parsePlayTime(playTimeField.text))) { + validationError = i18n("Invalid play time. Use the format H:MM:SS."); + return false; + } validationError = ""; return true; } @@ -234,7 +241,8 @@ "launchOptions": launchOptionsField.text || "", "enableLogging": enableLoggingCheck.checked, "logoPath": logoField.text || "", - "steamGridDbId": sgdbId + "steamGridDbId": sgdbId, + "playTime": TimeUtils.parsePlayTime(playTimeField.text) }; if (editMode) { @@ -321,6 +329,14 @@ placeholderText: i18n("My Game") } + QQC2.TextField { + id: playTimeField + Layout.topMargin: 10 + Layout.fillWidth: true + Kirigami.FormData.label: i18n("Play Time (H:MM:SS):") + placeholderText: "0:00:00" + } + RowLayout { Layout.fillWidth: true visible: runtimePicker.runtimeType === "steam" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/qml/AppGridView.qml new/vermouth-2.0.2/qml/AppGridView.qml --- old/vermouth-2.0.1/qml/AppGridView.qml 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/qml/AppGridView.qml 2026-08-11 12:07:42.000000000 +0200 @@ -93,6 +93,7 @@ required property string uzdoomPath required property var uzdoomMods required property bool hidden + required property int playTime readonly property bool hasPrefix: runtimeType === "proton" || runtimeType === "wine" readonly property string resolvedRuntimeType: runtimeType === "default" ? settingsManager.defaultRuntimeType : runtimeType diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/qml/GameCardFrame.qml new/vermouth-2.0.2/qml/GameCardFrame.qml --- old/vermouth-2.0.1/qml/GameCardFrame.qml 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/qml/GameCardFrame.qml 2026-08-11 12:07:42.000000000 +0200 @@ -2,6 +2,7 @@ import QtQuick.Controls as QQC2 import QtQuick.Layouts import org.kde.kirigami as Kirigami +import "TimeUtils.js" as TimeUtils Item { id: frameRoot @@ -273,6 +274,39 @@ maximumLineCount: 2 } } + + Rectangle { + id: playTimeChip + visible: frameRoot.gv && frameRoot.gv.viewType !== "icon" && frameRoot.gv.showPlayTime && frameRoot.playTime > 0 + anchors.bottom: artNameOverlay.visible ? artNameOverlay.top : parent.bottom + anchors.right: parent.right + anchors.bottomMargin: Kirigami.Units.smallSpacing + anchors.rightMargin: Kirigami.Units.smallSpacing + height: playTimeLabel.implicitHeight + Kirigami.Units.smallSpacing * 2 + width: playTimeRow.implicitWidth + Kirigami.Units.smallSpacing * 2 + radius: Kirigami.Units.cornerRadius + color: Qt.rgba(0, 0, 0, 0.65) + + Row { + id: playTimeRow + anchors.centerIn: parent + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + anchors.verticalCenter: parent.verticalCenter + width: 12 * frameRoot.gv.scaleFactor + height: 12 * frameRoot.gv.scaleFactor + source: "clock-symbolic" + color: "#ffffff" + } + QQC2.Label { + id: playTimeLabel + text: TimeUtils.formatPlayTime(frameRoot.playTime) + color: "#ffffff" + font.pixelSize: 10 * frameRoot.gv.scaleFactor + } + } + } } Rectangle { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/qml/GameGridView.qml new/vermouth-2.0.2/qml/GameGridView.qml --- old/vermouth-2.0.1/qml/GameGridView.qml 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/qml/GameGridView.qml 2026-08-11 12:07:42.000000000 +0200 @@ -8,6 +8,7 @@ property real scaleFactor: 1.0 property string viewType: "grid" property bool showNames: true + property bool showPlayTime: true property bool active: true property bool lightsOut: false property bool showHidden: true @@ -24,6 +25,7 @@ property alias scaleFactor: gridView.scaleFactor property alias viewType: gridView.viewType property alias showNames: gridView.showNames + property alias showPlayTime: gridView.showPlayTime property alias showHidden: gridView.showHidden property alias sortField: gridView.sortField property alias sortAscending: gridView.sortAscending diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/qml/Main.qml new/vermouth-2.0.2/qml/Main.qml --- old/vermouth-2.0.1/qml/Main.qml 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/qml/Main.qml 2026-08-11 12:07:42.000000000 +0200 @@ -277,6 +277,13 @@ checked: gridView.showNames onTriggered: gridView.showNames = checked } + QQC2.MenuItem { + text: i18n("Show play time") + icon.name: "clock-symbolic" + checkable: true + checked: gridView.showPlayTime + onTriggered: gridView.showPlayTime = checked + } QQC2.MenuSeparator {} RowLayout { spacing: Kirigami.Units.smallSpacing @@ -328,6 +335,12 @@ checked: gridView.sortField === "date" onTriggered: gridView.sortField = "date" } + QQC2.MenuItem { + text: i18n("Play Time") + checkable: true + checked: gridView.sortField === "playtime" + onTriggered: gridView.sortField = "playtime" + } QQC2.MenuSeparator {} QQC2.MenuItem { text: i18n("A–Z") @@ -851,6 +864,12 @@ checked: gridView.sortField === "date" onTriggered: gridView.sortField = "date" } + QQC2.MenuItem { + text: i18n("Play Time") + checkable: true + checked: gridView.sortField === "playtime" + onTriggered: gridView.sortField = "playtime" + } QQC2.MenuSeparator {} QQC2.MenuItem { text: i18n("A–Z") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/qml/TimeUtils.js new/vermouth-2.0.2/qml/TimeUtils.js --- old/vermouth-2.0.1/qml/TimeUtils.js 1970-01-01 01:00:00.000000000 +0100 +++ new/vermouth-2.0.2/qml/TimeUtils.js 2026-08-11 12:07:42.000000000 +0200 @@ -0,0 +1,40 @@ +.pragma library + +// Format a play time given in seconds as "H:MM:SS". Returns an empty string +// when there is nothing to show (zero or invalid input). +function formatPlayTime(seconds) { + if (!seconds || seconds <= 0) + return ""; + seconds = Math.floor(seconds); + var h = Math.floor(seconds / 3600); + var m = Math.floor((seconds % 3600) / 60); + var s = seconds % 60; + function pad(n) { + return n < 10 ? "0" + n : "" + n; + } + return h + ":" + pad(m) + ":" + pad(s); +} + +// Parse a play time in "H:MM:SS", "M:SS" or plain-seconds form into seconds. +// Returns 0 for an empty string, or NaN for invalid input. +function parsePlayTime(text) { + var t = (text || "").trim(); + if (t === "") + return 0; + var parts = t.split(":"); + if (parts.length > 3) + return NaN; + var total = 0; + for (var i = 0; i < parts.length; i++) { + var n = parseInt(parts[i], 10); + if (isNaN(n) || n < 0) + return NaN; + if (i === parts.length - 1) + total += n; // seconds + else if (i === parts.length - 2) + total += n * 60; // minutes + else + total += n * 3600; // hours + } + return total; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/src/appentry.cpp new/vermouth-2.0.2/src/appentry.cpp --- old/vermouth-2.0.1/src/appentry.cpp 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/src/appentry.cpp 2026-08-11 12:07:42.000000000 +0200 @@ -52,6 +52,7 @@ obj[QStringLiteral("launchOptions")] = launchOptions; obj[QStringLiteral("enableLogging")] = enableLogging; obj[QStringLiteral("hidden")] = hidden; + obj[QStringLiteral("playTime")] = static_cast<double>(playTime); obj[QStringLiteral("dateAdded")] = dateAdded.toString(Qt::ISODate); return obj; } @@ -80,6 +81,7 @@ {QStringLiteral("launchOptions"), launchOptions}, {QStringLiteral("enableLogging"), enableLogging}, {QStringLiteral("hidden"), hidden}, + {QStringLiteral("playTime"), playTime}, {QStringLiteral("dateAdded"), dateAdded}, }; } @@ -113,6 +115,7 @@ e.launchOptions = obj[QStringLiteral("launchOptions")].toString(); e.enableLogging = obj[QStringLiteral("enableLogging")].toBool(false); e.hidden = obj[QStringLiteral("hidden")].toBool(false); + e.playTime = obj.value(QStringLiteral("playTime")).toInteger(0); e.dateAdded = QDateTime::fromString(obj[QStringLiteral("dateAdded")].toString(), Qt::ISODate); return e; } @@ -142,4 +145,5 @@ launchOptions = app[QStringLiteral("launchOptions")].toString(); enableLogging = app.value(QStringLiteral("enableLogging"), false).toBool(); hidden = app.value(QStringLiteral("hidden"), false).toBool(); + playTime = app.value(QStringLiteral("playTime"), 0).toLongLong(); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/src/appentry.h new/vermouth-2.0.2/src/appentry.h --- old/vermouth-2.0.1/src/appentry.h 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/src/appentry.h 2026-08-11 12:07:42.000000000 +0200 @@ -60,6 +60,7 @@ QString launchOptions; bool enableLogging = false; bool hidden = false; + qint64 playTime = 0; // seconds QDateTime dateAdded; QJsonObject toJson() const; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/src/appmodel.cpp new/vermouth-2.0.2/src/appmodel.cpp --- old/vermouth-2.0.1/src/appmodel.cpp 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/src/appmodel.cpp 2026-08-11 12:07:42.000000000 +0200 @@ -38,6 +38,10 @@ result = ea.dateAdded > eb.dateAdded ? 1 : (ea.dateAdded < eb.dateAdded ? -1 : 0); if (result == 0) result = ea.name.compare(eb.name, Qt::CaseInsensitive); + } else if (m_sortField == QLatin1String("playtime")) { + result = ea.playTime < eb.playTime ? -1 : (ea.playTime > eb.playTime ? 1 : 0); + if (result == 0) + result = ea.name.compare(eb.name, Qt::CaseInsensitive); } else { result = ea.name.compare(eb.name, Qt::CaseInsensitive); } @@ -145,6 +149,8 @@ return e.enableLogging; case HiddenRole: return e.hidden; + case PlayTimeRole: + return e.playTime; } return {}; } @@ -173,6 +179,7 @@ {LaunchOptionsRole, "launchOptions"}, {EnableLoggingRole, "enableLogging"}, {HiddenRole, "hidden"}, + {PlayTimeRole, "playTime"}, }; } @@ -271,6 +278,28 @@ } } +void AppModel::addPlayTime(const QString &exePath, qint64 seconds) +{ + for (int i = 0; i < m_entries.size(); ++i) { + if (m_entries[i].exePath != exePath) + continue; + m_entries[i].playTime += seconds; + for (int f = 0; f < m_filtered.size(); ++f) { + if (m_filtered[f] == i) { + QModelIndex idx = index(f, 0); + Q_EMIT dataChanged(idx, idx, {PlayTimeRole}); + break; + } + } + // Throttle disk writes: flush at most every 10 seconds while a game runs. + if (++m_saveCounter >= 10) { + m_saveCounter = 0; + save(); + } + return; + } +} + QVariantMap AppModel::getApp(int index) const { int src = sourceIndex(index); @@ -354,8 +383,14 @@ for (const auto &e : m_entries) arr.append(e.toJson()); - QFile f(configPath()); + // Atomic write: write to a temp file, then rename over the real one. + const QString path = configPath(); + const QString tmpPath = path + QStringLiteral(".tmp"); + QFile f(tmpPath); if (!f.open(QIODevice::WriteOnly)) return; f.write(QJsonDocument(arr).toJson()); + f.close(); + QFile::remove(path); + QFile::rename(tmpPath, path); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/src/appmodel.h new/vermouth-2.0.2/src/appmodel.h --- old/vermouth-2.0.1/src/appmodel.h 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/src/appmodel.h 2026-08-11 12:07:42.000000000 +0200 @@ -35,6 +35,7 @@ LaunchOptionsRole, EnableLoggingRole, HiddenRole, + PlayTimeRole, }; explicit AppModel(QObject *parent = nullptr); @@ -60,6 +61,7 @@ Q_INVOKABLE QString generateUUID() const; Q_INVOKABLE void updateAppArt(const QString &id, const QString &iconPath, const QString &gridPath, const QString &heroPath, const QString &logoPath, int steamGridDbId = 0); + Q_INVOKABLE void addPlayTime(const QString &exePath, qint64 seconds); bool showHidden() const { @@ -99,4 +101,5 @@ bool m_showHidden = false; QString m_sortField = QStringLiteral("name"); bool m_sortAscending = true; + int m_saveCounter = 0; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/src/launcher.cpp new/vermouth-2.0.2/src/launcher.cpp --- old/vermouth-2.0.1/src/launcher.cpp 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/src/launcher.cpp 2026-08-11 12:07:42.000000000 +0200 @@ -359,6 +359,16 @@ const QString &logName, bool appendExe) { + if (binary.isEmpty()) { + Q_EMIT launchError(exePath, QStringLiteral("No runtime is set for this game.")); + return -1; + } + const bool binaryFound = binary.contains(QLatin1Char('/')) ? QFileInfo::exists(binary) : !QStandardPaths::findExecutable(binary).isEmpty(); + if (!binaryFound) { + Q_EMIT launchError(exePath, QStringLiteral("Runtime binary not found: %1").arg(binary)); + return -1; + } + auto *proc = new QProcess(this); auto *timer = new QElapsedTimer(); timer->start(); @@ -450,6 +460,10 @@ // "default" runtime is inferred from settings at launch time - never copied into the entry. if (runtimeType == QStringLiteral("default")) { + if (m_defaultRuntimeType.isEmpty()) { + Q_EMIT launchError(name, QStringLiteral("No default runtime is set in Settings.")); + return -1; + } QVariantMap resolved = app; resolved[QStringLiteral("runtimeType")] = m_defaultRuntimeType; if (resolved[QStringLiteral("protonPath")].toString().isEmpty()) @@ -502,20 +516,30 @@ if (runtimeType == QStringLiteral("proton")) { QString protonPath = app[QStringLiteral("protonPath")].toString(); + if (protonPath.isEmpty()) { + Q_EMIT launchError(name, QStringLiteral("Proton is not set. Download it or set its path in Settings.")); + return -1; + } QString prefix = app[QStringLiteral("protonPrefix")].toString(); if (!prefix.isEmpty()) QDir().mkpath(prefix); - QString umoBin = m_umuPath; - if (umoBin.isEmpty()) - umoBin = QStandardPaths::findExecutable(QStringLiteral("umu-run")); - - if (!umoBin.isEmpty()) { + QString umuBin; + if (!m_umuPath.isEmpty() && QFileInfo::exists(m_umuPath)) + umuBin = m_umuPath; + if (umuBin.isEmpty()) + umuBin = QStandardPaths::findExecutable(QStringLiteral("umu-run")); + + if (!umuBin.isEmpty()) { + if (!QFileInfo::exists(protonPath + QStringLiteral("/proton"))) { + Q_EMIT launchError(name, QStringLiteral("Proton not found at %1. Download it or set its path in Settings.").arg(protonPath)); + return -1; + } env.insert(QStringLiteral("PROTONPATH"), protonPath); env.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), prefix); env.insert(QStringLiteral("GAMEID"), QStringLiteral("0")); env.insert(QStringLiteral("WINEPREFIX"), prefix); - return launch(umoBin, {}, exePath, env, opts, logging, name); + return launch(umuBin, {}, exePath, env, opts, logging, name); } else { env.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), QDir::homePath() + QStringLiteral("/.steam/steam")); env.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), prefix); @@ -550,12 +574,17 @@ env.insert(QStringLiteral("APPIMAGE"), exePath); return launch(binary, baseArgs, exePath, env, opts, logging, name, false); } else { + QString wineBinary = app[QStringLiteral("wineBinary")].toString(); + if (wineBinary.isEmpty()) { + Q_EMIT launchError(name, QStringLiteral("Wine is not set. Set its path in Settings.")); + return -1; + } QString prefix = app[QStringLiteral("winePrefix")].toString(); if (!prefix.isEmpty()) { QDir().mkpath(prefix); env.insert(QStringLiteral("WINEPREFIX"), prefix); } - return launch(app[QStringLiteral("wineBinary")].toString(), {}, exePath, env, opts, logging, name); + return launch(wineBinary, {}, exePath, env, opts, logging, name); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/src/main.cpp new/vermouth-2.0.2/src/main.cpp --- old/vermouth-2.0.1/src/main.cpp 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/src/main.cpp 2026-08-11 12:07:42.000000000 +0200 @@ -35,6 +35,7 @@ #include <QQmlContext> #include <QQuickImageProvider> #include <QQuickStyle> +#include <QTimer> class IconImageProvider : public QQuickImageProvider { @@ -217,6 +218,33 @@ QObject::connect(&app, &QApplication::aboutToQuit, [&]() { launcher.restoreHdrState(); + appModel.save(); + }); + + QTimer playTimeTimer; + playTimeTimer.setInterval(1000); + QObject::connect(&playTimeTimer, &QTimer::timeout, [&]() { + const auto paths = launcher.runningExePaths(); + for (const QString &path : paths) + appModel.addPlayTime(path, 1); + }); + QTimer playTimeStartDelay; + playTimeStartDelay.setSingleShot(true); + playTimeStartDelay.setInterval(5000); + QObject::connect(&playTimeStartDelay, &QTimer::timeout, [&]() { + playTimeTimer.start(); + }); + QObject::connect(&launcher, &Launcher::runningExePathsChanged, [&]() { + if (launcher.runningExePaths().isEmpty()) { + playTimeStartDelay.stop(); + playTimeTimer.stop(); + appModel.save(); + } else if (!playTimeTimer.isActive() && !playTimeStartDelay.isActive()) { + playTimeStartDelay.start(); + } + }); + QObject::connect(&launcher, &Launcher::processFinished, [&](int) { + appModel.save(); }); // Restore sleep inhibition state diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vermouth-2.0.1/src/protondownloader.h new/vermouth-2.0.2/src/protondownloader.h --- old/vermouth-2.0.1/src/protondownloader.h 2026-08-05 15:43:55.000000000 +0200 +++ new/vermouth-2.0.2/src/protondownloader.h 2026-08-11 12:07:42.000000000 +0200 @@ -19,7 +19,7 @@ {"ge", "https://github.com/GloriousEggroll/proton-ge-custom/releases/latest", "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/%1/%2", - "%1.tar.gz", + "%1-x86_64.tar.gz", "-xzf", ".tar.gz"}, {"cachyos",
