diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 86744d6..ea86ac4 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -19,6 +19,7 @@ Kirigami.ApplicationWindow { height: Kirigami.Units.gridUnit * 40 property var selectedGame: null + property var pendingRemoveGame: null property string currentSource: "all" property bool searchActive: false property bool settingsLayerOpen: false @@ -498,19 +499,9 @@ Kirigami.ApplicationWindow { } onRemoveRequested: { - if (root.selectedGame) { - let gameId = root.selectedGame.id - let gameName = root.selectedGame.name - App.removeGame(root.selectedGame) - detailsSheet.close() - root.selectedGame = null - showPassiveNotification( - i18n("%1 removed", gameName), - "long", - i18n("Undo"), - function() { App.restoreGame(gameId) } - ) - } + if (!root.selectedGame) return + root.pendingRemoveGame = root.selectedGame + removeGameConfirmDialog.open() } onClosed: { @@ -518,6 +509,33 @@ Kirigami.ApplicationWindow { } } + Kirigami.PromptDialog { + id: removeGameConfirmDialog + title: i18n("Remove Game") + subtitle: root.pendingRemoveGame + ? i18n("Are you sure you want to remove '%1'?", root.pendingRemoveGame.name) + : i18n("Are you sure you want to remove this game?") + standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onAccepted: { + if (!root.pendingRemoveGame) return + let gameId = root.pendingRemoveGame.id + let gameName = root.pendingRemoveGame.name + App.removeGame(root.pendingRemoveGame) + root.pendingRemoveGame = null + detailsSheet.close() + root.selectedGame = null + showPassiveNotification( + i18n("%1 removed", gameName), + "long", + i18n("Undo"), + function() { App.restoreGame(gameId) } + ) + } + onRejected: { + root.pendingRemoveGame = null + } + } + Kirigami.OverlaySheet { id: importSheet title: i18n("Import Games") diff --git a/src/qml/SettingsPage.qml b/src/qml/SettingsPage.qml index 992fb43..8bdae0b 100644 --- a/src/qml/SettingsPage.qml +++ b/src/qml/SettingsPage.qml @@ -17,6 +17,17 @@ ColumnLayout { showPlatformBadgesDelegate.forceActiveFocus() } + property var pendingDisableImportApply: null + property var pendingDisableImportDelegate: null + property string pendingDisableImportName: "" + + function requestDisableImport(delegate, sourceName, applyFn) { + pendingDisableImportDelegate = delegate + pendingDisableImportName = sourceName + pendingDisableImportApply = applyFn + disableImportConfirmDialog.open() + } + FormCard.FormHeader { Layout.fillWidth: true title: i18n("Appearance") @@ -88,8 +99,26 @@ FormCard.FormHeader { secondary: "steam" resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg") } + property bool restoring: false checked: App.config.importSteam - onToggled: App.config.importSteam = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importSteam) return + + if (!checked && App.gameModel.hasPlatformPrefix("Steam")) { + restoring = true + checked = Qt.binding(function() { return App.config.importSteam }) + restoring = false + settingsPage.requestDisableImport(this, i18n("Steam"), function() { App.config.importSteam = false }) + return + } + + App.config.importSteam = checked + restoring = true + checked = Qt.binding(function() { return App.config.importSteam }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -101,8 +130,26 @@ FormCard.FormHeader { primary: "lutris" secondary: "applications-games" } + property bool restoring: false checked: App.config.importLutris - onToggled: App.config.importLutris = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importLutris) return + + if (!checked && App.gameModel.hasPlatformPrefix("Lutris")) { + restoring = true + checked = Qt.binding(function() { return App.config.importLutris }) + restoring = false + settingsPage.requestDisableImport(this, i18n("Lutris"), function() { App.config.importLutris = false }) + return + } + + App.config.importLutris = checked + restoring = true + checked = Qt.binding(function() { return App.config.importLutris }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -115,8 +162,26 @@ FormCard.FormHeader { primary: "com.heroicgameslauncher.hgl" secondary: "applications-games" } + property bool restoring: false checked: App.config.importHeroic - onToggled: App.config.importHeroic = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importHeroic) return + + if (!checked && App.gameModel.hasPlatformPrefix("Heroic")) { + restoring = true + checked = Qt.binding(function() { return App.config.importHeroic }) + restoring = false + settingsPage.requestDisableImport(this, i18n("Heroic Games Launcher"), function() { App.config.importHeroic = false }) + return + } + + App.config.importHeroic = checked + restoring = true + checked = Qt.binding(function() { return App.config.importHeroic }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -129,8 +194,26 @@ FormCard.FormHeader { primary: "user-desktop" secondary: "computer" } + property bool restoring: false checked: App.config.importDesktop - onToggled: App.config.importDesktop = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importDesktop) return + + if (!checked && App.gameModel.hasPlatformPrefix("Desktop")) { + restoring = true + checked = Qt.binding(function() { return App.config.importDesktop }) + restoring = false + settingsPage.requestDisableImport(this, i18n("Desktop Entries"), function() { App.config.importDesktop = false }) + return + } + + App.config.importDesktop = checked + restoring = true + checked = Qt.binding(function() { return App.config.importDesktop }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -143,8 +226,26 @@ FormCard.FormHeader { primary: "com.usebottles.bottles" secondary: "application-x-executable" } + property bool restoring: false checked: App.config.importBottles - onToggled: App.config.importBottles = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importBottles) return + + if (!checked && App.gameModel.hasPlatformPrefix("Bottles")) { + restoring = true + checked = Qt.binding(function() { return App.config.importBottles }) + restoring = false + settingsPage.requestDisableImport(this, i18n("Bottles"), function() { App.config.importBottles = false }) + return + } + + App.config.importBottles = checked + restoring = true + checked = Qt.binding(function() { return App.config.importBottles }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -157,8 +258,26 @@ FormCard.FormHeader { primary: "flatpak-discover" secondary: "applications-games" } + property bool restoring: false checked: App.config.importFlatpak - onToggled: App.config.importFlatpak = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importFlatpak) return + + if (!checked && App.gameModel.hasPlatformPrefix("Flatpak")) { + restoring = true + checked = Qt.binding(function() { return App.config.importFlatpak }) + restoring = false + settingsPage.requestDisableImport(this, i18n("Flatpak"), function() { App.config.importFlatpak = false }) + return + } + + App.config.importFlatpak = checked + restoring = true + checked = Qt.binding(function() { return App.config.importFlatpak }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -172,8 +291,26 @@ FormCard.FormHeader { secondary: "itch" resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg") } + property bool restoring: false checked: App.config.importItch - onToggled: App.config.importItch = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importItch) return + + if (!checked && App.gameModel.hasPlatformPrefix("itch.io")) { + restoring = true + checked = Qt.binding(function() { return App.config.importItch }) + restoring = false + settingsPage.requestDisableImport(this, i18n("itch.io"), function() { App.config.importItch = false }) + return + } + + App.config.importItch = checked + restoring = true + checked = Qt.binding(function() { return App.config.importItch }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -186,8 +323,26 @@ FormCard.FormHeader { primary: "legendary" secondary: "applications-games" } + property bool restoring: false checked: App.config.importLegendary - onToggled: App.config.importLegendary = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importLegendary) return + + if (!checked && App.gameModel.hasPlatformPrefix("Legendary")) { + restoring = true + checked = Qt.binding(function() { return App.config.importLegendary }) + restoring = false + settingsPage.requestDisableImport(this, i18n("Legendary"), function() { App.config.importLegendary = false }) + return + } + + App.config.importLegendary = checked + restoring = true + checked = Qt.binding(function() { return App.config.importLegendary }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -201,8 +356,26 @@ FormCard.FormHeader { secondary: "retroarch" resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg") } + property bool restoring: false checked: App.config.importRetroArch - onToggled: App.config.importRetroArch = checked + onToggled: { + if (restoring) return + + if (checked === App.config.importRetroArch) return + + if (!checked && App.gameModel.hasPlatformPrefix("RetroArch")) { + restoring = true + checked = Qt.binding(function() { return App.config.importRetroArch }) + restoring = false + settingsPage.requestDisableImport(this, i18n("RetroArch"), function() { App.config.importRetroArch = false }) + return + } + + App.config.importRetroArch = checked + restoring = true + checked = Qt.binding(function() { return App.config.importRetroArch }) + restoring = false + } } FormCard.FormDelegateSeparator {} @@ -309,7 +482,7 @@ FormCard.FormHeader { description: App.steamGridDB.busy ? i18n("Fetching...") : i18n("Download covers for all games") icon.name: "download" enabled: App.steamGridDB.enabled && App.steamGridDB.apiKey.length > 0 && !App.steamGridDB.busy - onClicked: App.steamGridDB.fetchAllCovers() + onClicked: fetchAllCoversConfirmDialog.open() } FormCard.FormDelegateSeparator {} @@ -355,7 +528,7 @@ FormCard.FormHeader { text: i18n("Remove Missing Games") description: i18n("Remove games whose executables no longer exist") icon.name: "edit-delete" - onClicked: App.removeMissingGames() + onClicked: removeMissingConfirmDialog.open() } FormCard.FormDelegateSeparator {} @@ -377,6 +550,44 @@ FormCard.FormHeader { } } + Kirigami.PromptDialog { + id: disableImportConfirmDialog + title: i18n("Disable Import Source") + subtitle: i18n("Disabling %1 will remove all games imported from that source. Are you sure?", settingsPage.pendingDisableImportName) + standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onAccepted: { + if (settingsPage.pendingDisableImportApply) { + settingsPage.pendingDisableImportApply() + } + settingsPage.pendingDisableImportApply = null + settingsPage.pendingDisableImportDelegate = null + settingsPage.pendingDisableImportName = "" + } + onRejected: { + settingsPage.pendingDisableImportApply = null + settingsPage.pendingDisableImportDelegate = null + settingsPage.pendingDisableImportName = "" + } + } + + Kirigami.PromptDialog { + id: fetchAllCoversConfirmDialog + title: i18n("Fetch All Covers") + subtitle: App.steamGridDB.preferSteamGridDB + ? i18n("This will download cover art for all games and may replace existing covers. Continue?") + : i18n("This will download cover art for games that are missing covers. Continue?") + standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onAccepted: App.steamGridDB.fetchAllCovers() + } + + Kirigami.PromptDialog { + id: removeMissingConfirmDialog + title: i18n("Remove Missing Games") + subtitle: i18n("This will remove games whose executables cannot be found. This cannot be undone. Continue?") + standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onAccepted: App.removeMissingGames() + } + Kirigami.PromptDialog { id: clearConfirmDialog title: i18n("Clear Library")