From e6a80703d80b63a009ea066dfd2e8031bf5705c4 Mon Sep 17 00:00:00 2001 From: Lemmy Date: Fri, 16 Jan 2026 11:48:05 -0500 Subject: [PATCH] Startup: proper display of a dialog to confirm anonymous telemetry acceptance. --- Modules/Panels/SetupWizard/SetupWizard.qml | 67 +++++++++++----- Services/Noctalia/UpdateService.qml | 79 ++++++++++++++---- Services/UI/PanelService.qml | 18 +++++ shell.qml | 93 +++++++++++++++------- 4 files changed, 191 insertions(+), 66 deletions(-) diff --git a/Modules/Panels/SetupWizard/SetupWizard.qml b/Modules/Panels/SetupWizard/SetupWizard.qml index 08894197b..7e4f287b4 100644 --- a/Modules/Panels/SetupWizard/SetupWizard.qml +++ b/Modules/Panels/SetupWizard/SetupWizard.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Wayland import qs.Commons import qs.Modules.MainScreen +import qs.Services.Noctalia import qs.Services.System import qs.Services.UI import qs.Widgets @@ -12,10 +13,15 @@ import qs.Widgets SmartPanel { id: root + // When true, only shows step 0 with modified text for returning users (telemetry notification) + property bool telemetryOnlyMode: false + + signal telemetryWizardCompleted + preferredWidth: Math.round(preferredWidthRatio * 2560 * Style.uiScaleRatio) preferredHeight: Math.round(preferredHeightRatio * 1440 * Style.uiScaleRatio) preferredWidthRatio: 0.4 - preferredHeightRatio: 0.6 + preferredHeightRatio: root.telemetryOnlyMode ? 0.45 : 0.6 panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true @@ -27,7 +33,7 @@ SmartPanel { // Wizard state (lazy-loaded with panelContent) property int currentStep: 0 - readonly property int totalSteps: 5 + readonly property int totalSteps: root.telemetryOnlyMode ? 1 : 5 property bool isCompleting: false // Setup wizard data @@ -72,27 +78,44 @@ SmartPanel { } try { - Logger.i("SetupWizard", "Completing setup with selected options"); + Logger.i("SetupWizard", root.telemetryOnlyMode ? "Completing telemetry wizard" : "Completing setup with selected options"); isCompleting = true; - if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) { - if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) { - Settings.data.wallpaper.directory = selectedWallpaperDirectory; - WallpaperService.refreshWallpapersList(); + // In telemetry-only mode, we only need to save the telemetry setting + if (!root.telemetryOnlyMode) { + if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) { + if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) { + Settings.data.wallpaper.directory = selectedWallpaperDirectory; + WallpaperService.refreshWallpapersList(); + } + + if (selectedWallpaper !== "") { + WallpaperService.changeWallpaper(selectedWallpaper, undefined); + } } - if (selectedWallpaper !== "") { - WallpaperService.changeWallpaper(selectedWallpaper, undefined); - } + Settings.data.general.scaleRatio = selectedScaleRatio; + Settings.data.bar.position = selectedBarPosition; } - Settings.data.general.scaleRatio = selectedScaleRatio; - Settings.data.bar.position = selectedBarPosition; + // Mark the current version as seen to prevent telemetry wizard on next startup + // (only for full setup wizard - telemetry wizard lets changelog mark it seen) + if (!root.telemetryOnlyMode) { + UpdateService.markChangelogSeen(UpdateService.currentVersion); + } + + // Initialize telemetry now that user has made their choice + TelemetryService.init(); // Save settings immediately and wait for settingsSaved signal before closing Settings.saveImmediate(); Logger.i("SetupWizard", "Setup completed successfully, waiting for settings save confirmation"); + // Emit signal for telemetry wizard completion (shell.qml will show changelog) + if (root.telemetryOnlyMode) { + root.telemetryWizardCompleted(); + } + // Fallback: if settingsSaved signal doesn't fire within 2 seconds, close anyway closeTimer.start(); } catch (error) { @@ -140,7 +163,7 @@ SmartPanel { Item { ColumnLayout { anchors.centerIn: parent - width: Math.round(Math.min(parent.width - Style.marginXL * 2, 420)) + width: Math.round(Math.max(parent.width * 0.5, 420)) spacing: Style.marginXL // Logo with subtle glow effect @@ -210,7 +233,7 @@ SmartPanel { spacing: Style.marginM NText { - text: "Welcome to Noctalia! ✨" + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-title") : I18n.tr("setup.welcome-title") pointSize: Style.fontSizeXXL * 1.4 font.weight: Style.fontWeightBold color: Color.mOnSurface @@ -219,7 +242,7 @@ SmartPanel { } NText { - text: "Let's make your desktop uniquely yours" + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-subtitle") : I18n.tr("setup.welcome-subtitle") pointSize: Style.fontSizeL color: Color.mOnSurfaceVariant Layout.fillWidth: true @@ -234,12 +257,11 @@ SmartPanel { Layout.preferredHeight: childrenRect.height + Style.marginXL color: Color.mSurfaceVariant radius: Style.radiusL - opacity: 0.4 NText { anchors.centerIn: parent width: parent.width - Style.marginL * 2 - text: I18n.tr("setup.welcome-note") + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-note") : I18n.tr("setup.welcome-note") pointSize: Style.fontSizeM color: Color.mOnSurfaceVariant horizontalAlignment: Text.AlignHCenter @@ -308,12 +330,14 @@ SmartPanel { Layout.preferredHeight: 1 color: Color.mOutline opacity: 0.2 + visible: !root.telemetryOnlyMode } // Modern progress indicator with labels Item { Layout.fillWidth: true Layout.preferredHeight: 32 + visible: !root.telemetryOnlyMode RowLayout { anchors.centerIn: parent @@ -410,8 +434,9 @@ SmartPanel { spacing: Style.marginM NButton { - text: "Skip Setup" + text: I18n.tr("setup.skip-setup") outlined: true + visible: !root.telemetryOnlyMode Layout.preferredHeight: 44 onClicked: { panelContent.completeSetup(); @@ -423,9 +448,9 @@ SmartPanel { } NButton { - text: "← Back" + text: "← " + I18n.tr("common.back") outlined: true - visible: currentStep > 0 + visible: currentStep > 0 && !root.telemetryOnlyMode Layout.preferredHeight: 44 onClicked: { if (currentStep > 0) { @@ -435,7 +460,7 @@ SmartPanel { } NButton { - text: currentStep === totalSteps - 1 ? "All Done!" : "Continue →" + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-done") : (currentStep === totalSteps - 1 ? I18n.tr("setup.all-done") : I18n.tr("common.continue") + " →") Layout.preferredHeight: 44 onClicked: { if (currentStep < totalSteps - 1) { diff --git a/Services/Noctalia/UpdateService.qml b/Services/Noctalia/UpdateService.qml index 7c663bc69..7db109584 100644 --- a/Services/Noctalia/UpdateService.qml +++ b/Services/Noctalia/UpdateService.qml @@ -11,11 +11,14 @@ Singleton { id: root // Version properties - readonly property string baseVersion: "4.0.1" + readonly property string baseVersion: "4.0.2" readonly property bool isDevelopment: true readonly property string developmentSuffix: "-git" readonly property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + developmentSuffix}` + // Telemetry was introduced in this version - users upgrading from earlier need to see the wizard + readonly property string telemetryIntroVersion: "4.0.2" + // URLs readonly property string discordUrl: "https://discord.noctalia.dev" readonly property string feedbackUrl: Quickshell.env("NOCTALIA_CHANGELOG_FEEDBACK_URL") || "" @@ -35,6 +38,7 @@ Singleton { property string changelogLastSeenVersion: "" property bool changelogStateLoaded: false property bool pendingShowRequest: false + property bool pendingTelemetryWizardCheck: false // Fix for FileView race condition property bool saveInProgress: false @@ -53,6 +57,7 @@ Singleton { } signal popupQueued(string fromVersion, string toVersion) + signal telemetryWizardNeeded function init() { if (initialized) @@ -211,6 +216,47 @@ Singleton { return 0; } + // Check if user is upgrading from a version before telemetry was introduced + function shouldShowTelemetryWizard() { + if (!changelogStateLoaded) + return false; + if (Settings.isFreshInstall) + return false; + if (Settings.shouldOpenSetupWizard) + return false; + + // No previous version recorded but settings exist - assume upgrading from old version + // (e.g., user deleted shell-state.json but has existing settings) + if (!changelogLastSeenVersion || changelogLastSeenVersion === "") + return true; + + // Check if last seen version is before telemetry introduction + return compareVersions(changelogLastSeenVersion, telemetryIntroVersion) < 0; + } + + // Called by shell.qml to check for telemetry wizard after init + // If state isn't loaded yet, sets a pending flag and emits telemetryWizardNeeded later + function checkTelemetryWizardOrChangelog() { + Logger.d("UpdateService", "checkTelemetryWizardOrChangelog called, stateLoaded:", changelogStateLoaded); + if (!changelogStateLoaded) { + // State not loaded yet, set pending flags + Logger.d("UpdateService", "State not loaded yet, setting pending flags"); + pendingTelemetryWizardCheck = true; + pendingShowRequest = true; + return; + } + + // State is already loaded, check immediately + const needsTelemetryWizard = shouldShowTelemetryWizard(); + Logger.d("UpdateService", "shouldShowTelemetryWizard:", needsTelemetryWizard, "lastSeenVersion:", changelogLastSeenVersion); + if (needsTelemetryWizard) { + Logger.i("UpdateService", "Emitting telemetryWizardNeeded signal"); + root.telemetryWizardNeeded(); + } else { + showLatestChangelog(); + } + } + function parseReleaseNotes(body) { if (!body) return []; @@ -239,19 +285,11 @@ Singleton { return; } - const monitors = Settings.data.bar.monitors || []; - const allowPanelsOnScreenWithoutBar = Settings.data.general.allowPanelsOnScreenWithoutBar; - - function canShowPanelsOnScreen(screen) { - const name = screen?.name || ""; - return allowPanelsOnScreenWithoutBar || monitors.length === 0 || monitors.includes(name); - } - let targetScreen = viewChangelogTargetScreen; if (targetScreen) { // Explicit screen requested - validate it - if (!canShowPanelsOnScreen(targetScreen)) { + if (!PanelService.canShowPanelsOnScreen(targetScreen)) { Logger.w("UpdateService", "Changelog cannot be shown on screen without bar:", targetScreen.name); popupScheduled = false; viewChangelogTargetScreen = null; @@ -259,13 +297,7 @@ Singleton { } } else { // No explicit screen - find one that can show panels - for (let i = 0; i < Quickshell.screens.length; i++) { - if (canShowPanelsOnScreen(Quickshell.screens[i])) { - targetScreen = Quickshell.screens[i]; - break; - } - } - + targetScreen = PanelService.findScreenForPanels(); if (!targetScreen) { Logger.w("UpdateService", "No screen available to show changelog"); popupScheduled = false; @@ -367,6 +399,19 @@ Singleton { Logger.e("UpdateService", "Failed to load changelog state:", error); } changelogStateLoaded = true; + + // Handle pending telemetry wizard check first + if (pendingTelemetryWizardCheck) { + pendingTelemetryWizardCheck = false; + if (shouldShowTelemetryWizard()) { + root.telemetryWizardNeeded(); + } else if (pendingShowRequest) { + pendingShowRequest = false; + Qt.callLater(root.showLatestChangelog); + } + return; + } + if (pendingShowRequest) { pendingShowRequest = false; Qt.callLater(root.showLatestChangelog); diff --git a/Services/UI/PanelService.qml b/Services/UI/PanelService.qml index cbb2296a7..c2b1c094c 100644 --- a/Services/UI/PanelService.qml +++ b/Services/UI/PanelService.qml @@ -92,6 +92,24 @@ Singleton { return name in registeredPanels; } + // Check if panels can be shown on a given screen (has bar enabled or allowPanelsOnScreenWithoutBar) + function canShowPanelsOnScreen(screen) { + const name = screen?.name || ""; + const monitors = Settings.data.bar.monitors || []; + const allowPanelsOnScreenWithoutBar = Settings.data.general.allowPanelsOnScreenWithoutBar; + return allowPanelsOnScreenWithoutBar || monitors.length === 0 || monitors.includes(name); + } + + // Find a screen that can show panels + function findScreenForPanels() { + for (let i = 0; i < Quickshell.screens.length; i++) { + if (canShowPanelsOnScreen(Quickshell.screens[i])) { + return Quickshell.screens[i]; + } + } + return null; + } + // Timer to switch from Exclusive to OnDemand keyboard focus on Hyprland Timer { id: keyboardInitTimer diff --git a/shell.qml b/shell.qml index d3d1a58b6..09da7c1d5 100644 --- a/shell.qml +++ b/shell.qml @@ -102,10 +102,8 @@ ShellRoot { PowerProfileService.init(); HostService.init(); GitHubService.init(); - TelemetryService.init(); delayedInitTimer.running = true; - checkSetupWizard(); } Overview {} @@ -152,7 +150,7 @@ ShellRoot { } // --------------------------------------------- - // Delayed timer + // Delayed initialization and wizard/changelog // --------------------------------------------- Timer { id: delayedInitTimer @@ -161,42 +159,81 @@ ShellRoot { onTriggered: { FontService.init(); UpdateService.init(); - UpdateService.showLatestChangelog(); + showWizardOrChangelog(); } } - // --------------------------------------------- - // Setup Wizard - // --------------------------------------------- + // Retry timer for when panel isn't ready yet Timer { - id: setupWizardTimer + id: wizardRetryTimer running: false - interval: 2000 - onTriggered: { - showSetupWizard(); + interval: 500 + property string pendingWizardType: "" // "setup", "telemetry", or "" + onTriggered: showWizardOrChangelog() + } + + // Connect to telemetry wizard signal from UpdateService (for async state loading) + Connections { + target: UpdateService + function onTelemetryWizardNeeded() { + wizardRetryTimer.pendingWizardType = "telemetry"; + showWizardOrChangelog(); } } - function checkSetupWizard() { - // Only open the setup wizard for new users - if (!Settings.shouldOpenSetupWizard) { + property var telemetryWizardConnection: null + + function showWizardOrChangelog() { + // Determine what to show: setup wizard > telemetry wizard > changelog + var wizardType = wizardRetryTimer.pendingWizardType; + + if (wizardType === "") { + // First call - determine wizard type + if (Settings.shouldOpenSetupWizard) { + wizardType = "setup"; + } else if (UpdateService.shouldShowTelemetryWizard()) { + wizardType = "telemetry"; + } else { + // No wizard needed - init telemetry and show changelog + TelemetryService.init(); + UpdateService.checkTelemetryWizardOrChangelog(); + return; + } + } + + var targetScreen = PanelService.findScreenForPanels(); + if (!targetScreen) { + Logger.w("Shell", "No screen available to show wizard"); + wizardRetryTimer.pendingWizardType = ""; return; } - setupWizardTimer.start(); - } - - function showSetupWizard() { - // Open Setup Wizard as a panel in the same windowing system as Settings/ControlCenter - if (Quickshell.screens.length > 0) { - var targetScreen = Quickshell.screens[0]; - var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen); - if (setupPanel) { - setupPanel.open(); - } else { - // If not yet loaded, ensure it loads and try again shortly - setupWizardTimer.restart(); - } + var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen); + if (!setupPanel) { + // Panel not ready, retry + wizardRetryTimer.pendingWizardType = wizardType; + wizardRetryTimer.restart(); + return; } + + // Panel is ready, show it + wizardRetryTimer.pendingWizardType = ""; + + if (wizardType === "telemetry") { + setupPanel.telemetryOnlyMode = true; + + // Connect to completion signal to show changelog afterward + if (telemetryWizardConnection) { + setupPanel.telemetryWizardCompleted.disconnect(telemetryWizardConnection); + } + telemetryWizardConnection = function () { + UpdateService.showLatestChangelog(); + }; + setupPanel.telemetryWizardCompleted.connect(telemetryWizardConnection); + } else { + setupPanel.telemetryOnlyMode = false; + } + + setupPanel.open(); } }