From bc71048bf7f51851361760f2e68670cc3793c8d5 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Thu, 15 Jan 2026 09:27:48 +0000 Subject: [PATCH 01/28] feat: Improve Wayland shell surface robustness and add conditional `WakeManager` calls in test pages. --- apps/test/manifest.json | 6 +- apps/test/pages/AlarmTestsPage.qml | 76 +++++++++++++------ apps/test/pages/SensorTestsPage.qml | 20 ++++- apps/test/pages/TelephonyTestsPage.qml | 40 ++++++---- .../components/WaylandShellSurfaceItem.qml | 11 ++- 5 files changed, 108 insertions(+), 45 deletions(-) diff --git a/apps/test/manifest.json b/apps/test/manifest.json index c7ebe72e0..21430d5ff 100644 --- a/apps/test/manifest.json +++ b/apps/test/manifest.json @@ -8,6 +8,9 @@ "author": "Marathon OS", "category": "development", "permissions": [ + "system", + "storage", + "network", "system.telephony", "system.notifications", "system.audio", @@ -21,5 +24,4 @@ "backgroundExecution": false, "systemIntegration": true } -} - +} \ No newline at end of file diff --git a/apps/test/pages/AlarmTestsPage.qml b/apps/test/pages/AlarmTestsPage.qml index baefb6559..e5ee556c2 100644 --- a/apps/test/pages/AlarmTestsPage.qml +++ b/apps/test/pages/AlarmTestsPage.qml @@ -130,11 +130,19 @@ Item { variant: "primary" onClicked: { HapticService.light(); - WakeManager.wake("alarm"); - Logger.info("TestApp", "Wake: alarm"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; + if (typeof WakeManager !== 'undefined' && typeof WakeManager.wake === 'function') { + WakeManager.wake("alarm"); + Logger.info("TestApp", "Wake: alarm"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else { + Logger.warn("TestApp", "WakeManager not available (shell-only)"); + if (testApp) { + testApp.failedTests++; + testApp.totalTests++; + } } } } @@ -144,11 +152,19 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - WakeManager.wake("call"); - Logger.info("TestApp", "Wake: call"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; + if (typeof WakeManager !== 'undefined' && typeof WakeManager.wake === 'function') { + WakeManager.wake("call"); + Logger.info("TestApp", "Wake: call"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else { + Logger.warn("TestApp", "WakeManager not available (shell-only)"); + if (testApp) { + testApp.failedTests++; + testApp.totalTests++; + } } } } @@ -158,11 +174,19 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - WakeManager.wake("notification"); - Logger.info("TestApp", "Wake: notification"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; + if (typeof WakeManager !== 'undefined' && typeof WakeManager.wake === 'function') { + WakeManager.wake("notification"); + Logger.info("TestApp", "Wake: notification"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else { + Logger.warn("TestApp", "WakeManager not available (shell-only)"); + if (testApp) { + testApp.failedTests++; + testApp.totalTests++; + } } } } @@ -172,13 +196,21 @@ Item { variant: "accent" onClicked: { HapticService.light(); - var wakeTime = new Date(); - wakeTime.setMinutes(wakeTime.getMinutes() + 1); - WakeManager.scheduleWake(wakeTime, "test"); - Logger.info("TestApp", "Scheduled wake in 1 minute"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; + if (typeof WakeManager !== 'undefined' && typeof WakeManager.scheduleWake === 'function') { + var wakeTime = new Date(); + wakeTime.setMinutes(wakeTime.getMinutes() + 1); + WakeManager.scheduleWake(wakeTime, "test"); + Logger.info("TestApp", "Scheduled wake in 1 minute"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else { + Logger.warn("TestApp", "WakeManager.scheduleWake not available (shell-only)"); + if (testApp) { + testApp.failedTests++; + testApp.totalTests++; + } } } } diff --git a/apps/test/pages/SensorTestsPage.qml b/apps/test/pages/SensorTestsPage.qml index 701101723..fab0601e3 100644 --- a/apps/test/pages/SensorTestsPage.qml +++ b/apps/test/pages/SensorTestsPage.qml @@ -71,7 +71,10 @@ Item { variant: "primary" onClicked: { HapticService.light(); - AmbientLightSensor.enable(); + if (typeof AmbientLightSensor !== 'undefined' && typeof AmbientLightSensor.enable === 'function') + AmbientLightSensor.enable(); + else if (typeof AmbientLightSensor !== 'undefined') + AmbientLightSensor.active = true; Logger.info("TestApp", "Enabled ambient light sensor"); if (testApp) { testApp.passedTests++; @@ -85,7 +88,10 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - AmbientLightSensor.disable(); + if (typeof AmbientLightSensor !== 'undefined' && typeof AmbientLightSensor.disable === 'function') + AmbientLightSensor.disable(); + else if (typeof AmbientLightSensor !== 'undefined') + AmbientLightSensor.active = false; Logger.info("TestApp", "Disabled ambient light sensor"); if (testApp) { testApp.passedTests++; @@ -133,7 +139,10 @@ Item { variant: "primary" onClicked: { HapticService.light(); - ProximitySensor.enable(); + if (typeof ProximitySensor !== 'undefined' && typeof ProximitySensor.enable === 'function') + ProximitySensor.enable(); + else if (typeof ProximitySensor !== 'undefined') + ProximitySensor.active = true; Logger.info("TestApp", "Enabled proximity sensor"); if (testApp) { testApp.passedTests++; @@ -147,7 +156,10 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - ProximitySensor.disable(); + if (typeof ProximitySensor !== 'undefined' && typeof ProximitySensor.disable === 'function') + ProximitySensor.disable(); + else if (typeof ProximitySensor !== 'undefined') + ProximitySensor.active = false; Logger.info("TestApp", "Disabled proximity sensor"); if (testApp) { testApp.passedTests++; diff --git a/apps/test/pages/TelephonyTestsPage.qml b/apps/test/pages/TelephonyTestsPage.qml index e9156a08c..62cfe5449 100644 --- a/apps/test/pages/TelephonyTestsPage.qml +++ b/apps/test/pages/TelephonyTestsPage.qml @@ -1,9 +1,9 @@ -import QtQuick import MarathonApp.Test import MarathonOS.Shell +import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme -import MarathonUI.Containers +import QtQuick Item { Flickable { @@ -13,18 +13,21 @@ Item { Column { id: telephonyColumn + width: parent.width spacing: MSpacing.md padding: MSpacing.lg Row { spacing: MSpacing.sm + Icon { name: "phone" size: 24 color: MColors.accent anchors.verticalCenter: parent.verticalCenter } + MLabel { text: "Telephony & SMS" variant: "headline" @@ -60,7 +63,7 @@ Item { variant: "primary" onClicked: { HapticService.light(); - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined' && typeof TelephonyService.simulateIncomingCall === 'function') { TelephonyService.simulateIncomingCall("+1234567890"); Logger.info("TestApp", "Simulated incoming call"); if (testApp) { @@ -68,7 +71,7 @@ Item { testApp.totalTests++; } } else { - Logger.error("TestApp", "TelephonyService not available"); + Logger.warn("TestApp", "TelephonyService.simulateIncomingCall not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; @@ -82,8 +85,8 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - if (typeof TelephonyService !== 'undefined') { - if (typeof ContactsManager !== 'undefined') { + if (typeof TelephonyService !== 'undefined' && typeof TelephonyService.simulateIncomingCall === 'function') { + if (typeof ContactsManager !== 'undefined' && typeof ContactsManager.addContact === 'function') { ContactsManager.addContact("John Doe", "+1555123456", "john@example.com"); Logger.info("TestApp", "Added test contact"); } @@ -94,7 +97,7 @@ Item { testApp.totalTests++; } } else { - Logger.error("TestApp", "TelephonyService not available"); + Logger.warn("TestApp", "TelephonyService.simulateIncomingCall not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; @@ -134,7 +137,7 @@ Item { variant: "primary" onClicked: { HapticService.light(); - if (typeof SMSService !== 'undefined') { + if (typeof SMSService !== 'undefined' && typeof SMSService.simulateIncomingSMS === 'function') { SMSService.simulateIncomingSMS("+1234567890", "Hey! This is a test message from the Marathon Test Suite."); Logger.info("TestApp", "Simulated incoming SMS"); if (testApp) { @@ -142,7 +145,7 @@ Item { testApp.totalTests++; } } else { - Logger.error("TestApp", "SMSService not available"); + Logger.warn("TestApp", "SMSService.simulateIncomingSMS not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; @@ -156,7 +159,7 @@ Item { variant: "accent" onClicked: { HapticService.light(); - if (typeof SMSService !== 'undefined') { + if (typeof SMSService !== 'undefined' && typeof SMSService.simulateIncomingSMS === 'function') { for (var i = 0; i < 3; i++) { SMSService.simulateIncomingSMS("+1555987654" + i, "Message " + (i + 1) + " from test suite"); } @@ -166,7 +169,7 @@ Item { testApp.totalTests++; } } else { - Logger.error("TestApp", "SMSService not available"); + Logger.warn("TestApp", "SMSService.simulateIncomingSMS not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; @@ -207,7 +210,7 @@ Item { variant: "primary" onClicked: { HapticService.light(); - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined' && typeof TelephonyService.simulateCallStateChange === 'function') { TelephonyService.simulateCallStateChange("active"); Logger.info("TestApp", "Call state: active"); if (testApp) { @@ -215,6 +218,7 @@ Item { testApp.totalTests++; } } else { + Logger.warn("TestApp", "simulateCallStateChange not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; @@ -228,7 +232,7 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined' && typeof TelephonyService.simulateCallStateChange === 'function') { TelephonyService.simulateCallStateChange("ringing"); Logger.info("TestApp", "Call state: ringing"); if (testApp) { @@ -236,6 +240,7 @@ Item { testApp.totalTests++; } } else { + Logger.warn("TestApp", "simulateCallStateChange not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; @@ -249,7 +254,7 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined' && typeof TelephonyService.simulateCallStateChange === 'function') { TelephonyService.simulateCallStateChange("idle"); Logger.info("TestApp", "Call state: idle"); if (testApp) { @@ -257,6 +262,7 @@ Item { testApp.totalTests++; } } else { + Logger.warn("TestApp", "simulateCallStateChange not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; @@ -270,10 +276,11 @@ Item { variant: "danger" onClicked: { HapticService.light(); - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined' && typeof TelephonyService.simulateIncomingCall === 'function') { TelephonyService.simulateIncomingCall("+1555111222"); Qt.callLater(function () { - TelephonyService.simulateCallStateChange("terminated"); + if (typeof TelephonyService.simulateCallStateChange === 'function') + TelephonyService.simulateCallStateChange("terminated"); }); Logger.info("TestApp", "Simulated missed call"); if (testApp) { @@ -281,6 +288,7 @@ Item { testApp.totalTests++; } } else { + Logger.warn("TestApp", "simulateIncomingCall not available (shell-only)"); if (testApp) { testApp.failedTests++; testApp.totalTests++; diff --git a/shell/qml/components/WaylandShellSurfaceItem.qml b/shell/qml/components/WaylandShellSurfaceItem.qml index 2f857a363..58e4438e4 100644 --- a/shell/qml/components/WaylandShellSurfaceItem.qml +++ b/shell/qml/components/WaylandShellSurfaceItem.qml @@ -130,7 +130,16 @@ ShellSurfaceItem { autoCreatePopupItems: true opacity: hasSentInitialSize && hasFirstFrame ? 1 : 0 bufferLocked: isMinimized - shellSurface: _xdgSurfaceFromObj(surfaceObj) + // CRITICAL: Only assign shellSurface when the underlying surface is fully initialized. + // Qt's QWaylandQuickShellSurfaceItem::setShellSurface connects signals to the surface, + // and will SIGSEGV if xdgSurface.surface is null during the handoff. + shellSurface: { + var xdg = _xdgSurfaceFromObj(surfaceObj); + if (xdg && xdg.surface) + return xdg; + + return null; + } touchEventsEnabled: true focusOnClick: true // Ensure keyboard focus on click output: AppLaunchService.compositor ? AppLaunchService.compositor.output : null From 79e4dd27751206e55929c30ffb5b6c2327f53df6 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:30:32 +0000 Subject: [PATCH 02/28] refactor: group shell sources by domain Move shell C++ services, managers, models, controllers, and components into scoped directories and update build wiring/includes to match the new layout. --- shell/CMakeLists.txt | 208 +++++----- shell/main.cpp | 312 +++++---------- shell/src/{ => components}/bluetoothagent.cpp | 20 +- shell/src/{ => components}/bluetoothagent.h | 28 +- shell/src/{ => components}/crashhandler.cpp | 21 +- shell/src/components/crashhandler.h | 32 ++ .../{ => components}/desktopfileparser.cpp | 118 +++--- .../src/{ => components}/desktopfileparser.h | 2 +- .../{ => components}/lunasvgimageprovider.cpp | 28 +- shell/src/components/lunasvgimageprovider.h | 15 + shell/src/{ => components}/mpris_types.h | 3 +- shell/src/{ => components}/portalmanager.cpp | 15 +- shell/src/{ => components}/portalmanager.h | 10 +- shell/src/{ => components}/ratelimiter.h | 0 shell/src/{ => components}/securitylogger.h | 0 .../audiopolicycontroller.cpp | 15 +- .../{ => controllers}/audiopolicycontroller.h | 0 .../{ => controllers}/audioroutingmanager.cpp | 23 +- .../{ => controllers}/audioroutingmanager.h | 62 +-- .../displaypolicycontroller.cpp | 4 +- .../displaypolicycontroller.h | 8 - .../powerpolicycontroller.cpp | 26 +- .../{ => controllers}/powerpolicycontroller.h | 40 +- shell/src/crashhandler.h | 45 --- shell/src/dbus/freedesktopnotifications.cpp | 42 +- shell/src/dbus/freedesktopnotifications.h | 2 +- shell/src/dbus/notificationdatabase.h | 2 +- shell/src/ipc/shellipcserver.cpp | 46 ++- shell/src/ipc/shellipcserver.h | 31 +- shell/src/ipc/shellservices.cpp | 213 +++++++++- shell/src/ipc/shellservices.h | 116 +++++- shell/src/lunasvgimageprovider.h | 32 -- shell/src/managers/alarmmanagercpp.cpp | 368 ++++++++++++++++++ shell/src/managers/alarmmanagercpp.h | 91 +++++ shell/src/{ => managers}/audiomanagercpp.cpp | 32 +- shell/src/{ => managers}/audiomanagercpp.h | 4 +- shell/src/{ => managers}/bluetoothmanager.cpp | 47 +-- shell/src/{ => managers}/bluetoothmanager.h | 3 +- shell/src/managers/clipboardmanagercpp.cpp | 106 +++++ shell/src/managers/clipboardmanagercpp.h | 44 +++ shell/src/{ => managers}/cursormanager.cpp | 11 +- shell/src/{ => managers}/cursormanager.h | 7 - .../src/{ => managers}/displaymanagercpp.cpp | 64 +-- shell/src/{ => managers}/displaymanagercpp.h | 16 +- .../{ => managers}/flashlightmanagercpp.cpp | 11 +- .../src/{ => managers}/flashlightmanagercpp.h | 4 +- shell/src/{ => managers}/hapticmanager.cpp | 34 +- shell/src/{ => managers}/hapticmanager.h | 8 +- shell/src/{ => managers}/locationmanager.cpp | 13 +- shell/src/{ => managers}/locationmanager.h | 3 +- .../marathoninputmethodengine.cpp | 5 +- .../marathoninputmethodengine.h | 0 shell/src/{ => managers}/modemmanagercpp.cpp | 53 +-- shell/src/{ => managers}/modemmanagercpp.h | 18 +- shell/src/{ => managers}/mpris2controller.cpp | 79 +--- shell/src/{ => managers}/mpris2controller.h | 76 ++-- .../src/{ => managers}/networkmanagercpp.cpp | 124 ++---- shell/src/{ => managers}/networkmanagercpp.h | 32 +- shell/src/{ => managers}/platform.h | 0 shell/src/managers/platformcpp.h | 104 +++++ shell/src/{ => managers}/powermanagercpp.cpp | 92 ++--- shell/src/{ => managers}/powermanagercpp.h | 101 +++-- shell/src/{ => managers}/rotationmanager.cpp | 12 +- shell/src/{ => managers}/rotationmanager.h | 2 +- shell/src/{ => managers}/rtscheduler.cpp | 12 +- shell/src/managers/rtscheduler.h | 37 ++ shell/src/{ => managers}/screenmetrics.cpp | 0 shell/src/{ => managers}/screenmetrics.h | 1 - shell/src/{ => managers}/securitymanager.cpp | 0 shell/src/managers/securitymanager.h | 133 +++++++ shell/src/{ => managers}/sensormanagercpp.cpp | 5 +- shell/src/{ => managers}/sensormanagercpp.h | 0 shell/src/{ => managers}/settingsmanager.cpp | 57 +-- shell/src/{ => managers}/settingsmanager.h | 237 +++++------ shell/src/marathonpermissionmanager.cpp | 118 ++++-- shell/src/marathonpermissionmanager.h | 60 +-- shell/src/{ => models}/appmodel.cpp | 20 +- shell/src/{ => models}/appmodel.h | 4 +- shell/src/{ => models}/notificationmodel.cpp | 11 +- shell/src/{ => models}/notificationmodel.h | 3 +- shell/src/{ => models}/taskmodel.cpp | 11 +- shell/src/{ => models}/taskmodel.h | 8 +- shell/src/platformcpp.h | 23 -- shell/src/rtscheduler.h | 74 ---- shell/src/securitymanager.h | 144 ------- shell/src/{ => services}/applaunchservice.cpp | 3 +- shell/src/{ => services}/applaunchservice.h | 45 +-- .../{ => services}/applifecyclemanager.cpp | 24 +- .../src/{ => services}/applifecyclemanager.h | 6 - .../marathonappstoreservice.cpp | 18 +- .../{ => services}/marathonappstoreservice.h | 10 +- shell/src/services/navigationroutercpp.cpp | 142 +++++++ shell/src/services/navigationroutercpp.h | 72 ++++ shell/src/services/notificationhandlercpp.cpp | 90 +++++ shell/src/services/notificationhandlercpp.h | 32 ++ shell/src/services/notificationservicecpp.cpp | 293 ++++++++++++++ shell/src/services/notificationservicecpp.h | 108 +++++ shell/src/services/powerbatteryhandlercpp.cpp | 50 +++ shell/src/services/powerbatteryhandlercpp.h | 36 ++ shell/src/services/screenshotservicecpp.cpp | 124 ++++++ shell/src/services/screenshotservicecpp.h | 52 +++ shell/src/services/statemanagercpp.cpp | 131 +++++++ shell/src/services/statemanagercpp.h | 37 ++ .../src/services/statusbariconservicecpp.cpp | 98 +++++ shell/src/services/statusbariconservicecpp.h | 27 ++ .../src/services/telephonyintegrationcpp.cpp | 203 ++++++++++ shell/src/services/telephonyintegrationcpp.h | 104 +++++ .../src/services/unifiedsearchservicecpp.cpp | 366 +++++++++++++++++ shell/src/services/unifiedsearchservicecpp.h | 88 +++++ shell/src/wayland/waylandcompositor.cpp | 1 - shell/src/wayland/waylandcompositormanager.h | 2 +- 111 files changed, 4276 insertions(+), 1927 deletions(-) rename shell/src/{ => components}/bluetoothagent.cpp (92%) rename shell/src/{ => components}/bluetoothagent.h (74%) rename shell/src/{ => components}/crashhandler.cpp (85%) create mode 100644 shell/src/components/crashhandler.h rename shell/src/{ => components}/desktopfileparser.cpp (69%) rename shell/src/{ => components}/desktopfileparser.h (96%) rename shell/src/{ => components}/lunasvgimageprovider.cpp (67%) create mode 100644 shell/src/components/lunasvgimageprovider.h rename shell/src/{ => components}/mpris_types.h (86%) rename shell/src/{ => components}/portalmanager.cpp (93%) rename shell/src/{ => components}/portalmanager.h (83%) rename shell/src/{ => components}/ratelimiter.h (100%) rename shell/src/{ => components}/securitylogger.h (100%) rename shell/src/{ => controllers}/audiopolicycontroller.cpp (93%) rename shell/src/{ => controllers}/audiopolicycontroller.h (100%) rename shell/src/{ => controllers}/audioroutingmanager.cpp (91%) rename shell/src/{ => controllers}/audioroutingmanager.h (51%) rename shell/src/{ => controllers}/displaypolicycontroller.cpp (95%) rename shell/src/{ => controllers}/displaypolicycontroller.h (83%) rename shell/src/{ => controllers}/powerpolicycontroller.cpp (87%) rename shell/src/{ => controllers}/powerpolicycontroller.h (64%) delete mode 100644 shell/src/crashhandler.h delete mode 100644 shell/src/lunasvgimageprovider.h create mode 100644 shell/src/managers/alarmmanagercpp.cpp create mode 100644 shell/src/managers/alarmmanagercpp.h rename shell/src/{ => managers}/audiomanagercpp.cpp (91%) rename shell/src/{ => managers}/audiomanagercpp.h (97%) rename shell/src/{ => managers}/bluetoothmanager.cpp (89%) rename shell/src/{ => managers}/bluetoothmanager.h (98%) create mode 100644 shell/src/managers/clipboardmanagercpp.cpp create mode 100644 shell/src/managers/clipboardmanagercpp.h rename shell/src/{ => managers}/cursormanager.cpp (85%) rename shell/src/{ => managers}/cursormanager.h (75%) rename shell/src/{ => managers}/displaymanagercpp.cpp (84%) rename shell/src/{ => managers}/displaymanagercpp.h (87%) rename shell/src/{ => managers}/flashlightmanagercpp.cpp (93%) rename shell/src/{ => managers}/flashlightmanagercpp.h (88%) rename shell/src/{ => managers}/hapticmanager.cpp (86%) rename shell/src/{ => managers}/hapticmanager.h (78%) rename shell/src/{ => managers}/locationmanager.cpp (92%) rename shell/src/{ => managers}/locationmanager.h (96%) rename shell/src/{ => managers}/marathoninputmethodengine.cpp (97%) rename shell/src/{ => managers}/marathoninputmethodengine.h (100%) rename shell/src/{ => managers}/modemmanagercpp.cpp (87%) rename shell/src/{ => managers}/modemmanagercpp.h (92%) rename shell/src/{ => managers}/mpris2controller.cpp (82%) rename shell/src/{ => managers}/mpris2controller.h (74%) rename shell/src/{ => managers}/networkmanagercpp.cpp (87%) rename shell/src/{ => managers}/networkmanagercpp.h (85%) rename shell/src/{ => managers}/platform.h (100%) create mode 100644 shell/src/managers/platformcpp.h rename shell/src/{ => managers}/powermanagercpp.cpp (85%) rename shell/src/{ => managers}/powermanagercpp.h (68%) rename shell/src/{ => managers}/rotationmanager.cpp (89%) rename shell/src/{ => managers}/rotationmanager.h (98%) rename shell/src/{ => managers}/rtscheduler.cpp (96%) create mode 100644 shell/src/managers/rtscheduler.h rename shell/src/{ => managers}/screenmetrics.cpp (100%) rename shell/src/{ => managers}/screenmetrics.h (93%) rename shell/src/{ => managers}/securitymanager.cpp (100%) create mode 100644 shell/src/managers/securitymanager.h rename shell/src/{ => managers}/sensormanagercpp.cpp (94%) rename shell/src/{ => managers}/sensormanagercpp.h (100%) rename shell/src/{ => managers}/settingsmanager.cpp (95%) rename shell/src/{ => managers}/settingsmanager.h (70%) rename shell/src/{ => models}/appmodel.cpp (90%) rename shell/src/{ => models}/appmodel.h (97%) rename shell/src/{ => models}/notificationmodel.cpp (94%) rename shell/src/{ => models}/notificationmodel.h (97%) rename shell/src/{ => models}/taskmodel.cpp (93%) rename shell/src/{ => models}/taskmodel.h (94%) delete mode 100644 shell/src/platformcpp.h delete mode 100644 shell/src/rtscheduler.h delete mode 100644 shell/src/securitymanager.h rename shell/src/{ => services}/applaunchservice.cpp (99%) rename shell/src/{ => services}/applaunchservice.h (65%) rename shell/src/{ => services}/applifecyclemanager.cpp (89%) rename shell/src/{ => services}/applifecyclemanager.h (91%) rename shell/src/{ => services}/marathonappstoreservice.cpp (94%) rename shell/src/{ => services}/marathonappstoreservice.h (90%) create mode 100644 shell/src/services/navigationroutercpp.cpp create mode 100644 shell/src/services/navigationroutercpp.h create mode 100644 shell/src/services/notificationhandlercpp.cpp create mode 100644 shell/src/services/notificationhandlercpp.h create mode 100644 shell/src/services/notificationservicecpp.cpp create mode 100644 shell/src/services/notificationservicecpp.h create mode 100644 shell/src/services/powerbatteryhandlercpp.cpp create mode 100644 shell/src/services/powerbatteryhandlercpp.h create mode 100644 shell/src/services/screenshotservicecpp.cpp create mode 100644 shell/src/services/screenshotservicecpp.h create mode 100644 shell/src/services/statemanagercpp.cpp create mode 100644 shell/src/services/statemanagercpp.h create mode 100644 shell/src/services/statusbariconservicecpp.cpp create mode 100644 shell/src/services/statusbariconservicecpp.h create mode 100644 shell/src/services/telephonyintegrationcpp.cpp create mode 100644 shell/src/services/telephonyintegrationcpp.h create mode 100644 shell/src/services/unifiedsearchservicecpp.cpp create mode 100644 shell/src/services/unifiedsearchservicecpp.h diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 87df118ff..bca837ebc 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -1,95 +1,116 @@ set(SOURCES main.cpp - src/desktopfileparser.h - src/desktopfileparser.cpp - src/lunasvgimageprovider.h - src/lunasvgimageprovider.cpp - src/appmodel.h - src/appmodel.cpp - src/applaunchservice.h - src/applaunchservice.cpp - src/applifecyclemanager.h - src/applifecyclemanager.cpp + src/components/desktopfileparser.h + src/components/desktopfileparser.cpp + src/components/lunasvgimageprovider.h + src/components/lunasvgimageprovider.cpp + src/models/appmodel.h + src/models/appmodel.cpp + src/services/applaunchservice.h + src/services/applaunchservice.cpp + src/services/applifecyclemanager.h + src/services/applifecyclemanager.cpp src/ipc/shellipcserver.h src/ipc/shellipcserver.cpp src/ipc/shellservices.h src/ipc/shellservices.cpp - src/taskmodel.h - src/taskmodel.cpp - src/notificationmodel.h - src/notificationmodel.cpp - src/crashhandler.h - src/crashhandler.cpp + src/models/taskmodel.h + src/models/taskmodel.cpp + src/services/statemanagercpp.h + src/services/statemanagercpp.cpp + src/services/unifiedsearchservicecpp.h + src/services/unifiedsearchservicecpp.cpp + src/models/notificationmodel.h + src/models/notificationmodel.cpp + src/services/notificationhandlercpp.h + src/services/notificationhandlercpp.cpp + src/services/notificationservicecpp.h + src/services/notificationservicecpp.cpp + src/services/telephonyintegrationcpp.h + src/services/telephonyintegrationcpp.cpp + src/components/crashhandler.h + src/components/crashhandler.cpp qml/keyboard/Data/WordEngine.h qml/keyboard/Data/WordEngine.cpp qml/keyboard/Data/WordTrie.h qml/keyboard/Data/WordTrie.cpp - src/networkmanagercpp.h - src/networkmanagercpp.cpp - src/powermanagercpp.h - src/powermanagercpp.cpp - src/powerpolicycontroller.h - src/powerpolicycontroller.cpp - src/displaypolicycontroller.h - src/displaypolicycontroller.cpp - src/displaymanagercpp.h - src/displaymanagercpp.cpp - src/audiomanagercpp.h - src/audiomanagercpp.cpp - src/modemmanagercpp.h - src/modemmanagercpp.cpp - src/sensormanagercpp.h - src/sensormanagercpp.cpp - src/settingsmanager.h - src/settingsmanager.cpp - src/platformcpp.h - src/bluetoothmanager.h - src/bluetoothmanager.cpp - src/bluetoothagent.h - src/bluetoothagent.cpp - src/marathoninputmethodengine.h - src/marathoninputmethodengine.cpp - src/marathoninputmethodengine.h - src/marathoninputmethodengine.cpp - src/rtscheduler.h - src/rtscheduler.cpp + src/managers/networkmanagercpp.h + src/managers/networkmanagercpp.cpp + src/managers/powermanagercpp.h + src/managers/powermanagercpp.cpp + src/controllers/powerpolicycontroller.h + src/controllers/powerpolicycontroller.cpp + src/services/powerbatteryhandlercpp.h + src/services/powerbatteryhandlercpp.cpp + src/controllers/displaypolicycontroller.h + src/controllers/displaypolicycontroller.cpp + src/managers/displaymanagercpp.h + src/managers/displaymanagercpp.cpp + src/services/screenshotservicecpp.h + src/services/screenshotservicecpp.cpp + src/managers/audiomanagercpp.h + src/managers/audiomanagercpp.cpp + src/managers/modemmanagercpp.h + src/managers/modemmanagercpp.cpp + src/managers/sensormanagercpp.h + src/managers/sensormanagercpp.cpp + src/managers/settingsmanager.h + src/managers/settingsmanager.cpp + src/managers/platform.h + src/managers/platformcpp.h + src/services/navigationroutercpp.h + src/services/navigationroutercpp.cpp + src/managers/bluetoothmanager.h + src/managers/bluetoothmanager.cpp + src/components/bluetoothagent.h + src/components/bluetoothagent.cpp + src/managers/marathoninputmethodengine.h + src/managers/marathoninputmethodengine.cpp + src/managers/rtscheduler.h + src/managers/rtscheduler.cpp src/dbus/freedesktopnotifications.h src/dbus/freedesktopnotifications.cpp src/dbus/notificationdatabase.h src/dbus/notificationdatabase.cpp src/marathonpermissionmanager.h src/marathonpermissionmanager.cpp - src/portalmanager.h - src/portalmanager.cpp - src/marathonappstoreservice.h - src/marathonappstoreservice.cpp + src/components/portalmanager.h + src/components/portalmanager.cpp + src/services/marathonappstoreservice.h + src/services/marathonappstoreservice.cpp # (Contacts/SMS/Telephony/Media/Music moved to MarathonServices) - src/mpris2controller.h - src/mpris2controller.cpp - src/rotationmanager.h - src/rotationmanager.cpp - src/locationmanager.h - src/locationmanager.cpp - src/hapticmanager.h - src/hapticmanager.cpp - src/flashlightmanagercpp.h - src/flashlightmanagercpp.cpp - src/audioroutingmanager.h - src/audioroutingmanager.cpp - src/audiopolicycontroller.h - src/audiopolicycontroller.cpp - src/securitymanager.h - src/securitymanager.cpp - src/screenmetrics.h - src/screenmetrics.cpp + src/managers/mpris2controller.h + src/managers/mpris2controller.cpp + src/managers/rotationmanager.h + src/managers/rotationmanager.cpp + src/managers/locationmanager.h + src/managers/locationmanager.cpp + src/managers/clipboardmanagercpp.h + src/managers/clipboardmanagercpp.cpp + src/managers/hapticmanager.h + src/managers/hapticmanager.cpp + src/managers/alarmmanagercpp.h + src/managers/alarmmanagercpp.cpp + src/services/statusbariconservicecpp.h + src/services/statusbariconservicecpp.cpp + src/managers/flashlightmanagercpp.h + src/managers/flashlightmanagercpp.cpp + src/controllers/audioroutingmanager.h + src/controllers/audioroutingmanager.cpp + src/controllers/audiopolicycontroller.h + src/controllers/audiopolicycontroller.cpp + src/managers/securitymanager.h + src/managers/securitymanager.cpp + src/managers/screenmetrics.h + src/managers/screenmetrics.cpp ) list(APPEND SOURCES src/wayland/waylandcompositormanager.h src/wayland/waylandcompositormanager.cpp - src/cursormanager.h - src/cursormanager.cpp + src/managers/cursormanager.h + src/managers/cursormanager.cpp ) if(TARGET Qt6::WaylandCompositor) @@ -206,25 +227,7 @@ set(QML_FILES qml/core/SurfaceRegistry.qml qml/core/Router.qml qml/models/FilteredAppModel.qml - qml/services/Platform.qml - qml/services/AudioManager.qml - qml/services/NotificationService.qml - qml/services/LocationService.qml - qml/services/StatusBarIconService.qml - qml/services/ScreenshotService.qml - qml/services/HapticService.qml - qml/services/ClipboardService.qml - qml/services/NavigationRouter.qml - qml/services/UnifiedSearchService.qml - qml/services/NotificationHandler.qml - qml/services/TelephonyIntegration.qml - qml/services/PowerBatteryHandler.qml - qml/services/AlarmManager.qml - qml/services/CalendarManager.qml qml/services/CalendarWorker.js - qml/services/AmbientLightSensor.qml - qml/services/ProximitySensor.qml - qml/services/StateManager.qml ) set(JSON_FILES @@ -261,23 +264,6 @@ set(SINGLETON_FILES qml/core/Logger.qml qml/core/SurfaceRegistry.qml qml/core/Router.qml - qml/services/Platform.qml - qml/services/AudioManager.qml - qml/services/NotificationService.qml - qml/services/LocationService.qml - qml/services/StatusBarIconService.qml - qml/services/ScreenshotService.qml - qml/services/HapticService.qml - qml/services/ClipboardService.qml - qml/services/NavigationRouter.qml - qml/services/UnifiedSearchService.qml - qml/services/NotificationHandler.qml - qml/services/TelephonyIntegration.qml - qml/services/PowerBatteryHandler.qml - qml/services/AlarmManager.qml - qml/services/CalendarManager.qml - qml/services/AmbientLightSensor.qml - qml/services/ProximitySensor.qml qml/keyboard/Core/LanguageManager.qml qml/keyboard/Data/Dictionary.qml qml/keyboard/Data/AutoCorrect.qml @@ -321,6 +307,14 @@ qt6_add_qml_module(marathon-shell OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/shell/qml/MarathonOS/Shell ) +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/qmllint") + +set_property(TARGET marathon-shell PROPERTY QT_QML_IMPORT_PATH + "${CMAKE_BINARY_DIR}/qmllint" + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/shell/qml" +) + # Link the big resources to the target target_sources(marathon-shell PRIVATE ${BIG_RESOURCES}) @@ -334,6 +328,12 @@ pkg_check_modules(PULSEAUDIO REQUIRED libpulse) target_include_directories(marathon-shell PRIVATE ${PULSEAUDIO_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/src/components + ${CMAKE_CURRENT_SOURCE_DIR}/src/services + ${CMAKE_CURRENT_SOURCE_DIR}/src/managers + ${CMAKE_CURRENT_SOURCE_DIR}/src/models + ${CMAKE_CURRENT_SOURCE_DIR}/src/controllers ) find_package(Qt6 REQUIRED COMPONENTS Gui) diff --git a/shell/main.cpp b/shell/main.cpp index 36bfcf121..52be5cd8d 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -18,28 +18,32 @@ #include #endif -#include "src/desktopfileparser.h" -#include "src/crashhandler.h" -#include "src/appmodel.h" -#include "src/taskmodel.h" -#include "src/applaunchservice.h" -#include "src/applifecyclemanager.h" -#include "src/notificationmodel.h" -#include "src/networkmanagercpp.h" -#include "src/powermanagercpp.h" -#include "src/powerpolicycontroller.h" -#include "src/displaymanagercpp.h" -#include "src/displaypolicycontroller.h" -#include "src/audiomanagercpp.h" -#include "src/modemmanagercpp.h" -#include "src/sensormanagercpp.h" -#include "src/settingsmanager.h" -#include "src/bluetoothmanager.h" +#include "src/components/desktopfileparser.h" +#include "src/components/crashhandler.h" +#include "src/models/appmodel.h" +#include "src/models/taskmodel.h" +#include "src/managers/alarmmanagercpp.h" +#include "src/services/applaunchservice.h" +#include "src/services/applifecyclemanager.h" +#include "src/models/notificationmodel.h" +#include "src/services/notificationhandlercpp.h" +#include "src/services/notificationservicecpp.h" +#include "src/managers/networkmanagercpp.h" +#include "src/managers/powermanagercpp.h" +#include "src/controllers/powerpolicycontroller.h" +#include "src/managers/displaymanagercpp.h" +#include "src/controllers/displaypolicycontroller.h" +#include "src/services/powerbatteryhandlercpp.h" +#include "src/managers/audiomanagercpp.h" +#include "src/managers/modemmanagercpp.h" +#include "src/managers/sensormanagercpp.h" +#include "src/managers/settingsmanager.h" +#include "src/managers/bluetoothmanager.h" #include "marathonappregistry.h" #include "marathonappscanner.h" #include "marathonappinstaller.h" #include "src/marathonpermissionmanager.h" -#include "src/marathonappstoreservice.h" +#include "src/services/marathonappstoreservice.h" #include "contactsmanager.h" #include "telephonyservice.h" #include "callhistorymanager.h" @@ -47,21 +51,28 @@ #include "medialibrarymanager.h" #include "musiclibrarymanager.h" #include "src/wayland/waylandcompositormanager.h" -#include "src/marathoninputmethodengine.h" -#include "src/rtscheduler.h" -#include "src/cursormanager.h" -#include "src/lunasvgimageprovider.h" - -#include "src/mpris2controller.h" -#include "src/rotationmanager.h" -#include "src/locationmanager.h" -#include "src/hapticmanager.h" -#include "src/flashlightmanagercpp.h" -#include "src/audioroutingmanager.h" -#include "src/audiopolicycontroller.h" -#include "src/securitymanager.h" -#include "src/screenmetrics.h" -#include "src/platformcpp.h" +#include "src/managers/marathoninputmethodengine.h" +#include "src/managers/rtscheduler.h" +#include "src/managers/cursormanager.h" +#include "src/components/lunasvgimageprovider.h" + +#include "src/managers/mpris2controller.h" +#include "src/managers/rotationmanager.h" +#include "src/managers/locationmanager.h" +#include "src/managers/clipboardmanagercpp.h" +#include "src/managers/hapticmanager.h" +#include "src/managers/flashlightmanagercpp.h" +#include "src/controllers/audioroutingmanager.h" +#include "src/controllers/audiopolicycontroller.h" +#include "src/managers/securitymanager.h" +#include "src/managers/screenmetrics.h" +#include "src/managers/platformcpp.h" +#include "src/services/navigationroutercpp.h" +#include "src/services/statusbariconservicecpp.h" +#include "src/services/statemanagercpp.h" +#include "src/services/unifiedsearchservicecpp.h" +#include "src/services/telephonyintegrationcpp.h" +#include "src/services/screenshotservicecpp.h" #include "qml/keyboard/Data/WordEngine.h" #include "src/dbus/freedesktopnotifications.h" #include "src/dbus/notificationdatabase.h" @@ -87,24 +98,20 @@ static T *createObject(QQmlContext *ctx, const char *qmlName, Args &&...args) { return obj; } -// Custom message handler for logging Qt messages static QFile *logFile = nullptr; static QTextStream *logStream = nullptr; static QMutex logMutex; static bool g_debugEnabled = false; static void marathonMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - // Qt can call this concurrently from multiple threads (GUI/render/DBus/etc). + QMutexLocker locker(&logMutex); - // Only suppress truly harmless warnings (not in debug mode) if (!g_debugEnabled && type == QtWarningMsg) { - // In non-debug mode, suppress known benign warnings + if ((msg.contains("Could not connect") && (msg.contains("NetworkManager") || msg.contains("UPower"))) || - // Mesa/Wayland often prints these repeatedly in mixed GPU/VM environments. - // They can spam the logs and cause flush storms on embedded builds where - // info/debug output is compiled out, so warnings are the hot path. + msg.contains("libEGL warning: failed to get driver name for fd -1") || msg.contains("MESA-LOADER: failed to retrieve device information") || msg.contains("Failed to initialize EGL display")) { @@ -128,22 +135,18 @@ static void marathonMessageHandler(QtMsgType type, const QMessageLogCont logMessage += QString(" (%1:%2)").arg(context.file).arg(context.line); } - // Write to file if (logStream && logFile && logFile->isOpen()) { (*logStream) << logMessage << "\n"; - // Flushing every line is expensive on low-end storage. - // Flush only when debugging or for warnings and above. + if (g_debugEnabled || type >= QtWarningMsg) { logStream->flush(); } } - // Avoid spamming stderr in production; keep warnings+. if (g_debugEnabled || type >= QtWarningMsg) { fprintf(stderr, "%s\n", qPrintable(logMessage)); } - // For fatal errors, close log file before abort if (type == QtFatalMsg) { if (logFile) { if (logStream) { @@ -159,19 +162,14 @@ static void marathonMessageHandler(QtMsgType type, const QMessageLogCont } } -#include "src/mpris_types.h" +#include "src/components/mpris_types.h" int main(int argc, char *argv[]) { - // Enable local file reading for QML XMLHttpRequest (required for keyboard layouts) + qputenv("QML_XHR_ALLOW_FILE_READ", "1"); - // Register D-Bus types needed for MPRIS2 metadata (a{sv} -> QVariantMap) registerMprisTypes(); - // Enable software rendering for better compatibility in VMs or low-end hardware - // qputenv("QMLSCENE_DEVICE", "software"); // Commented out - let Qt choose best renderer - - // Check debug mode FIRST before setting any logging rules QString debugEnv = qgetenv("MARATHON_DEBUG"); bool debugEnabled = (debugEnv == "1" || debugEnv.toLower() == "true"); g_debugEnabled = debugEnabled; @@ -182,33 +180,29 @@ int main(int argc, char *argv[]) { qDebug() << "[Profiler] Startup begins..."; } - // Configure Qt logging based on debug mode const QString profileEnv = qgetenv("MARATHON_PROFILE"); const bool profileMode = (profileEnv == "1" || profileEnv.toLower() == "true"); if (debugEnabled) { - // Debug mode: enable OUR logs but suppress Qt internal spam + QString rules = - // Enable all our C++ service logs by default - QStringLiteral("*.debug=true\n") // Enable debug for our code - + QStringLiteral("*.info=true\n") + QStringLiteral("*.warning=true\n") + - QStringLiteral("*.error=true\n") - // AGGRESSIVE: Suppress ALL Qt internal debug spam + + QStringLiteral("*.debug=true\n") + QStringLiteral("*.info=true\n") + + QStringLiteral("*.warning=true\n") + QStringLiteral("*.error=true\n") + + QStringLiteral("qt.*.debug=false\n") + QStringLiteral("qt.*.info=false\n") + - QStringLiteral("qt.*.warning=true\n") // Keep Qt warnings/errors - // CRITICAL: Enable console.log() from QML (uses QtDebugMsg) - + QStringLiteral("qml.debug=true\n") // Enable QML console.log() - + QStringLiteral("js.debug=true\n") // Enable JS console.log() - + QStringLiteral("default.debug=true\n") // Enable default category debug - + QStringLiteral("default.info=true\n") + QStringLiteral("default.warning=true\n"); - - // Profiling mode: allow scenegraph timing categories (used by some Qt tools/env vars). + QStringLiteral("qt.*.warning=true\n") + + + QStringLiteral("qml.debug=true\n") + QStringLiteral("js.debug=true\n") + + QStringLiteral("default.debug=true\n") + QStringLiteral("default.info=true\n") + + QStringLiteral("default.warning=true\n"); + if (profileMode) { rules += QStringLiteral("qt.scenegraph.time.*=true\n"); rules += QStringLiteral("qt.scenegraph.time.renderloop=true\n"); } QLoggingCategory::setFilterRules(rules); } else { - // Production mode: filter out noisy categories + QString rules = QStringLiteral("*.debug=false\n") + QStringLiteral("*.info=false\n") + QStringLiteral("*.warning=true\n") + QStringLiteral("*.error=true\n") + QStringLiteral("qt.qpa.*=false\n") + QStringLiteral("qt.pointer.*=false\n") + @@ -228,41 +222,14 @@ int main(int argc, char *argv[]) { QtWebEngineQuick::initialize(); #endif - // CRITICAL: Disable Qt's automatic HiDPI scaling for the compositor window - // - // Problem: On HiDPI host displays (devicePixelRatio=2), Qt automatically doubles the window's - // internal resolution. For a 540x1140 window, Qt would render at 1080x2280 internally, then - // downscale to fit the window, causing blurriness in embedded Wayland apps. - // - // Solution: Set PassThrough policy to disable automatic scaling, ensuring 1:1 pixel mapping. - // Combined with m_output->setScaleFactor(1) in the compositor, this forces apps to render - // at the exact window size (540x1140) without any scaling artifacts. - // - // Must be called BEFORE creating QApplication. - // NOTE: We use QApplication (not QGuiApplication) to support QWidget-based components - // like QTermWidget. QApplication inherits from QGuiApplication and adds widget support. QApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); - // CRITICAL: Enable Touch Synthesis for Mouse Events (Trackball Support) - // This allows "Click and Drag" with the trackball to act like a touch swipe/scroll QCoreApplication::setAttribute(Qt::AA_SynthesizeTouchForUnhandledMouseEvents); QCoreApplication::setAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents); - QApplication app(argc, argv); - - // NOTE: Q20 trackball has native scroll mode via CapsLock key (firmware) - // No custom emulation needed - scroll wheel events sent automatically + QApplication app(argc, argv); - // ============================================================================ - // CRITICAL: Install Crash Protection (MITIGATION ONLY - NOT A FIX!) - // ============================================================================ - // This installs signal handlers to capture shell crashes and write useful diagnostics. - // NOTE: Marathon apps are launched out-of-process (strict isolation). These handlers - // do NOT "protect" the shell from external app process crashes; they only help with - // diagnosing faults within this process. - // - // ============================================================================ CrashHandler *crashHandler = CrashHandler::instance(); crashHandler->install(); crashHandler->setCrashCallback([](const QString &msg) { @@ -271,7 +238,6 @@ int main(int argc, char *argv[]) { }); qInfo() << "[Marathon] Crash protection installed (signal handlers active)"; - // Set RT priority for input handling (Priority 85 per Marathon OS spec) #ifdef Q_OS_LINUX struct sched_param param; param.sched_priority = 85; @@ -285,7 +251,6 @@ int main(int argc, char *argv[]) { } #endif - // Initialize logging QString logPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.marathon"; QDir logDir(logPath); if (!logDir.exists()) { @@ -309,13 +274,12 @@ int main(int argc, char *argv[]) { QQuickStyle::setStyle("Basic"); - // Debug mode was already checked at the start if (debugEnabled) { qDebug() << "Debug mode enabled via MARATHON_DEBUG"; } #ifdef HAVE_WAYLAND - // Register types needed for signal marshalling to QML + qmlRegisterUncreatableType("MarathonOS.Wayland", 1, 0, "WaylandSurface", "WaylandSurface cannot be created from QML"); qmlRegisterUncreatableType("MarathonOS.Wayland", 1, 0, "WaylandXdgSurface", @@ -323,7 +287,6 @@ int main(int argc, char *argv[]) { qmlRegisterUncreatableType("MarathonOS.Wayland", 1, 0, "WaylandCompositor", "WaylandCompositor is created in C++"); - // CRITICAL: Register pointer types for signal/slot marshalling across C++/QML boundary qRegisterMetaType("QWaylandSurface*"); qRegisterMetaType("QWaylandXdgSurface*"); qRegisterMetaType("QObject*"); @@ -335,47 +298,35 @@ int main(int argc, char *argv[]) { QQmlApplicationEngine engine; - // Register LunaSVG image provider for proper SVG rendering - // Qt's SVG module doesn't support clipPath, which breaks GNOME icons engine.addImageProvider("lunasvg", new LunaSvgImageProvider()); - // MarathonUI currently references image://lunasvgqrc/... for QRC SVGs - // Provide a compatibility alias so icons in task cards / app grids render correctly. + engine.addImageProvider("lunasvgqrc", new LunaSvgImageProvider()); qInfo() << "[MarathonShell] ✓ LunaSVG image provider registered"; auto *ctx = engine.rootContext(); - // Screen metrics (primary screen) for QML defaults — avoids hard-coded device sizes in QML. createObject(ctx, "ScreenMetricsCpp", &app); - // Initialize MPRIS2 Controller (media player control) createObject(ctx, "MPRIS2Controller", &app); qInfo() << "[MarathonShell] ✓ MPRIS2 media controller initialized"; - // CRITICAL: Create SettingsManager BEFORE compositor manager - // The compositor needs access to userScaleFactor for physical size calculation auto *settingsManager = createObject(ctx, "SettingsManagerCpp", &app); - // Register compositor manager (available on all platforms, returns null on unsupported platforms) createObject(ctx, "WaylandCompositorManager", &app); if (debugEnabled || profileMode) { qWarning() << "[Profiler] Compositor Manager initialized:" << timer.elapsed() << "ms"; } - // Set debug mode context property ctx->setContextProperty("MARATHON_DEBUG_ENABLED", debugEnabled); - // Expose Wayland availability to QML #ifdef HAVE_WAYLAND ctx->setContextProperty("HAVE_WAYLAND", true); #else ctx->setContextProperty("HAVE_WAYLAND", false); #endif - // Register DesktopFileParser as a singleton accessible from QML createObject(ctx, "DesktopFileParserCpp", &app); - // Register Marathon App System auto *appRegistry = createObject(ctx, "MarathonAppRegistry", &app); auto *appScanner = createObject(ctx, "MarathonAppScanner", appRegistry, &app); @@ -385,20 +336,18 @@ int main(int argc, char *argv[]) { qWarning() << "[Profiler] App System initialized:" << timer.elapsed() << "ms"; } - // Register Marathon Input Method Engine createObject(ctx, "InputMethodEngine", &app); qInfo() << "Input Method Engine initialized"; - // Register C++ models auto *appModel = createObject(ctx, "AppModel", &app); auto *taskModel = createObject(ctx, "TaskModel", &app); auto *notificationModel = createObject(ctx, "NotificationModel", &app); - // - // Register NotificationModel enums so they're accessible in QML + auto *navigationRouter = createObject(ctx, "NavigationRouter", &app); + createObject(ctx, "StateManager", &app); + qmlRegisterUncreatableMetaObject(NotificationModel::staticMetaObject, "MarathonOS.Shell", 1, 0, "NotificationRoles", "Cannot create NotificationRoles enum"); - // Register C++ services (SettingsManager already created above for compositor) auto *networkManager = createObject(ctx, "NetworkManagerCpp", &app); auto *powerManager = createObject(ctx, "PowerManagerService", &app); auto *rotationManager = createObject(ctx, "RotationManager", &app); @@ -406,22 +355,24 @@ int main(int argc, char *argv[]) { rotationManager, &app); auto *audioManager = createObject(ctx, "AudioManagerCpp", &app); createObject(ctx, "ModemManagerCpp", &app); - createObject(ctx, "SensorManagerCpp", &app); + auto *sensorManager = createObject(ctx, "SensorManagerCpp", &app); auto *bluetoothManager = createObject(ctx, "BluetoothManagerCpp", &app); - createObject(ctx, "LocationManager", &app); - createObject(ctx, "HapticManager", &app); + auto *locationManager = createObject(ctx, "LocationManager", &app); + auto *hapticManager = createObject(ctx, "HapticManager", &app); + createObject(ctx, "ClipboardManagerCpp", settingsManager, &app); + auto *alarmManager = createObject( + ctx, "AlarmManagerCpp", settingsManager, powerManager, audioManager, hapticManager, &app); createObject(ctx, "FlashlightManagerCpp", &app); auto *audioRoutingManager = createObject(ctx, "AudioRoutingManagerCpp", &app); createObject(ctx, "SecurityManagerCpp", &app); - // App launch orchestration (C++). auto *appLaunchService = createObject(ctx, "AppLaunchService", appModel, taskModel, &app); + createObject(ctx, "UnifiedSearchService", appModel, appRegistry, + appScanner, settingsManager, navigationRouter, + appLaunchService, &app); - // Optional dev helper: auto-launch a specific app at startup (useful for debugging IPC/apps). - // Example: - // MARATHON_AUTO_LAUNCH_APP_ID=settings MARATHON_DEBUG=1 ./run.sh const QByteArray autoLaunchRaw = qgetenv("MARATHON_AUTO_LAUNCH_APP_ID").trimmed(); if (!autoLaunchRaw.isEmpty()) { const QString autoAppId = QString::fromLocal8Bit(autoLaunchRaw); @@ -442,34 +393,34 @@ int main(int argc, char *argv[]) { QTimer::singleShot(3000, &app, tryLaunch); } - // App lifecycle orchestration (C++). Replaces the QML singleton AppLifecycleManager.qml. - // QML registers app instances via AppLifecycleManager.registerApp(appId, instance). auto *appLifecycleManager = new AppLifecycleManager(taskModel, appLaunchService, &app); ctx->setContextProperty("AppLifecycleManager", appLifecycleManager); - // Power policy/orchestration lives in C++ (Deepak: keep perf-sensitive system glue out of QML) + auto *hapticsObj = + qobject_cast(ctx->contextProperty("HapticManager").value()); + auto *powerPolicyController = new PowerPolicyController(powerManager, displayManager, &app); ctx->setContextProperty("PowerPolicyControllerCpp", powerPolicyController); - // Display policy/orchestration lives in C++ (Deepak: keep system glue out of QML) auto *displayPolicyController = new DisplayPolicyController(displayManager, settingsManager, &app); ctx->setContextProperty("DisplayPolicyControllerCpp", displayPolicyController); - - // Audio policy/orchestration lives in C++ (Deepak: keep system glue out of QML) - auto *hapticsObj = - qobject_cast(ctx->contextProperty("HapticManager").value()); + createObject(ctx, "PowerBatteryHandler", powerPolicyController, + displayPolicyController, displayManager, hapticsObj, &app); auto *audioPolicyController = new AudioPolicyController(audioManager, settingsManager, hapticsObj, &app); ctx->setContextProperty("AudioPolicyControllerCpp", audioPolicyController); + auto *notificationService = createObject( + ctx, "NotificationService", notificationModel, settingsManager, audioPolicyController, + hapticsObj, &app); + createObject(ctx, "ScreenshotService", audioPolicyController, hapticsObj, + notificationService, &app); - // Cursor auto-hide manager for EGLFS createObject(ctx, "CursorManager", &app); if (debugEnabled || profileMode) { qWarning() << "[Profiler] Hardware Managers initialized:" << timer.elapsed() << "ms"; } - // Wire AudioManager to PowerManager for audio playback wakelocks QObject::connect(audioManager, &AudioManagerCpp::isPlayingChanged, powerManager, [powerManager, audioManager]() { if (audioManager->isPlaying()) { @@ -484,17 +435,16 @@ int main(int argc, char *argv[]) { }); qInfo() << "[MarathonShell] ✓ Audio playback wakelock integration enabled"; - // Platform utilities (hardware detection, etc.) - createObject(ctx, "PlatformCpp", &app); + auto *platform = createObject(ctx, "Platform", &app); + ctx->setContextProperty("PlatformCpp", platform); + createObject(ctx, "StatusBarIconService", &app); qInfo() << "[MarathonShell] ✓ Security Manager initialized (PAM + fprintd)"; - // Word Engine for spell-checking and predictions auto *wordEngine = createObject(ctx, "WordEngine", &app); wordEngine->setLanguage("en_US"); wordEngine->setEnabled(true); qInfo() << "[MarathonShell] ✓ Word Engine initialized"; - // Register RT Scheduler for thread priority management auto *rtScheduler = createObject(ctx, "RTScheduler", &app); if (rtScheduler->isRealtimeKernel()) { qInfo() << "[MarathonShell] RT Scheduler initialized (PREEMPT_RT kernel detected)"; @@ -502,7 +452,6 @@ int main(int argc, char *argv[]) { << "Priority:" << rtScheduler->getCurrentPriority(); } - // Initialize Marathon D-Bus Services qInfo() << "[MarathonShell] Initializing Marathon Service Bus (D-Bus)..."; QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { @@ -510,16 +459,13 @@ int main(int argc, char *argv[]) { } else { qInfo() << "[MarathonShell] ✓ Connected to D-Bus session bus"; - // Initialize NotificationDatabase NotificationDatabase *notifDb = new NotificationDatabase(&app); if (!notifDb->initialize()) { qWarning() << "[MarathonShell] Failed to initialize notification database"; } - // Load existing notifications from database into model notificationModel->loadFromDatabase(notifDb); - // Register freedesktop.org Notifications (standard interface for 3rd-party apps) auto *freedesktopNotif = createObject( ctx, "FreedesktopNotifications", notifDb, notificationModel, powerManager, &app); if (freedesktopNotif->registerService()) { @@ -532,26 +478,27 @@ int main(int argc, char *argv[]) { qDebug() << "[Profiler] DBus Services initialized:" << timer.elapsed() << "ms"; } - // Register Permission Manager (shell-local UI + DBus service for isolated apps) auto *permissionManager = createObject(ctx, "PermissionManager", &app); qInfo() << "[MarathonShell] ✓ Permission Manager initialized"; - // Register App Store Service createObject(ctx, "AppStoreService", appInstaller, &app); qInfo() << "[MarathonShell] ✓ App Store Service initialized"; - // Register Telephony & Messaging services auto *contactsManager = createObject(ctx, "ContactsManager", &app); auto *telephonyService = createObject(ctx, "TelephonyService", &app); auto *callHistoryManager = createObject(ctx, "CallHistoryManager", &app); auto *smsService = createObject(ctx, "SMSService", &app); + createObject(ctx, "NotificationHandler", notificationService, + navigationRouter, telephonyService, &app); + createObject( + ctx, "TelephonyIntegration", contactsManager, notificationService, powerPolicyController, + powerManager, displayPolicyController, displayManager, audioPolicyController, hapticsObj, + telephonyService, smsService, &app); - // Wire up contacts to call history for name resolution callHistoryManager->setContactsManager(contactsManager); smsService->setContactsManager(contactsManager); - // Wire AudioRoutingManager to TelephonyService for call audio routing QObject::connect(telephonyService, &TelephonyService::callStateChanged, audioRoutingManager, [audioRoutingManager](const QString &state) { if (state == "active" || state == "incoming") { @@ -562,24 +509,21 @@ int main(int argc, char *argv[]) { }); qInfo() << "[MarathonShell] ✓ Audio routing wired to telephony"; - // Register Media Library services (needed by Gallery app under strict isolation) auto *mediaLibraryManager = createObject(ctx, "MediaLibraryManager", &app); createObject(ctx, "MusicLibraryManager", &app); - // Register shell → app DBus IPC (no in-runner stubs; apps talk to the shell over DBus) { auto *ipc = new ShellIpcServer( permissionManager, contactsManager, callHistoryManager, telephonyService, smsService, mediaLibraryManager, settingsManager, bluetoothManager, displayManager, powerManager, - audioManager, audioPolicyController, networkManager, appLaunchService, &app); + audioManager, audioPolicyController, networkManager, hapticsObj, sensorManager, + locationManager, alarmManager, appLaunchService, &app); if (!ipc->registerOnSessionBus()) { qCritical() << "[MarathonShell] Failed to register app IPC on DBus (org.marathonos.Shell)"; } } - // Wire CallHistoryManager to TelephonyService for call logging - // Track call start time and calculate duration static qint64 callStartTime = 0; static QString lastCalledNumber; static bool wasIncoming = false; @@ -593,19 +537,19 @@ int main(int argc, char *argv[]) { QObject::connect(telephonyService, &TelephonyService::callStateChanged, callHistoryManager, [callHistoryManager, telephonyService](const QString &state) { if (state == "active" && callStartTime == 0) { - // Outgoing call started + callStartTime = QDateTime::currentMSecsSinceEpoch(); lastCalledNumber = telephonyService->activeNumber(); wasIncoming = false; } else if (state == "idle" || state == "terminated") { - // Call ended - calculate duration and log it + if (callStartTime > 0 && !lastCalledNumber.isEmpty()) { qint64 endTime = QDateTime::currentMSecsSinceEpoch(); - int duration = (endTime - callStartTime) / 1000; // seconds + int duration = (endTime - callStartTime) / 1000; QString callType; if (wasIncoming) { - // If duration > 0, call was answered, otherwise it was missed + callType = (duration > 0) ? "incoming" : "missed"; } else { callType = "outgoing"; @@ -616,7 +560,6 @@ int main(int argc, char *argv[]) { qInfo() << "[MarathonShell] ✓ Call logged:" << callType << lastCalledNumber << duration << "s"; - // Reset tracking callStartTime = 0; lastCalledNumber.clear(); wasIncoming = false; @@ -625,57 +568,27 @@ int main(int argc, char *argv[]) { }); qInfo() << "[MarathonShell] ✓ Call history wired to telephony"; - // Media library services already registered above (needed for DBus IPC too). - - // Note: Marathon apps are auto-initialized in AppModel constructor. - // Load any already-registered Marathon apps immediately (fast). appModel->loadFromRegistry(appRegistry); - // Add QML import path for embedded Qt resources - // NOTE: Only use one form - "qrc:/" and ":/" are equivalent and using both - // causes QML modules to be found twice, triggering "module is ambiguous" errors engine.addImportPath("qrc:/"); - // ============================================================================ - // CRITICAL: MarathonUI QML Module Import Paths - // - // MarathonUI is a separate QML module library that must be built and either: - // 1. Installed to a known location, OR - // 2. Found in the build directory (for development without install) - // - // Qt searches for QML modules in paths added via engine.addImportPath(). - // For a module URI like "MarathonUI.Theme", Qt looks for: - // /MarathonUI/Theme/qmldir - // ============================================================================ - - // Priority 1: User-local installation (recommended for development) - // This is where `cmake --install .` installs to by default (without sudo) QString userMarathonUIPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/marathon-ui"; engine.addImportPath(userMarathonUIPath); qDebug() << "[QML Import] User-local MarathonUI:" << userMarathonUIPath; - // Priority 2: System-wide installation (production deployments) - // This is where system packages would install MarathonUI - QString systemMarathonUIPath = "/usr/lib/qt6/qml"; // Parent of MarathonUI/ + QString systemMarathonUIPath = "/usr/lib/qt6/qml"; engine.addImportPath(systemMarathonUIPath); qDebug() << "[QML Import] System-wide Qt modules:" << systemMarathonUIPath; - // Priority 2b: Local System-wide installation (common on some distros/manual installs) QString localSystemMarathonUIPath = "/usr/local/lib/qt6/qml"; engine.addImportPath(localSystemMarathonUIPath); qDebug() << "[QML Import] Local System-wide Qt modules:" << localSystemMarathonUIPath; - // Priority 3: Build directory (for development without install) - // When running from build dir, MarathonUI modules are in build/MarathonUI/ - // QCoreApplication::applicationDirPath() = /build/shell - // So ../MarathonUI = /build/MarathonUI (WRONG - this doesn't exist!) - // Correct path: ../ = /build (contains MarathonUI/ subdirectory) QString buildMarathonUIPath = QCoreApplication::applicationDirPath() + "/.."; engine.addImportPath(buildMarathonUIPath); qDebug() << "[QML Import] Build directory:" << buildMarathonUIPath; - // Verify MarathonUI.Theme is loadable (most critical dependency) QDir themeCheck1(userMarathonUIPath + "/MarathonUI/Theme"); QDir themeCheck2(systemMarathonUIPath + "/MarathonUI/Theme"); QDir themeCheck2b(localSystemMarathonUIPath + "/MarathonUI/Theme"); @@ -715,8 +628,6 @@ int main(int argc, char *argv[]) { qCritical() << "========================================================================"; qCritical() << ""; - // Don't exit immediately - let QML engine fail with detailed error - // This helps users see which specific module import failed } else { qInfo() << "[MarathonShell] ✓ MarathonUI modules found"; if (themeCheck1.exists()) @@ -727,14 +638,11 @@ int main(int argc, char *argv[]) { qDebug() << " - Using build directory (development mode)"; } - // Qt 6.5+ uses ':/qt/qml/' as the default resource prefix for QML modules const QUrl url(QStringLiteral("qrc:/qt/qml/MarathonOS/Shell/qml/Main.qml")); - // Register custom D-Bus types qRegisterMetaType("GeoClueTimestamp"); qDBusRegisterMetaType(); - // Debug: List input devices to diagnose touchscreen issues const auto devices = QInputDevice::devices(); #if !defined(QT_NO_INFO_OUTPUT) qInfo() << "[MarathonShell] Detected Input Devices:"; @@ -763,17 +671,13 @@ int main(int argc, char *argv[]) { return -1; } - // Defer expensive scans until the UI is up and the event loop is running. QTimer::singleShot( 0, &app, [&app, settingsManager, appModel, appRegistry, appScanner, permissionManager]() { - // 1) Scan for native apps (.desktop). This can be slow on low-end storage. const QStringList searchPaths = { "/usr/share/applications", "/usr/local/share/applications", QDir::homePath() + "/.local/share/applications", - "/var/lib/flatpak/exports/share/applications", // System Flatpak apps - QDir::homePath() + - "/.local/share/flatpak/exports/share/applications" // User Flatpak apps - }; + "/var/lib/flatpak/exports/share/applications", + QDir::homePath() + "/.local/share/flatpak/exports/share/applications"}; const bool filterMobile = settingsManager->filterMobileFriendlyApps(); @@ -797,11 +701,6 @@ int main(int argc, char *argv[]) { appModel->addApps(nativeApps); #endif - // 2) Scan for Marathon apps (already supports async internally). - // - // IMPORTANT for strict multi-process isolation: - // Protected (built-in) apps must have their manifest-declared permissions granted up-front, - // otherwise they will spam "AccessDenied" on startup when the runner queries shell services. auto grantProtected = [permissionManager, appRegistry](const QString &appId) { if (!permissionManager || !appRegistry) return; @@ -817,7 +716,6 @@ int main(int argc, char *argv[]) { QObject::connect(appScanner, &MarathonAppScanner::appDiscovered, &app, grantProtected); QObject::connect(appScanner, &MarathonAppScanner::scanComplete, &app, [appModel, appRegistry, grantProtected](int) { - // Ensure any already-registered apps are also granted (safety net). for (const QString &id : appRegistry->getAllAppIds()) grantProtected(id); appModel->loadFromRegistry(appRegistry); diff --git a/shell/src/bluetoothagent.cpp b/shell/src/components/bluetoothagent.cpp similarity index 92% rename from shell/src/bluetoothagent.cpp rename to shell/src/components/bluetoothagent.cpp index 6b88e1b83..5346033e4 100644 --- a/shell/src/bluetoothagent.cpp +++ b/shell/src/components/bluetoothagent.cpp @@ -26,17 +26,15 @@ bool BluetoothAgent::registerAgent() { QDBusConnection bus = QDBusConnection::systemBus(); - // Register agent object on D-Bus if (!bus.registerObject(m_agentPath, this, QDBusConnection::ExportAllSlots)) { qWarning() << "[BluetoothAgent] Failed to register object:" << bus.lastError().message(); return false; } - // Register agent with BlueZ AgentManager QDBusInterface agentManager("org.bluez", "/org/bluez", "org.bluez.AgentManager1", bus); if (!agentManager.isValid()) { - // Only log as debug - expected when no bluetooth hardware is present + qDebug() << "[BluetoothAgent] BlueZ AgentManager not available:" << agentManager.lastError().message(); qDebug() << "[BluetoothAgent] This is expected when no bluetooth hardware is present"; @@ -44,7 +42,6 @@ bool BluetoothAgent::registerAgent() { return false; } - // Register agent with "KeyboardDisplay" capability - supports all pairing methods QDBusReply reply = agentManager.call("RegisterAgent", QVariant::fromValue(QDBusObjectPath(m_agentPath)), QString("KeyboardDisplay")); @@ -56,14 +53,12 @@ bool BluetoothAgent::registerAgent() { return false; } - // Request to be the default agent QDBusReply defaultReply = agentManager.call("RequestDefaultAgent", QVariant::fromValue(QDBusObjectPath(m_agentPath))); if (!defaultReply.isValid()) { qWarning() << "[BluetoothAgent] Failed to set as default agent:" << defaultReply.error().message(); - // This is not critical - continue anyway } m_registered = true; @@ -109,8 +104,7 @@ QString BluetoothAgent::RequestPinCode(const QDBusObjectPath &device) { emit pairingPinCodeRequested(devicePath, deviceName); - // Response will be sent via providePinCode() - return QString(); // Won't be used due to delayed reply + return QString(); } quint32 BluetoothAgent::RequestPasskey(const QDBusObjectPath &device) { @@ -127,8 +121,7 @@ quint32 BluetoothAgent::RequestPasskey(const QDBusObjectPath &device) { emit pairingPasskeyRequested(devicePath, deviceName); - // Response will be sent via providePasskey() - return 0; // Won't be used due to delayed reply + return 0; } void BluetoothAgent::DisplayPinCode(const QDBusObjectPath &device, const QString &pincode) { @@ -164,8 +157,6 @@ void BluetoothAgent::RequestConfirmation(const QDBusObjectPath &device, quint32 setDelayedReply(true); emit pairingConfirmationRequested(devicePath, deviceName, passkey); - - // Response will be sent via confirmPairing() } void BluetoothAgent::RequestAuthorization(const QDBusObjectPath &device) { @@ -180,8 +171,6 @@ void BluetoothAgent::RequestAuthorization(const QDBusObjectPath &device) { setDelayedReply(true); emit pairingAuthorizationRequested(devicePath, deviceName); - - // Response will be sent via authorizeConnection() } void BluetoothAgent::AuthorizeService(const QDBusObjectPath &device, const QString &uuid) { @@ -190,9 +179,6 @@ void BluetoothAgent::AuthorizeService(const QDBusObjectPath &device, const QStri qInfo() << "[BluetoothAgent] Service authorization requested for device:" << deviceName << "UUID:" << uuid; - - // Auto-authorize all services for now - // Could be made configurable in the future } void BluetoothAgent::Cancel() { diff --git a/shell/src/bluetoothagent.h b/shell/src/components/bluetoothagent.h similarity index 74% rename from shell/src/bluetoothagent.h rename to shell/src/components/bluetoothagent.h index 7c725668f..dbbb1683d 100644 --- a/shell/src/bluetoothagent.h +++ b/shell/src/components/bluetoothagent.h @@ -7,16 +7,6 @@ #include #include -/** - * BluetoothAgent - Implements org.bluez.Agent1 interface - * - * Handles Bluetooth pairing requests including: - * - PIN code requests - * - Passkey requests - * - Passkey display - * - Confirmation requests - * - Authorization requests - */ class BluetoothAgent : public QObject, protected QDBusContext { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.bluez.Agent1") @@ -29,7 +19,7 @@ class BluetoothAgent : public QObject, protected QDBusContext { void unregisterAgent(); public slots: - // org.bluez.Agent1 interface methods + void Release(); QString RequestPinCode(const QDBusObjectPath &device); quint32 RequestPasskey(const QDBusObjectPath &device); @@ -41,7 +31,7 @@ class BluetoothAgent : public QObject, protected QDBusContext { void Cancel(); signals: - // Signals to UI for user interaction + void pairingPinCodeRequested(const QString &devicePath, const QString &deviceName); void pairingPasskeyRequested(const QString &devicePath, const QString &deviceName); void pairingPasskeyDisplay(const QString &devicePath, const QString &deviceName, @@ -53,23 +43,21 @@ class BluetoothAgent : public QObject, protected QDBusContext { void pairingCompleted(const QString &devicePath, bool success); public: - // Methods for UI to respond to pairing requests void providePinCode(const QString &pinCode); void providePasskey(quint32 passkey); void confirmPairing(bool confirmed); void authorizeConnection(bool authorized); private: - QString getDeviceName(const QString &devicePath); - void sendDBusError(const QString &errorName, const QString &errorMessage); + QString getDeviceName(const QString &devicePath); + void sendDBusError(const QString &errorName, const QString &errorMessage); - QString m_agentPath; - QString m_pendingDevicePath; - bool m_registered; + QString m_agentPath; + QString m_pendingDevicePath; + bool m_registered; - // For synchronous D-Bus method responses QDBusMessage m_pendingMessage; bool m_waitingForResponse; }; -#endif // BLUETOOTHAGENT_H +#endif diff --git a/shell/src/crashhandler.cpp b/shell/src/components/crashhandler.cpp similarity index 85% rename from shell/src/crashhandler.cpp rename to shell/src/components/crashhandler.cpp index 59c919619..3758f52e6 100644 --- a/shell/src/crashhandler.cpp +++ b/shell/src/components/crashhandler.cpp @@ -39,7 +39,6 @@ void CrashHandler::install() { qDebug() << "[CrashHandler] Installing signal handlers"; setupSignalHandlers(); - // Set terminate handler for uncaught exceptions std::set_terminate([]() { qCritical() << "[CrashHandler] Uncaught exception detected!"; qCritical() << "[CrashHandler] This is a critical error - the app may be unstable"; @@ -48,7 +47,6 @@ void CrashHandler::install() { emit s_instance->crashDetected("EXCEPTION", "Uncaught C++ exception"); } - // Try to print backtrace #ifdef __GLIBC__ void *array[50]; size_t size = backtrace(array, 50); @@ -58,8 +56,6 @@ void CrashHandler::install() { qCritical() << "[CrashHandler] Backtrace not available (musl libc)"; #endif - // Don't call the default terminate handler (which would abort) - // Instead, try to continue (may not always work) qCritical() << "[CrashHandler] Attempting to continue (shell may be unstable)"; }); @@ -77,7 +73,7 @@ bool CrashHandler::isInCrashHandler() { } void CrashHandler::signalHandler(int signum) { - // Prevent recursive crash handling + if (s_inCrashHandler) { const char *msg = "[CrashHandler] Recursive crash detected - aborting\n"; ::write(STDERR_FILENO, msg, std::strlen(msg)); @@ -112,7 +108,6 @@ void CrashHandler::signalHandler(int signum) { break; } - // IMPORTANT: This is a signal handler. Avoid Qt/QString allocations here. writeStr(STDERR_FILENO, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); writeStr(STDERR_FILENO, "[CrashHandler] CRASH DETECTED!\n"); writeStr(STDERR_FILENO, "[CrashHandler] Signal: "); @@ -123,7 +118,6 @@ void CrashHandler::signalHandler(int signum) { writeStr(STDERR_FILENO, "\n"); writeStr(STDERR_FILENO, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); - // Print backtrace (best-effort). Use symbols_fd to avoid heap allocations. #ifdef __GLIBC__ void *array[50]; size_t size = backtrace(array, 50); @@ -137,18 +131,17 @@ void CrashHandler::signalHandler(int signum) { writeStr(STDERR_FILENO, "[CrashHandler] Shell will now exit (cannot safely continue)\n"); writeStr(STDERR_FILENO, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); - // Restore default handler and re-raise to get core dump signal(signum, SIG_DFL); raise(signum); } void CrashHandler::setupSignalHandlers() { - // Install handlers for various crash signals - signal(SIGSEGV, signalHandler); // Segmentation fault - signal(SIGABRT, signalHandler); // Abort - signal(SIGFPE, signalHandler); // Floating point exception - signal(SIGILL, signalHandler); // Illegal instruction - signal(SIGBUS, signalHandler); // Bus error + + signal(SIGSEGV, signalHandler); + signal(SIGABRT, signalHandler); + signal(SIGFPE, signalHandler); + signal(SIGILL, signalHandler); + signal(SIGBUS, signalHandler); qDebug() << "[CrashHandler] Installed signal handlers for: SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS"; diff --git a/shell/src/components/crashhandler.h b/shell/src/components/crashhandler.h new file mode 100644 index 000000000..ca748aadc --- /dev/null +++ b/shell/src/components/crashhandler.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +class CrashHandler : public QObject { + Q_OBJECT + + public: + static CrashHandler *instance(); + + void install(); + + void setCrashCallback(std::function callback); + + static bool isInCrashHandler(); + + signals: + void crashDetected(const QString &signal, const QString &message); + + private: + explicit CrashHandler(QObject *parent = nullptr); + ~CrashHandler() override; + + static void signalHandler(int signum); + static void setupSignalHandlers(); + + std::function m_crashCallback; + static CrashHandler *s_instance; + static bool s_inCrashHandler; +}; diff --git a/shell/src/desktopfileparser.cpp b/shell/src/components/desktopfileparser.cpp similarity index 69% rename from shell/src/desktopfileparser.cpp rename to shell/src/components/desktopfileparser.cpp index a161fc51a..86e62f310 100644 --- a/shell/src/desktopfileparser.cpp +++ b/shell/src/components/desktopfileparser.cpp @@ -10,7 +10,7 @@ DesktopFileParser::DesktopFileParser(QObject *parent) : QObject(parent) {} QVariantList DesktopFileParser::scanApplications(const QStringList &searchPaths) { - // Default: don't filter (for backwards compatibility) + return scanApplications(searchPaths, false); } @@ -37,7 +37,7 @@ QVariantList DesktopFileParser::scanApplications(const QStringList &searchPaths, for (const QFileInfo &fileInfo : desktopFiles) { QVariantMap app = parseDesktopFile(fileInfo.absoluteFilePath()); if (!app.isEmpty()) { - // Apply mobile-friendly filter if requested + if (filterMobileFriendly) { if (isMobileFriendly(app)) { apps.append(app); @@ -118,26 +118,24 @@ QVariantMap DesktopFileParser::parseDesktopFile(const QString &filePath) { app["hidden"] = (value.toLower() == "true"); } else if (key == "Type") { if (value != "Application") { - return QVariantMap(); // Not an application + return QVariantMap(); } } else if (key == "X-Purism-FormFactor") { - // Phosh/Purism form factor specification + app["purismFormFactor"] = value.split(';', Qt::SkipEmptyParts); } else if (key == "X-KDE-FormFactors") { - // KDE form factor specification + app["kdeFormFactors"] = value.split(';', Qt::SkipEmptyParts); } } - // Validate required fields if (!app.contains("name") || !app.contains("exec") || app["noDisplay"].toBool() || app["hidden"].toBool()) { return QVariantMap(); } - // Generate ID from filename (use completeBaseName to keep full name like "org.gnome.Calendar") QFileInfo fileInfo(filePath); - QString id = fileInfo.completeBaseName(); // Remove only .desktop extension, keep dots + QString id = fileInfo.completeBaseName(); app["id"] = id; file.close(); @@ -155,7 +153,6 @@ QString DesktopFileParser::resolveIconPath(const QString &iconName) { return fallback; } - // If it's already an absolute path, use it if (iconName.startsWith('/')) { if (QFile::exists(iconName)) { m_iconCache.insert(iconName, iconName); @@ -166,8 +163,6 @@ QString DesktopFileParser::resolveIconPath(const QString &iconName) { return fallback; } - // If it already has an extension, check if it exists (relative to what? usually implies absolute if extension present) - // But some themes might specify "firefox.png" without path. We should still search. if (iconName.endsWith(".svg") || iconName.endsWith(".png") || iconName.endsWith(".xpm") || iconName.endsWith(".jpg")) { if (QFile::exists(iconName)) { @@ -176,25 +171,28 @@ QString DesktopFileParser::resolveIconPath(const QString &iconName) { } } - // SEARCH PRIORITY: Scalable SVG (best quality) -> High Resolution PNG -> Low Resolution - // We search standard paths AND Flatpak/Snap paths - // NOTE: Icons can be in various XDG contexts: apps, categories, devices, places, etc. QStringList searchPaths = { - // 1. SCALABLE FIRST (SVGs render perfectly at any size) - all XDG contexts + QDir::homePath() + "/.local/share/icons/hicolor/scalable/apps/", - "/usr/share/icons/hicolor/scalable/apps/", "/usr/share/icons/hicolor/scalable/devices/", + "/usr/share/icons/hicolor/scalable/apps/", + "/usr/share/icons/hicolor/scalable/devices/", "/usr/share/icons/hicolor/scalable/places/", - "/usr/share/icons/hicolor/scalable/categories/", "/usr/share/icons/PiXtrix/scalable/apps/", + "/usr/share/icons/hicolor/scalable/categories/", + "/usr/share/icons/PiXtrix/scalable/apps/", "/usr/share/icons/PiXtrix/scalable/categories/", - "/usr/share/icons/PiXtrix/scalable/devices/", "/usr/share/icons/PiXtrix/scalable/places/", - "/usr/share/icons/Adwaita/scalable/apps/", "/usr/share/icons/Adwaita/scalable/categories/", - "/usr/share/icons/Adwaita/scalable/devices/", "/usr/share/icons/Adwaita/scalable/places/", - "/usr/share/icons/gnome/scalable/apps/", "/usr/share/icons/gnome/scalable/categories/", - "/usr/share/icons/gnome/scalable/devices/", "/usr/share/icons/gnome/scalable/places/", + "/usr/share/icons/PiXtrix/scalable/devices/", + "/usr/share/icons/PiXtrix/scalable/places/", + "/usr/share/icons/Adwaita/scalable/apps/", + "/usr/share/icons/Adwaita/scalable/categories/", + "/usr/share/icons/Adwaita/scalable/devices/", + "/usr/share/icons/Adwaita/scalable/places/", + "/usr/share/icons/gnome/scalable/apps/", + "/usr/share/icons/gnome/scalable/categories/", + "/usr/share/icons/gnome/scalable/devices/", + "/usr/share/icons/gnome/scalable/places/", QDir::homePath() + "/.local/share/flatpak/exports/share/icons/hicolor/scalable/apps/", "/var/lib/flatpak/exports/share/icons/hicolor/scalable/apps/", - // 2. High-res pixel icons (512x512 and 256x256) QDir::homePath() + "/.local/share/icons/hicolor/512x512/apps/", "/usr/share/icons/hicolor/512x512/apps/", QDir::homePath() + "/.local/share/flatpak/exports/share/icons/hicolor/512x512/apps/", @@ -205,22 +203,26 @@ QString DesktopFileParser::resolveIconPath(const QString &iconName) { QDir::homePath() + "/.local/share/flatpak/exports/share/icons/hicolor/256x256/apps/", "/var/lib/flatpak/exports/share/icons/hicolor/256x256/apps/", - // 3. Medium-res (128x128) - including devices/places QDir::homePath() + "/.local/share/icons/hicolor/128x128/apps/", - "/usr/share/icons/hicolor/128x128/apps/", "/usr/share/icons/hicolor/128x128/devices/", + "/usr/share/icons/hicolor/128x128/apps/", + "/usr/share/icons/hicolor/128x128/devices/", "/usr/share/icons/hicolor/128x128/places/", QDir::homePath() + "/.local/share/flatpak/exports/share/icons/hicolor/128x128/apps/", "/var/lib/flatpak/exports/share/icons/hicolor/128x128/apps/", - // 4. PiXtrix theme - all sizes and contexts (Raspberry Pi OS default theme) - "/usr/share/icons/PiXtrix/96x96/apps/", "/usr/share/icons/PiXtrix/96x96/categories/", - "/usr/share/icons/PiXtrix/96x96/devices/", "/usr/share/icons/PiXtrix/96x96/places/", - "/usr/share/icons/PiXtrix/64x64/apps/", "/usr/share/icons/PiXtrix/64x64/categories/", - "/usr/share/icons/PiXtrix/64x64/devices/", "/usr/share/icons/PiXtrix/64x64/places/", - "/usr/share/icons/PiXtrix/48x48/apps/", "/usr/share/icons/PiXtrix/48x48/categories/", - "/usr/share/icons/PiXtrix/48x48/devices/", "/usr/share/icons/PiXtrix/48x48/places/", + "/usr/share/icons/PiXtrix/96x96/apps/", + "/usr/share/icons/PiXtrix/96x96/categories/", + "/usr/share/icons/PiXtrix/96x96/devices/", + "/usr/share/icons/PiXtrix/96x96/places/", + "/usr/share/icons/PiXtrix/64x64/apps/", + "/usr/share/icons/PiXtrix/64x64/categories/", + "/usr/share/icons/PiXtrix/64x64/devices/", + "/usr/share/icons/PiXtrix/64x64/places/", + "/usr/share/icons/PiXtrix/48x48/apps/", + "/usr/share/icons/PiXtrix/48x48/categories/", + "/usr/share/icons/PiXtrix/48x48/devices/", + "/usr/share/icons/PiXtrix/48x48/places/", - // 5. AdwaitaLegacy (contains many standard icons) "/usr/share/icons/AdwaitaLegacy/48x48/legacy/", "/usr/share/icons/AdwaitaLegacy/48x48/devices/", "/usr/share/icons/AdwaitaLegacy/48x48/places/", @@ -231,22 +233,21 @@ QString DesktopFileParser::resolveIconPath(const QString &iconName) { "/usr/share/icons/AdwaitaLegacy/24x24/devices/", "/usr/share/icons/AdwaitaLegacy/24x24/places/", - // 6. Snap exports "/var/lib/snapd/desktop/icons/", - // 7. Mid-range fallbacks (64x64) - "/usr/share/icons/hicolor/64x64/apps/", "/usr/share/icons/hicolor/64x64/devices/", + "/usr/share/icons/hicolor/64x64/apps/", + "/usr/share/icons/hicolor/64x64/devices/", "/usr/share/icons/hicolor/64x64/places/", QDir::homePath() + "/.local/share/icons/hicolor/64x64/apps/", - // 8. Low-res fallbacks (Last resort - 48x48, 32x32 and pixmaps) - "/usr/share/icons/hicolor/48x48/apps/", "/usr/share/icons/hicolor/48x48/devices/", - "/usr/share/icons/hicolor/48x48/places/", "/usr/share/icons/hicolor/32x32/apps/", - "/usr/share/icons/hicolor/32x32/devices/", "/usr/share/icons/hicolor/32x32/places/", + "/usr/share/icons/hicolor/48x48/apps/", + "/usr/share/icons/hicolor/48x48/devices/", + "/usr/share/icons/hicolor/48x48/places/", + "/usr/share/icons/hicolor/32x32/apps/", + "/usr/share/icons/hicolor/32x32/devices/", + "/usr/share/icons/hicolor/32x32/places/", "/usr/share/pixmaps/"}; - // Prefer PNG over SVG - some SVGs with mask patterns render with black backgrounds - // in Qt's SVG rasterizer due to implicit black fills in mask definitions QStringList extensions = {".png", ".svg", ".xpm", ".jpg", ""}; for (const QString &basePath : searchPaths) { @@ -254,8 +255,7 @@ QString DesktopFileParser::resolveIconPath(const QString &iconName) { QString fullPath = basePath + iconName + ext; if (QFile::exists(fullPath)) { qDebug() << "[DesktopFileParser] Found icon:" << fullPath; - // Return raw path - QML Image handles absolute paths directly - // Using file:// prefix can interfere with Qt's SVG format detection + m_iconCache.insert(iconName, fullPath); return fullPath; } @@ -269,14 +269,12 @@ QString DesktopFileParser::resolveIconPath(const QString &iconName) { } QString DesktopFileParser::cleanExecLine(const QString &exec) { - // Remove field codes like %f, %F, %u, %U, etc. + QString cleaned = exec; QRegularExpression re("%[fFuUdDnNickvm]"); cleaned.remove(re); cleaned = cleaned.trimmed(); - // CRITICAL FIX: Remove flags that cause apps to open in separate windows - // These flags bypass the Wayland compositor and open in host/parent compositor QStringList windowFlags = {"--new-window", "-new-window", "--new-tab", "-new-tab", "--new-instance", "-new-instance"}; @@ -289,16 +287,13 @@ QString DesktopFileParser::cleanExecLine(const QString &exec) { } } - // Convert "gapplication launch org.gnome.AppName" to direct binary execution - // This bypasses D-Bus activation which would launch apps in host session if (cleaned.startsWith("gapplication launch ")) { - QString appId = cleaned.mid(20).trimmed(); // Extract app ID after "gapplication launch " + QString appId = cleaned.mid(20).trimmed(); - // Convert app ID to binary name: org.gnome.Weather -> gnome-weather QStringList parts = appId.split('.'); if (parts.size() >= 2) { QString binaryName = parts.last().toLower(); - // Add vendor prefix if it exists (e.g., gnome-, kde-) + if (parts.size() >= 3) { QString vendor = parts[parts.size() - 2].toLower(); binaryName = vendor + "-" + binaryName; @@ -309,37 +304,30 @@ QString DesktopFileParser::cleanExecLine(const QString &exec) { return binaryName; } else { qWarning() << "[DesktopFileParser] Failed to parse gapplication app ID:" << appId; - return cleaned; // Return as-is if we can't parse + return cleaned; } } - // Handle --gapplication-service flag (background services, should be filtered out by NoDisplay) if (cleaned.contains("--gapplication-service")) { qDebug() << "[DesktopFileParser] Skipping gapplication service:" << cleaned; - return QString(); // Return empty to filter out + return QString(); } - // Handle Flatpak applications if (cleaned.startsWith("flatpak run ")) { qDebug() << "[DesktopFileParser] Detected Flatpak app, adding Wayland permissions:" << cleaned; - // Note: Marathon compositor socket name and path will be added at launch time - // For now, just mark it for special handling + cleaned = "FLATPAK:" + cleaned; return cleaned; } - // Handle Snap applications if (cleaned.startsWith("snap run ") || cleaned.startsWith("/snap/bin/")) { qDebug() << "[DesktopFileParser] Detected Snap app:" << cleaned; - // Note: Snap wayland interface must be connected: snap connect APP:wayland :wayland - // Mark for special handling + cleaned = "SNAP:" + cleaned; return cleaned; } - // Strip absolute paths to just binary name (for better compatibility) - // /usr/bin/firefox -> firefox if (cleaned.startsWith('/')) { QStringList parts = cleaned.split(' ', Qt::SkipEmptyParts); if (!parts.isEmpty()) { @@ -355,7 +343,7 @@ QString DesktopFileParser::cleanExecLine(const QString &exec) { } bool DesktopFileParser::isMobileFriendly(const QVariantMap &app) { - // Check X-Purism-FormFactor (Phosh standard) + if (app.contains("purismFormFactor")) { QStringList formFactors = app["purismFormFactor"].toStringList(); for (const QString &factor : formFactors) { @@ -367,7 +355,6 @@ bool DesktopFileParser::isMobileFriendly(const QVariantMap &app) { } } - // Check X-KDE-FormFactors (KDE standard) if (app.contains("kdeFormFactors")) { QStringList formFactors = app["kdeFormFactors"].toStringList(); for (const QString &factor : formFactors) { @@ -380,6 +367,5 @@ bool DesktopFileParser::isMobileFriendly(const QVariantMap &app) { } } - // If no form factor is specified, consider it desktop-only return false; } diff --git a/shell/src/desktopfileparser.h b/shell/src/components/desktopfileparser.h similarity index 96% rename from shell/src/desktopfileparser.h rename to shell/src/components/desktopfileparser.h index 065fe2208..9ade02796 100644 --- a/shell/src/desktopfileparser.h +++ b/shell/src/components/desktopfileparser.h @@ -27,4 +27,4 @@ class DesktopFileParser : public QObject { QHash m_iconCache; }; -#endif // DESKTOPFILEPARSER_H +#endif diff --git a/shell/src/lunasvgimageprovider.cpp b/shell/src/components/lunasvgimageprovider.cpp similarity index 67% rename from shell/src/lunasvgimageprovider.cpp rename to shell/src/components/lunasvgimageprovider.cpp index b8a74ea28..3cf66d305 100644 --- a/shell/src/lunasvgimageprovider.cpp +++ b/shell/src/components/lunasvgimageprovider.cpp @@ -9,22 +9,16 @@ LunaSvgImageProvider::LunaSvgImageProvider() QImage LunaSvgImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) { - // The id is the file path part of the image:// URL. - // - // We support: - // - absolute filesystem paths: /home/.../icon.svg - // - Qt resource paths (qrc:/... or :/...) - // - "images/..." (common when callers strip the leading '/' from qrc paths) + QString path = id; if (path.startsWith("qrc:/")) { - // Map qrc:/foo -> /foo (Qt resource prefix ":" will be applied below) + path = path.mid(3); } - // Heuristic: if it looks like a filesystem absolute path, try that first. QString fsPath = path; if (!fsPath.startsWith('/') && !fsPath.isEmpty()) { - // Prepend '/' (some QML URLs drop it). + fsPath = "/" + fsPath; } @@ -33,14 +27,14 @@ QImage LunaSvgImageProvider::requestImage(const QString &id, QSize *size, if (!fsPath.isEmpty() && QFile::exists(fsPath)) { document = lunasvg::Document::loadFromFile(fsPath.toStdString()); } else { - // Try Qt resources + QString resPath; if (path.startsWith(":/")) { resPath = path; } else if (path.startsWith("/")) { - resPath = ":" + path; // /images/x.svg -> :/images/x.svg + resPath = ":" + path; } else if (!path.isEmpty()) { - resPath = ":/" + path; // images/x.svg -> :/images/x.svg + resPath = ":/" + path; } if (resPath.isEmpty() || !QFile::exists(resPath)) { @@ -67,24 +61,18 @@ QImage LunaSvgImageProvider::requestImage(const QString &id, QSize *size, return QImage(); } - // Determine output size - int width = requestedSize.width() > 0 ? requestedSize.width() : 128; - int height = requestedSize.height() > 0 ? requestedSize.height() : 128; + int width = requestedSize.width() > 0 ? requestedSize.width() : 128; + int height = requestedSize.height() > 0 ? requestedSize.height() : 128; - // Render SVG to bitmap auto bitmap = document->renderToBitmap(width, height); if (!bitmap.valid()) { qWarning() << "[LunaSvgImageProvider] Failed to render SVG:" << id; return QImage(); } - // LunaSVG renders to BGRA format with premultiplied alpha - // On little-endian systems (x86/ARM), BGRA = B,G,R,A bytes in memory - // Qt's Format_ARGB32_Premultiplied is stored as BGRA on little-endian QImage image(reinterpret_cast(bitmap.data()), bitmap.width(), bitmap.height(), static_cast(bitmap.stride()), QImage::Format_ARGB32_Premultiplied); - // Make a deep copy since bitmap data will be freed when function returns QImage result = image.copy(); if (size) { diff --git a/shell/src/components/lunasvgimageprovider.h b/shell/src/components/lunasvgimageprovider.h new file mode 100644 index 000000000..e4950b9ed --- /dev/null +++ b/shell/src/components/lunasvgimageprovider.h @@ -0,0 +1,15 @@ +#ifndef LUNASVGIMAGEPROVIDER_H +#define LUNASVGIMAGEPROVIDER_H + +#include +#include +#include + +class LunaSvgImageProvider : public QQuickImageProvider { + public: + LunaSvgImageProvider(); + + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; +}; + +#endif diff --git a/shell/src/mpris_types.h b/shell/src/components/mpris_types.h similarity index 86% rename from shell/src/mpris_types.h rename to shell/src/components/mpris_types.h index 0b12a743c..2d6d3cca6 100644 --- a/shell/src/mpris_types.h +++ b/shell/src/components/mpris_types.h @@ -6,7 +6,6 @@ #include #include -// Define the type for MPRIS metadata (a{sv}) typedef QMap MprisMetadata; Q_DECLARE_METATYPE(MprisMetadata) @@ -18,4 +17,4 @@ inline void registerMprisTypes() { qDBusRegisterMetaType>(); } -#endif // MPRIS_TYPES_H +#endif diff --git a/shell/src/portalmanager.cpp b/shell/src/components/portalmanager.cpp similarity index 93% rename from shell/src/portalmanager.cpp rename to shell/src/components/portalmanager.cpp index 93bfda9c6..871830be3 100644 --- a/shell/src/portalmanager.cpp +++ b/shell/src/components/portalmanager.cpp @@ -5,7 +5,6 @@ #include #include -// Portal constants const QString PORTAL_SERVICE = "org.freedesktop.portal.Desktop"; const QString PORTAL_PATH = "/org/freedesktop/portal/desktop"; const QString PORTAL_ACCESS_INTERFACE = "org.freedesktop.portal.Access"; @@ -22,12 +21,11 @@ PortalManager::PortalManager(QObject *parent) PortalManager::~PortalManager() {} void PortalManager::checkPortals() { - // Use async calls to prevent blocking startup (50s hang) + QDBusMessage message = QDBusMessage::createMethodCall(PORTAL_SERVICE, PORTAL_PATH, "org.freedesktop.DBus.Properties", "Get"); message << PORTAL_ACCESS_INTERFACE << "version"; - // Async call for Access interface QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); @@ -38,7 +36,7 @@ void PortalManager::checkPortals() { << reply.value().toUInt() << ")"; m_portalsAvailable = true; } else { - // Fallback: Check Camera interface + checkCameraPortal(); } w->deleteLater(); @@ -74,14 +72,13 @@ bool PortalManager::isPortalAvailable(const QString &portalName) { if (portalName.isEmpty()) return true; - // Check specific portal availability QString interface; if (portalName == "camera") interface = PORTAL_CAMERA_INTERFACE; else if (portalName == "location") interface = PORTAL_LOCATION_INTERFACE; else - return true; // Assume available if generic + return true; QDBusMessage message = QDBusMessage::createMethodCall(PORTAL_SERVICE, PORTAL_PATH, "org.freedesktop.DBus.Properties", "Get"); @@ -92,7 +89,7 @@ bool PortalManager::isPortalAvailable(const QString &portalName) { } QString PortalManager::getRequestHandleToken(const QString &appId) { - // Generate a unique token for the request + return QString("marathon_req_%1_%2").arg(appId).arg(QUuid::createUuid().toString(QUuid::Id128)); } @@ -108,7 +105,6 @@ void PortalManager::requestCameraAccess(const QString &appId) { PORTAL_CAMERA_INTERFACE, "AccessCamera"); QVariantMap options; - // options.insert("handle_token", getRequestHandleToken(appId)); // Optional, system can generate one message << options; @@ -119,7 +115,6 @@ void PortalManager::requestCameraAccess(const QString &appId) { &PortalManager::onCameraRequestFinished); } -// Helper class to handle portal responses class PortalRequest : public QObject { Q_OBJECT public: @@ -158,7 +153,6 @@ void PortalManager::onCameraRequestFinished(QDBusPendingCallWatcher *watcher) { QString requestPath = reply.value().path(); qInfo() << "[PortalManager] Camera request initiated, handle:" << requestPath; - // Create helper to listen for response new PortalRequest(requestPath, appId, this); m_pendingCameraRequests.insert(requestPath, appId); @@ -200,7 +194,6 @@ void PortalManager::onLocationRequestFinished(QDBusPendingCallWatcher *watcher) QString requestPath = reply.value().path(); qInfo() << "[PortalManager] Location session created, handle:" << requestPath; - // For location, we treat session creation as success for now emit locationAccessGranted(appId); } watcher->deleteLater(); diff --git a/shell/src/portalmanager.h b/shell/src/components/portalmanager.h similarity index 83% rename from shell/src/portalmanager.h rename to shell/src/components/portalmanager.h index 49d09f9ce..158548cb3 100644 --- a/shell/src/portalmanager.h +++ b/shell/src/components/portalmanager.h @@ -15,16 +15,14 @@ class PortalManager : public QObject { ~PortalManager(); void checkCameraPortal(); - // Check if portals are available on the system bool isPortalAvailable(const QString &portalName = QString()); - // Request access via portal void requestCameraAccess(const QString &appId); void requestLocationAccess(const QString &appId); void requestMicrophoneAccess(const QString &appId); signals: - // Signals to notify PermissionManager of portal results + void cameraAccessGranted(const QString &appId); void cameraAccessDenied(const QString &appId); @@ -40,14 +38,12 @@ class PortalManager : public QObject { void onMicrophoneRequestFinished(QDBusPendingCallWatcher *watcher); public: - // Called by helper class void handlePortalResponse(const QString &path, const QString &appId, uint response, const QVariantMap &results); private: - bool m_portalsAvailable; + bool m_portalsAvailable; - // Map request handle paths to appIds to track pending requests QMap m_pendingCameraRequests; QMap m_pendingLocationRequests; QMap m_pendingMicrophoneRequests; @@ -56,4 +52,4 @@ class PortalManager : public QObject { QString getRequestHandleToken(const QString &appId); }; -#endif // PORTALMANAGER_H +#endif diff --git a/shell/src/ratelimiter.h b/shell/src/components/ratelimiter.h similarity index 100% rename from shell/src/ratelimiter.h rename to shell/src/components/ratelimiter.h diff --git a/shell/src/securitylogger.h b/shell/src/components/securitylogger.h similarity index 100% rename from shell/src/securitylogger.h rename to shell/src/components/securitylogger.h diff --git a/shell/src/audiopolicycontroller.cpp b/shell/src/controllers/audiopolicycontroller.cpp similarity index 93% rename from shell/src/audiopolicycontroller.cpp rename to shell/src/controllers/audiopolicycontroller.cpp index 5ea09a224..79536e276 100644 --- a/shell/src/audiopolicycontroller.cpp +++ b/shell/src/controllers/audiopolicycontroller.cpp @@ -17,7 +17,7 @@ namespace { } return QUrl::fromLocalFile(pathOrUrl); } -} // namespace +} AudioPolicyController::AudioPolicyController(AudioManagerCpp *audioManager, SettingsManager *settings, HapticManager *haptics, @@ -34,7 +34,7 @@ AudioPolicyController::AudioPolicyController(AudioManagerCpp *audioManager, , m_alarmOutput(nullptr) , m_previewPlayer(nullptr) , m_previewOutput(nullptr) { - // Keep SettingsManager's persisted master volume in sync with runtime changes. + if (m_audioManager && m_settings) { connect(m_audioManager, &AudioManagerCpp::volumeChanged, this, [this]() { if (!m_audioManager || !m_settings) @@ -48,13 +48,6 @@ AudioPolicyController::AudioPolicyController(AudioManagerCpp *audioManager, wirePlayerVolumes(); - // IMPORTANT: - // When running the shell inside another desktop session (dev/testing), we must NOT - // stomp global system audio state (mute/volume) on startup, or we will "duck" whatever - // audio is already playing on the host. - // - // Only enforce persisted profile side-effects automatically when we are actually the - // active desktop session. if (m_settings) { const QString profile = m_settings->audioProfile(); const QString desktop = qEnvironmentVariable("XDG_CURRENT_DESKTOP"); @@ -131,13 +124,13 @@ void AudioPolicyController::applyProfileSideEffects(const QString &profile) { } else if (profile == "loud") { setMuted(false); setVibrationEnabled(true); - // "Loud" implies a high master volume. + setMasterVolume(0.9); } } void AudioPolicyController::ensurePlayer(QMediaPlayer *&player, QAudioOutput *&output, - double initialVolume, bool /*loopForever*/) { + double initialVolume, bool) { if (!output) { output = new QAudioOutput(this); output->setMuted(false); diff --git a/shell/src/audiopolicycontroller.h b/shell/src/controllers/audiopolicycontroller.h similarity index 100% rename from shell/src/audiopolicycontroller.h rename to shell/src/controllers/audiopolicycontroller.h diff --git a/shell/src/audioroutingmanager.cpp b/shell/src/controllers/audioroutingmanager.cpp similarity index 91% rename from shell/src/audioroutingmanager.cpp rename to shell/src/controllers/audioroutingmanager.cpp index b33b8070b..73f1dad47 100644 --- a/shell/src/audioroutingmanager.cpp +++ b/shell/src/controllers/audioroutingmanager.cpp @@ -14,11 +14,9 @@ AudioRoutingManager::AudioRoutingManager(QObject *parent) , m_deviceDetectionTimer(new QTimer(this)) { qDebug() << "[AudioRoutingManager] Initializing"; - // Detect audio devices on startup detectAudioDevices(); - // Periodic device detection (in case devices are hotplugged) - m_deviceDetectionTimer->setInterval(5000); // Every 5 seconds + m_deviceDetectionTimer->setInterval(5000); connect(m_deviceDetectionTimer, &QTimer::timeout, this, &AudioRoutingManager::detectAudioDevices); m_deviceDetectionTimer->start(); @@ -48,11 +46,8 @@ void AudioRoutingManager::startCallAudio() { m_isInCall = true; emit inCallChanged(true); - // Switch to VoiceCall UCM profile (optimized for phone calls) - // This routes audio to the earpiece and configures microphone for voice switchProfile("VoiceCall"); - // Default to earpiece unless speakerphone was enabled if (!m_isSpeakerphoneEnabled) { selectAudioDevice("earpiece"); } @@ -69,15 +64,12 @@ void AudioRoutingManager::stopCallAudio() { m_isInCall = false; emit inCallChanged(false); - // Restore normal audio profile (HiFi for music/media) switchProfile(m_previousProfile.isEmpty() ? "HiFi" : m_previousProfile); - // Unmute if muted if (m_isMuted) { setMuted(false); } - // Reset speakerphone m_isSpeakerphoneEnabled = false; emit speakerphoneChanged(false); } @@ -107,7 +99,6 @@ void AudioRoutingManager::setMuted(bool muted) { m_isMuted = muted; emit mutedChanged(muted); - // Mute/unmute the default microphone source runWpctlCommand("wpctl", QStringList() << "set-mute" << "@DEFAULT_SOURCE@" << (muted ? "1" : "0")); } @@ -120,9 +111,8 @@ void AudioRoutingManager::selectAudioDevice(const QString &device) { qInfo() << "[AudioRoutingManager] Selecting audio device:" << device; m_currentAudioDevice = device; - emit audioDeviceChanged(device); + emit audioDeviceChanged(device); - // Map device names to sink IDs QString sinkId; if (device == "earpiece") { @@ -137,7 +127,7 @@ void AudioRoutingManager::selectAudioDevice(const QString &device) { setDefaultSink(sinkId); } else { qWarning() << "[AudioRoutingManager] Device sink ID not found:" << device; - // Fallback: try to switch profile directly + if (device == "earpiece" || device == "speaker") { switchProfile("VoiceCall"); } @@ -147,16 +137,15 @@ void AudioRoutingManager::selectAudioDevice(const QString &device) { void AudioRoutingManager::switchProfile(const QString &profileName) { if (m_audioCardId.isEmpty()) { qWarning() << "[AudioRoutingManager] Audio card ID not detected, cannot switch profile"; - detectAudioDevices(); // Retry detection + detectAudioDevices(); return; } qInfo() << "[AudioRoutingManager] Switching to profile:" << profileName << "on card" << m_audioCardId; - // Save current profile if switching away from HiFi if (profileName == "VoiceCall") { - m_previousProfile = "HiFi"; // Assume HiFi was active + m_previousProfile = "HiFi"; } runWpctlCommand("wpctl", QStringList() << "set-profile" << m_audioCardId << profileName); @@ -173,7 +162,7 @@ void AudioRoutingManager::setDefaultSink(const QString &sinkId) { } void AudioRoutingManager::runWpctlCommand(const QString &command, const QStringList &args) { - // Kill any existing process + if (m_wpctlProcess && m_wpctlProcess->state() == QProcess::Running) { m_wpctlProcess->kill(); m_wpctlProcess->waitForFinished(1000); diff --git a/shell/src/audioroutingmanager.h b/shell/src/controllers/audioroutingmanager.h similarity index 51% rename from shell/src/audioroutingmanager.h rename to shell/src/controllers/audioroutingmanager.h index 8844a6ae7..3269974bc 100644 --- a/shell/src/audioroutingmanager.h +++ b/shell/src/controllers/audioroutingmanager.h @@ -6,18 +6,6 @@ #include #include -/** - * @brief Manages audio routing for phone calls using PipeWire/WirePlumber - * - * This class handles: - * - Switching between HiFi and VoiceCall ALSA UCM profiles - * - Earpiece vs speakerphone routing - * - Microphone mute/unmute - * - Bluetooth headset routing - * - * Based on KDE Plasma Mobile's approach but using direct PipeWire integration - * instead of callaudiod dependency. - */ class AudioRoutingManager : public QObject { Q_OBJECT Q_PROPERTY(bool isInCall READ isInCall NOTIFY inCallChanged) @@ -43,36 +31,15 @@ class AudioRoutingManager : public QObject { } public slots: - /** - * @brief Start call audio routing - * Switches from HiFi profile to VoiceCall profile - * Routes audio to earpiece by default - */ + void startCallAudio(); - /** - * @brief Stop call audio routing - * Switches back from VoiceCall to HiFi profile - * Restores normal audio routing - */ void stopCallAudio(); - /** - * @brief Toggle speakerphone on/off - * @param enabled - true for speakerphone, false for earpiece - */ void setSpeakerphone(bool enabled); - /** - * @brief Toggle microphone mute - * @param muted - true to mute, false to unmute - */ void setMuted(bool muted); - /** - * @brief Manually select audio output device - * @param device - "earpiece", "speaker", "bluetooth", "headset" - */ void selectAudioDevice(const QString &device); signals: @@ -87,28 +54,27 @@ class AudioRoutingManager : public QObject { void detectAudioDevices(); private: - void switchProfile(const QString &profileName); - void setDefaultSink(const QString &sinkName); - void runWpctlCommand(const QString &command, const QStringList &args); - QString findAudioCard(); - QString findSinkByName(const QString &name); - - bool m_isInCall; - bool m_isSpeakerphoneEnabled; - bool m_isMuted; - QString m_currentAudioDevice; - - // Audio device IDs discovered from wpctl status + void switchProfile(const QString &profileName); + void setDefaultSink(const QString &sinkName); + void runWpctlCommand(const QString &command, const QStringList &args); + QString findAudioCard(); + QString findSinkByName(const QString &name); + + bool m_isInCall; + bool m_isSpeakerphoneEnabled; + bool m_isMuted; + QString m_currentAudioDevice; + QString m_audioCardId; QString m_earpieceSinkId; QString m_speakerSinkId; QString m_bluetoothSinkId; QString m_microphoneSourceId; - QString m_previousProfile; // Store HiFi profile to restore + QString m_previousProfile; QProcess *m_wpctlProcess; QTimer *m_deviceDetectionTimer; }; -#endif // AUDIOROUTINGMANAGER_H +#endif diff --git a/shell/src/displaypolicycontroller.cpp b/shell/src/controllers/displaypolicycontroller.cpp similarity index 95% rename from shell/src/displaypolicycontroller.cpp rename to shell/src/controllers/displaypolicycontroller.cpp index a62fe5387..8e16e75db 100644 --- a/shell/src/displaypolicycontroller.cpp +++ b/shell/src/controllers/displaypolicycontroller.cpp @@ -14,14 +14,13 @@ DisplayPolicyController::DisplayPolicyController(DisplayManagerCpp *displayManag if (m_settingsManager) { connect(m_settingsManager, &SettingsManager::screenTimeoutChanged, this, [this]() { emit screenTimeoutMsChanged(); - // Keep DisplayManagerCpp in sync (it uses seconds) + if (m_displayManager) m_displayManager->setScreenTimeout( qMax(0, m_settingsManager->screenTimeout() / 1000)); }); connect(m_settingsManager, &SettingsManager::autoBrightnessChanged, this, [this]() { - // Keep DisplayManagerCpp in sync if (m_displayManager) m_displayManager->setAutoBrightness(m_settingsManager->autoBrightness()); emit autoBrightnessEnabledChanged(); @@ -30,7 +29,6 @@ DisplayPolicyController::DisplayPolicyController(DisplayManagerCpp *displayManag if (m_displayManager) { connect(m_displayManager, &DisplayManagerCpp::autoBrightnessEnabledChanged, this, [this]() { - // Mirror to SettingsManager so apps/UI can read consistently if (m_settingsManager && m_settingsManager->autoBrightness() != m_displayManager->autoBrightnessEnabled()) m_settingsManager->setAutoBrightness(m_displayManager->autoBrightnessEnabled()); diff --git a/shell/src/displaypolicycontroller.h b/shell/src/controllers/displaypolicycontroller.h similarity index 83% rename from shell/src/displaypolicycontroller.h rename to shell/src/controllers/displaypolicycontroller.h index 9b03d4ce9..47e0af98c 100644 --- a/shell/src/displaypolicycontroller.h +++ b/shell/src/controllers/displaypolicycontroller.h @@ -5,14 +5,6 @@ class DisplayManagerCpp; class SettingsManager; -/** - * DisplayPolicyController - * - * Owns display-related policy/glue that should not live in QML: - * - syncing user preferences (SettingsManager) <-> hardware backend (DisplayManagerCpp) - * - screen on/off state tracking - * - screen timeout ms API + display string formatting - */ class DisplayPolicyController : public QObject { Q_OBJECT Q_PROPERTY(int screenTimeoutMs READ screenTimeoutMs WRITE setScreenTimeoutMs NOTIFY diff --git a/shell/src/powerpolicycontroller.cpp b/shell/src/controllers/powerpolicycontroller.cpp similarity index 87% rename from shell/src/powerpolicycontroller.cpp rename to shell/src/controllers/powerpolicycontroller.cpp index 9545b81a8..f3afec067 100644 --- a/shell/src/powerpolicycontroller.cpp +++ b/shell/src/controllers/powerpolicycontroller.cpp @@ -12,14 +12,12 @@ PowerPolicyController::PowerPolicyController(PowerManagerCpp *powerManager, , m_powerManager(powerManager) , m_displayManager(displayManager) { - // Keep canSleep reactive when wakelocks change if (m_powerManager) { connect(m_powerManager, &PowerManagerCpp::activeWakelocksChanged, this, [this]() { emit wakeLockCountChanged(); emitCanSleepMaybeChanged(); }); - // Battery policy lives here (QML should not encode threshold logic). connect(m_powerManager, &PowerManagerCpp::batteryLevelChanged, this, &PowerPolicyController::handleBatteryPolicy); connect(m_powerManager, &PowerManagerCpp::isChargingChanged, this, @@ -28,7 +26,6 @@ PowerPolicyController::PowerPolicyController(PowerManagerCpp *powerManager, m_lastCanSleep = canSleep(); - // Evaluate battery policy once on startup (best-effort). handleBatteryPolicy(); } @@ -84,8 +81,6 @@ void PowerPolicyController::handleBatteryPolicy() { return; } - // Prefer UPower WarningLevel for policy: - // 0 Unknown, 1 None, 2 Discharging (UPS only), 3 Low, 4 Critical, 5 Action if (warning >= 4 && !m_hasShownCriticalWarning) { QString action = QStringLiteral("shutdown"); if (m_powerManager) { @@ -96,8 +91,7 @@ void PowerPolicyController::handleBatteryPolicy() { emit batteryWarning( QStringLiteral("Critical Battery"), QStringLiteral("Device will %1 in 10 seconds to prevent data loss").arg(action), - QStringLiteral("battery-warning"), - /*hapticLevel*/ 3); + QStringLiteral("battery-warning"), 3); m_hasShownCriticalWarning = true; m_lastBatteryWarningLevel = qMin(level, 3); if (!m_emergencyShutdownArmed) { @@ -107,28 +101,24 @@ void PowerPolicyController::handleBatteryPolicy() { return; } - // If we recovered to a non-critical warning level, disarm. if (m_emergencyShutdownArmed && warning < 4) { m_emergencyShutdownArmed = false; emit emergencyShutdownDisarmed(); } - // Low warning: show a single toast when UPower says Low. if (warning == 3 && m_lastBatteryWarningLevel > 20) { emit batteryWarning(QStringLiteral("Battery Low"), QStringLiteral("%1% remaining").arg(level), QStringLiteral("battery"), - /*hapticLevel*/ 1); + 1); m_lastBatteryWarningLevel = 20; return; } - // Fallback % warnings only when WarningLevel isn't available. if (warning <= 2 && level <= 5 && m_lastBatteryWarningLevel > 5) { emit batteryWarning( QStringLiteral("Very Low Battery"), QStringLiteral("%1% remaining. Connect charger immediately.").arg(level), - QStringLiteral("battery-warning"), - /*hapticLevel*/ 3); + QStringLiteral("battery-warning"), 3); m_lastBatteryWarningLevel = 5; return; } @@ -136,8 +126,7 @@ void PowerPolicyController::handleBatteryPolicy() { if (warning <= 2 && level <= 10 && m_lastBatteryWarningLevel > 10) { emit batteryWarning(QStringLiteral("Low Battery"), QStringLiteral("%1% remaining. Connect charger soon.").arg(level), - QStringLiteral("battery"), - /*hapticLevel*/ 2); + QStringLiteral("battery"), 2); m_lastBatteryWarningLevel = 10; return; } @@ -145,7 +134,7 @@ void PowerPolicyController::handleBatteryPolicy() { if (warning <= 2 && level <= 20 && m_lastBatteryWarningLevel > 20) { emit batteryWarning(QStringLiteral("Battery Low"), QStringLiteral("%1% remaining").arg(level), QStringLiteral("battery"), - /*hapticLevel*/ 1); + 1); m_lastBatteryWarningLevel = 20; return; } @@ -183,7 +172,7 @@ void PowerPolicyController::performCriticalPowerAction() { } else if (action.compare("Suspend", Qt::CaseInsensitive) == 0) { m_powerManager->suspend(); } else { - // Conservative fallback. + m_powerManager->shutdown(); } } @@ -192,13 +181,11 @@ QString PowerPolicyController::wake(const QString &reason) { m_wakeReason = reason; emit wakeReasonChanged(); - // Best-effort: wake screen if (m_displayManager) m_displayManager->setScreenState(true); emit systemWaking(reason); - // Acquire a temporary wakelock (use reason as identifier) if (m_powerManager) m_powerManager->acquireWakelock(reason); @@ -212,7 +199,6 @@ bool PowerPolicyController::sleep() { emit systemSleeping(); - // Best-effort: blank screen before suspend if (m_displayManager) m_displayManager->setScreenState(false); diff --git a/shell/src/powerpolicycontroller.h b/shell/src/controllers/powerpolicycontroller.h similarity index 64% rename from shell/src/powerpolicycontroller.h rename to shell/src/controllers/powerpolicycontroller.h index d35558bbc..aa511452c 100644 --- a/shell/src/powerpolicycontroller.h +++ b/shell/src/controllers/powerpolicycontroller.h @@ -6,16 +6,6 @@ class PowerManagerCpp; class DisplayManagerCpp; -/** - * PowerPolicyController - * - * Owns shell-level power orchestration/policy that should not live in QML: - * - wake/sleep helpers - * - scheduled wake bookkeeping (RTC alarm) - * - battery warnings / emergency shutdown policy (UI presentation stays in QML) - * - * Low-level system integration remains in PowerManagerCpp / DisplayManagerCpp. - */ class PowerPolicyController : public QObject { Q_OBJECT Q_PROPERTY(int wakeLockCount READ wakeLockCount NOTIFY wakeLockCountChanged) @@ -67,26 +57,17 @@ class PowerPolicyController : public QObject { return m_wakeReason; } - Q_INVOKABLE QString wake(const QString &reason); - Q_INVOKABLE bool sleep(); + Q_INVOKABLE QString wake(const QString &reason); + Q_INVOKABLE bool sleep(); - // Power button press policy: - // - If screen is off -> turn it on - // - Else if session is unlocked -> lock and turn screen off - // - Else (already locked) -> turn screen off Q_INVOKABLE PowerButtonAction powerButtonAction(bool screenOn, bool sessionLocked) const; - // Power menu policy: - // - If unlocked -> lock first, then sleep - // - If already locked -> sleep immediately - Q_INVOKABLE SleepAction sleepAction(bool sessionLocked) const; + Q_INVOKABLE SleepAction sleepAction(bool sessionLocked) const; - // Execute the system-configured UPower critical action (best-effort). - Q_INVOKABLE void performCriticalPowerAction(); + Q_INVOKABLE void performCriticalPowerAction(); - // Epoch seconds (UTC) keeps QML↔C++ conversion simple - Q_INVOKABLE QString scheduleWakeEpoch(qint64 epochSeconds, const QString &reason); - Q_INVOKABLE bool cancelScheduledWake(const QString &wakeId); + Q_INVOKABLE QString scheduleWakeEpoch(qint64 epochSeconds, const QString &reason); + Q_INVOKABLE bool cancelScheduledWake(const QString &wakeId); signals: void wakeLockCountChanged(); @@ -98,8 +79,6 @@ class PowerPolicyController : public QObject { void systemWaking(const QString &reason); void systemSleeping(); - // Battery policy signals (presentation belongs in QML). - // hapticLevel: 1=light, 2=medium, 3=heavy void batteryWarning(const QString &title, const QString &body, const QString &iconName, int hapticLevel); void emergencyShutdownArmed(int secondsUntilShutdown); @@ -118,8 +97,7 @@ class PowerPolicyController : public QObject { QVariantList m_scheduledWakes; QString m_wakeReason; - // Battery policy state (prevents repeated warnings). - int m_lastBatteryWarningLevel = 100; - bool m_hasShownCriticalWarning = false; - bool m_emergencyShutdownArmed = false; + int m_lastBatteryWarningLevel = 100; + bool m_hasShownCriticalWarning = false; + bool m_emergencyShutdownArmed = false; }; diff --git a/shell/src/crashhandler.h b/shell/src/crashhandler.h deleted file mode 100644 index f27b9ca7d..000000000 --- a/shell/src/crashhandler.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include - -/** - * @brief CrashHandler - Provides crash protection for Marathon Shell - * - * This class installs signal handlers to catch crashes (SIGSEGV, SIGABRT, etc.) - * and attempts to prevent them from taking down the entire shell. - * - * WARNING: This is a MITIGATION, not a solution. The proper fix is to run - * apps in separate processes. Signal handlers have limitations and can't - * always recover from all types of crashes. - */ -class CrashHandler : public QObject { - Q_OBJECT - - public: - static CrashHandler *instance(); - - // Install crash handlers - void install(); - - // Set callback for crash notifications - void setCrashCallback(std::function callback); - - // Check if we're currently in a crash handler - static bool isInCrashHandler(); - - signals: - void crashDetected(const QString &signal, const QString &message); - - private: - explicit CrashHandler(QObject *parent = nullptr); - ~CrashHandler() override; - - static void signalHandler(int signum); - static void setupSignalHandlers(); - - std::function m_crashCallback; - static CrashHandler *s_instance; - static bool s_inCrashHandler; -}; diff --git a/shell/src/dbus/freedesktopnotifications.cpp b/shell/src/dbus/freedesktopnotifications.cpp index 0721eb5ee..54fb3eaa3 100644 --- a/shell/src/dbus/freedesktopnotifications.cpp +++ b/shell/src/dbus/freedesktopnotifications.cpp @@ -1,6 +1,6 @@ #include "freedesktopnotifications.h" -#include "../notificationmodel.h" -#include "../powermanagercpp.h" +#include "notificationmodel.h" +#include "powermanagercpp.h" #include #include #include @@ -26,12 +26,10 @@ bool FreedesktopNotifications::registerService() { qDebug() << "[FreedesktopNotifications] org.freedesktop.Notifications already registered " "(desktop environment)"; - // Fallback for nested testing: Register org.marathon.Notifications if (bus.registerService("org.marathon.Notifications")) { qInfo() << "[FreedesktopNotifications] ✓ Registered fallback service: " "org.marathon.Notifications"; - // Register the object path for the fallback service too if (!bus.registerObject("/org/freedesktop/Notifications", this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) { @@ -93,14 +91,11 @@ uint FreedesktopNotifications::Notify(const QString &app_name, uint replaces_id, record.priority = mapUrgencyToPriority(hints); - // Acquire temporary wakelock for high-priority notifications - // Urgency: 0 = low, 1 = normal, 2 = critical int urgency = hints.value("urgency", 1).toInt(); if (urgency >= 2 && m_powerManager) { m_powerManager->acquireWakelock("notification"); qInfo() << "[FreedesktopNotifications] Acquired wakelock for critical notification"; - // Release after 5 seconds QTimer::singleShot(5000, this, [this]() { if (m_powerManager) { m_powerManager->releaseWakelock("notification"); @@ -143,29 +138,20 @@ void FreedesktopNotifications::CloseNotification(uint id) { qDebug() << "[FreedesktopNotifications] CloseNotification:" << id; if (m_database->dismiss(id)) { - emit NotificationClosed(id, 3); // Reason: closed by CloseNotification call + emit NotificationClosed(id, 3); } } QStringList FreedesktopNotifications::GetCapabilities() { - // Return list of supported capabilities - // See: https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html - return QStringList{ - "actions", // Supports notification actions - "body", // Supports body text - "body-markup", // Supports markup in body (we strip it but declare support) - "icon-static", // Supports icon - "persistence", // Notifications persist - "action-icons", // Action icons supported - "inline-reply" // Inline reply support for quick responses - }; + + return QStringList{"actions", "body", "body-markup", "icon-static", + "persistence", "action-icons", "inline-reply"}; } void FreedesktopNotifications::InvokeReply(uint id, const QString &text) { qInfo() << "[FreedesktopNotifications] InvokeReply:" << id << "text:" << text; emit NotificationReplied(id, text); - // Emit ActionInvoked for compatibility with apps expecting "default" action emit ActionInvoked(id, "inline-reply"); } @@ -177,14 +163,14 @@ void FreedesktopNotifications::GetServerInformation(QString &name, QString &vend if (version.isEmpty()) { version = "1.0.0"; } - spec_version = "1.2"; // freedesktop.org notification spec version + spec_version = "1.2"; qDebug() << "[FreedesktopNotifications] GetServerInformation called"; } QString FreedesktopNotifications::extractAppName(const QString &provided, const QVariantMap &hints) { - // Prefer desktop-entry from hints for proper app identification + if (hints.contains("desktop-entry")) { QString desktopEntry = hints.value("desktop-entry").toString(); if (!desktopEntry.isEmpty()) { @@ -192,12 +178,10 @@ QString FreedesktopNotifications::extractAppName(const QString &provided, } } - // Fallback to provided app_name if (!provided.isEmpty()) { return provided; } - // Last resort: try sender from DBus context if (calledFromDBus()) { return message().service(); } @@ -206,18 +190,16 @@ QString FreedesktopNotifications::extractAppName(const QString &provided, } int FreedesktopNotifications::mapUrgencyToPriority(const QVariantMap &hints) { - // freedesktop urgency: 0=low, 1=normal, 2=critical - // Marathon priority: 0=low, 1=normal, 2=high, 3=urgent if (hints.contains("urgency")) { int urgency = hints.value("urgency").toInt(); switch (urgency) { - case 0: return 0; // low -> low - case 1: return 1; // normal -> normal - case 2: return 3; // critical -> urgent + case 0: return 0; + case 1: return 1; + case 2: return 3; default: return 1; } } - return 1; // default: normal + return 1; } diff --git a/shell/src/dbus/freedesktopnotifications.h b/shell/src/dbus/freedesktopnotifications.h index 58387e583..6762e7f1e 100644 --- a/shell/src/dbus/freedesktopnotifications.h +++ b/shell/src/dbus/freedesktopnotifications.h @@ -54,4 +54,4 @@ class FreedesktopNotifications : public QObject, protected QDBusContext { int mapUrgencyToPriority(const QVariantMap &hints); }; -#endif // FREEDESKTOPNOTIFICATIONS_H +#endif diff --git a/shell/src/dbus/notificationdatabase.h b/shell/src/dbus/notificationdatabase.h index c4a982534..71a65fa8f 100644 --- a/shell/src/dbus/notificationdatabase.h +++ b/shell/src/dbus/notificationdatabase.h @@ -48,4 +48,4 @@ class NotificationDatabase : public QObject { NotificationRecord recordFromQuery(class QSqlQuery &query); }; -#endif // NOTIFICATIONDATABASE_H +#endif diff --git a/shell/src/ipc/shellipcserver.cpp b/shell/src/ipc/shellipcserver.cpp index 38e05e22f..fd50c894d 100644 --- a/shell/src/ipc/shellipcserver.cpp +++ b/shell/src/ipc/shellipcserver.cpp @@ -2,20 +2,24 @@ #include "shellservices.h" -#include "../marathonpermissionmanager.h" +#include "marathonpermissionmanager.h" #include "callhistorymanager.h" #include "contactsmanager.h" #include "smsservice.h" #include "telephonyservice.h" #include "medialibrarymanager.h" -#include "../settingsmanager.h" -#include "../bluetoothmanager.h" -#include "../displaymanagercpp.h" -#include "../networkmanagercpp.h" -#include "../powermanagercpp.h" -#include "../audiomanagercpp.h" -#include "../audiopolicycontroller.h" +#include "settingsmanager.h" +#include "bluetoothmanager.h" +#include "displaymanagercpp.h" +#include "networkmanagercpp.h" +#include "powermanagercpp.h" +#include "audiomanagercpp.h" +#include "audiopolicycontroller.h" +#include "hapticmanager.h" +#include "sensormanagercpp.h" +#include "locationmanager.h" +#include "alarmmanagercpp.h" #include #include @@ -27,8 +31,10 @@ ShellIpcServer::ShellIpcServer(MarathonPermissionManager *permissions, ContactsM SettingsManager *settingsManager, BluetoothManager *bluetoothManager, DisplayManagerCpp *displayManager, PowerManagerCpp *powerManager, AudioManagerCpp *audioManager, AudioPolicyController *audioPolicy, - NetworkManagerCpp *networkManager, - AppLaunchService *appLaunchService, QObject *parent) + NetworkManagerCpp *networkManager, HapticManager *hapticManager, + SensorManagerCpp *sensorManager, LocationManager *locationManager, + AlarmManagerCpp *alarmManager, AppLaunchService *appLaunchService, + QObject *parent) : QObject(parent) , m_permissions(permissions) , m_contacts(contacts) @@ -43,6 +49,10 @@ ShellIpcServer::ShellIpcServer(MarathonPermissionManager *permissions, ContactsM , m_audioManager(audioManager) , m_audioPolicy(audioPolicy) , m_networkManager(networkManager) + , m_hapticManager(hapticManager) + , m_sensorManager(sensorManager) + , m_locationManager(locationManager) + , m_alarmManager(alarmManager) , m_appLaunchService(appLaunchService) {} bool ShellIpcServer::registerOnSessionBus() { @@ -54,14 +64,12 @@ bool ShellIpcServer::registerOnSessionBus() { qInfo() << "[ShellIpcServer] Registering DBus service org.marathonos.Shell…"; if (!bus.registerService("org.marathonos.Shell")) { - // If another shell instance is running, this will fail; that's OK for dev, - // but we should treat it as fatal for correctness. + qWarning() << "[ShellIpcServer] Failed to register service org.marathonos.Shell:" << bus.lastError().message(); return false; } - // Register dedicated DBus objects with strict caller enforcement. auto *permObj = new PermissionsObject(m_permissions, m_appLaunchService, this); auto *contactsObj = new ContactsObject(m_contacts, m_permissions, m_appLaunchService, this); auto *callHistoryObj = @@ -79,6 +87,10 @@ bool ShellIpcServer::registerOnSessionBus() { new AudioObject(m_audioManager, m_audioPolicy, m_permissions, m_appLaunchService, this); auto *networkObj = new NetworkObject(m_networkManager, m_permissions, m_appLaunchService, this); auto *navObj = new NavigationObject(m_appLaunchService, this); + auto *hapticObj = new HapticObject(m_hapticManager, m_appLaunchService, this); + auto *sensorObj = new SensorObject(m_sensorManager, m_appLaunchService, this); + auto *locObj = new LocationObject(m_locationManager, m_appLaunchService, this); + auto *alarmObj = new AlarmObject(m_alarmManager, m_appLaunchService, this); bool ok = true; ok &= bus.registerObject("/org/marathonos/Shell/Permissions", permObj, @@ -107,6 +119,14 @@ bool ShellIpcServer::registerOnSessionBus() { QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); ok &= bus.registerObject("/org/marathonos/Shell/Navigation", navObj, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); + ok &= bus.registerObject("/org/marathonos/Shell/Haptic", hapticObj, + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); + ok &= bus.registerObject("/org/marathonos/Shell/Sensor", sensorObj, + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); + ok &= bus.registerObject("/org/marathonos/Shell/Location", locObj, + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); + ok &= bus.registerObject("/org/marathonos/Shell/Alarm", alarmObj, + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); if (!ok) { qWarning() << "[ShellIpcServer] Failed to register one or more DBus objects:" diff --git a/shell/src/ipc/shellipcserver.h b/shell/src/ipc/shellipcserver.h index b6a68ab08..a9b1c65e5 100644 --- a/shell/src/ipc/shellipcserver.h +++ b/shell/src/ipc/shellipcserver.h @@ -18,26 +18,11 @@ class NetworkManagerCpp; class PowerManagerCpp; class AudioManagerCpp; class AudioPolicyController; +class HapticManager; +class SensorManagerCpp; +class LocationManager; +class AlarmManagerCpp; -/** - * Registers the shell-side DBus API used by isolated Marathon app processes. - * - * Service: org.marathonos.Shell - * Objects: - * - /org/marathonos/Shell/Permissions (org.marathonos.Shell.Permissions1) - * - /org/marathonos/Shell/Contacts (org.marathonos.Shell.Contacts1) - * - /org/marathonos/Shell/CallHistory (org.marathonos.Shell.CallHistory1) - * - /org/marathonos/Shell/Telephony (org.marathonos.Shell.Telephony1) - * - /org/marathonos/Shell/Sms (org.marathonos.Shell.Sms1) - * - /org/marathonos/Shell/MediaLibrary (org.marathonos.Shell.MediaLibrary1) - * - /org/marathonos/Shell/Settings (org.marathonos.Shell.Settings1) - * - /org/marathonos/Shell/Bluetooth (org.marathonos.Shell.Bluetooth1) - * - /org/marathonos/Shell/Display (org.marathonos.Shell.Display1) - * - /org/marathonos/Shell/Power (org.marathonos.Shell.Power1) - * - /org/marathonos/Shell/Audio (org.marathonos.Shell.Audio1) - * - /org/marathonos/Shell/Network (org.marathonos.Shell.Network1) - * - /org/marathonos/Shell/Navigation (org.marathonos.Shell.Navigation1) - */ class ShellIpcServer : public QObject { Q_OBJECT @@ -48,7 +33,9 @@ class ShellIpcServer : public QObject { SettingsManager *settingsManager, BluetoothManager *bluetoothManager, DisplayManagerCpp *displayManager, PowerManagerCpp *powerManager, AudioManagerCpp *audioManager, AudioPolicyController *audioPolicy, - NetworkManagerCpp *networkManager, AppLaunchService *appLaunchService, + NetworkManagerCpp *networkManager, HapticManager *hapticManager, + SensorManagerCpp *sensorManager, LocationManager *locationManager, + AlarmManagerCpp *alarmManager, AppLaunchService *appLaunchService, QObject *parent = nullptr); bool registerOnSessionBus(); @@ -67,5 +54,9 @@ class ShellIpcServer : public QObject { AudioManagerCpp *m_audioManager = nullptr; AudioPolicyController *m_audioPolicy = nullptr; NetworkManagerCpp *m_networkManager = nullptr; + HapticManager *m_hapticManager = nullptr; + SensorManagerCpp *m_sensorManager = nullptr; + LocationManager *m_locationManager = nullptr; + AlarmManagerCpp *m_alarmManager = nullptr; AppLaunchService *m_appLaunchService = nullptr; }; diff --git a/shell/src/ipc/shellservices.cpp b/shell/src/ipc/shellservices.cpp index e905c3aa9..7a9871dbb 100644 --- a/shell/src/ipc/shellservices.cpp +++ b/shell/src/ipc/shellservices.cpp @@ -1,16 +1,20 @@ #include "shellservices.h" -#include "../applaunchservice.h" -#include "../marathonpermissionmanager.h" -#include "../settingsmanager.h" -#include "../bluetoothmanager.h" -#include "../displaymanagercpp.h" -#include "../networkmanagercpp.h" -#include "../powermanagercpp.h" -#include "../audiomanagercpp.h" -#include "../audiopolicycontroller.h" -#include "../ratelimiter.h" -#include "../securitylogger.h" +#include "applaunchservice.h" +#include "marathonpermissionmanager.h" +#include "settingsmanager.h" +#include "bluetoothmanager.h" +#include "displaymanagercpp.h" +#include "networkmanagercpp.h" +#include "powermanagercpp.h" +#include "audiomanagercpp.h" +#include "audiopolicycontroller.h" +#include "hapticmanager.h" +#include "sensormanagercpp.h" +#include "locationmanager.h" +#include "alarmmanagercpp.h" +#include "ratelimiter.h" +#include "securitylogger.h" #include "callhistorymanager.h" #include "contactsmanager.h" @@ -1492,6 +1496,18 @@ void TelephonyObject::SendDTMF(const QString &digit) { m_telephony->sendDTMF(digit); } +void TelephonyObject::SimulateIncomingCall(const QString &number) { + if (!requirePhone()) + return; + m_telephony->simulateIncomingCall(number); +} + +void TelephonyObject::SimulateCallStateChange(const QString &state) { + if (!requirePhone()) + return; + m_telephony->simulateCallStateChange(state); +} + SmsObject::SmsObject(SMSService *sms, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -1561,6 +1577,12 @@ QString SmsObject::GenerateConversationId(const QString &number) { return m_sms->generateConversationId(number); } +void SmsObject::SimulateIncomingSMS(const QString &sender, const QString &message) { + if (!requireSms()) + return; + m_sms->simulateIncomingSMS(sender, message); +} + NavigationObject::NavigationObject(AppLaunchService *launchService, QObject *parent) : QObject(parent) , m_launchService(launchService) { @@ -1650,3 +1672,172 @@ bool NavigationObject::LaunchAppWithRoute(const QString &appId, const QString &r } return ok; } + +HapticObject::HapticObject(HapticManager *haptic, AppLaunchService *launchService, QObject *parent) + : QObject(parent) + , m_haptic(haptic) + , m_launchService(launchService) { + Q_ASSERT(m_haptic); +} + +bool HapticObject::IsAvailable() const { + return m_haptic ? m_haptic->available() : false; +} + +void HapticObject::Light() { + if (m_haptic && m_haptic->available()) + m_haptic->vibrate(10); +} + +void HapticObject::Medium() { + if (m_haptic && m_haptic->available()) + m_haptic->vibrate(25); +} + +void HapticObject::Heavy() { + if (m_haptic && m_haptic->available()) + m_haptic->vibrate(50); +} + +void HapticObject::Vibrate(int duration) { + if (m_haptic && m_haptic->available()) + m_haptic->vibrate(duration); +} + +void HapticObject::VibratePattern(const QVariantList &pattern) { + if (m_haptic && m_haptic->available()) + m_haptic->vibratePatternVariant(pattern); +} + +void HapticObject::Stop() { + if (m_haptic) + m_haptic->cancelVibration(); +} + +SensorObject::SensorObject(SensorManagerCpp *sensors, AppLaunchService *launchService, + QObject *parent) + : QObject(parent) + , m_sensors(sensors) + , m_launchService(launchService) { + Q_ASSERT(m_sensors); + QObject::connect(m_sensors, &SensorManagerCpp::proximityNearChanged, this, + [this]() { emit ProximityChanged(m_sensors->proximityNear()); }); + QObject::connect(m_sensors, &SensorManagerCpp::ambientLightChanged, this, + [this]() { emit AmbientLightChanged(m_sensors->ambientLight()); }); +} + +bool SensorObject::IsAvailable() const { + return m_sensors ? m_sensors->available() : false; +} + +bool SensorObject::ProximityNear() const { + return m_sensors ? m_sensors->proximityNear() : false; +} + +int SensorObject::AmbientLight() const { + return m_sensors ? m_sensors->ambientLight() : 0; +} + +LocationObject::LocationObject(LocationManager *location, AppLaunchService *launchService, + QObject *parent) + : QObject(parent) + , m_location(location) + , m_launchService(launchService) { + Q_ASSERT(m_location); + QObject::connect(m_location, &LocationManager::locationChanged, this, + [this]() { emit LocationChanged(GetLocation()); }); + QObject::connect(m_location, &LocationManager::activeChanged, this, + [this]() { emit activeChanged(m_location->active()); }); +} + +bool LocationObject::IsAvailable() const { + return m_location ? m_location->available() : false; +} + +bool LocationObject::IsActive() const { + return m_location ? m_location->active() : false; +} + +void LocationObject::Start() { + if (m_location) + m_location->start(); +} + +void LocationObject::Stop() { + if (m_location) + m_location->stop(); +} + +QVariantMap LocationObject::GetLocation() const { + QVariantMap loc; + if (m_location) { + loc["latitude"] = m_location->latitude(); + loc["longitude"] = m_location->longitude(); + loc["accuracy"] = m_location->accuracy(); + loc["altitude"] = m_location->altitude(); + loc["speed"] = m_location->speed(); + loc["heading"] = m_location->heading(); + loc["timestamp"] = m_location->timestamp(); + } + return loc; +} + +AlarmObject::AlarmObject(AlarmManagerCpp *alarms, AppLaunchService *launchService, QObject *parent) + : QObject(parent) + , m_alarms(alarms) + , m_launchService(launchService) { + Q_ASSERT(m_alarms); + connect(m_alarms, &AlarmManagerCpp::alarmsChanged, this, &AlarmObject::AlarmsChanged); + connect(m_alarms, &AlarmManagerCpp::activeAlarmsChanged, this, + &AlarmObject::ActiveAlarmsChanged); + connect(m_alarms, &AlarmManagerCpp::alarmTriggered, this, &AlarmObject::AlarmTriggered); + connect(m_alarms, &AlarmManagerCpp::alarmDismissed, this, &AlarmObject::AlarmDismissed); + connect(m_alarms, &AlarmManagerCpp::alarmSnoozed, this, &AlarmObject::AlarmSnoozed); +} + +QVariantList AlarmObject::GetAlarms() const { + return m_alarms ? m_alarms->alarms() : QVariantList(); +} + +QVariantList AlarmObject::GetActiveAlarms() const { + return m_alarms ? m_alarms->activeAlarms() : QVariantList(); +} + +QString AlarmObject::CreateAlarm(const QString &time, const QString &label, + const QList &repeat, const QVariantMap &options) { + return m_alarms ? m_alarms->createAlarm(time, label, repeat, options) : QString(); +} + +bool AlarmObject::UpdateAlarm(const QString &id, const QVariantMap &updates) { + return m_alarms ? m_alarms->updateAlarm(id, updates) : false; +} + +bool AlarmObject::DeleteAlarm(const QString &id) { + return m_alarms ? m_alarms->deleteAlarm(id) : false; +} + +bool AlarmObject::EnableAlarm(const QString &id) { + return m_alarms ? m_alarms->enableAlarm(id) : false; +} + +bool AlarmObject::DisableAlarm(const QString &id) { + return m_alarms ? m_alarms->disableAlarm(id) : false; +} + +bool AlarmObject::SnoozeAlarm(const QString &id) { + return m_alarms ? m_alarms->snoozeAlarm(id) : false; +} + +bool AlarmObject::DismissAlarm(const QString &id) { + return m_alarms ? m_alarms->dismissAlarm(id) : false; +} + +void AlarmObject::StopAll() { + if (m_alarms) + m_alarms->stopAll(); +} + +void AlarmObject::TriggerAlarmNow(const QString &label) { + if (m_alarms) + m_alarms->triggerAlarmNow(label); +} diff --git a/shell/src/ipc/shellservices.h b/shell/src/ipc/shellservices.h index 26e5ef4cb..e675e4d66 100644 --- a/shell/src/ipc/shellservices.h +++ b/shell/src/ipc/shellservices.h @@ -21,10 +21,10 @@ class NetworkManagerCpp; class PowerManagerCpp; class AudioManagerCpp; class AudioPolicyController; - -// Service: org.marathonos.Shell -// All objects enforce strict caller identity: -// DBus sender → PID → appId (via AppLaunchService) and optional permission checks. +class HapticManager; +class SensorManagerCpp; +class LocationManager; +class AlarmManagerCpp; class PermissionsObject : public QObject, protected QDBusContext { Q_OBJECT @@ -129,6 +129,9 @@ class TelephonyObject : public QObject, protected QDBusContext { void Hangup(); void SendDTMF(const QString &digit); + void SimulateIncomingCall(const QString &number); + void SimulateCallStateChange(const QString &state); + signals: void CallStateChanged(const QString &state); void IncomingCall(const QString &number); @@ -161,6 +164,8 @@ class SmsObject : public QObject, protected QDBusContext { void MarkAsRead(const QString &conversationId); QString GenerateConversationId(const QString &number); + void SimulateIncomingSMS(const QString &sender, const QString &message); + signals: void MessageReceived(const QString &sender, const QString &text, qint64 timestamp); void MessageSent(const QString &recipient, qint64 timestamp); @@ -457,3 +462,106 @@ class NavigationObject : public QObject, protected QDBusContext { QString callerAppIdOrEmpty() const; AppLaunchService *m_launchService = nullptr; }; + +class HapticObject : public QObject, protected QDBusContext { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.marathonos.Shell.Haptic1") + + public: + HapticObject(HapticManager *haptic, AppLaunchService *launchService, QObject *parent = nullptr); + + public slots: + bool IsAvailable() const; + void Light(); + void Medium(); + void Heavy(); + void Vibrate(int duration); + void VibratePattern(const QVariantList &pattern); + void Stop(); + + private: + HapticManager *m_haptic = nullptr; + AppLaunchService *m_launchService = nullptr; +}; + +class SensorObject : public QObject, protected QDBusContext { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.marathonos.Shell.Sensor1") + + public: + SensorObject(SensorManagerCpp *sensors, AppLaunchService *launchService, + QObject *parent = nullptr); + + public slots: + bool IsAvailable() const; + bool ProximityNear() const; + int AmbientLight() const; + + signals: + void ProximityChanged(bool near); + void AmbientLightChanged(int lux); + + private: + SensorManagerCpp *m_sensors = nullptr; + AppLaunchService *m_launchService = nullptr; +}; + +class LocationObject : public QObject, protected QDBusContext { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.marathonos.Shell.Location1") + + public: + LocationObject(LocationManager *location, AppLaunchService *launchService, + QObject *parent = nullptr); + + public slots: + bool IsAvailable() const; + bool IsActive() const; + void Start(); + void Stop(); + QVariantMap GetLocation() const; + + signals: + void LocationChanged(const QVariantMap &location); + void activeChanged(bool active); + + private: + LocationManager *m_location = nullptr; + AppLaunchService *m_launchService = nullptr; +}; + +class AlarmObject : public QObject, protected QDBusContext { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.marathonos.Shell.Alarm1") + + public: + AlarmObject(AlarmManagerCpp *alarms, AppLaunchService *launchService, + QObject *parent = nullptr); + + public slots: + + QVariantList GetAlarms() const; + QVariantList GetActiveAlarms() const; + + QString CreateAlarm(const QString &time, const QString &label, const QList &repeat, + const QVariantMap &options); + bool UpdateAlarm(const QString &id, const QVariantMap &updates); + bool DeleteAlarm(const QString &id); + bool EnableAlarm(const QString &id); + bool DisableAlarm(const QString &id); + bool SnoozeAlarm(const QString &id); + bool DismissAlarm(const QString &id); + void StopAll(); + void TriggerAlarmNow(const QString &label); + + signals: + void AlarmsChanged(); + void ActiveAlarmsChanged(); + void AlarmTriggered(const QString &id, const QString &label); + void AlarmDismissed(const QString &id); + void AlarmSnoozed(const QString &id, int minutes); + + private: + AlarmManagerCpp *m_alarms = nullptr; + AppLaunchService *m_launchService = nullptr; +}; diff --git a/shell/src/lunasvgimageprovider.h b/shell/src/lunasvgimageprovider.h deleted file mode 100644 index 4956aa7a8..000000000 --- a/shell/src/lunasvgimageprovider.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef LUNASVGIMAGEPROVIDER_H -#define LUNASVGIMAGEPROVIDER_H - -#include -#include -#include - -/** - * @brief QML Image Provider that uses LunaSVG for rendering SVG files. - * - * Qt's built-in SVG renderer doesn't support clipPath elements, which causes - * GNOME icons to render with black backgrounds. LunaSVG provides full SVG - * support including clipPath, masks, and filters. - * - * Usage in QML: - * Image { source: "image://lunasvg/" + iconPath } - */ -class LunaSvgImageProvider : public QQuickImageProvider { - public: - LunaSvgImageProvider(); - - /** - * @brief Render an SVG file to a QImage at the requested size. - * @param id The file path of the SVG (without "image://lunasvg/" prefix) - * @param size Requested output size - * @param requestedSize The size requested by QML (may be invalid) - * @return Rendered QImage, or empty QImage on failure - */ - QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; -}; - -#endif // LUNASVGIMAGEPROVIDER_H diff --git a/shell/src/managers/alarmmanagercpp.cpp b/shell/src/managers/alarmmanagercpp.cpp new file mode 100644 index 000000000..b102d8a31 --- /dev/null +++ b/shell/src/managers/alarmmanagercpp.cpp @@ -0,0 +1,368 @@ +#include "alarmmanagercpp.h" +#include "settingsmanager.h" +#include "powermanagercpp.h" +#include "audiomanagercpp.h" +#include "hapticmanager.h" +#include +#include +#include +#include +#include +#include + +Alarm Alarm::fromJson(const QJsonObject &json) { + Alarm a; + a.id = json["id"].toString(); + a.time = json["time"].toString(); + a.enabled = json["enabled"].toBool(); + a.label = json["label"].toString(); + a.sound = json["sound"].toString(); + a.vibrate = json["vibrate"].toBool(); + a.snoozeEnabled = json["snoozeEnabled"].toBool(); + a.snoozeDuration = json["snoozeDuration"].toInt(10); + + QJsonArray repeatArr = json["repeat"].toArray(); + for (const auto &v : repeatArr) { + a.repeat.append(v.toInt()); + } + return a; +} + +QJsonObject Alarm::toJson() const { + QJsonObject json; + json["id"] = id; + json["time"] = time; + json["enabled"] = enabled; + json["label"] = label; + json["sound"] = sound; + json["vibrate"] = vibrate; + json["snoozeEnabled"] = snoozeEnabled; + json["snoozeDuration"] = snoozeDuration; + + QJsonArray repeatArr; + for (int day : repeat) + repeatArr.append(day); + json["repeat"] = repeatArr; + + return json; +} + +QVariantMap Alarm::toVariantMap() const { + return toJson().toVariantMap(); +} + +AlarmManagerCpp::AlarmManagerCpp(SettingsManager *settings, PowerManagerCpp *power, + AudioManagerCpp *audio, HapticManager *haptic, QObject *parent) + : QObject(parent) + , m_settings(settings) + , m_power(power) + , m_audio(audio) + , m_haptic(haptic) + , m_checkTimer(new QTimer(this)) + , m_soundEffect(new QSoundEffect(this)) { + + m_soundEffect->setLoopCount(QSoundEffect::Infinite); + m_checkTimer->setSingleShot(true); + connect(m_checkTimer, &QTimer::timeout, this, &AlarmManagerCpp::checkAlarms); + + QTimer::singleShot(0, this, [this]() { + loadAlarms(); + scheduleNextAlarm(); + }); +} + +QVariantList AlarmManagerCpp::alarms() const { + QVariantList list; + for (const auto &a : m_alarms) + list.append(a.toVariantMap()); + return list; +} + +QVariantList AlarmManagerCpp::activeAlarms() const { + QVariantList list; + for (const auto &a : m_activeAlarms) + list.append(a.toVariantMap()); + return list; +} + +bool AlarmManagerCpp::hasEnabledAlarm() const { + for (const auto &a : m_alarms) { + if (a.enabled) + return true; + } + return false; +} + +bool AlarmManagerCpp::hasActiveAlarm() const { + return !m_activeAlarms.isEmpty(); +} + +QString AlarmManagerCpp::createAlarm(const QString &time, const QString &label, + const QList &repeat, const QVariantMap &options) { + Alarm a; + a.time = time; + a.label = label.isEmpty() ? "Alarm" : label; + a.repeat = repeat; + a.enabled = true; + a.sound = options.value("sound", "default").toString(); + a.vibrate = options.value("vibrate", true).toBool(); + a.snoozeEnabled = options.value("snoozeEnabled", true).toBool(); + a.snoozeDuration = options.value("snoozeDuration", 10).toInt(); + + QString input = QString::number(QDateTime::currentMSecsSinceEpoch()) + time + label; + a.id = QString(QCryptographicHash::hash(input.toUtf8(), QCryptographicHash::Md5).toHex()); + + m_alarms.append(a); + emit alarmsChanged(); + saveAlarms(); + scheduleNextAlarm(); + qInfo() << "[AlarmManagerCpp] Created alarm:" << a.id << "at" << time; + return a.id; +} + +bool AlarmManagerCpp::updateAlarm(const QString &id, const QVariantMap &updates) { + for (auto &a : m_alarms) { + if (a.id == id) { + if (updates.contains("time")) + a.time = updates["time"].toString(); + if (updates.contains("label")) + a.label = updates["label"].toString(); + if (updates.contains("enabled")) + a.enabled = updates["enabled"].toBool(); + if (updates.contains("sound")) + a.sound = updates["sound"].toString(); + if (updates.contains("vibrate")) + a.vibrate = updates["vibrate"].toBool(); + if (updates.contains("snoozeEnabled")) + a.snoozeEnabled = updates["snoozeEnabled"].toBool(); + if (updates.contains("snoozeDuration")) + a.snoozeDuration = updates["snoozeDuration"].toInt(); + if (updates.contains("repeat")) { + a.repeat.clear(); + QVariantList r = updates["repeat"].toList(); + for (const auto &v : r) + a.repeat.append(v.toInt()); + } + + emit alarmsChanged(); + saveAlarms(); + scheduleNextAlarm(); + qInfo() << "[AlarmManagerCpp] Updated alarm:" << id; + return true; + } + } + return false; +} + +bool AlarmManagerCpp::deleteAlarm(const QString &id) { + for (int i = 0; i < m_alarms.size(); ++i) { + if (m_alarms[i].id == id) { + m_alarms.removeAt(i); + emit alarmsChanged(); + saveAlarms(); + scheduleNextAlarm(); + qInfo() << "[AlarmManagerCpp] Deleted alarm:" << id; + return true; + } + } + return false; +} + +bool AlarmManagerCpp::enableAlarm(const QString &id) { + return updateAlarm(id, {{"enabled", true}}); +} + +bool AlarmManagerCpp::disableAlarm(const QString &id) { + return updateAlarm(id, {{"enabled", false}}); +} + +bool AlarmManagerCpp::snoozeAlarm(const QString &id) { + for (int i = 0; i < m_activeAlarms.size(); ++i) { + if (m_activeAlarms[i].id == id) { + Alarm a = m_activeAlarms[i]; + m_activeAlarms.removeAt(i); + emit activeAlarmsChanged(); + + if (m_activeAlarms.isEmpty()) + stopAll(); + + qInfo() << "[AlarmManagerCpp] Snoozed alarm:" << id; + emit alarmSnoozed(id, a.snoozeDuration); + return true; + } + } + return false; +} + +bool AlarmManagerCpp::dismissAlarm(const QString &id) { + for (int i = 0; i < m_activeAlarms.size(); ++i) { + if (m_activeAlarms[i].id == id) { + m_activeAlarms.removeAt(i); + emit activeAlarmsChanged(); + + if (m_activeAlarms.isEmpty()) + stopAll(); + + emit alarmDismissed(id); + qInfo() << "[AlarmManagerCpp] Dismissed alarm:" << id; + scheduleNextAlarm(); + return true; + } + } + return false; +} + +void AlarmManagerCpp::stopAll() { + if (m_soundEffect->isPlaying()) + m_soundEffect->stop(); + if (m_haptic) + m_haptic->cancelVibration(); + if (m_power) + m_power->releaseWakelock("alarm_ringing"); +} + +void AlarmManagerCpp::triggerAlarmNow(const QString &label) { + Alarm a; + a.time = QTime::currentTime().toString("HH:mm"); + a.label = label.isEmpty() ? "Test Alarm" : label; + a.repeat = {}; + a.enabled = true; + a.sound = "default"; + a.vibrate = true; + a.snoozeEnabled = true; + a.snoozeDuration = 10; + + const QString input = QString::number(QDateTime::currentMSecsSinceEpoch()) + a.time + a.label; + a.id = QString(QCryptographicHash::hash(input.toUtf8(), QCryptographicHash::Md5).toHex()); + triggerAlarm(a); +} + +void AlarmManagerCpp::loadAlarms() { + QString jsonStr = m_settings->get("alarms/data", "[]").toString(); + QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8()); + QJsonArray arr = doc.array(); + + m_alarms.clear(); + for (const auto &v : arr) { + m_alarms.append(Alarm::fromJson(v.toObject())); + } + emit alarmsChanged(); + qInfo() << "[AlarmManagerCpp] Loaded" << m_alarms.size() << "alarms"; +} + +void AlarmManagerCpp::saveAlarms() { + QJsonArray arr; + for (const auto &a : m_alarms) + arr.append(a.toJson()); + + QJsonDocument doc(arr); + m_settings->set("alarms/data", QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); +} + +void AlarmManagerCpp::scheduleNextAlarm() { + QDateTime now = QDateTime::currentDateTime(); + QDateTime nextTime; + bool found = false; + + for (const auto &a : m_alarms) { + if (!a.enabled) + continue; + QDateTime t = calculateNextOccurrence(a, now); + if (!found || t < nextTime) { + nextTime = t; + found = true; + } + } + + m_checkTimer->stop(); + if (found) { + qint64 ms = now.msecsTo(nextTime); + if (ms < 0) + ms = 0; + + if (m_power) + m_power->setRtcAlarm(nextTime.toSecsSinceEpoch()); + + m_checkTimer->start(ms); + qInfo() << "[AlarmManagerCpp] Next alarm in" << ms / 60000 << "minutes"; + } +} + +QDateTime AlarmManagerCpp::calculateNextOccurrence(const Alarm &alarm, const QDateTime &from) { + QStringList parts = alarm.time.split(":"); + if (parts.size() != 2) + return from.addYears(1); + + int h = parts[0].toInt(); + int m = parts[1].toInt(); + + QDateTime next = from; + next.setTime(QTime(h, m)); + + if (next <= from) + next = next.addDays(1); + + if (alarm.repeat.isEmpty()) + return next; + + for (int i = 0; i < 7; ++i) { + + int checkDay = next.date().dayOfWeek() % 7; + if (alarm.repeat.contains(checkDay)) + return next; + + next = next.addDays(1); + } + return next; +} + +void AlarmManagerCpp::checkAlarms() { + QDateTime now = QDateTime::currentDateTime(); + QString timeStr = now.time().toString("HH:mm"); + int day = now.date().dayOfWeek() % 7; + + for (const auto &a : m_alarms) { + if (!a.enabled) + continue; + if (a.time != timeStr) + continue; + + if (!a.repeat.isEmpty() && !a.repeat.contains(day)) + continue; + + triggerAlarm(a); + } + scheduleNextAlarm(); +} + +void AlarmManagerCpp::triggerAlarm(const Alarm &alarm) { + qInfo() << "[AlarmManagerCpp] TRIGGERING ALARM:" << alarm.label; + + m_activeAlarms.append(alarm); + emit activeAlarmsChanged(); + emit alarmTriggered(alarm.id, alarm.label); + + if (m_power) + m_power->acquireWakelock("alarm_ringing"); + + if (!alarm.sound.isEmpty()) { + QString soundPath = alarm.sound; + if (soundPath == "default" || soundPath.isEmpty()) { + if (m_settings) + soundPath = m_settings->alarmSound(); + } + + if (!soundPath.contains("://")) + soundPath = "file://" + soundPath; + + m_soundEffect->setSource(QUrl(soundPath)); + m_soundEffect->setVolume(1.0); + m_soundEffect->play(); + } + + if (m_haptic && alarm.vibrate) { + QVariantList pattern; + pattern << 500 << 500; + m_haptic->vibratePatternVariant(pattern); + } +} diff --git a/shell/src/managers/alarmmanagercpp.h b/shell/src/managers/alarmmanagercpp.h new file mode 100644 index 000000000..ab4a38a5c --- /dev/null +++ b/shell/src/managers/alarmmanagercpp.h @@ -0,0 +1,91 @@ +#ifndef ALARMMANAGERCPP_H +#define ALARMMANAGERCPP_H + +#include +#include +#include +#include +#include +#include +#include + +class SettingsManager; +class PowerManagerCpp; +class AudioManagerCpp; +class HapticManager; +class QSoundEffect; + +struct Alarm { + QString id; + QString time; + bool enabled; + QString label; + QList repeat; + QString sound; + bool vibrate; + bool snoozeEnabled; + int snoozeDuration; + + static Alarm fromJson(const QJsonObject &json); + QJsonObject toJson() const; + QVariantMap toVariantMap() const; + static QList toIntList(const QJsonArray &array); +}; + +class AlarmManagerCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QVariantList alarms READ alarms NOTIFY alarmsChanged) + Q_PROPERTY(QVariantList activeAlarms READ activeAlarms NOTIFY activeAlarmsChanged) + Q_PROPERTY(bool hasEnabledAlarm READ hasEnabledAlarm NOTIFY alarmsChanged) + Q_PROPERTY(bool hasActiveAlarm READ hasActiveAlarm NOTIFY activeAlarmsChanged) + + public: + explicit AlarmManagerCpp(SettingsManager *settings, PowerManagerCpp *power, + AudioManagerCpp *audio, HapticManager *haptic, + QObject *parent = nullptr); + + QVariantList alarms() const; + QVariantList activeAlarms() const; + bool hasEnabledAlarm() const; + bool hasActiveAlarm() const; + + Q_INVOKABLE QString createAlarm(const QString &time, const QString &label, + const QList &repeat, const QVariantMap &options); + Q_INVOKABLE bool updateAlarm(const QString &id, const QVariantMap &updates); + Q_INVOKABLE bool deleteAlarm(const QString &id); + Q_INVOKABLE bool enableAlarm(const QString &id); + Q_INVOKABLE bool disableAlarm(const QString &id); + Q_INVOKABLE bool snoozeAlarm(const QString &id); + Q_INVOKABLE bool dismissAlarm(const QString &id); + Q_INVOKABLE void stopAll(); + Q_INVOKABLE void triggerAlarmNow(const QString &label = QString()); + + signals: + void alarmsChanged(); + void activeAlarmsChanged(); + void alarmTriggered(const QString &id, const QString &label); + void alarmDismissed(const QString &id); + void alarmSnoozed(const QString &id, int minutes); + + private slots: + void checkAlarms(); + + private: + void loadAlarms(); + void saveAlarms(); + void scheduleNextAlarm(); + void triggerAlarm(const Alarm &alarm); + QDateTime calculateNextOccurrence(const Alarm &alarm, const QDateTime &from); + + SettingsManager *m_settings; + PowerManagerCpp *m_power; + AudioManagerCpp *m_audio; + HapticManager *m_haptic; + + QList m_alarms; + QList m_activeAlarms; + QTimer *m_checkTimer; + QSoundEffect *m_soundEffect; +}; + +#endif diff --git a/shell/src/audiomanagercpp.cpp b/shell/src/managers/audiomanagercpp.cpp similarity index 91% rename from shell/src/audiomanagercpp.cpp rename to shell/src/managers/audiomanagercpp.cpp index b99a2b8b3..daba4fe29 100644 --- a/shell/src/audiomanagercpp.cpp +++ b/shell/src/managers/audiomanagercpp.cpp @@ -8,7 +8,6 @@ #include #include -// AudioStreamModel implementation AudioStreamModel::AudioStreamModel(QObject *parent) : QAbstractListModel(parent) {} @@ -61,7 +60,6 @@ AudioStream *AudioStreamModel::getStream(int streamId) { return nullptr; } -// AudioManagerCpp implementation AudioManagerCpp::AudioManagerCpp(QObject *parent) : QObject(parent) , m_available(false) @@ -75,7 +73,6 @@ AudioManagerCpp::AudioManagerCpp(QObject *parent) , m_pa_context(nullptr) { qDebug() << "[AudioManagerCpp] Initializing"; - // Try PipeWire first via wpctl QProcess checkPipewire; checkPipewire.start("wpctl", {"status"}); checkPipewire.waitForFinished(1000); @@ -85,13 +82,11 @@ AudioManagerCpp::AudioManagerCpp(QObject *parent) m_isPipeWire = true; qInfo() << "[AudioManagerCpp] PipeWire/WirePlumber available with per-app volume support"; - // Get initial volume QProcess process; process.start("wpctl", {"get-volume", "@DEFAULT_AUDIO_SINK@"}); process.waitForFinished(); - QString output = process.readAllStandardOutput(); + QString output = process.readAllStandardOutput(); - // Parse "Volume: 0.60" or "Volume: 0.60 [MUTED]" QRegularExpression re("Volume: ([0-9.]+)"); QRegularExpressionMatch match = re.match(output); if (match.hasMatch()) { @@ -104,12 +99,11 @@ AudioManagerCpp::AudioManagerCpp(QObject *parent) emit mutedChanged(); } - // Parse streams and start monitoring parseWpctlStatus(); startStreamMonitoring(); } else { - // Fallback to PulseAudio + if (initPulseAudio()) { m_available = true; m_isPipeWire = false; @@ -132,10 +126,8 @@ void AudioManagerCpp::setVolume(double volume) { return; } - // Clamp volume to 0.0-1.0 volume = qBound(0.0, volume, 1.0); - // Try PipeWire first if (m_isPipeWire) { QProcess wpctl; wpctl.start("wpctl", {"set-volume", "@DEFAULT_AUDIO_SINK@", QString::number(volume)}); @@ -149,7 +141,7 @@ void AudioManagerCpp::setVolume(double volume) { return; } } else { - // Fallback to PulseAudio + if (m_pa_context && m_pa_mainloop) { if (m_defaultSinkName.isEmpty()) { qWarning() << "[PulseAudio] Default sink name is empty!"; @@ -181,7 +173,6 @@ void AudioManagerCpp::setMuted(bool muted) { return; } - // Try PipeWire first if (m_isPipeWire) { QProcess wpctl; wpctl.start("wpctl", {"set-mute", "@DEFAULT_AUDIO_SINK@", muted ? "1" : "0"}); @@ -194,7 +185,7 @@ void AudioManagerCpp::setMuted(bool muted) { return; } } else { - // Fallback to PulseAudio + if (m_pa_context && m_pa_mainloop) { pa_threaded_mainloop_lock(m_pa_mainloop); @@ -267,9 +258,6 @@ void AudioManagerCpp::parseWpctlStatus() { QString output = process.readAllStandardOutput(); QList streams; - // Parse wpctl status output to find audio streams - // Format: " │ ├─ 47. Firefox [vol: 0.65]" - // or " │ ├─ 47. Firefox [vol: 0.65 MUTED]" QRegularExpression streamRe( "^\\s+[│├─]+\\s+(\\d+)\\.\\s+(.+?)\\s+\\[vol:\\s+([0-9.]+)(?:\\s+MUTED)?\\]", QRegularExpression::MultilineOption); @@ -294,7 +282,6 @@ void AudioManagerCpp::parseWpctlStatus() { continue; } - // Only parse streams in the Sinks section or Streams section if (!inSinksSection && !inStreamsSection) { continue; } @@ -304,12 +291,11 @@ void AudioManagerCpp::parseWpctlStatus() { AudioStream stream; stream.id = match.captured(1).toInt(); stream.name = match.captured(2).trimmed(); - stream.appName = stream.name; // Use name as appName for now + stream.appName = stream.name; stream.volume = match.captured(3).toDouble(); stream.muted = line.contains("MUTED"); stream.mediaClass = inStreamsSection ? "Stream/Output/Audio" : "Audio/Sink"; - // Only add output streams (not sinks themselves) if (inStreamsSection && stream.id > 0) { streams.append(stream); } @@ -320,26 +306,21 @@ void AudioManagerCpp::parseWpctlStatus() { m_streamModel->updateStreams(streams); emit streamsChanged(); - // Update playback state based on active streams updatePlaybackState(); } void AudioManagerCpp::startStreamMonitoring() { - // Refresh streams every 5 seconds + connect(m_streamRefreshTimer, &QTimer::timeout, this, &AudioManagerCpp::refreshStreams); m_streamRefreshTimer->start(5000); qDebug() << "[AudioManagerCpp] Started stream monitoring (5s interval)"; } void AudioManagerCpp::updatePlaybackState() { - // Check if any streams are currently playing - // A stream is considered "playing" if it exists and is not muted - // (wpctl doesn't provide explicit playback state, so we use stream existence as a proxy) bool wasPlaying = m_isPlaying; bool isNowPlaying = false; - // If we have any active streams in the model, consider audio as playing if (m_streamModel->rowCount() > 0) { isNowPlaying = true; } @@ -352,7 +333,6 @@ void AudioManagerCpp::updatePlaybackState() { } } -// PulseAudio bool AudioManagerCpp::initPulseAudio() { m_pa_mainloop = pa_threaded_mainloop_new(); if (!m_pa_mainloop) diff --git a/shell/src/audiomanagercpp.h b/shell/src/managers/audiomanagercpp.h similarity index 97% rename from shell/src/audiomanagercpp.h rename to shell/src/managers/audiomanagercpp.h index 3e2378c6d..e17823d54 100644 --- a/shell/src/audiomanagercpp.h +++ b/shell/src/managers/audiomanagercpp.h @@ -13,7 +13,7 @@ struct AudioStream { QString appName; double volume; bool muted; - QString mediaClass; // "Stream/Output/Audio" etc. + QString mediaClass; }; class AudioStreamModel : public QAbstractListModel { @@ -117,4 +117,4 @@ class AudioManagerCpp : public QObject { pa_context *m_pa_context; }; -#endif // AUDIOMANAGERCPP_H +#endif diff --git a/shell/src/bluetoothmanager.cpp b/shell/src/managers/bluetoothmanager.cpp similarity index 89% rename from shell/src/bluetoothmanager.cpp rename to shell/src/managers/bluetoothmanager.cpp index 785f26cb9..1ab270496 100644 --- a/shell/src/bluetoothmanager.cpp +++ b/shell/src/managers/bluetoothmanager.cpp @@ -7,7 +7,6 @@ #include #include -// BluetoothDevice implementation BluetoothDevice::BluetoothDevice(const QString &path, QObject *parent) : QObject(parent) , m_path(path) { @@ -69,13 +68,12 @@ void BluetoothDevice::updateProperties() { } QString rawIcon = props.value("Icon").toString(); - QString newIcon = "bluetooth"; // Default + QString newIcon = "bluetooth"; - // Map Freedesktop icon names to available Lucide icons if (rawIcon.contains("audio-card") || rawIcon.contains("speaker")) { newIcon = "volume-2"; } else if (rawIcon.contains("headphone") || rawIcon.contains("headset")) { - newIcon = "music"; // We don't have headphones icon yet + newIcon = "music"; } else if (rawIcon.contains("computer") || rawIcon.contains("laptop") || rawIcon.contains("monitor")) { newIcon = "monitor"; @@ -86,10 +84,6 @@ void BluetoothDevice::updateProperties() { } else if (rawIcon.contains("watch")) { newIcon = "clock"; } else if (!rawIcon.isEmpty()) { - // Try to use the raw name if we have it, otherwise default - // This is a bit risky without checking existence, but "bluetooth" is a safe fallback in QML if load fails? - // Actually QML warns if load fails. Let's stick to mapped icons or default. - // newIcon = rawIcon; } if (newIcon != m_icon) { @@ -101,13 +95,11 @@ void BluetoothDevice::updateProperties() { << ") paired=" << m_paired << "connected=" << m_connected; } -// BluetoothManager implementation BluetoothManager::BluetoothManager(QObject *parent) : QObject(parent) , m_bus(QDBusConnection::systemBus()) { qDebug() << "[BluetoothManager] Initializing"; - // Register the complex type for DBus qRegisterMetaType("ManagedObjectMap"); qDBusRegisterMetaType(); qRegisterMetaType("PropertyMap"); @@ -139,7 +131,6 @@ void BluetoothManager::initializeAdapter() { QDBusPendingCall asyncCall = manager.asyncCall("GetManagedObjects"); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(asyncCall, this); - // Ensure watcher is cleaned up even if the connected slot returns early. connect(watcher, &QDBusPendingCallWatcher::finished, watcher, &QObject::deleteLater); connect( @@ -147,7 +138,7 @@ void BluetoothManager::initializeAdapter() { QDBusPendingReply reply = *call; if (reply.isError()) { - // Log once only - bluez may not be running in VM or on systems without Bluetooth + static bool hasLogged = false; if (!hasLogged) { qDebug() << "[BluetoothManager] Bluetooth not available (bluez service not " @@ -164,7 +155,6 @@ void BluetoothManager::initializeAdapter() { qDebug() << "[BluetoothManager] Scanning" << objects.count() << "managed objects..."; - // First pass: Find adapter for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) { if (it.value().contains("org.bluez.Adapter1")) { m_adapterPath = it.key().path(); @@ -188,12 +178,11 @@ void BluetoothManager::initializeAdapter() { return; } - // Second pass: Find devices int deviceCount = 0; for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) { if (it.value().contains("org.bluez.Device1")) { QString path = it.key().path(); - // Only add devices belonging to our adapter + if (path.startsWith(m_adapterPath)) { addDevice(path); deviceCount++; @@ -211,15 +200,13 @@ void BluetoothManager::initializeAdapter() { SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))); } - // Create and register Bluetooth pairing agent m_agent = new BluetoothAgent(this); if (!m_agent->registerAgent()) { - // This is expected on desktop/VM environments where BlueZ isn't running + qDebug() << "[BluetoothManager] Pairing agent not registered (expected without BlueZ)"; } - // Forward pairing interaction signals from agent to UI connect(m_agent, &BluetoothAgent::pairingPinCodeRequested, this, [this](const QString &devicePath, const QString &deviceName) { QString address = devicePath.section('/', -1); @@ -238,14 +225,11 @@ void BluetoothManager::initializeAdapter() { emit passkeyConfirmation(address, deviceName, passkey); }); - // Initialize scan timer after adapter is found m_scanTimer = new QTimer(this); - m_scanTimer->setInterval(30000); // Stop scan after 30s + m_scanTimer->setInterval(30000); m_scanTimer->setSingleShot(true); connect(m_scanTimer, &QTimer::timeout, this, &BluetoothManager::stopScan); - // While scanning, periodically refresh managed objects. - // Some BlueZ setups don't emit InterfacesAdded for all discoveries, but properties update. m_deviceRefreshTimer = new QTimer(this); m_deviceRefreshTimer->setInterval(2000); connect(m_deviceRefreshTimer, &QTimer::timeout, this, @@ -253,10 +237,7 @@ void BluetoothManager::initializeAdapter() { }); } -void BluetoothManager::connectToBlueZ() { - // This method is now empty as its content has been moved to initializeAdapter - // to ensure signal connections are made after the adapter is found. -} +void BluetoothManager::connectToBlueZ() {} void BluetoothManager::updateAdapterProperties() { if (!m_adapter) @@ -304,16 +285,13 @@ void BluetoothManager::updateAdapterProperties() { void BluetoothManager::refreshDevices() { QDBusInterface manager("org.bluez", "/", "org.freedesktop.DBus.ObjectManager", m_bus); - // GetManagedObjects returns a deeply nested structure: - // a{oa{sa{sv}}} - // Use the registered ManagedObjectMap type (see bluetoothmanager.h). + QDBusReply reply = manager.call("GetManagedObjects"); if (!reply.isValid()) { return; } - // Update properties on already-known devices (RSSI/connected/paired can change during discovery). for (QObject *obj : m_devices) { if (auto *dev = qobject_cast(obj)) { dev->updateProperties(); @@ -323,7 +301,7 @@ void BluetoothManager::refreshDevices() { const ManagedObjectMap objects = reply.value(); for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) { const QString path = it.key().path(); - // Only consider devices belonging to our adapter (if known). + if (!m_adapterPath.isEmpty() && !path.startsWith(m_adapterPath)) continue; if (it.value().contains("org.bluez.Device1")) { @@ -343,7 +321,7 @@ void BluetoothManager::setEnabled(bool enabled) { qDebug() << "[BluetoothManager] Setting powered to" << enabled; if (enabled) { - // Ensure RFKILL is unblocked + QProcess::execute("rfkill", {"unblock", "bluetooth"}); } } @@ -370,7 +348,6 @@ void BluetoothManager::startScan() { m_scanTimer->start(); } - // Kick an immediate refresh + keep refreshing while scanning. refreshDevices(); if (m_deviceRefreshTimer) m_deviceRefreshTimer->start(); @@ -397,7 +374,6 @@ void BluetoothManager::pairDevice(const QString &address, const QString &pin) { qDebug() << "[BluetoothManager] Pairing with" << address; - // If PIN provided, we might need to handle it via agent, but usually Pair() handles it QDBusInterface deviceIface("org.bluez", device->path(), "org.bluez.Device1", m_bus); deviceIface.asyncCall("Pair"); } @@ -479,7 +455,6 @@ void BluetoothManager::onDeviceAdded(const QDBusObjectPath &objectPath, const InterfaceMap &interfaces) { QString path = objectPath.path(); - // Check if it's a device interface if (interfaces.contains("org.bluez.Device1")) { qDebug() << "[BluetoothManager] Device added signal:" << path; addDevice(path); @@ -547,7 +522,7 @@ BluetoothDevice *BluetoothManager::findDeviceByAddress(const QString &address) { void BluetoothManager::addDevice(const QString &path) { if (findDeviceByPath(path)) { - return; // Already exists + return; } BluetoothDevice *device = new BluetoothDevice(path, this); diff --git a/shell/src/bluetoothmanager.h b/shell/src/managers/bluetoothmanager.h similarity index 98% rename from shell/src/bluetoothmanager.h rename to shell/src/managers/bluetoothmanager.h index f204ec13f..03f7b64ed 100644 --- a/shell/src/bluetoothmanager.h +++ b/shell/src/managers/bluetoothmanager.h @@ -13,7 +13,6 @@ #include #include -// Define types for GetManagedObjects return value typedef QMap PropertyMap; typedef QMap InterfaceMap; typedef QMap ManagedObjectMap; @@ -146,7 +145,7 @@ class BluetoothManager : public QObject { void availableChanged(); void pairingFailed(const QString &address, const QString &error); void pairingSucceeded(const QString &address); - // Pairing interaction signals (forwarded from agent) + void pinRequested(const QString &address, const QString &deviceName); void passkeyRequested(const QString &address, const QString &deviceName); void passkeyConfirmation(const QString &address, const QString &deviceName, quint32 passkey); diff --git a/shell/src/managers/clipboardmanagercpp.cpp b/shell/src/managers/clipboardmanagercpp.cpp new file mode 100644 index 000000000..c4986ff63 --- /dev/null +++ b/shell/src/managers/clipboardmanagercpp.cpp @@ -0,0 +1,106 @@ +#include "clipboardmanagercpp.h" +#include "settingsmanager.h" + +#include +#include +#include +#include + +ClipboardManagerCpp::ClipboardManagerCpp(SettingsManager *settings, QObject *parent) + : QObject(parent) + , m_settings(settings) { + load(); +} + +QVariantList ClipboardManagerCpp::getHistory() const { + return m_history; +} + +void ClipboardManagerCpp::copyToClipboard(const QString &text, const QString &type) { + addToHistory(text, type); +} + +void ClipboardManagerCpp::deleteItem(int index) { + if (index < 0 || index >= m_history.size()) + return; + + m_history.removeAt(index); + save(); + emit historyChanged(); +} + +void ClipboardManagerCpp::clearHistory() { + if (m_history.isEmpty()) + return; + + m_history.clear(); + save(); + emit historyChanged(); + emit historyCleared(); +} + +void ClipboardManagerCpp::load() { + if (!m_settings) { + m_history.clear(); + return; + } + + const QVariant raw = m_settings->get("clipboard/history", QVariantList()); + m_history = parseHistory(raw); +} + +void ClipboardManagerCpp::save() { + if (!m_settings) + return; + + m_settings->set("clipboard/history", m_history); + m_settings->sync(); +} + +void ClipboardManagerCpp::addToHistory(const QString &text, const QString &type) { + if (text.isEmpty()) + return; + + for (int i = 0; i < m_history.size(); ++i) { + const QVariantMap item = m_history[i].toMap(); + if (item.value("text").toString() == text) { + m_history.removeAt(i); + break; + } + } + + QVariantMap item; + item["text"] = text; + item["type"] = type; + item["timestamp"] = QDateTime::currentMSecsSinceEpoch(); + m_history.prepend(item); + + if (m_history.size() > m_maxHistorySize) + m_history = m_history.mid(0, m_maxHistorySize); + + save(); + emit historyChanged(); +} + +QVariantList ClipboardManagerCpp::parseHistory(const QVariant &raw) const { + if (raw.canConvert()) + return raw.toList(); + + if (raw.typeId() == QMetaType::QString) { + const QJsonDocument doc = QJsonDocument::fromJson(raw.toString().toUtf8()); + if (!doc.isArray()) + return {}; + + QVariantList list; + const QJsonArray arr = doc.array(); + list.reserve(arr.size()); + for (const QJsonValue &value : arr) { + if (!value.isObject()) + continue; + list.append(value.toObject().toVariantMap()); + } + return list; + } + + return {}; +} diff --git a/shell/src/managers/clipboardmanagercpp.h b/shell/src/managers/clipboardmanagercpp.h new file mode 100644 index 000000000..6483e9b9e --- /dev/null +++ b/shell/src/managers/clipboardmanagercpp.h @@ -0,0 +1,44 @@ +#ifndef CLIPBOARDMANAGERCPP_H +#define CLIPBOARDMANAGERCPP_H + +#include +#include + +class SettingsManager; + +class ClipboardManagerCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged) + Q_PROPERTY(int maxHistorySize READ maxHistorySize CONSTANT) + + public: + explicit ClipboardManagerCpp(SettingsManager *settings, QObject *parent = nullptr); + + QVariantList history() const { + return m_history; + } + int maxHistorySize() const { + return m_maxHistorySize; + } + + Q_INVOKABLE QVariantList getHistory() const; + Q_INVOKABLE void copyToClipboard(const QString &text, const QString &type = "text"); + Q_INVOKABLE void deleteItem(int index); + Q_INVOKABLE void clearHistory(); + + signals: + void historyChanged(); + void historyCleared(); + + private: + void load(); + void save(); + void addToHistory(const QString &text, const QString &type); + QVariantList parseHistory(const QVariant &raw) const; + + SettingsManager *m_settings = nullptr; + QVariantList m_history; + int m_maxHistorySize = 20; +}; + +#endif diff --git a/shell/src/cursormanager.cpp b/shell/src/managers/cursormanager.cpp similarity index 85% rename from shell/src/cursormanager.cpp rename to shell/src/managers/cursormanager.cpp index 3af0b370f..b468eef3a 100644 --- a/shell/src/cursormanager.cpp +++ b/shell/src/managers/cursormanager.cpp @@ -6,7 +6,7 @@ CursorManager::CursorManager(QObject *parent) : QObject(parent) , m_cursorVisible(true) - , m_hideDelayMs(2000) // 2 seconds default + , m_hideDelayMs(2000) , m_hideTimer(new QTimer(this)) , m_overrideActive(false) { @@ -17,7 +17,7 @@ CursorManager::CursorManager(QObject *parent) } CursorManager::~CursorManager() { - // Restore cursor if we hid it + if (m_overrideActive) { QGuiApplication::restoreOverrideCursor(); } @@ -30,13 +30,13 @@ void CursorManager::setCursorVisible(bool visible) { m_cursorVisible = visible; if (visible) { - // Show cursor - restore the override if active + if (m_overrideActive) { QGuiApplication::restoreOverrideCursor(); m_overrideActive = false; } } else { - // Hide cursor - push blank cursor override + if (!m_overrideActive) { QGuiApplication::setOverrideCursor(Qt::BlankCursor); m_overrideActive = true; @@ -55,12 +55,11 @@ void CursorManager::setHideDelayMs(int ms) { } void CursorManager::onMouseActivity() { - // Show cursor + if (!m_cursorVisible) { setCursorVisible(true); } - // Restart hide timer m_hideTimer->start(m_hideDelayMs); } diff --git a/shell/src/cursormanager.h b/shell/src/managers/cursormanager.h similarity index 75% rename from shell/src/cursormanager.h rename to shell/src/managers/cursormanager.h index d83bddfb9..5ef79d841 100644 --- a/shell/src/cursormanager.h +++ b/shell/src/managers/cursormanager.h @@ -3,12 +3,6 @@ #include #include -/** - * @brief CursorManager - Controls global cursor visibility for EGLFS - * - * QML MouseArea cursorShape doesn't work reliably with EGLFS hardware cursor. - * This class uses QGuiApplication::setOverrideCursor() for reliable control. - */ class CursorManager : public QObject { Q_OBJECT Q_PROPERTY( @@ -29,7 +23,6 @@ class CursorManager : public QObject { Q_INVOKABLE void setCursorVisible(bool visible); Q_INVOKABLE void setHideDelayMs(int ms); - // Call this when mouse moves to show cursor and reset hide timer Q_INVOKABLE void onMouseActivity(); signals: diff --git a/shell/src/displaymanagercpp.cpp b/shell/src/managers/displaymanagercpp.cpp similarity index 84% rename from shell/src/displaymanagercpp.cpp rename to shell/src/managers/displaymanagercpp.cpp index 5fa931151..b4f467baa 100644 --- a/shell/src/displaymanagercpp.cpp +++ b/shell/src/managers/displaymanagercpp.cpp @@ -24,10 +24,10 @@ DisplayManagerCpp::DisplayManagerCpp(PowerManagerCpp *powerManager, , m_maxBrightness(100) , m_autoBrightnessEnabled(false) , m_rotationLocked(false) - , m_screenTimeout(300) // 5 minutes default + , m_screenTimeout(300) , m_brightness(0.5) , m_nightLightEnabled(false) - , m_nightLightTemperature(3400) // Warm default (between 2700-6500K) + , m_nightLightTemperature(3400) , m_nightLightSchedule("off") , m_powerManager(powerManager) , m_rotationManager(rotationManager) { @@ -39,7 +39,6 @@ DisplayManagerCpp::DisplayManagerCpp(PowerManagerCpp *powerManager, qInfo() << "[DisplayManagerCpp] Backlight control available:" << m_backlightDevice; m_brightness = getBrightness(); - // Monitor brightness changes from hardware keys via D-Bus setupBrightnessMonitoring(); } else { qInfo() << "[DisplayManagerCpp] No backlight devices found"; @@ -62,7 +61,6 @@ bool DisplayManagerCpp::detectBacklightDevice() { return false; } - // Prioritize apple-panel-bl (Apple Silicon) or intel_backlight if (devices.contains("apple-panel-bl")) { m_backlightDevice = "apple-panel-bl"; } else if (devices.contains("intel_backlight")) { @@ -89,7 +87,7 @@ bool DisplayManagerCpp::detectBacklightDevice() { double DisplayManagerCpp::getBrightness() { if (!m_available) { - return 0.5; // Default 50% + return 0.5; } QString brightnessPath = QString("/sys/class/backlight/%1/brightness").arg(m_backlightDevice); @@ -100,7 +98,6 @@ double DisplayManagerCpp::getBrightness() { int currentValue = value.toInt(); file.close(); - // Convert to 0.0-1.0 range double brightness = static_cast(currentValue) / m_maxBrightness; qDebug() << "[DisplayManagerCpp] Current brightness:" << currentValue << "/" << m_maxBrightness << "=" << (brightness * 100) << "%"; @@ -108,7 +105,7 @@ double DisplayManagerCpp::getBrightness() { } qWarning() << "[DisplayManagerCpp] Failed to read brightness"; - return 0.5; // Default fallback + return 0.5; } void DisplayManagerCpp::setBrightness(double brightness) { @@ -117,14 +114,8 @@ void DisplayManagerCpp::setBrightness(double brightness) { return; } - // Clamp brightness to 0.0-1.0 brightness = qBound(0.0, brightness, 1.0); - // Force update even if value seems same, to ensure hardware sync - // if (qAbs(m_brightness - brightness) < 0.01) { - // return; - // } - qInfo() << "[DisplayManagerCpp] Setting brightness to" << brightness << " (current internal:" << m_brightness << ")"; @@ -134,8 +125,6 @@ void DisplayManagerCpp::setBrightness(double brightness) { QString brightnessPath = QString("/sys/class/backlight/%1/brightness").arg(m_backlightDevice); - // 1. Try GNOME Settings Daemon (GSD) via D-Bus - // This is the preferred method when GSD is running (as indicated by logs) QDBusMessage message = QDBusMessage::createMethodCall("org.gnome.SettingsDaemon.Power", "/org/gnome/SettingsDaemon/Power", "org.freedesktop.DBus.Properties", "Set"); @@ -143,7 +132,7 @@ void DisplayManagerCpp::setBrightness(double brightness) { QList args; args << "org.gnome.SettingsDaemon.Power.Screen"; args << "Brightness"; - // GSD expects integer percentage (0-100), NOT the raw hardware value + int gsdValue = static_cast(brightness * 100.0); args << QVariant::fromValue(QDBusVariant(gsdValue)); message.setArguments(args); @@ -157,17 +146,15 @@ void DisplayManagerCpp::setBrightness(double brightness) { qDebug() << "[DisplayManagerCpp] GSD D-Bus call failed:" << reply.errorMessage(); } - // 2. Try using systemd-logind SetBrightness method (Standard for Droidian/pmOS/Linux) - // This works without root if the session is active if (Platform::hasLogind()) { QDBusMessage logindMsg = QDBusMessage::createMethodCall( "org.freedesktop.login1", "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session", "SetBrightness"); QList logindArgs; - logindArgs << "backlight"; // Subsystem - logindArgs << m_backlightDevice; // Device name - logindArgs << (uint)brightnessValue; // Brightness value (uint32) + logindArgs << "backlight"; + logindArgs << m_backlightDevice; + logindArgs << (uint)brightnessValue; logindMsg.setArguments(logindArgs); QDBusMessage logindReply = QDBusConnection::systemBus().call(logindMsg); @@ -180,7 +167,6 @@ void DisplayManagerCpp::setBrightness(double brightness) { } } - // 3. Fallback: Try direct sysfs write (requires permissions/udev rules) QFile file(brightnessPath); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream stream(&file); @@ -205,9 +191,6 @@ void DisplayManagerCpp::setAutoBrightness(bool enabled) { saveSettings(); qInfo() << "[DisplayManagerCpp] Auto-brightness" << (enabled ? "enabled" : "disabled"); - - // TODO: Implement actual auto-brightness using ambient light sensor - // For now, this just tracks the preference } void DisplayManagerCpp::setRotationLock(bool locked) { @@ -262,14 +245,12 @@ QString DisplayManagerCpp::screenTimeoutString() const { } void DisplayManagerCpp::loadSettings() { - // Load from SettingsManager or QSettings - // For now, use defaults - will be integrated with SettingsManager later + qDebug() << "[DisplayManagerCpp] Settings loaded"; } void DisplayManagerCpp::saveSettings() { - // Save to SettingsManager or QSettings - // For now, just log - will be integrated with SettingsManager later + qDebug() << "[DisplayManagerCpp] Settings saved"; } @@ -284,13 +265,10 @@ void DisplayManagerCpp::setNightLightEnabled(bool enabled) { qInfo() << "[DisplayManagerCpp] Night Light" << (enabled ? "enabled" : "disabled") << "at" << m_nightLightTemperature << "K"; - - // TODO: Apply color temperature filter - // This would require compositor-level color correction or QML shader } void DisplayManagerCpp::setNightLightTemperature(int temperature) { - // Clamp to valid range (2700K = very warm, 6500K = daylight) + temperature = qBound(2700, temperature, 6500); if (m_nightLightTemperature == temperature) { @@ -302,8 +280,6 @@ void DisplayManagerCpp::setNightLightTemperature(int temperature) { saveSettings(); qInfo() << "[DisplayManagerCpp] Night Light temperature set to" << temperature << "K"; - - // TODO: Apply new color temperature if enabled } void DisplayManagerCpp::setNightLightSchedule(const QString &schedule) { @@ -316,8 +292,6 @@ void DisplayManagerCpp::setNightLightSchedule(const QString &schedule) { saveSettings(); qInfo() << "[DisplayManagerCpp] Night Light schedule:" << schedule; - - // TODO: Implement schedule logic (sunset/sunrise based on location, custom times) } void DisplayManagerCpp::setScreenState(bool on) { @@ -339,7 +313,6 @@ void DisplayManagerCpp::setScreenState(bool on) { qDebug() << "[DisplayManagerCpp] Screen power control disabled (Qt GuiPrivate not enabled)"; #endif - // Manage display wakelock based on screen state if (m_powerManager) { if (on) { m_powerManager->acquireWakelock("display"); @@ -352,7 +325,7 @@ void DisplayManagerCpp::setScreenState(bool on) { } void DisplayManagerCpp::setupBrightnessMonitoring() { - // 1. Monitor sysfs via QFileSystemWatcher (already implemented) + if (!m_backlightDevice.isEmpty()) { QString actualPath = QString("/sys/class/backlight/%1/actual_brightness").arg(m_backlightDevice); @@ -367,8 +340,6 @@ void DisplayManagerCpp::setupBrightnessMonitoring() { qInfo() << "[DisplayManagerCpp] Monitoring sysfs paths:" << actualPath << "and" << reqPath; } - // 2. Monitor GNOME Settings Daemon (GSD) - // GSD handles hardware keys and updates brightness. We listen for its property changes. QDBusConnection bus = QDBusConnection::sessionBus(); bool connected = bus.connect("org.gnome.SettingsDaemon.Power", "/org/gnome/SettingsDaemon/Power", @@ -383,12 +354,10 @@ void DisplayManagerCpp::setupBrightnessMonitoring() { } void DisplayManagerCpp::onExternalBrightnessChanged() { - // Re-using the existing logic but ensuring we read the file if (m_backlightDevice.isEmpty()) return; - // Poll actual_brightness (hardware state) QString actualPath = QString("/sys/class/backlight/%1/actual_brightness").arg(m_backlightDevice); QFile actualFile(actualPath); @@ -400,7 +369,6 @@ void DisplayManagerCpp::onExternalBrightnessChanged() { actualFile.close(); } - // Poll brightness (requested state) QString reqPath = QString("/sys/class/backlight/%1/brightness").arg(m_backlightDevice); QFile reqFile(reqPath); int reqVal = -1; @@ -445,8 +413,8 @@ void DisplayManagerCpp::onDBusPropertiesChanged(const QString &interface, if (interface == "org.gnome.SettingsDaemon.Power.Screen") { if (changed.contains("Brightness")) { - int gsdBrightness = changed["Brightness"].toInt(); - // GSD usually reports 0-100 + int gsdBrightness = changed["Brightness"].toInt(); + double newBrightness = gsdBrightness / 100.0; if (qAbs(newBrightness - m_brightness) > 0.01) { @@ -458,6 +426,4 @@ void DisplayManagerCpp::onDBusPropertiesChanged(const QString &interface, } } -void DisplayManagerCpp::pollBrightness() { - // DEPRECATED - Replaced by QFileSystemWatcher -} +void DisplayManagerCpp::pollBrightness() {} diff --git a/shell/src/displaymanagercpp.h b/shell/src/managers/displaymanagercpp.h similarity index 87% rename from shell/src/displaymanagercpp.h rename to shell/src/managers/displaymanagercpp.h index 1e037b82a..cb7cbc495 100644 --- a/shell/src/displaymanagercpp.h +++ b/shell/src/managers/displaymanagercpp.h @@ -56,7 +56,7 @@ class DisplayManagerCpp : public QObject { } void setBrightness(double brightness); - Q_INVOKABLE double getBrightness(); // Read current brightness + Q_INVOKABLE double getBrightness(); Q_INVOKABLE void setScreenState(bool on); Q_INVOKABLE void setAutoBrightness(bool enabled); Q_INVOKABLE void setRotationLock(bool locked); @@ -82,19 +82,19 @@ class DisplayManagerCpp : public QObject { int m_maxBrightness; bool m_autoBrightnessEnabled; bool m_rotationLocked; - int m_screenTimeout; // in seconds + int m_screenTimeout; double m_brightness; bool m_nightLightEnabled; - int m_nightLightTemperature; // 2700K (warm) to 6500K (cool) - QString m_nightLightSchedule; // "off", "manual", "sunset", "custom" - PowerManagerCpp *m_powerManager; // For wakelock management - RotationManager *m_rotationManager; // For rotation lock + int m_nightLightTemperature; + QString m_nightLightSchedule; + PowerManagerCpp *m_powerManager; + RotationManager *m_rotationManager; bool detectBacklightDevice(); void loadSettings(); void saveSettings(); void setupBrightnessMonitoring(); - void pollBrightness(); // Fallback method + void pollBrightness(); private slots: void onExternalBrightnessChanged(); @@ -102,4 +102,4 @@ class DisplayManagerCpp : public QObject { const QStringList &invalidated); }; -#endif // DISPLAYMANAGERCPP_H +#endif diff --git a/shell/src/flashlightmanagercpp.cpp b/shell/src/managers/flashlightmanagercpp.cpp similarity index 93% rename from shell/src/flashlightmanagercpp.cpp rename to shell/src/managers/flashlightmanagercpp.cpp index 845b70619..92a0ff066 100644 --- a/shell/src/flashlightmanagercpp.cpp +++ b/shell/src/managers/flashlightmanagercpp.cpp @@ -7,7 +7,7 @@ static bool isCandidateLedName(const QString &name) { const QString n = name.toLower(); - // Common naming conventions on mobile Linux devices. + return n.contains("torch") || n.contains("flash") || n.contains("flashlight") || n.contains("camera") && n.contains("led"); } @@ -29,7 +29,7 @@ void FlashlightManagerCpp::refresh() { m_available = discoverLed(); if (!m_available) { - // If hardware vanished, force-off. + if (m_enabled) { m_enabled = false; emit enabledChanged(); @@ -54,8 +54,7 @@ bool FlashlightManagerCpp::discoverLed() { const QStringList entries = ledsDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); - // Prefer obvious candidates first. - QString best; + QString best; for (const QString &e : entries) { if (isCandidateLedName(e)) { best = e; @@ -63,7 +62,7 @@ bool FlashlightManagerCpp::discoverLed() { } } if (best.isEmpty() && !entries.isEmpty()) { - // No obvious candidates; do not guess in production (avoid writing to random LEDs). + m_ledPath.clear(); m_maxBrightness = 255; return false; @@ -125,7 +124,7 @@ void FlashlightManagerCpp::setEnabled(bool enabled) { return; } } else { - // Stop pulse timer and turn off. + if (m_pulseTimer->isActive()) m_pulseTimer->stop(); (void)writeBrightness(0); diff --git a/shell/src/flashlightmanagercpp.h b/shell/src/managers/flashlightmanagercpp.h similarity index 88% rename from shell/src/flashlightmanagercpp.h rename to shell/src/managers/flashlightmanagercpp.h index a110727cb..2444a9a07 100644 --- a/shell/src/flashlightmanagercpp.h +++ b/shell/src/managers/flashlightmanagercpp.h @@ -5,8 +5,6 @@ class QTimer; -// Controls a flashlight/torch LED via sysfs (/sys/class/leds/*). -// Backend lives in C++ (perf + permissions + IO), QML should be a thin wrapper. class FlashlightManagerCpp : public QObject { Q_OBJECT Q_PROPERTY(bool available READ available NOTIFY availableChanged) @@ -58,7 +56,7 @@ class FlashlightManagerCpp : public QObject { bool m_enabled = false; int m_brightness = 255; int m_maxBrightness = 255; - QString m_ledPath; // absolute path to LED dir in /sys/class/leds/ + QString m_ledPath; QTimer *m_pulseTimer = nullptr; }; diff --git a/shell/src/hapticmanager.cpp b/shell/src/managers/hapticmanager.cpp similarity index 86% rename from shell/src/hapticmanager.cpp rename to shell/src/managers/hapticmanager.cpp index 5900bdbad..f04a9cd17 100644 --- a/shell/src/hapticmanager.cpp +++ b/shell/src/managers/hapticmanager.cpp @@ -32,7 +32,7 @@ HapticManager::HapticManager(QObject *parent) } bool HapticManager::detectVibrator() { - // Common vibrator paths + QStringList paths = {"/sys/class/leds/vibrator/brightness", "/sys/class/leds/vibrator/activate", "/sys/class/timed_output/vibrator/enable", "/sys/devices/virtual/timed_output/vibrator/enable"}; @@ -66,13 +66,36 @@ void HapticManager::vibrate(int duration) { qDebug() << "[HapticManager] Vibrating for" << duration << "ms"; - // Activate vibrator writeVibrator(duration); - // Auto-stop after duration (safety fallback) QTimer::singleShot(duration + 100, this, &HapticManager::cancelVibration); } +void HapticManager::light() { + vibrate(10); +} + +void HapticManager::medium() { + vibrate(25); +} + +void HapticManager::heavy() { + vibrate(50); +} + +void HapticManager::pattern(const QVariantList &durations) { + vibratePatternVariant(durations); +} + +void HapticManager::vibratePattern(const QVariantList &durations, int repeat) { + Q_UNUSED(repeat); + vibratePatternVariant(durations); +} + +void HapticManager::stopVibration() { + cancelVibration(); +} + void HapticManager::vibratePattern(const QList &pattern) { if (!m_available || !m_enabled || pattern.isEmpty()) { return; @@ -85,17 +108,16 @@ void HapticManager::vibratePattern(const QList &pattern) { int duration = pattern[i]; if (i % 2 == 0) { - // Even indices: vibrate + QTimer::singleShot(totalTime, this, [this, duration]() { writeVibrator(duration); }); } else { - // Odd indices: pause + QTimer::singleShot(totalTime, this, &HapticManager::cancelVibration); } totalTime += duration; } - // Ensure stopped at end QTimer::singleShot(totalTime + 50, this, &HapticManager::cancelVibration); } diff --git a/shell/src/hapticmanager.h b/shell/src/managers/hapticmanager.h similarity index 78% rename from shell/src/hapticmanager.h rename to shell/src/managers/hapticmanager.h index 466cdd61e..a0e2734f5 100644 --- a/shell/src/hapticmanager.h +++ b/shell/src/managers/hapticmanager.h @@ -25,6 +25,12 @@ class HapticManager : public QObject { Q_INVOKABLE void vibratePattern(const QList &pattern); Q_INVOKABLE void cancelVibration(); Q_INVOKABLE void vibratePatternVariant(const QVariantList &pattern); + Q_INVOKABLE void light(); + Q_INVOKABLE void medium(); + Q_INVOKABLE void heavy(); + Q_INVOKABLE void pattern(const QVariantList &durations); + Q_INVOKABLE void vibratePattern(const QVariantList &durations, int repeat = 0); + Q_INVOKABLE void stopVibration(); signals: void availableChanged(); @@ -39,4 +45,4 @@ class HapticManager : public QObject { QString m_vibratorPath; }; -#endif // HAPTICMANAGER_H +#endif diff --git a/shell/src/locationmanager.cpp b/shell/src/managers/locationmanager.cpp similarity index 92% rename from shell/src/locationmanager.cpp rename to shell/src/managers/locationmanager.cpp index a4703cbcb..d75d9d363 100644 --- a/shell/src/locationmanager.cpp +++ b/shell/src/managers/locationmanager.cpp @@ -34,7 +34,6 @@ LocationManager::LocationManager(QObject *parent) , m_timestamp(0) { qDebug() << "[LocationManager] Initializing"; - // Register custom D-Bus type for GeoClue2 Timestamp qRegisterMetaType("GeoClueTimestamp"); qDBusRegisterMetaType(); @@ -81,7 +80,6 @@ void LocationManager::createClient() { qDebug() << "[LocationManager] Creating client asynchronously..."; - // Create client asynchronously QDBusPendingCall asyncCall = m_manager->asyncCall("GetClient"); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(asyncCall, this); @@ -103,13 +101,11 @@ void LocationManager::createClient() { if (!m_client->isValid()) { qWarning() << "[LocationManager] Client interface invalid"; } else { - // Set desktop ID + m_client->setProperty("DesktopId", "org.marathon.Shell"); - // Request high accuracy - m_client->setProperty("RequestedAccuracyLevel", 8); // EXACT (GPS) + m_client->setProperty("RequestedAccuracyLevel", 8); - // Monitor location updates QDBusConnection::systemBus().connect( "org.freedesktop.GeoClue2", m_clientPath, "org.freedesktop.GeoClue2.Client", "LocationUpdated", this, @@ -117,7 +113,6 @@ void LocationManager::createClient() { qInfo() << "[LocationManager] Client configured and ready"; - // Auto-start if requested or implied start(); } } @@ -157,7 +152,6 @@ void LocationManager::stop() { qInfo() << "[LocationManager] Stopping location updates"; - // We don't strictly need to wait for Stop to finish, but using asyncCall prevents blocking m_client->asyncCall("Stop"); m_active = false; emit activeChanged(); @@ -184,7 +178,6 @@ void LocationManager::updateLocation(const QString &locationPath) { return; } - // Get location properties m_latitude = location.property("Latitude").toDouble(); m_longitude = location.property("Longitude").toDouble(); m_accuracy = location.property("Accuracy").toDouble(); @@ -192,8 +185,6 @@ void LocationManager::updateLocation(const QString &locationPath) { m_speed = location.property("Speed").toDouble(); m_heading = location.property("Heading").toDouble(); - // Timestamp is (seconds, microseconds) - (tt) - // We use the Properties interface directly to avoid QDBusAbstractInterface property() issues with custom types QDBusInterface props("org.freedesktop.GeoClue2", location.path(), "org.freedesktop.DBus.Properties", QDBusConnection::systemBus()); diff --git a/shell/src/locationmanager.h b/shell/src/managers/locationmanager.h similarity index 96% rename from shell/src/locationmanager.h rename to shell/src/managers/locationmanager.h index cbce2324e..6e10d799e 100644 --- a/shell/src/locationmanager.h +++ b/shell/src/managers/locationmanager.h @@ -8,7 +8,6 @@ #include #include -// GeoClue2 Timestamp is (tt) - seconds and microseconds struct GeoClueTimestamp { qulonglong seconds; qulonglong microseconds; @@ -93,4 +92,4 @@ class LocationManager : public QObject { qint64 m_timestamp; }; -#endif // LOCATIONMANAGER_H +#endif diff --git a/shell/src/marathoninputmethodengine.cpp b/shell/src/managers/marathoninputmethodengine.cpp similarity index 97% rename from shell/src/marathoninputmethodengine.cpp rename to shell/src/managers/marathoninputmethodengine.cpp index c81a1f81d..1c4a971ab 100644 --- a/shell/src/marathoninputmethodengine.cpp +++ b/shell/src/managers/marathoninputmethodengine.cpp @@ -131,7 +131,6 @@ void MarathonInputMethodEngine::commitText(const QString &text) { int key = charToKey(ch); Qt::KeyboardModifiers mods = Qt::NoModifier; - // Add shift modifier for uppercase letters if (ch.isLetter() && ch.isUpper()) { mods |= Qt::ShiftModifier; } @@ -257,7 +256,7 @@ void MarathonInputMethodEngine::onCursorRectangleChanged() { bool MarathonInputMethodEngine::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::FocusIn) { - // We only care if the object is enabled for input events + QInputMethodQueryEvent query(Qt::ImEnabled | Qt::ImHints); QGuiApplication::sendEvent(obj, &query); @@ -266,7 +265,7 @@ bool MarathonInputMethodEngine::eventFilter(QObject *obj, QEvent *event) { << "HW Keyboard:" << Platform::hasHardwareKeyboard(); if (!Platform::hasHardwareKeyboard()) { - // Force show keyboard for internal QML apps + showKeyboard(true); emit inputItemFocused(); } diff --git a/shell/src/marathoninputmethodengine.h b/shell/src/managers/marathoninputmethodengine.h similarity index 100% rename from shell/src/marathoninputmethodengine.h rename to shell/src/managers/marathoninputmethodengine.h diff --git a/shell/src/modemmanagercpp.cpp b/shell/src/managers/modemmanagercpp.cpp similarity index 87% rename from shell/src/modemmanagercpp.cpp rename to shell/src/managers/modemmanagercpp.cpp index 3074ec137..0c9e30894 100644 --- a/shell/src/modemmanagercpp.cpp +++ b/shell/src/managers/modemmanagercpp.cpp @@ -24,26 +24,22 @@ ModemManagerCpp::ModemManagerCpp(QObject *parent) , m_dataConnected(false) { qDebug() << "[ModemManagerCpp] Initializing"; - // Setup state monitor (will start when D-Bus connects) m_stateMonitor = new QTimer(this); - m_stateMonitor->setInterval(5000); // Poll every 5 seconds + m_stateMonitor->setInterval(5000); connect(m_stateMonitor, &QTimer::timeout, this, &ModemManagerCpp::queryModemState); - // Setup D-Bus retry timer m_dbusRetryTimer = new QTimer(this); m_dbusRetryTimer->setSingleShot(true); connect(m_dbusRetryTimer, &QTimer::timeout, this, &ModemManagerCpp::retryDBusConnection); - // Initial connection attempt initializeDBusConnection(); } void ModemManagerCpp::initializeDBusConnection() { - // Check if system bus is available + if (!QDBusConnection::systemBus().isConnected()) { qWarning() << "[ModemManagerCpp] D-Bus system bus not connected, will retry..."; - // Exponential backoff: 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, then 5s max const int maxRetries = 10; if (m_dbusRetryCount < maxRetries) { int delay = qMin(100 * (1 << m_dbusRetryCount), 5000); @@ -59,14 +55,13 @@ void ModemManagerCpp::initializeDBusConnection() { return; } - // Create ModemManager D-Bus interface m_mmInterface = new QDBusInterface( "org.freedesktop.ModemManager1", "/org/freedesktop/ModemManager1", "org.freedesktop.DBus.ObjectManager", QDBusConnection::systemBus(), this); if (m_mmInterface->isValid()) { m_hasModemManager = true; - m_dbusRetryCount = 0; // Reset retry count on success + m_dbusRetryCount = 0; qInfo() << "[ModemManagerCpp] ✓ Connected to ModemManager D-Bus"; setupDBusConnections(); discoverModem(); @@ -75,7 +70,6 @@ void ModemManagerCpp::initializeDBusConnection() { qDebug() << "[ModemManagerCpp] ModemManager service not available:" << m_mmInterface->lastError().message(); - // Retry with exponential backoff const int maxRetries = 10; if (m_dbusRetryCount < maxRetries) { int delay = qMin(100 * (1 << m_dbusRetryCount), 5000); @@ -97,7 +91,6 @@ void ModemManagerCpp::setupDBusConnections() { if (!m_hasModemManager) return; - // Connect to InterfacesAdded signal for modem hotplug bool connected = QDBusConnection::systemBus().connect( "org.freedesktop.ModemManager1", "/org/freedesktop/ModemManager1", "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", this, SLOT(discoverModem())); @@ -112,7 +105,6 @@ void ModemManagerCpp::discoverModem() { if (!m_hasModemManager) return; - // Get list of modems - use correct DBus type signature typedef QMap InterfaceList; typedef QMap ManagedObjectList; @@ -130,7 +122,6 @@ void ModemManagerCpp::discoverModem() { return; } - // Parse the ObjectManager reply with correct signature: a{oa{sa{sv}}} const QDBusArgument arg = reply.arguments().at(0).value(); ManagedObjectList objects; arg >> objects; @@ -144,7 +135,6 @@ void ModemManagerCpp::discoverModem() { return; } - // Find first modem with Modem interface for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) { QString path = it.key().path(); InterfaceList interfaces = it.value(); @@ -160,7 +150,6 @@ void ModemManagerCpp::discoverModem() { } } - // No valid modem found if (m_modemAvailable) { m_modemAvailable = false; emit modemAvailableChanged(); @@ -172,14 +161,12 @@ void ModemManagerCpp::queryModemState() { if (!m_hasModemManager || !m_modemAvailable || m_modemPath.isEmpty()) return; - // Query Modem interface QDBusInterface modem("org.freedesktop.ModemManager1", m_modemPath, "org.freedesktop.ModemManager1.Modem", QDBusConnection::systemBus()); if (!modem.isValid()) return; - // Get signal strength QDBusInterface modemSignal("org.freedesktop.ModemManager1", m_modemPath, "org.freedesktop.ModemManager1.Modem.Signal", QDBusConnection::systemBus()); @@ -187,9 +174,8 @@ void ModemManagerCpp::queryModemState() { if (modemSignal.isValid()) { QVariant signalVar = modemSignal.property("Rssi"); if (signalVar.isValid()) { - int rssi = signalVar.toInt(); // RSSI in dBm - // Convert RSSI to percentage (rough approximation) - // -50 dBm (excellent) = 100%, -100 dBm (poor) = 0% + int rssi = signalVar.toInt(); + int strength = qBound(0, (rssi + 100) * 2, 100); if (m_signalStrength != strength) { m_signalStrength = strength; @@ -198,7 +184,6 @@ void ModemManagerCpp::queryModemState() { } } - // Get operator name and network type QDBusInterface modem3gpp("org.freedesktop.ModemManager1", m_modemPath, "org.freedesktop.ModemManager1.Modem.Modem3gpp", QDBusConnection::systemBus()); @@ -211,14 +196,13 @@ void ModemManagerCpp::queryModemState() { } uint registrationState = modem3gpp.property("RegistrationState").toUInt(); - bool isRegistered = (registrationState == 1 || registrationState == 5); // HOME or ROAMING + bool isRegistered = (registrationState == 1 || registrationState == 5); if (m_registered != isRegistered) { m_registered = isRegistered; emit registeredChanged(); } } - // Get access technology (network type) uint accessTech = modem.property("AccessTechnologies").toUInt(); QString netType = networkTypeFromAccessTech(accessTech); if (m_networkType != netType) { @@ -228,23 +212,23 @@ void ModemManagerCpp::queryModemState() { } QString ModemManagerCpp::networkTypeFromAccessTech(uint accessTech) { - // ModemManager access technology bitmask + if (accessTech & 0x8000) - return "5G"; // MM_MODEM_ACCESS_TECHNOLOGY_5GNR + return "5G"; if (accessTech & 0x4000) - return "LTE"; // MM_MODEM_ACCESS_TECHNOLOGY_LTE + return "LTE"; if (accessTech & 0x0600) - return "HSPA+"; // HSUPA/HSDPA + return "HSPA+"; if (accessTech & 0x0100) return "HSPA"; if (accessTech & 0x0020) - return "UMTS"; // 3G + return "UMTS"; if (accessTech & 0x0010) - return "EDGE"; // 2.5G + return "EDGE"; if (accessTech & 0x0002) - return "GPRS"; // 2.5G + return "GPRS"; if (accessTech & 0x0001) - return "GSM"; // 2G + return "GSM"; return "Unknown"; } @@ -286,7 +270,6 @@ void ModemManagerCpp::enableData() { return; } - // Connect bearer via ModemManager Simple interface QDBusInterface simpleInterface("org.freedesktop.ModemManager1", m_modemPath, "org.freedesktop.ModemManager1.Modem.Simple", QDBusConnection::systemBus()); @@ -297,9 +280,8 @@ void ModemManagerCpp::enableData() { return; } - // Connect with default APN (empty will use carrier default) QVariantMap properties; - properties["apn"] = ""; // Use carrier default APN + properties["apn"] = ""; QDBusReply reply = simpleInterface.call("Connect", properties); if (!reply.isValid()) { @@ -322,7 +304,6 @@ void ModemManagerCpp::disableData() { return; } - // Disconnect bearer QDBusInterface simpleInterface("org.freedesktop.ModemManager1", m_modemPath, "org.freedesktop.ModemManager1.Modem.Simple", QDBusConnection::systemBus()); @@ -331,7 +312,6 @@ void ModemManagerCpp::disableData() { return; } - // Disconnect all bearers QDBusReply reply = simpleInterface.call("Disconnect", "/"); if (!reply.isValid()) { qWarning() << "[ModemManagerCpp] Failed to disable data:" << reply.error().message(); @@ -353,11 +333,9 @@ void ModemManagerCpp::setApn(const QString &apn, const QString &username, const m_apnUsername = username; m_apnPassword = password; - // If data is currently enabled, reconnect with new APN if (m_dataEnabled) { disableData(); - // Wait a moment for disconnect QTimer::singleShot(500, this, [this, apn, username, password]() { if (!m_hasModemManager || !m_modemAvailable || m_modemPath.isEmpty()) { return; @@ -371,7 +349,6 @@ void ModemManagerCpp::setApn(const QString &apn, const QString &username, const return; } - // Connect with APN settings QVariantMap properties; properties["apn"] = apn; diff --git a/shell/src/modemmanagercpp.h b/shell/src/managers/modemmanagercpp.h similarity index 92% rename from shell/src/modemmanagercpp.h rename to shell/src/managers/modemmanagercpp.h index a070078be..7546f0512 100644 --- a/shell/src/modemmanagercpp.h +++ b/shell/src/managers/modemmanagercpp.h @@ -57,12 +57,11 @@ class ModemManagerCpp : public QObject { return m_dataConnected; } - Q_INVOKABLE void enable(); - Q_INVOKABLE void disable(); - Q_INVOKABLE void enableData(); - Q_INVOKABLE void disableData(); + Q_INVOKABLE void enable(); + Q_INVOKABLE void disable(); + Q_INVOKABLE void enableData(); + Q_INVOKABLE void disableData(); - // APN Configuration Q_INVOKABLE void setApn(const QString &apn, const QString &username = QString(), const QString &password = QString()); Q_INVOKABLE QString getApn() const; @@ -109,10 +108,9 @@ class ModemManagerCpp : public QObject { bool m_dataConnected; QString m_modemPath; - // APN settings cache - QString m_apn; - QString m_apnUsername; - QString m_apnPassword; + QString m_apn; + QString m_apnUsername; + QString m_apnPassword; }; -#endif // MODEMMANAGERCPP_H +#endif diff --git a/shell/src/mpris2controller.cpp b/shell/src/managers/mpris2controller.cpp similarity index 82% rename from shell/src/mpris2controller.cpp rename to shell/src/managers/mpris2controller.cpp index 6fbfb6151..f5d6980f8 100644 --- a/shell/src/mpris2controller.cpp +++ b/shell/src/managers/mpris2controller.cpp @@ -28,21 +28,17 @@ MPRIS2Controller::MPRIS2Controller(QObject *parent) , m_canSeek(false) { qDebug() << "[MPRIS2Controller] Initializing"; - // Setup D-Bus monitoring for new/removed players setupDBusMonitoring(); - // Setup position update timer (1 second intervals) m_positionTimer = new QTimer(this); m_positionTimer->setInterval(1000); connect(m_positionTimer, &QTimer::timeout, this, &MPRIS2Controller::updatePosition); - // Setup player scan timer (every 10 seconds) m_scanTimer = new QTimer(this); m_scanTimer->setInterval(10000); connect(m_scanTimer, &QTimer::timeout, this, &MPRIS2Controller::scanForPlayers); m_scanTimer->start(); - // Initial scan scanForPlayers(); qInfo() << "[MPRIS2Controller] Initialized and monitoring for media players"; @@ -53,7 +49,7 @@ MPRIS2Controller::~MPRIS2Controller() { } void MPRIS2Controller::setupDBusMonitoring() { - // Monitor D-Bus for new media players appearing/disappearing + QDBusConnection::sessionBus().connect("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", this, SLOT(onNameOwnerChanged(QString, QString, QString))); @@ -65,7 +61,6 @@ void MPRIS2Controller::onNameOwnerChanged(const QString &name, const QString &ol const QString &newOwner) { Q_UNUSED(oldOwner); - // NameOwnerChanged is the canonical way to detect MPRIS players coming/going on the bus. if (!name.startsWith("org.mpris.MediaPlayer2.")) return; @@ -78,7 +73,7 @@ void MPRIS2Controller::onNameOwnerChanged(const QString &name, const QString &ol } void MPRIS2Controller::scanForPlayers() { - // Get all service names on session bus + QDBusMessage call = QDBusMessage::createMethodCall( "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); @@ -91,7 +86,6 @@ void MPRIS2Controller::scanForPlayers() { QStringList services = reply.value(); QStringList mprisPlayers; - // Filter for MPRIS2 players for (const QString &service : services) { if (service.startsWith("org.mpris.MediaPlayer2.")) { mprisPlayers.append(service); @@ -106,12 +100,10 @@ void MPRIS2Controller::scanForPlayers() { << "media players:" << mprisPlayers; } - // If we don't have an active player, connect to the first available if (!m_hasActivePlayer && !mprisPlayers.isEmpty()) { connectToPlayer(mprisPlayers.first()); } - // If our current player disappeared, try to find another if (m_hasActivePlayer && !mprisPlayers.contains(m_currentBusName)) { qInfo() << "[MPRIS2Controller] Current player" << m_currentBusName << "disappeared"; disconnectFromPlayer(); @@ -125,10 +117,8 @@ void MPRIS2Controller::scanForPlayers() { void MPRIS2Controller::connectToPlayer(const QString &busName) { qInfo() << "[MPRIS2Controller] Connecting to player:" << busName; - // Disconnect from any existing player disconnectFromPlayer(); - // Create interface to the player m_playerInterface = new QDBusInterface(busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", QDBusConnection::sessionBus(), this); @@ -144,16 +134,14 @@ void MPRIS2Controller::connectToPlayer(const QString &busName) { m_currentBusName = busName; m_hasActivePlayer = true; - // Extract player name from bus name (e.g., "org.mpris.MediaPlayer2.spotify" -> "Spotify") QString name = busName; name.remove("org.mpris.MediaPlayer2."); int dotIndex = name.indexOf('.'); if (dotIndex != -1) { - name = name.left(dotIndex); // Remove instance IDs + name = name.left(dotIndex); } - m_playerName = name.at(0).toUpper() + name.mid(1); // Capitalize + m_playerName = name.at(0).toUpper() + name.mid(1); - // Fetch DesktopEntry (app ID) from root interface QDBusInterface rootInterface(busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2", QDBusConnection::sessionBus()); if (rootInterface.isValid()) { @@ -162,10 +150,8 @@ void MPRIS2Controller::connectToPlayer(const QString &busName) { m_desktopEntry.clear(); } - // DesktopEntry is optional in the MPRIS2 spec and is often empty for some players. - // Ensure we always have a usable, non-empty identifier for launching / lookup. if (m_desktopEntry.isEmpty()) { - // Prefer bus-name suffix (lowercased) without instance IDs. + m_desktopEntry = name.toLower(); } @@ -173,22 +159,17 @@ void MPRIS2Controller::connectToPlayer(const QString &busName) { qInfo() << "[MPRIS2Controller] ✓ Connected to" << m_playerName; - // Connect to property changes (PlaybackStatus/Metadata/Capabilities/etc) - // NOTE: PropertiesChanged has args; wiring it to a no-arg slot silently breaks updates. QDBusConnection::sessionBus().connect( busName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))); - // Initial state fetch updatePlaybackStatus(); updateMetadata(); updateCapabilities(); updatePosition(); - // Start position timer m_positionTimer->start(); - // Poll metadata every 2 seconds (some players don't emit PropertiesChanged for metadata) QTimer *metadataTimer = new QTimer(this); connect(metadataTimer, &QTimer::timeout, this, &MPRIS2Controller::updateMetadata); metadataTimer->start(2000); @@ -198,7 +179,7 @@ void MPRIS2Controller::connectToPlayer(const QString &busName) { void MPRIS2Controller::disconnectFromPlayer() { if (m_playerInterface) { - // Disconnect PropertiesChanged handler for the active player + if (!m_currentBusName.isEmpty()) { QDBusConnection::sessionBus().disconnect( m_currentBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", @@ -227,11 +208,9 @@ void MPRIS2Controller::onPropertiesChanged(const QString &interfaceName, const QStringList &invalidatedProperties) { Q_UNUSED(invalidatedProperties); - // We only care about the Player interface properties for UI state. if (interfaceName != "org.mpris.MediaPlayer2.Player") return; - // Prefer directly consuming the signal payload (fast + avoids flaky remote property reads). if (changedProperties.contains("PlaybackStatus")) { const QString status = changedProperties.value("PlaybackStatus").toString(); if (!status.isEmpty() && status != m_playbackStatus) { @@ -241,7 +220,6 @@ void MPRIS2Controller::onPropertiesChanged(const QString &interfaceName, } } - // Capabilities sometimes arrive in the same PropertiesChanged. if (changedProperties.contains("CanPlay") || changedProperties.contains("CanPause") || changedProperties.contains("CanGoNext") || changedProperties.contains("CanGoPrevious") || changedProperties.contains("CanSeek")) { @@ -257,8 +235,6 @@ void MPRIS2Controller::updatePlaybackStatus() { if (!m_playerInterface) return; - // Use explicit DBus call to Properties.Get (more reliable than QDBusInterface::property() - // for some players / Qt versions). QDBusMessage call = QDBusMessage::createMethodCall(m_currentBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); call << "org.mpris.MediaPlayer2.Player" << "PlaybackStatus"; @@ -268,7 +244,7 @@ void MPRIS2Controller::updatePlaybackStatus() { if (reply.isValid()) { status = reply.value().variant().toString(); } else { - // Fallback to the property getter + status = m_playerInterface->property("PlaybackStatus").toString(); } @@ -283,7 +259,6 @@ void MPRIS2Controller::updateMetadata() { if (!m_playerInterface) return; - // Use explicit DBus call to Properties.Get to handle the a{sv} type correctly QDBusMessage call = QDBusMessage::createMethodCall(m_currentBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); call << "org.mpris.MediaPlayer2.Player" << "Metadata"; @@ -292,20 +267,20 @@ void MPRIS2Controller::updateMetadata() { QVariantMap metadata; if (reply.isValid()) { - // The Get call returns a VARIANT, which contains our a{sv} (QVariantMap) + QVariant innerValue = reply.value().variant(); if (innerValue.canConvert()) { metadata = innerValue.value(); } else if (innerValue.canConvert()) { - // Fallback: manual extraction if automatic conversion fails + QDBusArgument arg = innerValue.value(); if (arg.currentType() == QDBusArgument::MapType) { arg >> metadata; } } } else { - // Try the property getter directly as fallback + QVariant val = m_playerInterface->property("Metadata"); if (val.canConvert()) { metadata = val.value(); @@ -315,8 +290,6 @@ void MPRIS2Controller::updateMetadata() { } } - // QVariantMap metadata = qdbus_cast(metadataVar); // Old broken way - QString title = extractMetadataString(metadata, "xesam:title"); QStringList artists = extractMetadataStringList(metadata, "xesam:artist"); QString artist = artists.isEmpty() ? "" : artists.first(); @@ -362,8 +335,6 @@ void MPRIS2Controller::updatePosition() { return; } - // Position is a *property* (int64, microseconds) on org.mpris.MediaPlayer2.Player. - // Query via org.freedesktop.DBus.Properties.Get for correctness/reliability. QDBusMessage call = QDBusMessage::createMethodCall(m_currentBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); call << "org.mpris.MediaPlayer2.Player" << "Position"; @@ -420,7 +391,7 @@ void MPRIS2Controller::updateCapabilities() { } void MPRIS2Controller::onDBusServiceRegistered(const QString &serviceName) { - // Check if a new MPRIS2 player appeared + if (serviceName.startsWith("org.mpris.MediaPlayer2.")) { qDebug() << "[MPRIS2Controller] New media player detected:" << serviceName; scanForPlayers(); @@ -428,15 +399,13 @@ void MPRIS2Controller::onDBusServiceRegistered(const QString &serviceName) { } void MPRIS2Controller::onDBusServiceUnregistered(const QString &serviceName) { - // Check if an MPRIS2 player disappeared + if (serviceName.startsWith("org.mpris.MediaPlayer2.")) { qDebug() << "[MPRIS2Controller] Media player removed:" << serviceName; scanForPlayers(); } } -// Playback control methods - void MPRIS2Controller::play() { if (!m_playerInterface || !m_canPlay) return; @@ -461,7 +430,7 @@ void MPRIS2Controller::playPause() { qDebug() << "[MPRIS2Controller] Calling PlayPause()"; m_playerInterface->asyncCall("PlayPause"); - // Some players update PlaybackStatus asynchronously; refresh shortly after issuing the command. + QTimer::singleShot(150, this, &MPRIS2Controller::updatePlaybackStatus); } @@ -478,20 +447,16 @@ void MPRIS2Controller::next() { if (!m_playerInterface) return; - // Smart Skip Logic: - // If track is long (> 20 mins) and seekable, skip forward 30 seconds (Podcast mode) - // Otherwise, go to next track (Music mode) - const qint64 podcastThreshold = 20 * 60 * 1000000; // 20 minutes in microseconds + const qint64 podcastThreshold = 20 * 60 * 1000000; if (m_canSeek && m_trackLength > podcastThreshold) { qInfo() << "[MPRIS2Controller] Smart Skip: Long track detected (" << m_trackLength << "us), seeking +30s"; - seek(30000000); // +30 seconds + seek(30000000); } else if (m_canGoNext) { qDebug() << "[MPRIS2Controller] Calling Next()"; m_playerInterface->asyncCall("Next"); - // Metadata will change, so update it QTimer::singleShot(500, this, &MPRIS2Controller::updateMetadata); } } @@ -500,20 +465,16 @@ void MPRIS2Controller::previous() { if (!m_playerInterface) return; - // Smart Skip Logic: - // If track is long (> 20 mins) and seekable, skip back 10 seconds (Podcast mode) - // Otherwise, go to previous track (Music mode) - const qint64 podcastThreshold = 20 * 60 * 1000000; // 20 minutes in microseconds + const qint64 podcastThreshold = 20 * 60 * 1000000; if (m_canSeek && m_trackLength > podcastThreshold) { qInfo() << "[MPRIS2Controller] Smart Skip: Long track detected (" << m_trackLength << "us), seeking -10s"; - seek(-10000000); // -10 seconds + seek(-10000000); } else if (m_canGoPrevious) { qDebug() << "[MPRIS2Controller] Calling Previous()"; m_playerInterface->asyncCall("Previous"); - // Metadata will change, so update it QTimer::singleShot(500, this, &MPRIS2Controller::updateMetadata); } } @@ -533,7 +494,6 @@ void MPRIS2Controller::setPosition(qint64 position) { qDebug() << "[MPRIS2Controller] Setting position to" << position; - // SetPosition requires track ID, get it from metadata QVariant metadataVar = m_playerInterface->property("Metadata"); QVariantMap metadata = qdbus_cast(metadataVar); QDBusObjectPath trackId = metadata.value("mpris:trackid").value(); @@ -551,8 +511,6 @@ void MPRIS2Controller::switchToPlayer(const QString &busName) { } } -// Helper methods for metadata extraction - QString MPRIS2Controller::extractMetadataString(const QVariantMap &metadata, const QString &key) { if (metadata.contains(key)) { return metadata.value(key).toString(); @@ -572,12 +530,10 @@ QStringList MPRIS2Controller::extractMetadataStringList(const QVariantMap &metad if (metadata.contains(key)) { QVariant value = metadata.value(key); - // Try to convert to string list if (value.canConvert()) { return value.toStringList(); } - // Try DBus variant if (value.canConvert()) { QDBusVariant dbusVar = value.value(); QVariant innerValue = dbusVar.variant(); @@ -586,7 +542,6 @@ QStringList MPRIS2Controller::extractMetadataStringList(const QVariantMap &metad } } - // Fallback: single string return QStringList() << value.toString(); } return QStringList(); diff --git a/shell/src/mpris2controller.h b/shell/src/managers/mpris2controller.h similarity index 74% rename from shell/src/mpris2controller.h rename to shell/src/managers/mpris2controller.h index 768e4c13f..7f1f432c9 100644 --- a/shell/src/mpris2controller.h +++ b/shell/src/managers/mpris2controller.h @@ -10,18 +10,9 @@ #include #include -/** - * MPRIS2Controller - Control media players via MPRIS2 D-Bus interface - * - * Monitors all media players (Spotify, VLC, Firefox, Chromium, etc.) - * and provides unified playback control. - * - * MPRIS2 spec: https://specifications.freedesktop.org/mpris-spec/latest/ - */ class MPRIS2Controller : public QObject { Q_OBJECT - // Player state Q_PROPERTY(bool hasActivePlayer READ hasActivePlayer NOTIFY activePlayerChanged) Q_PROPERTY(QString playerName READ playerName NOTIFY activePlayerChanged) Q_PROPERTY(QString playbackStatus READ playbackStatus NOTIFY playbackStatusChanged) @@ -29,7 +20,6 @@ class MPRIS2Controller : public QObject { Q_PROPERTY(bool isPaused READ isPaused NOTIFY playbackStatusChanged) Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY activePlayerChanged) - // Metadata Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY metadataChanged) Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY metadataChanged) Q_PROPERTY(QString trackAlbum READ trackAlbum NOTIFY metadataChanged) @@ -37,7 +27,6 @@ class MPRIS2Controller : public QObject { Q_PROPERTY(qint64 trackLength READ trackLength NOTIFY metadataChanged) Q_PROPERTY(qint64 position READ position NOTIFY positionChanged) - // Capabilities Q_PROPERTY(bool canPlay READ canPlay NOTIFY capabilitiesChanged) Q_PROPERTY(bool canPause READ canPause NOTIFY capabilitiesChanged) Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY capabilitiesChanged) @@ -48,7 +37,6 @@ class MPRIS2Controller : public QObject { explicit MPRIS2Controller(QObject *parent = nullptr); ~MPRIS2Controller(); - // Property getters bool hasActivePlayer() const { return m_hasActivePlayer; } @@ -104,7 +92,7 @@ class MPRIS2Controller : public QObject { } public slots: - // Playback control + void play(); void pause(); void playPause(); @@ -114,7 +102,6 @@ class MPRIS2Controller : public QObject { void seek(qint64 offset); void setPosition(qint64 position); - // Player management void scanForPlayers(); void switchToPlayer(const QString &busName); @@ -138,42 +125,37 @@ class MPRIS2Controller : public QObject { void onDBusServiceUnregistered(const QString &serviceName); private: - void connectToPlayer(const QString &busName); - void disconnectFromPlayer(); - void setupDBusMonitoring(); - QString extractMetadataString(const QVariantMap &metadata, const QString &key); - qint64 extractMetadataInt64(const QVariantMap &metadata, const QString &key); - QStringList extractMetadataStringList(const QVariantMap &metadata, const QString &key); - - // D-Bus interfaces + void connectToPlayer(const QString &busName); + void disconnectFromPlayer(); + void setupDBusMonitoring(); + QString extractMetadataString(const QVariantMap &metadata, const QString &key); + qint64 extractMetadataInt64(const QVariantMap &metadata, const QString &key); + QStringList extractMetadataStringList(const QVariantMap &metadata, const QString &key); + QDBusInterface *m_playerInterface; QTimer *m_positionTimer; QTimer *m_scanTimer; - // Current player state - QString m_currentBusName; - bool m_hasActivePlayer; - QString m_playerName; - QString m_desktopEntry; - QString m_playbackStatus; - - // Metadata - QString m_trackTitle; - QString m_trackArtist; - QString m_trackAlbum; - QString m_albumArtUrl; - qint64 m_trackLength; - qint64 m_position; - - // Capabilities - bool m_canPlay; - bool m_canPause; - bool m_canGoNext; - bool m_canGoPrevious; - bool m_canSeek; - - // Available players - QStringList m_availablePlayers; + QString m_currentBusName; + bool m_hasActivePlayer; + QString m_playerName; + QString m_desktopEntry; + QString m_playbackStatus; + + QString m_trackTitle; + QString m_trackArtist; + QString m_trackAlbum; + QString m_albumArtUrl; + qint64 m_trackLength; + qint64 m_position; + + bool m_canPlay; + bool m_canPause; + bool m_canGoNext; + bool m_canGoPrevious; + bool m_canSeek; + + QStringList m_availablePlayers; }; -#endif // MPRIS2CONTROLLER_H +#endif diff --git a/shell/src/networkmanagercpp.cpp b/shell/src/managers/networkmanagercpp.cpp similarity index 87% rename from shell/src/networkmanagercpp.cpp rename to shell/src/managers/networkmanagercpp.cpp index 6b186068c..e03f5da29 100644 --- a/shell/src/networkmanagercpp.cpp +++ b/shell/src/managers/networkmanagercpp.cpp @@ -12,30 +12,27 @@ #include static QByteArray dbusByteArrayFromVariant(const QVariant &v) { - // NetworkManager exposes SSID as `ay` (byte array). Depending on Qt/DBus, - // it may arrive as QByteArray or as QDBusArgument. + if (v.canConvert()) return v.toByteArray(); if (v.userType() == qMetaTypeId()) { - // First try QtDBus' built-in conversion from `ay` to QByteArray. + { const QByteArray out = qdbus_cast(v); if (!out.isEmpty()) return out; } - // Some builds expose `ay` as QDBusArgument. Try streaming directly to QByteArray. { const QDBusArgument arg = v.value(); QByteArray out; - // For many DBus types, this works (including `ay`). + arg >> out; if (!out.isEmpty()) return out; } - // Last-resort manual decode as an array of bytes. const QDBusArgument arg = v.value(); QByteArray out; arg.beginArray(); @@ -73,7 +70,6 @@ NetworkManagerCpp::NetworkManagerCpp(QObject *parent) , m_hotspotActive(false) { qDebug() << "[NetworkManagerCpp] Initializing"; - // Try to connect to NetworkManager D-Bus m_nmInterface = new QDBusInterface("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", QDBusConnection::systemBus(), this); @@ -82,15 +78,12 @@ NetworkManagerCpp::NetworkManagerCpp(QObject *parent) m_hasNetworkManager = true; qInfo() << "[NetworkManagerCpp] ✓ Connected to NetworkManager D-Bus"; - // Detect hardware availability detectHardwareAvailability(); setupDBusConnections(); queryWifiState(); queryConnectionState(); - // Populate AP list immediately from cached NetworkManager state so UIs don't start empty. - // (RequestScan may be slow or polkit-gated; AccessPoints/GetAllAccessPoints usually has data.) if (!m_wifiDevicePath.isEmpty()) scanAccessPoints(); @@ -100,7 +93,7 @@ NetworkManagerCpp::NetworkManagerCpp(QObject *parent) qInfo() << "[NetworkManagerCpp] NetworkManager D-Bus not available:" << m_nmInterface->lastError().message(); qInfo() << "[NetworkManagerCpp] Using mock mode (no hardware available)"; - // Fallback to simulated mode - no hardware available + m_wifiAvailable = false; m_bluetoothAvailable = false; m_wifiEnabled = false; @@ -109,27 +102,23 @@ NetworkManagerCpp::NetworkManagerCpp(QObject *parent) m_wifiSignalStrength = 0; } - // Setup signal strength monitor m_signalMonitor = new QTimer(this); - m_signalMonitor->setInterval(5000); // Update every 5 seconds + m_signalMonitor->setInterval(5000); connect(m_signalMonitor, &QTimer::timeout, this, &NetworkManagerCpp::updateWifiSignalStrength); m_signalMonitor->start(); - // Setup connection state monitor m_connectionMonitor = new QTimer(this); - m_connectionMonitor->setInterval(3000); // Check every 3 seconds + m_connectionMonitor->setInterval(3000); connect(m_connectionMonitor, &QTimer::timeout, this, &NetworkManagerCpp::queryConnectionState); if (m_hasNetworkManager) { m_connectionMonitor->start(); } - // Setup WiFi scan timer (auto-scan every 30 seconds when WiFi is enabled) m_scanTimer = new QTimer(this); - m_scanTimer->setInterval(30000); // Scan every 30 seconds + m_scanTimer->setInterval(30000); connect(m_scanTimer, &QTimer::timeout, this, &NetworkManagerCpp::scanAccessPoints); if (m_hasNetworkManager && m_wifiAvailable && m_wifiEnabled) { - // Do an immediate access-point refresh so UIs have something to show quickly. - // This reads cached APs (no RequestScan required) and avoids "empty lists" on first launch. + QTimer::singleShot(0, this, &NetworkManagerCpp::scanAccessPoints); m_scanTimer->start(); } @@ -148,7 +137,6 @@ void NetworkManagerCpp::detectHardwareAvailability() { return; } - // Get all devices from NetworkManager QDBusReply> devicesReply = m_nmInterface->call("GetDevices"); if (!devicesReply.isValid()) { qDebug() << "[NetworkManagerCpp] Failed to get devices:" << devicesReply.error().message(); @@ -167,14 +155,13 @@ void NetworkManagerCpp::detectHardwareAvailability() { if (!device.isValid()) continue; - // Device type: 2 = WiFi, 5 = Bluetooth uint deviceType = device.property("DeviceType").toUInt(); - if (deviceType == 2) { // NM_DEVICE_TYPE_WIFI + if (deviceType == 2) { m_wifiAvailable = true; - m_wifiDevicePath = devicePath.path(); // Store for scanning + m_wifiDevicePath = devicePath.path(); qInfo() << "[NetworkManagerCpp] WiFi hardware detected at" << m_wifiDevicePath; - } else if (deviceType == 5) { // NM_DEVICE_TYPE_BT + } else if (deviceType == 5) { m_bluetoothAvailable = true; qInfo() << "[NetworkManagerCpp] Bluetooth hardware detected"; } @@ -182,7 +169,7 @@ void NetworkManagerCpp::detectHardwareAvailability() { if (!m_wifiAvailable) { qInfo() << "[NetworkManagerCpp] No WiFi hardware detected"; - // Disable WiFi state if no hardware + m_wifiEnabled = false; m_wifiConnected = false; emit wifiEnabledChanged(); @@ -200,7 +187,6 @@ void NetworkManagerCpp::setupDBusConnections() { if (!m_hasNetworkManager) return; - // Connect to NetworkManager state changes bool connected = QDBusConnection::systemBus().connect( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", "StateChanged", this, SLOT(queryWifiState())); @@ -217,7 +203,6 @@ void NetworkManagerCpp::queryWifiState() { if (!m_hasNetworkManager) return; - // Query WiFi hardware state QDBusReply wifiState = m_nmInterface->call("GetWifiEnabled"); if (wifiState.isValid()) { bool enabled = wifiState.value() != 0; @@ -245,7 +230,6 @@ void NetworkManagerCpp::queryConnectionState() { if (!m_hasNetworkManager) return; - // Get active connections QVariant activeConnsVar = m_nmInterface->property("ActiveConnections"); QList activeConns = qdbus_cast>(activeConnsVar); @@ -266,18 +250,16 @@ void NetworkManagerCpp::queryConnectionState() { QString type = conn.property("Type").toString(); uint state = conn.property("State").toUInt(); - if (state == 2) { // NM_ACTIVE_CONNECTION_STATE_ACTIVATED + if (state == 2) { if (type == "802-11-wireless") { hasWifi = true; - // Get specific wireless device path for signal strength queries QVariant devicesVar = conn.property("Devices"); QList devices = qdbus_cast>(devicesVar); if (!devices.isEmpty()) { wifiDevicePath = devices.first().path(); } - // Get connection ID (SSID) QString connId = conn.property("Id").toString(); if (!connId.isEmpty()) { wifiSsid = connId; @@ -286,7 +268,6 @@ void NetworkManagerCpp::queryConnectionState() { } else if (type == "802-3-ethernet") { hasEthernet = true; - // Get connection ID (Ethernet connection name) QString connId = conn.property("Id").toString(); if (!connId.isEmpty()) { ethernetName = connId; @@ -295,7 +276,6 @@ void NetworkManagerCpp::queryConnectionState() { } } - // Update WiFi state if (m_wifiConnected != hasWifi) { m_wifiConnected = hasWifi; emit wifiConnectedChanged(); @@ -310,11 +290,10 @@ void NetworkManagerCpp::queryConnectionState() { if (!wifiDevicePath.isEmpty()) { m_activeWifiDevicePath = wifiDevicePath; - // Query signal strength for this connection + updateWifiDetails(); } - // Update Ethernet state if (m_ethernetConnected != hasEthernet) { m_ethernetConnected = hasEthernet; emit ethernetConnectedChanged(); @@ -345,22 +324,21 @@ void NetworkManagerCpp::updateWifiDetails() { if (apPath.path() == "/" || apPath.path().isEmpty()) return; - // Cache the AP path to avoid repeated queries if (m_activeApPath == apPath.path()) { - // If same AP, just query signal strength + QDBusInterface ap("org.freedesktop.NetworkManager", apPath.path(), "org.freedesktop.NetworkManager.AccessPoint", QDBusConnection::systemBus()); if (ap.isValid()) { - uint strength = ap.property("Strength").toUInt(); // 0-100 + uint strength = ap.property("Strength").toUInt(); if (m_wifiSignalStrength != static_cast(strength)) { m_wifiSignalStrength = strength; emit wifiSignalStrengthChanged(); } } } else { - // New AP, query everything + m_activeApPath = apPath.path(); QDBusInterface ap("org.freedesktop.NetworkManager", apPath.path(), @@ -420,7 +398,7 @@ void NetworkManagerCpp::toggleWifi() { void NetworkManagerCpp::scanWifi() { qDebug() << "[NetworkManagerCpp] User requested WiFi scan"; if (m_hasNetworkManager && !m_wifiDevicePath.isEmpty()) { - // Request scan on the WiFi device + QDBusInterface wifiDevice("org.freedesktop.NetworkManager", m_wifiDevicePath, "org.freedesktop.NetworkManager.Device.Wireless", QDBusConnection::systemBus()); @@ -429,13 +407,13 @@ void NetworkManagerCpp::scanWifi() { QDBusReply reply = wifiDevice.call("RequestScan", QVariantMap()); if (reply.isValid()) { qDebug() << "[NetworkManagerCpp] WiFi scan requested successfully"; - // Wait 2 seconds for scan to complete, then get access points + QTimer::singleShot(2000, this, &NetworkManagerCpp::scanAccessPoints); } else { qDebug() << "[NetworkManagerCpp] Failed to request WiFi scan:" << reply.error().message(); emit networkError("Failed to scan for networks"); - // Even if RequestScan is denied, refresh cached AP list so the UI isn't empty. + QTimer::singleShot(250, this, &NetworkManagerCpp::scanAccessPoints); } } @@ -460,9 +438,6 @@ void NetworkManagerCpp::scanAccessPoints() { return; } - // Get list of access points. - // NOTE: The `AccessPoints` property can be stale/empty depending on NM/driver. - // Prefer the method call, fall back to the property. QList accessPoints; { QDBusReply> r = wifiDevice.call("GetAllAccessPoints"); @@ -482,7 +457,6 @@ void NetworkManagerCpp::scanAccessPoints() { processAccessPoint(apPath.path()); } - // Sort by strongest signal, with the currently-connected SSID pinned to the top. std::sort(m_availableNetworks.begin(), m_availableNetworks.end(), [this](const QVariant &a, const QVariant &b) { const QVariantMap am = a.toMap(); @@ -490,7 +464,7 @@ void NetworkManagerCpp::scanAccessPoints() { const bool ac = am.value("connected").toBool(); const bool bc = bm.value("connected").toBool(); if (ac != bc) - return ac; // connected first + return ac; return am.value("strength").toInt() > bm.value("strength").toInt(); }); @@ -507,20 +481,16 @@ void NetworkManagerCpp::processAccessPoint(const QString &apPath) { return; } - // Get SSID (DBus `ay`) const QVariant ssidVar = accessPoint.property("Ssid"); const QByteArray ssidBytes = dbusByteArrayFromVariant(ssidVar); const QString ssid = QString::fromUtf8(ssidBytes); - // Skip hidden networks or invalid SSIDs if (ssid.isEmpty()) { return; } - // Get signal strength (0-100) - uint strength = accessPoint.property("Strength").toUInt(); + uint strength = accessPoint.property("Strength").toUInt(); - // Get security flags uint wpaFlags = accessPoint.property("WpaFlags").toUInt(); uint rsnFlags = accessPoint.property("RsnFlags").toUInt(); @@ -533,10 +503,8 @@ void NetworkManagerCpp::processAccessPoint(const QString &apPath) { securityType = "WPA"; } - // Get frequency/mode - uint frequency = accessPoint.property("Frequency").toUInt(); + uint frequency = accessPoint.property("Frequency").toUInt(); - // Create network object QVariantMap network; network["ssid"] = ssid; network["strength"] = (int)strength; @@ -544,15 +512,13 @@ void NetworkManagerCpp::processAccessPoint(const QString &apPath) { network["security"] = securityType; network["frequency"] = (int)frequency; network["path"] = apPath; - network["connected"] = - (m_wifiConnected && m_wifiSsid == ssid); // Mark if this is the active connection + network["connected"] = (m_wifiConnected && m_wifiSsid == ssid); - // Check if we already have this SSID (keep strongest signal) bool found = false; for (int i = 0; i < m_availableNetworks.size(); ++i) { QVariantMap existing = m_availableNetworks[i].toMap(); if (existing["ssid"].toString() == ssid) { - // Keep the one with stronger signal + if ((int)strength > existing["strength"].toInt()) { m_availableNetworks[i] = network; } @@ -576,7 +542,6 @@ void NetworkManagerCpp::connectToNetwork(const QString &ssid, const QString &pas return; } - // Find the access point for this SSID QString apPath; bool isSecured = false; @@ -597,24 +562,20 @@ void NetworkManagerCpp::connectToNetwork(const QString &ssid, const QString &pas qDebug() << "[NetworkManagerCpp] Found AP at:" << apPath << "Secured:" << isSecured; - // Build connection settings QMap> connectionSettings; - // Connection settings - QMap connection; + QMap connection; connection["type"] = "802-11-wireless"; connection["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); connection["id"] = ssid; - connection["autoconnect"] = true; // Auto-reconnect on boot + connection["autoconnect"] = true; connectionSettings["connection"] = connection; - // Wireless settings QMap wireless; - wireless["ssid"] = ssid.toUtf8(); // SSID as byte array + wireless["ssid"] = ssid.toUtf8(); wireless["mode"] = "infrastructure"; connectionSettings["802-11-wireless"] = wireless; - // Security settings (if network is secured) if (isSecured && !password.isEmpty()) { QMap wirelessSecurity; wirelessSecurity["key-mgmt"] = "wpa-psk"; @@ -623,24 +584,20 @@ void NetworkManagerCpp::connectToNetwork(const QString &ssid, const QString &pas connectionSettings["802-11-wireless-security"] = wirelessSecurity; } - // IPv4 settings (DHCP) QMap ipv4; ipv4["method"] = "auto"; connectionSettings["ipv4"] = ipv4; - // IPv6 settings (auto) QMap ipv6; ipv6["method"] = "auto"; connectionSettings["ipv6"] = ipv6; qDebug() << "[NetworkManagerCpp] Calling AddAndActivateConnection..."; - // Call AddAndActivateConnection QDBusMessage msg = QDBusMessage::createMethodCall( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", "AddAndActivateConnection"); - // Convert QMap to QVariantMap for D-Bus QVariantMap dbusSettings; for (auto it = connectionSettings.begin(); it != connectionSettings.end(); ++it) { dbusSettings[it.key()] = QVariant::fromValue(it.value()); @@ -661,7 +618,6 @@ void NetworkManagerCpp::connectToNetwork(const QString &ssid, const QString &pas QString error = reply.error().message(); qWarning() << "[NetworkManagerCpp] Connection failed:" << error; - // Parse common errors QString userError; if (error.contains("secrets-required") || error.contains("no-secrets")) { userError = "Incorrect password"; @@ -682,7 +638,6 @@ void NetworkManagerCpp::connectToNetwork(const QString &ssid, const QString &pas emit wifiConnectedChanged(); emit connectionSuccess(); - // Update connection state immediately QTimer::singleShot(1000, this, &NetworkManagerCpp::queryConnectionState); } @@ -741,10 +696,8 @@ void NetworkManagerCpp::toggleBluetooth() { void NetworkManagerCpp::setAirplaneMode(bool enabled) { qDebug() << "[NetworkManagerCpp] Setting Airplane Mode to:" << enabled; - // Airplane mode is implemented by enabling/disabling all wireless devices - // This is a simplified implementation for now if (m_hasNetworkManager) { - // Disable WiFi + if (!m_wifiDevicePath.isEmpty()) { QDBusInterface wifiDevice("org.freedesktop.NetworkManager", m_wifiDevicePath, "org.freedesktop.DBus.Properties", @@ -753,7 +706,6 @@ void NetworkManagerCpp::setAirplaneMode(bool enabled) { QVariant::fromValue(QDBusVariant(!enabled))); } - // Update WiFi state if (enabled) { disableWifi(); } @@ -774,25 +726,21 @@ void NetworkManagerCpp::createHotspot(const QString &ssid, const QString &passwo return; } - // Build hotspot connection settings QMap> connectionSettings; - // Connection settings - QMap connection; + QMap connection; connection["type"] = "802-11-wireless"; connection["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); connection["id"] = ssid + " Hotspot"; connection["autoconnect"] = false; connectionSettings["connection"] = connection; - // Wireless settings - AP mode QMap wireless; wireless["ssid"] = ssid.toUtf8(); - wireless["mode"] = "ap"; // Access Point mode - wireless["band"] = "bg"; // 2.4GHz + wireless["mode"] = "ap"; + wireless["band"] = "bg"; connectionSettings["802-11-wireless"] = wireless; - // Security settings if (!password.isEmpty() && password.length() >= 8) { QMap wirelessSecurity; wirelessSecurity["key-mgmt"] = "wpa-psk"; @@ -800,19 +748,16 @@ void NetworkManagerCpp::createHotspot(const QString &ssid, const QString &passwo connectionSettings["802-11-wireless-security"] = wirelessSecurity; } - // IPv4 settings - shared mode (NAT) QMap ipv4; - ipv4["method"] = "shared"; // Enables NAT/DHCP + ipv4["method"] = "shared"; connectionSettings["ipv4"] = ipv4; - // IPv6 settings QMap ipv6; ipv6["method"] = "ignore"; connectionSettings["ipv6"] = ipv6; qDebug() << "[NetworkManagerCpp] Activating hotspot connection..."; - // Add and activate connection QDBusMessage msg = QDBusMessage::createMethodCall( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", "AddAndActivateConnection"); @@ -882,7 +827,6 @@ QVariantList NetworkManagerCpp::getVpnConnections() { return vpnList; } - // Get all connections QDBusInterface settingsInterface( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings", "org.freedesktop.NetworkManager.Settings", QDBusConnection::systemBus()); @@ -896,7 +840,6 @@ QVariantList NetworkManagerCpp::getVpnConnections() { return vpnList; } - // Filter for VPN connections for (const QDBusObjectPath &path : connectionsReply.value()) { QDBusInterface connectionInterface("org.freedesktop.NetworkManager", path.path(), "org.freedesktop.NetworkManager.Settings.Connection", @@ -950,7 +893,6 @@ void NetworkManagerCpp::disconnectVpn(const QString &connectionId) { return; } - // Find active connection QDBusInterface nmInterface("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", QDBusConnection::systemBus()); diff --git a/shell/src/networkmanagercpp.h b/shell/src/managers/networkmanagercpp.h similarity index 85% rename from shell/src/networkmanagercpp.h rename to shell/src/managers/networkmanagercpp.h index bdb9f51a2..a0d2a1922 100644 --- a/shell/src/networkmanagercpp.h +++ b/shell/src/managers/networkmanagercpp.h @@ -55,7 +55,7 @@ class NetworkManagerCpp : public QObject { } bool hotspotSupported() const { return m_wifiAvailable; - } // Hotspot requires WiFi hardware + } bool bluetoothEnabled() const { return m_bluetoothEnabled; } @@ -66,25 +66,23 @@ class NetworkManagerCpp : public QObject { return m_availableNetworks; } - Q_INVOKABLE void enableWifi(); - Q_INVOKABLE void disableWifi(); - Q_INVOKABLE void toggleWifi(); - Q_INVOKABLE void scanWifi(); - Q_INVOKABLE void connectToNetwork(const QString &ssid, const QString &password); - Q_INVOKABLE void disconnectWifi(); + Q_INVOKABLE void enableWifi(); + Q_INVOKABLE void disableWifi(); + Q_INVOKABLE void toggleWifi(); + Q_INVOKABLE void scanWifi(); + Q_INVOKABLE void connectToNetwork(const QString &ssid, const QString &password); + Q_INVOKABLE void disconnectWifi(); - Q_INVOKABLE void enableBluetooth(); - Q_INVOKABLE void disableBluetooth(); - Q_INVOKABLE void toggleBluetooth(); + Q_INVOKABLE void enableBluetooth(); + Q_INVOKABLE void disableBluetooth(); + Q_INVOKABLE void toggleBluetooth(); - Q_INVOKABLE void setAirplaneMode(bool enabled); + Q_INVOKABLE void setAirplaneMode(bool enabled); - // WiFi Hotspot - Q_INVOKABLE void createHotspot(const QString &ssid, const QString &password); - Q_INVOKABLE void stopHotspot(); - Q_INVOKABLE bool isHotspotActive() const; + Q_INVOKABLE void createHotspot(const QString &ssid, const QString &password); + Q_INVOKABLE void stopHotspot(); + Q_INVOKABLE bool isHotspotActive() const; - // VPN Management Q_INVOKABLE QVariantList getVpnConnections(); Q_INVOKABLE void connectVpn(const QString &connectionId); Q_INVOKABLE void disconnectVpn(const QString &connectionId); @@ -143,4 +141,4 @@ class NetworkManagerCpp : public QObject { bool m_hotspotActive; }; -#endif // NETWORKMANAGERCPP_H +#endif diff --git a/shell/src/platform.h b/shell/src/managers/platform.h similarity index 100% rename from shell/src/platform.h rename to shell/src/managers/platform.h diff --git a/shell/src/managers/platformcpp.h b/shell/src/managers/platformcpp.h new file mode 100644 index 000000000..80959c682 --- /dev/null +++ b/shell/src/managers/platformcpp.h @@ -0,0 +1,104 @@ +#ifndef PLATFORMCPP_H +#define PLATFORMCPP_H + +#include +#include +#include "platform.h" + +class PlatformCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QString os READ os CONSTANT) + Q_PROPERTY(bool isLinux READ isLinux CONSTANT) + Q_PROPERTY(bool isMacOS READ isMacOS CONSTANT) + Q_PROPERTY(bool isAndroid READ isAndroid CONSTANT) + Q_PROPERTY(bool isIOS READ isIOS CONSTANT) + Q_PROPERTY(bool hasDBus READ hasDBus CONSTANT) + Q_PROPERTY(bool hasSystemdLogind READ hasSystemdLogind CONSTANT) + Q_PROPERTY(bool hasUPower READ hasUPower CONSTANT) + Q_PROPERTY(bool hasNetworkManager READ hasNetworkManager CONSTANT) + Q_PROPERTY(bool hasModemManager READ hasModemManager CONSTANT) + Q_PROPERTY(bool hasPulseAudio READ hasPulseAudio CONSTANT) + Q_PROPERTY(bool hasHardwareKeyboard READ hasHardwareKeyboard CONSTANT) + Q_PROPERTY(QString backend READ backend CONSTANT) + + public: + explicit PlatformCpp(QObject *parent = nullptr) + : QObject(parent) { +#if defined(Q_OS_LINUX) + m_os = "linux"; + m_isLinux = true; +#elif defined(Q_OS_MACOS) + m_os = "osx"; + m_isMacOS = true; +#elif defined(Q_OS_ANDROID) + m_os = "android"; + m_isAndroid = true; +#elif defined(Q_OS_IOS) + m_os = "ios"; + m_isIOS = true; +#else + m_os = "unknown"; +#endif + m_hasDBus = m_isLinux || m_isAndroid; + m_hasSystemdLogind = m_isLinux; + m_hasUPower = m_isLinux; + m_hasNetworkManager = m_isLinux; + m_hasModemManager = m_isLinux; + m_hasPulseAudio = m_isLinux; + } + + QString os() const { + return m_os; + } + bool isLinux() const { + return m_isLinux; + } + bool isMacOS() const { + return m_isMacOS; + } + bool isAndroid() const { + return m_isAndroid; + } + bool isIOS() const { + return m_isIOS; + } + bool hasDBus() const { + return m_hasDBus; + } + bool hasSystemdLogind() const { + return m_hasSystemdLogind; + } + bool hasUPower() const { + return m_hasUPower; + } + bool hasNetworkManager() const { + return m_hasNetworkManager; + } + bool hasModemManager() const { + return m_hasModemManager; + } + bool hasPulseAudio() const { + return m_hasPulseAudio; + } + bool hasHardwareKeyboard() const { + return Platform::hasHardwareKeyboard(); + } + QString backend() const { + return m_os; + } + + private: + QString m_os; + bool m_isLinux = false; + bool m_isMacOS = false; + bool m_isAndroid = false; + bool m_isIOS = false; + bool m_hasDBus = false; + bool m_hasSystemdLogind = false; + bool m_hasUPower = false; + bool m_hasNetworkManager = false; + bool m_hasModemManager = false; + bool m_hasPulseAudio = false; +}; + +#endif diff --git a/shell/src/powermanagercpp.cpp b/shell/src/managers/powermanagercpp.cpp similarity index 85% rename from shell/src/powermanagercpp.cpp rename to shell/src/managers/powermanagercpp.cpp index af2c4614d..5c327362e 100644 --- a/shell/src/powermanagercpp.cpp +++ b/shell/src/managers/powermanagercpp.cpp @@ -44,7 +44,6 @@ PowerManagerCpp::PowerManagerCpp(QObject *parent) , m_criticalAction(QStringLiteral("PowerOff")) { qCritical() << "[PowerManagerCpp] Initializing PowerManagerCpp Service"; - // Try to connect to UPower D-Bus m_upowerInterface = new QDBusInterface("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", QDBusConnection::systemBus(), this); @@ -52,8 +51,7 @@ PowerManagerCpp::PowerManagerCpp(QObject *parent) if (m_upowerInterface->isValid()) { m_hasUPower = true; qInfo() << "[PowerManagerCpp] Connected to UPower D-Bus"; - // Read the system-configured critical action (UPower policy). - // This is advisory for shells; we use it to align our critical-battery behavior. + QDBusReply criticalReply = m_upowerInterface->call("GetCriticalAction"); if (criticalReply.isValid() && !criticalReply.value().isEmpty()) { m_criticalAction = criticalReply.value(); @@ -68,7 +66,6 @@ PowerManagerCpp::PowerManagerCpp(QObject *parent) qDebug() << "[PowerManagerCpp] Using simulated battery"; } - // Try to connect to systemd-logind m_logindInterface = new QDBusInterface("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", QDBusConnection::systemBus(), this); @@ -81,16 +78,13 @@ PowerManagerCpp::PowerManagerCpp(QObject *parent) << m_logindInterface->lastError().message(); } - // Check CPU governor support checkCPUGovernorSupport(); - // Check wakelock and RTC alarm support checkWakelockSupport(); checkRtcAlarmSupport(); - // Setup idle timer m_idleTimer = new QTimer(this); - m_idleTimer->setInterval(10000); // Check idle every 10 seconds + m_idleTimer->setInterval(10000); connect(m_idleTimer, &QTimer::timeout, this, &PowerManagerCpp::checkIdleState); m_idleTimer->start(); @@ -103,7 +97,7 @@ PowerManagerCpp::PowerManagerCpp(QObject *parent) } PowerManagerCpp::~PowerManagerCpp() { - // Clean up all wakelocks before shutdown + cleanupWakelocks(); releaseInhibitor(); @@ -116,7 +110,7 @@ PowerManagerCpp::~PowerManagerCpp() { } void PowerManagerCpp::setupDBusConnections() { - // Connect to PrepareForSleep signal for lock-before-suspend + if (m_hasLogind) { QDBusConnection::systemBus().connect("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "PrepareForSleep", @@ -132,13 +126,11 @@ void PowerManagerCpp::setupDisplayDevice() { if (m_displayDeviceInterface->isValid()) { qInfo() << "[PowerManagerCpp] Connected to DisplayDevice"; - // Connect to property changes QDBusConnection::systemBus().connect("org.freedesktop.UPower", "/org/freedesktop/UPower/devices/DisplayDevice", "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(updateAggregateState())); - // Initial update updateAggregateState(); } else { qWarning() << "[PowerManagerCpp] DisplayDevice not available:" @@ -148,14 +140,13 @@ void PowerManagerCpp::setupDisplayDevice() { void PowerManagerCpp::updateAggregateState() { if (!m_displayDeviceInterface || !m_displayDeviceInterface->isValid()) { - // VM/No Battery Fallback + if (m_batteryLevel != 100) { - m_batteryLevel = 100; - m_isCharging = - false; // "Fully Charged" usually means not charging, but for VM we can say plugged in. + m_batteryLevel = 100; + m_isCharging = false; m_isPluggedIn = true; - m_warningLevel = 1; // None - m_batteryLevelCoarse = 1; // Not supported + m_warningLevel = 1; + m_batteryLevelCoarse = 1; emit batteryLevelChanged(); emit warningLevelChanged(); emit batteryLevelCoarseChanged(); @@ -173,10 +164,8 @@ void PowerManagerCpp::updateAggregateState() { m_displayDeviceInterface->property("TimeToEmpty").toLongLong(); const qint64 timeToFullSeconds = m_displayDeviceInterface->property("TimeToFull").toLongLong(); - // UPower Device.State: - // 0 Unknown, 1 Charging, 2 Discharging, 3 Empty, 4 Fully charged, 5 Pending charge, 6 Pending discharge - const bool newCharging = (state == 1 || state == 5); - const bool newPluggedIn = (state == 1 || state == 4 || state == 5); + const bool newCharging = (state == 1 || state == 5); + const bool newPluggedIn = (state == 1 || state == 4 || state == 5); if (m_batteryLevel != newLevel) { qInfo() << "[PowerManagerCpp] Battery Level Changed:" << m_batteryLevel << "->" << newLevel; @@ -187,7 +176,7 @@ void PowerManagerCpp::updateAggregateState() { if (m_warningLevel != warningLevel) { m_warningLevel = warningLevel; emit warningLevelChanged(); - // Critical and Action warning levels indicate immediate user attention is required. + if (m_warningLevel >= 4) { emit criticalBattery(); } @@ -208,13 +197,11 @@ void PowerManagerCpp::updateAggregateState() { emit isPluggedInChanged(); } - // Estimated time (minutes): TimeToEmpty when discharging; TimeToFull when charging. - // UPower reports seconds; 0 means unknown. int newEstimatedMinutes = -1; - if (state == 2 || state == 6) { // Discharging / Pending discharge + if (state == 2 || state == 6) { if (timeToEmptySeconds > 0) newEstimatedMinutes = static_cast(timeToEmptySeconds / 60); - } else if (state == 1 || state == 5) { // Charging / Pending charge + } else if (state == 1 || state == 5) { if (timeToFullSeconds > 0) newEstimatedMinutes = static_cast(timeToFullSeconds / 60); } @@ -236,7 +223,6 @@ void PowerManagerCpp::onPrepareForSleep(bool beforeSleep) { emit aboutToSleep(); emit prepareForSuspend(); - // Release inhibitor if we had one (delay inhibitor releases automatically) releaseInhibitor(); } else { qInfo() << "[PowerManagerCpp] System resumed from sleep"; @@ -245,8 +231,6 @@ void PowerManagerCpp::onPrepareForSleep(bool beforeSleep) { emit resumedFromSleep(); emit resumedFromSuspend(); - // Re-acquire wakelocks that were held before suspend - // (kernel wakelocks persist across suspend, but we re-apply for safety) for (auto it = m_activeWakelocks.begin(); it != m_activeWakelocks.end(); ++it) { if (it.value()) { qDebug() << "[PowerManagerCpp] Re-acquiring wakelock after resume:" << it.key(); @@ -257,7 +241,7 @@ void PowerManagerCpp::onPrepareForSleep(bool beforeSleep) { } void PowerManagerCpp::simulateBatteryUpdate() { - // Simple simulation for testing + if (m_isCharging) { if (m_batteryLevel < 100) { m_batteryLevel = qMin(100, m_batteryLevel + 1); @@ -413,12 +397,9 @@ void PowerManagerCpp::checkIdleState() { qInfo() << "[PowerManagerCpp] Idle state changed:" << (nowIdle ? "idle" : "active"); emit idleStateChanged(nowIdle); - // If idle and auto-suspend is enabled, emit aboutToSleep if (nowIdle && m_autoSuspendEnabled) { qInfo() << "[PowerManagerCpp] Auto-suspend triggered after" << m_idleTimeout << "seconds"; - // Note: Actual suspend should be handled by QML/UI layer - // This just signals that the system should prepare for sleep } } } @@ -437,9 +418,6 @@ void PowerManagerCpp::applyCPUGovernor(PowerProfile profile) { default: governor = "schedutil"; break; } - // Try to set CPU governor via sysfs - // Note: This requires write permission to /sys/devices/system/cpu/cpufreq/policy*/scaling_governor - // which should be granted by udev rules (70-marathon-shell.rules) QProcess process; process.start("sh", {"-c", @@ -456,7 +434,7 @@ void PowerManagerCpp::applyCPUGovernor(PowerProfile profile) { } void PowerManagerCpp::checkCPUGovernorSupport() { - // Check if CPU frequency scaling is available + QFile scalingAvailable("/sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors"); if (scalingAvailable.exists() && scalingAvailable.open(QIODevice::ReadOnly)) { QString governors = QString::fromLatin1(scalingAvailable.readAll()).trimmed(); @@ -470,18 +448,14 @@ void PowerManagerCpp::checkCPUGovernorSupport() { } } -// ============================================================================ -// Wakelock Management -// ============================================================================ - void PowerManagerCpp::checkWakelockSupport() { - // Check if kernel wakelock interface is available + QFile wakeLockFile("/sys/power/wake_lock"); m_wakelockSupported = wakeLockFile.exists(); if (m_wakelockSupported) { qInfo() << "[PowerManagerCpp] Kernel wakelock support detected"; - // Test if we can write to it (requires CAP_BLOCK_SUSPEND or appropriate permissions) + if (wakeLockFile.open(QIODevice::WriteOnly | QIODevice::Text)) { wakeLockFile.close(); m_fallbackMode = "wakelock"; @@ -504,7 +478,6 @@ bool PowerManagerCpp::acquireWakelock(const QString &name) { return true; } - // Try kernel wakelock first if (m_wakelockSupported && m_fallbackMode != "inhibitor") { if (writeToFile("/sys/power/wake_lock", name)) { m_activeWakelocks[name] = true; @@ -518,7 +491,6 @@ bool PowerManagerCpp::acquireWakelock(const QString &name) { } } - // Fallback to systemd-logind inhibitor if (m_hasLogind) { bool success = inhibitSuspend("Marathon Shell", "Wakelock: " + name); if (success) { @@ -541,7 +513,6 @@ bool PowerManagerCpp::releaseWakelock(const QString &name) { bool success = false; - // Release kernel wakelock if that's what we're using if (m_fallbackMode == "wakelock") { success = writeToFile("/sys/power/wake_unlock", name); if (success) { @@ -550,8 +521,7 @@ bool PowerManagerCpp::releaseWakelock(const QString &name) { qWarning() << "[PowerManagerCpp] Failed to release wakelock:" << name; } } else if (m_fallbackMode == "inhibitor") { - // For inhibitors, we release all at once (single FD) - // This is a limitation of the inhibitor API + releaseInhibitor(); qInfo() << "[PowerManagerCpp] Released inhibitor lock for:" << name; success = true; @@ -575,15 +545,8 @@ bool PowerManagerCpp::inhibitSuspend(const QString &who, const QString &why) { return false; } - // Call Inhibit method: Inhibit(what, who, why, mode) - // Mode "delay" allows system to delay suspend for up to 5 seconds (safer for mobile) QDBusReply reply = - m_logindInterface->call("Inhibit", - "sleep", // what: sleep, shutdown, idle, handle-power-key - who, // who: human-readable application name - why, // why: human-readable reason - "delay" // mode: "block" or "delay" - ); + m_logindInterface->call("Inhibit", "sleep", who, why, "delay"); if (reply.isValid()) { m_inhibitorFd = reply.value(); @@ -597,8 +560,7 @@ bool PowerManagerCpp::inhibitSuspend(const QString &who, const QString &why) { } void PowerManagerCpp::releaseInhibitor() { - // Closing the file descriptor releases the inhibitor lock - // QDBusUnixFileDescriptor automatically closes FD when reset + if (m_inhibitorFd.isValid()) { m_inhibitorFd = QDBusUnixFileDescriptor(); qDebug() << "[PowerManagerCpp] Released inhibitor lock"; @@ -606,7 +568,7 @@ void PowerManagerCpp::releaseInhibitor() { } void PowerManagerCpp::cleanupWakelocks() { - // Release all active wakelocks + qInfo() << "[PowerManagerCpp] Cleaning up wakelocks..."; for (auto it = m_activeWakelocks.begin(); it != m_activeWakelocks.end(); ++it) { if (it.value()) { @@ -629,18 +591,14 @@ bool PowerManagerCpp::writeToFile(const QString &path, const QString &content) { return true; } -// ============================================================================ -// RTC Alarm Support -// ============================================================================ - void PowerManagerCpp::checkRtcAlarmSupport() { - // Check if RTC wakealarm interface is available + QFile rtcWakeAlarm("/sys/class/rtc/rtc0/wakealarm"); m_rtcAlarmSupported = rtcWakeAlarm.exists(); if (m_rtcAlarmSupported) { qInfo() << "[PowerManagerCpp] RTC wakealarm support detected"; - // Test if we can write to it + if (rtcWakeAlarm.open(QIODevice::WriteOnly | QIODevice::Text)) { rtcWakeAlarm.close(); qInfo() << "[PowerManagerCpp] RTC wakealarm write access granted"; @@ -659,13 +617,11 @@ bool PowerManagerCpp::setRtcAlarm(qint64 epochTime) { return false; } - // Clear existing alarm first if (!writeToRtcWakeAlarm("0")) { qWarning() << "[PowerManagerCpp] Failed to clear existing RTC alarm"; return false; } - // Set new alarm if (!writeToRtcWakeAlarm(QString::number(epochTime))) { qWarning() << "[PowerManagerCpp] Failed to set RTC alarm"; return false; diff --git a/shell/src/powermanagercpp.h b/shell/src/managers/powermanagercpp.h similarity index 68% rename from shell/src/powermanagercpp.h rename to shell/src/managers/powermanagercpp.h index fd0cea45f..51176be43 100644 --- a/shell/src/powermanagercpp.h +++ b/shell/src/managers/powermanagercpp.h @@ -13,14 +13,11 @@ class PowerManagerCpp : public QObject, protected QDBusContext { Q_OBJECT - // ... properties ... - - // ... Q_PROPERTY(int batteryLevel READ batteryLevel NOTIFY batteryLevelChanged) - // UPower WarningLevel (0..5): Unknown/None/Discharging(UPS)/Low/Critical/Action + Q_PROPERTY(uint warningLevel READ warningLevel NOTIFY warningLevelChanged) - // UPower BatteryLevel (coarse) is optional; 1 means "not supported". Exposed for debugging. + Q_PROPERTY(uint batteryLevelCoarse READ batteryLevelCoarse NOTIFY batteryLevelCoarseChanged) Q_PROPERTY(bool isCharging READ isCharging NOTIFY isChargingChanged) Q_PROPERTY(bool isPluggedIn READ isPluggedIn NOTIFY isPluggedInChanged) @@ -37,8 +34,7 @@ class PowerManagerCpp : public QObject, protected QDBusContext { Q_PROPERTY(bool rtcAlarmSupported READ rtcAlarmSupported CONSTANT) Q_PROPERTY(QStringList activeWakelocks READ activeWakelocks NOTIFY activeWakelocksChanged) Q_PROPERTY(int wakeLockCount READ wakeLockCount NOTIFY activeWakelocksChanged) - // System-configured action for critical battery, from UPower.GetCriticalAction() - // Common values: "HybridSleep", "Hibernate", "PowerOff" + Q_PROPERTY(QString criticalAction READ criticalAction NOTIFY criticalActionChanged) public: @@ -114,14 +110,12 @@ class PowerManagerCpp : public QObject, protected QDBusContext { Q_INVOKABLE void setIdleTimeout(int seconds); Q_INVOKABLE void setAutoSuspendEnabled(bool enabled); - // Wakelock management Q_INVOKABLE bool acquireWakelock(const QString &name); Q_INVOKABLE bool releaseWakelock(const QString &name); Q_INVOKABLE bool hasWakelock(const QString &name) const; Q_INVOKABLE bool inhibitSuspend(const QString &who, const QString &why); Q_INVOKABLE void releaseInhibitor(); - // RTC alarm support Q_INVOKABLE bool setRtcAlarm(qint64 epochTime); Q_INVOKABLE bool clearRtcAlarm(); @@ -140,12 +134,11 @@ class PowerManagerCpp : public QObject, protected QDBusContext { void systemSuspendedChanged(); void criticalBattery(); void powerError(const QString &message); - void aboutToSleep(); // Emitted before system suspends - void resumedFromSleep(); // Emitted after system resumes - void - prepareForSuspend(); // Emitted when system is about to suspend (from PrepareForSleep signal) - void resumedFromSuspend(); // Emitted when system resumes from suspend - void idleStateChanged(bool idle); // Emitted when idle state changes + void aboutToSleep(); + void resumedFromSleep(); + void prepareForSuspend(); + void resumedFromSuspend(); + void idleStateChanged(bool idle); void activeWakelocksChanged(); private slots: @@ -154,50 +147,48 @@ class PowerManagerCpp : public QObject, protected QDBusContext { void checkIdleState(); private: - void setupDBusConnections(); - void simulateBatteryUpdate(); - void applyCPUGovernor(PowerProfile profile); - void checkCPUGovernorSupport(); - void checkWakelockSupport(); - void checkRtcAlarmSupport(); - void cleanupWakelocks(); - void setupDisplayDevice(); - bool writeToFile(const QString &path, const QString &content); - bool writeToRtcWakeAlarm(const QString &value); - - QDBusInterface *m_upowerInterface; - QDBusInterface *m_displayDeviceInterface; - QDBusInterface *m_logindInterface; - QTimer *m_idleTimer; - - int m_batteryLevel; - uint m_warningLevel; - uint m_batteryLevelCoarse; - bool m_isCharging; - bool m_isPluggedIn; - bool m_isPowerSaveMode; - int m_estimatedBatteryTime; - bool m_hasUPower; - bool m_hasLogind; - - PowerProfile m_currentProfile; - QString m_powerProfileString; - bool m_powerProfilesSupported; - int m_idleTimeout; - bool m_autoSuspendEnabled; - bool m_isIdle; - qint64 m_lastActivityTime; - - // Wakelock support + void setupDBusConnections(); + void simulateBatteryUpdate(); + void applyCPUGovernor(PowerProfile profile); + void checkCPUGovernorSupport(); + void checkWakelockSupport(); + void checkRtcAlarmSupport(); + void cleanupWakelocks(); + void setupDisplayDevice(); + bool writeToFile(const QString &path, const QString &content); + bool writeToRtcWakeAlarm(const QString &value); + + QDBusInterface *m_upowerInterface; + QDBusInterface *m_displayDeviceInterface; + QDBusInterface *m_logindInterface; + QTimer *m_idleTimer; + + int m_batteryLevel; + uint m_warningLevel; + uint m_batteryLevelCoarse; + bool m_isCharging; + bool m_isPluggedIn; + bool m_isPowerSaveMode; + int m_estimatedBatteryTime; + bool m_hasUPower; + bool m_hasLogind; + + PowerProfile m_currentProfile; + QString m_powerProfileString; + bool m_powerProfilesSupported; + int m_idleTimeout; + bool m_autoSuspendEnabled; + bool m_isIdle; + qint64 m_lastActivityTime; + QMap m_activeWakelocks; QDBusUnixFileDescriptor m_inhibitorFd; bool m_systemSuspended; bool m_wakelockSupported; - QString m_fallbackMode; // "wakelock", "inhibitor", or "none" + QString m_fallbackMode; - // RTC alarm support - bool m_rtcAlarmSupported; - QString m_criticalAction; + bool m_rtcAlarmSupported; + QString m_criticalAction; }; -#endif // POWERMANAGERCPP_H +#endif diff --git a/shell/src/rotationmanager.cpp b/shell/src/managers/rotationmanager.cpp similarity index 89% rename from shell/src/rotationmanager.cpp rename to shell/src/managers/rotationmanager.cpp index d54e90231..90607ef34 100644 --- a/shell/src/rotationmanager.cpp +++ b/shell/src/managers/rotationmanager.cpp @@ -52,14 +52,10 @@ QString RotationManager::orientationToString(QOrientationReading::Orientation o) int RotationManager::orientationToRotation(QOrientationReading::Orientation o) { switch (o) { - case QOrientationReading::TopUp: // portrait - return 0; - case QOrientationReading::RightUp: // landscape - return 90; - case QOrientationReading::TopDown: // portrait-inverted - return 180; - case QOrientationReading::LeftUp: // landscape-inverted - return 270; + case QOrientationReading::TopUp: return 0; + case QOrientationReading::RightUp: return 90; + case QOrientationReading::TopDown: return 180; + case QOrientationReading::LeftUp: return 270; default: return 0; } } diff --git a/shell/src/rotationmanager.h b/shell/src/managers/rotationmanager.h similarity index 98% rename from shell/src/rotationmanager.h rename to shell/src/managers/rotationmanager.h index 05a4301a2..ee2055173 100644 --- a/shell/src/rotationmanager.h +++ b/shell/src/managers/rotationmanager.h @@ -57,4 +57,4 @@ class RotationManager : public QObject { QOrientationSensor *m_sensor; }; -#endif // ROTATIONMANAGER_H +#endif diff --git a/shell/src/rtscheduler.cpp b/shell/src/managers/rtscheduler.cpp similarity index 96% rename from shell/src/rtscheduler.cpp rename to shell/src/managers/rtscheduler.cpp index 82ef25ee5..9b1235357 100644 --- a/shell/src/rtscheduler.cpp +++ b/shell/src/managers/rtscheduler.cpp @@ -8,7 +8,7 @@ #include #include #include -#include // for strerror +#include #endif RTScheduler::RTScheduler(QObject *parent) @@ -20,7 +20,7 @@ RTScheduler::RTScheduler(QObject *parent) void RTScheduler::detectKernelCapabilities() { #ifdef Q_OS_LINUX - // Check if running on PREEMPT_RT kernel + QFile realtimeFile("/sys/kernel/realtime"); if (realtimeFile.exists() && realtimeFile.open(QIODevice::ReadOnly)) { QTextStream stream(&realtimeFile); @@ -36,7 +36,7 @@ void RTScheduler::detectKernelCapabilities() { qCritical() << "[RTScheduler] Rebuild kernel with CONFIG_PREEMPT_RT=y"; } } else { - // Fallback: check uname for "PREEMPT_RT" + QFile versionFile("/proc/version"); if (versionFile.open(QIODevice::ReadOnly)) { QTextStream stream(&versionFile); @@ -46,13 +46,12 @@ void RTScheduler::detectKernelCapabilities() { } } - // Check if we have RT scheduling permissions struct sched_param param; param.sched_priority = 1; if (sched_setscheduler(0, SCHED_FIFO, ¶m) == 0) { m_hasRTPermissions = true; - // Reset to normal scheduling + param.sched_priority = 0; sched_setscheduler(0, SCHED_OTHER, ¶m); qInfo() << "[RTScheduler] ✓ RT scheduling permissions available"; @@ -138,7 +137,6 @@ bool RTScheduler::setThreadPriority(QThread *thread, int priority) { return false; } - // Get pthread handle from QThread pthread_t threadHandle = reinterpret_cast(thread->currentThreadId()); struct sched_param param; @@ -190,7 +188,7 @@ QString RTScheduler::getCurrentPolicy() const { int RTScheduler::getCurrentPriority() const { #ifdef Q_OS_LINUX struct sched_param param; - int policy = sched_getscheduler(0); + sched_getscheduler(0); if (sched_getparam(0, ¶m) == 0) { return param.sched_priority; diff --git a/shell/src/managers/rtscheduler.h b/shell/src/managers/rtscheduler.h new file mode 100644 index 000000000..010734de8 --- /dev/null +++ b/shell/src/managers/rtscheduler.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +class RTScheduler : public QObject { + Q_OBJECT + + public: + enum Priority { + InputHandling = 85, + CompositorRender = 75, + DefaultUserRT = 80, + KernelIRQ = 50 + }; + + explicit RTScheduler(QObject *parent = nullptr); + + Q_INVOKABLE bool setRealtimePriority(int priority); + + Q_INVOKABLE bool setThreadPriority(QThread *thread, int priority); + + Q_INVOKABLE bool isRealtimeKernel() const; + + Q_INVOKABLE bool hasRealtimePermissions() const; + + Q_INVOKABLE QString getCurrentPolicy() const; + + Q_INVOKABLE int getCurrentPriority() const; + + private: + bool m_isRealtimeKernel; + bool m_hasRTPermissions; + + void detectKernelCapabilities(); + bool setThreadSchedParam(int policy, int priority); +}; diff --git a/shell/src/screenmetrics.cpp b/shell/src/managers/screenmetrics.cpp similarity index 100% rename from shell/src/screenmetrics.cpp rename to shell/src/managers/screenmetrics.cpp diff --git a/shell/src/screenmetrics.h b/shell/src/managers/screenmetrics.h similarity index 93% rename from shell/src/screenmetrics.h rename to shell/src/managers/screenmetrics.h index fc1e417f3..087081630 100644 --- a/shell/src/screenmetrics.h +++ b/shell/src/managers/screenmetrics.h @@ -5,7 +5,6 @@ class QScreen; -// Exposes primary screen metrics to QML so we don't hard-code device sizes in QML. class ScreenMetrics : public QObject { Q_OBJECT Q_PROPERTY(int width READ width NOTIFY metricsChanged) diff --git a/shell/src/securitymanager.cpp b/shell/src/managers/securitymanager.cpp similarity index 100% rename from shell/src/securitymanager.cpp rename to shell/src/managers/securitymanager.cpp diff --git a/shell/src/managers/securitymanager.h b/shell/src/managers/securitymanager.h new file mode 100644 index 000000000..544725eb4 --- /dev/null +++ b/shell/src/managers/securitymanager.h @@ -0,0 +1,133 @@ +#ifndef SECURITYMANAGER_H +#define SECURITYMANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class SecurityManager : public QObject { + Q_OBJECT + Q_PROPERTY(AuthMode authMode READ authMode WRITE setAuthMode NOTIFY authModeChanged) + Q_PROPERTY(bool hasQuickPIN READ hasQuickPIN NOTIFY quickPINChanged) + Q_PROPERTY( + bool fingerprintAvailable READ fingerprintAvailable NOTIFY fingerprintAvailableChanged) + Q_PROPERTY(bool isLockedOut READ isLockedOut NOTIFY lockoutStateChanged) + Q_PROPERTY(int lockoutSecondsRemaining READ lockoutSecondsRemaining NOTIFY lockoutStateChanged) + Q_PROPERTY(int failedAttempts READ failedAttempts NOTIFY failedAttemptsChanged) + + public: + explicit SecurityManager(QObject *parent = nullptr); + ~SecurityManager(); + + enum AuthMode { + SystemPassword, + QuickPIN, + Biometric, + BiometricPIN + }; + Q_ENUM(AuthMode) + + enum BiometricType { + Fingerprint, + FaceRecognition + }; + Q_ENUM(BiometricType) + + AuthMode authMode() const { + return m_authMode; + } + bool hasQuickPIN() const { + return m_hasQuickPIN; + } + bool fingerprintAvailable() const { + return m_fingerprintAvailable; + } + bool isLockedOut() const { + return m_isLockedOut; + } + int lockoutSecondsRemaining() const; + int failedAttempts() const { + return m_failedAttempts; + } + + void setAuthMode(AuthMode mode); + + Q_INVOKABLE void authenticatePassword(const QString &password); + Q_INVOKABLE void authenticateQuickPIN(const QString &pin); + Q_INVOKABLE void authenticateBiometric(BiometricType type = Fingerprint); + Q_INVOKABLE void cancelAuthentication(); + + Q_INVOKABLE void setQuickPIN(const QString &pin, const QString &systemPassword); + Q_INVOKABLE void removeQuickPIN(const QString &systemPassword); + + Q_INVOKABLE bool isBiometricEnrolled(BiometricType type) const; + + Q_INVOKABLE void resetLockout(); + + Q_INVOKABLE QString getCurrentUsername() const; + + signals: + void authenticationSuccess(); + void authenticationFailed(const QString &reason); + void authModeChanged(); + void quickPINChanged(); + void fingerprintAvailableChanged(); + void lockoutStateChanged(); + void failedAttemptsChanged(); + void biometricPrompt(const QString &message); + + private slots: + void onFingerprintVerifyStatus(const QString &result, bool done); + void checkLockoutTimer(); + void onPasswordAuthFinished(); + void onQuickPINAuthFinished(); + + private: + bool authenticateViaPAM(const QString &password); + static int pamConversationCallback(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr); + + bool verifyQuickPIN(const QString &pin); + bool storeQuickPIN(const QString &pin); + QString retrieveQuickPIN(); + void clearQuickPIN(); + + static QByteArray generateSalt(int length); + static QByteArray hashWithIterations(const QByteArray &data, int iterations); + + void initFingerprintDevice(); + void startFingerprintAuth(); + void stopFingerprintAuth(); + void checkFingerprintEnrollment(); + + void updateLockoutStatus(); + void recordFailedAttempt(); + void resetFailedAttempts(); + int queryFaillockAttempts(); + + AuthMode m_authMode; + bool m_hasQuickPIN; + bool m_fingerprintAvailable; + bool m_isLockedOut; + int m_failedAttempts; + QDateTime m_lockoutUntil; + QTimer *m_lockoutTimer; + + QDBusInterface *m_fprintdManager; + QDBusInterface *m_fprintdDevice; + bool m_fprintdAuthInProgress; + + QString m_currentPassword; + + QFutureWatcher *m_passwordAuthWatcher; + QFutureWatcher *m_quickPINAuthWatcher; +}; + +#endif diff --git a/shell/src/sensormanagercpp.cpp b/shell/src/managers/sensormanagercpp.cpp similarity index 94% rename from shell/src/sensormanagercpp.cpp rename to shell/src/managers/sensormanagercpp.cpp index 2a9d33acf..3b5eb79d6 100644 --- a/shell/src/sensormanagercpp.cpp +++ b/shell/src/managers/sensormanagercpp.cpp @@ -5,8 +5,7 @@ SensorManagerCpp::SensorManagerCpp(QObject *parent) : QObject(parent) , m_available(false) , m_proximityNear(false) - , m_ambientLight(500) // Default to moderate light -{ + , m_ambientLight(500) { qDebug() << "[SensorManagerCpp] Using QtSensors backend"; m_proximity = new QProximitySensor(this); @@ -18,7 +17,6 @@ SensorManagerCpp::SensorManagerCpp(QObject *parent) m_available = ok1 || ok2; emit availableChanged(); - // Proximity if (ok1) { connect(m_proximity, &QProximitySensor::readingChanged, this, &SensorManagerCpp::onProximityChanged); @@ -28,7 +26,6 @@ SensorManagerCpp::SensorManagerCpp(QObject *parent) qInfo() << "[SensorManagerCpp] No proximity sensor backend"; } - // Ambient light if (ok2) { connect(m_light, &QLightSensor::readingChanged, this, &SensorManagerCpp::onLightChanged); m_light->start(); diff --git a/shell/src/sensormanagercpp.h b/shell/src/managers/sensormanagercpp.h similarity index 100% rename from shell/src/sensormanagercpp.h rename to shell/src/managers/sensormanagercpp.h diff --git a/shell/src/settingsmanager.cpp b/shell/src/managers/settingsmanager.cpp similarity index 95% rename from shell/src/settingsmanager.cpp rename to shell/src/managers/settingsmanager.cpp index eebcecd9b..5cc9a7f48 100644 --- a/shell/src/settingsmanager.cpp +++ b/shell/src/managers/settingsmanager.cpp @@ -28,14 +28,14 @@ SettingsManager::SettingsManager(QObject *parent) , m_dndEnabled(false) , m_vibrationEnabled(true) , m_audioProfile("normal") - , m_screenTimeout(120000) // 2 minutes default + , m_screenTimeout(120000) , m_autoBrightness(false) , m_statusBarClockPosition("center") , m_showNotificationsOnLockScreen(true) - , m_filterMobileFriendlyApps(true) // Default to ON for better mobile UX + , m_filterMobileFriendlyApps(true) , m_hiddenApps() , m_appSortOrder("alphabetical") - , m_appGridColumns(0) // 0 = auto + , m_appGridColumns(0) , m_searchNativeApps(true) , m_showNotificationBadges(true) , m_showFrequentApps(false) @@ -91,14 +91,14 @@ namespace { return "qrc:/wallpapers/" + relativePath.mid(QString("wallpapers/").size()); } if (relativePath.startsWith("sounds/")) { - // In resources.qrc, sounds are under prefix "/sounds" and live at resources/sounds/... + return "qrc:/sounds/resources/sounds/" + relativePath.mid(QString("sounds/").size()); } return {}; } QString migrateQrcToFile(const QString &pathOrUrl, const QString &dataDir) { - // Convert legacy qrc paths to the new filesystem layout when possible. + if (dataDir.isEmpty()) return pathOrUrl; @@ -135,7 +135,7 @@ namespace { out.sort(Qt::CaseInsensitive); return out; } -} // namespace +} QString SettingsManager::assetUrl(const QString &relativePath) const { const QString dir = dataDirFromEnvOrDefault(); @@ -152,13 +152,11 @@ QString SettingsManager::assetUrl(const QString &relativePath) const { void SettingsManager::load() { const QString dataDir = dataDirFromEnvOrDefault(); - // Existing m_userScaleFactor = m_settings.value("ui/userScaleFactor", 1.0).toReal(); m_wallpaperPath = m_settings.value("ui/wallpaperPath", assetUrl("wallpapers/wallpaper.jpg")).toString(); m_wallpaperPath = migrateQrcToFile(m_wallpaperPath, dataDir); - // Migrated from QML m_deviceName = m_settings.value("system/deviceName", "Marathon OS").toString(); m_autoLock = m_settings.value("system/autoLock", true).toBool(); m_autoLockTimeout = m_settings.value("system/autoLockTimeout", 300).toInt(); @@ -166,7 +164,6 @@ void SettingsManager::load() { m_timeFormat = m_settings.value("system/timeFormat", "12h").toString(); m_dateFormat = m_settings.value("system/dateFormat", "US").toString(); - // Audio m_ringtone = m_settings.value("audio/ringtone", assetUrl("sounds/phone/bbpro1.wav")).toString(); m_ringtone = migrateQrcToFile(m_ringtone, dataDir); m_notificationSound = @@ -184,17 +181,14 @@ void SettingsManager::load() { m_vibrationEnabled = m_settings.value("audio/vibrationEnabled", true).toBool(); m_audioProfile = m_settings.value("audio/audioProfile", "normal").toString(); - // Display m_screenTimeout = m_settings.value("display/screenTimeout", 120000).toInt(); m_autoBrightness = m_settings.value("display/autoBrightness", false).toBool(); m_statusBarClockPosition = m_settings.value("display/statusBarClockPosition", "center").toString(); - // Notifications m_showNotificationsOnLockScreen = m_settings.value("notifications/showOnLockScreen", true).toBool(); - // App Management m_filterMobileFriendlyApps = m_settings.value("apps/filterMobileFriendly", true).toBool(); m_hiddenApps = m_settings.value("apps/hiddenApps", QStringList()).toStringList(); m_appSortOrder = m_settings.value("apps/sortOrder", "alphabetical").toString(); @@ -203,7 +197,6 @@ void SettingsManager::load() { m_showNotificationBadges = m_settings.value("apps/showBadges", true).toBool(); m_showFrequentApps = m_settings.value("apps/showFrequentApps", false).toBool(); - // Load default apps map QVariantMap defaultApps; defaultApps["browser"] = m_settings.value("apps/defaultBrowser", "").toString(); defaultApps["dialer"] = m_settings.value("apps/defaultDialer", "").toString(); @@ -216,10 +209,8 @@ void SettingsManager::load() { defaultApps["files"] = m_settings.value("apps/defaultFiles", "").toString(); m_defaultApps = defaultApps; - // OOBE m_firstRunComplete = m_settings.value("system/firstRunComplete", false).toBool(); - // Quick Settings - Default to all tiles enabled in default order QStringList defaultTiles = { "wifi", "bluetooth", "flight", "cellular", "rotation", "autobrightness", "location", "hotspot", "vibration", "nightlight", "torch", "notifications", @@ -229,7 +220,6 @@ void SettingsManager::load() { m_quickSettingsTileOrder = m_settings.value("quicksettings/tileOrder", defaultTiles).toStringList(); - // Keyboard settings - BB10-style defaults (all features enabled except predictive spacing) m_keyboardAutoCorrection = m_settings.value("keyboard/autoCorrection", true).toBool(); m_keyboardPredictiveText = m_settings.value("keyboard/predictiveText", true).toBool(); m_keyboardWordFling = m_settings.value("keyboard/wordFling", true).toBool(); @@ -247,11 +237,10 @@ void SettingsManager::load() { } void SettingsManager::save() { - // Existing + m_settings.setValue("ui/userScaleFactor", m_userScaleFactor); m_settings.setValue("ui/wallpaperPath", m_wallpaperPath); - // Migrated m_settings.setValue("system/deviceName", m_deviceName); m_settings.setValue("system/autoLock", m_autoLock); m_settings.setValue("system/autoLockTimeout", m_autoLockTimeout); @@ -259,7 +248,6 @@ void SettingsManager::save() { m_settings.setValue("system/timeFormat", m_timeFormat); m_settings.setValue("system/dateFormat", m_dateFormat); - // Audio m_settings.setValue("audio/ringtone", m_ringtone); m_settings.setValue("audio/notificationSound", m_notificationSound); m_settings.setValue("audio/alarmSound", m_alarmSound); @@ -272,15 +260,12 @@ void SettingsManager::save() { m_settings.setValue("audio/vibrationEnabled", m_vibrationEnabled); m_settings.setValue("audio/audioProfile", m_audioProfile); - // Display m_settings.setValue("display/screenTimeout", m_screenTimeout); m_settings.setValue("display/autoBrightness", m_autoBrightness); m_settings.setValue("display/statusBarClockPosition", m_statusBarClockPosition); - // Notifications m_settings.setValue("notifications/showOnLockScreen", m_showNotificationsOnLockScreen); - // App Management m_settings.setValue("apps/filterMobileFriendly", m_filterMobileFriendlyApps); m_settings.setValue("apps/hiddenApps", m_hiddenApps); m_settings.setValue("apps/sortOrder", m_appSortOrder); @@ -289,7 +274,6 @@ void SettingsManager::save() { m_settings.setValue("apps/showBadges", m_showNotificationBadges); m_settings.setValue("apps/showFrequentApps", m_showFrequentApps); - // Save default apps m_settings.setValue("apps/defaultBrowser", m_defaultApps.value("browser", "").toString()); m_settings.setValue("apps/defaultDialer", m_defaultApps.value("dialer", "").toString()); m_settings.setValue("apps/defaultMessaging", m_defaultApps.value("messaging", "").toString()); @@ -300,14 +284,11 @@ void SettingsManager::save() { m_settings.setValue("apps/defaultVideo", m_defaultApps.value("video", "").toString()); m_settings.setValue("apps/defaultFiles", m_defaultApps.value("files", "").toString()); - // OOBE m_settings.setValue("system/firstRunComplete", m_firstRunComplete); - // Quick Settings m_settings.setValue("quicksettings/enabledTiles", m_enabledQuickSettingsTiles); m_settings.setValue("quicksettings/tileOrder", m_quickSettingsTileOrder); - // Keyboard settings m_settings.setValue("keyboard/autoCorrection", m_keyboardAutoCorrection); m_settings.setValue("keyboard/predictiveText", m_keyboardPredictiveText); m_settings.setValue("keyboard/wordFling", m_keyboardWordFling); @@ -343,7 +324,6 @@ void SettingsManager::setWallpaperPath(const QString &path) { qDebug() << "[SettingsManager] wallpaperPath changed to" << path; } -// Migrated setters void SettingsManager::setDeviceName(const QString &name) { if (m_deviceName == name) return; @@ -392,7 +372,6 @@ void SettingsManager::setDateFormat(const QString &format) { emit dateFormatChanged(); } -// Audio setters void SettingsManager::setRingtone(const QString &path) { if (m_ringtone == path) return; @@ -492,7 +471,6 @@ void SettingsManager::setAudioProfile(const QString &profile) { qDebug() << "[SettingsManager] Audio profile changed to" << m_audioProfile; } -// Display setters void SettingsManager::setScreenTimeout(int ms) { if (m_screenTimeout == ms) return; @@ -514,7 +492,7 @@ void SettingsManager::setAutoBrightness(bool enabled) { void SettingsManager::setStatusBarClockPosition(const QString &position) { if (m_statusBarClockPosition == position) return; - // Validate input + if (position != "left" && position != "center" && position != "right") { qWarning() << "[SettingsManager] Invalid clock position:" << position << "- using 'center'"; return; @@ -525,7 +503,6 @@ void SettingsManager::setStatusBarClockPosition(const QString &position) { qDebug() << "[SettingsManager] Status bar clock position changed to" << position; } -// Notification setters void SettingsManager::setShowNotificationsOnLockScreen(bool enabled) { if (m_showNotificationsOnLockScreen == enabled) return; @@ -534,7 +511,6 @@ void SettingsManager::setShowNotificationsOnLockScreen(bool enabled) { emit showNotificationsOnLockScreenChanged(); } -// OOBE setters void SettingsManager::setFirstRunComplete(bool complete) { if (m_firstRunComplete == complete) return; @@ -562,7 +538,6 @@ void SettingsManager::setQuickSettingsTileOrder(const QStringList &order) { qDebug() << "[SettingsManager] Quick Settings tile order changed:" << order; } -// Keyboard setters void SettingsManager::setKeyboardAutoCorrection(bool enabled) { if (m_keyboardAutoCorrection == enabled) return; @@ -615,7 +590,6 @@ void SettingsManager::setKeyboardLanguage(const QString &language) { emit keyboardLanguageChanged(); } -// Sound scanning methods QStringList SettingsManager::availableRingtones() { const QString dataDir = dataDirFromEnvOrDefault(); QStringList ringtones; @@ -627,7 +601,6 @@ QStringList SettingsManager::availableRingtones() { return ringtones; } - // Fallback for older builds / minimal installs: qrc list. ringtones = {"qrc:/sounds/resources/sounds/phone/bbpro1.wav", "qrc:/sounds/resources/sounds/phone/bbpro2.wav", "qrc:/sounds/resources/sounds/phone/bbpro3.wav", @@ -661,8 +634,7 @@ QStringList SettingsManager::availableNotificationSounds() { return sounds; } - sounds = {// Text sounds - "qrc:/sounds/resources/sounds/text/bikebell.wav", + sounds = {"qrc:/sounds/resources/sounds/text/bikebell.wav", "qrc:/sounds/resources/sounds/text/brief.wav", "qrc:/sounds/resources/sounds/text/caffeine.wav", "qrc:/sounds/resources/sounds/text/chigong.wav", @@ -673,7 +645,7 @@ QStringList SettingsManager::availableNotificationSounds() { "qrc:/sounds/resources/sounds/text/pure.wav", "qrc:/sounds/resources/sounds/text/tight.wav", "qrc:/sounds/resources/sounds/text/ufo.wav", - // Message sounds + "qrc:/sounds/resources/sounds/messages/bright.wav", "qrc:/sounds/resources/sounds/messages/confident.wav", "qrc:/sounds/resources/sounds/messages/contentment.wav", @@ -722,7 +694,7 @@ int SettingsManager::screenTimeoutValue(const QString &option) { return 300000; if (option == "Never") return 0; - return 120000; // Default to 2 minutes + return 120000; } void SettingsManager::setFilterMobileFriendlyApps(bool enabled) { @@ -822,24 +794,21 @@ void SettingsManager::setDefaultApps(const QVariantMap &apps) { } QString SettingsManager::formatSoundName(const QString &path) { - // Extract filename from URL/path + QString p = path; const QUrl url(path); if (url.isValid() && url.isLocalFile()) p = url.toLocalFile(); - // qrc:/... is not a local file; QFileInfo still parses the basename fine. + QFileInfo info(p); QString baseName = info.baseName(); - // Remove prefixes like "alarm_" baseName.remove("alarm_"); baseName.remove("ring_"); - // Replace underscores/hyphens with spaces baseName.replace('_', ' '); baseName.replace('-', ' '); - // Capitalize first letter of each word QStringList words = baseName.split(' '); for (int i = 0; i < words.size(); ++i) { if (!words[i].isEmpty()) { diff --git a/shell/src/settingsmanager.h b/shell/src/managers/settingsmanager.h similarity index 70% rename from shell/src/settingsmanager.h rename to shell/src/managers/settingsmanager.h index 3eb5ae255..1ceabd957 100644 --- a/shell/src/settingsmanager.h +++ b/shell/src/managers/settingsmanager.h @@ -8,13 +8,11 @@ class SettingsManager : public QObject { Q_OBJECT - // Existing properties Q_PROPERTY(qreal userScaleFactor READ userScaleFactor WRITE setUserScaleFactor NOTIFY userScaleFactorChanged) Q_PROPERTY( QString wallpaperPath READ wallpaperPath WRITE setWallpaperPath NOTIFY wallpaperPathChanged) - // Migrated from QML SettingsManager Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged) Q_PROPERTY(bool autoLock READ autoLock WRITE setAutoLock NOTIFY autoLockChanged) Q_PROPERTY(int autoLockTimeout READ autoLockTimeout WRITE setAutoLockTimeout NOTIFY @@ -24,7 +22,6 @@ class SettingsManager : public QObject { Q_PROPERTY(QString timeFormat READ timeFormat WRITE setTimeFormat NOTIFY timeFormatChanged) Q_PROPERTY(QString dateFormat READ dateFormat WRITE setDateFormat NOTIFY dateFormatChanged) - // Audio properties Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged) Q_PROPERTY(QString notificationSound READ notificationSound WRITE setNotificationSound NOTIFY notificationSoundChanged) @@ -43,7 +40,6 @@ class SettingsManager : public QObject { Q_PROPERTY( QString audioProfile READ audioProfile WRITE setAudioProfile NOTIFY audioProfileChanged) - // Display properties Q_PROPERTY( int screenTimeout READ screenTimeout WRITE setScreenTimeout NOTIFY screenTimeoutChanged) Q_PROPERTY(bool autoBrightness READ autoBrightness WRITE setAutoBrightness NOTIFY @@ -51,11 +47,9 @@ class SettingsManager : public QObject { Q_PROPERTY(QString statusBarClockPosition READ statusBarClockPosition WRITE setStatusBarClockPosition NOTIFY statusBarClockPositionChanged) - // Notification properties Q_PROPERTY(bool showNotificationsOnLockScreen READ showNotificationsOnLockScreen WRITE setShowNotificationsOnLockScreen NOTIFY showNotificationsOnLockScreenChanged) - // App management properties Q_PROPERTY(bool filterMobileFriendlyApps READ filterMobileFriendlyApps WRITE setFilterMobileFriendlyApps NOTIFY filterMobileFriendlyAppsChanged) Q_PROPERTY(QStringList hiddenApps READ hiddenApps WRITE setHiddenApps NOTIFY hiddenAppsChanged) @@ -72,17 +66,14 @@ class SettingsManager : public QObject { Q_PROPERTY( QVariantMap defaultApps READ defaultApps WRITE setDefaultApps NOTIFY defaultAppsChanged) - // OOBE properties Q_PROPERTY(bool firstRunComplete READ firstRunComplete WRITE setFirstRunComplete NOTIFY firstRunCompleteChanged) - // Quick Settings customization Q_PROPERTY(QStringList enabledQuickSettingsTiles READ enabledQuickSettingsTiles WRITE setEnabledQuickSettingsTiles NOTIFY enabledQuickSettingsTilesChanged) Q_PROPERTY(QStringList quickSettingsTileOrder READ quickSettingsTileOrder WRITE setQuickSettingsTileOrder NOTIFY quickSettingsTileOrderChanged) - // Keyboard settings Q_PROPERTY(bool keyboardAutoCorrection READ keyboardAutoCorrection WRITE setKeyboardAutoCorrection NOTIFY keyboardAutoCorrectionChanged) Q_PROPERTY(bool keyboardPredictiveText READ keyboardPredictiveText WRITE @@ -100,7 +91,6 @@ class SettingsManager : public QObject { explicit SettingsManager(QObject *parent = nullptr); ~SettingsManager() override = default; - // Existing getters qreal userScaleFactor() const { return m_userScaleFactor; } @@ -108,7 +98,6 @@ class SettingsManager : public QObject { return m_wallpaperPath; } - // Migrated getters QString deviceName() const { return m_deviceName; } @@ -128,7 +117,6 @@ class SettingsManager : public QObject { return m_dateFormat; } - // Audio getters QString ringtone() const { return m_ringtone; } @@ -163,7 +151,6 @@ class SettingsManager : public QObject { return m_audioProfile; } - // Display getters int screenTimeout() const { return m_screenTimeout; } @@ -174,12 +161,10 @@ class SettingsManager : public QObject { return m_statusBarClockPosition; } - // Notification getters bool showNotificationsOnLockScreen() const { return m_showNotificationsOnLockScreen; } - // App management getters bool filterMobileFriendlyApps() const { return m_filterMobileFriendlyApps; } @@ -205,12 +190,10 @@ class SettingsManager : public QObject { return m_defaultApps; } - // OOBE getters bool firstRunComplete() const { return m_firstRunComplete; } - // Quick Settings getters QStringList enabledQuickSettingsTiles() const { return m_enabledQuickSettingsTiles; } @@ -218,7 +201,6 @@ class SettingsManager : public QObject { return m_quickSettingsTileOrder; } - // Keyboard getters bool keyboardAutoCorrection() const { return m_keyboardAutoCorrection; } @@ -238,86 +220,73 @@ class SettingsManager : public QObject { return m_keyboardLanguage; } - // Existing setters - void setUserScaleFactor(qreal factor); - void setWallpaperPath(const QString &path); - - // Migrated setters - void setDeviceName(const QString &name); - void setAutoLock(bool enabled); - void setAutoLockTimeout(int seconds); - void setShowNotificationPreviews(bool show); - void setTimeFormat(const QString &format); - void setDateFormat(const QString &format); - - // Audio setters - void setRingtone(const QString &path); - void setNotificationSound(const QString &path); - void setAlarmSound(const QString &path); - void setMediaVolume(qreal volume); - void setRingtoneVolume(qreal volume); - void setAlarmVolume(qreal volume); - void setNotificationVolume(qreal volume); - void setSystemVolume(qreal volume); - void setDndEnabled(bool enabled); - void setVibrationEnabled(bool enabled); - void setAudioProfile(const QString &profile); - - // Display setters - void setScreenTimeout(int ms); - void setAutoBrightness(bool enabled); - void setStatusBarClockPosition(const QString &position); - - // Notification setters - void setShowNotificationsOnLockScreen(bool enabled); - - // App management setters - void setFilterMobileFriendlyApps(bool enabled); - void setHiddenApps(const QStringList &apps); - void setAppSortOrder(const QString &order); - void setAppGridColumns(int columns); - void setSearchNativeApps(bool enabled); - void setShowNotificationBadges(bool enabled); - void setShowFrequentApps(bool enabled); - void setDefaultApps(const QVariantMap &apps); - - // OOBE setters - void setFirstRunComplete(bool complete); - - // Quick Settings setters - void setEnabledQuickSettingsTiles(const QStringList &tiles); - void setQuickSettingsTileOrder(const QStringList &order); - - // Keyboard setters - void setKeyboardAutoCorrection(bool enabled); - void setKeyboardPredictiveText(bool enabled); - void setKeyboardWordFling(bool enabled); - void setKeyboardPredictiveSpacing(bool enabled); - void setKeyboardHapticStrength(const QString &strength); - void setKeyboardLanguage(const QString &language); - - // Invokable methods for sound lists + void setUserScaleFactor(qreal factor); + void setWallpaperPath(const QString &path); + + void setDeviceName(const QString &name); + void setAutoLock(bool enabled); + void setAutoLockTimeout(int seconds); + void setShowNotificationPreviews(bool show); + void setTimeFormat(const QString &format); + void setDateFormat(const QString &format); + + void setRingtone(const QString &path); + void setNotificationSound(const QString &path); + void setAlarmSound(const QString &path); + void setMediaVolume(qreal volume); + void setRingtoneVolume(qreal volume); + void setAlarmVolume(qreal volume); + void setNotificationVolume(qreal volume); + void setSystemVolume(qreal volume); + void setDndEnabled(bool enabled); + void setVibrationEnabled(bool enabled); + void setAudioProfile(const QString &profile); + + void setScreenTimeout(int ms); + void setAutoBrightness(bool enabled); + void setStatusBarClockPosition(const QString &position); + + void setShowNotificationsOnLockScreen(bool enabled); + + void setFilterMobileFriendlyApps(bool enabled); + void setHiddenApps(const QStringList &apps); + void setAppSortOrder(const QString &order); + void setAppGridColumns(int columns); + void setSearchNativeApps(bool enabled); + void setShowNotificationBadges(bool enabled); + void setShowFrequentApps(bool enabled); + void setDefaultApps(const QVariantMap &apps); + + void setFirstRunComplete(bool complete); + + void setEnabledQuickSettingsTiles(const QStringList &tiles); + void setQuickSettingsTileOrder(const QStringList &order); + + void setKeyboardAutoCorrection(bool enabled); + void setKeyboardPredictiveText(bool enabled); + void setKeyboardWordFling(bool enabled); + void setKeyboardPredictiveSpacing(bool enabled); + void setKeyboardHapticStrength(const QString &strength); + void setKeyboardLanguage(const QString &language); + Q_INVOKABLE QStringList availableRingtones(); Q_INVOKABLE QStringList availableNotificationSounds(); Q_INVOKABLE QStringList availableAlarmSounds(); Q_INVOKABLE QStringList screenTimeoutOptions(); Q_INVOKABLE int screenTimeoutValue(const QString &option); Q_INVOKABLE QString formatSoundName(const QString &path); - // Shared assets (Option C): resolve `wallpapers/foo.jpg` or `sounds/phone/bar.wav` to a URL. - // Prefers filesystem assets under MARATHON_DATA_DIR, falls back to qrc for compatibility. - Q_INVOKABLE QString assetUrl(const QString &relativePath) const; - // Existing invokables - Q_INVOKABLE QVariant get(const QString &key, const QVariant &defaultValue = QVariant()); - Q_INVOKABLE void set(const QString &key, const QVariant &value); - Q_INVOKABLE void sync(); + Q_INVOKABLE QString assetUrl(const QString &relativePath) const; + + Q_INVOKABLE QVariant get(const QString &key, const QVariant &defaultValue = QVariant()); + Q_INVOKABLE void set(const QString &key, const QVariant &value); + Q_INVOKABLE void sync(); signals: - // Existing signals + void userScaleFactorChanged(); void wallpaperPathChanged(); - // Migrated signals void deviceNameChanged(); void autoLockChanged(); void autoLockTimeoutChanged(); @@ -325,7 +294,6 @@ class SettingsManager : public QObject { void timeFormatChanged(); void dateFormatChanged(); - // Audio signals void ringtoneChanged(); void notificationSoundChanged(); void alarmSoundChanged(); @@ -338,15 +306,12 @@ class SettingsManager : public QObject { void vibrationEnabledChanged(); void audioProfileChanged(); - // Display signals void screenTimeoutChanged(); void autoBrightnessChanged(); void statusBarClockPositionChanged(); - // Notification signals void showNotificationsOnLockScreenChanged(); - // App management signals void filterMobileFriendlyAppsChanged(); void hiddenAppsChanged(); void appSortOrderChanged(); @@ -356,14 +321,11 @@ class SettingsManager : public QObject { void showFrequentAppsChanged(); void defaultAppsChanged(); - // OOBE signals void firstRunCompleteChanged(); - // Quick Settings signals void enabledQuickSettingsTilesChanged(); void quickSettingsTileOrderChanged(); - // Keyboard signals void keyboardAutoCorrectionChanged(); void keyboardPredictiveTextChanged(); void keyboardWordFlingChanged(); @@ -372,45 +334,39 @@ class SettingsManager : public QObject { void keyboardLanguageChanged(); private: - void load(); - void save(); - - QSettings m_settings; - - // Existing members - qreal m_userScaleFactor; - QString m_wallpaperPath; - - // Migrated members - QString m_deviceName; - bool m_autoLock; - int m_autoLockTimeout; - bool m_showNotificationPreviews; - QString m_timeFormat; - QString m_dateFormat; - - // Audio members - QString m_ringtone; - QString m_notificationSound; - QString m_alarmSound; - qreal m_mediaVolume; - qreal m_ringtoneVolume; - qreal m_alarmVolume; - qreal m_notificationVolume; - qreal m_systemVolume; - bool m_dndEnabled; - bool m_vibrationEnabled; - QString m_audioProfile; - - // Display members - int m_screenTimeout; - bool m_autoBrightness; - QString m_statusBarClockPosition; - - // Notification members - bool m_showNotificationsOnLockScreen; - - // App management members + void load(); + void save(); + + QSettings m_settings; + + qreal m_userScaleFactor; + QString m_wallpaperPath; + + QString m_deviceName; + bool m_autoLock; + int m_autoLockTimeout; + bool m_showNotificationPreviews; + QString m_timeFormat; + QString m_dateFormat; + + QString m_ringtone; + QString m_notificationSound; + QString m_alarmSound; + qreal m_mediaVolume; + qreal m_ringtoneVolume; + qreal m_alarmVolume; + qreal m_notificationVolume; + qreal m_systemVolume; + bool m_dndEnabled; + bool m_vibrationEnabled; + QString m_audioProfile; + + int m_screenTimeout; + bool m_autoBrightness; + QString m_statusBarClockPosition; + + bool m_showNotificationsOnLockScreen; + bool m_filterMobileFriendlyApps; QStringList m_hiddenApps; QString m_appSortOrder; @@ -420,18 +376,15 @@ class SettingsManager : public QObject { bool m_showFrequentApps; QVariantMap m_defaultApps; - // OOBE members - bool m_firstRunComplete; + bool m_firstRunComplete; - // Quick Settings members QStringList m_enabledQuickSettingsTiles; QStringList m_quickSettingsTileOrder; - // Keyboard members - bool m_keyboardAutoCorrection; - bool m_keyboardPredictiveText; - bool m_keyboardWordFling; - bool m_keyboardPredictiveSpacing; - QString m_keyboardHapticStrength; - QString m_keyboardLanguage; + bool m_keyboardAutoCorrection; + bool m_keyboardPredictiveText; + bool m_keyboardWordFling; + bool m_keyboardPredictiveSpacing; + QString m_keyboardHapticStrength; + QString m_keyboardLanguage; }; diff --git a/shell/src/marathonpermissionmanager.cpp b/shell/src/marathonpermissionmanager.cpp index f48d1400e..0d04fcfb8 100644 --- a/shell/src/marathonpermissionmanager.cpp +++ b/shell/src/marathonpermissionmanager.cpp @@ -3,10 +3,11 @@ #include #include #include +#include #include #include -#include #include +#include MarathonPermissionManager::MarathonPermissionManager(QObject *parent) : QObject(parent) @@ -28,10 +29,14 @@ MarathonPermissionManager::MarathonPermissionManager(QObject *parent) loadPermissions(); - // Initialize Portal Manager m_portalManager = new PortalManager(this); + m_batchTimer = new QTimer(this); + m_batchTimer->setSingleShot(true); + m_batchTimer->setInterval(150); // 150ms debounce window + + connect(m_batchTimer, &QTimer::timeout, this, + &MarathonPermissionManager::processPendingRequests); - // Connect Portal signals connect(m_portalManager, &PortalManager::cameraAccessGranted, this, [this](const QString &appId) { setPermission(appId, "camera", true, true); }); connect(m_portalManager, &PortalManager::cameraAccessDenied, this, @@ -85,7 +90,6 @@ void MarathonPermissionManager::loadPermissions() { QJsonObject root = doc.object(); - // Load permissions for each app for (const QString &appId : root.keys()) { QJsonObject appPerms = root.value(appId).toObject(); QMap permissions; @@ -105,7 +109,6 @@ void MarathonPermissionManager::savePermissions() { QString filePath = getPermissionsFilePath(); QJsonObject root; - // Save permissions for each app for (auto it = m_permissions.constBegin(); it != m_permissions.constEnd(); ++it) { QString appId = it.key(); QJsonObject appPerms; @@ -143,10 +146,16 @@ bool MarathonPermissionManager::hasPermission(const QString &appId, const QStrin return appPerms[permission]; } +void MarathonPermissionManager::requestPermissions(const QString &appId, + const QStringList &permissions) { + for (const QString &perm : permissions) { + requestPermission(appId, perm); + } +} + void MarathonPermissionManager::requestPermission(const QString &appId, const QString &permission) { qDebug() << "[MarathonPermissionManager] Permission requested:" << appId << permission; - // Check if already granted or denied PermissionStatus status = getPermissionStatus(appId, permission); if (status == Granted) { @@ -161,7 +170,6 @@ void MarathonPermissionManager::requestPermission(const QString &appId, const QS return; } - // Try to use XDG Portal if available if (m_portalManager->isPortalAvailable(permission)) { qInfo() << "[MarathonPermissionManager] Delegating request to XDG Portal for" << permission; @@ -172,49 +180,109 @@ void MarathonPermissionManager::requestPermission(const QString &appId, const QS m_portalManager->requestLocationAccess(appId); return; } - // Fall through for other permissions not yet supported by our PortalManager wrapper } - // Fallback to custom dialog - m_promptActive = true; - m_currentAppId = appId; - m_currentPermission = permission; + // Buffer the request + m_pendingRequests.append({appId, permission}); + + // Reset debounce timer + m_batchTimer->start(); +} + +void MarathonPermissionManager::processPendingRequests() { + if (m_pendingRequests.isEmpty()) + return; + + // If a prompt is already active, do nothing (wait for it to close) + if (m_promptActive) + return; + + // Process requests for the FIRST app in the queue + QString appId = m_pendingRequests.first().appId; + + QStringList batch; + QMutableListIterator i(m_pendingRequests); + while (i.hasNext()) { + const PendingRequest &req = i.next(); + if (req.appId == appId) { + if (!batch.contains(req.permission)) { + batch.append(req.permission); + } + i.remove(); + } + } + + if (batch.isEmpty()) { + checkQueue(); // Should normally not happen + return; + } + + qDebug() << "[MarathonPermissionManager] Batching" << batch.size() << "permissions for" + << appId; + + m_promptActive = true; + m_currentAppId = appId; + m_currentPermissions = batch; + m_currentPermission = batch.first(); // Legacy compatibility, though UI should switch to list emit promptActiveChanged(); emit currentRequestChanged(); - emit permissionRequested(appId, permission); - qDebug() << "[MarathonPermissionManager] Showing custom permission prompt"; + // Still emit individual signals for compatibility if needed, but primarily the batch + // signal + emit permissionsRequested(appId, batch); + + qDebug() << "[MarathonPermissionManager] Showing custom permission batch prompt"; +} + +void MarathonPermissionManager::checkQueue() { + if (!m_pendingRequests.isEmpty() && !m_promptActive) { + // Schedule next processing + QTimer::singleShot(50, this, &MarathonPermissionManager::processPendingRequests); + } } void MarathonPermissionManager::setPermission(const QString &appId, const QString &permission, bool granted, bool remember) { - qDebug() << "[MarathonPermissionManager] Setting permission:" << appId << permission << granted - << remember; + setPermissions(appId, {permission}, granted, remember); +} + +void MarathonPermissionManager::setPermissions(const QString &appId, const QStringList &permissions, + bool granted, bool remember) { + qDebug() << "[MarathonPermissionManager] Setting permissions:" << appId << permissions + << granted << remember; if (remember) { - // Store permanently if (!m_permissions.contains(appId)) { m_permissions[appId] = QMap(); } - m_permissions[appId][permission] = granted; + for (const QString &perm : permissions) { + m_permissions[appId][perm] = granted; + } savePermissions(); } - // Hide prompt + // Close prompt m_promptActive = false; m_currentAppId.clear(); + m_currentPermissions.clear(); m_currentPermission.clear(); + emit promptActiveChanged(); emit currentRequestChanged(); - // Emit appropriate signal - if (granted) { - emit permissionGranted(appId, permission); - } else { - emit permissionDenied(appId, permission); + // Emit results for each permission + for (const QString &perm : permissions) { + if (granted) { + emit permissionGranted(appId, perm); + } else { + emit permissionDenied(appId, perm); + } } + + // Check if there are more pending requests + checkQueue(); } void MarathonPermissionManager::revokePermission(const QString &appId, const QString &permission) { @@ -236,7 +304,7 @@ QStringList MarathonPermissionManager::getAppPermissions(const QString &appId) { const QMap &appPerms = m_permissions[appId]; for (auto it = appPerms.constBegin(); it != appPerms.constEnd(); ++it) { - if (it.value()) { // Only include granted permissions + if (it.value()) { permissions.append(it.key()); } } diff --git a/shell/src/marathonpermissionmanager.h b/shell/src/marathonpermissionmanager.h index c5a523312..94c1a3d49 100644 --- a/shell/src/marathonpermissionmanager.h +++ b/shell/src/marathonpermissionmanager.h @@ -11,46 +11,41 @@ class MarathonPermissionManager : public QObject { Q_PROPERTY(bool promptActive READ promptActive NOTIFY promptActiveChanged) Q_PROPERTY(QString currentAppId READ currentAppId NOTIFY currentRequestChanged) Q_PROPERTY(QString currentPermission READ currentPermission NOTIFY currentRequestChanged) + Q_PROPERTY(QStringList currentPermissions READ currentPermissions NOTIFY currentRequestChanged) public: enum PermissionStatus { NotRequested, Granted, Denied, - Prompt // User needs to decide + Prompt }; Q_ENUM(PermissionStatus) explicit MarathonPermissionManager(QObject *parent = nullptr); - // Check if an app has a specific permission Q_INVOKABLE bool hasPermission(const QString &appId, const QString &permission); - // Request permission (shows prompt if not already granted/denied) Q_INVOKABLE void requestPermission(const QString &appId, const QString &permission); + Q_INVOKABLE void requestPermissions(const QString &appId, const QStringList &permissions); - // Set permission (called by UI when user responds) Q_INVOKABLE void setPermission(const QString &appId, const QString &permission, bool granted, bool remember = true); + Q_INVOKABLE void setPermissions(const QString &appId, const QStringList &permissions, + bool granted, bool remember = true); - // Get all permissions for an app - Q_INVOKABLE QStringList getAppPermissions(const QString &appId); + Q_INVOKABLE QStringList getAppPermissions(const QString &appId); - // Revoke a specific permission - Q_INVOKABLE void revokePermission(const QString &appId, const QString &permission); + Q_INVOKABLE void revokePermission(const QString &appId, const QString &permission); - // Get permission status Q_INVOKABLE PermissionStatus getPermissionStatus(const QString &appId, const QString &permission); - // Get list of all available permissions - Q_INVOKABLE QStringList getAvailablePermissions(); + Q_INVOKABLE QStringList getAvailablePermissions(); - // Get human-readable permission description - Q_INVOKABLE QString getPermissionDescription(const QString &permission); + Q_INVOKABLE QString getPermissionDescription(const QString &permission); - // Properties - bool promptActive() const { + bool promptActive() const { return m_promptActive; } QString currentAppId() const { @@ -59,30 +54,43 @@ class MarathonPermissionManager : public QObject { QString currentPermission() const { return m_currentPermission; } + QStringList currentPermissions() const { + return m_currentPermissions; + } signals: void permissionGranted(const QString &appId, const QString &permission); void permissionDenied(const QString &appId, const QString &permission); void permissionRevoked(const QString &appId, const QString &permission); void permissionRequested(const QString &appId, const QString &permission); + void permissionsRequested(const QString &appId, const QStringList &permissions); void promptActiveChanged(); void currentRequestChanged(); + private slots: + void processPendingRequests(); + private: - void loadPermissions(); - void savePermissions(); - QString getPermissionsFilePath(); + void loadPermissions(); + void savePermissions(); + QString getPermissionsFilePath(); + void checkQueue(); - // Stored permissions: appId -> permission -> granted QMap> m_permissions; - // Current permission request - bool m_promptActive; - QString m_currentAppId; - QString m_currentPermission; + bool m_promptActive; + QString m_currentAppId; + QString m_currentPermission; + QStringList m_currentPermissions; - // Permission descriptions - QMap m_permissionDescriptions; + QMap m_permissionDescriptions; - class PortalManager *m_portalManager; + class PortalManager *m_portalManager; + class QTimer *m_batchTimer; + + struct PendingRequest { + QString appId; + QString permission; + }; + QList m_pendingRequests; }; diff --git a/shell/src/appmodel.cpp b/shell/src/models/appmodel.cpp similarity index 90% rename from shell/src/appmodel.cpp rename to shell/src/models/appmodel.cpp index 3692eea50..daf82da48 100644 --- a/shell/src/appmodel.cpp +++ b/shell/src/models/appmodel.cpp @@ -54,13 +54,12 @@ App *AppModel::getAppAtIndex(int index) { void AppModel::addApp(const QString &id, const QString &name, const QString &icon, const QString &type, const QString &exec) { - // Check if app already exists + if (m_appIndex.contains(id)) { qDebug() << "[AppModel] App already exists:" << id; return; } - // Validate inputs if (name.isEmpty()) { qWarning() << "[AppModel] Invalid app: empty name for ID:" << id; return; @@ -71,7 +70,6 @@ void AppModel::addApp(const QString &id, const QString &name, const QString &ico return; } - // Validate type is one of: "native", "marathon", "system" if (type != "native" && type != "marathon" && type != "system") { qWarning() << "[AppModel] Invalid app type:" << type << "for ID:" << id; return; @@ -185,7 +183,7 @@ void AppModel::loadFromRegistry(QObject *registryObj) { qDebug() << "[AppModel] Loading apps from registry..."; QStringList appIds = registry->getAllAppIds(); - appIds.sort(); // Sort alphabetically for consistent ordering + appIds.sort(); for (const QString &appId : appIds) { QVariantMap appInfo = registry->getApp(appId); @@ -194,28 +192,25 @@ void AppModel::loadFromRegistry(QObject *registryObj) { QString icon = appInfo.value("icon").toString(); int typeInt = appInfo.value("type").toInt(); - // Convert type enum to string - QString type = "marathon"; + QString type = "marathon"; if (typeInt == MarathonAppRegistry::Native) { type = "native"; } else if (typeInt == MarathonAppRegistry::System) { type = "marathon"; } - // Convert relative icon path to absolute if needed QString absolutePath = appInfo.value("absolutePath").toString(); if (!icon.isEmpty() && !icon.startsWith("qrc:") && !icon.startsWith("file://")) { if (!icon.startsWith("/")) { icon = absolutePath + "/" + icon; } - // Add file:// prefix for filesystem paths + icon = "file://" + icon; } - // Add or update app if (m_appIndex.contains(id)) { qDebug() << "[AppModel] Updating app from registry:" << id; - // Replace hardcoded placeholder with real app from filesystem + removeApp(id); addApp(id, name, icon, type); } else { @@ -226,12 +221,11 @@ void AppModel::loadFromRegistry(QObject *registryObj) { qDebug() << "[AppModel] Loaded" << appIds.count() << "apps from registry"; - // Remove hardcoded placeholders that don't exist in the filesystem cleanupMissingApps(appIds); } void AppModel::cleanupMissingApps(const QStringList ®istryAppIds) { - // List of hardcoded app IDs that should be removed if not found in registry + QStringList hardcodedAppIds = {"phone", "messages", "browser", "camera", "gallery", "music", "calendar", "clock", "maps", "notes"}; @@ -247,11 +241,9 @@ void AppModel::cleanupMissingApps(const QStringList ®istryAppIds) { void AppModel::sortAppsByName() { qDebug() << "[AppModel] Sorting all apps alphabetically by name..."; - // Sort the apps vector by name (case-insensitive) std::sort(m_apps.begin(), m_apps.end(), [](const App *a, const App *b) { return a->name().toLower() < b->name().toLower(); }); - // Notify the view that the data has changed emit dataChanged(index(0), index(m_apps.count() - 1)); qDebug() << "[AppModel] Sorted" << m_apps.count() << "apps alphabetically"; diff --git a/shell/src/appmodel.h b/shell/src/models/appmodel.h similarity index 97% rename from shell/src/appmodel.h rename to shell/src/models/appmodel.h index e89cd31c4..a32cd5d94 100644 --- a/shell/src/appmodel.h +++ b/shell/src/models/appmodel.h @@ -91,9 +91,9 @@ class AppModel : public QAbstractListModel { private: QVector m_apps; - QHash m_appIndex; // O(1) lookup + QHash m_appIndex; void cleanupMissingApps(const QStringList ®istryAppIds); }; -#endif // APPMODEL_H +#endif diff --git a/shell/src/notificationmodel.cpp b/shell/src/models/notificationmodel.cpp similarity index 94% rename from shell/src/notificationmodel.cpp rename to shell/src/models/notificationmodel.cpp index d15568a63..a9fe5e123 100644 --- a/shell/src/notificationmodel.cpp +++ b/shell/src/models/notificationmodel.cpp @@ -49,11 +49,20 @@ QHash NotificationModel::roleNames() const { return roles; } +int NotificationModel::roleId(const QString &name) const { + const auto roles = roleNames(); + for (auto it = roles.constBegin(); it != roles.constEnd(); ++it) { + if (QString::fromUtf8(it.value()) == name) + return it.key(); + } + return -1; +} + int NotificationModel::addNotification(const QString &appId, const QString &title, const QString &body, const QString &icon) { int id = m_nextId++; - beginInsertRows(QModelIndex(), 0, 0); // Insert at top + beginInsertRows(QModelIndex(), 0, 0); Notification *notification = new Notification(id, appId, title, body, icon, this); m_notifications.prepend(notification); m_notificationIndex[id] = notification; diff --git a/shell/src/notificationmodel.h b/shell/src/models/notificationmodel.h similarity index 97% rename from shell/src/notificationmodel.h rename to shell/src/models/notificationmodel.h index a475aa45c..184e96ff1 100644 --- a/shell/src/notificationmodel.h +++ b/shell/src/models/notificationmodel.h @@ -107,6 +107,7 @@ class NotificationModel : public QAbstractListModel { Q_INVOKABLE void markAsRead(int id); Q_INVOKABLE void dismissAllNotifications(); Q_INVOKABLE Notification *getNotification(int id); + Q_INVOKABLE int roleId(const QString &name) const; void loadFromDatabase(class NotificationDatabase *database); @@ -125,4 +126,4 @@ class NotificationModel : public QAbstractListModel { int m_unreadCount; }; -#endif // NOTIFICATIONMODEL_H +#endif diff --git a/shell/src/taskmodel.cpp b/shell/src/models/taskmodel.cpp similarity index 93% rename from shell/src/taskmodel.cpp rename to shell/src/models/taskmodel.cpp index a86dc6ab7..025435fcf 100644 --- a/shell/src/taskmodel.cpp +++ b/shell/src/models/taskmodel.cpp @@ -55,7 +55,7 @@ QHash TaskModel::roleNames() const { void TaskModel::launchTask(const QString &appId, const QString &appName, const QString &appIcon, const QString &appType, int surfaceId, QObject *waylandSurface) { - // Check if task for this app already exists + if (m_appIndex.contains(appId)) { qDebug() << "[TaskModel] Task already exists for app:" << appId; return; @@ -63,11 +63,10 @@ void TaskModel::launchTask(const QString &appId, const QString &appName, const Q QString taskId = "task_" + QString::number(QDateTime::currentMSecsSinceEpoch()); - // Insert at index 0 (newest first) instead of appending (oldest first) beginInsertRows(QModelIndex(), 0, 0); Task *task = new Task(taskId, appId, appName, appIcon, appType, surfaceId, waylandSurface, this); - m_tasks.prepend(task); // Changed from append() to prepend() + m_tasks.prepend(task); m_taskIndex[taskId] = task; m_appIndex[appId] = task; endInsertRows(); @@ -113,7 +112,7 @@ Task *TaskModel::getTaskByAppId(const QString &appId) { } Task *TaskModel::getTaskBySurfaceId(int surfaceId) { - // Search through all tasks to find one with matching surfaceId + for (Task *task : m_tasks) { if (task && task->surfaceId() == surfaceId) { return task; @@ -131,7 +130,6 @@ void TaskModel::updateTaskSnapshot(const QString &appId, const QImage &snapshot) task->setSnapshot(snapshot); - // Notify model that this task's data changed int index = m_tasks.indexOf(task); if (index >= 0) { QModelIndex modelIndex = createIndex(index, 0); @@ -150,7 +148,6 @@ void TaskModel::updateTaskSurface(const QString &appId, QObject *surface) { task->setWaylandSurface(surface); - // Notify model that this task's data changed int index = m_tasks.indexOf(task); if (index >= 0) { QModelIndex modelIndex = createIndex(index, 0); @@ -166,12 +163,10 @@ void TaskModel::updateTaskNativeInfo(const QString &appId, int surfaceId, QObjec return; } - // Update all native app properties task->setAppType("native"); task->setSurfaceId(surfaceId); task->setWaylandSurface(surface); - // Notify model that relevant data changed int index = m_tasks.indexOf(task); if (index >= 0) { QModelIndex modelIndex = createIndex(index, 0); diff --git a/shell/src/taskmodel.h b/shell/src/models/taskmodel.h similarity index 94% rename from shell/src/taskmodel.h rename to shell/src/models/taskmodel.h index f4c9a9732..8471566e0 100644 --- a/shell/src/taskmodel.h +++ b/shell/src/models/taskmodel.h @@ -101,7 +101,7 @@ class Task : public QObject { QString m_icon; QString m_appType; int m_surfaceId; - QPointer m_waylandSurface; // Use QPointer for safe lifecycle management + QPointer m_waylandSurface; qint64 m_timestamp; QImage m_snapshot; }; @@ -153,8 +153,8 @@ class TaskModel : public QAbstractListModel { private: QVector m_tasks; - QHash m_taskIndex; // task ID -> Task - QHash m_appIndex; // app ID -> Task (for quick lookup) + QHash m_taskIndex; + QHash m_appIndex; }; -#endif // TASKMODEL_H +#endif diff --git a/shell/src/platformcpp.h b/shell/src/platformcpp.h deleted file mode 100644 index 0ad8a9112..000000000 --- a/shell/src/platformcpp.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef PLATFORMCPP_H -#define PLATFORMCPP_H - -#include -#include "platform.h" - -/** - * QML-accessible wrapper for Platform namespace functions - */ -class PlatformCpp : public QObject { - Q_OBJECT - Q_PROPERTY(bool hasHardwareKeyboard READ hasHardwareKeyboard CONSTANT) - - public: - explicit PlatformCpp(QObject *parent = nullptr) - : QObject(parent) {} - - bool hasHardwareKeyboard() const { - return Platform::hasHardwareKeyboard(); - } -}; - -#endif // PLATFORMCPP_H diff --git a/shell/src/rtscheduler.h b/shell/src/rtscheduler.h deleted file mode 100644 index d8d751b01..000000000 --- a/shell/src/rtscheduler.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include - -/** - * @brief Real-Time Thread Scheduler for Marathon Shell - * - * Implements SCHED_FIFO priority scheduling per Marathon OS Technical Spec: - * - Priority 75: Compositor rendering thread - * - Priority 85: Input event handling thread - * - * Requires PREEMPT_RT kernel (Linux 6.12+) and CAP_SYS_NICE capability or - * /etc/security/limits.d/99-marathon.conf configuration. - */ -class RTScheduler : public QObject { - Q_OBJECT - - public: - enum Priority { - InputHandling = 85, // Highest - instant touch response - CompositorRender = 75, // High - smooth UI - DefaultUserRT = 80, // Default for user RT apps - KernelIRQ = 50 // Kernel IRQ handlers - }; - - explicit RTScheduler(QObject *parent = nullptr); - - /** - * @brief Sets real-time priority for the calling thread - * @param priority RT priority (1-99) - * @return true if successful, false otherwise - */ - Q_INVOKABLE bool setRealtimePriority(int priority); - - /** - * @brief Sets real-time priority for a specific thread - * @param thread QThread to set priority for - * @param priority RT priority (1-99) - * @return true if successful, false otherwise - */ - Q_INVOKABLE bool setThreadPriority(QThread *thread, int priority); - - /** - * @brief Checks if PREEMPT_RT kernel is active - * @return true if running on PREEMPT_RT kernel - */ - Q_INVOKABLE bool isRealtimeKernel() const; - - /** - * @brief Checks if process has RT scheduling permissions - * @return true if CAP_SYS_NICE or rtprio limit is set - */ - Q_INVOKABLE bool hasRealtimePermissions() const; - - /** - * @brief Gets current thread's scheduling policy - * @return "SCHED_FIFO", "SCHED_RR", "SCHED_OTHER", etc. - */ - Q_INVOKABLE QString getCurrentPolicy() const; - - /** - * @brief Gets current thread's RT priority - * @return Priority (0 for SCHED_OTHER, 1-99 for RT) - */ - Q_INVOKABLE int getCurrentPriority() const; - - private: - bool m_isRealtimeKernel; - bool m_hasRTPermissions; - - void detectKernelCapabilities(); - bool setThreadSchedParam(int policy, int priority); -}; diff --git a/shell/src/securitymanager.h b/shell/src/securitymanager.h deleted file mode 100644 index a410c6781..000000000 --- a/shell/src/securitymanager.h +++ /dev/null @@ -1,144 +0,0 @@ -#ifndef SECURITYMANAGER_H -#define SECURITYMANAGER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class SecurityManager : public QObject { - Q_OBJECT - Q_PROPERTY(AuthMode authMode READ authMode WRITE setAuthMode NOTIFY authModeChanged) - Q_PROPERTY(bool hasQuickPIN READ hasQuickPIN NOTIFY quickPINChanged) - Q_PROPERTY( - bool fingerprintAvailable READ fingerprintAvailable NOTIFY fingerprintAvailableChanged) - Q_PROPERTY(bool isLockedOut READ isLockedOut NOTIFY lockoutStateChanged) - Q_PROPERTY(int lockoutSecondsRemaining READ lockoutSecondsRemaining NOTIFY lockoutStateChanged) - Q_PROPERTY(int failedAttempts READ failedAttempts NOTIFY failedAttemptsChanged) - - public: - explicit SecurityManager(QObject *parent = nullptr); - ~SecurityManager(); - - enum AuthMode { - SystemPassword, // PAM authentication with system password - QuickPIN, // Optional quick PIN stored in libsecret - Biometric, // Fingerprint only - BiometricPIN // Fingerprint with PIN fallback - }; - Q_ENUM(AuthMode) - - enum BiometricType { - Fingerprint, - FaceRecognition - }; - Q_ENUM(BiometricType) - - // Property getters - AuthMode authMode() const { - return m_authMode; - } - bool hasQuickPIN() const { - return m_hasQuickPIN; - } - bool fingerprintAvailable() const { - return m_fingerprintAvailable; - } - bool isLockedOut() const { - return m_isLockedOut; - } - int lockoutSecondsRemaining() const; - int failedAttempts() const { - return m_failedAttempts; - } - - // Property setters - void setAuthMode(AuthMode mode); - - // Authentication methods - Q_INVOKABLE void authenticatePassword(const QString &password); - Q_INVOKABLE void authenticateQuickPIN(const QString &pin); - Q_INVOKABLE void authenticateBiometric(BiometricType type = Fingerprint); - Q_INVOKABLE void cancelAuthentication(); - - // Quick PIN management (requires system password to set/change) - Q_INVOKABLE void setQuickPIN(const QString &pin, const QString &systemPassword); - Q_INVOKABLE void removeQuickPIN(const QString &systemPassword); - - // Biometric enrollment check - Q_INVOKABLE bool isBiometricEnrolled(BiometricType type) const; - - // Lockout management - Q_INVOKABLE void resetLockout(); // Admin/recovery function - - // Utility methods - Q_INVOKABLE QString getCurrentUsername() const; - - signals: - void authenticationSuccess(); - void authenticationFailed(const QString &reason); - void authModeChanged(); - void quickPINChanged(); - void fingerprintAvailableChanged(); - void lockoutStateChanged(); - void failedAttemptsChanged(); - void biometricPrompt(const QString &message); // "Place your finger", etc. - - private slots: - void onFingerprintVerifyStatus(const QString &result, bool done); - void checkLockoutTimer(); - void onPasswordAuthFinished(); - void onQuickPINAuthFinished(); - - private: - bool authenticateViaPAM(const QString &password); - static int pamConversationCallback(int num_msg, const struct pam_message **msg, - struct pam_response **resp, void *appdata_ptr); - - bool verifyQuickPIN(const QString &pin); - bool storeQuickPIN(const QString &pin); - QString retrieveQuickPIN(); - void clearQuickPIN(); - - static QByteArray generateSalt(int length); - static QByteArray hashWithIterations(const QByteArray &data, int iterations); - - void initFingerprintDevice(); - void startFingerprintAuth(); - void stopFingerprintAuth(); - void checkFingerprintEnrollment(); - - void updateLockoutStatus(); - void recordFailedAttempt(); - void resetFailedAttempts(); - int queryFaillockAttempts(); - - // Members - AuthMode m_authMode; - bool m_hasQuickPIN; - bool m_fingerprintAvailable; - bool m_isLockedOut; - int m_failedAttempts; - QDateTime m_lockoutUntil; - QTimer *m_lockoutTimer; - - // fprintd D-Bus - QDBusInterface *m_fprintdManager; - QDBusInterface *m_fprintdDevice; - bool m_fprintdAuthInProgress; - - // PAM conversation data - QString m_currentPassword; - - // Async authentication watchers - QFutureWatcher *m_passwordAuthWatcher; - QFutureWatcher *m_quickPINAuthWatcher; -}; - -#endif // SECURITYMANAGER_H diff --git a/shell/src/applaunchservice.cpp b/shell/src/services/applaunchservice.cpp similarity index 99% rename from shell/src/applaunchservice.cpp rename to shell/src/services/applaunchservice.cpp index 67514ff6f..560062c5a 100644 --- a/shell/src/applaunchservice.cpp +++ b/shell/src/services/applaunchservice.cpp @@ -266,8 +266,7 @@ bool AppLaunchService::launchNativeApp(const QVariantMap &app, QObject *composit return true; } -bool AppLaunchService::launchMarathonApp(const QVariantMap &app, QObject * /*compositorRef*/, - QObject *appWindowRef) { +bool AppLaunchService::launchMarathonApp(const QVariantMap &app, QObject *, QObject *appWindowRef) { const QString appId = app.value("id").toString(); const QString name = app.value("name").toString(); const QString icon = app.value("icon").toString(); diff --git a/shell/src/applaunchservice.h b/shell/src/services/applaunchservice.h similarity index 65% rename from shell/src/applaunchservice.h rename to shell/src/services/applaunchservice.h index 92870b281..0068d0ccb 100644 --- a/shell/src/applaunchservice.h +++ b/shell/src/services/applaunchservice.h @@ -14,16 +14,6 @@ class QWaylandSurface; class QWaylandXdgSurface; #endif -/** - * C++ replacement for `shell/qml/services/AppLaunchService.qml`. - * - * Keeps launch state and app lookup in C++ (perf/maintainability), while still - * interoperating with QML singletons for UI state via QObject pointers set from QML: - * - uiStore: UIStore singleton (openApp/restoreApp) - * - appLifecycleManager: AppLifecycleManager singleton (isAppRunning/bringToForeground) - * - * QML sets these from `MarathonShell.qml` after startup. - */ class AppLaunchService : public QObject { Q_OBJECT @@ -79,7 +69,7 @@ class AppLaunchService : public QObject { QString appId; QString name; QString icon; - QString command; // the exact string passed into WaylandCompositor.launchApp() + QString command; qint64 pid = -1; }; @@ -98,34 +88,29 @@ class AppLaunchService : public QObject { QWaylandXdgSurface *xdgSurface); #endif - QPointer m_appModel; - QPointer m_taskModel; + QPointer m_appModel; + QPointer m_taskModel; - QPointer m_compositor; - QPointer m_appWindow; - QPointer m_uiStore; - QPointer m_appLifecycleManager; + QPointer m_compositor; + QPointer m_appWindow; + QPointer m_uiStore; + QPointer m_appLifecycleManager; - QSet m_launchingApps; + QSet m_launchingApps; - // Pending launches keyed by the command string passed to launchApp(). QHash m_pendingByCommand; - // Active launches keyed by pid (for surface correlation + cleanup). - QHash m_activeByPid; + + QHash m_activeByPid; public: - // Used by IPC layer to enforce strict isolation: DBus sender PID → appId. Q_INVOKABLE QString appIdForPid(qint64 pid) const; - // Fallback for strict IPC: used when an app calls DBus before we observed its pid via compositor - // signals. Only the shell may register mappings. - void registerPidForAppId(qint64 pid, const QString &appId); - // Used by lifecycle routing: determine if an appId is a Marathon app (vs native desktop app). - Q_INVOKABLE bool isMarathonAppId(const QString &appId) const; + void registerPidForAppId(qint64 pid, const QString &appId); + + Q_INVOKABLE bool isMarathonAppId(const QString &appId) const; - // Used by shell UI gestures: send lifecycle navigation events to the isolated app runner. - Q_INVOKABLE bool sendBackToRunner(const QString &appId); - Q_INVOKABLE bool sendForwardToRunner(const QString &appId); + Q_INVOKABLE bool sendBackToRunner(const QString &appId); + Q_INVOKABLE bool sendForwardToRunner(const QString &appId); private: QHash m_pidToAppId; diff --git a/shell/src/applifecyclemanager.cpp b/shell/src/services/applifecyclemanager.cpp similarity index 89% rename from shell/src/applifecyclemanager.cpp rename to shell/src/services/applifecyclemanager.cpp index 532b0fd48..57747f88a 100644 --- a/shell/src/applifecyclemanager.cpp +++ b/shell/src/services/applifecyclemanager.cpp @@ -26,7 +26,6 @@ void AppLifecycleManager::registerApp(const QString &appId, QObject *appInstance m_appStates.insert(appId, st); } - // If this app was queued to be foregrounded, do it now. if (m_pendingForegroundApps.contains(appId)) { m_pendingForegroundApps.removeAll(appId); bringToForeground(appId); @@ -39,7 +38,6 @@ void AppLifecycleManager::unregisterApp(const QString &appId) { if (appId.isEmpty()) return; - // Clean up task when app unregistered if (m_taskModel) { if (Task *task = m_taskModel->getTaskByAppId(appId)) { m_taskModel->closeTask(task->id()); @@ -76,7 +74,6 @@ void AppLifecycleManager::bringToForeground(const QString &appId) { if (appId.isEmpty()) return; - // Pause current foreground app if (!m_foregroundAppId.isEmpty() && m_foregroundAppId != appId) { if (QObject *prev = m_appRegistry.value(m_foregroundAppId)) { invokeVoid(prev, "pause"); @@ -103,7 +100,7 @@ void AppLifecycleManager::bringToForeground(const QString &appId) { } ensureTaskExists(appId); } else { - // Defer foreground until registration arrives + if (!m_pendingForegroundApps.contains(appId)) m_pendingForegroundApps.push_back(appId); } @@ -126,17 +123,15 @@ bool AppLifecycleManager::handleSystemBack() { const bool isNative = app->property("isNative").toBool(); if (isNative) { - // Marathon apps run out-of-process but are still rendered as Wayland clients (NativeAppWindow). - // For those, Escape injection is not the right "Back" semantic; instead send a lifecycle - // back event to the runner so it can call MApp.handleBack(). + if (m_appLaunchService && m_appLaunchService->isMarathonAppId(m_foregroundAppId)) { return m_appLaunchService->sendBackToRunner(m_foregroundAppId); } QObject *compositor = m_appLaunchService ? m_appLaunchService->compositor() : nullptr; if (compositor) { - // Escape key - if (invokeInjectKey(compositor, /*Qt.Key_Escape*/ 0x01000000, /*mods*/ 0, true) && - invokeInjectKey(compositor, /*Qt.Key_Escape*/ 0x01000000, /*mods*/ 0, false)) { + + if (invokeInjectKey(compositor, 0x01000000, 0, true) && + invokeInjectKey(compositor, 0x01000000, 0, false)) { return true; } } @@ -163,11 +158,9 @@ bool AppLifecycleManager::handleSystemForward() { } QObject *compositor = m_appLaunchService ? m_appLaunchService->compositor() : nullptr; if (compositor) { - // Alt+Right - if (invokeInjectKey(compositor, /*Qt.Key_Right*/ 0x01000014, - /*Qt.AltModifier*/ 0x08000000, true) && - invokeInjectKey(compositor, /*Qt.Key_Right*/ 0x01000014, - /*Qt.AltModifier*/ 0x08000000, false)) { + + if (invokeInjectKey(compositor, 0x01000014, 0x08000000, true) && + invokeInjectKey(compositor, 0x01000014, 0x08000000, false)) { return true; } } @@ -212,7 +205,6 @@ void AppLifecycleManager::closeApp(const QString &appId, bool skipNativeClose) { if (appId.isEmpty()) return; - // Remove from TaskModel if (m_taskModel) { if (Task *task = m_taskModel->getTaskByAppId(appId)) { m_taskModel->closeTask(task->id()); diff --git a/shell/src/applifecyclemanager.h b/shell/src/services/applifecyclemanager.h similarity index 91% rename from shell/src/applifecyclemanager.h rename to shell/src/services/applifecyclemanager.h index fc18ba25a..6a1c09bac 100644 --- a/shell/src/applifecyclemanager.h +++ b/shell/src/services/applifecyclemanager.h @@ -10,12 +10,6 @@ class TaskModel; class AppLaunchService; -/** - * C++ replacement for `shell/qml/services/AppLifecycleManager.qml`. - * - * Owns app registry/state in C++ and calls into registered QML app instances - * (MApp / NativeAppWindow) for lifecycle transitions. - */ class AppLifecycleManager : public QObject { Q_OBJECT diff --git a/shell/src/marathonappstoreservice.cpp b/shell/src/services/marathonappstoreservice.cpp similarity index 94% rename from shell/src/marathonappstoreservice.cpp rename to shell/src/services/marathonappstoreservice.cpp index bb553b947..d7f76dafd 100644 --- a/shell/src/marathonappstoreservice.cpp +++ b/shell/src/services/marathonappstoreservice.cpp @@ -18,11 +18,9 @@ MarathonAppStoreService::MarathonAppStoreService(MarathonAppInstaller *installer , m_catalogReply(nullptr) , m_catalogLoaded(false) , m_loading(false) - , m_repositoryUrl("https://apps.marathonos.org/catalog.json") // Default repository -{ + , m_repositoryUrl("https://apps.marathonos.org/catalog.json") { qDebug() << "[MarathonAppStoreService] Initialized"; - // Load cached catalog if available loadCachedCatalog(); } @@ -31,7 +29,6 @@ void MarathonAppStoreService::setRepositoryUrl(const QString &url) { m_repositoryUrl = url; emit repositoryUrlChanged(); - // Refresh catalog with new URL refreshCatalog(); } } @@ -159,7 +156,6 @@ void MarathonAppStoreService::handleCatalogReply() { emit catalogLoadedChanged(); emit catalogRefreshed(); - // Save to cache saveCatalogCache(); qDebug() << "[MarathonAppStoreService] Catalog loaded with" << m_catalog.size() << "apps"; @@ -210,7 +206,6 @@ QVariantList MarathonAppStoreService::getFeaturedApps() { } } - // If no apps marked as featured, return first 5 if (featured.isEmpty() && m_catalog.size() > 0) { for (int i = 0; i < qMin(5, m_catalog.size()); ++i) { featured.append(m_catalog.at(i)); @@ -253,18 +248,15 @@ void MarathonAppStoreService::downloadApp(const QString &appId) { qDebug() << "[MarathonAppStoreService] Downloading app:" << appId << "from:" << packageUrl; - // Cancel existing download if any if (m_downloadReplies.contains(appId)) { m_downloadReplies[appId]->abort(); m_downloadReplies[appId]->deleteLater(); m_downloadReplies.remove(appId); } - // Start download QNetworkRequest request{QUrl(packageUrl)}; QNetworkReply *reply = m_networkManager->get(request); - // Store app ID in reply for later use reply->setProperty("appId", appId); connect(reply, &QNetworkReply::downloadProgress, this, @@ -308,7 +300,6 @@ void MarathonAppStoreService::handleDownloadFinished() { return; } - // Save downloaded package QString downloadDir = getDownloadCachePath(); QString packagePath = downloadDir + "/" + appId + ".marathon"; @@ -326,23 +317,20 @@ void MarathonAppStoreService::handleDownloadFinished() { qDebug() << "[MarathonAppStoreService] Download complete:" << appId; emit downloadComplete(appId, packagePath); - // Automatically trigger installation if (m_installer) { m_installer->installFromPackage(packagePath); } } QVariantList MarathonAppStoreService::getAvailableUpdates() { - // TODO: Implement version checking against installed apps + QVariantList updates; return updates; } void MarathonAppStoreService::checkForUpdates() { - // Refresh catalog first + refreshCatalog(); - // TODO: Compare catalog versions with installed apps - // For now, just emit signal with 0 updates emit updatesAvailable(0); } diff --git a/shell/src/marathonappstoreservice.h b/shell/src/services/marathonappstoreservice.h similarity index 90% rename from shell/src/marathonappstoreservice.h rename to shell/src/services/marathonappstoreservice.h index 80e699625..55c90700b 100644 --- a/shell/src/marathonappstoreservice.h +++ b/shell/src/services/marathonappstoreservice.h @@ -20,7 +20,6 @@ class MarathonAppStoreService : public QObject { public: explicit MarathonAppStoreService(MarathonAppInstaller *installer, QObject *parent = nullptr); - // Properties bool catalogLoaded() const { return m_catalogLoaded; } @@ -30,20 +29,17 @@ class MarathonAppStoreService : public QObject { QString repositoryUrl() const { return m_repositoryUrl; } - void setRepositoryUrl(const QString &url); + void setRepositoryUrl(const QString &url); - // Catalog operations Q_INVOKABLE void refreshCatalog(); Q_INVOKABLE QVariantList searchApps(const QString &query = QString()); Q_INVOKABLE QVariantMap getApp(const QString &appId); Q_INVOKABLE QVariantList getFeaturedApps(); Q_INVOKABLE QVariantList getAppsByCategory(const QString &category); - // Download and install - Q_INVOKABLE void downloadApp(const QString &appId); - Q_INVOKABLE void cancelDownload(const QString &appId); + Q_INVOKABLE void downloadApp(const QString &appId); + Q_INVOKABLE void cancelDownload(const QString &appId); - // Updates Q_INVOKABLE QVariantList getAvailableUpdates(); Q_INVOKABLE void checkForUpdates(); diff --git a/shell/src/services/navigationroutercpp.cpp b/shell/src/services/navigationroutercpp.cpp new file mode 100644 index 000000000..b9ffe1cf4 --- /dev/null +++ b/shell/src/services/navigationroutercpp.cpp @@ -0,0 +1,142 @@ +#include "navigationroutercpp.h" + +#include +#include +#include + +NavigationRouterCpp::NavigationRouterCpp(QObject *parent) + : QObject(parent) {} + +bool NavigationRouterCpp::navigate(const QString &uri) { + return navigateInternal(uri, true); +} + +bool NavigationRouterCpp::goBack() { + if (m_history.size() <= 1) + return false; + + m_history.removeLast(); + emit historyChanged(); + + const QVariantMap previous = m_history.last().toMap(); + const QString uri = previous.value("uri").toString(); + return navigateInternal(uri, false); +} + +void NavigationRouterCpp::clearHistory() { + if (m_history.isEmpty()) + return; + + m_history.clear(); + emit historyChanged(); + m_currentApp.clear(); + m_currentPage.clear(); + m_currentParams.clear(); + emit currentAppChanged(); + emit currentPageChanged(); + emit currentParamsChanged(); +} + +bool NavigationRouterCpp::navigateToDeepLink(const QString &appId, const QString &route, + const QVariantMap ¶ms) { + if (appId.isEmpty()) + return false; + + emit deepLinkRequested(appId, route, params); + return true; +} + +bool NavigationRouterCpp::navigateInternal(const QString &uri, bool recordHistory) { + ParsedUri parsed = parseUri(uri); + if (!parsed.valid) { + const QString error = QStringLiteral("Invalid URI format: %1").arg(uri); + emit navigationFailed(uri, error); + return false; + } + + if (recordHistory) { + m_history.append(buildHistoryEntry(uri, parsed)); + emit historyChanged(); + } + + setCurrentState(parsed); + + if (parsed.app == "hub") + return handleHubRoute(parsed); + if (parsed.app == "browser") + return handleBrowserRoute(parsed); + return handleAppRoute(parsed); +} + +NavigationRouterCpp::ParsedUri NavigationRouterCpp::parseUri(const QString &uri) const { + ParsedUri result; + const QUrl url(uri); + if (!url.isValid() || url.scheme() != "marathon") + return result; + + const QStringList parts = url.path().split('/', Qt::SkipEmptyParts); + if (parts.isEmpty()) + return result; + + result.valid = true; + result.app = parts.value(0); + result.page = parts.value(1); + result.subpage = parts.value(2); + + QUrlQuery query(url); + const auto items = query.queryItems(); + for (const auto &item : items) + result.params.insert(item.first, item.second); + + return result; +} + +QVariantMap NavigationRouterCpp::buildHistoryEntry(const QString &uri, + const ParsedUri &parsed) const { + QVariantMap entry; + entry.insert("uri", uri); + entry.insert("app", parsed.app); + entry.insert("page", parsed.page); + entry.insert("params", parsed.params); + entry.insert("timestamp", QDateTime::currentMSecsSinceEpoch()); + return entry; +} + +bool NavigationRouterCpp::handleHubRoute(const ParsedUri &parsed) { + if (parsed.page == "messages") + emit hubTabRequested(0); + else if (parsed.page == "notifications") + emit hubTabRequested(1); + else if (parsed.page == "calendar") + emit hubTabRequested(2); + + emit navigated(QStringLiteral("marathon://hub/%1").arg(parsed.page)); + return true; +} + +bool NavigationRouterCpp::handleBrowserRoute(const ParsedUri &parsed) { + QVariantMap params; + params.insert("url", parsed.params.value("url").toString()); + emit deepLinkRequested("browser", "", params); + emit navigated(QStringLiteral("marathon://browser")); + return true; +} + +bool NavigationRouterCpp::handleAppRoute(const ParsedUri &parsed) { + emit deepLinkRequested(parsed.app, "", QVariantMap()); + emit navigated(QStringLiteral("marathon://%1").arg(parsed.app)); + return true; +} + +void NavigationRouterCpp::setCurrentState(const ParsedUri &parsed) { + if (m_currentApp != parsed.app) { + m_currentApp = parsed.app; + emit currentAppChanged(); + } + if (m_currentPage != parsed.page) { + m_currentPage = parsed.page; + emit currentPageChanged(); + } + m_currentParams = parsed.params; + emit currentParamsChanged(); +} diff --git a/shell/src/services/navigationroutercpp.h b/shell/src/services/navigationroutercpp.h new file mode 100644 index 000000000..0862c9908 --- /dev/null +++ b/shell/src/services/navigationroutercpp.h @@ -0,0 +1,72 @@ +#ifndef NAVIGATIONROUTERCPP_H +#define NAVIGATIONROUTERCPP_H + +#include +#include +#include + +class NavigationRouterCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QString currentApp READ currentApp NOTIFY currentAppChanged) + Q_PROPERTY(QString currentPage READ currentPage NOTIFY currentPageChanged) + Q_PROPERTY(QVariantMap currentParams READ currentParams NOTIFY currentParamsChanged) + Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged) + + public: + explicit NavigationRouterCpp(QObject *parent = nullptr); + + QString currentApp() const { + return m_currentApp; + } + QString currentPage() const { + return m_currentPage; + } + QVariantMap currentParams() const { + return m_currentParams; + } + QVariantList history() const { + return m_history; + } + + Q_INVOKABLE bool navigate(const QString &uri); + Q_INVOKABLE bool goBack(); + Q_INVOKABLE void clearHistory(); + Q_INVOKABLE bool navigateToDeepLink(const QString &appId, const QString &route, + const QVariantMap ¶ms = QVariantMap()); + + signals: + void navigated(const QString &uri); + void navigationFailed(const QString &uri, const QString &error); + void settingsNavigationRequested(const QString &page, const QString &subpage, + const QVariantMap ¶ms); + void hubTabRequested(int tabIndex); + void deepLinkRequested(const QString &appId, const QString &route, const QVariantMap ¶ms); + void currentAppChanged(); + void currentPageChanged(); + void currentParamsChanged(); + void historyChanged(); + + private: + struct ParsedUri { + bool valid = false; + QString app; + QString page; + QString subpage; + QVariantMap params; + }; + + bool navigateInternal(const QString &uri, bool recordHistory); + ParsedUri parseUri(const QString &uri) const; + QVariantMap buildHistoryEntry(const QString &uri, const ParsedUri &parsed) const; + bool handleHubRoute(const ParsedUri &parsed); + bool handleBrowserRoute(const ParsedUri &parsed); + bool handleAppRoute(const ParsedUri &parsed); + void setCurrentState(const ParsedUri &parsed); + + QString m_currentApp; + QString m_currentPage; + QVariantMap m_currentParams; + QVariantList m_history; +}; + +#endif diff --git a/shell/src/services/notificationhandlercpp.cpp b/shell/src/services/notificationhandlercpp.cpp new file mode 100644 index 000000000..2b0f224b3 --- /dev/null +++ b/shell/src/services/notificationhandlercpp.cpp @@ -0,0 +1,90 @@ +#include "notificationhandlercpp.h" + +#include "navigationroutercpp.h" +#include "notificationservicecpp.h" +#include "telephonyservice.h" + +NotificationHandlerCpp::NotificationHandlerCpp(NotificationServiceCpp *notifications, + NavigationRouterCpp *router, + TelephonyService *telephony, QObject *parent) + : QObject(parent) + , m_notifications(notifications) + , m_router(router) + , m_telephony(telephony) {} + +void NotificationHandlerCpp::handleNotificationClick(int id) { + const QVariantMap notification = findNotification(id); + if (notification.isEmpty()) + return; + + navigateToNotificationTarget(notification); + if (m_notifications) + m_notifications->dismissNotification(id); +} + +void NotificationHandlerCpp::handleNotificationAction(int id, const QString &action) { + const QVariantMap notification = findNotification(id); + if (notification.isEmpty()) + return; + + const QString appId = notification.value("appId").toString(); + if (appId == "phone") { + if (action == "call_back") { + const QString number = notification.value("body").toString().replace("From: ", ""); + if (m_telephony) + m_telephony->dial(number); + if (m_router) + m_router->navigateToDeepLink("phone", "", {}); + } else if (action == "message") { + const QString number = notification.value("body").toString().replace("From: ", ""); + if (m_router) + m_router->navigateToDeepLink("messages", "conversation", + QVariantMap{{"number", number}}); + } + return; + } + + if (appId == "messages") { + if (action == "reply") { + if (m_router) + m_router->navigateToDeepLink("messages", "", {}); + } else if (action == "mark_read") { + if (m_notifications) + m_notifications->dismissNotification(id); + } + } +} + +QVariantMap NotificationHandlerCpp::findNotification(int id) const { + if (!m_notifications) + return {}; + + return m_notifications->getNotification(id); +} + +void NotificationHandlerCpp::navigateToNotificationTarget(const QVariantMap ¬ification) { + if (!m_router) + return; + + const QString appId = notification.value("appId").toString(); + const QString idText = notification.value("id").toString(); + if (appId == "phone") { + m_router->navigateToDeepLink(appId, "history", {}); + return; + } + + if (appId == "messages") { + if (idText.startsWith("sms_")) { + const QStringList parts = idText.split("_"); + if (parts.size() >= 2) { + m_router->navigateToDeepLink(appId, "conversation", + QVariantMap{{"number", parts.at(1)}}); + return; + } + } + m_router->navigateToDeepLink(appId, "", {}); + return; + } + + m_router->navigateToDeepLink(appId, "", {}); +} diff --git a/shell/src/services/notificationhandlercpp.h b/shell/src/services/notificationhandlercpp.h new file mode 100644 index 000000000..e15f3133f --- /dev/null +++ b/shell/src/services/notificationhandlercpp.h @@ -0,0 +1,32 @@ +#ifndef NOTIFICATIONHANDLERCPP_H +#define NOTIFICATIONHANDLERCPP_H + +#include +#include +#include + +class NotificationServiceCpp; +class NavigationRouterCpp; +class TelephonyService; + +class NotificationHandlerCpp : public QObject { + Q_OBJECT + + public: + explicit NotificationHandlerCpp(NotificationServiceCpp *notifications, + NavigationRouterCpp *router, TelephonyService *telephony, + QObject *parent = nullptr); + + Q_INVOKABLE void handleNotificationClick(int id); + Q_INVOKABLE void handleNotificationAction(int id, const QString &action); + + private: + QVariantMap findNotification(int id) const; + void navigateToNotificationTarget(const QVariantMap ¬ification); + + QPointer m_notifications; + QPointer m_router; + QPointer m_telephony; +}; + +#endif diff --git a/shell/src/services/notificationservicecpp.cpp b/shell/src/services/notificationservicecpp.cpp new file mode 100644 index 000000000..68e9e73fc --- /dev/null +++ b/shell/src/services/notificationservicecpp.cpp @@ -0,0 +1,293 @@ +#include "notificationservicecpp.h" +#include "notificationmodel.h" +#include "settingsmanager.h" +#include "audiopolicycontroller.h" +#include "hapticmanager.h" + +#include + +NotificationServiceCpp::NotificationServiceCpp(NotificationModel *model, SettingsManager *settings, + AudioPolicyController *audioPolicy, + HapticManager *haptics, QObject *parent) + : QObject(parent) + , m_model(model) + , m_settings(settings) + , m_audioPolicy(audioPolicy) + , m_haptics(haptics) { + if (m_model) { + const int count = m_model->rowCount(); + m_notifications.reserve(count); + for (int row = 0; row < count; ++row) { + const QModelIndex index = m_model->index(row, 0); + QVariantMap map; + map.insert("id", m_model->data(index, NotificationModel::IdRole)); + map.insert("appId", m_model->data(index, NotificationModel::AppIdRole)); + map.insert("title", m_model->data(index, NotificationModel::TitleRole)); + map.insert("body", m_model->data(index, NotificationModel::BodyRole)); + map.insert("icon", m_model->data(index, NotificationModel::IconRole)); + map.insert("timestamp", m_model->data(index, NotificationModel::TimestampRole)); + map.insert("read", m_model->data(index, NotificationModel::IsReadRole)); + map.insert("category", "message"); + map.insert("priority", "normal"); + map.insert("actions", QVariantList()); + map.insert("persistent", false); + m_notifications.append(map); + } + if (!m_notifications.isEmpty()) + emit notificationsChanged(); + connect(m_model, &NotificationModel::notificationAdded, this, + &NotificationServiceCpp::onNotificationAdded); + connect(m_model, &NotificationModel::notificationDismissed, this, + &NotificationServiceCpp::onNotificationDismissed); + connect(m_model, &NotificationModel::unreadCountChanged, this, + &NotificationServiceCpp::onUnreadCountChanged); + } + if (m_settings) { + connect(m_settings, &SettingsManager::dndEnabledChanged, this, + &NotificationServiceCpp::onDndChanged); + } +} + +int NotificationServiceCpp::unreadCount() const { + return m_model ? m_model->unreadCount() : 0; +} + +bool NotificationServiceCpp::isDndEnabled() const { + return m_settings ? m_settings->dndEnabled() : false; +} + +int NotificationServiceCpp::sendNotification(const QString &appId, const QString &title, + const QString &body, const QVariantMap &options) { + if (!m_notificationsEnabled || !m_model) + return -1; + + const QString resolvedAppId = appId.isEmpty() ? QStringLiteral("system") : appId; + const QString icon = options.value("icon").toString(); + const int id = m_model->addNotification(resolvedAppId, title, body, icon); + + QVariantMap meta; + meta.insert("icon", icon); + meta.insert("category", options.value("category", "message")); + meta.insert("priority", options.value("priority", "normal")); + meta.insert("actions", options.value("actions", QVariantList())); + meta.insert("persistent", options.value("persistent", false)); + m_notificationMeta.insert(id, meta); + + return id; +} + +void NotificationServiceCpp::dismissNotification(int id) { + if (!m_model) + return; + + m_model->dismissNotification(id); +} + +void NotificationServiceCpp::dismissAllNotifications() { + if (!m_model) + return; + + m_model->dismissAllNotifications(); + m_notifications.clear(); + m_notificationMeta.clear(); + emit notificationsChanged(); + emit unreadCountChanged(); +} + +void NotificationServiceCpp::markAsRead(int id) { + if (!m_model) + return; + + m_model->markAsRead(id); + for (int i = 0; i < m_notifications.size(); ++i) { + QVariantMap map = m_notifications[i].toMap(); + if (map.value("id").toInt() == id) { + map["read"] = true; + m_notifications[i] = map; + emit notificationsChanged(); + break; + } + } +} + +void NotificationServiceCpp::markAllAsRead() { + if (!m_model) + return; + + for (const QVariant &item : m_notifications) { + const QVariantMap map = item.toMap(); + if (!map.value("read").toBool()) + m_model->markAsRead(map.value("id").toInt()); + } + + for (int i = 0; i < m_notifications.size(); ++i) { + QVariantMap map = m_notifications[i].toMap(); + map["read"] = true; + m_notifications[i] = map; + } + emit notificationsChanged(); + emit unreadCountChanged(); +} + +void NotificationServiceCpp::clearAll() { + QList ids; + ids.reserve(m_notifications.size()); + for (const QVariant &item : m_notifications) + ids.append(item.toMap().value("id").toInt()); + + for (int id : ids) + dismissNotification(id); +} + +void NotificationServiceCpp::clickNotification(int id) { + markAsRead(id); + emit notificationClicked(id); +} + +void NotificationServiceCpp::triggerAction(int id, const QString &action) { + emit notificationActionTriggered(id, action); +} + +QVariantMap NotificationServiceCpp::getNotification(int id) const { + for (const QVariant &item : m_notifications) { + const QVariantMap map = item.toMap(); + if (map.value("id").toInt() == id) + return map; + } + return {}; +} + +QVariantList NotificationServiceCpp::getNotificationsByApp(const QString &appId) const { + QVariantList out; + for (const QVariant &item : m_notifications) { + const QVariantMap map = item.toMap(); + if (map.value("appId").toString() == appId) + out.append(map); + } + return out; +} + +QVariantList NotificationServiceCpp::getUnreadNotifications() const { + QVariantList out; + for (const QVariant &item : m_notifications) { + const QVariantMap map = item.toMap(); + if (!map.value("read").toBool()) + out.append(map); + } + return out; +} + +int NotificationServiceCpp::getNotificationCountForApp(const QString &appId) const { + int count = 0; + for (const QVariant &item : m_notifications) { + const QVariantMap map = item.toMap(); + if (map.value("appId").toString() == appId && !map.value("read").toBool()) + count++; + } + return count; +} + +void NotificationServiceCpp::setNotificationsEnabled(bool enabled) { + if (m_notificationsEnabled == enabled) + return; + m_notificationsEnabled = enabled; + emit notificationsEnabledChanged(); +} + +void NotificationServiceCpp::setSoundEnabled(bool enabled) { + if (m_soundEnabled == enabled) + return; + m_soundEnabled = enabled; + emit soundEnabledChanged(); +} + +void NotificationServiceCpp::setVibrationEnabled(bool enabled) { + if (m_vibrationEnabled == enabled) + return; + m_vibrationEnabled = enabled; + emit vibrationEnabledChanged(); +} + +void NotificationServiceCpp::setLedEnabled(bool enabled) { + if (m_ledEnabled == enabled) + return; + m_ledEnabled = enabled; + emit ledEnabledChanged(); +} + +void NotificationServiceCpp::onNotificationAdded(int id) { + if (!m_model) + return; + + Notification *notif = m_model->getNotification(id); + if (!notif) + return; + + QVariantMap map = buildNotificationMapFromModel(notif); + m_notifications.append(map); + emit notificationsChanged(); + emit notificationReceived(map); + playFeedbackIfNeeded(); +} + +void NotificationServiceCpp::onNotificationDismissed(int id) { + removeNotificationById(id); + emit notificationDismissed(id); +} + +void NotificationServiceCpp::onUnreadCountChanged() { + emit unreadCountChanged(); +} + +void NotificationServiceCpp::onDndChanged() { + emit isDndEnabledChanged(); +} + +QVariantMap +NotificationServiceCpp::buildNotificationMapFromModel(Notification *notification) const { + QVariantMap map; + map.insert("id", notification->id()); + map.insert("appId", notification->appId()); + map.insert("title", notification->title()); + map.insert("body", notification->body()); + map.insert("icon", notification->icon()); + map.insert("timestamp", notification->timestamp()); + map.insert("read", notification->isRead()); + + const QVariantMap meta = m_notificationMeta.value(notification->id()); + if (!meta.isEmpty()) { + map.insert("category", meta.value("category")); + map.insert("priority", meta.value("priority")); + map.insert("actions", meta.value("actions")); + map.insert("persistent", meta.value("persistent")); + } else { + map.insert("category", "message"); + map.insert("priority", "normal"); + map.insert("actions", QVariantList()); + map.insert("persistent", false); + } + return map; +} + +void NotificationServiceCpp::playFeedbackIfNeeded() { + if (isDndEnabled()) + return; + + if (m_soundEnabled && m_audioPolicy) + m_audioPolicy->playNotificationSound(); + + if (m_vibrationEnabled && m_haptics) + m_haptics->pattern({50, 100, 50}); +} + +void NotificationServiceCpp::removeNotificationById(int id) { + for (int i = 0; i < m_notifications.size(); ++i) { + const QVariantMap map = m_notifications[i].toMap(); + if (map.value("id").toInt() == id) { + m_notifications.removeAt(i); + emit notificationsChanged(); + break; + } + } + m_notificationMeta.remove(id); +} diff --git a/shell/src/services/notificationservicecpp.h b/shell/src/services/notificationservicecpp.h new file mode 100644 index 000000000..f8b18ad2f --- /dev/null +++ b/shell/src/services/notificationservicecpp.h @@ -0,0 +1,108 @@ +#ifndef NOTIFICATIONSERVICECPP_H +#define NOTIFICATIONSERVICECPP_H + +#include +#include +#include +#include + +class NotificationModel; +class SettingsManager; +class AudioPolicyController; +class HapticManager; + +class NotificationServiceCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QVariantList notifications READ notifications NOTIFY notificationsChanged) + Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadCountChanged) + Q_PROPERTY(bool notificationsEnabled READ notificationsEnabled WRITE setNotificationsEnabled + NOTIFY notificationsEnabledChanged) + Q_PROPERTY(bool soundEnabled READ soundEnabled WRITE setSoundEnabled NOTIFY soundEnabledChanged) + Q_PROPERTY(bool vibrationEnabled READ vibrationEnabled WRITE setVibrationEnabled NOTIFY + vibrationEnabledChanged) + Q_PROPERTY(bool ledEnabled READ ledEnabled WRITE setLedEnabled NOTIFY ledEnabledChanged) + Q_PROPERTY(bool isDndEnabled READ isDndEnabled NOTIFY isDndEnabledChanged) + + public: + explicit NotificationServiceCpp(NotificationModel *model, SettingsManager *settings, + AudioPolicyController *audioPolicy, HapticManager *haptics, + QObject *parent = nullptr); + + QVariantList notifications() const { + return m_notifications; + } + int unreadCount() const; + bool notificationsEnabled() const { + return m_notificationsEnabled; + } + bool soundEnabled() const { + return m_soundEnabled; + } + bool vibrationEnabled() const { + return m_vibrationEnabled; + } + bool ledEnabled() const { + return m_ledEnabled; + } + bool isDndEnabled() const; + + Q_INVOKABLE int sendNotification(const QString &appId, const QString &title, + const QString &body, + const QVariantMap &options = QVariantMap()); + Q_INVOKABLE void dismissNotification(int id); + Q_INVOKABLE void dismissAllNotifications(); + Q_INVOKABLE void markAsRead(int id); + Q_INVOKABLE void markAllAsRead(); + Q_INVOKABLE void clearAll(); + Q_INVOKABLE void clickNotification(int id); + Q_INVOKABLE void triggerAction(int id, const QString &action); + Q_INVOKABLE QVariantMap getNotification(int id) const; + Q_INVOKABLE QVariantList getNotificationsByApp(const QString &appId) const; + Q_INVOKABLE QVariantList getUnreadNotifications() const; + Q_INVOKABLE int getNotificationCountForApp(const QString &appId) const; + + void setNotificationsEnabled(bool enabled); + void setSoundEnabled(bool enabled); + void setVibrationEnabled(bool enabled); + void setLedEnabled(bool enabled); + + signals: + void notificationsChanged(); + void unreadCountChanged(); + void notificationsEnabledChanged(); + void soundEnabledChanged(); + void vibrationEnabledChanged(); + void ledEnabledChanged(); + void isDndEnabledChanged(); + + void notificationReceived(const QVariantMap ¬ification); + void notificationDismissed(int id); + void notificationClicked(int id); + void notificationActionTriggered(int id, const QString &action); + + private slots: + void onNotificationAdded(int id); + void onNotificationDismissed(int id); + void onUnreadCountChanged(); + void onDndChanged(); + + private: + QVariantMap buildNotificationMapFromModel(class Notification *notification) const; + void playFeedbackIfNeeded(); + void removeNotificationById(int id); + + NotificationModel *m_model = nullptr; + SettingsManager *m_settings = nullptr; + QPointer m_audioPolicy; + QPointer m_haptics; + + QVariantList m_notifications; + QHash m_notificationMeta; + + bool m_notificationsEnabled = true; + bool m_soundEnabled = true; + bool m_vibrationEnabled = true; + bool m_ledEnabled = true; +}; + +#endif diff --git a/shell/src/services/powerbatteryhandlercpp.cpp b/shell/src/services/powerbatteryhandlercpp.cpp new file mode 100644 index 000000000..96cb68d11 --- /dev/null +++ b/shell/src/services/powerbatteryhandlercpp.cpp @@ -0,0 +1,50 @@ +#include "powerbatteryhandlercpp.h" + +#include "displaymanagercpp.h" +#include "displaypolicycontroller.h" +#include "hapticmanager.h" +#include "powerpolicycontroller.h" + +PowerBatteryHandlerCpp::PowerBatteryHandlerCpp(PowerPolicyController *powerPolicy, + DisplayPolicyController *displayPolicy, + DisplayManagerCpp *displayManager, + HapticManager *haptics, QObject *parent) + : QObject(parent) + , m_powerPolicy(powerPolicy) + , m_displayPolicy(displayPolicy) + , m_displayManager(displayManager) + , m_haptics(haptics) {} + +void PowerBatteryHandlerCpp::handlePowerButtonPress(bool sessionLocked, bool screenOnHint) { + if (!m_powerPolicy) + return; + + const bool screenOn = m_displayPolicy ? m_displayPolicy->screenOn() : screenOnHint; + const auto action = m_powerPolicy->powerButtonAction(screenOn, sessionLocked); + + if (action == PowerPolicyController::TurnScreenOn) { + turnScreenOn(); + } else if (action == PowerPolicyController::LockAndTurnScreenOff) { + emit lockRequested(); + turnScreenOff(); + } else if (action == PowerPolicyController::TurnScreenOff) { + turnScreenOff(); + } + + if (m_haptics) + m_haptics->medium(); +} + +void PowerBatteryHandlerCpp::turnScreenOn() { + if (m_displayPolicy) + m_displayPolicy->turnScreenOn(); + else if (m_displayManager) + m_displayManager->setScreenState(true); +} + +void PowerBatteryHandlerCpp::turnScreenOff() { + if (m_displayPolicy) + m_displayPolicy->turnScreenOff(); + else if (m_displayManager) + m_displayManager->setScreenState(false); +} diff --git a/shell/src/services/powerbatteryhandlercpp.h b/shell/src/services/powerbatteryhandlercpp.h new file mode 100644 index 000000000..7bf6f93da --- /dev/null +++ b/shell/src/services/powerbatteryhandlercpp.h @@ -0,0 +1,36 @@ +#ifndef POWERBATTERYHANDLERCPP_H +#define POWERBATTERYHANDLERCPP_H + +#include +#include + +class PowerPolicyController; +class DisplayPolicyController; +class DisplayManagerCpp; +class HapticManager; + +class PowerBatteryHandlerCpp : public QObject { + Q_OBJECT + + public: + explicit PowerBatteryHandlerCpp(PowerPolicyController *powerPolicy, + DisplayPolicyController *displayPolicy, + DisplayManagerCpp *displayManager, HapticManager *haptics, + QObject *parent = nullptr); + + Q_INVOKABLE void handlePowerButtonPress(bool sessionLocked, bool screenOnHint = true); + + signals: + void lockRequested(); + + private: + void turnScreenOn(); + void turnScreenOff(); + + QPointer m_powerPolicy; + QPointer m_displayPolicy; + QPointer m_displayManager; + QPointer m_haptics; +}; + +#endif diff --git a/shell/src/services/screenshotservicecpp.cpp b/shell/src/services/screenshotservicecpp.cpp new file mode 100644 index 000000000..f4bb6db16 --- /dev/null +++ b/shell/src/services/screenshotservicecpp.cpp @@ -0,0 +1,124 @@ +#include "screenshotservicecpp.h" + +#include "audiopolicycontroller.h" +#include "hapticmanager.h" +#include "notificationservicecpp.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +ScreenshotServiceCpp::ScreenshotServiceCpp(AudioPolicyController *audioPolicy, + HapticManager *haptics, + NotificationServiceCpp *notificationService, + QObject *parent) + : QObject(parent) + , m_audioPolicy(audioPolicy) + , m_haptics(haptics) + , m_notificationService(notificationService) { + m_screenshotsPath = resolveScreenshotsPath(); +} + +void ScreenshotServiceCpp::setShellWindow(QObject *window) { + if (m_shellWindow == window) + return; + m_shellWindow = window; + emit shellWindowChanged(); +} + +void ScreenshotServiceCpp::takeScreenshot(QObject *windowItem) { + captureScreen(windowItem); +} + +void ScreenshotServiceCpp::captureScreen(QObject *windowItem) { + QObject *target = windowItem ? windowItem : m_shellWindow; + if (!target) { + notifyFailure("No window available"); + return; + } + + const QString fullPath = buildScreenshotPath(); + + if (auto *window = qobject_cast(target)) { + const QImage image = window->grabWindow(); + if (image.isNull()) { + notifyFailure("Failed to capture image"); + return; + } + if (!image.save(fullPath)) { + notifyFailure("Failed to save file"); + return; + } + handleSavedScreenshot(fullPath); + return; + } + + auto *item = qobject_cast(target); + if (!item) { + notifyFailure("grabToImage not supported"); + return; + } + + QSharedPointer grabResult = item->grabToImage(); + if (!grabResult) { + notifyFailure("Failed to capture image"); + return; + } + + QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, + [this, grabResult, fullPath]() { + if (!grabResult->saveToFile(fullPath)) { + notifyFailure("Failed to save file"); + return; + } + handleSavedScreenshot(fullPath); + }); +} + +QString ScreenshotServiceCpp::resolveScreenshotsPath() const { + const QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString picturesPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + QString path; + if (!picturesPath.isEmpty()) + path = picturesPath + "/Screenshots/"; + else if (!homePath.isEmpty()) + path = homePath + "/Pictures/Screenshots/"; + else + path = "/tmp/Screenshots/"; + + QDir dir(path); + dir.mkpath("."); + return path; +} + +QString ScreenshotServiceCpp::buildScreenshotPath() const { + const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss"); + const QString filename = "Screenshot_" + timestamp + ".png"; + return m_screenshotsPath + filename; +} + +void ScreenshotServiceCpp::handleSavedScreenshot(const QString &filePath) { + emit screenshotCaptured(filePath, filePath); + + if (m_audioPolicy) + m_audioPolicy->playNotificationSound(); + if (m_haptics) + m_haptics->medium(); + if (m_notificationService) { + QVariantMap options; + options["icon"] = "camera"; + options["category"] = "system"; + options["priority"] = "low"; + m_notificationService->sendNotification("system", "Screenshot captured", + QFileInfo(filePath).fileName(), options); + } +} + +void ScreenshotServiceCpp::notifyFailure(const QString &message) { + emit screenshotFailed(message); +} diff --git a/shell/src/services/screenshotservicecpp.h b/shell/src/services/screenshotservicecpp.h new file mode 100644 index 000000000..5f49be9d4 --- /dev/null +++ b/shell/src/services/screenshotservicecpp.h @@ -0,0 +1,52 @@ +#ifndef SCREENSHOTSERVICECPP_H +#define SCREENSHOTSERVICECPP_H + +#include +#include + +class AudioPolicyController; +class HapticManager; +class NotificationServiceCpp; + +class ScreenshotServiceCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QString screenshotsPath READ screenshotsPath NOTIFY screenshotsPathChanged) + Q_PROPERTY(QObject *shellWindow READ shellWindow WRITE setShellWindow NOTIFY shellWindowChanged) + + public: + explicit ScreenshotServiceCpp(AudioPolicyController *audioPolicy, HapticManager *haptics, + NotificationServiceCpp *notificationService, + QObject *parent = nullptr); + + QString screenshotsPath() const { + return m_screenshotsPath; + } + + QObject *shellWindow() const { + return m_shellWindow; + } + void setShellWindow(QObject *window); + + Q_INVOKABLE void takeScreenshot(QObject *windowItem = nullptr); + Q_INVOKABLE void captureScreen(QObject *windowItem = nullptr); + + signals: + void screenshotsPathChanged(); + void shellWindowChanged(); + void screenshotCaptured(const QString &filePath, const QString &thumbnailPath); + void screenshotFailed(const QString &error); + + private: + QString resolveScreenshotsPath() const; + QString buildScreenshotPath() const; + void handleSavedScreenshot(const QString &filePath); + void notifyFailure(const QString &message); + + AudioPolicyController *m_audioPolicy = nullptr; + HapticManager *m_haptics = nullptr; + NotificationServiceCpp *m_notificationService = nullptr; + QObject *m_shellWindow = nullptr; + QString m_screenshotsPath; +}; + +#endif diff --git a/shell/src/services/statemanagercpp.cpp b/shell/src/services/statemanagercpp.cpp new file mode 100644 index 000000000..4772b4baf --- /dev/null +++ b/shell/src/services/statemanagercpp.cpp @@ -0,0 +1,131 @@ +#include "statemanagercpp.h" + +#include +#include +#include +#include + +StateManagerCpp::StateManagerCpp(QObject *parent) + : QObject(parent) + , m_settings("MarathonOS", "Shell") { + loadStateData(); + scheduleAutoSave(); +} + +void StateManagerCpp::registerApp(const QString &appId, bool isVisible) { + if (appId.isEmpty()) + return; + + m_runningApps[appId] = mapForRunningApp(appId, isVisible); +} + +void StateManagerCpp::unregisterApp(const QString &appId) { + if (appId.isEmpty()) + return; + + m_runningApps.remove(appId); +} + +void StateManagerCpp::updateAppVisibility(const QString &appId, bool isVisible) { + if (!m_runningApps.contains(appId)) + return; + + m_runningApps[appId] = mapForRunningApp(appId, isVisible); +} + +void StateManagerCpp::saveAppState(const QString &appId, const QString &state) { + if (appId.isEmpty()) + return; + + m_appStates[appId] = mapForState(state); +} + +void StateManagerCpp::saveAllStates() { + const QStringList appIds = m_runningApps.keys(); + const QJsonArray appIdArray = QJsonArray::fromStringList(appIds); + + QJsonObject statesObject; + for (auto it = m_appStates.constBegin(); it != m_appStates.constEnd(); ++it) { + if (it.value().canConvert()) + statesObject.insert(it.key(), QJsonObject::fromVariantMap(it.value().toMap())); + } + + m_settings.setValue( + "AppState/runningAppIds", + QString::fromUtf8(QJsonDocument(appIdArray).toJson(QJsonDocument::Compact))); + m_settings.setValue( + "AppState/appStateData", + QString::fromUtf8(QJsonDocument(statesObject).toJson(QJsonDocument::Compact))); + m_settings.sync(); +} + +QStringList StateManagerCpp::restoreStates() { + const QString raw = m_settings.value("AppState/runningAppIds").toString(); + if (raw.isEmpty()) + return {}; + + const QJsonDocument doc = QJsonDocument::fromJson(raw.toUtf8()); + if (!doc.isArray()) + return {}; + + QStringList appIds; + const QJsonArray arr = doc.array(); + appIds.reserve(arr.size()); + for (const QJsonValue &value : arr) { + if (value.isString()) + appIds.append(value.toString()); + } + return appIds; +} + +QVariantMap StateManagerCpp::getAppState(const QString &appId) const { + return m_appStates.value(appId).toMap(); +} + +void StateManagerCpp::clearAppState(const QString &appId) { + m_appStates.remove(appId); +} + +void StateManagerCpp::clearAllStates() { + m_runningApps.clear(); + m_appStates.clear(); + m_settings.setValue("AppState/runningAppIds", ""); + m_settings.setValue("AppState/appStateData", ""); + m_settings.sync(); +} + +void StateManagerCpp::loadStateData() { + const QString rawStates = m_settings.value("AppState/appStateData").toString(); + if (rawStates.isEmpty()) + return; + + const QJsonDocument doc = QJsonDocument::fromJson(rawStates.toUtf8()); + if (!doc.isObject()) + return; + + const QJsonObject obj = doc.object(); + for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) + m_appStates.insert(it.key(), it.value().toObject().toVariantMap()); +} + +void StateManagerCpp::scheduleAutoSave() { + m_autoSaveTimer.setInterval(5000); + m_autoSaveTimer.setSingleShot(false); + connect(&m_autoSaveTimer, &QTimer::timeout, this, &StateManagerCpp::saveAllStates); + m_autoSaveTimer.start(); +} + +QVariantMap StateManagerCpp::mapForRunningApp(const QString &appId, bool isVisible) const { + QVariantMap map; + map["appId"] = appId; + map["isVisible"] = isVisible; + map["timestamp"] = QDateTime::currentMSecsSinceEpoch(); + return map; +} + +QVariantMap StateManagerCpp::mapForState(const QString &state) const { + QVariantMap map; + map["state"] = state; + map["timestamp"] = QDateTime::currentMSecsSinceEpoch(); + return map; +} diff --git a/shell/src/services/statemanagercpp.h b/shell/src/services/statemanagercpp.h new file mode 100644 index 000000000..1676c997b --- /dev/null +++ b/shell/src/services/statemanagercpp.h @@ -0,0 +1,37 @@ +#ifndef STATEMANAGERCPP_H +#define STATEMANAGERCPP_H + +#include +#include +#include +#include + +class StateManagerCpp : public QObject { + Q_OBJECT + + public: + explicit StateManagerCpp(QObject *parent = nullptr); + + Q_INVOKABLE void registerApp(const QString &appId, bool isVisible); + Q_INVOKABLE void unregisterApp(const QString &appId); + Q_INVOKABLE void updateAppVisibility(const QString &appId, bool isVisible); + Q_INVOKABLE void saveAppState(const QString &appId, const QString &state); + Q_INVOKABLE void saveAllStates(); + Q_INVOKABLE QStringList restoreStates(); + Q_INVOKABLE QVariantMap getAppState(const QString &appId) const; + Q_INVOKABLE void clearAppState(const QString &appId); + Q_INVOKABLE void clearAllStates(); + + private: + void loadStateData(); + void scheduleAutoSave(); + QVariantMap mapForRunningApp(const QString &appId, bool isVisible) const; + QVariantMap mapForState(const QString &state) const; + + QSettings m_settings; + QVariantMap m_runningApps; + QVariantMap m_appStates; + QTimer m_autoSaveTimer; +}; + +#endif diff --git a/shell/src/services/statusbariconservicecpp.cpp b/shell/src/services/statusbariconservicecpp.cpp new file mode 100644 index 000000000..e1462dc62 --- /dev/null +++ b/shell/src/services/statusbariconservicecpp.cpp @@ -0,0 +1,98 @@ +#include "statusbariconservicecpp.h" + +QString StatusBarIconServiceCpp::getBatteryIcon(int level, bool isCharging) const { + if (isCharging) + return "battery-charging"; + if (level <= 10) + return "battery-warning"; + if (level <= 25) + return "battery-low"; + if (level <= 50) + return "battery-medium"; + return "battery-full"; +} + +QString StatusBarIconServiceCpp::getBatteryColor(int level, bool isCharging) const { + if (isCharging) + return "#00CCCC"; + if (level <= 10) + return "#FF4444"; + if (level <= 20) + return "#FF8800"; + return "#FFFFFF"; +} + +QString StatusBarIconServiceCpp::getSignalIcon(int strength) const { + if (strength == 0) + return "signal-zero"; + if (strength <= 25) + return "signal-low"; + if (strength <= 50) + return "signal-medium"; + if (strength <= 75) + return "signal"; + return "signal-high"; +} + +qreal StatusBarIconServiceCpp::getSignalOpacity(int strength) const { + if (strength == 0) + return 0.3; + if (strength <= 25) + return 0.6; + if (strength <= 50) + return 0.8; + if (strength <= 75) + return 0.9; + return 1.0; +} + +QString StatusBarIconServiceCpp::getWifiIcon(bool isEnabled, int strength, bool isConnected) const { + if (!isEnabled || !isConnected) + return "wifi-off"; + if (strength == 0) + return "wifi-zero"; + if (strength <= 33) + return "wifi-low"; + if (strength <= 66) + return "wifi"; + return "wifi-high"; +} + +qreal StatusBarIconServiceCpp::getWifiOpacity(bool isEnabled, int strength, + bool isConnected) const { + if (!isEnabled || !isConnected) + return 0.3; + if (strength == 0) + return 0.4; + if (strength <= 33) + return 0.6; + if (strength <= 66) + return 0.8; + return 1.0; +} + +QString StatusBarIconServiceCpp::getBluetoothIcon(bool isEnabled, bool isConnected) const { + Q_UNUSED(isEnabled); + Q_UNUSED(isConnected); + return "bluetooth"; +} + +qreal StatusBarIconServiceCpp::getBluetoothOpacity(bool isEnabled, bool isConnected) const { + if (!isEnabled) + return 0.3; + if (isConnected) + return 1.0; + return 0.6; +} + +bool StatusBarIconServiceCpp::shouldShowAirplaneMode(bool isEnabled) const { + return isEnabled; +} + +bool StatusBarIconServiceCpp::shouldShowDnd(bool isEnabled) const { + return isEnabled; +} + +bool StatusBarIconServiceCpp::shouldShowBluetooth(bool isEnabled) const { + return isEnabled; +} diff --git a/shell/src/services/statusbariconservicecpp.h b/shell/src/services/statusbariconservicecpp.h new file mode 100644 index 000000000..69c528223 --- /dev/null +++ b/shell/src/services/statusbariconservicecpp.h @@ -0,0 +1,27 @@ +#ifndef STATUSBARICONSERVICECPP_H +#define STATUSBARICONSERVICECPP_H + +#include +#include + +class StatusBarIconServiceCpp : public QObject { + Q_OBJECT + + public: + explicit StatusBarIconServiceCpp(QObject *parent = nullptr) + : QObject(parent) {} + + Q_INVOKABLE QString getBatteryIcon(int level, bool isCharging) const; + Q_INVOKABLE QString getBatteryColor(int level, bool isCharging) const; + Q_INVOKABLE QString getSignalIcon(int strength) const; + Q_INVOKABLE qreal getSignalOpacity(int strength) const; + Q_INVOKABLE QString getWifiIcon(bool isEnabled, int strength, bool isConnected) const; + Q_INVOKABLE qreal getWifiOpacity(bool isEnabled, int strength, bool isConnected) const; + Q_INVOKABLE QString getBluetoothIcon(bool isEnabled, bool isConnected) const; + Q_INVOKABLE qreal getBluetoothOpacity(bool isEnabled, bool isConnected) const; + Q_INVOKABLE bool shouldShowAirplaneMode(bool isEnabled) const; + Q_INVOKABLE bool shouldShowDnd(bool isEnabled) const; + Q_INVOKABLE bool shouldShowBluetooth(bool isEnabled) const; +}; + +#endif diff --git a/shell/src/services/telephonyintegrationcpp.cpp b/shell/src/services/telephonyintegrationcpp.cpp new file mode 100644 index 000000000..2d3b45951 --- /dev/null +++ b/shell/src/services/telephonyintegrationcpp.cpp @@ -0,0 +1,203 @@ +#include "telephonyintegrationcpp.h" + +#include "notificationservicecpp.h" +#include "powermanagercpp.h" +#include "displaymanagercpp.h" +#include "displaypolicycontroller.h" +#include "powerpolicycontroller.h" +#include "audiopolicycontroller.h" +#include "hapticmanager.h" +#include "telephonyservice.h" +#include "smsservice.h" +#include "contactsmanager.h" + +#include + +TelephonyIntegrationCpp::TelephonyIntegrationCpp( + ContactsManager *contactsManager, NotificationServiceCpp *notificationService, + PowerPolicyController *powerPolicy, PowerManagerCpp *powerManager, + DisplayPolicyController *displayPolicy, DisplayManagerCpp *displayManager, + AudioPolicyController *audioPolicy, HapticManager *haptics, TelephonyService *telephonyService, + SMSService *smsService, QObject *parent) + : QObject(parent) + , m_contactsManager(contactsManager) + , m_notificationService(notificationService) + , m_powerPolicy(powerPolicy) + , m_powerManager(powerManager) + , m_displayPolicy(displayPolicy) + , m_displayManager(displayManager) + , m_audioPolicy(audioPolicy) + , m_haptics(haptics) + , m_telephonyService(telephonyService) + , m_smsService(smsService) { + if (m_telephonyService) { + connect(m_telephonyService, &TelephonyService::incomingCall, this, + &TelephonyIntegrationCpp::handleIncomingCall); + connect(m_telephonyService, &TelephonyService::callStateChanged, this, + &TelephonyIntegrationCpp::handleCallStateChanged); + } + + if (m_smsService) { + connect(m_smsService, &SMSService::messageReceived, this, + &TelephonyIntegrationCpp::handleMessageReceived); + } +} + +void TelephonyIntegrationCpp::setLastCallState(const QString &state) { + if (m_lastCallState == state) + return; + m_lastCallState = state; + emit lastCallStateChanged(); +} + +void TelephonyIntegrationCpp::setLastCallerNumber(const QString &number) { + if (m_lastCallerNumber == number) + return; + m_lastCallerNumber = number; + emit lastCallerNumberChanged(); +} + +void TelephonyIntegrationCpp::setCallWasAnswered(bool answered) { + if (m_callWasAnswered == answered) + return; + m_callWasAnswered = answered; + emit callWasAnsweredChanged(); +} + +void TelephonyIntegrationCpp::setIncomingCallOverlay(QObject *overlay) { + if (m_incomingCallOverlay == overlay) + return; + m_incomingCallOverlay = overlay; + emit incomingCallOverlayChanged(); + + if (!m_pendingIncomingNumber.isEmpty() && m_incomingCallOverlay) { + showIncomingOverlay(m_pendingIncomingNumber); + m_pendingIncomingNumber.clear(); + } +} + +void TelephonyIntegrationCpp::handleIncomingCall(const QString &number) { + setLastCallerNumber(number); + setCallWasAnswered(false); + + if (m_powerPolicy) + m_powerPolicy->wake("call"); + else if (m_powerManager) + m_powerManager->acquireWakelock("call"); + + if (m_displayPolicy) + m_displayPolicy->turnScreenOn(); + else if (m_displayManager) + m_displayManager->setScreenState(true); + + if (m_audioPolicy) + m_audioPolicy->playRingtone(); + + if (m_haptics) + m_haptics->vibratePattern(QVariantList{1000, 500}, -1); + + showIncomingOverlay(number); +} + +void TelephonyIntegrationCpp::handleCallStateChanged(const QString &state) { + updateHasActiveCall(state); + + if (m_lastCallState == "incoming" && (state == "idle" || state == "terminated")) { + if (!m_callWasAnswered) + createMissedCallNotification(m_lastCallerNumber); + } + + if (state == "active" || state == "idle" || state == "terminated") { + if (m_audioPolicy) + m_audioPolicy->stopRingtone(); + if (m_haptics) + m_haptics->stopVibration(); + hideIncomingOverlay(); + emit unlockRequested(); + } + + setLastCallState(state); +} + +void TelephonyIntegrationCpp::handleMessageReceived(const QString &sender, const QString &text, + qint64) { + if (m_powerPolicy) + m_powerPolicy->wake("notification"); + else if (m_powerManager) + m_powerManager->acquireWakelock("notification"); + + const QString contactName = resolveContactName(sender); + if (m_notificationService) { + QVariantList actions; + actions.append(QVariantMap{{"id", "reply"}, {"label", "Reply"}}); + actions.append(QVariantMap{{"id", "mark_read"}, {"label", "Mark Read"}}); + QVariantMap options; + options["icon"] = "message-circle"; + options["category"] = "message"; + options["priority"] = "high"; + options["actions"] = actions; + m_notificationService->sendNotification("messages", contactName, text, options); + } + + if (m_haptics) + m_haptics->medium(); + if (m_audioPolicy) + m_audioPolicy->playNotificationSound(); +} + +QString TelephonyIntegrationCpp::resolveContactName(const QString &number) const { + if (!m_contactsManager) + return number; + const QVariantMap contact = m_contactsManager->getContactByNumber(number); + const QString name = contact.value("name").toString(); + return name.isEmpty() ? number : name; +} + +void TelephonyIntegrationCpp::createMissedCallNotification(const QString &number) { + if (!m_notificationService) + return; + + const QString contactName = resolveContactName(number); + QVariantList actions; + actions.append(QVariantMap{{"id", "call_back"}, {"label", "Call Back"}}); + actions.append(QVariantMap{{"id", "message"}, {"label", "Message"}}); + + QVariantMap options; + options["icon"] = "phone-missed"; + options["category"] = "call"; + options["priority"] = "high"; + options["actions"] = actions; + + m_notificationService->sendNotification("phone", "Missed Call", "From: " + contactName, + options); + + if (m_haptics) + m_haptics->heavy(); +} + +void TelephonyIntegrationCpp::showIncomingOverlay(const QString &number) { + if (!m_incomingCallOverlay) { + m_pendingIncomingNumber = number; + emit incomingCallRequested(number); + return; + } + + const QString contactName = resolveContactName(number); + QMetaObject::invokeMethod(m_incomingCallOverlay, "show", Q_ARG(QVariant, number), + Q_ARG(QVariant, contactName)); +} + +void TelephonyIntegrationCpp::hideIncomingOverlay() { + if (m_incomingCallOverlay) + QMetaObject::invokeMethod(m_incomingCallOverlay, "hide"); + m_pendingIncomingNumber.clear(); + emit callOverlayDismissRequested(); +} + +void TelephonyIntegrationCpp::updateHasActiveCall(const QString &state) { + const bool active = (state == "incoming" || state == "ringing" || state == "active"); + if (m_hasActiveCall == active) + return; + m_hasActiveCall = active; + emit hasActiveCallChanged(); +} diff --git a/shell/src/services/telephonyintegrationcpp.h b/shell/src/services/telephonyintegrationcpp.h new file mode 100644 index 000000000..909498273 --- /dev/null +++ b/shell/src/services/telephonyintegrationcpp.h @@ -0,0 +1,104 @@ +#ifndef TELEPHONYINTEGRATIONCPP_H +#define TELEPHONYINTEGRATIONCPP_H + +#include +#include +#include + +class ContactsManager; +class NotificationServiceCpp; +class PowerPolicyController; +class PowerManagerCpp; +class DisplayPolicyController; +class DisplayManagerCpp; +class AudioPolicyController; +class HapticManager; +class TelephonyService; +class SMSService; + +class TelephonyIntegrationCpp : public QObject { + Q_OBJECT + Q_PROPERTY( + QString lastCallState READ lastCallState WRITE setLastCallState NOTIFY lastCallStateChanged) + Q_PROPERTY(QString lastCallerNumber READ lastCallerNumber WRITE setLastCallerNumber NOTIFY + lastCallerNumberChanged) + Q_PROPERTY(bool callWasAnswered READ callWasAnswered WRITE setCallWasAnswered NOTIFY + callWasAnsweredChanged) + Q_PROPERTY(QObject *incomingCallOverlay READ incomingCallOverlay WRITE setIncomingCallOverlay + NOTIFY incomingCallOverlayChanged) + Q_PROPERTY(bool hasActiveCall READ hasActiveCall NOTIFY hasActiveCallChanged) + + public: + explicit TelephonyIntegrationCpp( + ContactsManager *contactsManager, NotificationServiceCpp *notificationService, + PowerPolicyController *powerPolicy, PowerManagerCpp *powerManager, + DisplayPolicyController *displayPolicy, DisplayManagerCpp *displayManager, + AudioPolicyController *audioPolicy, HapticManager *haptics, + TelephonyService *telephonyService, SMSService *smsService, QObject *parent = nullptr); + + QString lastCallState() const { + return m_lastCallState; + } + void setLastCallState(const QString &state); + + QString lastCallerNumber() const { + return m_lastCallerNumber; + } + void setLastCallerNumber(const QString &number); + + bool callWasAnswered() const { + return m_callWasAnswered; + } + void setCallWasAnswered(bool answered); + + QObject *incomingCallOverlay() const { + return m_incomingCallOverlay; + } + void setIncomingCallOverlay(QObject *overlay); + + bool hasActiveCall() const { + return m_hasActiveCall; + } + + signals: + void lastCallStateChanged(); + void lastCallerNumberChanged(); + void callWasAnsweredChanged(); + void incomingCallOverlayChanged(); + void hasActiveCallChanged(); + void incomingCallRequested(const QString &number); + void callOverlayDismissRequested(); + void unlockRequested(); + + private slots: + void handleIncomingCall(const QString &number); + void handleCallStateChanged(const QString &state); + void handleMessageReceived(const QString &sender, const QString &text, qint64 timestamp); + + private: + QString resolveContactName(const QString &number) const; + void createMissedCallNotification(const QString &number); + void showIncomingOverlay(const QString &number); + void hideIncomingOverlay(); + void updateHasActiveCall(const QString &state); + + ContactsManager *m_contactsManager = nullptr; + NotificationServiceCpp *m_notificationService = nullptr; + PowerPolicyController *m_powerPolicy = nullptr; + PowerManagerCpp *m_powerManager = nullptr; + DisplayPolicyController *m_displayPolicy = nullptr; + DisplayManagerCpp *m_displayManager = nullptr; + AudioPolicyController *m_audioPolicy = nullptr; + HapticManager *m_haptics = nullptr; + TelephonyService *m_telephonyService = nullptr; + SMSService *m_smsService = nullptr; + + QString m_lastCallState = "idle"; + QString m_lastCallerNumber; + bool m_callWasAnswered = false; + QObject *m_incomingCallOverlay = nullptr; + bool m_hasActiveCall = false; + QString m_pendingIncomingNumber; +}; + +#endif diff --git a/shell/src/services/unifiedsearchservicecpp.cpp b/shell/src/services/unifiedsearchservicecpp.cpp new file mode 100644 index 000000000..da48b1e4a --- /dev/null +++ b/shell/src/services/unifiedsearchservicecpp.cpp @@ -0,0 +1,366 @@ +#include "unifiedsearchservicecpp.h" + +#include "applaunchservice.h" +#include "appmodel.h" +#include "marathonappregistry.h" +#include "marathonappscanner.h" +#include "navigationroutercpp.h" +#include "settingsmanager.h" + +#include +#include +#include +#include +#include + +UnifiedSearchServiceCpp::UnifiedSearchServiceCpp(AppModel *appModel, MarathonAppRegistry *registry, + MarathonAppScanner *scanner, + SettingsManager *settings, + NavigationRouterCpp *router, + AppLaunchService *appLaunch, QObject *parent) + : QObject(parent) + , m_appModel(appModel) + , m_registry(registry) + , m_scanner(scanner) + , m_settings(settings) + , m_router(router) + , m_appLaunch(appLaunch) { + loadRecentSearches(); + + if (m_scanner) { + connect(m_scanner, &MarathonAppScanner::scanComplete, this, + [this](int) { buildSearchIndex(); }); + } + + if (m_appModel) { + connect(m_appModel, &AppModel::countChanged, this, [this]() { buildSearchIndex(); }); + } + + buildSearchIndex(); +} + +QVariantList UnifiedSearchServiceCpp::searchIndex() const { + QVariantList out; + out.reserve(m_items.size()); + for (const SearchItem &item : m_items) + out.append(mapForItem(item, 0)); + return out; +} + +QStringList UnifiedSearchServiceCpp::recentSearches() const { + return m_recentSearches; +} + +void UnifiedSearchServiceCpp::buildSearchIndex() { + m_isIndexing = true; + emit isIndexingChanged(); + m_items.clear(); + + appendAppItems(); + appendDeepLinkItems(); + + m_isIndexing = false; + emit isIndexingChanged(); + emit searchIndexChanged(); + emit indexingComplete(); +} + +QVariantList UnifiedSearchServiceCpp::search(const QString &query) { + const QString normalized = query.toLower().trimmed(); + if (normalized.isEmpty()) + return {}; + + struct RankedResult { + QVariantMap map; + int score; + }; + + QVector ranked; + ranked.reserve(m_items.size()); + + for (const SearchItem &item : m_items) { + const int score = computeScore(item, normalized); + if (score <= 0) + continue; + + ranked.push_back({mapForItem(item, score), score}); + } + + std::sort(ranked.begin(), ranked.end(), [](const RankedResult &a, const RankedResult &b) { + if (a.score != b.score) + return a.score > b.score; + return a.map.value("title").toString() < b.map.value("title").toString(); + }); + + QVariantList results; + results.reserve(ranked.size()); + for (const RankedResult &entry : ranked) + results.append(entry.map); + + emit searchCompleted(results); + return results; +} + +void UnifiedSearchServiceCpp::addToRecentSearches(const QString &query) { + const QString normalized = query.trimmed(); + if (normalized.isEmpty()) + return; + + m_recentSearches.removeAll(normalized); + m_recentSearches.prepend(normalized); + while (m_recentSearches.size() > m_maxRecentSearches) + m_recentSearches.removeLast(); + + persistRecentSearches(); + emit recentSearchesChanged(); +} + +void UnifiedSearchServiceCpp::clearRecentSearches() { + if (m_recentSearches.isEmpty()) + return; + + m_recentSearches.clear(); + persistRecentSearches(); + emit recentSearchesChanged(); +} + +void UnifiedSearchServiceCpp::executeSearchResult(const QVariantMap &result) { + const QString type = result.value("type").toString(); + const QVariantMap data = result.value("data").toMap(); + + if (type == "app") { + if (!m_appLaunch) + return; + QVariantMap appMap; + appMap["id"] = data.value("id"); + appMap["name"] = data.value("name"); + appMap["icon"] = data.value("icon"); + appMap["type"] = data.value("type", "marathon"); + m_appLaunch->launchApp(appMap); + return; + } + + if (type == "deeplink") { + if (m_router) + m_router->navigateToDeepLink(data.value("appId").toString(), + data.value("route").toString(), {}); + return; + } + + if (type == "setting") { + emit settingRequested(result.value("id").toString()); + } +} + +QVariantMap UnifiedSearchServiceCpp::mapForItem(const SearchItem &item, int score) const { + QVariantMap map; + map["type"] = item.type; + map["id"] = item.id; + map["appId"] = item.appId; + map["title"] = item.title; + map["subtitle"] = item.subtitle; + map["icon"] = item.icon; + map["keywords"] = item.keywords; + map["searchText"] = item.searchText; + map["data"] = item.data; + map["score"] = score; + return map; +} + +UnifiedSearchServiceCpp::SearchItem UnifiedSearchServiceCpp::itemForApp(QObject *app) const { + SearchItem item; + if (!app) + return item; + + const QVariant idVar = app->property("id"); + const QVariant nameVar = app->property("name"); + const QVariant iconVar = app->property("icon"); + const QVariant typeVar = app->property("type"); + + const QString appId = idVar.toString(); + const QString appName = nameVar.toString(); + const QString appIcon = iconVar.toString(); + const QString appType = typeVar.toString(); + + item.type = "app"; + item.id = appId; + item.title = appName; + item.subtitle = appType == "native" ? "Native App" : "Marathon App"; + item.icon = appIcon; + item.keywords = {appName.toLower(), appId.toLower()}; + + const QStringList nameParts = + appName.toLower().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + item.keywords.append(nameParts); + + item.searchText = appName.toLower() + " " + appId.toLower(); + item.data = QVariantMap{{"id", appId}, {"name", appName}, {"icon", appIcon}, {"type", appType}}; + return item; +} + +void UnifiedSearchServiceCpp::appendAppItems() { + if (!m_appModel) + return; + + const int count = m_appModel->count(); + const bool indexNativeApps = shouldIndexNativeApp(); + for (int i = 0; i < count; ++i) { + QObject *app = m_appModel->getAppAtIndex(i); + if (!app) + continue; + + const QString appType = app->property("type").toString(); + if (appType == "native" && !indexNativeApps) + continue; + + SearchItem item = itemForApp(app); + if (!item.title.isEmpty() && !item.id.isEmpty()) + m_items.push_back(item); + } +} + +void UnifiedSearchServiceCpp::appendDeepLinkItems() { + if (!m_registry) + return; + + const QStringList appIds = m_registry->getAllAppIds(); + for (const QString &appId : appIds) { + const QVariantMap appData = m_registry->getApp(appId); + if (!appData.value("id").isValid()) + continue; + + const QString appName = appData.value("name").toString(); + const QString appIcon = appData.value("icon").toString(); + const QString deepLinksJson = appData.value("deepLinks").toString(); + if (deepLinksJson.isEmpty()) + continue; + + const QJsonDocument doc = QJsonDocument::fromJson(deepLinksJson.toUtf8()); + if (!doc.isObject()) + continue; + + const QJsonObject obj = doc.object(); + for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { + const QString route = it.key(); + if (!it.value().isObject()) + continue; + + const QJsonObject link = it.value().toObject(); + QStringList keywords; + const QJsonArray keywordsArray = link.value("keywords").toArray(); + for (const QJsonValue &keyword : keywordsArray) { + if (keyword.isString()) + keywords.append(keyword.toString()); + } + keywords.append(appName.toLower()); + keywords.append(route.toLower()); + + SearchItem item; + item.type = "deeplink"; + item.id = route; + item.appId = appId; + item.title = link.value("title").toString(route); + item.subtitle = link.value("description").toString(appName); + item.icon = appIcon.isEmpty() ? "qrc:/images/app-icon-placeholder.svg" : appIcon; + item.keywords = keywords; + item.searchText = item.title.toLower() + " " + keywords.join(" ").toLower(); + item.data = QVariantMap{{"appId", appId}, {"route", route}, {"appName", appName}}; + m_items.push_back(item); + } + } +} + +bool UnifiedSearchServiceCpp::shouldIndexNativeApp() const { + if (!m_settings) + return true; + return m_settings->searchNativeApps(); +} + +int UnifiedSearchServiceCpp::computeScore(const SearchItem &item, const QString &query) const { + const QString title = item.title.toLower(); + int score = 0; + if (title == query) + score = 10000; + else if (title.startsWith(query)) + score = 5000; + else if (item.keywords.contains(query)) + score = 3000; + else { + for (const QString &keyword : item.keywords) { + if (keyword.startsWith(query)) { + score = std::max(score, 2000); + break; + } + } + } + + if (score == 0 && title.contains(query)) + score = 1000; + + if (score == 0) { + for (const QString &keyword : item.keywords) { + if (keyword.contains(query)) + score = std::max(score, 500); + } + } + + if (score == 0) { + const double fuzzy = fuzzyMatch(item.searchText, query); + if (fuzzy > 0) + score = int(fuzzy * 100); + } + + if (score > 0 && item.type == "app") + score += 100; + + return score; +} + +double UnifiedSearchServiceCpp::fuzzyMatch(const QString &text, const QString &pattern) const { + int patternIdx = 0; + int score = 0; + int consecutiveMatches = 0; + + for (int i = 0; i < text.size() && patternIdx < pattern.size(); ++i) { + if (text.at(i) == pattern.at(patternIdx)) { + score += 1 + consecutiveMatches * 2; + ++consecutiveMatches; + ++patternIdx; + } else { + consecutiveMatches = 0; + } + } + + if (patternIdx != pattern.size()) + return 0; + + return double(score) / double(pattern.size()); +} + +void UnifiedSearchServiceCpp::loadRecentSearches() { + if (!m_settings) + return; + + const QVariant raw = m_settings->get("search/recent", QVariantList()); + if (raw.canConvert()) { + m_recentSearches = raw.toStringList(); + return; + } + + if (raw.typeId() == QMetaType::QString) { + const QJsonDocument doc = QJsonDocument::fromJson(raw.toString().toUtf8()); + if (!doc.isArray()) + return; + const QJsonArray arr = doc.array(); + for (const QJsonValue &value : arr) { + if (value.isString()) + m_recentSearches.append(value.toString()); + } + } +} + +void UnifiedSearchServiceCpp::persistRecentSearches() { + if (!m_settings) + return; + m_settings->set("search/recent", m_recentSearches); +} diff --git a/shell/src/services/unifiedsearchservicecpp.h b/shell/src/services/unifiedsearchservicecpp.h new file mode 100644 index 000000000..0557ee8b4 --- /dev/null +++ b/shell/src/services/unifiedsearchservicecpp.h @@ -0,0 +1,88 @@ +#ifndef UNIFIEDSEARCHSERVICECPP_H +#define UNIFIEDSEARCHSERVICECPP_H + +#include +#include +#include +#include + +class AppModel; +class MarathonAppRegistry; +class MarathonAppScanner; +class SettingsManager; +class NavigationRouterCpp; +class AppLaunchService; + +class UnifiedSearchServiceCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QVariantList searchIndex READ searchIndex NOTIFY searchIndexChanged) + Q_PROPERTY(QStringList recentSearches READ recentSearches NOTIFY recentSearchesChanged) + Q_PROPERTY(bool isIndexing READ isIndexing NOTIFY isIndexingChanged) + Q_PROPERTY(int maxRecentSearches READ maxRecentSearches CONSTANT) + + public: + explicit UnifiedSearchServiceCpp(AppModel *appModel, MarathonAppRegistry *registry, + MarathonAppScanner *scanner, SettingsManager *settings, + NavigationRouterCpp *router, AppLaunchService *appLaunch, + QObject *parent = nullptr); + + QVariantList searchIndex() const; + QStringList recentSearches() const; + bool isIndexing() const { + return m_isIndexing; + } + int maxRecentSearches() const { + return m_maxRecentSearches; + } + + Q_INVOKABLE void buildSearchIndex(); + Q_INVOKABLE QVariantList search(const QString &query); + Q_INVOKABLE void addToRecentSearches(const QString &query); + Q_INVOKABLE void clearRecentSearches(); + Q_INVOKABLE void executeSearchResult(const QVariantMap &result); + + signals: + void searchIndexChanged(); + void recentSearchesChanged(); + void isIndexingChanged(); + void searchCompleted(const QVariantList &results); + void indexingComplete(); + void settingRequested(const QString &settingId); + + private: + struct SearchItem { + QString type; + QString id; + QString appId; + QString title; + QString subtitle; + QString icon; + QStringList keywords; + QString searchText; + QVariantMap data; + }; + + QVariantMap mapForItem(const SearchItem &item, int score) const; + SearchItem itemForApp(QObject *app) const; + void appendAppItems(); + void appendDeepLinkItems(); + bool shouldIndexNativeApp() const; + int computeScore(const SearchItem &item, const QString &query) const; + double fuzzyMatch(const QString &text, const QString &pattern) const; + void loadRecentSearches(); + void persistRecentSearches(); + + AppModel *m_appModel = nullptr; + MarathonAppRegistry *m_registry = nullptr; + MarathonAppScanner *m_scanner = nullptr; + SettingsManager *m_settings = nullptr; + NavigationRouterCpp *m_router = nullptr; + AppLaunchService *m_appLaunch = nullptr; + + QVector m_items; + QStringList m_recentSearches; + bool m_isIndexing = false; + int m_maxRecentSearches = 10; +}; + +#endif diff --git a/shell/src/wayland/waylandcompositor.cpp b/shell/src/wayland/waylandcompositor.cpp index f3fae765b..6d95ddac7 100644 --- a/shell/src/wayland/waylandcompositor.cpp +++ b/shell/src/wayland/waylandcompositor.cpp @@ -438,7 +438,6 @@ void WaylandCompositor::launchApp(const QString &command, const QVariantMap &ext } } - // Resolve marathon-sandbox path to support local development builds QString sandboxPath = QStringLiteral("marathon-sandbox"); const QDir shellBinDir(QCoreApplication::applicationDirPath()); const QString devSandboxPath = diff --git a/shell/src/wayland/waylandcompositormanager.h b/shell/src/wayland/waylandcompositormanager.h index 9a9596763..c777022a2 100644 --- a/shell/src/wayland/waylandcompositormanager.h +++ b/shell/src/wayland/waylandcompositormanager.h @@ -24,4 +24,4 @@ class WaylandCompositorManager : public QObject { #endif }; -#endif // WAYLANDCOMPOSITORMANAGER_H +#endif From ac007a88fa0d09c94388b5bf33e14d5eef2d398e Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:30:53 +0000 Subject: [PATCH 03/28] refactor(shell): align QML with C++ services Remove QML service singletons and update shell UI, stores, and keyboard components to use the C++ managers and service APIs. --- diagnostics.qml | 9 - shell/qml/Main.qml | 2 - shell/qml/MarathonShell.qml | 137 +++--- shell/qml/apps/native/NativeAppWindow.qml | 2 - shell/qml/apps/template/TemplateApp.qml | 8 - shell/qml/components/AppContextMenu.qml | 4 +- shell/qml/components/BluetoothPairDialog.qml | 55 +-- shell/qml/components/ClipboardManager.qml | 14 +- shell/qml/components/EdgeGestures.qml | 4 +- shell/qml/components/ErrorToast.qml | 31 +- shell/qml/components/IncomingCallOverlay.qml | 40 +- shell/qml/components/MarathonAlarmOverlay.qml | 17 +- shell/qml/components/MarathonAppGrid.qml | 30 +- shell/qml/components/MarathonAppWindow.qml | 34 +- shell/qml/components/MarathonBottomBar.qml | 14 +- shell/qml/components/MarathonHub.qml | 26 +- shell/qml/components/MarathonLockScreen.qml | 150 ++----- shell/qml/components/MarathonNavBar.qml | 47 +- shell/qml/components/MarathonOOBE.qml | 115 ++--- shell/qml/components/MarathonPageView.qml | 47 +- shell/qml/components/MarathonPeek.qml | 42 +- shell/qml/components/MarathonPinScreen.qml | 116 ++--- .../qml/components/MarathonQuickSettings.qml | 41 +- shell/qml/components/MarathonSearch.qml | 13 +- shell/qml/components/MarathonStatusBar.qml | 37 +- shell/qml/components/MarathonTaskSwitcher.qml | 45 +- shell/qml/components/MediaPlaybackManager.qml | 42 +- shell/qml/components/NotificationToast.qml | 6 - shell/qml/components/PermissionDialog.qml | 108 +++-- shell/qml/components/PowerMenu.qml | 29 +- shell/qml/components/QuickSettingsTile.qml | 25 +- shell/qml/components/SafeAppLoader.qml | 1 - shell/qml/components/ScreenshotPreview.qml | 1 - shell/qml/components/ShareSheet.qml | 2 +- shell/qml/components/TaskCard.qml | 26 +- shell/qml/components/TaskPageIndicator.qml | 7 - shell/qml/components/VirtualKeyboard.qml | 22 +- .../components/WaylandShellSurfaceItem.qml | 20 +- shell/qml/components/WiFiPasswordDialog.qml | 46 +- shell/qml/components/ui/Button.qml | 9 +- shell/qml/components/ui/ListPickerModal.qml | 4 +- shell/qml/components/ui/Modal.qml | 1 - shell/qml/components/ui/PageIndicator.qml | 1 + .../qml/components/ui/StorageDetailsModal.qml | 4 +- shell/qml/components/ui/TextInputModal.qml | 1 - shell/qml/core/Constants.qml | 103 +---- shell/qml/core/Logger.qml | 2 - .../qml/keyboard/Core/KeyboardPerformance.qml | 2 +- shell/qml/keyboard/Core/MarathonKeyboard.qml | 1 + shell/qml/keyboard/Input/InputContext.qml | 14 +- shell/qml/keyboard/Layouts/DynamicLayout.qml | 3 +- shell/qml/keyboard/Layouts/EmailLayout.qml | 25 +- shell/qml/keyboard/Layouts/EmojiLayout.qml | 43 +- shell/qml/keyboard/Layouts/NumberLayout.qml | 6 +- shell/qml/keyboard/Layouts/PhoneLayout.qml | 13 +- shell/qml/keyboard/Layouts/QwertyLayout.qml | 6 +- shell/qml/keyboard/Layouts/SymbolLayout.qml | 6 +- shell/qml/keyboard/Layouts/TerminalLayout.qml | 6 +- shell/qml/keyboard/Layouts/UrlLayout.qml | 25 +- shell/qml/keyboard/UI/Key.qml | 6 +- shell/qml/keyboard/UI/LanguageSelector.qml | 4 +- shell/qml/keyboard/UI/PredictionBar.qml | 2 +- shell/qml/keyboard/UI/ToolBar.qml | 2 +- shell/qml/models/FilteredAppModel.qml | 17 - shell/qml/services/AlarmManager.qml | 346 --------------- shell/qml/services/AmbientLightSensor.qml | 114 ----- shell/qml/services/AudioManager.qml | 117 ----- shell/qml/services/CalendarManager.qml | 255 ----------- shell/qml/services/ClipboardService.qml | 71 --- shell/qml/services/HapticService.qml | 142 ------ shell/qml/services/LocationService.qml | 200 --------- shell/qml/services/NavigationRouter.qml | 240 ---------- shell/qml/services/NotificationHandler.qml | 73 --- shell/qml/services/NotificationService.qml | 420 ------------------ shell/qml/services/Platform.qml | 44 -- shell/qml/services/PowerBatteryHandler.qml | 34 -- shell/qml/services/ProximitySensor.qml | 45 -- shell/qml/services/ScreenshotService.qml | 93 ---- shell/qml/services/StateManager.qml | 99 ----- shell/qml/services/StatusBarIconService.qml | 155 ------- shell/qml/services/TelephonyIntegration.qml | 128 ------ shell/qml/services/UnifiedSearchService.qml | 290 ------------ shell/qml/stores/AppStore.qml | 7 - shell/qml/stores/SessionStore.qml | 86 +--- shell/qml/stores/SystemControlStore.qml | 43 +- shell/qml/stores/SystemStatusStore.qml | 14 +- shell/qml/stores/UIStore.qml | 14 +- shell/qml/stores/WallpaperStore.qml | 6 - 88 files changed, 496 insertions(+), 4261 deletions(-) delete mode 100644 shell/qml/services/AlarmManager.qml delete mode 100644 shell/qml/services/AmbientLightSensor.qml delete mode 100644 shell/qml/services/AudioManager.qml delete mode 100644 shell/qml/services/CalendarManager.qml delete mode 100644 shell/qml/services/ClipboardService.qml delete mode 100644 shell/qml/services/HapticService.qml delete mode 100644 shell/qml/services/LocationService.qml delete mode 100644 shell/qml/services/NavigationRouter.qml delete mode 100644 shell/qml/services/NotificationHandler.qml delete mode 100644 shell/qml/services/NotificationService.qml delete mode 100644 shell/qml/services/Platform.qml delete mode 100644 shell/qml/services/PowerBatteryHandler.qml delete mode 100644 shell/qml/services/ProximitySensor.qml delete mode 100644 shell/qml/services/ScreenshotService.qml delete mode 100644 shell/qml/services/StateManager.qml delete mode 100644 shell/qml/services/StatusBarIconService.qml delete mode 100644 shell/qml/services/TelephonyIntegration.qml delete mode 100644 shell/qml/services/UnifiedSearchService.qml diff --git a/diagnostics.qml b/diagnostics.qml index 978c1170d..1bc30ab88 100644 --- a/diagnostics.qml +++ b/diagnostics.qml @@ -1,6 +1,3 @@ -// Marathon Shell Internal Diagnostics -// Shows what all C++ services are reporting - import MarathonOS.Shell import QtQuick import QtQuick.Controls @@ -40,7 +37,6 @@ ApplicationWindow { color: "#00ff00" } - // Bluetooth Rectangle { width: parent.width height: bluetoothCol.height + 20 @@ -104,7 +100,6 @@ ApplicationWindow { } } - // Display/Brightness Rectangle { width: parent.width height: displayCol.height + 20 @@ -154,7 +149,6 @@ ApplicationWindow { } } - // Audio Rectangle { width: parent.width height: audioCol.height + 20 @@ -197,7 +191,6 @@ ApplicationWindow { } } - // Network Rectangle { width: parent.width height: networkCol.height + 20 @@ -261,7 +254,6 @@ ApplicationWindow { } } - // Power Rectangle { width: parent.width height: powerCol.height + 20 @@ -311,7 +303,6 @@ ApplicationWindow { } } - // QML Services Status Text { text: "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" font.family: "monospace" diff --git a/shell/qml/Main.qml b/shell/qml/Main.qml index ee6b84f85..462498ebd 100644 --- a/shell/qml/Main.qml +++ b/shell/qml/Main.qml @@ -4,8 +4,6 @@ import QtQuick.Window Window { id: window - // OnePlus 6 aspect ratio (9:19) scaled to 50% for desktop debugging - // Actual device: 1080×2280, Debug window: 540×1140 width: 540 height: 1140 visible: true diff --git a/shell/qml/MarathonShell.qml b/shell/qml/MarathonShell.qml index 4563b578d..fc776cb31 100644 --- a/shell/qml/MarathonShell.qml +++ b/shell/qml/MarathonShell.qml @@ -14,6 +14,8 @@ Item { property int currentPage: 0 property int totalPages: 1 property var pendingNotification: null + property IncomingCallOverlay incomingCallOverlay: null + property var shellWindow: Window.window readonly property real maxQuickSettingsHeight: shell.height - Constants.statusBarHeight readonly property real quickSettingsThreshold: maxQuickSettingsHeight * Constants.quickSettingsDismissThreshold property var pendingLaunch: null @@ -65,7 +67,7 @@ Item { function handleHomeKey() { if (virtualKeyboard.active) { - HapticService.light(); + HapticManager.light(); virtualKeyboard.active = false; return; } @@ -101,7 +103,7 @@ Item { } else if (event.key === Qt.Key_PowerOff || event.key === Qt.Key_Sleep || event.key === Qt.Key_Suspend) { powerButtonPressed = false; if (powerButtonTimer.running) { - PowerBatteryHandler.handlePowerButtonPress(); + PowerBatteryHandler.handlePowerButtonPress(SessionStore.isLocked, typeof DisplayPolicyControllerCpp !== "undefined" ? DisplayPolicyControllerCpp.screenOn : true); powerButtonTimer.stop(); } event.accepted = true; @@ -109,7 +111,7 @@ Item { } Component.onCompleted: { shell.forceActiveFocus(); - compositor = shellInitialization.initialize(shell, Window.window); + compositor = shellInitialization.initialize(shell, shellWindow); AppLaunchService.compositor = compositor; AppLaunchService.appWindow = appWindow; AppLaunchService.uiStore = UIStore; @@ -166,34 +168,34 @@ Item { } else if ((event.key === Qt.Key_Space) && (event.modifiers & Qt.ControlModifier)) { Logger.debug("Shell", "Cmd+Space pressed - Opening Universal Search"); UIStore.toggleSearch(); - HapticService.light(); + HapticManager.light(); event.accepted = true; } else if ((event.key === Qt.Key_K) && (event.modifiers & Qt.ControlModifier)) { Logger.debug("Shell", "Cmd+K pressed - Toggling Virtual Keyboard"); virtualKeyboard.active = !virtualKeyboard.active; - HapticService.light(); + HapticManager.light(); event.accepted = true; } else if (event.key === Qt.Key_Menu) { Logger.debug("Shell", "Menu key pressed - Toggling Virtual Keyboard"); virtualKeyboard.active = !virtualKeyboard.active; - HapticService.light(); + HapticManager.light(); event.accepted = true; } else if (event.key === Qt.Key_Print || event.key === Qt.Key_SysReq) { Logger.debug("Shell", "Print Screen pressed - Taking Screenshot"); screenshotFlash.trigger(); ScreenshotService.captureScreen(shell); - HapticService.medium(); + HapticManager.medium(); event.accepted = true; } else if ((event.key === Qt.Key_3) && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) { Logger.debug("Shell", "Ctrl+Shift+3 pressed - Taking Screenshot"); screenshotFlash.trigger(); ScreenshotService.captureScreen(shell); - HapticService.medium(); + HapticManager.medium(); event.accepted = true; } else if ((event.key === Qt.Key_V) && (event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier)) { Logger.debug("Shell", "Cmd+Shift+V pressed - Opening Clipboard Manager"); UIStore.openClipboardManager(); - HapticService.light(); + HapticManager.light(); event.accepted = true; } else if (shell.state === "home" && !UIStore.searchOpen && !UIStore.appWindowOpen) { if (event.text.length > 0 && event.text.match(/[a-zA-Z0-9]/)) { @@ -508,6 +510,14 @@ Item { target: NotificationService } + Connections { + function onLockRequested() { + SessionStore.lock(); + } + + target: PowerBatteryHandler + } + Comp.ShellInitialization { id: shellInitialization } @@ -635,7 +645,7 @@ Item { onToggleSearch: { Logger.info("Shell", "Search button clicked from nav bar"); UIStore.toggleSearch(); - HapticService.light(); + HapticManager.light(); } onSwipeLeft: { if (UIStore.appWindowOpen || UIStore.settingsOpen) @@ -678,7 +688,7 @@ Item { onShortSwipeUp: { if (virtualKeyboard.active) { Logger.info("NavBar", "Dismissing keyboard with short swipe up"); - HapticService.light(); + HapticManager.light(); virtualKeyboard.active = false; return; } @@ -692,7 +702,7 @@ Item { Logger.info("NavBar", "━━━━━━━ LONG SWIPE UP RECEIVED ━━━━━━━"); if (virtualKeyboard.active) { Logger.info("NavBar", "Dismissing keyboard with long swipe up"); - HapticService.light(); + HapticManager.light(); virtualKeyboard.active = false; return; } @@ -726,13 +736,13 @@ Item { Router.goToFrames(); } } - onMinimizeApp: velocity => { - Logger.info("Shell", "NavBar minimize gesture detected (velocity: " + velocity + ")"); + onMinimizeApp: { + Logger.info("Shell", "NavBar minimize gesture detected"); if (typeof AppLifecycleManager !== 'undefined') AppLifecycleManager.minimizeForegroundApp(); shell.isTransitioningToActiveFrames = true; - var initialVelocity = velocity < 0 ? velocity : -1000; + var initialVelocity = -1000; snapIntoGridAnimation.startWithVelocity(initialVelocity); } } @@ -1332,7 +1342,7 @@ Item { showPinScreen = true; pinScreen.show(); } - HapticService.medium(); + HapticManager.medium(); } onPhoneLaunched: { Logger.info("LockScreen", "Phone quick action tapped"); @@ -1348,7 +1358,7 @@ Item { showPinScreen = true; pinScreen.show(); } - HapticService.medium(); + HapticManager.medium(); } } @@ -1465,18 +1475,7 @@ Item { shell.forceActiveFocus(); } onResultSelected: result => { - if (result.type === "app") { - var app = { - "id": result.data.id, - "name": result.data.name, - "icon": result.data.icon, - "type": result.data.type || "marathon" - }; - AppLaunchService.launchApp(app, compositor, appWindow); - } else if (result.type === "deeplink") - UnifiedSearchService.executeSearchResult(result); - else if (result.type === "setting") - UnifiedSearchService.executeSearchResult(result); + UnifiedSearchService.executeSearchResult(result); UIStore.closeSearch(); } } @@ -1485,11 +1484,20 @@ Item { id: screenshotPreview } + Connections { + function onSettingRequested(settingId) { + if (typeof Router !== "undefined") + Router.navigateToSetting(settingId); + } + + target: UnifiedSearchService + } + Connections { function onScreenshotCaptured(filePath, thumbnailPath) { Logger.info("Shell", "Screenshot captured: " + filePath); screenshotPreview.show(filePath, thumbnailPath); - HapticService.medium(); + HapticManager.medium(); } function onScreenshotFailed(error) { @@ -1548,13 +1556,13 @@ Item { if (errorToast) errorToast.show(title, body, iconName); - if (typeof HapticService !== "undefined" && HapticService) { + if (typeof HapticManager !== "undefined" && HapticManager) { if (hapticLevel >= 3) - HapticService.heavy(); + HapticManager.heavy(); else if (hapticLevel === 2) - HapticService.medium(); + HapticManager.medium(); else if (hapticLevel === 1) - HapticService.light(); + HapticManager.light(); } } @@ -1576,7 +1584,7 @@ Item { interval: 10000 repeat: false onTriggered: { - Logger.critical("Battery", "Emergency critical power action due to battery"); + Logger.error("Battery", "Emergency critical power action due to battery"); if (typeof PowerPolicyControllerCpp !== "undefined" && PowerPolicyControllerCpp) PowerPolicyControllerCpp.performCriticalPowerAction(); else if (typeof PowerManagerService !== "undefined" && PowerManagerService) @@ -1624,13 +1632,17 @@ Item { } Connections { - function onAlarmTriggered(alarm) { - Logger.info("Shell", "Alarm triggered: " + (alarm && alarm.label ? alarm.label : "(unknown)")); + function onAlarmTriggered(id, label) { + var alarm = { + "id": id, + "label": label + }; + Logger.info("Shell", "Alarm triggered: " + (label ? label : "(unknown)")); alarmOverlay.show(alarm); - HapticService.heavy(); + HapticManager.heavy(); } - target: typeof AlarmManager !== 'undefined' ? AlarmManager : null + target: typeof AlarmManagerCpp !== 'undefined' ? AlarmManagerCpp : null } VirtualKeyboard { @@ -1640,16 +1652,15 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom Component.onCompleted: { - focusConnection.target = Window.window; + focusConnection.target = shellWindow; } Connections { id: focusConnection function onActiveFocusItemChanged() { - var item = focusConnection.target ? focusConnection.target.activeFocusItem : null; + var item = shellWindow ? shellWindow["activeFocusItem"] : null; if (!item) { - // Internal focus lost. If InputMethodEngine is also not active, hide. if (!InputMethodEngine.active) virtualKeyboard.active = false; @@ -1684,8 +1695,7 @@ Item { } function onInputItemUnfocused() { - // If internal focus is NOT on an input, hide. - var item = Window.window ? Window.window.activeFocusItem : null; + var item = shellWindow ? shellWindow["activeFocusItem"] : null; var isInternalInput = item && (item.toString().indexOf("TextInput") !== -1 || item.toString().indexOf("TextEdit") !== -1); if (!isInternalInput) { Logger.info("Shell", "InputMethodEngine: Input item unfocused"); @@ -1810,6 +1820,16 @@ Item { z: Constants.zIndexModalOverlay + 100 active: false source: Qt.resolvedUrl("components/IncomingCallOverlay.qml") + onLoaded: { + shell.incomingCallOverlay = item; + TelephonyIntegration.incomingCallOverlay = item; + } + onActiveChanged: { + if (!active) { + shell.incomingCallOverlay = null; + TelephonyIntegration.incomingCallOverlay = null; + } + } } Connections { @@ -1821,33 +1841,26 @@ Item { TelephonyIntegration.callWasAnswered = false; } - target: incomingCallOverlayLoader.item - enabled: incomingCallOverlayLoader.item !== null + target: shell.incomingCallOverlay + enabled: shell.incomingCallOverlay !== null } Connections { - function onIncomingCall(number) { + function onIncomingCallRequested(number) { incomingCallOverlayLoader.active = true; - TelephonyIntegration.incomingCallOverlay = incomingCallOverlayLoader.item; - TelephonyIntegration.handleIncomingCall(number); } - function onCallStateChanged(state) { - TelephonyIntegration.handleCallStateChanged(state); - if ((state === "active" || state === "idle" || state === "terminated") && incomingCallOverlayLoader.item && incomingCallOverlayLoader.item.visible) { - incomingCallOverlayLoader.item.hide(); - incomingCallOverlayLoader.active = false; - } + function onCallOverlayDismissRequested() { + if (shell.incomingCallOverlay && shell.incomingCallOverlay.visible) + shell.incomingCallOverlay.hide(); + incomingCallOverlayLoader.active = false; } - target: typeof TelephonyService !== 'undefined' ? TelephonyService : null - } - - Connections { - function onMessageReceived(sender, text, timestamp) { - TelephonyIntegration.handleMessageReceived(sender, text, timestamp); + function onUnlockRequested() { + if (typeof SessionStore !== "undefined" && SessionStore && !SessionStore.isLocked && SessionStore.showLockScreen) + SessionStore.unlock(); } - target: typeof SMSService !== 'undefined' ? SMSService : null + target: TelephonyIntegration } } diff --git a/shell/qml/apps/native/NativeAppWindow.qml b/shell/qml/apps/native/NativeAppWindow.qml index f9112a4e9..492921a05 100644 --- a/shell/qml/apps/native/NativeAppWindow.qml +++ b/shell/qml/apps/native/NativeAppWindow.qml @@ -46,8 +46,6 @@ MApp { Logger.info("NativeAppWindow", "Surface destroyed while minimized - keeping app alive"); } else { Logger.info("NativeAppWindow", "Surface destroyed (user closed app) - requesting close"); - // Defer: surface teardown can still be in-progress inside QtWayland. - // Requesting close synchronously here can cause use-after-free in compositor view cleanup. Qt.callLater(function () { nativeAppWindow.requestClose(true); }); diff --git a/shell/qml/apps/template/TemplateApp.qml b/shell/qml/apps/template/TemplateApp.qml index a183b1cb0..41bfd2125 100644 --- a/shell/qml/apps/template/TemplateApp.qml +++ b/shell/qml/apps/template/TemplateApp.qml @@ -6,17 +6,13 @@ import QtQuick MApp { id: templateApp - // App metadata (passed from launcher) - // Note: These shadow MApp's properties but are set before MApp's onCompleted property string _appId: "" property string _appName: "" property string _appIcon: "" - // Bind to MApp's properties appId: _appId appName: _appName appIcon: _appIcon - // Template apps have no navigation depth, so back gesture will close them automatically navigationDepth: 0 content: Rectangle { @@ -39,7 +35,6 @@ MApp { Icon { name: { var iconPath = appIcon.replace("qrc:/images/", "").replace(".svg", ""); - // Map app icons to lucide icon names (all downloaded from lucide icon set) if (iconPath === "phone") return "phone"; @@ -82,7 +77,6 @@ MApp { if (iconPath === "settings") return "settings"; - // Generic fallback for unmapped apps return "grid"; } size: 128 @@ -109,7 +103,5 @@ MApp { } } } - // End Rectangle (content) - } } diff --git a/shell/qml/components/AppContextMenu.qml b/shell/qml/components/AppContextMenu.qml index fac1db6c8..01714515c 100644 --- a/shell/qml/components/AppContextMenu.qml +++ b/shell/qml/components/AppContextMenu.qml @@ -99,7 +99,7 @@ Item { anchors.fill: parent onClicked: { Logger.info("AppContextMenu", "App info for: " + appData.name); - HapticService.light(); + HapticManager.light(); appInfo(); hide(); } @@ -152,7 +152,7 @@ Item { anchors.fill: parent onClicked: { Logger.info("AppContextMenu", "Uninstall: " + appData.name); - HapticService.medium(); + HapticManager.medium(); uninstall(); hide(); UIStore.showConfirmDialog("Uninstall " + appData.name + "?", "This app will be removed from your device.", function () { diff --git a/shell/qml/components/BluetoothPairDialog.qml b/shell/qml/components/BluetoothPairDialog.qml index 1a365fc74..cfd038c46 100644 --- a/shell/qml/components/BluetoothPairDialog.qml +++ b/shell/qml/components/BluetoothPairDialog.qml @@ -4,26 +4,14 @@ import MarathonUI.Theme import QtQuick import QtQuick.Controls -/** - * Polished Bluetooth Pairing Dialog - * - * Handles various Bluetooth pairing types: - * - Just Works (no input needed) - * - PIN Entry (user enters 4-6 digit PIN) - * - Passkey Entry (user enters 6-digit passkey) - * - Passkey Confirmation (user confirms displayed passkey) - * - Pairing in progress - * - Success/failure states - */ Item { id: bluetoothPairDialog - // Public API property string deviceName: "" property string deviceAddress: "" - property string deviceType: "" // "headphones", "keyboard", "mouse", "phone", etc. - property string pairingMode: "" // "pin", "passkey", "confirm", "justworks" - property string displayedPasskey: "" // For confirmation mode + property string deviceType: "" + property string pairingMode: "" + property string displayedPasskey: "" property bool isPairing: false property string errorMessage: "" @@ -31,7 +19,6 @@ Item { signal pairConfirmed(bool accepted) signal cancelled - // Show the dialog for pairing function show(name, address, type, mode) { deviceName = name; deviceAddress = address; @@ -49,11 +36,10 @@ Item { } bluetoothPairDialog.visible = true; showAnimation.start(); - HapticService.light(); + HapticManager.light(); Logger.info("BluetoothPairDialog", "Showing pairing dialog for: " + name); } - // Show passkey for confirmation function showPasskeyConfirmation(name, address, type, passkey) { deviceName = name; deviceAddress = address; @@ -64,29 +50,25 @@ Item { errorMessage = ""; bluetoothPairDialog.visible = true; showAnimation.start(); - HapticService.light(); + HapticManager.light(); Logger.info("BluetoothPairDialog", "Showing passkey confirmation: " + passkey); } - // Hide the dialog function hide() { hideAnimation.start(); } - // Show error function showError(message) { errorMessage = message; isPairing = false; - HapticService.medium(); + HapticManager.medium(); } - // Show pairing in progress function showPairing() { isPairing = true; errorMessage = ""; } - // Helper functions function getDeviceIcon(type) { switch (type.toLowerCase()) { case "headphones": @@ -153,13 +135,11 @@ Item { anchors.fill: parent visible: false z: Constants.zIndexModalOverlay + 10 - // Handle escape key Keys.onEscapePressed: { if (!isPairing) bluetoothPairDialog.hide(); } - // Background overlay Rectangle { id: overlay @@ -176,7 +156,6 @@ Item { } } - // Dialog card Rectangle { id: dialogCard @@ -189,7 +168,6 @@ Item { color: MColors.surface border.width: Constants.borderWidthThin border.color: MColors.border - // Glass morphism effect layer.enabled: true Column { @@ -201,12 +179,10 @@ Item { anchors.margins: Constants.spacingLarge spacing: Constants.spacingLarge - // Header row Row { width: parent.width spacing: Constants.spacingMedium - // Device icon Rectangle { width: Constants.touchTargetMedium height: Constants.touchTargetMedium @@ -222,7 +198,6 @@ Item { } } - // Device info Column { width: parent.width - Constants.touchTargetMedium - Constants.spacingMedium spacing: Constants.spacingXSmall @@ -248,7 +223,6 @@ Item { } } - // PIN/Passkey input (for pin/passkey modes) Rectangle { width: parent.width height: Constants.inputHeight @@ -288,7 +262,6 @@ Item { pairButton.clicked(); } - // Placeholder Text { text: "Enter PIN (4-6 digits)" font: pinInput.font @@ -316,7 +289,6 @@ Item { pairButton.clicked(); } - // Placeholder Text { text: "Enter 6-digit passkey" font: passkeyInput.font @@ -334,7 +306,6 @@ Item { } } - // Passkey confirmation display Rectangle { width: parent.width height: Math.round(100 * Constants.scaleFactor) @@ -368,7 +339,6 @@ Item { } } - // Error message Rectangle { width: parent.width height: errorText.height + Constants.spacingMedium @@ -404,7 +374,6 @@ Item { } } - // Pairing progress Column { width: parent.width spacing: Constants.spacingSmall @@ -431,14 +400,12 @@ Item { } } - // Action buttons Row { width: parent.width height: Constants.touchTargetMedium spacing: Constants.spacingMedium visible: !isPairing - // Cancel/Reject button Rectangle { width: (parent.width - Constants.spacingMedium) / 2 height: parent.height @@ -459,7 +426,7 @@ Item { anchors.fill: parent onClicked: { Logger.info("BluetoothPairDialog", "Pairing cancelled/rejected"); - HapticService.light(); + HapticManager.light(); if (pairingMode === "confirm") bluetoothPairDialog.pairConfirmed(false); else @@ -469,7 +436,6 @@ Item { } } - // Pair/Accept button Rectangle { id: pairButton @@ -491,13 +457,11 @@ Item { } MouseArea { - // Just works mode - anchors.fill: parent enabled: canPair() onClicked: { Logger.info("BluetoothPairDialog", "Pair/Accept clicked"); - HapticService.medium(); + HapticManager.medium(); bluetoothPairDialog.showPairing(); if (pairingMode === "confirm") bluetoothPairDialog.pairConfirmed(true); @@ -512,7 +476,6 @@ Item { } } - // Help text Text { text: getHelpText(pairingMode) font.pixelSize: MTypography.sizeXSmall @@ -536,7 +499,6 @@ Item { } } - // Show animation ParallelAnimation { id: showAnimation @@ -559,7 +521,6 @@ Item { } } - // Hide animation SequentialAnimation { id: hideAnimation diff --git a/shell/qml/components/ClipboardManager.qml b/shell/qml/components/ClipboardManager.qml index 6b245ba45..8c2f44aab 100644 --- a/shell/qml/components/ClipboardManager.qml +++ b/shell/qml/components/ClipboardManager.qml @@ -60,8 +60,8 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - ClipboardService.clearHistory(); - HapticService.light(); + ClipboardManagerCpp.clearHistory(); + HapticManager.light(); } } } @@ -72,7 +72,7 @@ Rectangle { height: parent.height - 56 clip: true spacing: Constants.spacingSmall - model: ClipboardService.getHistory() + model: ClipboardManagerCpp.history Text { visible: parent.count === 0 @@ -152,8 +152,8 @@ Rectangle { anchors.fill: parent onClicked: { - ClipboardService.deleteItem(index); - HapticService.light(); + ClipboardManagerCpp.deleteItem(index); + HapticManager.light(); } } @@ -172,8 +172,8 @@ Rectangle { anchors.rightMargin: Math.round(52 * Constants.scaleFactor) onClicked: { Logger.info("ClipboardManager", "Selected item: " + modelData.text.substring(0, 30)); - ClipboardService.copyToClipboard(modelData.text); - HapticService.light(); + ClipboardManagerCpp.copyToClipboard(modelData.text); + HapticManager.light(); UIStore.closeClipboardManager(); } } diff --git a/shell/qml/components/EdgeGestures.qml b/shell/qml/components/EdgeGestures.qml index d3fc358c1..451551f49 100644 --- a/shell/qml/components/EdgeGestures.qml +++ b/shell/qml/components/EdgeGestures.qml @@ -50,7 +50,7 @@ Item { mouse.accepted = true; } else if (activeGesture === "left") { var dragX = mouse.x - startX; - var progress = Math.max(0, Math.min(1, dragX / 300)); // Hub width approximation + var progress = Math.max(0, Math.min(1, dragX / 300)); leftSwipeProgress(progress); mouse.accepted = true; } else if (activeGesture === "right") { @@ -67,7 +67,7 @@ Item { bottomSwipeReleased(committed); } else if (activeGesture === "left") { var dragX = mouse.x - startX; - var committed = dragX > 150; // Hub width half + var committed = dragX > 150; leftSwipeReleased(committed); } else if (activeGesture === "right") { var dragX = startX - mouse.x; diff --git a/shell/qml/components/ErrorToast.qml b/shell/qml/components/ErrorToast.qml index ba64bcede..941d81d36 100644 --- a/shell/qml/components/ErrorToast.qml +++ b/shell/qml/components/ErrorToast.qml @@ -2,12 +2,6 @@ import MarathonUI.Core import MarathonUI.Theme import QtQuick -/** - * ErrorToast - System-wide error notification - * - * Shows temporary error messages with icons and dismiss action - * Auto-dismisses after 5 seconds or user can swipe away - */ Item { id: errorToast @@ -18,7 +12,6 @@ Item { signal dismissed - // Show error with message function show(title, message, icon) { errorTitle = title || "Error"; errorMessage = message; @@ -26,20 +19,18 @@ Item { visible = true; showAnimation.start(); dismissTimer.restart(); - HapticService.medium(); + HapticManager.medium(); Logger.warn("ErrorToast", title + ": " + message); } - // Hide error function hide() { hideAnimation.start(); } anchors.fill: parent visible: false - z: 2500 // Above everything except keyboard + z: 2500 - // Auto-dismiss timer Timer { id: dismissTimer @@ -47,13 +38,11 @@ Item { onTriggered: errorToast.hide() } - // Background overlay (tap to dismiss) MouseArea { anchors.fill: parent onClicked: errorToast.hide() } - // Error card Rectangle { id: errorCard @@ -67,14 +56,9 @@ Item { border.width: Constants.borderWidthMedium border.color: MColors.error opacity: 0 - // Glass morphism layer.enabled: true - // Swipe down to dismiss MouseArea { - // Swiped down far enough - dismiss - // Snap back - id: swipeArea anchors.fill: parent @@ -88,7 +72,6 @@ Item { else snapBackAnimation.start(); } - // Prevent click-through onClicked: mouse.accepted = true } @@ -101,12 +84,10 @@ Item { anchors.margins: Constants.spacingMedium spacing: Constants.spacingSmall - // Header row Row { width: parent.width spacing: Constants.spacingMedium - // Error icon Rectangle { width: Constants.touchTargetSmall height: Constants.touchTargetSmall @@ -122,7 +103,6 @@ Item { } } - // Title and close Column { width: parent.width - Constants.touchTargetSmall - Constants.touchTargetMinimum - Constants.spacingMedium * 2 spacing: Constants.spacingXSmall @@ -146,7 +126,6 @@ Item { } } - // Close button Rectangle { width: Constants.touchTargetMinimum height: Constants.touchTargetMinimum @@ -166,14 +145,13 @@ Item { anchors.fill: parent onClicked: { - HapticService.light(); + HapticManager.light(); errorToast.hide(); } } } } - // Error message Text { text: errorToast.errorMessage color: MColors.textSecondary @@ -190,7 +168,6 @@ Item { } } - // Show animation ParallelAnimation { id: showAnimation @@ -213,7 +190,6 @@ Item { } } - // Hide animation SequentialAnimation { id: hideAnimation @@ -243,7 +219,6 @@ Item { } } - // Snap back animation (after partial swipe) NumberAnimation { id: snapBackAnimation diff --git a/shell/qml/components/IncomingCallOverlay.qml b/shell/qml/components/IncomingCallOverlay.qml index 578ac538e..4d480c720 100644 --- a/shell/qml/components/IncomingCallOverlay.qml +++ b/shell/qml/components/IncomingCallOverlay.qml @@ -3,15 +3,7 @@ import MarathonUI.Theme import QtQuick import QtQuick.Controls -/** - * Lock Screen Incoming Call Overlay - * - * Displays over lock screen and all other content when a call comes in. - * Provides answer/decline buttons and shows caller information. - */ Rectangle { - // 1 second pulse - id: callOverlay property string callerNumber: "" @@ -26,13 +18,11 @@ Rectangle { callerName = name || "Unknown"; isRinging = true; visible = true; - // Play ringtone - if (typeof AudioManager !== 'undefined') - AudioManager.playRingtone(); + if (typeof AudioPolicyControllerCpp !== 'undefined') + AudioPolicyControllerCpp.playRingtone(); - // Vibrate - if (typeof HapticService !== 'undefined') - HapticService.vibrate(1000); + if (typeof HapticManager !== 'undefined') + HapticManager.vibrate(1000); Logger.info("IncomingCallOverlay", "Showing call from: " + number); } @@ -40,20 +30,18 @@ Rectangle { function hide() { isRinging = false; visible = false; - // Stop ringtone - if (typeof AudioManager !== 'undefined') - AudioManager.stopRingtone(); + if (typeof AudioPolicyControllerCpp !== 'undefined') + AudioPolicyControllerCpp.stopRingtone(); Logger.info("IncomingCallOverlay", "Hiding call overlay"); } anchors.fill: parent color: MColors.background - z: Constants.zIndexModalOverlay + 100 // Above everything + z: Constants.zIndexModalOverlay + 100 visible: false opacity: callOverlay.visible ? 1 : 0 - // Background with blur effect Rectangle { anchors.fill: parent color: MColors.background @@ -65,12 +53,10 @@ Rectangle { spacing: Constants.spacingXLarge * 3 width: parent.width * 0.85 - // Caller info section Column { anchors.horizontalCenter: parent.horizontalCenter spacing: Constants.spacingXLarge - // Avatar/Icon with pulsing animation Rectangle { width: Math.round(Constants.iconSizeXLarge * 3) height: Math.round(Constants.iconSizeXLarge * 3) @@ -87,7 +73,6 @@ Rectangle { color: MColors.accent } - // Ripple effect Repeater { model: 3 @@ -129,7 +114,6 @@ Rectangle { } } - // Pulsing animation SequentialAnimation on scale { running: callOverlay.isRinging loops: Animation.Infinite @@ -150,7 +134,6 @@ Rectangle { } } - // Caller details Column { anchors.horizontalCenter: parent.horizontalCenter spacing: Constants.spacingSmall @@ -184,12 +167,10 @@ Rectangle { } } - // Action buttons Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Constants.spacingXLarge * 3 - // Decline button Column { spacing: Constants.spacingMedium @@ -218,7 +199,7 @@ Rectangle { anchors.margins: -Constants.spacingMedium onClicked: { Logger.info("IncomingCallOverlay", "Call declined"); - HapticService.medium(); + HapticManager.medium(); if (typeof TelephonyService !== 'undefined') TelephonyService.hangup(); @@ -244,7 +225,6 @@ Rectangle { } } - // Answer button Column { spacing: Constants.spacingMedium @@ -272,7 +252,7 @@ Rectangle { anchors.margins: -Constants.spacingMedium onClicked: { Logger.info("IncomingCallOverlay", "Call answered"); - HapticService.heavy(); + HapticManager.heavy(); if (typeof TelephonyService !== 'undefined') TelephonyService.answer(); @@ -301,7 +281,6 @@ Rectangle { } } - // Additional actions (swipe down hint on lock screen) Text { anchors.horizontalCenter: parent.horizontalCenter text: SessionStore.isLocked ? "Answering will unlock your device" : "" @@ -313,7 +292,6 @@ Rectangle { } } - // Slide in animation transform: Translate { id: slideTransform diff --git a/shell/qml/components/MarathonAlarmOverlay.qml b/shell/qml/components/MarathonAlarmOverlay.qml index 480406e98..86fcaebb9 100644 --- a/shell/qml/components/MarathonAlarmOverlay.qml +++ b/shell/qml/components/MarathonAlarmOverlay.qml @@ -17,7 +17,7 @@ Rectangle { function dismiss() { if (currentAlarm) { - AlarmManager.dismissAlarm(currentAlarm.id); + AlarmManagerCpp.dismissAlarm(currentAlarm.id); currentAlarm = null; } visible = false; @@ -25,7 +25,7 @@ Rectangle { function snooze() { if (currentAlarm) { - AlarmManager.snoozeAlarm(currentAlarm.id); + AlarmManagerCpp.snoozeAlarm(currentAlarm.id); currentAlarm = null; } visible = false; @@ -128,7 +128,7 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - HapticService.medium(); + HapticManager.medium(); alarmOverlay.snooze(); } } @@ -163,7 +163,7 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - HapticService.medium(); + HapticManager.medium(); alarmOverlay.dismiss(); } } @@ -221,10 +221,13 @@ Rectangle { } Connections { - function onAlarmTriggered(alarm) { - alarmOverlay.show(alarm); + function onAlarmTriggered(id, label) { + alarmOverlay.show({ + "id": id, + "label": label + }); } - target: typeof AlarmManager !== 'undefined' ? AlarmManager : null + target: typeof AlarmManagerCpp !== 'undefined' ? AlarmManagerCpp : null } } diff --git a/shell/qml/components/MarathonAppGrid.qml b/shell/qml/components/MarathonAppGrid.qml index 4d3ec8b29..baf865c79 100644 --- a/shell/qml/components/MarathonAppGrid.qml +++ b/shell/qml/components/MarathonAppGrid.qml @@ -6,19 +6,14 @@ import QtQuick.Effects Item { id: appGrid - // Properties passed from parent (MarathonPageView) property var appModel: null property int pageIndex: 0 - // Grid configuration property int columns: SettingsManagerCpp.appGridColumns > 0 ? SettingsManagerCpp.appGridColumns : (Constants.screenWidth < 700 ? 4 : (Constants.screenWidth < 900 ? 5 : 6)) property int rows: Constants.screenWidth < 700 ? 5 : 4 property int itemsPerPage: columns * rows - // Search gesture properties property real searchPullProgress: 0 property bool searchGestureActive: false - // Calculate start index for this page readonly property int startIndex: pageIndex * itemsPerPage - // Calculate how many items to show on this page readonly property int pageItemCount: { if (!appModel) return 0; @@ -27,11 +22,9 @@ Item { return Math.max(0, Math.min(remaining, itemsPerPage)); } - // Signals signal appLaunched(var app) signal longPress - // Auto-dismiss if gesture ends and search not fully open Timer { id: autoDismissTimer @@ -43,7 +36,6 @@ Item { } } - // Reset to 0 if search closes while gesture active Connections { function onSearchOpenChanged() { if (!UIStore.searchOpen && !searchGestureActive) @@ -53,7 +45,6 @@ Item { target: UIStore } - // Main Grid Layout Grid { id: iconGrid @@ -68,12 +59,10 @@ Item { model: appGrid.pageItemCount Item { - // Get app data from shared model using offset readonly property var appData: appGrid.appModel ? appGrid.appModel.getAppAtIndex(appGrid.startIndex + index) : null width: (iconGrid.width - (appGrid.columns - 1) * iconGrid.spacing) / appGrid.columns height: (iconGrid.height - (appGrid.rows - 1) * iconGrid.spacing) / appGrid.rows - // Icon Transform Animation transform: [ Scale { origin.x: width / 2 @@ -122,7 +111,6 @@ Item { width: Constants.appIconSize height: Constants.appIconSize - // Press glow Rectangle { anchors.centerIn: parent width: parent.width * 1.2 @@ -140,18 +128,16 @@ Item { } } - // Shadow MAppIcon { anchors.centerIn: parent anchors.verticalCenterOffset: 2 source: appData ? appData.icon : "" size: parent.width opacity: 0.3 - color: "black" // Shadow color for font icons + color: "black" z: 1 } - // Icon MAppIcon { id: appIcon @@ -161,7 +147,6 @@ Item { z: 2 } - // Notification Badge Rectangle { anchors.top: parent.top anchors.right: parent.right @@ -198,7 +183,6 @@ Item { } Text { - // Constrain width to parent cell to prevent overflow into neighbors width: parent.parent.width horizontalAlignment: Text.AlignHCenter text: appData ? appData.name : "" @@ -206,9 +190,8 @@ Item { font.pixelSize: MTypography.sizeSmall font.family: MTypography.fontFamily font.weight: Font.DemiBold - elide: Text.ElideRight // Truncate with ... if too long - maximumLineCount: 1 // Single line only - // Text Shadow + elide: Text.ElideRight + maximumLineCount: 1 style: Text.Outline styleColor: Qt.rgba(0, 0, 0, 0.6) } @@ -262,7 +245,7 @@ Item { if (!isSearchGesture && Math.abs(dragDistance) < 15 && deltaTime < 500) { if (appData) { appGrid.appLaunched(appData); - HapticService.medium(); + HapticManager.medium(); } } isSearchGesture = false; @@ -270,8 +253,7 @@ Item { onPressAndHold: { if (appData) { var globalPos = mapToItem(appGrid.parent, mouseX, mouseY); - HapticService.heavy(); - // Context menu logic would go here + HapticManager.heavy(); appGrid.longPress(); } } @@ -280,7 +262,6 @@ Item { } } - // GESTURE MASK (for gaps between icons) MouseArea { id: gestureMask @@ -331,7 +312,6 @@ Item { } } - // Smooth animation when resetting progress Behavior on searchPullProgress { enabled: !searchGestureActive && searchPullProgress > 0.01 && !UIStore.searchOpen diff --git a/shell/qml/components/MarathonAppWindow.qml b/shell/qml/components/MarathonAppWindow.qml index 009d80b6c..37b47bb86 100644 --- a/shell/qml/components/MarathonAppWindow.qml +++ b/shell/qml/components/MarathonAppWindow.qml @@ -17,6 +17,7 @@ Rectangle { property string loadError: "" property bool hasError: false property bool suppressSplash: false + property var appContainer: null property var activeDialogs: [] property bool isClosing: false property Component dialogOverlayComponent @@ -24,7 +25,6 @@ Rectangle { signal closed signal minimized - // Helper function for finishing component creation (moved to top level to avoid hoisting issues) function _finishNativeCreation(component, id, name, icon, surface, sid) { if (!component) return; @@ -71,7 +71,7 @@ Rectangle { } function show(id, name, icon, type, surface, sid) { - if (appWindow.appId === id && type === "native" && appContentLoader.item && appContentLoader.status === Loader.Ready && appContentLoader.item.appInstance && appContentLoader.item.appInstance.visible) { + if (appWindow.appId === id && type === "native" && appWindow.appContainer && appContentLoader.status === Loader.Ready && appWindow.appContainer["appInstance"] && appWindow.appContainer["appInstance"].visible) { if (appWindow.waylandSurface !== null && appWindow.waylandSurface !== surface && surface !== null) { Logger.info("AppWindow", "Secondary toplevel (dialog) detected for: " + id + " - creating overlay"); var dialog = dialogOverlayComponent.createObject(appWindow, { @@ -87,8 +87,8 @@ Rectangle { Logger.info("AppWindow", "Seamlessly updating surface for active app: " + id); appWindow.waylandSurface = surface; appWindow.surfaceId = sid; - appContentLoader.item.appInstance.waylandSurface = surface; - appContentLoader.item.appInstance.surfaceId = sid; + appWindow.appContainer["appInstance"].waylandSurface = surface; + appWindow.appContainer["appInstance"].surfaceId = sid; appWindow.visible = true; appWindow.forceActiveFocus(); if (appWindow.opacity < 1) @@ -106,9 +106,9 @@ Rectangle { hasError = false; loadError = ""; Logger.info("AppWindow", "Showing app window for: " + name + " (type: " + appType + ")"); - if (appContentLoader.item) { - if (appContentLoader.item.children.length > 0) { - var currentChild = appContentLoader.item.children[0]; + if (appWindow.appContainer) { + if (appWindow.appContainer["children"].length > 0) { + var currentChild = appWindow.appContainer["children"][0]; if (currentChild && currentChild.parent) { Logger.info("AppWindow", "Unparenting previous app instance"); currentChild.parent = null; @@ -125,8 +125,8 @@ Rectangle { Logger.info("AppWindow", "Reusing existing native app instance: " + id); existingNativeInstance.visible = true; appWindow.pendingAppInstance = existingNativeInstance; - if (appContentLoader.status === Loader.Ready && appContentLoader.item) { - appContentLoader.item.adoptPendingApp(); + if (appContentLoader.status === Loader.Ready && appWindow.appContainer) { + appWindow.appContainer["adoptPendingApp"](); } else { suppressSplash = true; appContentLoader.sourceComponent = undefined; @@ -140,7 +140,6 @@ Rectangle { if (component.status === Component.Ready) { _finishNativeCreation(component, id, name, icon, surface, sid); } else { - // Capture variables for the callback var capturedComponent = component; var capturedId = id; var capturedName = name; @@ -165,11 +164,11 @@ Rectangle { function detachCurrentApp() { Logger.info("AppWindow", "Detaching current app instance"); - if (appContentLoader.item && appContentLoader.item.children.length > 0) { - var app = appContentLoader.item.children[0]; + if (appWindow.appContainer && appWindow.appContainer["children"].length > 0) { + var app = appWindow.appContainer["children"][0]; if (app) { app.parent = null; - appContentLoader.item.appInstance = null; + appWindow.appContainer["appInstance"] = null; return app; } } @@ -227,9 +226,9 @@ Rectangle { appContentLoader.source = "appInstanceContainer"; appContentLoader.sourceComponent = appInstanceContainer; } else { - instance.parent = appContentLoader.item; - instance.anchors.fill = appContentLoader.item; - appContentLoader.item.appInstance = instance; + instance.parent = appWindow.appContainer; + instance.anchors.fill = appWindow.appContainer; + appWindow.appContainer["appInstance"] = instance; } instance.visible = true; appWindow.visible = true; @@ -254,7 +253,7 @@ Rectangle { anchors.fill: parent color: MColors.background - visible: appWindow.isLoadingComponent && !appWindow.hasError && !(appContentLoader.status === Loader.Ready && appContentLoader.item && appContentLoader.item.appInstance && appContentLoader.item.appInstance.revealReady === true) + visible: appWindow.isLoadingComponent && !appWindow.hasError && !(appContentLoader.status === Loader.Ready && appWindow.appContainer && appWindow.appContainer["appInstance"] && appWindow.appContainer["appInstance"].revealReady === true) z: 1000 Column { @@ -457,6 +456,7 @@ Rectangle { asynchronous: true visible: status === Loader.Ready && item !== null opacity: status === Loader.Ready ? 1 : 0 + onItemChanged: appWindow.appContainer = item onStatusChanged: { if (status === Loader.Error) { Logger.error("AppWindow", "Failed to load app content for: " + appId); diff --git a/shell/qml/components/MarathonBottomBar.qml b/shell/qml/components/MarathonBottomBar.qml index f6ef922c9..d1db6c777 100644 --- a/shell/qml/components/MarathonBottomBar.qml +++ b/shell/qml/components/MarathonBottomBar.qml @@ -74,7 +74,6 @@ Item { startY = mouse.y; } onReleased: mouse => { - // Only trigger click if no significant vertical movement (not a swipe) const deltaY = Math.abs(mouse.y - startY); if (deltaY < 10) { var app = { @@ -104,7 +103,7 @@ Item { width: bottomBar.currentPage === -2 ? Constants.pageIndicatorHubSizeActive : Constants.pageIndicatorHubSizeInactive height: bottomBar.currentPage === -2 ? Constants.pageIndicatorHubSizeActive : Constants.pageIndicatorHubSizeInactive - radius: 999 // BB10: True circle + radius: 999 color: bottomBar.currentPage === -2 ? "#FFFFFF" : "transparent" anchors.verticalCenter: parent.verticalCenter @@ -145,12 +144,12 @@ Item { width: bottomBar.currentPage === -1 ? Constants.pageIndicatorHubSizeActive : Constants.pageIndicatorHubSizeInactive height: bottomBar.currentPage === -1 ? Constants.pageIndicatorHubSizeActive : Constants.pageIndicatorHubSizeInactive - radius: 999 // BB10: True circle + radius: 999 color: bottomBar.currentPage === -1 ? "#FFFFFF" : "transparent" anchors.verticalCenter: parent.verticalCenter Icon { - name: "layers" // Task Switcher usually represented by layers + name: "layers" size: bottomBar.currentPage === -1 ? Constants.iconSizeSmall : Constants.fontSizeSmall anchors.centerIn: parent color: bottomBar.currentPage === -1 ? "black" : "white" @@ -192,7 +191,7 @@ Item { width: index === bottomBar.currentPage ? Constants.pageIndicatorSizeActive : Constants.pageIndicatorSizeInactive height: index === bottomBar.currentPage ? Constants.pageIndicatorSizeActive : Constants.pageIndicatorSizeInactive - radius: 999 // BB10: True circle + radius: 999 color: index === bottomBar.currentPage ? "#FFFFFF" : "#444444" anchors.verticalCenter: parent.verticalCenter @@ -238,7 +237,6 @@ Item { } } - // Drag/scrub gesture handler MouseArea { id: scrubGesture @@ -246,7 +244,6 @@ Item { property int lastHoveredPage: -999 function checkPageUnderMouse(mouseX, mouseY) { - // Check Hub indicator var hubPos = mapToItem(hubIndicator, mouseX, mouseY); if (hubPos.x >= 0 && hubPos.x <= hubIndicator.width && hubPos.y >= 0 && hubPos.y <= hubIndicator.height) { if (lastHoveredPage !== -2) { @@ -255,7 +252,6 @@ Item { } return; } - // Check Frames indicator var framesPos = mapToItem(framesIndicator, mouseX, mouseY); if (framesPos.x >= 0 && framesPos.x <= framesIndicator.width && framesPos.y >= 0 && framesPos.y <= framesIndicator.height) { if (lastHoveredPage !== -1) { @@ -264,7 +260,6 @@ Item { } return; } - // Check each app grid page indicator for (var i = 0; i < appGridIndicators.count; i++) { var indicator = appGridIndicators.itemAt(i); if (indicator) { @@ -343,7 +338,6 @@ Item { startY = mouse.y; } onReleased: mouse => { - // Only trigger click if no significant vertical movement (not a swipe) const deltaY = Math.abs(mouse.y - startY); if (deltaY < 10) { var app = { diff --git a/shell/qml/components/MarathonHub.qml b/shell/qml/components/MarathonHub.qml index 75c7f438c..b07d14c2c 100644 --- a/shell/qml/components/MarathonHub.qml +++ b/shell/qml/components/MarathonHub.qml @@ -107,7 +107,7 @@ Rectangle { variant: "ghost" iconName: "check" onClicked: { - HapticService.light(); + HapticManager.light(); NotificationService.markAllAsRead(); Logger.info("Hub", "Marked all notifications as read"); } @@ -118,7 +118,7 @@ Rectangle { variant: "ghost" iconName: "trash-2" onClicked: { - HapticService.medium(); + HapticManager.medium(); NotificationService.clearAll(); Logger.info("Hub", "Cleared all notifications"); } @@ -269,7 +269,7 @@ Rectangle { anchors.fill: parent onClicked: { Logger.info("Hub", "Notification clicked: " + model.title); - HapticService.light(); + HapticManager.light(); NotificationModel.markAsRead(model.id); if (model.appId) { NavigationRouter.navigateToDeepLink(model.appId, "", { @@ -277,13 +277,13 @@ Rectangle { "action": "view", "from": "hub" }); - Router.goHome(); + Router.goToHub(); } } onPressAndHold: { Logger.info("Hub", "Long press on notification: " + model.id); - HapticService.medium(); - contextMenu.open(); + HapticManager.medium(); + contextMenu.showing = true; } } @@ -301,11 +301,11 @@ Rectangle { iconName: model.isRead ? "mail" : "mail-open" showChevron: false onSettingClicked: { - HapticService.light(); + HapticManager.light(); if (model.isRead) {} else { NotificationModel.markAsRead(model.id); } - contextMenu.close(); + contextMenu.showing = false; } } @@ -315,12 +315,12 @@ Rectangle { showChevron: false visible: model.appId !== "" onSettingClicked: { - HapticService.light(); + HapticManager.light(); if (model.appId) { NavigationRouter.navigateToDeepLink(model.appId, "", {}); - Router.goHome(); + Router.goToHub(); } - contextMenu.close(); + contextMenu.showing = false; } } @@ -329,9 +329,9 @@ Rectangle { iconName: "trash-2" showChevron: false onSettingClicked: { - HapticService.medium(); + HapticManager.medium(); NotificationService.dismissNotification(model.id); - contextMenu.close(); + contextMenu.showing = false; } } } diff --git a/shell/qml/components/MarathonLockScreen.qml b/shell/qml/components/MarathonLockScreen.qml index db76c26cd..d5d4a77fb 100644 --- a/shell/qml/components/MarathonLockScreen.qml +++ b/shell/qml/components/MarathonLockScreen.qml @@ -6,38 +6,46 @@ import QtQuick import QtQuick.Effects Item { - // No overlay needed - content underneath (PIN/launcher) is already visible - id: lockScreen property real swipeProgress: 0 - property string expandedCategory: "" // Track which notification category is expanded - property int idleTimeoutMs: 30000 // 30 seconds idle timeout before blanking screen + property string expandedCategory: "" + property int idleTimeoutMs: 30000 signal unlockRequested signal cameraLaunched signal phoneLaunched signal notificationTapped(int notifId, string appId, string title) - // Reset idle timer on any user interaction + function roleId(name) { + if (!NotificationModel || !name) + return -1; + return NotificationModel.roleId(name); + } + + readonly property int roleIsRead: roleId("isRead") + readonly property int roleAppId: roleId("appId") + readonly property int roleIcon: roleId("icon") + readonly property int roleIdValue: roleId("id") + readonly property int roleTitle: roleId("title") + readonly property int roleBody: roleId("body") + readonly property int roleTimestamp: roleId("timestamp") + function resetIdleTimer() { if (lockScreen.visible && (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp ? DisplayPolicyControllerCpp.screenOn : true)) idleTimer.restart(); } - // Update categories function function updateCategories() { var cats = {}; - // Iterate through NotificationModel using Repeater pattern for (var i = 0; i < NotificationModel.rowCount(); i++) { var idx = NotificationModel.index(i, 0); - var isRead = NotificationModel.data(idx, Shell.NotificationRoles.IsReadRole) || false; - // Skip read notifications + var isRead = NotificationModel.data(idx, roleIsRead) || false; if (isRead) continue; - var appId = NotificationModel.data(idx, Shell.NotificationRoles.AppIdRole) || "other"; - var icon = NotificationModel.data(idx, Shell.NotificationRoles.IconRole) || "bell"; + var appId = NotificationModel.data(idx, roleAppId) || "other"; + var icon = NotificationModel.data(idx, roleIcon) || "bell"; if (!cats[appId]) cats[appId] = { "appId": appId, @@ -47,7 +55,6 @@ Item { cats[appId].count++; } - // Rebuild model categoriesModel.clear(); for (var cat in cats) { categoriesModel.append(cats[cat]); @@ -56,11 +63,7 @@ Item { } anchors.fill: parent - // Keep lock screen visible during entire swipe (only hide when opacity reaches 0) - // This prevents home screen flash-through visible: opacity > 0.01 - // Refresh categories when lock screen becomes visible - // Update SessionStore to show lock icon in status bar onVisibleChanged: { if (visible) { Logger.info("LockScreen", "Lock screen visible - refreshing categories"); @@ -70,7 +73,6 @@ Item { SessionStore.isOnLockScreen = false; } } - // Set initial state on component creation Component.onCompleted: { if (visible) { Logger.info("LockScreen", "Lock screen created visible - setting initial state"); @@ -79,11 +81,9 @@ Item { lockScreen.updateCategories(); } } - // Performance optimization: use layers for static content layer.enabled: true layer.smooth: true - // Idle timer to blank screen after inactivity Timer { id: idleTimer @@ -91,11 +91,9 @@ Item { running: lockScreen.visible && (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp ? DisplayPolicyControllerCpp.screenOn : true) repeat: false onTriggered: { - // Check if any surface inhibits idle (e.g., video playback) - // The compositor checks all native wayland surfaces for inhibitsIdle if (typeof compositor !== 'undefined' && compositor.hasIdleInhibitingSurface) { Logger.info("LockScreen", "Idle inhibitor active (video playback?) - postponing screen blank"); - idleTimer.restart(); // Keep checking + idleTimer.restart(); return; } Logger.info("LockScreen", "Idle timeout - blanking screen"); @@ -106,12 +104,10 @@ Item { } } - // Auto-expand notifications when they arrive while on lock screen Connections { function onNotificationReceived(notification) { if (lockScreen.visible) { Logger.info("LockScreen", "New notification while on lock screen: " + notification.title); - // Wake screen if off var screenOn = (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) ? DisplayPolicyControllerCpp.screenOn : true; if (!screenOn) { if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) @@ -119,10 +115,8 @@ Item { else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) DisplayManagerCpp.setScreenState(true); } - // Auto-expand the notification's category var appId = notification.appId || "other"; expandedCategory = appId; - // Reset idle timer resetIdleTimer(); Logger.info("LockScreen", "Auto-expanded category: " + appId); } @@ -131,14 +125,9 @@ Item { target: NotificationService } - // Restart idle timer when screen turns on Connections { - // Note: Don't lock the session when screen turns off - preserve grace period! - // When screen turns on, showLock() displays lock screen but keeps isLocked state - function onScreenStateChanged(isOn) { if (isOn) { - // Show lock screen when screen turns on (preserves grace period if unlocked) SessionStore.showLock(); if (lockScreen.visible) { Logger.info("LockScreen", "Screen turned on - starting idle timer"); @@ -150,12 +139,10 @@ Item { target: typeof DisplayManagerCpp !== "undefined" ? DisplayManagerCpp : null } - // Categories Model - Moved to top level for reliable access ListModel { id: categoriesModel } - // Update categories when notifications change Connections { function onCountChanged() { lockScreen.updateCategories(); @@ -169,10 +156,8 @@ Item { anchors.fill: parent z: 1 - // Fade out effect as user swipes opacity: 1 - Math.pow(swipeProgress, 0.7) - // Wallpaper with proper caching Image { anchors.fill: parent source: WallpaperStore.path @@ -180,12 +165,10 @@ Item { asynchronous: true cache: true smooth: true - // GPU-accelerated layer layer.enabled: true layer.smooth: true } - // Dismiss expanded notifications when tapping elsewhere MouseArea { anchors.fill: parent z: 1 @@ -204,18 +187,15 @@ Item { z: 5 } - // Time and Date - centered, or pushed up if unread notifications present Column { id: clockColumn anchors.horizontalCenter: parent.horizontalCenter spacing: Constants.spacingSmall - width: parent.width * 0.9 // Constrain width for children - // Default state (centered) + width: parent.width * 0.9 anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: Math.round(-80 * Constants.scaleFactor) onYChanged: Logger.info("LockScreen", "ClockColumn Y changed to: " + y) - // GPU layer for text rendering layer.enabled: true layer.smooth: true @@ -223,7 +203,6 @@ Item { text: SystemStatusStore.timeString color: MColors.text font.pixelSize: Constants.fontSizeGigantic - // font.family: MTypography.fontFamily <-- REMOVED to match Task Switcher font.weight: Font.Thin anchors.horizontalCenter: parent.horizontalCenter renderType: Text.NativeRendering @@ -255,7 +234,6 @@ Item { } } - // Media Player on Lock Screen Item { width: parent.width height: Constants.spacingMedium @@ -269,17 +247,14 @@ Item { anchors.horizontalCenter: parent.horizontalCenter visible: hasMedia border.width: Constants.borderWidthThin - border.color: Qt.rgba(0, 191 / 255, 165 / 255, 0.3) // MColors.marathonTealBorder approx + border.color: Qt.rgba(0, 191 / 255, 165 / 255, 0.3) - // Override background opacity for lock screen integration - // Dark teal gradient background gradient: Gradient { GradientStop { position: 0 color: Qt.rgba(0, 191 / 255, 165 / 255, 0.15) } - // MColors.marathonTealGlowTop approx GradientStop { position: 1 color: Qt.rgba(0, 0, 0, 0.2) @@ -287,7 +262,6 @@ Item { } } - // States for layout transitions states: State { name: "hasNotifications" when: categoriesModel.count > 0 @@ -299,9 +273,8 @@ Item { } PropertyChanges { - target: clockColumn - anchors.topMargin: Math.round(80 * Constants.scaleFactor) - anchors.verticalCenterOffset: 0 + clockColumn.anchors.topMargin: Math.round(80 * Constants.scaleFactor) + clockColumn.anchors.verticalCenterOffset: 0 } } @@ -319,11 +292,9 @@ Item { } } - // BB10-style Hub Notifications Item { id: notificationContainer - // Anchor to clockColumn to ensure no overlap anchors.top: clockColumn.bottom anchors.topMargin: Constants.spacingMedium anchors.bottom: parent.bottom @@ -333,16 +304,11 @@ Item { z: 10 onYChanged: Logger.info("LockScreen", "NotificationContainer Y changed to: " + y) - // Category icons on left edge Column { - // Expose category count for clock positioning - NO LONGER NEEDED, using top level model - // property alias unreadCategoryCount: categoriesModel.count - id: categoryIcons anchors.left: parent.left anchors.leftMargin: Constants.spacingMedium - // Position relative to container anchors.top: categoriesModel.count <= 3 ? parent.top : undefined anchors.topMargin: categoriesModel.count <= 3 ? Math.round(20 * Constants.scaleFactor) : 0 anchors.verticalCenter: categoriesModel.count > 3 ? parent.verticalCenter : undefined @@ -359,7 +325,6 @@ Item { width: Math.round(56 * Constants.scaleFactor) height: Math.round(56 * Constants.scaleFactor) - // Category icon Rectangle { id: categoryIconBg @@ -380,7 +345,6 @@ Item { anchors.centerIn: parent } - // Badge count Rectangle { visible: model.count > 0 anchors.right: parent.right @@ -430,7 +394,7 @@ Item { MouseArea { anchors.fill: parent onClicked: { - HapticService.light(); + HapticManager.light(); resetIdleTimer(); if (expandedCategory === category) { expandedCategory = ""; @@ -448,7 +412,6 @@ Item { Item { id: lineAndChevron - // Calculate chevron Y position based on active category function calculateChevronY() { var activeIndex = -1; for (var i = 0; i < categoriesModel.count; i++) { @@ -460,11 +423,8 @@ Item { if (activeIndex === -1) activeIndex = 0; - // Account for item height (56px), spacing (20px), and centering - // Icon center: activeIndex * (56 + 20) + 28 - // Chevron is 16px, center it: icon_center - 8 = activeIndex * 76 + 20 var itemHeight = Math.round(56 * Constants.scaleFactor); - var itemSpacing = Math.round(20 * Constants.scaleFactor); // Constants.spacingLarge + var itemSpacing = Math.round(20 * Constants.scaleFactor); var yPos = activeIndex * (itemHeight + itemSpacing) + Math.round(20 * Constants.scaleFactor); Logger.info("LockScreen", "Chevron Y calculated: " + yPos + " for category: " + expandedCategory + " at index: " + activeIndex); return yPos; @@ -481,14 +441,13 @@ Item { Connections { function onExpandedCategoryChanged() { chevronCanvas.y = lineAndChevron.calculateChevronY(); - topLineSegment.height = chevronCanvas.y; // Line goes right up to chevron - bottomLineSegment.anchors.topMargin = chevronCanvas.y + Math.round(16 * Constants.scaleFactor); // Start right after chevron + topLineSegment.height = chevronCanvas.y; + bottomLineSegment.anchors.topMargin = chevronCanvas.y + Math.round(16 * Constants.scaleFactor); } target: lockScreen } - // Top line segment (from top to chevron) Rectangle { id: topLineSegment @@ -510,11 +469,10 @@ Item { } } - // Chevron positioned at the active icon's vertical center Canvas { id: chevronCanvas - anchors.right: topLineSegment.horizontalCenter // Trailing edge aligned with line center + anchors.right: topLineSegment.horizontalCenter y: Math.round(20 * Constants.scaleFactor) width: Math.round(12 * Constants.scaleFactor) height: Math.round(16 * Constants.scaleFactor) @@ -530,7 +488,6 @@ Item { ctx.lineWidth = 2; ctx.globalAlpha = 0.6; ctx.beginPath(); - // Draw left-pointing chevron (right edge is the trailing edge) ctx.moveTo(width, 0); ctx.lineTo(0, height / 2); ctx.lineTo(width, height); @@ -547,7 +504,6 @@ Item { } } - // Bottom line segment (from chevron to bottom) Rectangle { id: bottomLineSegment @@ -571,7 +527,6 @@ Item { } } - // Notification list for active category - positioned to the right of line, aligned with icons ListView { id: notificationList @@ -582,18 +537,17 @@ Item { for (var i = 0; i < NotificationModel.rowCount(); i++) { var idx = NotificationModel.index(i, 0); - var isRead = NotificationModel.data(idx, Shell.NotificationRoles.IsReadRole) || false; - // Skip read notifications + var isRead = NotificationModel.data(idx, roleIsRead) || false; if (isRead) continue; - var appId = NotificationModel.data(idx, Shell.NotificationRoles.AppIdRole) || "other"; + var appId = NotificationModel.data(idx, roleAppId) || "other"; if (appId === expandedCategory) filteredNotificationsModel.append({ - "notifId": NotificationModel.data(idx, Shell.NotificationRoles.IdRole), - "title": NotificationModel.data(idx, Shell.NotificationRoles.TitleRole), - "body": NotificationModel.data(idx, Shell.NotificationRoles.BodyRole), - "timestamp": NotificationModel.data(idx, Shell.NotificationRoles.TimestampRole) + "notifId": NotificationModel.data(idx, roleIdValue), + "title": NotificationModel.data(idx, roleTitle), + "body": NotificationModel.data(idx, roleBody), + "timestamp": NotificationModel.data(idx, roleTimestamp) }); } } @@ -609,7 +563,6 @@ Item { spacing: Constants.spacingSmall clip: true z: 40 - // Update filtered notifications when category changes onVisibleChanged: { if (visible) updateFilteredNotifications(); @@ -645,7 +598,6 @@ Item { anchors.margins: Constants.spacingMedium spacing: 0 - // Left content Column { width: parent.width - timestampText.width - Constants.spacingMedium anchors.verticalCenter: parent.verticalCenter @@ -689,7 +641,6 @@ Item { } } - // Timestamp on right Text { id: timestampText @@ -713,22 +664,19 @@ Item { MouseArea { anchors.fill: parent onClicked: { - HapticService.light(); + HapticManager.light(); resetIdleTimer(); Logger.info("LockScreen", "Notification tapped: " + model.title); - // Get the full notification data var notifId = model.notifId || 0; var appId = ""; - // Find the appId from the original notification for (var i = 0; i < NotificationModel.rowCount(); i++) { var idx = NotificationModel.index(i, 0); - var id = NotificationModel.data(idx, Shell.NotificationRoles.IdRole); + var id = NotificationModel.data(idx, roleIdValue); if (id === notifId) { - appId = NotificationModel.data(idx, Shell.NotificationRoles.AppIdRole) || ""; + appId = NotificationModel.data(idx, roleAppId) || ""; break; } } - // Emit signal with notification info notificationTapped(notifId, appId, model.title); } } @@ -736,7 +684,6 @@ Item { } } - // Use actual BottomBar component MarathonBottomBar { id: lockScreenBottomBar @@ -747,18 +694,17 @@ Item { z: 10 onAppLaunched: app => { if (app.id === "phone") { - HapticService.medium(); + HapticManager.medium(); Logger.info("LockScreen", "Phone quick action tapped"); phoneLaunched(); } else if (app.id === "camera") { - HapticService.medium(); + HapticManager.medium(); Logger.info("LockScreen", "Camera quick action tapped"); cameraLaunched(); } } } - // Swipe up indicator - vertically centered with bottom bar icons Column { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: lockScreenBottomBar.verticalCenter @@ -809,10 +755,7 @@ Item { } } - // Optimized touch handling with momentum MouseArea { - // Don't reject here - we need to track the gesture to determine if it's a swipe or tap - property real startY: 0 property real lastY: 0 property real velocity: 0 @@ -839,37 +782,29 @@ Item { lastY = mouse.y; lastTime = now; - // Allow swipes from anywhere on the screen (including swipe indicator area) const totalDelta = startY - mouse.y; if (totalDelta > 10) { isDragging = true; - // Once we detect dragging, stop propagating to notification taps mouse.accepted = true; } if (isDragging) { - // Super easy: only need to swipe 15% of screen height const threshold = height * 0.15; swipeProgress = Math.max(0, Math.min(1, totalDelta / threshold)); - // Haptic feedback at 50% and 100% if (swipeProgress > 0.5 && swipeProgress < 0.55) - HapticService.light(); + HapticManager.light(); } } onReleased: mouse => { if (isDragging) { - // Very low threshold: 20% progress OR positive velocity if (swipeProgress > 0.2 || velocity > 0.5) { - // Animate to complete swipeProgress = 1; - HapticService.medium(); + HapticManager.medium(); unlockTimer.start(); } else { - // Snap back swipeProgress = 0; expandedCategory = ""; } } else { - // Was a tap, not a swipe - notifications will handle it via their MouseAreas Logger.info("LockScreen", "Tap detected (no drag), x=" + mouse.x + ", y=" + mouse.y); } isDragging = false; @@ -887,7 +822,6 @@ Item { } } - // Smooth spring animation for swipe progress Behavior on swipeProgress { enabled: swipeProgress < 1 diff --git a/shell/qml/components/MarathonNavBar.qml b/shell/qml/components/MarathonNavBar.qml index e8c70bba0..d15c67fb1 100644 --- a/shell/qml/components/MarathonNavBar.qml +++ b/shell/qml/components/MarathonNavBar.qml @@ -15,7 +15,7 @@ Rectangle { property real gestureProgress: 0 property bool keyboardVisible: false property bool searchActive: false - property bool pinScreenMode: false // When true, hide pill and search button + property bool pinScreenMode: false signal swipeLeft signal swipeRight @@ -49,7 +49,7 @@ Rectangle { height: Constants.spacingXSmall radius: Constants.borderRadiusSharp color: MColors.text - opacity: pinScreenMode ? 0 : 0.9 // Hide in PIN screen mode + opacity: pinScreenMode ? 0 : 0.9 visible: !pinScreenMode antialiasing: Constants.enableAntialiasing x: targetX + dragX @@ -76,7 +76,6 @@ Rectangle { } } - // Search button (small, bottom left of nav bar) Item { id: searchButton @@ -86,7 +85,7 @@ Rectangle { width: 16 height: 16 z: 300 - visible: !pinScreenMode // Hide in PIN screen mode + visible: !pinScreenMode opacity: pinScreenMode ? 0 : 1 Rectangle { @@ -126,13 +125,12 @@ Rectangle { anchors.fill: parent anchors.margins: -4 onClicked: { - HapticService.light(); + HapticManager.light(); navBar.toggleSearch(); } } } - // Keyboard button (small, bottom right of nav bar) Item { id: keyboardButton @@ -180,17 +178,13 @@ Rectangle { anchors.fill: parent anchors.margins: -4 onClicked: { - HapticService.light(); + HapticManager.light(); navBar.toggleKeyboard(); } } } MouseArea { - // Right zone (keyboard) always enabled - // When app is open, swipe left = back gesture - // Otherwise, navigate pages left - id: navMouseArea property real velocityX: 0 @@ -214,13 +208,9 @@ Rectangle { velocityX = 0; velocityY = 0; isVerticalGesture = false; - // Detect if press is in left or right zone (narrower zones, more centered on buttons) var leftBoundary = parent.width * 0.15; - // Left 15% for search button var rightBoundary = parent.width * 0.85; - // Right 15% for keyboard button isLeftZone = !pinScreenMode && mouse.x < leftBoundary; - // Left zone disabled in PIN mode (no search) isRightZone = mouse.x > rightBoundary; if (isLeftZone) Logger.info("NavBar", "🔵 Touch in LEFT ZONE (x=" + mouse.x + ")"); @@ -232,7 +222,7 @@ Rectangle { var dt = now - lastTime; if (dt > 0) { velocityX = (mouse.x - lastX) / dt * 1000; - velocityY = (mouse.y - lastY) / dt * 1000; // Positive = downward, negative = upward (standard) + velocityY = (mouse.y - lastY) / dt * 1000; } lastX = mouse.x; lastY = mouse.y; @@ -246,7 +236,6 @@ Rectangle { if (isVerticalGesture) { currentY = Math.max(0, diffY); currentX = 0; - // Drag Quick Settings up when open if (UIStore.quickSettingsOpen || UIStore.quickSettingsHeight > 0) { if (!UIStore.quickSettingsDragging) UIStore.quickSettingsDragging = true; @@ -254,10 +243,9 @@ Rectangle { var newHeight = UIStore.quickSettingsHeight - diffY; var maxHeight = UIStore.shellRef ? UIStore.shellRef.maxQuickSettingsHeight : 1000; UIStore.quickSettingsHeight = Math.max(0, Math.min(maxHeight, newHeight)); - startY = mouse.y; // Update startY for continuous tracking + startY = mouse.y; } else if (isAppOpen) { var oldProgress = gestureProgress; - // Density-aware progress: 25% of screen height = 100% progress var progressThreshold = Constants.screenHeight * 0.25; gestureProgress = Math.min(1, diffY / progressThreshold); if (oldProgress <= 0.15 && gestureProgress > 0.15) @@ -279,18 +267,16 @@ Rectangle { "isAppOpen": isAppOpen, "quickSettingsOpen": UIStore.quickSettingsOpen }); - // Check for left/right zone swipe-up quick actions if ((isLeftZone || isRightZone) && diffY > 50) { if (isLeftZone) { Logger.info("NavBar", "🔵 LEFT ZONE SWIPE-UP → Toggle Search (diffY: " + diffY + ")"); - HapticService.medium(); + HapticManager.medium(); navBar.toggleSearch(); } else if (isRightZone) { Logger.info("NavBar", "🔴 RIGHT ZONE SWIPE-UP → Toggle Keyboard (diffY: " + diffY + ")"); - HapticService.medium(); + HapticManager.medium(); navBar.toggleKeyboard(); } - // Reset state and return startX = 0; startY = 0; currentX = 0; @@ -303,12 +289,10 @@ Rectangle { gestureProgress = 0; return; } - // Snap Quick Settings open/closed based on threshold or velocity if ((UIStore.quickSettingsOpen || UIStore.quickSettingsHeight > 0) && isVerticalGesture) { Logger.info("NavBar", "Quick Settings height: " + UIStore.quickSettingsHeight + ", diffY: " + diffY); UIStore.quickSettingsDragging = false; var threshold = UIStore.shellRef ? UIStore.shellRef.quickSettingsThreshold : 400; - // Check for fling up gesture (velocity < -500 px/s = upward swipe, closing) var isFlingUp = velocityY < -500; if (isFlingUp || UIStore.quickSettingsHeight < threshold) UIStore.closeQuickSettings(); @@ -319,7 +303,6 @@ Rectangle { "velocityY": velocityY, "flingUp": isFlingUp }); - // Reset gesture state startX = 0; startY = 0; velocityX = 0; @@ -331,11 +314,9 @@ Rectangle { gestureProgress = 0; return; } - // Close Search with upward gesture - ONLY close search, don't navigate if (UIStore.searchOpen && isVerticalGesture && diffY > 60) { Logger.info("NavBar", "Closing Search with upward gesture"); UIStore.closeSearch(); - // Reset all state and RETURN to prevent further navigation startX = 0; startY = 0; velocityX = 0; @@ -345,10 +326,8 @@ Rectangle { gestureProgress = 0; return; } - // Only process navigation gestures if search is NOT open if (isVerticalGesture && diffY > 30 && !UIStore.searchOpen) { if (diffY > longSwipeThreshold) { - // Long swipe up - Always go to task switcher Logger.info("NavBar", " LONG SWIPE UP TRIGGERED "); Logger.info("NavBar", " diffY: " + diffY + ", longSwipeThreshold: " + longSwipeThreshold); Logger.info("NavBar", " isAppOpen: " + isAppOpen); @@ -359,22 +338,16 @@ Rectangle { currentY = 0; gestureProgress = 0; } else if (isAppOpen && (diffY > 100 || gestureProgress > 0.4)) { - // Short swipe up while app is open - Minimize app Logger.info("NavBar", "⬆⬆⬆ MINIMIZE GESTURE TRIGGERED ⬆⬆⬆"); Logger.info("NavBar", " diffY: " + diffY + ", gestureProgress: " + gestureProgress); Logger.info("NavBar", " isAppOpen: " + isAppOpen); Logger.info("NavBar", " UIStore.appWindowOpen: " + UIStore.appWindowOpen); Logger.info("NavBar", " UIStore.settingsOpen: " + UIStore.settingsOpen); - // Pass negative velocityY (upward is negative in Qt coordinate system usually, but here diffY is positive up) - // Wait, velocityY calculation: velocityY = (mouse.y - lastY) / dt * 1000; - // If moving UP, mouse.y decreases, so velocityY is NEGATIVE. minimizeApp(velocityY); - // Reset immediately - Shell takes over animation via isTransitioningToActiveFrames currentX = 0; currentY = 0; gestureProgress = 0; } else if (diffY > shortSwipeThreshold) { - // Short swipe up - Go home Logger.info("NavBar", "Short swipe up - Go home"); shortSwipeUp(); currentX = 0; @@ -397,14 +370,12 @@ Rectangle { "isAppOpen": isAppOpen, "diffX": diffX }); - // When app is open, swipe right = forward gesture (Shell will try handleSystemForward()). swipeRight(); } currentX = 0; currentY = 0; gestureProgress = 0; } else { - // Cancelled gesture - reset immediately Logger.info("NavBar", " GESTURE CANCELLED - diffX: " + diffX + ", diffY: " + diffY); currentX = 0; currentY = 0; diff --git a/shell/qml/components/MarathonOOBE.qml b/shell/qml/components/MarathonOOBE.qml index 094855e9d..04677135b 100644 --- a/shell/qml/components/MarathonOOBE.qml +++ b/shell/qml/components/MarathonOOBE.qml @@ -6,15 +6,11 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -/** - * Marathon OS - Out-of-Box Experience (OOBE) - * - * World-class first-run setup with Marathon design system - */ Item { id: oobeRoot - // State management + property WiFiPasswordDialog activePasswordDialog: null + property var pendingPasswordRequest: null property int currentPage: 0 readonly property var pages: [ { @@ -45,12 +41,10 @@ Item { visible: !SettingsManagerCpp.firstRunComplete z: Constants.zIndexModalOverlay - // Background with subtle radial gradient pattern Rectangle { anchors.fill: parent color: MColors.background - // Subtle radial gradient overlay for depth Rectangle { anchors.fill: parent opacity: 0.08 @@ -76,9 +70,6 @@ Item { } } - // ========================================================================= - // Use actual Marathon Status Bar component - // ========================================================================= MarathonStatusBar { id: statusBar @@ -88,7 +79,6 @@ Item { z: 100 } - // Page container SwipeView { id: swipeView @@ -101,7 +91,6 @@ Item { interactive: false clip: true - // Page 0: Welcome Item { Column { anchors.centerIn: parent @@ -113,15 +102,13 @@ Item { height: Math.round(180 * Constants.scaleFactor) Image { - // Removed animations for better performance - anchors.centerIn: parent width: Math.min(parent.width * 0.45, Math.round(180 * Constants.scaleFactor)) height: width source: "qrc:/images/marathon.png" fillMode: Image.PreserveAspectFit - smooth: false // Better performance - mipmap: false // Better performance + smooth: false + mipmap: false asynchronous: true cache: true } @@ -161,9 +148,7 @@ Item { } } - // Page 1: WiFi Item { - // Header row with title and skip button Row { id: wifiHeader @@ -208,7 +193,6 @@ Item { width: parent.width } - // WiFi toggle - styled like settings app MCard { width: parent.width height: MSpacing.touchTargetMedium @@ -259,7 +243,6 @@ Item { } } - // Available Networks Section Column { width: parent.width spacing: MSpacing.md @@ -274,7 +257,6 @@ Item { color: MColors.text } - // Network List Repeater { model: (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) ? NetworkManagerCpp.availableNetworks : [] @@ -286,10 +268,9 @@ Item { onPressedChanged: { border.color = pressed ? MColors.accent : MColors.border; } - // Use MCard's built-in onClicked signal instead of redundant MouseArea onClicked: { Logger.info("OOBE", "WiFi network selected:", modelData.ssid); - HapticService.light(); + HapticManager.light(); wifiPasswordDialogLoader.show(modelData.ssid, modelData.strength, modelData.security, modelData.secured); } @@ -298,23 +279,17 @@ Item { anchors.margins: MSpacing.md spacing: MSpacing.md - // Signal strength icon (proper signal bars, not opacity) Icon { - // 3-4 bars (excellent) - name: { - // Use proper signal bar icons based on strength if (modelData.strength === 0) return "wifi-zero"; if (modelData.strength <= 33) return "wifi-low"; - // 1-2 bars (weak) if (modelData.strength <= 66) return "wifi"; - // 2-3 bars (good) return "wifi-high"; } size: Math.round(24 * Constants.scaleFactor) @@ -340,7 +315,6 @@ Item { Row { spacing: MSpacing.sm - // Show "Connected" for active network Text { text: modelData.connected ? "Connected" : (modelData.security || "Open") font.pixelSize: MTypography.sizeSmall @@ -353,7 +327,7 @@ Item { text: "•" font.pixelSize: MTypography.sizeSmall color: MColors.textSecondary - visible: !modelData.connected // Hide separator for connected networks + visible: !modelData.connected } Text { @@ -361,20 +335,19 @@ Item { font.pixelSize: MTypography.sizeSmall font.family: MTypography.fontFamily color: MColors.textSecondary - visible: !modelData.connected // Hide strength % for connected networks (icon is enough) + visible: !modelData.connected } Icon { name: "lock" size: Math.round(16 * Constants.scaleFactor) color: MColors.textTertiary - visible: modelData.secured && !modelData.connected // Hide lock for connected networks + visible: modelData.secured && !modelData.connected anchors.verticalCenter: parent.verticalCenter } } } - // Checkmark for connected network Icon { name: "check-circle" size: Math.round(24 * Constants.scaleFactor) @@ -390,9 +363,7 @@ Item { } } - // Page 2: Time & Date Item { - // Header row with title Row { id: timeHeader @@ -514,7 +485,7 @@ Item { height: MSpacing.touchTargetSmall onClicked: { SettingsManagerCpp.timeFormat = "12h"; - HapticService.light(); + HapticManager.light(); } } @@ -524,7 +495,7 @@ Item { height: MSpacing.touchTargetSmall onClicked: { SettingsManagerCpp.timeFormat = "24h"; - HapticService.light(); + HapticManager.light(); } } } @@ -544,9 +515,7 @@ Item { } } - // Page 3: Gestures Item { - // Header row with title Row { id: gesturesHeader @@ -673,7 +642,6 @@ Item { } } - // Page 4: Complete Item { Column { anchors.centerIn: parent @@ -719,9 +687,6 @@ Item { } } - // ========================================================================= - // Navigation buttons - // ========================================================================= Row { id: navigationRow @@ -742,7 +707,7 @@ Item { visible: oobeRoot.currentPage > 0 onClicked: { if (oobeRoot.currentPage > 0) { - HapticService.light(); + HapticManager.light(); oobeRoot.currentPage--; } } @@ -760,7 +725,7 @@ Item { text: oobeRoot.currentPage === oobeRoot.pages.length - 1 ? "Get Started" : "Next" variant: "primary" onClicked: { - HapticService.light(); + HapticManager.light(); if (oobeRoot.currentPage < oobeRoot.pages.length - 1) { oobeRoot.currentPage++; } else { @@ -771,9 +736,6 @@ Item { } } - // ========================================================================= - // Page indicators - styled like shell - // ========================================================================= Row { id: pageIndicatorRow @@ -781,7 +743,7 @@ Item { anchors.bottom: navBar.top anchors.bottomMargin: MSpacing.xxl spacing: MSpacing.md - height: Math.round(20 * Constants.scaleFactor) // Fixed height for vertical alignment + height: Math.round(20 * Constants.scaleFactor) Repeater { model: oobeRoot.pages.length @@ -792,12 +754,11 @@ Item { radius: oobeRoot.currentPage === index ? Math.round(10 * Constants.scaleFactor) : Math.round(6 * Constants.scaleFactor) color: oobeRoot.currentPage === index ? MColors.accent : MColors.textTertiary opacity: oobeRoot.currentPage === index ? 1 : 0.5 - anchors.verticalCenter: parent.verticalCenter // Vertically align all dots + anchors.verticalCenter: parent.verticalCenter } } } - // Skip button - positioned in top right of page content area MButton { anchors.top: swipeView.top anchors.topMargin: MSpacing.lg @@ -809,14 +770,11 @@ Item { z: 200 onClicked: { SettingsManagerCpp.firstRunComplete = true; - HapticService.light(); + HapticManager.light(); oobeRoot.setupComplete(); } } - // ========================================================================= - // Use actual Marathon Nav Bar component - // ========================================================================= MarathonNavBar { id: navBar @@ -824,33 +782,51 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom z: 100 - // Hook up nav bar back gesture to OOBE navigation onSwipeLeft: { if (oobeRoot.currentPage > 0) { - HapticService.light(); + HapticManager.light(); oobeRoot.currentPage--; } } onSwipeRight: { if (oobeRoot.currentPage < oobeRoot.pages.length - 1) { - HapticService.light(); + HapticManager.light(); oobeRoot.currentPage++; } } } - // WiFi password dialog Loader { id: wifiPasswordDialogLoader function show(ssid, strength, security, secured) { active = true; - if (item) - item.show(ssid, strength, security, secured); + oobeRoot.pendingPasswordRequest = { + "ssid": ssid, + "strength": strength, + "security": security, + "secured": secured + }; + if (oobeRoot.activePasswordDialog) { + oobeRoot.activePasswordDialog.show(ssid, strength, security, secured); + oobeRoot.pendingPasswordRequest = null; + } } anchors.fill: parent active: false + onLoaded: { + oobeRoot.activePasswordDialog = item; + if (oobeRoot.pendingPasswordRequest) { + var request = oobeRoot.pendingPasswordRequest; + oobeRoot.pendingPasswordRequest = null; + oobeRoot.activePasswordDialog.show(request.ssid, request.strength, request.security, request.secured); + } + } + onActiveChanged: { + if (!active) + oobeRoot.activePasswordDialog = null; + } sourceComponent: WiFiPasswordDialog { onConnectRequested: (ssid, password) => { @@ -866,16 +842,15 @@ Item { Connections { function onConnectionSuccess() { - if (wifiPasswordDialogLoader.active && wifiPasswordDialogLoader.item) { - wifiPasswordDialogLoader.item.hide(); - wifiPasswordDialogLoader.active = false; - } - HapticService.medium(); + if (oobeRoot.activePasswordDialog) + oobeRoot.activePasswordDialog.hide(); + wifiPasswordDialogLoader.active = false; + HapticManager.medium(); } function onConnectionFailed(message) { - if (wifiPasswordDialogLoader.active && wifiPasswordDialogLoader.item) - wifiPasswordDialogLoader.item.showError(message); + if (oobeRoot.activePasswordDialog) + oobeRoot.activePasswordDialog.showError(message); } target: typeof NetworkManagerCpp !== "undefined" ? NetworkManagerCpp : null diff --git a/shell/qml/components/MarathonPageView.qml b/shell/qml/components/MarathonPageView.qml index 89cc68f37..b7cc3f958 100644 --- a/shell/qml/components/MarathonPageView.qml +++ b/shell/qml/components/MarathonPageView.qml @@ -2,17 +2,15 @@ import MarathonUI.Theme import QtQuick Item { - // Don't force focus - let Shell manage keyboard input - id: pageViewContainer property alias currentIndex: pageView.currentIndex property alias currentPage: pageView.currentPage property alias isGestureActive: pageView.isGestureActive property alias count: pageView.count - property real searchPullProgress: 0 // Exposed to Shell for search overlay - property int internalAppGridPage: 0 // Track the internal page within app grid - property var compositor: null // Wayland compositor reference for native apps + property real searchPullProgress: 0 + property int internalAppGridPage: 0 + property var compositor: null property bool initialPageSet: false signal hubVisible(bool visible) @@ -28,17 +26,13 @@ Item { } function navigateToPage(page) { - // page: -2 = Hub, -1 = Task Switcher, 0+ = App Grid pages if (page === -2) { pageView.currentIndex = 0; } else if (page === -1) { pageView.currentIndex = 1; } else if (page >= 0) { - // Store the target app grid page pageViewContainer.internalAppGridPage = page; - // Navigate to app grid (index 2) pageView.currentIndex = 2; - // Use a timer to navigate to the specific page after the grid loads Qt.callLater(function () { var loader = pageView.itemAtIndex(2); if (loader && loader.item && typeof loader.item.navigateToPage === 'function') @@ -49,11 +43,10 @@ Item { Component.onCompleted: {} - // Timer to force index after model updates settle Timer { id: forceIndexTimer - interval: 100 // Give ListView time to process model change + interval: 100 repeat: false onTriggered: { Logger.info("PageView", "Forcing view to App Grid (Index 2)"); @@ -62,13 +55,11 @@ Item { } } - // Shared model for all grid pages FilteredAppModel { id: sharedAppModel onCountChanged: { if (!pageViewContainer.initialPageSet && count > 0) { - // Force to App Grid (index 2) when data first loads Logger.info("PageView", "Model loaded with " + count + " apps. Scheduling index force."); forceIndexTimer.restart(); pageViewContainer.initialPageSet = true; @@ -84,9 +75,6 @@ Item { } ListView { - // Connections no longer needed as binding handles updates - // Update internal page for app grid (index 2+) - id: pageView property int currentPage: currentIndex - 2 @@ -98,30 +86,24 @@ Item { snapMode: ListView.SnapOneItem highlightRangeMode: ListView.StrictlyEnforceRange interactive: true - pressDelay: 200 // Increased from 100 to give nested scrollables time to claim gestures - // PHYSICS TUNING for smooth, snappy flick (EASIER swiping) + pressDelay: 200 flickDeceleration: 3000 - // Reduced from 5000 for smoother coasting and less aggressive stealing maximumFlickVelocity: 10000 - // Higher velocity = more responsive to lighter flicks flickableDirection: Flickable.HorizontalFlick currentIndex: 2 boundsBehavior: Flickable.StopAtBounds - highlightMoveDuration: 250 // Smooth programmatic transitions + highlightMoveDuration: 250 preferredHighlightBegin: 0 preferredHighlightEnd: width cacheBuffer: width * 3 - // PERFORMANCE OPTIMIZATIONS for touch fluidity pixelAligned: true reuseItems: true - synchronousDrag: false // Async dragging for better performance + synchronousDrag: false model: sharedAppModel.count > 0 ? 2 + pageCount : 4 onCurrentIndexChanged: { - // currentPage is automatically updated via binding on line 37 Logger.debug("PageView", "Page changed to index: " + currentIndex + ", page: " + currentPage); pageViewContainer.hubVisible(currentIndex === 0); pageViewContainer.framesVisible(currentIndex === 1); - // Reset search pull progress when navigating away from app grid pages if (currentIndex < 2) pageViewContainer.searchPullProgress = 0; else @@ -143,8 +125,7 @@ Item { MarathonTaskSwitcher { opacity: 1 - compositor: pageViewContainer.compositor // Pass compositor reference - // Expose search progress from task switcher + compositor: pageViewContainer.compositor onSearchPullProgressChanged: { pageViewContainer.searchPullProgress = searchPullProgress; } @@ -158,9 +139,6 @@ Item { id: appGridComponent MarathonAppGrid { - // No need to propagate internal page changes as PageView handles it - - // Pass shared model appModel: sharedAppModel columns: 4 rows: 4 @@ -188,18 +166,11 @@ Item { return appGridComponent; } - ListView.onReused: { - if (item && typeof item.searchPullProgress !== 'undefined') { - item.searchPullProgress = 0; - item.searchGestureActive = false; - } - } - Binding { target: item property: "pageIndex" value: pageNumber - when: index >= 2 // Only for app grid pages + when: index >= 2 } } } diff --git a/shell/qml/components/MarathonPeek.qml b/shell/qml/components/MarathonPeek.qml index 6714940f7..1b1b473b2 100644 --- a/shell/qml/components/MarathonPeek.qml +++ b/shell/qml/components/MarathonPeek.qml @@ -2,16 +2,11 @@ import MarathonUI.Core import MarathonUI.Theme import QtQuick -// Peek & Flow - THE signature BlackBerry 10 feature -// Swipe from left edge to peek at Hub, continue to open fully Item { - // BackGestureIndicator removed - was visually distracting - // The peek animation itself provides enough visual feedback - id: peekComponent - property real peekProgress: 0 // 0 = closed, 1 = fully open - property real peekThreshold: 0.4 // 40% of screen width triggers full open + property real peekProgress: 0 + property real peekThreshold: 0.4 property bool isPeeking: false property bool isFullyOpen: false property real gestureStartX: 0 @@ -23,7 +18,6 @@ Item { signal fullyOpened signal notificationTapped(var notification) - // Public API for external gesture capture function startPeekGesture(x) { gestureStartX = x; gestureLastX = x; @@ -43,7 +37,6 @@ Item { gestureLastX = gestureStartX + deltaX; gestureLastTime = now; - // Update peek progress (0 to 1) peekProgress = Math.max(0, Math.min(1, deltaX / (peekComponent.width * 0.85))); Logger.info("Peek", "Progress: " + (peekProgress * 100).toFixed(1) + "%, notif visible: " + (peekProgress < 0.35) + ", hub visible: " + (peekProgress >= 0.3)); } @@ -53,7 +46,6 @@ Item { return; isPeeking = false; - // Velocity-based or threshold-based decision var shouldOpen = (gestureVelocity > 300) || (peekProgress > peekThreshold); if (shouldOpen) openPeek(); @@ -62,7 +54,6 @@ Item { Logger.info("Peek", "Gesture ended - " + (shouldOpen ? "opening" : "closing") + " (velocity: " + gestureVelocity.toFixed(0) + "px/s, progress: " + (peekProgress * 100).toFixed(0) + "%)"); } - // Functions function openPeek() { peekProgress = 1; isFullyOpen = true; @@ -81,7 +72,6 @@ Item { anchors.fill: parent clip: true - // Escape key to close Keys.onPressed: event => { if (event.key === Qt.Key_Escape && peekProgress > 0) { closePeek(); @@ -96,7 +86,6 @@ Item { Logger.debug("Peek", "Visibility changed: " + visible); } - // Main content area (dims as peek opens) Rectangle { anchors.fill: parent color: "#000000" @@ -112,7 +101,6 @@ Item { } } - // Hub content (slides in from left) Item { id: hubPanelContainer @@ -130,7 +118,6 @@ Item { Logger.info("Peek", "hubPanelContainer initialized, width: " + width); } - // Notification Preview (0-35% peek) - shows icons vertically stacked Item { id: notificationPreview @@ -210,7 +197,7 @@ Item { anchors.fill: parent onClicked: { - HapticService.light(); + HapticManager.light(); Logger.info("Peek", "Notification tapped: " + model.title); peekComponent.notificationTapped({ "id": model.id, @@ -242,7 +229,6 @@ Item { } } - // Full Hub (30%+ peek) - fades in as preview fades out MarathonHub { id: hubPanel @@ -264,8 +250,6 @@ Item { } } - // Drag-to-close gesture when peek is fully open - // Right-side close area (avoid blocking hub tabs on left) MouseArea { id: closeGestureArea @@ -278,9 +262,9 @@ Item { anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom - width: parent.width * 0.3 // Right 30% of screen + width: parent.width * 0.3 enabled: isFullyOpen - z: 100 // Above hub content + z: 100 onPressed: mouse => { startX = mouse.x; lastX = mouse.x; @@ -291,11 +275,10 @@ Item { onPositionChanged: mouse => { if (!isDragging) { var deltaX = mouse.x - startX; - // Detect left swipe (closing gesture) if (deltaX < -15) { isDragging = true; isPeeking = true; - startX = mouse.x; // Reset for tracking + startX = mouse.x; lastX = mouse.x; lastTime = Date.now(); Logger.info("Peek", "Close drag started"); @@ -308,7 +291,6 @@ Item { lastX = mouse.x; lastTime = now; - // Update progress: deltaX from reset startX var deltaX = mouse.x - startX; var maxDrag = hubPanelContainer.width; peekProgress = Math.max(0, Math.min(1, 1 + (deltaX / maxDrag))); @@ -318,12 +300,10 @@ Item { if (isDragging) { isDragging = false; isPeeking = false; - // Close if dragged left past threshold or velocity is high if (peekProgress < 0.65 || velocity < -500) { Logger.info("Peek", "Closing from drag (progress: " + peekProgress + ", velocity: " + velocity + ")"); closePeek(); } else { - // Snap back to open Logger.info("Peek", "Snapping back open"); peekProgress = 1; } @@ -348,7 +328,6 @@ Item { } } - // Gesture area for peek - ONLY on left edge to not block other interactions! MouseArea { id: gestureArea @@ -360,10 +339,9 @@ Item { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom - width: Constants.spacingSmall // Narrow to not block back button + width: Constants.spacingSmall enabled: !isFullyOpen onPressed: mouse => { - // Always start peek since we're already in left edge area startX = mouse.x; lastX = mouse.x; lastTime = Date.now(); @@ -374,7 +352,6 @@ Item { if (!isPeeking) return; - // Calculate absolute X position (since we're in a 50px wide area) var absoluteX = gestureArea.x + mouse.x; var deltaX = absoluteX - startX; var now = Date.now(); @@ -382,10 +359,8 @@ Item { if (deltaTime > 0) velocity = (absoluteX - lastX) / deltaTime * 1000; - // pixels per second lastX = absoluteX; lastTime = now; - // Update peek progress (0 to 1) based on parent width, not gestureArea width peekProgress = Math.max(0, Math.min(1, deltaX / (peekComponent.width * 0.85))); } onReleased: { @@ -393,14 +368,11 @@ Item { return; isPeeking = false; - // Decision logic: open fully or close if (peekProgress > peekThreshold || velocity > 500) { - // Open fully peekProgress = 1; isFullyOpen = true; fullyOpened(); } else { - // Close closePeek(); } } diff --git a/shell/qml/components/MarathonPinScreen.qml b/shell/qml/components/MarathonPinScreen.qml index 7c14b7648..6b667a5fe 100644 --- a/shell/qml/components/MarathonPinScreen.qml +++ b/shell/qml/components/MarathonPinScreen.qml @@ -4,19 +4,16 @@ import MarathonUI.Theme import QtQuick import QtQuick.Effects -// High-Performance PIN Entry Screen with Frosted Glass Effect Item { - // Small delay for visual feedback - id: pinScreen property string pin: "" property string password: "" property string error: "" property real entryProgress: 0 - property bool passwordMode: false // true = password input, false = PIN pad + property bool passwordMode: false property bool biometricInProgress: false - property bool authenticating: false // Show spinner when validating + property bool authenticating: false signal pinCorrect signal cancelled @@ -31,22 +28,17 @@ Item { } function verifyPin() { - // Check if locked out if (SecurityManagerCpp.isLockedOut) { var secs = SecurityManagerCpp.lockoutSecondsRemaining; error = "Locked for " + secs + "s"; - HapticService.heavy(); + HapticManager.heavy(); return; } - // Show spinner Logger.info("PinScreen", "🔄 Starting authentication, showing spinner"); authenticating = true; - // Authenticate based on current mode if (SecurityManagerCpp.hasQuickPIN && !passwordMode) { - // Quick PIN authentication SecurityManagerCpp.authenticateQuickPIN(pin); } else { - // System password authentication via PAM var inputPassword = passwordMode ? password : pin; SecurityManagerCpp.authenticatePassword(inputPassword); } @@ -56,15 +48,14 @@ Item { if (SecurityManagerCpp.isLockedOut) { var secs = SecurityManagerCpp.lockoutSecondsRemaining; error = "Locked for " + secs + "s"; - HapticService.heavy(); + HapticManager.heavy(); return; } if (password.trim().length === 0) { error = "Password cannot be empty"; - HapticService.light(); + HapticManager.light(); return; } - // Show spinner Logger.info("PinScreen", "🔄 Starting password authentication, showing spinner"); authenticating = true; SecurityManagerCpp.authenticatePassword(password); @@ -74,27 +65,26 @@ Item { if (SecurityManagerCpp.isLockedOut) { var secs = SecurityManagerCpp.lockoutSecondsRemaining; error = "Locked for " + secs + "s"; - HapticService.heavy(); + HapticManager.heavy(); return; } if (!SecurityManagerCpp.fingerprintAvailable) { error = "Fingerprint not enrolled"; - HapticService.light(); + HapticManager.light(); return; } biometricInProgress = true; error = "Place your finger..."; - SecurityManagerCpp.authenticateBiometric(0); // 0 = Fingerprint + SecurityManagerCpp.authenticateBiometric(0); } function switchToPasswordMode() { passwordMode = true; pin = ""; password = ""; - passwordTextInput.text = ""; // Explicitly clear TextInput + passwordTextInput.text = ""; error = ""; Logger.info("PinScreen", "Switched to password mode"); - // Focus the password field Qt.callLater(function () { passwordTextInput.forceActiveFocus(); }); @@ -104,7 +94,7 @@ Item { passwordMode = false; pin = ""; password = ""; - passwordTextInput.text = ""; // Explicitly clear TextInput + passwordTextInput.text = ""; error = ""; Logger.info("PinScreen", "Switched to PIN mode"); } @@ -112,7 +102,7 @@ Item { function reset() { pin = ""; password = ""; - passwordTextInput.text = ""; // Explicitly clear TextInput for security + passwordTextInput.text = ""; error = ""; entryProgress = 0; } @@ -120,20 +110,17 @@ Item { function show() { pin = ""; password = ""; - passwordTextInput.text = ""; // Explicitly clear TextInput for security + passwordTextInput.text = ""; error = ""; entryProgress = 1; - passwordMode = false; // Reset to PIN mode + passwordMode = false; forceActiveFocus(); Logger.info("PinScreen", "📱 PIN screen shown"); } anchors.fill: parent - // NO fade animation - opacity controlled by parent state for instant security - // Pre-render for zero-latency display layer.enabled: true layer.smooth: true - // Update SessionStore to show lock icon in status bar when PIN screen visible onVisibleChanged: { if (visible) { SessionStore.isOnLockScreen = true; @@ -156,30 +143,25 @@ Item { } focus: visible && entryProgress >= 1 - // Security Manager connections Connections { function onAuthenticationSuccess() { Logger.info("PinScreen", " Authentication successful, hiding spinner"); authenticating = false; - HapticService.medium(); - // Clear password field for security + HapticManager.medium(); pin = ""; password = ""; passwordTextInput.text = ""; - // Trigger unlock animation in status bar SessionStore.triggerUnlockAnimation(); - // Delay unlock to allow animation to play (350ms for smooth unlock visual) unlockDelayTimer.start(); } function onAuthenticationFailed(reason) { Logger.warn("PinScreen", " Authentication failed, hiding spinner:", reason); authenticating = false; - HapticService.heavy(); + HapticManager.heavy(); error = reason; errorTimer.start(); biometricInProgress = false; - // Trigger shake animation in status bar SessionStore.triggerShakeAnimation(); } @@ -199,7 +181,6 @@ Item { target: SecurityManagerCpp } - // Wallpaper background (source for blur) Image { id: wallpaperSource @@ -211,7 +192,6 @@ Item { z: 0 } - // Frosted glass overlay covering entire screen Rectangle { id: glassRect @@ -220,7 +200,6 @@ Item { opacity: 0.95 z: 1 - // Capture wallpaper for blurring ShaderEffectSource { id: wallpaperCapture @@ -230,7 +209,6 @@ Item { visible: false } - // Apply blur effect (Qt6 MultiEffect) MultiEffect { anchors.fill: parent source: wallpaperCapture @@ -243,7 +221,6 @@ Item { } } - // Solid background overlay for better contrast Rectangle { anchors.fill: parent color: MColors.overlay @@ -253,24 +230,20 @@ Item { Column { anchors.centerIn: parent anchors.verticalCenterOffset: Math.round(-20 * Constants.scaleFactor) - spacing: Math.round(24 * Constants.scaleFactor) // Reduced from 40 - z: 100 // PIN UI on top of blur - // GPU layer for column content + spacing: Math.round(24 * Constants.scaleFactor) + z: 100 layer.enabled: true layer.smooth: true - // Header Column { - // Lock icon removed - now shown in status bar for consistency - anchors.horizontalCenter: parent.horizontalCenter - spacing: Math.round(16 * Constants.scaleFactor) // Reduced from 24 + spacing: Math.round(16 * Constants.scaleFactor) Text { anchors.horizontalCenter: parent.horizontalCenter text: passwordMode ? "Enter Password" : "Enter PIN" color: MColors.text - font.pixelSize: Math.round(24 * Constants.scaleFactor) // Reduced from 28 + font.pixelSize: Math.round(24 * Constants.scaleFactor) font.weight: Font.Medium renderType: Text.NativeRendering @@ -299,12 +272,10 @@ Item { } } - // PIN dots with loading spinner Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Math.round(16 * Constants.scaleFactor) - // PIN dots Row { id: pinCircles @@ -321,10 +292,8 @@ Item { border.width: 2 border.color: index < pin.length ? MColors.accentBright : MColors.borderSubtle antialiasing: true - // Quick scale pulse scale: (index === pin.length - 1 && pin.length > 0) ? 1.3 : 1 - // Simple, fast animations Behavior on color { ColorAnimation { duration: 100 @@ -347,7 +316,6 @@ Item { } } - // Authentication spinner - aligned with PIN circles center MActivityIndicator { anchors.verticalCenter: pinCircles.verticalCenter size: Math.round(32 * Constants.scaleFactor) @@ -364,7 +332,6 @@ Item { } } - // Error message Rectangle { anchors.horizontalCenter: parent.horizontalCenter width: Math.round(300 * Constants.scaleFactor) @@ -389,7 +356,6 @@ Item { } } - // Simple shake SequentialAnimation on x { running: error !== "" @@ -420,18 +386,16 @@ Item { } } - // Number pad - larger, cleaner, faster (only visible in PIN mode) Column { anchors.horizontalCenter: parent.horizontalCenter - spacing: Math.round(12 * Constants.scaleFactor) // Reduced from 16 + spacing: Math.round(12 * Constants.scaleFactor) visible: !passwordMode Grid { anchors.horizontalCenter: parent.horizontalCenter columns: 3 columnSpacing: Math.round(16 * Constants.scaleFactor) - rowSpacing: Math.round(12 * Constants.scaleFactor) // Reduced from 16 - // GPU layer for grid + rowSpacing: Math.round(12 * Constants.scaleFactor) layer.enabled: true layer.smooth: true @@ -441,18 +405,18 @@ Item { delegate: Item { property string digit: modelData - width: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) // Reduced from 80 - height: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) // Reduced from 80 + width: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) + height: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) MCircularIconButton { anchors.centerIn: parent text: digit - iconSize: Math.round(28 * Constants.scaleFactor) // Reduced from 32 - buttonSize: Math.round(70 * Constants.scaleFactor) // Reduced from 80 + iconSize: Math.round(28 * Constants.scaleFactor) + buttonSize: Math.round(70 * Constants.scaleFactor) variant: "secondary" textColor: MColors.textPrimary onClicked: { - HapticService.light(); + HapticManager.light(); handleInput(parent.digit); } } @@ -460,12 +424,10 @@ Item { } } - // Bottom row: keyboard button, 0, and backspace Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Math.round(16 * Constants.scaleFactor) - // Keyboard button (switch to password mode) Item { width: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) height: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) @@ -478,13 +440,12 @@ Item { variant: "secondary" visible: !passwordMode onClicked: { - HapticService.light(); + HapticManager.light(); switchToPasswordMode(); } } } - // Zero button Item { width: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) height: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) @@ -497,13 +458,12 @@ Item { variant: "secondary" textColor: MColors.textPrimary onClicked: { - HapticService.light(); + HapticManager.light(); handleInput("0"); } } } - // Backspace button Item { width: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) height: Math.round(70 * Constants.scaleFactor) + Math.round(12 * Constants.scaleFactor) @@ -516,7 +476,7 @@ Item { variant: "secondary" iconColor: MColors.textSecondary onClicked: { - HapticService.light(); + HapticManager.light(); pin = ""; error = ""; } @@ -525,14 +485,12 @@ Item { } } - // Password input mode (alternative to PIN pad) Column { anchors.horizontalCenter: parent.horizontalCenter spacing: Math.round(24 * Constants.scaleFactor) visible: passwordMode width: Math.round(320 * Constants.scaleFactor) - // Password field with spinner overlay (no layout shift) Item { anchors.horizontalCenter: parent.horizontalCenter width: Math.round(280 * Constants.scaleFactor) @@ -569,7 +527,7 @@ Item { anchors.fill: parent anchors.leftMargin: Math.round(16 * Constants.scaleFactor) - anchors.rightMargin: Math.round(48 * Constants.scaleFactor) // Extra space for spinner + anchors.rightMargin: Math.round(48 * Constants.scaleFactor) verticalAlignment: TextInput.AlignVCenter color: MColors.textPrimary selectedTextColor: MColors.textOnAccent @@ -592,7 +550,6 @@ Item { } } - // Authentication spinner - overlays on right side of password field MActivityIndicator { anchors.right: parent.right anchors.rightMargin: Math.round(12 * Constants.scaleFactor) @@ -611,13 +568,11 @@ Item { } } - // Action buttons - horizontal layout Row { anchors.horizontalCenter: parent.horizontalCenter width: parent.width spacing: Math.round(12 * Constants.scaleFactor) - // Secondary button - Use PIN (left) MButton { width: (parent.width - parent.spacing) / 2 text: "Use PIN" @@ -625,7 +580,6 @@ Item { onClicked: switchToPINMode() } - // Primary button - Unlock (right) MButton { width: (parent.width - parent.spacing) / 2 text: "Unlock" @@ -636,7 +590,6 @@ Item { } } - // Fingerprint button (centered, below PIN pad) MCircularIconButton { anchors.horizontalCenter: parent.horizontalCenter iconName: "fingerprint" @@ -646,12 +599,11 @@ Item { visible: SecurityManagerCpp.fingerprintAvailable && !biometricInProgress && !passwordMode enabled: !SecurityManagerCpp.isLockedOut onClicked: { - HapticService.light(); + HapticManager.light(); startBiometric(); } } - // Cancel button Text { anchors.horizontalCenter: parent.horizontalCenter text: "Cancel" @@ -667,7 +619,7 @@ Item { anchors.fill: parent anchors.margins: Math.round(-16 * Constants.scaleFactor) onClicked: { - HapticService.light(); + HapticManager.light(); cancelled(); } } @@ -700,9 +652,9 @@ Item { Timer { id: unlockDelayTimer - interval: 350 // Match unlock animation duration + interval: 350 onTriggered: { - pinCorrect(); // Now emit the unlock signal + pinCorrect(); } } } diff --git a/shell/qml/components/MarathonQuickSettings.qml b/shell/qml/components/MarathonQuickSettings.qml index 8593789ea..69670812b 100644 --- a/shell/qml/components/MarathonQuickSettings.qml +++ b/shell/qml/components/MarathonQuickSettings.qml @@ -6,33 +6,21 @@ import QtQuick import QtQuick.Controls Rectangle { - // ... - // Note: "monitor" is info-only, no deep link - id: quickSettings - // RESPONSIVE GRID CALCULATIONS (like CSS Grid) - // 2 cols: < 800px (phones including 720x720) - // 3 cols: 800-1200px (tablets) - // 4 cols: > 1200px (large tablets, desktop) readonly property int gridColumns: Constants.screenWidth < 800 ? 2 : (Constants.screenWidth < 1200 ? 3 : 4) readonly property real tileHeight: Constants.hubHeaderHeight - // Calculate how many rows fit: subtract date (50) + sliders (160) + spacing readonly property real reservedHeight: 50 + 160 + Constants.spacingMedium * 4 + Constants.spacingLarge * 2 + 80 readonly property real availableGridHeight: Math.max(tileHeight * 3, height - reservedHeight) readonly property int maxGridRows: Math.max(3, Math.min(5, Math.floor(availableGridHeight / (tileHeight + Constants.spacingSmall)))) readonly property int tilesPerPage: gridColumns * maxGridRows readonly property real calculatedGridHeight: (tileHeight * maxGridRows) + (Constants.spacingSmall * (maxGridRows - 1)) - // Reactive properties for tile updates property string networkSubtitle: SystemStatusStore.ethernetConnected ? ((typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) ? (NetworkManagerCpp.ethernetConnectionName || "Wired") : "Wired") : (SystemStatusStore.wifiNetwork || "Not connected") - property string networkIcon: SystemStatusStore.ethernetConnected ? "plug-zap" : "wifi" // Using "plug-zap" for ethernet (Lucide icon) + property string networkIcon: SystemStatusStore.ethernetConnected ? "plug-zap" : "wifi" property string networkLabel: SystemStatusStore.ethernetConnected ? "Ethernet" : "Wi-Fi" property string cellularSubtitle: (typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp ? ModemManagerCpp.operatorName : "") || "No service" property string batterySubtitle: "Battery " + SystemStatusStore.batteryLevel + "%" - // Force model updates when key properties change property int updateTrigger: 0 - // ALL TILES MODEL (accessible from anywhere in quickSettings) - // This is the master list of all possible tiles property var allTiles: [ { "id": "settings", @@ -167,19 +155,14 @@ Rectangle { "trigger": updateTrigger } ] - // FILTERED TILES based on user preferences from SettingsManager - // Force recomputation when any tile state changes property var visibleTiles: { - updateTrigger; // Force dependency on updateTrigger for reactivity + updateTrigger; var enabled = SettingsManagerCpp.enabledQuickSettingsTiles; var order = SettingsManagerCpp.quickSettingsTileOrder; var result = []; - // Build tiles in custom order for (var i = 0; i < order.length; i++) { var tileId = order[i]; - // Only include if enabled if (enabled.indexOf(tileId) !== -1) { - // Find tile in allTiles var tile = null; for (var j = 0; j < allTiles.length; j++) { if (allTiles[j].id === tileId) { @@ -187,13 +170,10 @@ Rectangle { break; } } - // Add all enabled tiles, regardless of availability - // Tiles will show as disabled/grayed when not available if (tile) result.push(tile); } } - // Add any new tiles not in order list (for backwards compat) for (var k = 0; k < allTiles.length; k++) { if (order.indexOf(allTiles[k].id) === -1 && enabled.indexOf(allTiles[k].id) !== -1) result.push(allTiles[k]); @@ -204,7 +184,6 @@ Rectangle { signal closed signal launchApp(var app) - // Handle toggle tap function handleToggleTap(toggleId) { Logger.info("QuickSettings", "Toggle tapped: " + toggleId); if (toggleId === "wifi") { @@ -273,10 +252,8 @@ Rectangle { Logger.info("QuickSettings", "Device monitor - info only, no action"); } - // Handle long press (deep link to settings) function handleLongPress(toggleId) { Logger.info("QuickSettings", "Toggle long-pressed: " + toggleId); - // Ignore long press for settings, lock, power, and monitor (info-only/action tiles) if (toggleId === "settings" || toggleId === "lock" || toggleId === "power" || toggleId === "monitor") return; @@ -304,7 +281,6 @@ Rectangle { opacity: 0.98 Component.onCompleted: { Logger.info("QuickSettings", "Grid layout: " + gridColumns + " cols × " + maxGridRows + " rows (screen: " + Constants.screenWidth + "px)"); - // Don't log raw enabledQuickSettingsTiles length; it can contain legacy/unknown IDs. var enabled = SettingsManagerCpp.enabledQuickSettingsTiles || []; var knownCount = 0; var unknownCount = 0; @@ -386,7 +362,6 @@ Rectangle { target: SettingsManagerCpp } - // Center container for responsive layout Item { anchors.fill: parent @@ -394,7 +369,6 @@ Rectangle { id: contentContainer anchors.centerIn: parent - // Use full width on mobile (<= 1080px), max 800px on tablets/desktop width: Constants.screenWidth <= 1080 ? parent.width : Math.min(parent.width, 800) height: parent.height @@ -423,7 +397,6 @@ Rectangle { anchors.left: parent.left } - // Paginated Quick Settings Toggles Column { width: parent.width spacing: MSpacing.md @@ -436,7 +409,6 @@ Rectangle { clip: true interactive: count > 1 - // Dynamically create pages based on tilesPerPage Repeater { model: Math.ceil(visibleTiles.length / tilesPerPage) @@ -476,14 +448,12 @@ Rectangle { } } - // Media Playback Manager MediaPlaybackManager { id: mediaPlayer width: parent.width } - // Brightness Slider Column { width: parent.width spacing: MSpacing.sm @@ -500,7 +470,6 @@ Rectangle { width: parent.width from: 0 to: 100 - // Don't bind value - causes double-click issue Component.onCompleted: value = SystemControlStore.brightness onMoved: { brightnessDebounce.restart(); @@ -510,7 +479,6 @@ Rectangle { SystemControlStore.setBrightness(brightnessSlider.value); } - // Debounce timer to prevent UI freezing during drag Timer { id: brightnessDebounce @@ -518,7 +486,6 @@ Rectangle { onTriggered: SystemControlStore.setBrightness(brightnessSlider.value) } - // Update from external changes Connections { function onBrightnessChanged() { if (!brightnessSlider.pressed) @@ -530,7 +497,6 @@ Rectangle { } } - // Volume Slider Column { width: parent.width spacing: MSpacing.sm @@ -547,7 +513,6 @@ Rectangle { width: parent.width from: 0 to: 100 - // Don't bind value - causes double-click issue Component.onCompleted: value = SystemControlStore.volume onMoved: { volumeDebounce.restart(); @@ -557,7 +522,6 @@ Rectangle { SystemControlStore.setVolume(volumeSlider.value); } - // Debounce timer to prevent UI freezing during drag Timer { id: volumeDebounce @@ -565,7 +529,6 @@ Rectangle { onTriggered: SystemControlStore.setVolume(volumeSlider.value) } - // Update from external changes Connections { function onVolumeChanged() { if (!volumeSlider.pressed) diff --git a/shell/qml/components/MarathonSearch.qml b/shell/qml/components/MarathonSearch.qml index e17b4df67..326e6880e 100644 --- a/shell/qml/components/MarathonSearch.qml +++ b/shell/qml/components/MarathonSearch.qml @@ -6,7 +6,7 @@ Item { id: searchOverlay property bool active: false - property real pullProgress: 0 // 0.0 to 1.0, for pull-to-reveal animation + property real pullProgress: 0 property string searchQuery: "" property var searchResults: [] @@ -41,24 +41,19 @@ Item { } function selectResult(result) { - // Prevent double execution if (searchOverlay.opacity < 1 || !active) return; Logger.info("Search", "Result selected: " + result.title + " (type: " + result.type + ")"); UnifiedSearchService.addToRecentSearches(searchQuery); - // Close first to prevent double-tap through close(); - // Execute after a brief delay to ensure search is closed Qt.callLater(function () { - // Emit signal to parent (MarathonShell) to handle launch resultSelected(result); }); } visible: opacity > 0.01 - enabled: opacity > 0.01 // Block interactions whenever visible - // Opacity: active = full opacity, OR follows pullProgress during gesture + enabled: opacity > 0.01 opacity: active ? 1 : Math.max(0, pullProgress) onActiveChanged: { if (active) { @@ -74,14 +69,11 @@ Item { } } - // Full-screen mouse blocker when visible at any opacity - // Excludes nav bar area to allow swipe-up-to-close gesture MouseArea { anchors.fill: parent anchors.bottomMargin: Constants.bottomBarHeight enabled: searchOverlay.opacity > 0.01 && !searchOverlay.active onClicked: { - // Clicking on overlay when partially visible dismisses it searchOverlay.pullProgress = 0; } } @@ -343,7 +335,6 @@ Item { } } - // Smooth fade-out when search closes or progress resets Behavior on opacity { enabled: !active diff --git a/shell/qml/components/MarathonStatusBar.qml b/shell/qml/components/MarathonStatusBar.qml index bc7a53ea3..8feb74f09 100644 --- a/shell/qml/components/MarathonStatusBar.qml +++ b/shell/qml/components/MarathonStatusBar.qml @@ -49,11 +49,9 @@ Item { } } - // Center content: Clock OR Lock icon (animated transition) Item { id: centerContent - // Dynamic position based on setting property string position: (typeof SettingsManagerCpp !== 'undefined' && SettingsManagerCpp.statusBarClockPosition) ? SettingsManagerCpp.statusBarClockPosition : "center" anchors.verticalCenter: parent.verticalCenter @@ -72,8 +70,7 @@ Item { } PropertyChanges { - target: centerContent - anchors.leftMargin: leftIconGroup.x + leftIconGroup.width + Constants.spacingLarge + centerContent.anchors.leftMargin: leftIconGroup.x + leftIconGroup.width + Constants.spacingLarge } }, State { @@ -99,13 +96,11 @@ Item { } PropertyChanges { - target: centerContent - anchors.rightMargin: rightIconGroup.width + rightIconGroup.anchors.rightMargin + Constants.spacingLarge + centerContent.anchors.rightMargin: rightIconGroup.width + rightIconGroup.anchors.rightMargin + Constants.spacingLarge } } ] - // Clock text (shown when NOT on lock screen) Text { id: clockText @@ -125,28 +120,23 @@ Item { } } - // Lock icon (shown when on lock screen, animates on lock state changes) Icon { id: lockIcon - // Property to control whether name behavior is enabled property bool enableNameBehavior: true anchors.centerIn: parent visible: opacity > 0.01 opacity: SessionStore.isOnLockScreen ? 1 : 0 - // Removed excessive opacity logging - fires constantly during animations name: SessionStore.isLocked ? "lock" : "lock-keyhole-open" - size: Constants.iconSizeSmall // Match other status bar icons + size: Constants.iconSizeSmall color: MColors.text - // Debug logging Component.onCompleted: { console.log("[StatusBar] Lock icon initialized - isLocked:", SessionStore.isLocked, "name:", name); } onNameChanged: { console.log("[StatusBar] Lock icon name changed to:", name, "(isLocked:", SessionStore.isLocked, ")"); } - // Animated lock state transitions (300ms, bouncy feel) scale: SessionStore.isAnimatingLock ? 0.8 : 1 rotation: { if (SessionStore.lockTransition === "locking") @@ -157,11 +147,9 @@ Item { return 0; } - // GPU acceleration for smooth 60fps animations layer.enabled: true layer.smooth: true - // Shake animation for invalid PIN SequentialAnimation { id: shakeAnimation @@ -222,16 +210,9 @@ Item { } } - // Unlock animation for valid PIN SequentialAnimation { - // Unlocking is session logic and should be triggered by the auth flow (PinScreen), - // not by a UI animation. This also avoids duplicate unlocks if multiple status bars - // exist in different shell states. - // Return to normal scale; the icon name will update via SessionStore.isLocked changes. - id: unlockAnimation - // Disable automatic name behavior during custom animation PropertyAction { target: lockIcon property: "enableNameBehavior" @@ -241,7 +222,6 @@ Item { ScriptAction { script: console.log("[StatusBar] Unlock animation started!") } - // Pulse scale up NumberAnimation { target: lockIcon @@ -250,7 +230,6 @@ Item { duration: 200 easing.type: Easing.OutCubic } - // IMPORTANT: Do not call SessionStore.unlock() from the status bar. NumberAnimation { target: lockIcon @@ -259,7 +238,6 @@ Item { duration: 150 easing.type: Easing.OutBack } - // Re-enable automatic name behavior PropertyAction { target: lockIcon @@ -272,7 +250,6 @@ Item { } } - // Connect to SessionStore signals Connections { function onTriggerShakeAnimation() { console.log("[StatusBar] Received triggerShakeAnimation signal"); @@ -308,7 +285,6 @@ Item { } } - // Smooth morph animation when icon name changes Behavior on name { enabled: lockIcon.enableNameBehavior @@ -360,7 +336,7 @@ Item { color: MColors.text size: Constants.iconSizeSmall anchors.verticalCenter: parent.verticalCenter - visible: StatusBarIconService.shouldShowDND(SystemStatusStore.isDndMode) + visible: StatusBarIconService.shouldShowDnd(SystemStatusStore.isDndMode) opacity: 0.9 } @@ -373,7 +349,6 @@ Item { visible: (typeof BluetoothManagerCpp !== "undefined" && BluetoothManagerCpp ? BluetoothManagerCpp.available : false) && StatusBarIconService.shouldShowBluetooth(SystemStatusStore.isBluetoothOn) } - // Cellular - always show, signal-off (crossed antenna) when unavailable Icon { name: (typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp.modemAvailable) ? StatusBarIconService.getSignalIcon(SystemStatusStore.cellularStrength) : "smartphone" color: MColors.text @@ -382,9 +357,8 @@ Item { opacity: (typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp.modemAvailable) ? StatusBarIconService.getSignalOpacity(SystemStatusStore.cellularStrength) : 0.3 } - // Ethernet - only show when connected Icon { - name: "cable" // Using cable icon instead of plug-zap to avoid confusion with power + name: "cable" color: MColors.text size: Constants.iconSizeSmall anchors.verticalCenter: parent.verticalCenter @@ -392,7 +366,6 @@ Item { opacity: 1 } - // WiFi - always show, wifi-off when unavailable Icon { name: (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.wifiAvailable : false) ? StatusBarIconService.getWifiIcon(SystemStatusStore.isWifiOn, SystemStatusStore.wifiStrength, typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.wifiConnected : false) : "wifi-off" color: MColors.text diff --git a/shell/qml/components/MarathonTaskSwitcher.qml b/shell/qml/components/MarathonTaskSwitcher.qml index a42bb077e..ecce2ec59 100644 --- a/shell/qml/components/MarathonTaskSwitcher.qml +++ b/shell/qml/components/MarathonTaskSwitcher.qml @@ -3,16 +3,11 @@ import MarathonUI.Theme import QtQuick Item { - // No component definitions needed - we'll reference live app instances from AppLifecycleManager - id: taskSwitcher - // Expose HAVE_WAYLAND from C++ context readonly property bool haveWayland: typeof HAVE_WAYLAND !== 'undefined' ? HAVE_WAYLAND : false - // Track pull-down progress for inline animation property real searchPullProgress: 0 property bool searchGestureActive: false - // Compositor reference for closing native apps property var compositor: null signal closed @@ -21,7 +16,6 @@ Item { scale: visible ? 1 : 0.95 - // Gesture area for pull-down to search (only when empty) MouseArea { property real startX: 0 property real startY: 0 @@ -46,36 +40,30 @@ Item { if (pressed && !isDragging && !isVertical) { var deltaX = Math.abs(mouse.x - startX); var deltaY = mouse.y - startY; - // Decide gesture direction after 10px threshold if (deltaX > 10 || Math.abs(deltaY) > 10) { - // STRICT: Vertical must be at least 3x more than horizontal (max ~18° angle) if (Math.abs(deltaY) > deltaX * 3 && deltaY > 0) { isVertical = true; isDragging = true; taskSwitcher.searchGestureActive = true; Logger.info("TaskSwitcher", "Pull-down gesture started"); } else { - // Too diagonal or wrong direction - reject gesture isVertical = false; isDragging = false; return; } } } - // Update progress in real-time during gesture if (isDragging && pressed) { currentY = mouse.y; var deltaY = currentY - startY; - // Update pull progress for inline animation taskSwitcher.searchPullProgress = Math.min(1, deltaY / pullThreshold); } } onReleased: function (mouse) { if (isDragging && isVertical) { var deltaY = currentY - startY; - var deltaTime = Date.now() - startY; // Rough approximation + var deltaTime = Date.now() - startY; var velocity = deltaY / (deltaTime || 1); - // If pulled down more than threshold OR fast velocity if (taskSwitcher.searchPullProgress > commitThreshold || velocity > 0.25) { Logger.info("TaskSwitcher", "Pull down threshold met - opening search (" + deltaY + "px)"); UIStore.openSearch(); @@ -93,7 +81,6 @@ Item { } } - // Empty state - show time and date like lock screen Column { anchors.centerIn: parent anchors.verticalCenterOffset: -80 - Constants.navBarHeight @@ -108,7 +95,6 @@ Item { font.weight: Font.Thin anchors.horizontalCenter: parent.horizontalCenter - // Drop shadow using multiple text layers Text { text: parent.text color: "#80000000" @@ -129,7 +115,6 @@ Item { anchors.horizontalCenter: parent.horizontalCenter opacity: 0.9 - // Drop shadow using multiple text layers Text { text: parent.text color: "#80000000" @@ -144,14 +129,12 @@ Item { } } - // Background click to close (but don't steal events from cards) MouseArea { anchors.fill: parent enabled: TaskModel.taskCount > 0 - propagateComposedEvents: true // Let card MouseAreas handle their events - z: -1 // Behind the GridView + propagateComposedEvents: true + z: -1 onClicked: mouse => { - // Only close if clicking empty space (not on a card) mouse.accepted = false; closed(); } @@ -166,30 +149,15 @@ Item { } GridView { - // Flick up = previous page - // Flick down = next page - // Slow movement = snap to nearest - // Remove custom snap animation logic - /* - // Velocity-aware snap to page helper function - function snapToPage(velocity) { - // ... removed ... - } - */ - id: taskGrid - // Allow horizontal gestures to pass through to parent PageView - // This is critical for page switching when task switcher is full property bool allowHorizontalPassthrough: true - // Use SnapOneRow for page-by-page scrolling (one "page" = 2 rows = 4 cards) snapMode: GridView.SnapOneRow flickDeceleration: 1500 maximumFlickVelocity: 3000 anchors.fill: parent anchors.margins: 16 - // Right margin for the vertical page indicator anchors.rightMargin: TaskModel.taskCount > 4 ? 24 : 16 anchors.bottomMargin: Constants.bottomBarHeight + 16 cellWidth: width / 2 @@ -199,16 +167,13 @@ Item { console.log("[TaskSwitcher] GridView created, model count:", count); Logger.info("TaskSwitcher", "GridView created with " + count + " tasks, interactive: " + interactive); } - // Only allow vertical scrolling flickableDirection: Flickable.VerticalFlick - pressDelay: 0 // Claim gestures immediately (before PageView's pressDelay: 200) + pressDelay: 0 interactive: TaskModel.taskCount > 4 - // Boundaries boundsBehavior: Flickable.StopAtBounds model: TaskModel cacheBuffer: Math.max(0, height * 2) reuseItems: true - // DEBUG: Log gesture states onDraggingChanged: Logger.info("TaskSwitcher", "GridView dragging: " + dragging) onFlickingChanged: Logger.info("TaskSwitcher", "GridView flicking: " + flicking) onMovingChanged: Logger.info("TaskSwitcher", "GridView moving: " + moving + ", contentY: " + contentY) @@ -217,7 +182,6 @@ Item { delegate: TaskCard { width: GridView.view.cellWidth height: GridView.view.cellHeight - // Context properties (not from model, need explicit binding) haveWayland: taskSwitcher.haveWayland compositor: taskSwitcher.compositor taskSwitcherVisible: taskSwitcher.visible @@ -227,7 +191,6 @@ Item { } } - // Vertical page indicator (shown when more than 4 apps) TaskPageIndicator { taskCount: TaskModel.taskCount gridContentY: taskGrid.contentY diff --git a/shell/qml/components/MediaPlaybackManager.qml b/shell/qml/components/MediaPlaybackManager.qml index 5bb7da191..4ca5d79e5 100644 --- a/shell/qml/components/MediaPlaybackManager.qml +++ b/shell/qml/components/MediaPlaybackManager.qml @@ -2,22 +2,17 @@ import MarathonUI.Core import MarathonUI.Theme import QtQuick -// Media Playback Manager -// Shows currently playing media with playback controls -// ALWAYS VISIBLE - shows "No media" state when nothing playing Rectangle { id: mediaManager - // MPRIS2 Integration - Real media player control readonly property bool hasMedia: MPRIS2Controller ? MPRIS2Controller.hasActivePlayer : false readonly property bool isPlaying: MPRIS2Controller ? MPRIS2Controller.isPlaying : false readonly property string trackTitle: MPRIS2Controller && MPRIS2Controller.hasActivePlayer ? (MPRIS2Controller.trackTitle || "Unknown Track") : "No media playing" readonly property string artist: MPRIS2Controller ? MPRIS2Controller.trackArtist : "" readonly property string albumArt: MPRIS2Controller ? MPRIS2Controller.albumArtUrl : "" - readonly property real progress: MPRIS2Controller ? (MPRIS2Controller.position / 1e+06) : 0 // Convert microseconds to seconds - readonly property real duration: MPRIS2Controller ? (MPRIS2Controller.trackLength / 1e+06) : 0 // Convert microseconds to seconds + readonly property real progress: MPRIS2Controller ? (MPRIS2Controller.position / 1e+06) : 0 + readonly property real duration: MPRIS2Controller ? (MPRIS2Controller.trackLength / 1e+06) : 0 - // Format time helper function formatTime(seconds) { var mins = Math.floor(seconds / 60); var secs = Math.floor(seconds % 60); @@ -26,10 +21,10 @@ Rectangle { width: parent.width height: contentColumn.implicitHeight + Constants.spacingMedium + Constants.spacingLarge - visible: true // Always visible + visible: true radius: Constants.borderRadiusSmall border.width: Constants.borderWidthThin - border.color: Qt.rgba(0, 191 / 255, 165 / 255, 0.3) // MColors.marathonTealBorder approx + border.color: Qt.rgba(0, 191 / 255, 165 / 255, 0.3) Component.onCompleted: { if (MPRIS2Controller) { Logger.info("MediaPlaybackManager", "✓ Initialized with MPRIS2 integration"); @@ -39,24 +34,16 @@ Rectangle { } } - // Tap to launch app MouseArea { - // Z-index 0 is default, children (Column) are on top by default in QML order? - // No, children declared later are on top. Column is declared AFTER this if I put it here. - // Wait, I am inserting this BEFORE Column (line 33). - // So this MouseArea is BEHIND the Column. - // Buttons in Column will capture clicks first. - anchors.fill: parent onClicked: { if (mediaManager.hasMedia && MPRIS2Controller) { var appId = MPRIS2Controller.desktopEntry; if (appId && appId !== "") { - HapticService.light(); + HapticManager.light(); Logger.info("MediaPlayback", "Launching app: " + appId); AppLaunchService.launchApp(appId); } else { - // DesktopEntry is optional in MPRIS; don't spam warnings if missing. Logger.debug("MediaPlayback", "No desktop entry found for player"); } } @@ -72,23 +59,19 @@ Rectangle { anchors.margins: Constants.spacingMedium spacing: Constants.spacingSmall - // Title and artist Row { width: parent.width spacing: Constants.spacingMedium - // Album art thumbnail or music icon Rectangle { width: Constants.touchTargetSmall height: Constants.touchTargetSmall radius: Constants.borderRadiusSmall color: mediaManager.albumArt !== "" ? "transparent" : MColors.elevated visible: mediaManager.hasMedia - clip: true // CRITICAL: Clip child Image to rounded corners + clip: true Image { - // Note: Image doesn't have radius property - parent Rectangle clips it - anchors.fill: parent source: mediaManager.albumArt fillMode: Image.PreserveAspectCrop @@ -130,22 +113,17 @@ Rectangle { } } - // Playback controls Row { readonly property real buttonWidth: Constants.touchTargetMinimum anchors.horizontalCenter: parent.horizontalCenter - // height: Constants.touchTargetMinimum // REMOVED: Let Row expand to fit buttons (including glow) visible: mediaManager.hasMedia spacing: Constants.spacingSmall - // Previous button MCircularIconButton { buttonSize: Constants.touchTargetMinimum - // Smart Skip: Use rotate-ccw (10s back) for long tracks, otherwise skip-back iconName: (MPRIS2Controller && MPRIS2Controller.canSeek && MPRIS2Controller.trackLength > 20 * 60 * 1e+06) ? "rotate-ccw" : "skip-back" variant: "secondary" - // Smart Skip: Enable if we can go previous OR if it's a long track we can seek in enabled: mediaManager.hasMedia && MPRIS2Controller && (MPRIS2Controller.canGoPrevious || (MPRIS2Controller.canSeek && MPRIS2Controller.trackLength > 20 * 60 * 1e+06)) onClicked: { if (MPRIS2Controller) { @@ -155,13 +133,10 @@ Rectangle { } } - // Play/Pause button MCircularIconButton { buttonSize: Constants.touchTargetMinimum iconName: mediaManager.isPlaying ? "pause" : "play" variant: "primary" - // Some players incorrectly report CanPlay/CanPause; PlayPause is still valid. - // Keep the control usable whenever we have an active player. enabled: mediaManager.hasMedia && MPRIS2Controller onClicked: { if (MPRIS2Controller) { @@ -171,13 +146,10 @@ Rectangle { } } - // Next button MCircularIconButton { buttonSize: Constants.touchTargetMinimum - // Smart Skip: Use rotate-cw (30s forward) for long tracks, otherwise skip-forward iconName: (MPRIS2Controller && MPRIS2Controller.canSeek && MPRIS2Controller.trackLength > 20 * 60 * 1e+06) ? "rotate-cw" : "skip-forward" variant: "secondary" - // Smart Skip: Enable if we can go next OR if it's a long track we can seek in enabled: mediaManager.hasMedia && MPRIS2Controller && (MPRIS2Controller.canGoNext || (MPRIS2Controller.canSeek && MPRIS2Controller.trackLength > 20 * 60 * 1e+06)) onClicked: { if (MPRIS2Controller) { @@ -189,14 +161,12 @@ Rectangle { } } - // Dark teal gradient background gradient: Gradient { GradientStop { position: 0 color: Qt.rgba(0, 191 / 255, 165 / 255, 0.15) } - // MColors.marathonTealGlowTop approx GradientStop { position: 1 color: Qt.rgba(0, 0, 0, 0.2) diff --git a/shell/qml/components/NotificationToast.qml b/shell/qml/components/NotificationToast.qml index 3e53eaf3c..e509a3604 100644 --- a/shell/qml/components/NotificationToast.qml +++ b/shell/qml/components/NotificationToast.qml @@ -51,7 +51,6 @@ Item { elevation: 0 radius: 0 visible: false - // Custom bottom shadow only layer.enabled: true Item { @@ -109,7 +108,6 @@ Item { } } - // Inline reply field (for messaging notifications) Row { width: parent.width - MSpacing.xs * 2 height: 48 @@ -177,19 +175,15 @@ Item { } } onClicked: { - // Check if this is a messaging notification that supports inline reply var supportsInlineReply = currentToast && (currentToast.appId === "messages" || currentToast.appId === "org.telegram.desktop" || currentToast.appId === "signal-desktop" || (currentToast.category && currentToast.category.includes("message"))); if (supportsInlineReply && !showInlineReply) { - // Show inline reply field Logger.info("NotificationToast", "Showing inline reply for: " + currentToast.id); showInlineReply = true; autoHideTimer.stop(); - // Focus the reply field after a brief delay Qt.callLater(function () { replyField.forceActiveFocus(); }); } else if (!showInlineReply) { - // Navigate to app Logger.info("NotificationToast", "Toast tapped: " + currentToast.id); NotificationService.clickNotification(currentToast.id); NotificationModel.markAsRead(currentToast.id); diff --git a/shell/qml/components/PermissionDialog.qml b/shell/qml/components/PermissionDialog.qml index 33c5f2b9c..e8bd4c737 100644 --- a/shell/qml/components/PermissionDialog.qml +++ b/shell/qml/components/PermissionDialog.qml @@ -6,13 +6,12 @@ import QtQuick.Controls Item { id: permissionDialog - property string appId: PermissionManager.currentAppId - property string permission: PermissionManager.currentPermission + property string appId: PermissionManager && PermissionManager.currentAppId ? PermissionManager.currentAppId : "" + property string permission: PermissionManager && PermissionManager.currentPermission ? PermissionManager.currentPermission : "" property string appName: getAppName(appId) - property string permissionDesc: PermissionManager.getPermissionDescription(permission) + property string permissionDesc: permission ? PermissionManager.getPermissionDescription(permission) : "" function getAppName(id) { - // Try to get app name from registry if (!id) return "Unknown App"; @@ -37,14 +36,11 @@ Item { return icons[perm] || "help"; } - // CRITICAL: Must be parented to shell root overlay to appear above apps - parent: Overlay.overlay anchors.fill: parent z: 10000 visible: opacity > 0 opacity: PermissionManager.promptActive ? 1 : 0 - // Backdrop (tap outside to deny once) Rectangle { anchors.fill: parent color: MColors.overlay @@ -55,7 +51,6 @@ Item { } } - // Card (match PowerMenu "popover" sizing: width constrained, height fits content) Rectangle { id: dialog @@ -70,7 +65,6 @@ Item { border.color: MColors.border scale: PermissionManager.promptActive ? 1 : 0.95 - // Inner glow Rectangle { anchors.fill: parent anchors.margins: Math.max(1, Math.round(Constants.scaleFactor)) @@ -80,7 +74,6 @@ Item { border.color: Qt.rgba(255 / 255, 255 / 255, 255 / 255, 0.05) } - // Prevent click propagation MouseArea { anchors.fill: parent onClicked: {} @@ -93,7 +86,6 @@ Item { width: parent.width - dialog.pad * 2 spacing: MSpacing.lg - // Header (matches screenshot style) Row { width: parent.width spacing: MSpacing.md @@ -136,7 +128,6 @@ Item { } } - // App info Row { anchors.horizontalCenter: parent.horizontalCenter spacing: MSpacing.md @@ -171,45 +162,68 @@ Item { } } - // Permission description - Rectangle { - width: parent.width - radius: MRadius.md - color: MColors.bb10Elevated - border.width: Math.max(1, Math.round(Constants.scaleFactor)) - border.color: MColors.borderSubtle - implicitHeight: descRow.implicitHeight + MSpacing.lg * 2 - - Row { - id: descRow + ListView { + id: permList - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: MSpacing.lg - spacing: MSpacing.md + width: parent.width + height: Math.min(contentHeight, Math.round(300 * Constants.scaleFactor)) + clip: true + spacing: MSpacing.md + model: PermissionManager.currentPermissions - Text { - text: "●" - font.pixelSize: Math.round(22 * Constants.scaleFactor) - color: MColors.marathonTeal - anchors.verticalCenter: parent.verticalCenter - } + delegate: Rectangle { + width: parent.width + height: Math.round(56 * Constants.scaleFactor) + color: MColors.bb10Elevated + radius: MRadius.md + border.width: Math.max(1, Math.round(Constants.scaleFactor)) + border.color: MColors.borderSubtle - Text { - text: permissionDialog.permissionDesc - font.pixelSize: MTypography.sizeBody - font.family: MTypography.fontFamily - color: MColors.textPrimary - width: parent.width - Math.round(26 * Constants.scaleFactor) - wrapMode: Text.WordWrap + Row { + anchors.fill: parent + anchors.margins: MSpacing.md + spacing: MSpacing.md + + Item { + width: Math.round(24 * Constants.scaleFactor) + height: width + anchors.verticalCenter: parent.verticalCenter + + Icon { + anchors.centerIn: parent + name: permissionDialog.getPermissionIcon(modelData) + size: Math.round(24 * Constants.scaleFactor) + color: MColors.marathonTeal + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Math.round(60 * Constants.scaleFactor) + spacing: 2 + + Text { + text: modelData.charAt(0).toUpperCase() + modelData.slice(1) + font.pixelSize: MTypography.sizeBody + font.weight: Font.DemiBold + color: MColors.textPrimary + } + + Text { + text: PermissionManager.getPermissionDescription(modelData) + font.pixelSize: MTypography.sizeSmall + color: MColors.textSecondary + elide: Text.ElideRight + width: parent.width + } + } } } } Text { width: parent.width - text: "You can change this permission later in Settings." + text: "You can change these permissions later in Settings." font.pixelSize: MTypography.sizeSmall font.family: MTypography.fontFamily color: MColors.textSecondary @@ -222,24 +236,24 @@ Item { spacing: MSpacing.md MButton { - text: "Allow" + text: "Allow All" variant: "primary" width: parent.width - onClicked: PermissionManager.setPermission(permissionDialog.appId, permissionDialog.permission, true, true) + onClicked: PermissionManager.setPermissions(permissionDialog.appId, PermissionManager.currentPermissions, true, true) } MButton { text: "Allow Once" variant: "secondary" width: parent.width - onClicked: PermissionManager.setPermission(permissionDialog.appId, permissionDialog.permission, true, false) + onClicked: PermissionManager.setPermissions(permissionDialog.appId, PermissionManager.currentPermissions, true, false) } MButton { - text: "Deny" + text: "Deny All" variant: "tertiary" width: parent.width - onClicked: PermissionManager.setPermission(permissionDialog.appId, permissionDialog.permission, false, true) + onClicked: PermissionManager.setPermissions(permissionDialog.appId, PermissionManager.currentPermissions, false, true) } } } diff --git a/shell/qml/components/PowerMenu.qml b/shell/qml/components/PowerMenu.qml index 90deb0d3d..79bf82c68 100644 --- a/shell/qml/components/PowerMenu.qml +++ b/shell/qml/components/PowerMenu.qml @@ -2,10 +2,6 @@ import MarathonUI.Core import MarathonUI.Theme import QtQuick -/** - * World-Class Power Menu - Tile Grid Design - * Modern, visual, with large touchable tiles - */ Item { id: root @@ -19,7 +15,7 @@ Item { function show() { showing = true; visible = true; - HapticService.medium(); + HapticManager.medium(); fadeIn.start(); } @@ -32,7 +28,6 @@ Item { visible: false z: Constants.zIndexModal + 100 - // Dark backdrop Rectangle { id: backdrop @@ -49,7 +44,6 @@ Item { } } - // Power menu dialog Rectangle { id: dialog @@ -64,7 +58,6 @@ Item { opacity: 0 scale: 0.9 - // Inner glow Rectangle { anchors.fill: parent anchors.margins: Math.max(1, Math.round(Constants.scaleFactor)) @@ -74,7 +67,6 @@ Item { border.color: Qt.rgba(255 / 255, 255 / 255, 255 / 255, 0.05) } - // Prevent click propagation MouseArea { anchors.fill: parent onClicked: {} @@ -87,7 +79,6 @@ Item { width: parent.width - MSpacing.xxl * 2 spacing: MSpacing.lg - // Title Text { text: "Power Options" font.pixelSize: MTypography.sizeLarge @@ -98,14 +89,12 @@ Item { horizontalAlignment: Text.AlignHCenter } - // 2x2 Grid of power tiles Grid { width: parent.width columns: 2 rowSpacing: MSpacing.md columnSpacing: MSpacing.md - // Sleep Tile Rectangle { id: sleepTile @@ -117,7 +106,6 @@ Item { border.color: MColors.border scale: sleepMouseArea.pressed ? 0.95 : 1 - // Inner border Rectangle { anchors.fill: parent anchors.margins: 1 @@ -153,7 +141,7 @@ Item { anchors.fill: parent onClicked: { - HapticService.medium(); + HapticManager.medium(); root.sleepRequested(); root.hide(); } @@ -174,7 +162,6 @@ Item { } } - // Reboot Tile Rectangle { id: rebootTile @@ -186,7 +173,6 @@ Item { border.color: MColors.border scale: rebootMouseArea.pressed ? 0.95 : 1 - // Inner border Rectangle { anchors.fill: parent anchors.margins: 1 @@ -222,7 +208,7 @@ Item { anchors.fill: parent onClicked: { - HapticService.medium(); + HapticManager.medium(); root.rebootRequested(); root.hide(); } @@ -243,7 +229,6 @@ Item { } } - // Power Off Tile (primary/teal) Rectangle { id: powerOffTile @@ -253,7 +238,6 @@ Item { border.width: 0 scale: powerOffMouseArea.pressed ? 0.95 : 1 - // Outer glow Rectangle { visible: true anchors.centerIn: parent @@ -292,7 +276,7 @@ Item { anchors.fill: parent onClicked: { - HapticService.medium(); + HapticManager.medium(); root.shutdownRequested(); root.hide(); } @@ -326,7 +310,6 @@ Item { } } - // Cancel Tile Rectangle { id: cancelTile @@ -364,7 +347,7 @@ Item { anchors.fill: parent onClicked: { - HapticService.light(); + HapticManager.light(); root.canceled(); root.hide(); } @@ -388,7 +371,6 @@ Item { } } - // Fade in animation ParallelAnimation { id: fadeIn @@ -418,7 +400,6 @@ Item { } } - // Fade out animation SequentialAnimation { id: fadeOut diff --git a/shell/qml/components/QuickSettingsTile.qml b/shell/qml/components/QuickSettingsTile.qml index e8461d4d1..4d0210a33 100644 --- a/shell/qml/components/QuickSettingsTile.qml +++ b/shell/qml/components/QuickSettingsTile.qml @@ -3,16 +3,11 @@ import MarathonUI.Theme import QtQuick Item { - // Power is an action menu - // Alarm is a link to Clock app - // Screenshot is an action - id: tile property var toggleData: ({}) property real tileWidth: 160 property bool isAvailable: toggleData.available !== undefined ? toggleData.available : true - // Determine if this is a toggleable tile (has on/off state) vs a link tile readonly property bool isToggleable: toggleData.id !== "settings" && toggleData.id !== "lock" && toggleData.id !== "power" && toggleData.id !== "monitor" && toggleData.id !== "alarm" && toggleData.id !== "screenshot" property bool isPressed: false @@ -22,7 +17,6 @@ Item { width: tileWidth height: Constants.hubHeaderHeight - // TOGGLEABLE TILE: Split design - ALWAYS visible (structure consistent across states) Rectangle { id: toggleableTile @@ -36,19 +30,16 @@ Item { anchors.fill: parent spacing: 0 - // LEFT: Square icon box - ALWAYS VISIBLE Rectangle { id: iconBox width: Constants.hubHeaderHeight height: Constants.hubHeaderHeight radius: Constants.borderRadiusSharp - // OFF state: elevated surface. ON state: bright teal color: toggleData.active ? MColors.accentBright : MColors.bb10Elevated antialiasing: Constants.enableAntialiasing Icon { - // For toggleable items with -off variants, show the -off icon when inactive name: { var iconName = toggleData.icon || "grid"; if (!toggleData.active && (toggleData.id === "wifi" || toggleData.id === "bluetooth")) @@ -56,7 +47,6 @@ Item { return iconName; } - // OFF: standard text color. ON: dark (readable on teal). Unavailable: dim via opacity color: toggleData.active ? MColors.background : MColors.text size: Constants.iconSizeMedium anchors.centerIn: parent @@ -76,19 +66,15 @@ Item { } } - // RIGHT: Label box - ALWAYS VISIBLE Rectangle { width: parent.width - Constants.hubHeaderHeight height: Constants.hubHeaderHeight radius: Constants.borderRadiusSharp - // OFF: standard surface. ON: slightly elevated for cohesion color: isAvailable ? MColors.surface : Qt.rgba(MColors.surface.r, MColors.surface.g, MColors.surface.b, 0.5) border.width: Constants.borderWidthThin - // OFF: subtle border. ON: teal border for cohesion border.color: toggleData.active ? MColors.accentBright : MColors.border antialiasing: Constants.enableAntialiasing - // Inner border for depth Rectangle { anchors.fill: parent anchors.margins: 1 @@ -129,7 +115,6 @@ Item { } } - // Teal bar active indicator (bottom of label box) - ONLY when ON Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left @@ -158,7 +143,6 @@ Item { } } - // Press overlay Rectangle { anchors.fill: parent color: toggleData.active ? MColors.background : MColors.accentBright @@ -184,7 +168,6 @@ Item { } } - // ACTION/LINK TILE: Solid card design (Settings, Lock, Monitor, Alarm) Rectangle { id: linkTile @@ -193,13 +176,11 @@ Item { radius: Constants.borderRadiusSharp border.width: Constants.borderWidthThin border.color: MColors.border - // Action tiles use elevated surface for visual distinction from toggles color: isAvailable ? MColors.bb10Card : Qt.rgba(MColors.bb10Card.r, MColors.bb10Card.g, MColors.bb10Card.b, 0.5) antialiasing: Constants.enableAntialiasing scale: isPressed ? 0.98 : 1 opacity: isAvailable ? 1 : 0.5 - // Inner border for depth Rectangle { anchors.fill: parent anchors.margins: 1 @@ -219,7 +200,6 @@ Item { width: parent.width spacing: MSpacing.md - // Icon container - slightly different sizing for action tiles Rectangle { width: Constants.iconSizeMedium + MSpacing.md height: Constants.iconSizeMedium + MSpacing.md @@ -261,7 +241,6 @@ Item { } } - // Press overlay Rectangle { anchors.fill: parent color: MColors.text @@ -294,7 +273,7 @@ Item { enabled: isAvailable onPressed: function (mouse) { isPressed = true; - HapticService.light(); + HapticManager.light(); } onReleased: { isPressed = false; @@ -313,7 +292,7 @@ Item { if (!isAvailable) return; - HapticService.medium(); + HapticManager.medium(); tile.longPressed(); } } diff --git a/shell/qml/components/SafeAppLoader.qml b/shell/qml/components/SafeAppLoader.qml index 9a75fe2bc..7c88030a6 100644 --- a/shell/qml/components/SafeAppLoader.qml +++ b/shell/qml/components/SafeAppLoader.qml @@ -102,7 +102,6 @@ Item { onLoaded: { if (item) { console.log("SafeAppLoader: Successfully loaded app", root.appId); - // Connect to app's registration signals if (item.requestRegister) item.requestRegister.connect(function (appId, appInstance) { console.log("SafeAppLoader: App requested registration:", appId); diff --git a/shell/qml/components/ScreenshotPreview.qml b/shell/qml/components/ScreenshotPreview.qml index 4433e45b1..0a60f3016 100644 --- a/shell/qml/components/ScreenshotPreview.qml +++ b/shell/qml/components/ScreenshotPreview.qml @@ -87,7 +87,6 @@ Item { onClicked: { Logger.info("ScreenshotPreview", "Opening screenshot in gallery: " + filePath); hide(); - // Deep link to gallery app with the screenshot (shell handles showing the app window) NavigationRouter.navigateToDeepLink("gallery", "/image", { "path": filePath }); diff --git a/shell/qml/components/ShareSheet.qml b/shell/qml/components/ShareSheet.qml index ef91d55fd..45f10c622 100644 --- a/shell/qml/components/ShareSheet.qml +++ b/shell/qml/components/ShareSheet.qml @@ -174,7 +174,7 @@ Item { anchors.fill: parent onClicked: { Logger.info("ShareSheet", "Share to: " + modelData.name); - HapticService.light(); + HapticManager.light(); UIStore.closeShareSheet(); } } diff --git a/shell/qml/components/TaskCard.qml b/shell/qml/components/TaskCard.qml index 11cdcc118..cf2a84f82 100644 --- a/shell/qml/components/TaskCard.qml +++ b/shell/qml/components/TaskCard.qml @@ -19,6 +19,7 @@ Item { property bool gridMoving: false property bool gridDragging: false property bool suppressTapOpen: false + property Item nativeSurfaceItem: null signal closed signal taskClosed(string appId) @@ -113,7 +114,7 @@ Item { Rectangle { anchors.fill: parent anchors.margins: 1 - radius: parent.radius - 1 + radius: cardRoot.radius - 1 color: "transparent" border.width: 1 border.color: Qt.rgba(255, 255, 255, 0.03) @@ -126,7 +127,7 @@ Item { anchors.fill: parent anchors.bottomMargin: Math.round(50 * Constants.scaleFactor) color: MColors.background - radius: parent.parent.radius + radius: cardRoot.radius Loader { id: appPreview @@ -180,7 +181,6 @@ Item { Component.onCompleted: updateLiveApp() onLiveAppChanged: snapshotUpdateDebounce.restart() - // Debounced snapshot refresh (avoid spamming scheduleUpdate) Timer { id: snapshotUpdateDebounce @@ -192,7 +192,6 @@ Item { } } - // Prefer signal-based registration over polling. Connections { function onAppRegistered(appId, instance) { if (taskCard.type === "native") @@ -242,7 +241,6 @@ Item { width: parent.width height: (Constants.screenHeight / Constants.screenWidth) * width sourceItem: previewContainer.liveApp - // Snapshot-based preview: avoid live recursive rendering per-card. live: false recursive: true visible: previewContainer.liveApp !== null @@ -257,7 +255,6 @@ Item { } } - // Throttled live updates: keep previews feeling alive without per-frame cost. Timer { id: livePreviewRefreshTimer @@ -279,32 +276,35 @@ Item { source: taskCard.haveWayland ? "qrc:/qt/qml/MarathonOS/Shell/qml/components/WaylandShellSurfaceItem.qml" : "" onItemChanged: { if (item) { - item.anchors.fill = nativeSurfaceLoader; + taskCard.nativeSurfaceItem = item; + taskCard.nativeSurfaceItem.anchors.fill = nativeSurfaceLoader; item.autoResize = false; item.hasSentInitialSize = true; item.isMinimized = true; + } else { + taskCard.nativeSurfaceItem = null; } } Binding { - target: nativeSurfaceLoader.item + target: taskCard.nativeSurfaceItem property: "surfaceObj" value: taskCard.waylandSurface - when: nativeSurfaceLoader.item !== null + when: taskCard.nativeSurfaceItem !== null } Binding { - target: nativeSurfaceLoader.item + target: taskCard.nativeSurfaceItem property: "touchEventsEnabled" value: false - when: nativeSurfaceLoader.item !== null + when: taskCard.nativeSurfaceItem !== null } Binding { - target: nativeSurfaceLoader.item + target: taskCard.nativeSurfaceItem property: "inputEventsEnabled" value: false - when: nativeSurfaceLoader.item !== null + when: taskCard.nativeSurfaceItem !== null } } diff --git a/shell/qml/components/TaskPageIndicator.qml b/shell/qml/components/TaskPageIndicator.qml index 62a136939..ddddc7230 100644 --- a/shell/qml/components/TaskPageIndicator.qml +++ b/shell/qml/components/TaskPageIndicator.qml @@ -1,13 +1,9 @@ -// Vertical page indicator for task switcher -// Shows modern iOS-style pills/dots on the right when more than 4 tasks - import MarathonUI.Theme import QtQuick Column { id: pageIndicator - // Required properties required property int taskCount required property real gridContentY required property real gridHeight @@ -17,7 +13,6 @@ Column { return Math.max(0, Math.min(page, pageCount - 1)); } - // Positioned on right side (vertically centered) - matches vertical scrolling anchors.right: parent.right anchors.rightMargin: Constants.spacingMedium anchors.verticalCenter: parent.verticalCenter @@ -33,12 +28,10 @@ Column { property bool isActive: index === pageIndicator.currentPage - // Pill shape for active, circle for inactive width: 6 height: isActive ? 20 : 6 radius: 3 color: isActive ? MColors.accent : Qt.rgba(255, 255, 255, 0.4) - // Subtle shadow/glow layer.enabled: true layer.effect: null diff --git a/shell/qml/components/VirtualKeyboard.qml b/shell/qml/components/VirtualKeyboard.qml index b0d96bba9..61ee3eedc 100644 --- a/shell/qml/components/VirtualKeyboard.qml +++ b/shell/qml/components/VirtualKeyboard.qml @@ -1,33 +1,21 @@ import "../keyboard/Core" import QtQuick -// Virtual Keyboard Container - Marathon Custom Keyboard Item { - // DEBUG: Monitor height changes (disabled for performance) - // onKeyboardHeightChanged: { - // Logger.debug("VirtualKeyboard", "keyboardHeight changed: " + keyboardHeight) - // } - // onHeightChanged: { - // Logger.debug("VirtualKeyboard", "container height changed: " + height) - // } - id: keyboardContainer property bool keyboardAvailable: true property bool active: false - // Let keyboard determine its own height dynamically readonly property real keyboardHeight: marathonKeyboard.implicitHeight width: parent ? parent.width : 0 height: active ? keyboardHeight : 0 - // Proper Qt anchoring - ABOVE nav bar anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: Constants.navBarHeight // Position above nav bar + anchors.bottomMargin: Constants.navBarHeight z: Constants.zIndexKeyboard visible: active - // Watch for external active changes and show/hide keyboard onActiveChanged: { Logger.info("VirtualKeyboard", "Container active set to: " + active); if (active) @@ -36,30 +24,24 @@ Item { marathonKeyboard.hide(); } - // Marathon Custom Keyboard (dismiss button integrated into layout) MarathonKeyboard { id: marathonKeyboard anchors.fill: parent - // Wire to C++ Input Method Engine for proper text input onKeyPressed: function (text) { Logger.info("VirtualKeyboard", "Key pressed: " + text); - // Use C++ IME backend for reliable text input InputMethodEngine.commitText(text); } onBackspace: function () { Logger.info("VirtualKeyboard", "Backspace pressed"); - // Use C++ IME backend InputMethodEngine.sendBackspace(); } onEnter: function () { Logger.info("VirtualKeyboard", "Enter pressed"); - // Use C++ IME backend InputMethodEngine.sendEnter(); } - // Handle dismiss button click from keyboard onDismissRequested: { - HapticService.light(); + HapticManager.light(); keyboardContainer.active = false; Logger.info("VirtualKeyboard", "Keyboard dismissed via dismiss button"); } diff --git a/shell/qml/components/WaylandShellSurfaceItem.qml b/shell/qml/components/WaylandShellSurfaceItem.qml index 58e4438e4..c266b2695 100644 --- a/shell/qml/components/WaylandShellSurfaceItem.qml +++ b/shell/qml/components/WaylandShellSurfaceItem.qml @@ -4,6 +4,7 @@ import QtQuick import QtWayland.Compositor ShellSurfaceItem { + id: surfaceItem property var surfaceObj: null property int surfaceId: -1 property size lastSentSize: Qt.size(0, 0) @@ -84,9 +85,6 @@ ShellSurfaceItem { Logger.debug("WaylandShellSurfaceItem", "sendSizeToApp skipped: no toplevel (surfaceObj: " + (surfaceObj ? "exists" : "null") + ")"); return; } - // CRITICAL: Validate the entire xdgSurface chain before calling sendMaximized - // This prevents SIGSEGV when toplevel exists but internal structures are not yet initialized - // (race condition on Alpine/aarch64 with Qt6WaylandCompositor) var xdgSurface = toplevel.xdgSurface; if (!xdgSurface) { Logger.debug("WaylandShellSurfaceItem", "sendSizeToApp deferred: toplevel.xdgSurface not ready yet"); @@ -106,22 +104,15 @@ ShellSurfaceItem { lastSentSize = newSize; hasSentInitialSize = true; Logger.info("WaylandShellSurfaceItem", "Configuring app size: " + newSize.width + "x" + newSize.height); - // WORKAROUND: Use sendConfigure instead of sendMaximized to avoid - // Qt6WaylandCompositor + eglfs crash. sendMaximized causes SIGSEGV on - // embedded platforms due to OpenGL context issues in xdg-toplevel handling. - // sendConfigure with explicit size and states is more stable. var states = []; - states.push(1); // activated (XdgToplevel.State.Activated) - // Note: Don't push maximized state (3) - causes same crash on some platforms + states.push(1); toplevel.sendConfigure(newSize, states); if (AppLaunchService.compositor) { AppLaunchService.compositor.activateSurface(surfaceId); - // Take QML focus so keyboard events flow to the WaylandQuickItem Qt.callLater(takeFocusForKeyboard); } } - // Take QML focus when the surface is activated so keyboard events flow to it function takeFocusForKeyboard() { if (!isMinimized && hasSentInitialSize) forceActiveFocus(); @@ -130,9 +121,6 @@ ShellSurfaceItem { autoCreatePopupItems: true opacity: hasSentInitialSize && hasFirstFrame ? 1 : 0 bufferLocked: isMinimized - // CRITICAL: Only assign shellSurface when the underlying surface is fully initialized. - // Qt's QWaylandQuickShellSurfaceItem::setShellSurface connects signals to the surface, - // and will SIGSEGV if xdgSurface.surface is null during the handoff. shellSurface: { var xdg = _xdgSurfaceFromObj(surfaceObj); if (xdg && xdg.surface) @@ -141,7 +129,7 @@ ShellSurfaceItem { return null; } touchEventsEnabled: true - focusOnClick: true // Ensure keyboard focus on click + focusOnClick: true output: AppLaunchService.compositor ? AppLaunchService.compositor.output : null onShellSurfaceChanged: { if (shellSurface) { @@ -183,7 +171,7 @@ ShellSurfaceItem { Rectangle { anchors.fill: parent color: MColors.elevated - visible: !parent.parent.shellSurface + visible: !surfaceItem.hasFirstFrame Text { anchors.centerIn: parent diff --git a/shell/qml/components/WiFiPasswordDialog.qml b/shell/qml/components/WiFiPasswordDialog.qml index 5f1cce773..a10b35f90 100644 --- a/shell/qml/components/WiFiPasswordDialog.qml +++ b/shell/qml/components/WiFiPasswordDialog.qml @@ -4,21 +4,9 @@ import MarathonUI.Theme import QtQuick import QtQuick.Controls -/** - * Polished WiFi Password Dialog - * - * Slide-up modal for entering WiFi passwords with: - * - Auto-focus on input field - * - Show/hide password toggle - * - Signal strength indicator - * - Security type badge - * - Loading state during connection - * - Error handling with retry - */ Item { id: wifiDialog - // Public API property string networkSsid: "" property int signalStrength: 0 property string securityType: "" @@ -29,7 +17,6 @@ Item { signal connectRequested(string ssid, string password) signal cancelled - // Show the dialog function show(ssid, strength, security, isSecured) { networkSsid = ssid; signalStrength = strength; @@ -41,23 +28,20 @@ Item { passwordInput.forceActiveFocus(); wifiDialog.visible = true; showAnimation.start(); - HapticService.light(); + HapticManager.light(); Logger.info("WiFiDialog", "Showing dialog for: " + ssid); } - // Hide the dialog function hide() { hideAnimation.start(); } - // Show error function showError(message) { errorMessage = message; isConnecting = false; - HapticService.medium(); + HapticManager.medium(); } - // Show connecting state function showConnecting() { isConnecting = true; errorMessage = ""; @@ -66,13 +50,11 @@ Item { anchors.fill: parent visible: false z: Constants.zIndexModalOverlay + 10 - // Handle back button/escape key Keys.onEscapePressed: { if (!isConnecting) wifiDialog.hide(); } - // Background overlay Rectangle { id: overlay @@ -89,7 +71,6 @@ Item { } } - // Dialog card Rectangle { id: dialogCard @@ -102,7 +83,6 @@ Item { color: MColors.surface border.width: Constants.borderWidthThin border.color: MColors.border - // Glass morphism effect layer.enabled: true Column { @@ -114,12 +94,10 @@ Item { anchors.margins: Constants.spacingLarge spacing: Constants.spacingLarge - // Header row Row { width: parent.width spacing: Constants.spacingMedium - // Network icon with signal strength Rectangle { width: Constants.touchTargetMedium height: Constants.touchTargetMedium @@ -136,7 +114,6 @@ Item { } } - // Network info Column { width: parent.width - Constants.touchTargetMedium - Constants.spacingMedium spacing: Constants.spacingXSmall @@ -155,7 +132,6 @@ Item { Row { spacing: Constants.spacingSmall - // Security badge Rectangle { width: securityBadgeText.width + Math.round(16 * Constants.scaleFactor) height: Math.round(24 * Constants.scaleFactor) @@ -174,7 +150,6 @@ Item { } } - // Signal strength text Text { text: signalStrength >= 75 ? "Excellent" : signalStrength >= 50 ? "Good" : signalStrength >= 25 ? "Fair" : "Weak" font.pixelSize: MTypography.sizeXSmall @@ -186,7 +161,6 @@ Item { } } - // Password input Rectangle { width: parent.width height: Constants.inputHeight @@ -226,7 +200,6 @@ Item { connectButton.clicked(); } - // Placeholder text (TextInput doesn't render placeholderText, so we fake it) Text { text: "Enter password" font: passwordInput.font @@ -236,7 +209,6 @@ Item { } } - // Show/hide password toggle Rectangle { id: showPasswordToggle @@ -259,7 +231,7 @@ Item { anchors.fill: parent onClicked: { showPasswordToggle.checked = !showPasswordToggle.checked; - HapticService.light(); + HapticManager.light(); } } } @@ -272,7 +244,6 @@ Item { } } - // Error message Rectangle { width: parent.width height: errorText.height + Constants.spacingMedium @@ -308,7 +279,6 @@ Item { } } - // Connection progress Column { width: parent.width spacing: Constants.spacingSmall @@ -335,13 +305,11 @@ Item { } } - // Action buttons Row { width: parent.width height: Constants.touchTargetMedium spacing: Constants.spacingMedium - // Cancel button Rectangle { width: (parent.width - Constants.spacingMedium) / 2 height: parent.height @@ -364,14 +332,13 @@ Item { enabled: !isConnecting onClicked: { Logger.info("WiFiDialog", "Cancelled"); - HapticService.light(); + HapticManager.light(); wifiDialog.cancelled(); wifiDialog.hide(); } } } - // Connect button Rectangle { id: connectButton @@ -397,7 +364,7 @@ Item { enabled: !isConnecting && (!secured || passwordInput.text.length >= 8) onClicked: { Logger.info("WiFiDialog", "Connect clicked for: " + networkSsid); - HapticService.medium(); + HapticManager.medium(); wifiDialog.showConnecting(); wifiDialog.connectRequested(networkSsid, passwordInput.text); } @@ -405,7 +372,6 @@ Item { } } - // Help text Text { text: secured ? "Password must be at least 8 characters" : "This network is not secured" font.pixelSize: MTypography.sizeXSmall @@ -429,7 +395,6 @@ Item { } } - // Show animation ParallelAnimation { id: showAnimation @@ -452,7 +417,6 @@ Item { } } - // Hide animation SequentialAnimation { id: hideAnimation diff --git a/shell/qml/components/ui/Button.qml b/shell/qml/components/ui/Button.qml index 08b753912..45d115625 100644 --- a/shell/qml/components/ui/Button.qml +++ b/shell/qml/components/ui/Button.qml @@ -1,6 +1,4 @@ -// DEPRECATED: Use MarathonUI.Core.MButton instead -// This component is kept for backward compatibility only - +import MarathonUI.Core import MarathonUI.Theme import QtQuick @@ -21,7 +19,7 @@ Rectangle { console.warn("DEPRECATED: components/ui/Button is deprecated. Use MarathonUI.Core.MButton instead."); } width: Math.max(Math.round(120 * Constants.scaleFactor), buttonText.width + Math.round(32 * Constants.scaleFactor)) - height: Constants.touchTargetMedium // BB10: 70px minimum + height: Constants.touchTargetMedium radius: Constants.borderRadiusSmall color: { if (disabled) @@ -43,7 +41,7 @@ Rectangle { name: iconName size: 24 anchors.verticalCenter: parent.verticalCenter - color: "white" // Or bind to a property + color: "white" visible: iconName !== "" } @@ -77,7 +75,6 @@ Rectangle { } } - // BB10 Highlight behavior (page 111) Rectangle { anchors.fill: parent radius: parent.radius diff --git a/shell/qml/components/ui/ListPickerModal.qml b/shell/qml/components/ui/ListPickerModal.qml index 95060e139..f458ed697 100644 --- a/shell/qml/components/ui/ListPickerModal.qml +++ b/shell/qml/components/ui/ListPickerModal.qml @@ -14,7 +14,7 @@ Modal { Column { width: parent.width - spacing: 0 // No spacing between list items + spacing: 0 Repeater { model: options @@ -25,7 +25,6 @@ Modal { color: "transparent" radius: Constants.borderRadiusSmall - // Glass morphism hover effect Rectangle { anchors.fill: parent color: Qt.rgba(255, 255, 255, 0.02) @@ -42,7 +41,6 @@ Modal { } } - // Press feedback Rectangle { anchors.fill: parent color: MColors.marathonTeal diff --git a/shell/qml/components/ui/Modal.qml b/shell/qml/components/ui/Modal.qml index 374adaf6b..ec0a01f8c 100644 --- a/shell/qml/components/ui/Modal.qml +++ b/shell/qml/components/ui/Modal.qml @@ -77,7 +77,6 @@ Rectangle { border.width: 1 border.color: MColors.border - // Inner glow Rectangle { anchors.fill: parent anchors.margins: 1 diff --git a/shell/qml/components/ui/PageIndicator.qml b/shell/qml/components/ui/PageIndicator.qml index 299b90df4..f9907e79b 100644 --- a/shell/qml/components/ui/PageIndicator.qml +++ b/shell/qml/components/ui/PageIndicator.qml @@ -1,3 +1,4 @@ +import MarathonUI.Core import QtQuick Row { diff --git a/shell/qml/components/ui/StorageDetailsModal.qml b/shell/qml/components/ui/StorageDetailsModal.qml index 3d5a06371..8b94c5aff 100644 --- a/shell/qml/components/ui/StorageDetailsModal.qml +++ b/shell/qml/components/ui/StorageDetailsModal.qml @@ -13,7 +13,6 @@ Modal { width: parent.width spacing: Constants.spacingXLarge - // Total Storage Bar Column { width: parent.width spacing: Constants.spacingSmall @@ -46,7 +45,7 @@ Modal { color: MColors.background Rectangle { - width: parent.width * 0.71 // 45.2/64 + width: parent.width * 0.71 height: parent.height radius: parent.radius color: MColors.marathonTeal @@ -54,7 +53,6 @@ Modal { } } - // Storage Breakdown Column { width: parent.width spacing: 0 diff --git a/shell/qml/components/ui/TextInputModal.qml b/shell/qml/components/ui/TextInputModal.qml index f86d174f2..488d27143 100644 --- a/shell/qml/components/ui/TextInputModal.qml +++ b/shell/qml/components/ui/TextInputModal.qml @@ -103,7 +103,6 @@ Modal { border.width: Constants.borderWidthThin border.color: Qt.rgba(20, 184, 166, 0.4) - // Glow effect on hover Rectangle { anchors.fill: parent radius: parent.radius diff --git a/shell/qml/core/Constants.qml b/shell/qml/core/Constants.qml index cc2bfd199..d498e8b5b 100644 --- a/shell/qml/core/Constants.qml +++ b/shell/qml/core/Constants.qml @@ -2,95 +2,18 @@ pragma Singleton import QtQuick QtObject { - // ========================================================================= - // RESPONSIVE SIZING SYSTEM - // ========================================================================= - // Matches baseDPI for 1:1 scaling in desktop testing (device: 320) - // ========================================================================= - // Z-INDEX LAYERS - // ========================================================================= - // ========================================================================= - // GESTURE THRESHOLDS (responsive) - // ========================================================================= - // ========================================================================= - // ANIMATION DURATIONS (time-based, not size-based) - // ========================================================================= - // ========================================================================= - // SESSION & TIMEOUT - // ========================================================================= - // ========================================================================= - // PERFORMANCE MODE - // ========================================================================= - // ========================================================================= - // PEEK GESTURE THRESHOLDS (from legacy Theme.qml) - // ========================================================================= - // ========================================================================= - // LAYOUT DIMENSIONS (responsive) - // ========================================================================= - // ========================================================================= - // PAGE INDICATORS (responsive) - // ========================================================================= - // ========================================================================= - // LOCK SCREEN (responsive) - // ========================================================================= - // ========================================================================= - // SCROLLING PERFORMANCE (physics-based, tuned for touch) - // ========================================================================= - // Responsive to fast flicks - // ========================================================================= - // BB10 TOUCH TARGETS (responsive) - // ========================================================================= - // Divider lines - // ========================================================================= - // BB10 ACTION BAR (responsive) - // ========================================================================= - // ========================================================================= - // APP GRID (responsive) - // ========================================================================= - // ========================================================================= - // CARDS (responsive) - // ========================================================================= - // ========================================================================= - // TYPOGRAPHY (responsive) - // ========================================================================= - // ========================================================================= - // SPACING SYSTEM (responsive) - // ========================================================================= - // ========================================================================= - // BORDERS & RADII (responsive, BB10-inspired sharp) - // ========================================================================= - // ========================================================================= - // ICON SIZES (responsive) - // ========================================================================= - // ========================================================================= - // SHADOWS (responsive) - // ========================================================================= - // ========================================================================= - // MODAL & OVERLAY SIZES (responsive) - // ========================================================================= - // ========================================================================= - // HELPER FUNCTION - // ========================================================================= - id: constants - // Screen dimensions (initialized from ScreenMetricsCpp; updated by updateScreenSize()) property real screenWidth: (typeof ScreenMetricsCpp !== "undefined" && ScreenMetricsCpp) ? ScreenMetricsCpp.width : 0 property real screenHeight: (typeof ScreenMetricsCpp !== "undefined" && ScreenMetricsCpp) ? ScreenMetricsCpp.height : 0 - // Pixel diagonal (computed; updated automatically as screenWidth/screenHeight change) readonly property real screenDiagonal: Math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight) - // Physical DPI (fallback to baseDPI until updated) property real dpi: (typeof ScreenMetricsCpp !== "undefined" && ScreenMetricsCpp && ScreenMetricsCpp.dpi > 0) ? ScreenMetricsCpp.dpi : baseDPI - // Responsive scaling - scale everything based on ACTUAL DPI readonly property real baseDPI: 160 - property real userScaleFactor: 1 // Initialized from SettingsManagerCpp + property real userScaleFactor: 1 readonly property real scaleFactor: (dpi / baseDPI) * userScaleFactor - // Two-way binding for userScaleFactor property Binding userScaleFactorBinding - // Legacy height-based scaling readonly property real baseHeight: 800 readonly property real heightScaleFactor: screenHeight / baseHeight - // Aspect ratio detection readonly property real tallScreenRatio: 1.2 readonly property real squareScreenTolerance: 100 readonly property bool isTallScreen: (screenWidth > 0) ? (screenHeight / screenWidth > tallScreenRatio) : false @@ -113,6 +36,7 @@ QtObject { readonly property int zIndexNavBarApp: 1600 readonly property int zIndexStatusBarDrag: 1700 readonly property int zIndexModalOverlay: 2000 + readonly property int zIndexModal: zIndexModalOverlay readonly property int zIndexKeyboard: 3000 readonly property real gestureEdgeWidth: Math.round(50 * scaleFactor) readonly property real gesturePeekThreshold: Math.round(100 * scaleFactor) @@ -126,10 +50,9 @@ QtObject { readonly property int animationDurationFast: 150 readonly property int animationDurationNormal: 250 readonly property int animationDurationSlow: 400 - readonly property int sessionTimeout: 600000 // 10 minutes + readonly property int sessionTimeout: 600000 property bool performanceMode: false readonly property bool enableAnimations: !performanceMode - // Debug mode - controlled by MARATHON_DEBUG environment variable property bool debugMode: typeof MARATHON_DEBUG_ENABLED !== 'undefined' ? MARATHON_DEBUG_ENABLED : false readonly property int peekThreshold: 40 readonly property int commitThreshold: 100 @@ -147,27 +70,20 @@ QtObject { readonly property real lockScreenNotificationSize: Math.round(40 * scaleFactor) readonly property real lockScreenShortcutSize: Math.round(64 * scaleFactor) readonly property int flickDecelerationFast: 8000 - readonly property int flickVelocityMax: 5000 // Higher for responsive touch - // Touch-optimized flick physics + readonly property int flickVelocityMax: 5000 readonly property int touchFlickDeceleration: 25000 - // Snappy page transitions readonly property int touchFlickVelocity: 8000 readonly property real touchTargetLarge: Math.round(90 * scaleFactor) + readonly property real touchTargetXLarge: Math.round(110 * scaleFactor) readonly property real touchTargetMedium: Math.round(70 * scaleFactor) readonly property real touchTargetSmall: Math.round(60 * scaleFactor) readonly property real touchTargetIndicator: Math.round(50 * scaleFactor) readonly property real touchTargetMinimum: Math.max(44, Math.round(45 * scaleFactor)) - // Common component dimensions (responsive) readonly property real inputHeight: Math.round(48 * scaleFactor) - // Standard text input readonly property real listItemHeight: Math.round(56 * scaleFactor) - // Standard list item readonly property real iconButtonSize: Math.round(20 * scaleFactor) - // Small icon button readonly property real smallIndicatorSize: Math.round(8 * scaleFactor) - // Small dots/indicators readonly property real mediumIndicatorSize: Math.round(12 * scaleFactor) - // Medium indicators readonly property real dividerHeight: Math.max(1, Math.round(1 * scaleFactor)) readonly property real actionBarHeight: Math.round(72 * scaleFactor) readonly property real hubHeaderHeight: Math.round(80 * scaleFactor) @@ -185,7 +101,7 @@ QtObject { readonly property real fontSizeXLarge: Math.round(24 * scaleFactor) readonly property real fontSizeXXLarge: Math.round(32 * scaleFactor) readonly property real fontSizeHuge: Math.round(48 * scaleFactor) - readonly property real fontSizeGigantic: Math.round(96 * scaleFactor) // Lock screen clock + readonly property real fontSizeGigantic: Math.round(96 * scaleFactor) readonly property real spacingXSmall: Math.round(5 * scaleFactor) readonly property real spacingSmall: Math.round(10 * scaleFactor) readonly property real spacingMedium: Math.round(16 * scaleFactor) @@ -215,27 +131,22 @@ QtObject { readonly property real toastHeight: Math.round(64 * scaleFactor) readonly property real hudSize: Math.round(128 * scaleFactor) - // Update screen dimensions (called by shell on startup/resize) function updateScreenSize(width, height, deviceDpi) { screenWidth = width; screenHeight = height; - // Priority order: Reported > Fallback var dpiMin = 50; var dpiMax = 1000; - var newDpi = baseDPI; // Default fallback + var newDpi = baseDPI; var dpiSource = "fallback"; if (deviceDpi && deviceDpi >= dpiMin && deviceDpi <= dpiMax) { - // Trust reported DPI if reasonable (validated range) newDpi = deviceDpi; dpiSource = "reported"; } else { - // Fallback to baseDPI for unknown/invalid cases newDpi = baseDPI; dpiSource = "fallback"; if (deviceDpi && (deviceDpi < dpiMin || deviceDpi > dpiMax)) Logger.warn("Constants", "Invalid deviceDPI (" + deviceDpi + "), using baseDPI: " + newDpi); } - // Only update if changed (avoid unnecessary property updates and cascading recalculations) if (Math.abs(dpi - newDpi) > 0.1) { dpi = newDpi; Logger.debug("Constants", "Screen: " + width.toFixed(0) + "×" + height.toFixed(0) + " @ " + dpi.toFixed(0) + " DPI (source: " + dpiSource + ", scaleFactor: " + scaleFactor.toFixed(2) + ")"); diff --git a/shell/qml/core/Logger.qml b/shell/qml/core/Logger.qml index 7db6ece8b..57e4523f9 100644 --- a/shell/qml/core/Logger.qml +++ b/shell/qml/core/Logger.qml @@ -11,8 +11,6 @@ QtObject { ERROR = 3 } - // Default to WARN in production; allow full verbosity in debug. - // This keeps the core experience quiet + fast on low-end devices. property int currentLevel: Constants.debugMode ? Logger.Level.DEBUG : Logger.Level.WARN function debug(component, message) { diff --git a/shell/qml/keyboard/Core/KeyboardPerformance.qml b/shell/qml/keyboard/Core/KeyboardPerformance.qml index 651507067..f95b72570 100644 --- a/shell/qml/keyboard/Core/KeyboardPerformance.qml +++ b/shell/qml/keyboard/Core/KeyboardPerformance.qml @@ -7,7 +7,7 @@ QtObject { readonly property bool useTextMetricsCache: true readonly property int keyPressAnimationDuration: 50 readonly property bool lazyLoadPredictions: true - readonly property int touchEventPriority: Qt.HighEventPriority + readonly property int touchEventPriority: 1 readonly property bool optimizeBindings: true property var frameTimings: [] property real averageFrameTime: 16.67 diff --git a/shell/qml/keyboard/Core/MarathonKeyboard.qml b/shell/qml/keyboard/Core/MarathonKeyboard.qml index 207bfd021..b79164c65 100644 --- a/shell/qml/keyboard/Core/MarathonKeyboard.qml +++ b/shell/qml/keyboard/Core/MarathonKeyboard.qml @@ -594,6 +594,7 @@ Rectangle { inputContext: InputContext { id: inputContextInstance + keyboard: keyboard onTextInserted: function (text) { if (text === " " || text === "\n") diff --git a/shell/qml/keyboard/Input/InputContext.qml b/shell/qml/keyboard/Input/InputContext.qml index 70f83e5c9..367c63be0 100644 --- a/shell/qml/keyboard/Input/InputContext.qml +++ b/shell/qml/keyboard/Input/InputContext.qml @@ -3,7 +3,9 @@ import QtQuick Item { id: inputContext - readonly property bool hasActiveFocus: Qt.inputMethod && Qt.inputMethod.visible + property var inputMethod: Qt.inputMethod + property var keyboard: null + readonly property bool hasActiveFocus: inputMethod && inputMethod["visible"] property string inputMode: "text" readonly property string recommendedLayout: { switch (inputMode) { @@ -93,18 +95,18 @@ Item { } function getCurrentWord() { - if (parent && parent.keyboard && parent.keyboard.currentWord) - return parent.keyboard.currentWord; + if (keyboard && keyboard["currentWord"]) + return keyboard["currentWord"]; return ""; } function detectInputMode() { - if (!Qt.inputMethod) { + if (!inputMethod) { inputMode = "text"; return; } - var hints = Qt.inputMethod.inputItemHints; + var hints = inputMethod["inputItemHints"]; if (typeof hints === 'undefined' || hints === null) { inputMode = "text"; return; @@ -127,7 +129,7 @@ Item { Connections { function onVisibleChanged() { - if (Qt.inputMethod.visible) + if (inputMethod["visible"]) detectInputMode(); } diff --git a/shell/qml/keyboard/Layouts/DynamicLayout.qml b/shell/qml/keyboard/Layouts/DynamicLayout.qml index 3e6f03f72..f8f2316ff 100644 --- a/shell/qml/keyboard/Layouts/DynamicLayout.qml +++ b/shell/qml/keyboard/Layouts/DynamicLayout.qml @@ -5,7 +5,7 @@ Item { property bool shifted: false property bool capsLock: false - property var layoutData: null // Set via Connections.onLayoutLoaded only + property var layoutData: null property bool rtl: dynamicLayout.layoutData ? dynamicLayout.layoutData.rtl : false property bool isLoading: dynamicLayout.layoutData === null @@ -19,7 +19,6 @@ Item { signal languageSwitchClicked implicitHeight: layoutColumn.implicitHeight - // Initialize from LanguageManager if already loaded (for hot reloading) Component.onCompleted: { if (LanguageManager.currentLayout) dynamicLayout.layoutData = LanguageManager.currentLayout; diff --git a/shell/qml/keyboard/Layouts/EmailLayout.qml b/shell/qml/keyboard/Layouts/EmailLayout.qml index 11b3eb852..005323e90 100644 --- a/shell/qml/keyboard/Layouts/EmailLayout.qml +++ b/shell/qml/keyboard/Layouts/EmailLayout.qml @@ -1,15 +1,11 @@ import "../UI" -// Marathon Virtual Keyboard - Email Layout -// Optimized for email addresses with @ and domain shortcuts import QtQuick Item { id: layout - // Properties property bool shifted: false - // Signals signal keyClicked(string text) signal backspaceClicked signal enterClicked @@ -17,7 +13,6 @@ Item { signal layoutSwitchClicked(string layout) signal dismissClicked - // Expose Column's implicit height implicitHeight: layoutColumn.implicitHeight Column { @@ -26,8 +21,8 @@ Item { width: parent.width spacing: 0 - // Row 1: Q W E R T Y U I O P Row { + id: row1 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -37,7 +32,7 @@ Item { model: ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"] Key { - width: parent.keyWidth + width: row1.keyWidth text: modelData displayText: layout.shifted ? modelData.toUpperCase() : modelData onClicked: { @@ -53,8 +48,8 @@ Item { color: "#666666" } - // Row 2: A S D F G H J K L @ Row { + id: row2 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -64,7 +59,7 @@ Item { model: ["a", "s", "d", "f", "g", "h", "j", "k", "l", "@"] Key { - width: parent.keyWidth + width: row2.keyWidth text: modelData displayText: (modelData === "@" || !layout.shifted) ? modelData : modelData.toUpperCase() isSpecial: modelData === "@" @@ -81,15 +76,13 @@ Item { color: "#666666" } - // Row 3: Shift Z X C V B N M Backspace Row { width: parent.width spacing: Math.round(1 * Constants.scaleFactor) - // Shift key Key { width: Math.round(60 * Constants.scaleFactor) - iconName: layout.capsLock ? "chevrons-up" : "chevron-up" + iconName: layout.shifted ? "chevrons-up" : "chevron-up" isSpecial: true highlighted: layout.shifted onClicked: { @@ -97,7 +90,6 @@ Item { } } - // Letter keys Repeater { model: ["z", "x", "c", "v", "b", "n", "m"] @@ -111,7 +103,6 @@ Item { } } - // Backspace Key { width: Math.round(60 * Constants.scaleFactor) iconName: "delete" @@ -128,12 +119,10 @@ Item { color: "#666666" } - // Row 4: 123 . _ - Space .com .net Return Row { width: parent.width spacing: Math.round(1 * Constants.scaleFactor) - // 123 key Key { width: Math.round(55 * Constants.scaleFactor) text: "123" @@ -144,7 +133,6 @@ Item { } } - // Common email characters Key { width: Math.round(40 * Constants.scaleFactor) text: "." @@ -163,7 +151,6 @@ Item { displayText: "-" } - // Space bar Key { width: Math.round(120 * Constants.scaleFactor) text: " " @@ -173,7 +160,6 @@ Item { } } - // Domain shortcuts Key { width: Math.round(55 * Constants.scaleFactor) text: ".com" @@ -194,7 +180,6 @@ Item { } } - // Return key Key { width: Math.round(60 * Constants.scaleFactor) iconName: "corner-down-left" diff --git a/shell/qml/keyboard/Layouts/EmojiLayout.qml b/shell/qml/keyboard/Layouts/EmojiLayout.qml index 207342c45..f05e73b7f 100644 --- a/shell/qml/keyboard/Layouts/EmojiLayout.qml +++ b/shell/qml/keyboard/Layouts/EmojiLayout.qml @@ -2,22 +2,18 @@ import "../UI" import MarathonUI.Core import MarathonUI.Navigation import MarathonUI.Theme -// Marathon Virtual Keyboard - Emoji Layout import QtQuick import QtQuick.Controls Item { id: layout - // Properties property bool shifted: false property bool capsLock: false property string searchText: "" - // Emoji Data readonly property var recentEmojis: ["😂", "❤️", "👍", "🔥", "😊", "🤔", "😭", "🥰", "😎", "✨"] readonly property var smileys: ["😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "🥲", "☺️", "😊", "😇", "🙂", "🙃", "😉", "😌", "😍", "🥰", "😘", "😗", "😙", "😚", "😋", "😛", "😝", "😜", "🤪", "🤨", "🧐", "🤓", "😎", "🥸", "🤩", "🥳", "😏", "😒", "😞", "😔", "😟", "😕"] readonly property var animals: ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐻‍❄️", "🐨", "🐯", "🦁", "cow", "🐷", "🐽", "🐸", "🐵", "🙈", "🙉", "🙊", "🐒", "🐔", "🐧", "🐦", "🐤", "🐣", "🐥", "duck", "🦅", "🦉"] - // Category Data readonly property var categoryMap: { "recent": recentEmojis, "smileys": smileys, @@ -29,41 +25,33 @@ Item { "symbols": symbols, "flags": flags } - // Additional Categories readonly property var food: ["🍏", "🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭", "🍍", "🥥", "🥝", "🍅", "🍆", "🥑", "🥦", "🥬", "🥒", "🌶", "🫑", "🌽", "🥕", "🫒", "🧄", "🧅", "🥔", "🍠", "🥐", "🥯", "🍞", "🥖", "🥨", "🧀", "🥚", "🍳", "🧈", "🥞", "🧇", "🥓", "🥩", "🍗", "🍖", "🦴", "🌭", "🍔", "🍟", "🍕", "🫓", "🥪", "🥙", "🧆", "🌮", "🌯", "🫔", "🥗", "🥘", "🫕", "🥫", "🍝", "🍜", "🍲", "🍛", "🍣", "🍱", "🥟", "🦪", "🍤", "🍙", "🍚", "🍘", "🍥", "🥠", "🥮", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🧁", "🍰", "🎂", "🍮", "🍭", "🍬", "🍫", "🍿", "🍩", "🍪", "🌰", "🥜", "🍯", "🥛", "🍼", "☕️", "🫖", "🍵", "🧃", "🥤", "🧋", "🍶", "🍺", "🍻", "🥂", "🍷", "🥃", "🍸", "🍹", "🧉", "🍾", "🧊", "🥄", "🍴", "🍽", "🥣", "🥡", "🥢", "🧂"] readonly property var activities: ["⚽️", "🏀", "🏈", "⚾️", "🥎", "🎾", "🏐", "🏉", "🥏", "🎱", "🪀", "🏓", "🏸", "🏒", "🏑", "🥍", "🏏", "🪃", "🥅", "⛳️", "🪁", "🏹", "🎣", "🤿", "🥊", "🥋", "🎽", "🛹", "🛼", "🛷", "⛸", "🥌", "🎿", "⛷", "🏂", "🪂", "🏋️‍♀️", "🤼‍♀️", "🤸‍♀️", "⛹️‍♀️", "🤺", "🤾‍♀️", "🏌️‍♀️", "🏇", "🧘‍♀️", "🏄‍♀️", "🏊‍♀️", "🤽‍♀️", "🚣‍♀️", "🧗‍♀️", "🚵‍♀️", "🚴‍♀️", "🏆", "🥇", "🥈", "🥉", "🏅", "🎖", "ros", "🎗", "🎫", "🎟", "🎪", "🤹", "🎭", "🩰", "🎨", "🎬", "🎤", "🎧", "🎼", "🎹", "🥁", "🪘", "🎷", "🎺", "🪗", "🎸", "🪕", "🎻", "🎲", "♟", "🎯", "🎳", "🎮", "🎰", "🧩"] readonly property var travel: ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🦯", "🦽", "🦼", "🛴", "🚲", "🛵", "🏍", "🛺", "🚨", "🚔", "🚍", "🚘", "🚖", "🚡", "🚠", "🚟", "🚃", "🚋", "🚞", "🚝", "🚄", "🚅", "🚈", "🚂", "🚆", "🚇", "🚊", "🚉", "✈️", "🛫", "🛬", "🛩", "💺", "🛰", "🚀", "🛸", "🚁", "🛶", "⛵️", "🚤", "🛥", "🛳", "⛴", "🚢", "⚓️", "🪝", "⛽️", "🚧", "🚦", "🚥", "🚏", "🗺", "🗿", "🗽", "🗼", "🏰", "🏯", "🏟", "🎡", "🎢", "🎠", "⛲️", "⛱", "🏖", "🏝", "🏜", "🌋", "⛰", "🏔", "🗻", "camping", "⛺️", "🏠", "🏡", "🏘", "🏚", "🏗", "🏭", "🏢", "🏬", "🏣", "🏤", "🏥", "🏦", "🏨", "🏪", "🏫", "🏩", "💒", "🏛", "⛪️", "🕌", "🕍", "🛕", "🕋", "⛩", "🛤", "🛣", "🗾", "🎑", "🏞", "🌅", "🌄", "🌠", "🎇", "🎆", "🌇", "🌆", "🏙", "🌃", "🌌", "🌉", "🌁"] readonly property var objects: ["⌚️", "📱", "📲", "💻", "⌨️", "🖥", "🖨", "🖱", "🖲", "🕹", "🗜", "💽", "💾", "💿", "📀", "📼", "📷", "📸", "📹", "🎥", "📽", "🎞", "📞", "☎️", "📟", "📠", "📺", "📻", "🎙", "🎚", "🎛", "🧭", "⏱", "⏲", "⏰", "🕰", "⌛️", "⏳", "📡", "🔋", "🔌", "💡", "🔦", "🕯", "🪔", "🧯", "🛢", "💸", "💵", "💴", "💶", "💷", "🪙", "💰", "💳", "💎", "⚖️", "🪜", "🧰", "🪛", "🔧", "🔨", "⚒", "🛠", "⛏", "🪚", "🔩", "⚙️", "🪤", "🧱", "⛓", "🧲", "🔫", "💣", "🧨", "🪓", "🔪", "🗡", "⚔️", "🛡", "🚬", "⚰️", "🪦", "⚱️", "🏺", "🔮", "📿", "🧿", "💈", "⚗️", "🔭", "🔬", "🕳", "🩹", "🩺", "💊", "💉", "🩸", "🧬", "🦠", "🧫", "🧪", "🌡", "🧹", "🪠", "🧺", "🧻", "🚽", "🚰", "🚿", "🛁", "🛀", "🧼", "🪥", "🪒", "🧽", "🪣", "🧴", "🛎", "🔑", "🗝", "🚪", "🪑", "🛋", "🛏", "🛌", "🧸", "🪆", "🖼", "🪞", "🪟", "🛍", "🛒", "🎁", "🎈", "🎏", "🎀", "🪄", "🪅", "🎊", "🎉", "🎎", "🏮", "🎐", "🧧", "✉️", "📩", "📨", "📧", "💌", "📥", "📤", "📦", "🏷", "🪧", "📪", "📫", "📬", "📭", "📮", "📯", "📜", "📃", "📄", "📑", "🧾", "📊", "📈", "📉", "🗒", "🗓", "📆", "📅", "🗑", "📇", "🗃", "🗳", "🗄", "📋", "📁", "📂", "🗂", "🗞", "📰", "📓", "📔", "📒", "📕", "📗", "📘", "📙", "📚", "📖", "🔖", "🧷", "🔗", "📎", "🖇", "📐", "📏", "🧮", "📌", "📍", "✂️", "🖊", "🖋", "✒️", "🖌", "🖍", "📝", "✏️", "🔍", "🔎", "🔏", "🔐", "🔒", "🔓"] readonly property var symbols: ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟", "☮️", "✝️", "☪️", "🕉", "☸️", "✡️", "🔯", "🕎", "☯️", "☦️", "🛐", "⛎", "♈️", "♉️", "♊️", "♋️", "♌️", "♍️", "♎️", "♏️", "♐️", "♑️", "♒️", "♓️", "🆔", "⚛️", "🉑", "☢️", "☣️", "📴", "📳", "🈶", "🈚️", "🈸", "🈺", "🈷️", "✴️", "🆚", "💮", "🉐", "㊙️", "㊗️", "🈴", "🈵", "🈹", "🈲", "🅰️", "🅱️", "🆎", "🆑", "🅾️", "🆘", "❌", "⭕️", "🛑", "⛔️", "📛", "🚫", "💯", "💢", "♨️", "🚷", "🚯", "🚳", "🚱", "🔞", "📵", "🚭", "❗️", "❕", "❓", "❔", "‼️", "⁉️", "🔅", "🔆", "〽️", "⚠️", "🚸", "🔱", "⚜️", "🔰", "♻️", "✅", "🈯️", "💹", "❇️", "✳️", "❎", "🌐", "💠", "Ⓜ️", "🌀", "💤", "🏧", "🚾", "♿️", "🅿️", "🛗", "🈳", "🈂️", "🛂", "🛃", "🛄", "🛅", "🚹", "🚺", "🚼", "⚧", "🚻", "🚮", "🎦", "📶", "🈁", "🔣", "ℹ️", "🔤", "🔡", "🔠", "🆖", "🆗", "🆙", "🆒", "🆕", "🆓", "0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "🔢", "#️⃣", "*️⃣", "⏏️", "▶️", "⏸", "⏯", "⏹", "⏺", "⏭", "⏮", "⏩", "⏪", "⏫", "⏬", "◀️", "🔼", "🔽", "➡️", "⬅️", "⬆️", "⬇️", "↗️", "↘️", "↙️", "↖️", "↕️", "↔️", "↪️", "↩️", "⤴️", "⤵️", "🔀", "🔁", "🔂", "🔄", "🔃", "🎵", "🎶", "➕", "➖", "➗", "✖️", "♾", "💲", "💱", "™️", "©️", "®️", "👁‍🗨", "🔚", "🔙", "🔛", "🔝", "🔜", "〰️", "➰", "➿", "✔️", "☑️", "🔘", "🔴", "🟠", "🟡", "🟢", "🔵", "🟣", "⚫️", "⚪️", "🟤", "🔺", "🔻", "🔸", "🔹", "🔶", "🔷", "🔳", "🔲", "▪️", "▫️", "◾️", "◽️", "◼️", "◻️", "🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "⬛️", "⬜️", "🟫", "🔈", "🔇", "🔉", "🔊", "🔔", "🔕", "📣", "📢", "💬", "💭", "🗯", "♠️", "♣️", "♥️", "♦️", "🃏", "🎴", "🀄️", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛", "🕜", "🕝", "🕞", "🕟", "🕠", "🕡", "🕢", "🕣", "🕤", "🕥", "🕦", "🕧"] readonly property var flags: ["🏳️", "🏴", "🏁", "🚩", "🏳️‍🌈", "🏳️‍⚧️", "🏴‍☠️", "🇦🇫", "🇦🇽", "🇦🇱", "🇩🇿", "🇦🇸", "🇦🇩", "🇦🇴", "🇦🇮", "🇦🇶", "🇦🇬", "🇦🇷", "🇦🇲", "🇦🇼", "🇦🇺", "🇦🇹", "🇦🇿", "🇧🇸", "🇧🇭", "🇧🇩", "🇧🇧", "🇧🇾", "🇧🇪", "🇧🇿", "🇧🇯", "🇧🇲", "🇧🇹", "🇧🇴", "🇧🇦", "🇧🇼", "🇧🇷", "🇮🇴", "🇻🇬", "🇧🇳", "🇧🇬", "🇧🇫", "🇧🇮", "🇰🇭", "🇨🇲", "🇨🇦", "🇮🇨", "🇨🇻", "🇧🇶", "🇰🇾", "🇨🇫", "🇹🇩", "🇨🇱", "🇨🇳", "🇨🇽", "🇨🇨", "🇨🇴", "🇰🇲", "🇨🇬", "🇨🇩", "🇨🇰", "🇨🇷", "🇨🇮", "🇭🇷", "🇨🇺", "🇨🇼", "🇨🇾", "🇨🇿", "🇩🇰", "🇩🇯", "🇩🇲", "🇩🇴", "🇪🇨", "🇪🇬", "🇸🇻", "🇬🇶", "🇪🇷", "🇪🇪", "🇪🇹", "🇪🇺", "🇫🇰", "🇫🇴", "🇫🇯", "🇫🇮", "🇫🇷", "🇬🇫", "🇵🇫", "🇹🇫", "🇬🇦", "🇬🇲", "🇬🇪", "🇩🇪", "🇬🇭", "🇬🇮", "🇬🇷", "🇬🇱", "🇬🇩", "🇬🇵", "🇬🇺", "🇬🇹", "🇬🇬", "🇬🇳", "🇬🇼", "🇬🇾", "🇭🇹", "🇭🇳", "🇭🇰", "🇭🇺", "🇮🇸", "🇮🇳", "🇮🇩", "🇮🇷", "🇮🇶", "🇮🇪", "🇮🇲", "🇮🇱", "🇮🇹", "🇯🇲", "🇯🇵", "🎌", "🇯🇪", "🇯🇴", "🇰🇿", "🇰🇪", "🇰🇮", "🇽🇰", "🇰🇼", "🇰🇬", "🇱🇦", "🇱🇻", "🇱🇧", "🇱🇸", "🇱🇷", "🇱🇾", "🇱🇮", "🇱🇹", "🇱🇺", "🇲🇴", "🇲🇰", "🇲🇬", "🇲🇼", "🇲🇾", "🇲🇻", "🇲🇱", "🇲🇹", "🇲🇭", "🇲🇶", "🇲🇷", "🇲🇺", "🇾🇹", "🇲🇽", "🇫🇲", "🇲🇩", "🇲🇨", "🇲🇳", "🇲🇪", "🇲🇸", "🇲🇦", "🇲🇿", "🇲🇲", "🇳🇦", "🇳🇷", "🇳🇵", "🇳🇱", "🇳🇨", "🇳🇿", "🇳🇮", "🇳🇪", "🇳🇬", "🇳🇺", "🇳🇫", "🇰🇵", "🇲🇵", "🇳🇴", "🇴🇲", "🇵🇰", "🇵🇼", "🇵🇸", "🇵🇦", "🇵🇬", "🇵🇾", "🇵🇪", "🇵🇭", "🇵🇳", "🇵🇱", "🇵🇹", "🇵🇷", "🇶🇦", "🇷🇪", "🇷🇴", "🇷🇺", "🇷🇼", "🇼🇸", "🇸🇲", "🇸🇦", "🇸🇳", "🇷🇸", "🇸🇨", "🇸🇱", "🇸🇬", "🇸🇽", "🇸🇰", "🇸🇮", "🇬🇸", "🇸🇧", "🇸🇴", "🇿🇦", "🇰🇷", "🇸🇸", "🇪🇸", "🇱🇰", "🇧🇱", "🇸🇭", "🇰🇳", "🇱🇨", "🇵🇲", "🇻🇨", "🇸🇩", "🇸🇷", "🇸🇿", "🇸🇪", "🇨🇭", "🇸🇾", "🇹🇼", "🇹🇯", "🇹🇿", "🇹🇭", "🇹🇱", "🇹🇬", "🇹🇰", "🇹🇴", "🇹🇹", "🇹🇳", "🇹🇷", "🇹🇲", "🇹🇨", "🇹🇻", "🇺🇬", "🇺🇦", "🇦🇪", "🇬🇧", "🇺🇸", "🇺🇾", "🇺🇿", "🇻🇺", "🇻🇦", "🇻🇪", "🇻🇳", "🇼🇫", "🇪🇭", "🇾🇪", "🇿🇲", "🇿🇼"] - // Category State property string currentCategoryId: "smileys" - // Dynamic Model property var displayedEmojis: { if (searchText.length > 0) { - // Filter all categories var all = []; for (var key in categoryMap) { all = all.concat(categoryMap[key]); } - // Simple deduplication var unique = all.filter((item, pos) => { return all.indexOf(item) === pos; }); - // TODO: Map emojis to keywords for proper search return unique; } if (currentCategoryId === "recent") { - // Bind to parent keyboard's recents if available var parentRecents = findParentKeyboard(layout); if (parentRecents) return parentRecents.recentEmojis; - return recentEmojis; // Fallback + return recentEmojis; } return categoryMap[currentCategoryId] || smileys; } - // Category List readonly property var categoryList: [ { "icon": "clock", @@ -103,7 +91,6 @@ Item { } ] - // Signals signal keyClicked(string text) signal backspaceClicked signal enterClicked @@ -130,7 +117,6 @@ Item { return null; } - // Expose Column's implicit height implicitHeight: layoutColumn.implicitHeight Column { @@ -139,7 +125,6 @@ Item { width: parent.width spacing: 0 - // Search Bar Rectangle { width: parent.width height: Math.round(50 * Constants.scaleFactor) @@ -165,8 +150,6 @@ Item { } TextInput { - // Just focused - id: searchInput anchors.left: searchIcon.right @@ -177,10 +160,7 @@ Item { color: "white" font.pixelSize: 16 text: layout.searchText - // Prevent physical keyboard from interfering if possible, - // but we want onTextChanged to update layout.searchText onTextChanged: layout.searchText = text - // When focused, ensure we are in search mode onActiveFocusChanged: { if (activeFocus && layout.searchText === "") {} } @@ -194,7 +174,6 @@ Item { } } - // Clear button Icon { name: "x" size: 16 @@ -215,7 +194,6 @@ Item { } } - // Emoji Grid GridView { id: emojiGrid @@ -239,28 +217,22 @@ Item { } MouseArea { - // Optional: Add to recents logic here if we had access to it - anchors.fill: parent onClicked: { layout.keyClicked(modelData); - HapticService.light(); - // If in search mode, maybe clear search? - // iOS keeps search open. We'll keep it open. + HapticManager.light(); if (layout.searchText.length > 0) {} } } } } - // Bottom Container (Categories OR Keyboard) Item { id: bottomContainer width: parent.width height: layout.searchText.length > 0 ? searchKeyboard.height : categoryRow.height - // Categories & Navigation (Visible when NOT searching) Row { id: categoryRow @@ -269,7 +241,6 @@ Item { visible: layout.searchText.length === 0 spacing: 0 - // ABC Button Key { width: Math.round(60 * Constants.scaleFactor) height: parent.height @@ -278,7 +249,6 @@ Item { onClicked: layout.layoutSwitchClicked("qwerty") } - // Categories Flickable { id: categoryFlickable @@ -293,7 +263,6 @@ Item { id: tabBar height: parent.height - // Fix binding loop by using flickable's width width: Math.max(categoryFlickable.width, categoryList.length * 60 * Constants.scaleFactor) color: "transparent" border.width: 0 @@ -314,7 +283,6 @@ Item { } } - // Backspace Button Key { width: Math.round(60 * Constants.scaleFactor) height: parent.height @@ -324,13 +292,11 @@ Item { } } - // Search Keyboard (Visible when searching) QwertyLayout { id: searchKeyboard width: parent.width visible: layout.searchText.length > 0 - // We need to capture input and direct it to the search field onKeyClicked: text => { layout.searchText += text; } @@ -341,13 +307,12 @@ Item { onSpaceClicked: { layout.searchText += " "; } - // Hide search on Enter or Dismiss onEnterClicked: { - Qt.inputMethod.hide(); // Or just clear search? + Qt.inputMethod.visible = false; } onDismissClicked: { layout.searchText = ""; - Qt.inputMethod.hide(); + Qt.inputMethod.visible = false; } } } diff --git a/shell/qml/keyboard/Layouts/NumberLayout.qml b/shell/qml/keyboard/Layouts/NumberLayout.qml index e8213ebbb..4b6c5365f 100644 --- a/shell/qml/keyboard/Layouts/NumberLayout.qml +++ b/shell/qml/keyboard/Layouts/NumberLayout.qml @@ -20,6 +20,7 @@ Item { spacing: 0 Row { + id: row1 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -29,7 +30,7 @@ Item { model: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] Key { - width: parent.keyWidth + width: row1.keyWidth text: modelData displayText: modelData onClicked: layout.keyClicked(displayText) @@ -44,6 +45,7 @@ Item { } Row { + id: row2 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -53,7 +55,7 @@ Item { model: ["@", "#", "$", "%", "&", "*", "(", ")", "-", "+"] Key { - width: parent.keyWidth + width: row2.keyWidth text: modelData displayText: modelData onClicked: layout.keyClicked(displayText) diff --git a/shell/qml/keyboard/Layouts/PhoneLayout.qml b/shell/qml/keyboard/Layouts/PhoneLayout.qml index 758eb38be..6601dcbcc 100644 --- a/shell/qml/keyboard/Layouts/PhoneLayout.qml +++ b/shell/qml/keyboard/Layouts/PhoneLayout.qml @@ -1,6 +1,4 @@ import "../UI" -// Marathon Virtual Keyboard - Phone Layout -// Phone number pad with common symbols import QtQuick Item { @@ -21,8 +19,8 @@ Item { width: parent.width spacing: 0 - // Row 1: 1 2 3 + - ( ) Row { + id: row1 readonly property real keyWidth: (width - spacing * 6) / 7 width: parent.width @@ -32,7 +30,7 @@ Item { model: ["1", "2", "3", "+", "-", "(", ")"] Key { - width: parent.keyWidth + width: row1.keyWidth text: modelData displayText: modelData onClicked: { @@ -48,7 +46,6 @@ Item { color: "#666666" } - // Row 2: 4 5 6 # * Backspace Row { width: parent.width spacing: Math.round(1 * Constants.scaleFactor) @@ -66,7 +63,6 @@ Item { } } - // Backspace Key { width: Math.round(100 * Constants.scaleFactor) iconName: "delete" @@ -83,7 +79,6 @@ Item { color: "#666666" } - // Row 3: 7 8 9 , . Space Row { width: parent.width spacing: Math.round(1 * Constants.scaleFactor) @@ -101,7 +96,6 @@ Item { } } - // Space bar Key { width: Math.round(100 * Constants.scaleFactor) text: " " @@ -118,12 +112,10 @@ Item { color: "#666666" } - // Row 4: ABC * 0 # Return Row { width: parent.width spacing: Math.round(1 * Constants.scaleFactor) - // ABC key Key { width: Math.round(80 * Constants.scaleFactor) text: "ABC" @@ -147,7 +139,6 @@ Item { } } - // Return key Key { width: Math.round(120 * Constants.scaleFactor) iconName: "corner-down-left" diff --git a/shell/qml/keyboard/Layouts/QwertyLayout.qml b/shell/qml/keyboard/Layouts/QwertyLayout.qml index 9e35109f8..ad9c29e9c 100644 --- a/shell/qml/keyboard/Layouts/QwertyLayout.qml +++ b/shell/qml/keyboard/Layouts/QwertyLayout.qml @@ -136,6 +136,7 @@ Item { spacing: 0 Row { + id: row1 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -145,7 +146,7 @@ Item { model: row1Keys Key { - width: parent.keyWidth + width: row1.keyWidth text: modelData.char displayText: layout.shifted || layout.capsLock ? modelData.char.toUpperCase() : modelData.char alternateChars: modelData.alts @@ -166,6 +167,7 @@ Item { } Row { + id: row2 readonly property real keyWidth: (width - spacing * 8) / 9 width: parent.width @@ -175,7 +177,7 @@ Item { model: row2Keys Key { - width: parent.keyWidth + width: row2.keyWidth text: modelData.char displayText: layout.shifted || layout.capsLock ? modelData.char.toUpperCase() : modelData.char alternateChars: modelData.alts diff --git a/shell/qml/keyboard/Layouts/SymbolLayout.qml b/shell/qml/keyboard/Layouts/SymbolLayout.qml index 12a898996..eb8382a31 100644 --- a/shell/qml/keyboard/Layouts/SymbolLayout.qml +++ b/shell/qml/keyboard/Layouts/SymbolLayout.qml @@ -26,6 +26,7 @@ Item { spacing: 0 Row { + id: row1 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -35,7 +36,7 @@ Item { model: row1Keys Key { - width: parent.keyWidth + width: row1.keyWidth text: modelData displayText: modelData onClicked: layout.keyClicked(displayText) @@ -50,6 +51,7 @@ Item { } Row { + id: row2 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -59,7 +61,7 @@ Item { model: row2Keys Key { - width: parent.keyWidth + width: row2.keyWidth text: modelData displayText: modelData onClicked: layout.keyClicked(displayText) diff --git a/shell/qml/keyboard/Layouts/TerminalLayout.qml b/shell/qml/keyboard/Layouts/TerminalLayout.qml index 7b3dcbe30..4d0690d23 100644 --- a/shell/qml/keyboard/Layouts/TerminalLayout.qml +++ b/shell/qml/keyboard/Layouts/TerminalLayout.qml @@ -136,6 +136,7 @@ Item { spacing: 0 Row { + id: row1 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -145,7 +146,7 @@ Item { model: row1Keys Key { - width: parent.keyWidth + width: row1.keyWidth text: modelData.char displayText: layout.shifted || layout.capsLock ? modelData.char.toUpperCase() : modelData.char alternateChars: modelData.alts @@ -166,6 +167,7 @@ Item { } Row { + id: row2 readonly property real keyWidth: (width - spacing * 8) / 9 width: parent.width @@ -175,7 +177,7 @@ Item { model: row2Keys Key { - width: parent.keyWidth + width: row2.keyWidth text: modelData.char displayText: layout.shifted || layout.capsLock ? modelData.char.toUpperCase() : modelData.char alternateChars: modelData.alts diff --git a/shell/qml/keyboard/Layouts/UrlLayout.qml b/shell/qml/keyboard/Layouts/UrlLayout.qml index 11c09744a..74d62a8e7 100644 --- a/shell/qml/keyboard/Layouts/UrlLayout.qml +++ b/shell/qml/keyboard/Layouts/UrlLayout.qml @@ -1,15 +1,11 @@ import "../UI" -// Marathon Virtual Keyboard - URL Layout -// Optimized for URLs with /, :, and domain shortcuts import QtQuick Item { id: layout - // Properties property bool shifted: false - // Signals signal keyClicked(string text) signal backspaceClicked signal enterClicked @@ -17,7 +13,6 @@ Item { signal layoutSwitchClicked(string layout) signal dismissClicked - // Expose Column's implicit height implicitHeight: layoutColumn.implicitHeight Column { @@ -26,8 +21,8 @@ Item { width: parent.width spacing: 0 - // Row 1: Q W E R T Y U I O P Row { + id: row1 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -37,7 +32,7 @@ Item { model: ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"] Key { - width: parent.keyWidth + width: row1.keyWidth text: modelData displayText: layout.shifted ? modelData.toUpperCase() : modelData onClicked: { @@ -53,8 +48,8 @@ Item { color: "#666666" } - // Row 2: A S D F G H J K L / Row { + id: row2 readonly property real keyWidth: (width - spacing * 9) / 10 width: parent.width @@ -64,7 +59,7 @@ Item { model: ["a", "s", "d", "f", "g", "h", "j", "k", "l", "/"] Key { - width: parent.keyWidth + width: row2.keyWidth text: modelData displayText: (modelData === "/" || !layout.shifted) ? modelData : modelData.toUpperCase() isSpecial: modelData === "/" @@ -81,15 +76,13 @@ Item { color: "#666666" } - // Row 3: Shift Z X C V B N M Backspace Row { width: parent.width spacing: Math.round(1 * Constants.scaleFactor) - // Shift key Key { width: Math.round(60 * Constants.scaleFactor) - iconName: layout.capsLock ? "chevrons-up" : "chevron-up" + iconName: layout.shifted ? "chevrons-up" : "chevron-up" isSpecial: true highlighted: layout.shifted onClicked: { @@ -97,7 +90,6 @@ Item { } } - // Letter keys Repeater { model: ["z", "x", "c", "v", "b", "n", "m"] @@ -111,7 +103,6 @@ Item { } } - // Backspace Key { width: Math.round(60 * Constants.scaleFactor) iconName: "delete" @@ -128,12 +119,10 @@ Item { color: "#666666" } - // Row 4: 123 . : - Space .com www. Return Row { width: parent.width spacing: Math.round(1 * Constants.scaleFactor) - // 123 key Key { width: Math.round(55 * Constants.scaleFactor) text: "123" @@ -144,7 +133,6 @@ Item { } } - // Common URL characters Key { width: Math.round(40 * Constants.scaleFactor) text: "." @@ -163,7 +151,6 @@ Item { displayText: "-" } - // Space bar Key { width: Math.round(120 * Constants.scaleFactor) text: " " @@ -173,7 +160,6 @@ Item { } } - // URL shortcuts Key { width: Math.round(55 * Constants.scaleFactor) text: ".com" @@ -194,7 +180,6 @@ Item { } } - // Return key Key { width: Math.round(60 * Constants.scaleFactor) iconName: "corner-down-left" diff --git a/shell/qml/keyboard/UI/Key.qml b/shell/qml/keyboard/UI/Key.qml index a39450627..1e90353c1 100644 --- a/shell/qml/keyboard/UI/Key.qml +++ b/shell/qml/keyboard/UI/Key.qml @@ -211,7 +211,7 @@ Rectangle { anchors.fill: parent onClicked: { - HapticService.light(); + HapticManager.light(); key.alternateSelected(modelData); key.showingAlternates = false; } @@ -238,7 +238,7 @@ Rectangle { onPressed: function (mouse) { key.pressed = true; longPressTriggered = false; - HapticService.light(); + HapticManager.light(); if (key.alternateChars.length > 0) longPressTimer.restart(); @@ -269,7 +269,7 @@ Rectangle { repeat: false onTriggered: { if (key.alternateChars.length > 0) { - HapticService.medium(); + HapticManager.medium(); key.showingAlternates = true; mouseArea.longPressTriggered = true; key.pressAndHold(); diff --git a/shell/qml/keyboard/UI/LanguageSelector.qml b/shell/qml/keyboard/UI/LanguageSelector.qml index 2e42376b5..0a0673fa4 100644 --- a/shell/qml/keyboard/UI/LanguageSelector.qml +++ b/shell/qml/keyboard/UI/LanguageSelector.qml @@ -1,14 +1,12 @@ -pragma ComponentBehavior: Bound import QtQuick Rectangle { id: languageSelector - visible: false - signal languageSelected(string languageId) signal dismissed + visible: false width: parent.width height: languageSelector.visible ? selectorColumn.implicitHeight + Math.round(20 * Constants.scaleFactor) : 0 color: "#2a2a2a" diff --git a/shell/qml/keyboard/UI/PredictionBar.qml b/shell/qml/keyboard/UI/PredictionBar.qml index b3cd3946a..aff67046a 100644 --- a/shell/qml/keyboard/UI/PredictionBar.qml +++ b/shell/qml/keyboard/UI/PredictionBar.qml @@ -58,7 +58,7 @@ Rectangle { anchors.fill: parent onClicked: { - HapticService.light(); + HapticManager.light(); predictionBar.predictionSelected(modelData); } } diff --git a/shell/qml/keyboard/UI/ToolBar.qml b/shell/qml/keyboard/UI/ToolBar.qml index ec5227333..97c0f170c 100644 --- a/shell/qml/keyboard/UI/ToolBar.qml +++ b/shell/qml/keyboard/UI/ToolBar.qml @@ -40,7 +40,7 @@ Item { width: Math.round(40 * Constants.scaleFactor) height: Math.round(30 * Constants.scaleFactor) - onClicked: HapticService.light() + onClicked: HapticManager.light() background: Rectangle { color: parent.pressed ? "#33ffffff" : "transparent" diff --git a/shell/qml/models/FilteredAppModel.qml b/shell/qml/models/FilteredAppModel.qml index bea2660d3..febf7538e 100644 --- a/shell/qml/models/FilteredAppModel.qml +++ b/shell/qml/models/FilteredAppModel.qml @@ -1,27 +1,17 @@ import QtQuick Item { - // TODO: Implement usage tracking - // For now, fall back to alphabetical - // TODO: Implement install date tracking - // For now, fall back to alphabetical - id: filteredModel - // Expose the filtered list property var filteredApps: [] property int count: filteredApps.length - // React to source model changes property var sourceModel: AppModel - // React to settings changes property var hiddenApps: SettingsManagerCpp.hiddenApps property string sortOrder: SettingsManagerCpp.appSortOrder property bool showNotificationBadges: SettingsManagerCpp.showNotificationBadges - // Note: countChanged() signal is automatically generated for the 'count' property signal dataChanged - // Request a rebuild (debounced) function requestRebuild() { if (debounceTimer.running) debounceTimer.restart(); @@ -29,22 +19,18 @@ Item { debounceTimer.start(); } - // Rebuild the filtered list function rebuildFilteredList() { var apps = []; - // Step 1: Filter out hidden apps for (var i = 0; i < sourceModel.count; i++) { var app = sourceModel.getAppAtIndex(i); if (!app) continue; - // Check if app is hidden if (hiddenApps.indexOf(app.id) >= 0) continue; apps.push(app); } - // Step 2: Sort based on preference if (sortOrder === "alphabetical") apps.sort(function (a, b) { return a.name.localeCompare(b.name); @@ -62,7 +48,6 @@ Item { Logger.info("FilteredAppModel", "Rebuilt: " + filteredApps.length + " apps (filtered from " + sourceModel.count + ")"); } - // Get app at index function getAppAtIndex(index) { if (index < 0 || index >= filteredApps.length) return null; @@ -70,7 +55,6 @@ Item { return filteredApps[index]; } - // Get app by ID function getApp(appId) { for (var i = 0; i < filteredApps.length; i++) { if (filteredApps[i].id === appId) @@ -84,7 +68,6 @@ Item { Logger.info("FilteredAppModel", "Initialized"); } - // Debounce timer to prevent "thundering herd" updates Timer { id: debounceTimer diff --git a/shell/qml/services/AlarmManager.qml b/shell/qml/services/AlarmManager.qml deleted file mode 100644 index c6aa10892..000000000 --- a/shell/qml/services/AlarmManager.qml +++ /dev/null @@ -1,346 +0,0 @@ -pragma Singleton -import QtQuick - -Item { - // Alarm object structure: - // { - // id: string, - // time: "HH:MM", - // enabled: bool, - // label: string, - // repeat: [0,1,2,3,4,5,6], // days of week, 0 = Sunday - // sound: string, - // vibrate: bool, - // snoozeEnabled: bool, - // snoozeDuration: int (minutes) - // } - // Pattern - // Not scheduled for today - - id: alarmManager - - property var alarms: [] // Array of alarm objects - property var activeAlarms: [] // Currently ringing alarms - property bool hasActiveAlarm: activeAlarms.length > 0 - property var checkTimer - - signal alarmTriggered(var alarm) - signal alarmDismissed(string alarmId) - signal alarmSnoozed(string alarmId, int minutes) - signal alarmCreated(var alarm) - signal alarmUpdated(var alarm) - signal alarmDeleted(string alarmId) - - // Test/debug helper: allow triggering an alarm via the same codepath used by the scheduler. - // This keeps the "System Test Suite" app honest (sound, vibration, wake, UI). - function triggerAlarmForTesting(alarm) { - _triggerAlarm(alarm); - } - - function createAlarm(time, label, repeat, options) { - var alarm = { - "id": Qt.md5(Date.now() + time + label), - "time": time, - "enabled": true, - "label": label || "Alarm", - "repeat": repeat || [], - "sound": (options && options.sound) || "default", - "vibrate": (options && options.vibrate) !== false, - "snoozeEnabled": (options && options.snoozeEnabled) !== false, - "snoozeDuration": (options && options.snoozeDuration) || 10 - }; - alarms.push(alarm); - alarmsChanged(); - _saveAlarms(); - _scheduleNextAlarm(); - Logger.info("AlarmManager", "Alarm created: " + alarm.id + " at " + time); - alarmCreated(alarm); - return alarm.id; - } - - function updateAlarm(alarmId, updates) { - for (var i = 0; i < alarms.length; i++) { - if (alarms[i].id === alarmId) { - // Update properties - for (var key in updates) { - alarms[i][key] = updates[key]; - } - alarmsChanged(); - _saveAlarms(); - _scheduleNextAlarm(); - Logger.info("AlarmManager", "Alarm updated: " + alarmId); - alarmUpdated(alarms[i]); - return true; - } - } - return false; - } - - function deleteAlarm(alarmId) { - for (var i = 0; i < alarms.length; i++) { - if (alarms[i].id === alarmId) { - alarms.splice(i, 1); - alarmsChanged(); - _saveAlarms(); - _scheduleNextAlarm(); - Logger.info("AlarmManager", "Alarm deleted: " + alarmId); - alarmDeleted(alarmId); - return true; - } - } - return false; - } - - function enableAlarm(alarmId) { - return updateAlarm(alarmId, { - "enabled": true - }); - } - - function disableAlarm(alarmId) { - return updateAlarm(alarmId, { - "enabled": false - }); - } - - function toggleAlarm(alarmId) { - for (var i = 0; i < alarms.length; i++) { - if (alarms[i].id === alarmId) - return updateAlarm(alarmId, { - "enabled": !alarms[i].enabled - }); - } - return false; - } - - function snoozeAlarm(alarmId) { - for (var i = 0; i < activeAlarms.length; i++) { - if (activeAlarms[i].id === alarmId) { - var alarm = activeAlarms[i]; - var snoozeDuration = alarm.snoozeDuration || 10; - // Remove from active - activeAlarms.splice(i, 1); - activeAlarmsChanged(); - // Stop alarm feedback if nothing else is ringing - if (activeAlarms.length === 0) { - if (typeof AudioManager !== 'undefined') - AudioManager.stopAlarmSound(); - - if (typeof HapticService !== 'undefined') - HapticService.stopVibration(); - - // If the session is already unlocked (grace period), don't strand the user on the lock screen - // after dismissing the alarm UI. - if (typeof SessionStore !== "undefined" && SessionStore && !SessionStore.isLocked && SessionStore.showLockScreen) - SessionStore.unlock(); - } - // Schedule wake for snooze - _scheduleSnooze(alarm, snoozeDuration); - Logger.info("AlarmManager", "Alarm snoozed: " + alarmId + " for " + snoozeDuration + " minutes"); - alarmSnoozed(alarmId, snoozeDuration); - return true; - } - } - return false; - } - - function dismissAlarm(alarmId) { - for (var i = 0; i < activeAlarms.length; i++) { - if (activeAlarms[i].id === alarmId) { - activeAlarms.splice(i, 1); - activeAlarmsChanged(); - // Stop alarm feedback if nothing else is ringing - if (activeAlarms.length === 0) { - if (typeof AudioManager !== 'undefined') - AudioManager.stopAlarmSound(); - - if (typeof HapticService !== 'undefined') - HapticService.stopVibration(); - - // If the session is already unlocked (grace period), don't strand the user on the lock screen - // after dismissing the alarm UI. - if (typeof SessionStore !== "undefined" && SessionStore && !SessionStore.isLocked && SessionStore.showLockScreen) - SessionStore.unlock(); - } - Logger.info("AlarmManager", "Alarm dismissed: " + alarmId); - alarmDismissed(alarmId); - // Re-schedule if repeating - _rescheduleRepeatingAlarm(alarmId); - return true; - } - } - return false; - } - - function getNextAlarmTime() { - var now = new Date(); - var nextTime = null; - var nextAlarm = null; - for (var i = 0; i < alarms.length; i++) { - if (!alarms[i].enabled) - continue; - - var alarmTime = _calculateNextOccurrence(alarms[i], now); - if (alarmTime && (!nextTime || alarmTime < nextTime)) { - nextTime = alarmTime; - nextAlarm = alarms[i]; - } - } - return nextTime; - } - - function _calculateNextOccurrence(alarm, fromDate) { - var parts = alarm.time.split(":"); - var hours = parseInt(parts[0]); - var minutes = parseInt(parts[1]); - var next = new Date(fromDate); - next.setHours(hours, minutes, 0, 0); - // If time has passed today, start from tomorrow - if (next <= fromDate) - next.setDate(next.getDate() + 1); - - // Handle repeating alarms - if (alarm.repeat && alarm.repeat.length > 0) { - // Find next occurrence on a repeat day - var currentDay = next.getDay(); - var daysToAdd = 0; - var found = false; - for (var i = 0; i < 7; i++) { - var checkDay = (currentDay + i) % 7; - if (alarm.repeat.indexOf(checkDay) !== -1) { - daysToAdd = i; - found = true; - break; - } - } - if (found) - next.setDate(next.getDate() + daysToAdd); - else - return null; // No valid repeat day - } - return next; - } - - function _scheduleNextAlarm() { - var nextTime = getNextAlarmTime(); - if (nextTime) { - var now = new Date(); - var msUntil = nextTime - now; - Logger.info("AlarmManager", "Next alarm in " + Math.round(msUntil / 1000 / 60) + " minutes"); - // In production, this would set a system wake alarm via: - // - Linux: /sys/class/rtc/rtc0/wakealarm - // - OR: systemd timer with WakeSystem=true - if (typeof PowerPolicyControllerCpp !== "undefined" && PowerPolicyControllerCpp) { - var epochTime = Math.floor(nextTime.getTime() / 1000); - PowerPolicyControllerCpp.scheduleWakeEpoch(epochTime, "alarm"); - } else if (typeof PowerManagerService !== "undefined" && PowerManagerService) { - var epochTimeFallback = Math.floor(nextTime.getTime() / 1000); - PowerManagerService.setRtcAlarm(epochTimeFallback); - } - checkTimer.interval = Math.min(msUntil, 60000); // Check at least every minute - checkTimer.restart(); - } else { - Logger.info("AlarmManager", "No alarms scheduled"); - checkTimer.stop(); - } - } - - function _scheduleSnooze(alarm, minutes) { - var snoozeTime = new Date(); - snoozeTime.setMinutes(snoozeTime.getMinutes() + minutes); - if (typeof PowerPolicyControllerCpp !== "undefined" && PowerPolicyControllerCpp) { - var epochTime = Math.floor(snoozeTime.getTime() / 1000); - PowerPolicyControllerCpp.scheduleWakeEpoch(epochTime, "alarm_snooze"); - } else if (typeof PowerManagerService !== "undefined" && PowerManagerService) { - var epochTimeFallback = Math.floor(snoozeTime.getTime() / 1000); - PowerManagerService.setRtcAlarm(epochTimeFallback); - } - } - - function _rescheduleRepeatingAlarm(alarmId) { - // If alarm is repeating, it will be caught in next _scheduleNextAlarm() call - _scheduleNextAlarm(); - } - - function _checkAlarms() { - var now = new Date(); - var currentTime = Qt.formatTime(now, "HH:mm"); - for (var i = 0; i < alarms.length; i++) { - var alarm = alarms[i]; - if (!alarm.enabled) - continue; - - // Check if this alarm should trigger now - if (alarm.time === currentTime) { - // Check day of week for repeating alarms - if (alarm.repeat && alarm.repeat.length > 0) { - if (alarm.repeat.indexOf(now.getDay()) === -1) - continue; - } - // Trigger alarm - _triggerAlarm(alarm); - } - } - } - - function _triggerAlarm(alarm) { - Logger.info("AlarmManager", "ALARM TRIGGERED: " + alarm.label); - // Add to active alarms - activeAlarms.push(alarm); - activeAlarmsChanged(); - // Wake the system - if (typeof PowerPolicyControllerCpp !== "undefined" && PowerPolicyControllerCpp) - PowerPolicyControllerCpp.wake("alarm"); - else if (typeof PowerManagerService !== "undefined" && PowerManagerService) - PowerManagerService.acquireWakelock("alarm"); - // Turn on screen - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) - DisplayPolicyControllerCpp.turnScreenOn(); - else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.setScreenState(true); - // Play alarm sound - if (alarm.sound && typeof AudioManager !== 'undefined') - AudioManager.playAlarmSound(); - - // Vibrate - if (alarm.vibrate && typeof HapticService !== 'undefined') - HapticService.pattern([500, 200, 500, 200, 500]); - - // Show alarm UI - alarmTriggered(alarm); - } - - function _saveAlarms() { - var data = JSON.stringify(alarms); - SettingsManagerCpp.set("alarms/data", data); - } - - function _loadAlarms() { - var data = SettingsManagerCpp.get("alarms/data", "[]"); - try { - alarms = JSON.parse(data); - Logger.info("AlarmManager", "Loaded " + alarms.length + " alarms"); - } catch (e) { - Logger.error("AlarmManager", "Failed to load alarms: " + e); - alarms = []; - } - } - - Component.onCompleted: { - Logger.info("AlarmManager", "Initialized"); - _loadAlarms(); - _scheduleNextAlarm(); - } - - checkTimer: Timer { - id: checkTimer - - repeat: true - running: true - interval: 60000 - onTriggered: { - _checkAlarms(); - _scheduleNextAlarm(); - } - } -} diff --git a/shell/qml/services/AmbientLightSensor.qml b/shell/qml/services/AmbientLightSensor.qml deleted file mode 100644 index d30db0b50..000000000 --- a/shell/qml/services/AmbientLightSensor.qml +++ /dev/null @@ -1,114 +0,0 @@ -pragma Singleton -import QtQuick - -Item { - // Very dark - // Dark room - // Dim room - // Office - // Bright room - // Sunlight indirect - // Direct sunlight - // Default - - id: ambientLightSensor - - property bool available: SensorManagerCpp.available - property bool active: true - property real lightLevel: SensorManagerCpp.ambientLight - property real minLux: 0 - property real maxLux: 10000 - property bool autoBrightnessEnabled: false - // Brightness mapping - property var brightnessMap: [ - { - "maxLux": 10, - "brightness": 0.1 - }, - { - "maxLux": 50, - "brightness": 0.2 - }, - { - "maxLux": 100, - "brightness": 0.3 - }, - { - "maxLux": 300, - "brightness": 0.5 - }, - { - "maxLux": 1000, - "brightness": 0.7 - }, - { - "maxLux": 5000, - "brightness": 0.85 - }, - { - "maxLux": 999999, - "brightness": 1 - } - ] - - signal brightnessAdjusted(real value) - - function enableAutoBrightness() { - autoBrightnessEnabled = true; - Logger.info("AmbientLightSensor", "Auto brightness enabled"); - _adjustBrightness(lightLevel); - } - - function disableAutoBrightness() { - autoBrightnessEnabled = false; - Logger.info("AmbientLightSensor", "Auto brightness disabled"); - } - - function _adjustBrightness(lux) { - // Map lux to brightness using lookup table - var brightness = 0.5; - for (var i = 0; i < brightnessMap.length; i++) { - if (lux <= brightnessMap[i].maxLux) { - brightness = brightnessMap[i].brightness; - break; - } - } - // Smooth brightness changes - apply a simple moving average - var currentBrightness = (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? DisplayManagerCpp.brightness : 0.5; - var smoothed = currentBrightness * 0.7 + brightness * 0.3; - Logger.debug("AmbientLightSensor", "Auto-brightness: " + Math.round(smoothed * 100) + "% (lux: " + Math.round(lux) + ")"); - if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.brightness = smoothed; - - brightnessAdjusted(smoothed); - } - - Component.onCompleted: { - Logger.info("AmbientLightSensor", "Initialized"); - // Start if auto-brightness already enabled - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp && DisplayPolicyControllerCpp.autoBrightnessEnabled) - enableAutoBrightness(); - } - - Connections { - function onAmbientLightChanged() { - var lux = SensorManagerCpp.ambientLight; - lightLevel = Math.min(maxLux, Math.max(minLux, lux)); - _adjustBrightness(lightLevel); - } - - target: SensorManagerCpp - } - - // Sync with display policy auto-brightness setting - Connections { - function onAutoBrightnessEnabledChanged() { - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp && DisplayPolicyControllerCpp.autoBrightnessEnabled) - enableAutoBrightness(); - else - disableAutoBrightness(); - } - - target: typeof DisplayPolicyControllerCpp !== "undefined" ? DisplayPolicyControllerCpp : null - } -} diff --git a/shell/qml/services/AudioManager.qml b/shell/qml/services/AudioManager.qml deleted file mode 100644 index 316d22ebc..000000000 --- a/shell/qml/services/AudioManager.qml +++ /dev/null @@ -1,117 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: audioManager - - // Runtime master volume comes from C++ backend (wpctl/pulseaudio). - property real volume: typeof AudioManagerCpp !== 'undefined' ? AudioManagerCpp.volume : SettingsManagerCpp.systemVolume - property bool muted: typeof AudioManagerCpp !== 'undefined' ? AudioManagerCpp.muted : false - // Persisted policy lives in SettingsManager (C++), applied by AudioPolicyController (C++). - property bool vibrationEnabled: SettingsManagerCpp.vibrationEnabled - property bool dndEnabled: SettingsManagerCpp.dndEnabled - property string audioProfile: SettingsManagerCpp.audioProfile - readonly property var availableProfiles: ["silent", "vibrate", "normal", "loud"] - // Sound file properties - property string currentRingtone: SettingsManagerCpp.ringtone - property string currentNotificationSound: SettingsManagerCpp.notificationSound - property string currentAlarmSound: SettingsManagerCpp.alarmSound - // Available sounds (computed once) - readonly property var availableRingtones: SettingsManagerCpp.availableRingtones() - readonly property var availableNotificationSounds: SettingsManagerCpp.availableNotificationSounds() - readonly property var availableAlarmSounds: SettingsManagerCpp.availableAlarmSounds() - // Friendly names for UI display - readonly property string currentRingtoneName: SettingsManagerCpp.formatSoundName(currentRingtone) - readonly property string currentNotificationSoundName: SettingsManagerCpp.formatSoundName(currentNotificationSound) - readonly property string currentAlarmSoundName: SettingsManagerCpp.formatSoundName(currentAlarmSound) - - function setRingtone(path) { - SettingsManagerCpp.ringtone = path; - } - - function setNotificationSound(path) { - SettingsManagerCpp.notificationSound = path; - } - - function setAlarmSound(path) { - SettingsManagerCpp.alarmSound = path; - } - - function setVolume(value) { - var clamped = Math.max(0, Math.min(1, value)); - // Prefer direct AudioManagerCpp control (runner uses a DBus-backed proxy under strict isolation). - if (typeof AudioManagerCpp !== 'undefined') - AudioManagerCpp.setVolume(clamped); - else if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.setMasterVolume(clamped); - } - - function setMuted(mute) { - if (typeof AudioManagerCpp !== 'undefined') - AudioManagerCpp.setMuted(mute); - else if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.setMuted(mute); - } - - function toggleMute() { - setMuted(!muted); - } - - function setAudioProfile(profile) { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.setAudioProfile(profile); - } - - function setDoNotDisturb(enabled) { - // DND is persisted in SettingsManager; policy controller is optional. - SettingsManagerCpp.dndEnabled = enabled; - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.setDoNotDisturb(enabled); - } - - function playRingtone() { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.playRingtone(); - } - - function stopRingtone() { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.stopRingtone(); - } - - function playNotificationSound() { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.playNotificationSound(); - } - - function playAlarmSound() { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.playAlarmSound(); - } - - function stopAlarmSound() { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.stopAlarmSound(); - } - - // Preview functions for settings app - use dedicated preview player - function previewRingtone(soundPath) { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.previewSound(soundPath); - } - - function previewNotificationSound(soundPath) { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.previewSound(soundPath); - } - - function previewAlarmSound(soundPath) { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.previewSound(soundPath); - } - - function vibrate(pattern) { - if (typeof AudioPolicyControllerCpp !== 'undefined') - AudioPolicyControllerCpp.vibratePattern(pattern); - } -} diff --git a/shell/qml/services/CalendarManager.qml b/shell/qml/services/CalendarManager.qml deleted file mode 100644 index 8d9e1b919..000000000 --- a/shell/qml/services/CalendarManager.qml +++ /dev/null @@ -1,255 +0,0 @@ -pragma Singleton -import QtQuick -import QtQuick.LocalStorage 2.0 - -Item { - // NOTE: Current implementation loads all events into memory. - // This is suitable for a prototype but may need optimization (e.g. SQLite) - // for production use with thousands of events. - // No-op: SQLite saves immediately - - id: calendarManager - - property var events: [] - property int nextEventId: 1 - // Track triggered events to avoid duplicate notifications - property var triggeredEvents: [] - property var db: null - - signal eventCreated(var event) - signal eventUpdated(var event) - signal eventDeleted(int eventId) - signal eventsLoaded - - function initDatabase() { - db = LocalStorage.openDatabaseSync("CalendarDB", "1.0", "Calendar Events", 100000); - db.transaction(function (tx) { - tx.executeSql(`CREATE TABLE IF NOT EXISTS events( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT, - date TEXT, - time TEXT, - allDay INTEGER, - recurring TEXT, - description TEXT - )`); - }); - } - - function init() { - initDatabase(); - _loadEvents(); - _scheduleNextCheck(); - } - - function createEvent(event) { - var resId = -1; - db.transaction(function (tx) { - var rs = tx.executeSql(`INSERT INTO events (title, date, time, allDay, recurring, description) VALUES (?, ?, ?, ?, ?, ?)`, [event.title, event.date, event.time, event.allDay ? 1 : 0, event.recurring || "none", event.description || ""]); - resId = rs.insertId; - }); - if (resId !== -1) { - event.id = resId; - // Add to local model for immediate feedback - events.push(event); - Logger.info("CalendarManager", "Event created: " + event.title + " (ID: " + resId + ")"); - eventCreated(event); - return event; - } - return null; - } - - function updateEvent(event) { - var success = false; - db.transaction(function (tx) { - var rs = tx.executeSql(`UPDATE events SET title=?, date=?, time=?, allDay=?, recurring=?, description=? WHERE id=?`, [event.title, event.date, event.time, event.allDay ? 1 : 0, event.recurring || "none", event.description || "", event.id]); - if (rs.rowsAffected > 0) - success = true; - }); - if (success) { - // Update local model - for (var i = 0; i < events.length; i++) { - if (events[i].id === event.id) { - events[i] = event; - break; - } - } - Logger.info("CalendarManager", "Event updated: " + event.title); - eventUpdated(event); - return true; - } - return false; - } - - function deleteEvent(id) { - var success = false; - db.transaction(function (tx) { - var rs = tx.executeSql(`DELETE FROM events WHERE id=?`, [id]); - if (rs.rowsAffected > 0) - success = true; - }); - if (success) { - // Update local model - for (var i = 0; i < events.length; i++) { - if (events[i].id === id) { - events.splice(i, 1); - break; - } - } - Logger.info("CalendarManager", "Event deleted: " + id); - eventDeleted(id); - return true; - } - return false; - } - - function getEventsForDate(date) { - var dateStr = Qt.formatDate(date, "yyyy-MM-dd"); - var result = []; - for (var i = 0; i < events.length; i++) { - var event = events[i]; - if (event.date === dateStr) { - result.push(event); - } else if (event.recurring && event.recurring !== "none") { - var eventDate = new Date(event.date); - var checkDate = new Date(date); - if (event.recurring === "daily" && checkDate >= eventDate) { - result.push(event); - } else if (event.recurring === "weekly" && checkDate >= eventDate) { - var daysDiff = Math.floor((checkDate - eventDate) / (1000 * 60 * 60 * 24)); - if (daysDiff % 7 === 0) - result.push(event); - } else if (event.recurring === "monthly" && checkDate >= eventDate) { - if (checkDate.getDate() === eventDate.getDate()) - result.push(event); - } - } - } - return result; - } - - function _loadEvents() { - var rawEvents = []; - db.transaction(function (tx) { - var rs = tx.executeSql(`SELECT * FROM events`); - for (var i = 0; i < rs.rows.length; i++) { - var row = rs.rows.item(i); - // Convert SQLite types to JS types where needed - rawEvents.push({ - "id": row.id, - "title": row.title, - "date": row.date, - "time": row.time, - "allDay": row.allDay === 1, - "recurring": row.recurring, - "description": row.description - }); - } - }); - // Send to worker for any heavy processing (sorting, expansion) - // Even if we don't need parsing, the worker can handle sorting/filtering - calendarWorker.sendMessage({ - "action": 'processEvents', - "events": rawEvents - }); - } - - function _saveEvents() { - } - - function _checkReminders() { - var now = new Date(); - var currentDateStr = Qt.formatDate(now, "yyyy-MM-dd"); - var currentTimeStr = Qt.formatTime(now, "HH:mm"); - for (var i = 0; i < events.length; i++) { - var event = events[i]; - // Skip if already triggered recently (simple debounce) - // In a real app, we'd track this more robustly - var triggerId = event.id + "_" + currentDateStr + "_" + currentTimeStr; - if (triggeredEvents.indexOf(triggerId) !== -1) - continue; - - var shouldTrigger = false; - // Check date and time - if (event.date === currentDateStr && event.time === currentTimeStr) { - shouldTrigger = true; - } else if (event.recurring && event.recurring !== "none") { - // Check recurrence logic - var eventDate = new Date(event.date); - if (event.time === currentTimeStr) { - if (event.recurring === "daily" && now >= eventDate) { - shouldTrigger = true; - } else if (event.recurring === "weekly" && now >= eventDate) { - var daysDiff = Math.floor((now - eventDate) / (1000 * 60 * 60 * 24)); - if (daysDiff % 7 === 0) - shouldTrigger = true; - } else if (event.recurring === "monthly" && now >= eventDate) { - if (now.getDate() === eventDate.getDate()) - shouldTrigger = true; - } - } - } - if (shouldTrigger) { - _triggerNotification(event); - triggeredEvents.push(triggerId); - // Cleanup old triggered events (keep last 50) - if (triggeredEvents.length > 50) - triggeredEvents.shift(); - } - } - } - - function _triggerNotification(event) { - Logger.info("CalendarManager", "Triggering notification for: " + event.title); - NotificationService.sendNotification("calendar", event.title, event.time + (event.allDay ? " (All Day)" : ""), { - "icon": "qrc:/images/calendar.svg", - "category": "reminder", - "priority": "high", - "actions": ["Dismiss"] - }); - } - - function _scheduleNextCheck() { - // Align to next minute - var now = new Date(); - var seconds = now.getSeconds(); - var msToNextMinute = (60 - seconds) * 1000; - checkTimer.interval = msToNextMinute; - checkTimer.restart(); - } - - Component.onCompleted: { - Logger.info("CalendarManager", "Initialized"); - init(); - } - - WorkerScript { - id: calendarWorker - - source: "CalendarWorker.js" - onMessage: function (message) { - if (message.action === 'eventsParsed') { - calendarManager.events = message.events; - Logger.info("CalendarManager", "Loaded " + calendarManager.events.length + " events from DB"); - eventsLoaded(); - } else if (message.action === 'error') { - Logger.error("CalendarManager", "Worker failed: " + message.error); - calendarManager.events = []; - } - } - } - - Timer { - id: checkTimer - - repeat: true - running: true - interval: 60000 // Initial interval, adjusted by _scheduleNextCheck - onTriggered: { - _checkReminders(); - // Reset to 1 minute interval if we were aligned - if (interval !== 60000) - interval = 60000; - } - } -} diff --git a/shell/qml/services/ClipboardService.qml b/shell/qml/services/ClipboardService.qml deleted file mode 100644 index 7ade3c84b..000000000 --- a/shell/qml/services/ClipboardService.qml +++ /dev/null @@ -1,71 +0,0 @@ -pragma Singleton -import QtCore -import QtQuick - -QtObject { - id: clipboardService - - property var clipboardHistory: [] - readonly property int maxHistorySize: 20 - property var settings - - signal clipboardChanged(var item) - signal historyCleared - - function addToHistory(text, type) { - if (!text || text.length === 0) - return; - - var item = { - "text": text, - "type": type || "text", - "timestamp": Date.now() - }; - for (var i = 0; i < clipboardHistory.length; i++) { - if (clipboardHistory[i].text === text) { - clipboardHistory.splice(i, 1); - break; - } - } - clipboardHistory.unshift(item); - if (clipboardHistory.length > maxHistorySize) - clipboardHistory = clipboardHistory.slice(0, maxHistorySize); - - clipboardHistory = clipboardHistory; - clipboardChanged(item); - console.log("[ClipboardService] Added to history, total items:", clipboardHistory.length); - } - - function copyToClipboard(text) { - console.log("[ClipboardService] Copy to clipboard:", text); - addToHistory(text, "text"); - } - - function getHistory() { - return clipboardHistory; - } - - function deleteItem(index) { - if (index >= 0 && index < clipboardHistory.length) { - clipboardHistory.splice(index, 1); - clipboardHistory = clipboardHistory; - console.log("[ClipboardService] Deleted item at index:", index); - } - } - - function clearHistory() { - clipboardHistory = []; - historyCleared(); - console.log("[ClipboardService] History cleared"); - } - - Component.onCompleted: { - console.log("[ClipboardService] Initialized with", clipboardHistory.length, "items"); - } - - settings: Settings { - property alias history: clipboardService.clipboardHistory - - category: "Clipboard" - } -} diff --git a/shell/qml/services/HapticService.qml b/shell/qml/services/HapticService.qml deleted file mode 100644 index a883e2011..000000000 --- a/shell/qml/services/HapticService.qml +++ /dev/null @@ -1,142 +0,0 @@ -pragma Singleton -import QtQuick - -/** - * @singleton - * @brief Provides haptic feedback (vibration) for user interactions - * - * HapticService provides standardized haptic feedback patterns across - * different platforms (Linux, Android). Automatically disabled on platforms - * without haptic support (macOS, desktop). - * - * @example - * // Provide light feedback for button taps - * Button { - * onClicked: HapticService.light() - * } - * - * @example - * // Heavy feedback for critical actions - * Button { - * text: "Delete" - * onClicked: { - * HapticService.heavy() - * deleteItem() - * } - * } - */ -QtObject { - // no-op - - id: hapticService - - /** - * @brief Whether haptic feedback is available on this platform - * @type {bool} - * @readonly - */ - readonly property bool isAvailable: (typeof HapticManager !== "undefined" && HapticManager) ? HapticManager.available : false - /** - * @brief Whether haptic feedback is enabled (user preference) - * @type {bool} - * @default true - */ - property bool enabled: (typeof HapticManager !== "undefined" && HapticManager) ? HapticManager.enabled : false - property Binding enabledBinding - - /** - * @brief Provides light haptic feedback (10ms) - * - * Use for subtle interactions like button taps, switches, selections. - */ - function light() { - if (!enabled || !isAvailable) - return; - - vibrate(10); - } - - /** - * @brief Provides medium haptic feedback (25ms) - * - * Use for moderate actions like navigation, panel opens, significant state changes. - */ - function medium() { - if (!enabled || !isAvailable) - return; - - vibrate(25); - } - - /** - * @brief Provides heavy haptic feedback (50ms) - * - * Use for important actions like deletions, errors, warnings, or critical confirmations. - */ - function heavy() { - if (!enabled || !isAvailable) - return; - - vibrate(50); - } - - /** - * @brief Provides custom haptic pattern with specific durations - * - * @param {Array} durations - Array of vibration durations in milliseconds - * - * @example - * // Two quick pulses - * HapticService.pattern([50, 100, 50]) - */ - function pattern(durations) { - if (!enabled || !isAvailable) - return; - - if (typeof HapticManager !== "undefined" && HapticManager) - HapticManager.vibratePatternVariant(durations); - } - - /** - * @brief Vibrate with a repeating pattern - * @param {Array} pattern - [vibrate_ms, pause_ms] - * @param {int} repeat - Number of repetitions (-1 for infinite) - */ - function vibratePattern(durations, repeat) { - if (!enabled || !isAvailable) - return; - - // repeat is currently ignored; the backend supports a single finite pattern. - if (typeof HapticManager !== "undefined" && HapticManager) - HapticManager.vibratePatternVariant(durations); - } - - /** - * @brief Stop any ongoing vibration - */ - function stopVibration() { - if (!isAvailable) - return; - - if (typeof HapticManager !== "undefined" && HapticManager) - HapticManager.cancelVibration(); - } - - function vibrate(duration) { - if (!enabled || !isAvailable) - return; - - if (typeof HapticManager !== "undefined" && HapticManager) - HapticManager.vibrate(duration); - } - - Component.onCompleted: {} - - enabledBinding: Binding { - target: typeof HapticManager !== "undefined" ? HapticManager : null - property: "enabled" - value: hapticService.enabled - when: typeof HapticManager !== "undefined" && HapticManager - restoreMode: Binding.RestoreBinding - } -} diff --git a/shell/qml/services/LocationService.qml b/shell/qml/services/LocationService.qml deleted file mode 100644 index c40d05617..000000000 --- a/shell/qml/services/LocationService.qml +++ /dev/null @@ -1,200 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: locationService - - property bool locationEnabled: false - property bool hasPermission: false - property string accuracy: "high" - property real latitude: 0 - property real longitude: 0 - property real altitude: 0 - property real accuracy_meters: 0 - property real heading: 0 - property real speed: 0 - property string lastUpdate: "" - property bool isUpdating: false - // Listen for permission responses - property var _permissionConnections - property Timer locationUpdateTimer - - signal locationChanged(real lat, real lon, real accuracy) - signal locationError(string error) - signal permissionChanged(bool granted) - - function enable() { - enableLocation(); - } - - function enableLocation() { - console.log("[LocationService] Enabling location services..."); - locationEnabled = true; - _platformEnableLocation(); - startUpdating(); - } - - function disableLocation() { - console.log("[LocationService] Disabling location services..."); - locationEnabled = false; - stopUpdating(); - _platformDisableLocation(); - } - - function requestPermission() { - console.log("[LocationService] Requesting location permission..."); - _platformRequestPermission(); - } - - function startUpdating() { - if (!locationEnabled) { - console.warn("[LocationService] Location not enabled"); - return; - } - if (!hasPermission) { - console.warn("[LocationService] No location permission"); - requestPermission(); - return; - } - console.log("[LocationService] Starting location updates..."); - isUpdating = true; - locationUpdateTimer.start(); - _platformStartUpdating(); - } - - function stopUpdating() { - console.log("[LocationService] Stopping location updates..."); - isUpdating = false; - locationUpdateTimer.stop(); - _platformStopUpdating(); - } - - function setAccuracy(accuracyLevel) { - if (["low", "medium", "high"].indexOf(accuracyLevel) === -1) { - console.warn("[LocationService] Invalid accuracy level:", accuracyLevel); - return; - } - console.log("[LocationService] Setting accuracy:", accuracyLevel); - accuracy = accuracyLevel; - _platformSetAccuracy(accuracyLevel); - } - - function getCurrentPosition() { - return { - "latitude": latitude, - "longitude": longitude, - "altitude": altitude, - "accuracy": accuracy_meters, - "heading": heading, - "speed": speed, - "timestamp": lastUpdate - }; - } - - function _platformEnableLocation() { - if (Platform.isLinux) - console.log("[LocationService] Enabling via GeoClue D-Bus"); - else if (Platform.isMacOS) - console.log("[LocationService] macOS CoreLocation"); - } - - function _platformDisableLocation() { - if (Platform.isLinux) - console.log("[LocationService] Disabling GeoClue"); - } - - function _platformRequestPermission() { - if (Platform.isLinux || Platform.isMacOS) { - console.log("[LocationService] Requesting location permission via PermissionManager"); - // Request permission via system permission manager - // The appId is "shell" since LocationService is a system service - if (typeof PermissionManager !== 'undefined') { - PermissionManager.requestPermission("shell", "location"); - } else { - console.warn("[LocationService] PermissionManager not available, auto-granting"); - hasPermission = true; - permissionChanged(true); - } - } - } - - function _platformStartUpdating() { - if (Platform.isLinux) - console.log("[LocationService] D-Bus call to GeoClue Start"); - } - - function _platformStopUpdating() { - if (Platform.isLinux) - console.log("[LocationService] D-Bus call to GeoClue Stop"); - } - - function _platformSetAccuracy(accuracyLevel) { - var level = 0; - switch (accuracyLevel) { - case "low": - level = 4; - break; - case "medium": - level = 6; - break; - case "high": - level = 8; - break; - } - if (Platform.isLinux) - console.log("[LocationService] GeoClue accuracy level:", level); - } - - function _simulateLocationUpdate() { - latitude += (Math.random() - 0.5) * 0.001; - longitude += (Math.random() - 0.5) * 0.001; - altitude = 100 + Math.random() * 50; - accuracy_meters = 10 + Math.random() * 20; - heading = Math.random() * 360; - speed = Math.random() * 10; - lastUpdate = new Date().toISOString(); - locationChanged(latitude, longitude, accuracy_meters); - } - - Component.onCompleted: { - console.log("[LocationService] Initialized"); - console.log("[LocationService] GeoClue available:", Platform.isLinux); - latitude = 37.7749; - longitude = -122.419; - } - - _permissionConnections: Connections { - function onPermissionGranted(appId, permission) { - if (appId === "shell" && permission === "location") { - console.log("[LocationService] Location permission granted"); - hasPermission = true; - permissionChanged(true); - // Auto-start location updates if enabled - if (locationEnabled) - startUpdating(); - } - } - - function onPermissionDenied(appId, permission) { - if (appId === "shell" && permission === "location") { - console.log("[LocationService] Location permission denied"); - hasPermission = false; - permissionChanged(false); - locationEnabled = false; - } - } - - target: typeof PermissionManager !== 'undefined' ? PermissionManager : null - } - - locationUpdateTimer: Timer { - interval: 5000 - repeat: true - running: false - onTriggered: { - if (Platform.isLinux || Platform.isMacOS) {} else { - _simulateLocationUpdate(); - } - } - } -} diff --git a/shell/qml/services/NavigationRouter.qml b/shell/qml/services/NavigationRouter.qml deleted file mode 100644 index 695299ea8..000000000 --- a/shell/qml/services/NavigationRouter.qml +++ /dev/null @@ -1,240 +0,0 @@ -pragma Singleton -import QtQuick - -/** - * @singleton - * @brief Handles Marathon URI scheme navigation and deep linking - * - * NavigationRouter provides a centralized system for navigating between - * apps and pages using Marathon URIs (marathon://app/page?params). - * Supports deep linking, navigation history, and app launching. - * - * URI Format: `marathon://appId/page?param1=value1¶m2=value2` - * - * @example - * // Navigate to settings WiFi page - * NavigationRouter.navigate("marathon://settings/wifi") - * - * @example - * // Open browser with URL - * NavigationRouter.navigate("marathon://browser/view?url=https://example.com") - * - * @example - * // Quick launch shortcut - * NavigationRouter.quickLaunch("camera") - */ -QtObject { - // Generic app launch (includes Settings now) - // Navigate to messages tab - // Launch the app first using UIStore - - id: router - - /** - * @brief Currently active app ID - * @type {string} - */ - property string currentApp: "" - /** - * @brief Currently active page within the app - * @type {string} - */ - property string currentPage: "" - /** - * @brief Current navigation parameters - * @type {Object} - */ - property var currentParams: ({}) - /** - * @brief Navigation history stack - * @type {Array} - */ - property var history: [] - - /** - * @brief Emitted when navigation succeeds - * @param {string} uri - The URI that was navigated to - */ - signal navigated(string uri) - /** - * @brief Emitted when navigation fails - * @param {string} uri - The URI that failed - * @param {string} error - Error message - */ - signal navigationFailed(string uri, string error) - // Signals for specific navigation events - signal settingsNavigationRequested(string page, string subpage, var params) - signal hubTabRequested(int tabIndex) - signal deepLinkRequested(string appId, string route, var params) - - /** - * @brief Navigates to a Marathon URI - * - * @param {string} uri - Full Marathon URI (marathon://app/page?params) - * @returns {bool} Success status - * - * @example - * NavigationRouter.navigate("marathon://settings/display") - */ - function navigate(uri) { - Logger.info("NavigationRouter", "Navigate to: " + uri); - var parsed = parseURI(uri); - if (!parsed.valid) { - var error = "Invalid URI format: " + uri; - Logger.error("NavigationRouter", error); - navigationFailed(uri, error); - return false; - } - // Add to history - history.push({ - "uri": uri, - "app": parsed.app, - "page": parsed.page, - "params": parsed.params, - "timestamp": Date.now() - }); - // Update current state - currentApp = parsed.app; - currentPage = parsed.page; - currentParams = parsed.params; - // Route to appropriate handler - if (parsed.app === "hub") - return handleHubRoute(parsed); - else if (parsed.app === "browser") - return handleBrowserRoute(parsed); - else - return handleAppRoute(parsed); - } - - /** - * Go back in navigation history - */ - function goBack() { - if (history.length > 1) { - history.pop(); // Remove current - var previous = history[history.length - 1]; - navigate(previous.uri); - return true; - } - return false; - } - - /** - * Clear navigation history - */ - function clearHistory() { - history = []; - currentApp = ""; - currentPage = ""; - currentParams = {}; - Logger.info("NavigationRouter", "Navigation history cleared"); - } - - /** - * Parse Marathon URI into components - * @param uri {string} - Marathon URI - * @returns {object} - Parsed components - */ - function parseURI(uri) { - var result = { - "valid": false, - "app": "", - "page": "", - "subpage": "", - "params": {} - }; - // Check for marathon:// scheme - if (!uri.startsWith("marathon://")) - return result; - - // Remove scheme - var path = uri.substring("marathon://".length); - // Split query params - var pathAndQuery = path.split("?"); - var pathParts = pathAndQuery[0].split("/"); - // Parse path - result.app = pathParts[0] || ""; - result.page = pathParts[1] || ""; - result.subpage = pathParts[2] || ""; - // Parse query params - if (pathAndQuery.length > 1) { - var queryString = pathAndQuery[1]; - var queryParts = queryString.split("&"); - for (var i = 0; i < queryParts.length; i++) { - var pair = queryParts[i].split("="); - if (pair.length === 2) - result.params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); - } - } - result.valid = result.app !== ""; - return result; - } - - /** - * Handle Hub routing - */ - function handleHubRoute(parsed) { - Logger.info("NavigationRouter", "Routing to Hub: " + parsed.page); - // Open Hub and select tab - if (parsed.page === "messages") - hubTabRequested(0); - else if (parsed.page === "notifications") - hubTabRequested(1); - else if (parsed.page === "calendar") - hubTabRequested(2); - navigated("marathon://hub/" + parsed.page); - return true; - } - - /** - * Handle Browser routing - */ - function handleBrowserRoute(parsed) { - Logger.info("NavigationRouter", "Routing to Browser"); - var url = parsed.params.url || ""; - // Launch browser with URL parameter - deepLinkRequested("browser", "", { - "url": url - }); - navigated("marathon://browser"); - return true; - } - - /** - * Handle generic app routing - */ - function handleAppRoute(parsed) { - Logger.info("NavigationRouter", "Routing to app: " + parsed.app); - // Request app launch via deep link signal - deepLinkRequested(parsed.app, "", {}); - navigated("marathon://" + parsed.app); - return true; - } - - function navigateToDeepLink(appId, route, params) { - Logger.info("NavigationRouter", "Deep link requested: " + appId + " → " + route); - // Get app info - var appInfo = typeof MarathonAppRegistry !== 'undefined' ? MarathonAppRegistry.getApp(appId) : null; - if (!appInfo) { - Logger.error("NavigationRouter", "App not found for deep link: " + appId); - return false; - } - // Check if THIS specific app is already open - var isAppOpen = (UIStore.appWindowOpen && UIStore.currentAppId === appId); - if (!isAppOpen) - UIStore.openApp(appId, appInfo.name, appInfo.icon); - - // Emit deep link signal for app to handle (after a small delay if we just opened it) - if (isAppOpen) - deepLinkRequested(appId, route, params || {}); - else - Qt.callLater(function () { - deepLinkRequested(appId, route, params || {}); - }); - return true; - } - - Component.onCompleted: { - Logger.info("NavigationRouter", "Initialized"); - } -} diff --git a/shell/qml/services/NotificationHandler.qml b/shell/qml/services/NotificationHandler.qml deleted file mode 100644 index 4f8aaaba1..000000000 --- a/shell/qml/services/NotificationHandler.qml +++ /dev/null @@ -1,73 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: root - - function findNotification(id) { - for (var i = 0; i < NotificationService.notifications.length; i++) { - if (NotificationService.notifications[i].id === id) - return NotificationService.notifications[i]; - } - return null; - } - - function handleNotificationClick(id) { - Logger.info("NotificationHandler", "Notification clicked: " + id); - var notification = root.findNotification(id); - if (!notification) { - Logger.warn("NotificationHandler", "Notification not found: " + id); - return; - } - var appId = notification.appId; - if (appId === "phone") { - NavigationRouter.navigateToDeepLink(appId, "history", {}); - } else if (appId === "messages") { - var notifId = notification.id; - if (typeof notifId === "string" && notifId.startsWith("sms_")) { - var parts = notifId.split("_"); - if (parts.length >= 2) { - var phoneNumber = parts[1]; - NavigationRouter.navigateToDeepLink(appId, "conversation", { - "number": phoneNumber - }); - } else { - NavigationRouter.navigateToDeepLink(appId, "", {}); - } - } else { - NavigationRouter.navigateToDeepLink(appId, "", {}); - } - } else { - NavigationRouter.navigateToDeepLink(appId, "", {}); - } - NotificationService.dismissNotification(id); - } - - function handleNotificationAction(id, action) { - Logger.info("NotificationHandler", "Notification action: " + action + " for ID: " + id); - var notification = root.findNotification(id); - if (!notification) { - Logger.warn("NotificationHandler", "Notification not found: " + id); - return; - } - if (notification.appId === "phone") { - if (action === "call_back") { - var number = notification.body.replace("From: ", ""); - if (typeof TelephonyService !== 'undefined') - TelephonyService.dial(number); - - NavigationRouter.navigateToDeepLink("phone", "", {}); - } else if (action === "message") { - var number2 = notification.body.replace("From: ", ""); - NavigationRouter.navigateToDeepLink("messages", "conversation", { - "number": number2 - }); - } - } else if (notification.appId === "messages") { - if (action === "reply") - NavigationRouter.navigateToDeepLink("messages", "", {}); - else if (action === "mark_read") - NotificationService.dismissNotification(id); - } - } -} diff --git a/shell/qml/services/NotificationService.qml b/shell/qml/services/NotificationService.qml deleted file mode 100644 index 2eb825555..000000000 --- a/shell/qml/services/NotificationService.qml +++ /dev/null @@ -1,420 +0,0 @@ -pragma Singleton -import QtQuick - -/** - * @singleton - * @brief Manages system-wide notifications - * - * NotificationService provides a centralized system for sending, managing, - * and tracking notifications. Integrates with platform notification systems - * (D-Bus on Linux, NSUserNotification on macOS). - * - * @example - * // Send a simple notification - * NotificationService.sendNotification( - * "myapp", - * "Hello World", - * "This is a notification body" - * ) - * - * @example - * // Send notification with actions - * NotificationService.sendNotification( - * "messages", - * "New Message", - * "John: Hey, are you free?", - * { - * icon: "qrc:/images/messages.svg", - * category: "message", - * priority: "high", - * actions: ["reply", "dismiss"] - * } - * ) - */ -QtObject { - // Following freedesktop.org spec: ALL apps (internal or external) call the DBus interface - // The notification daemon (shell) handles everything in _handleExternalNotification() - // This prevents duplicate handling and follows the standard architecture - // app_name - // replaces_id (0 = new, >0 = replace existing) - // app_icon - // summary - // body - // actions - // hints - // expire_timeout (0 = never, else milliseconds) - - id: notificationService - - /** - * @brief Array of all notifications - * @type {Array} - */ - property var notifications: [] - /** - * @brief Count of unread notifications - * @type {int} - */ - property int unreadCount: 0 - /** - * @brief Whether notifications are globally enabled - * @type {bool} - * @default true - */ - property bool notificationsEnabled: true - /** - * @brief Whether notification sounds are enabled - * @type {bool} - * @default true - */ - property bool soundEnabled: true - /** - * @brief Whether notification vibrations are enabled - * @type {bool} - * @default true - */ - property bool vibrationEnabled: true - property bool ledEnabled: true - /** - * @brief Whether Do Not Disturb mode is active - * @type {bool} - * @default false - */ - property bool isDndEnabled: false - /** - * @brief Track last notification ID per app for replacement - * @type {Object} Map of appId -> notification ID - * @private - */ - property var appNotificationIds: ({}) - - /** - * @brief Emitted when a new notification is received - * @param {Object} notification - The notification object - */ - signal notificationReceived(var notification) - /** - * @brief Emitted when a notification is dismissed - * @param {int} id - Notification ID - */ - signal notificationDismissed(int id) - /** - * @brief Emitted when a notification is clicked - * @param {int} id - Notification ID - */ - signal notificationClicked(int id) - /** - * @brief Emitted when a notification action button is triggered - * @param {int} id - Notification ID - * @param {string} action - Action identifier - */ - signal notificationActionTriggered(int id, string action) - - /** - * @brief Sends a new notification - * - * @param {string} appId - Application identifier - * @param {string} title - Notification title - * @param {string} body - Notification body text - * @param {Object} options - Optional configuration - * @param {string} options.icon - Icon URL - * @param {string} options.image - Large image URL - * @param {string} options.category - Category ("message", "email", "system", etc.) - * @param {string} options.priority - Priority level ("low", "normal", "high") - * @param {Array} options.actions - Action button labels - * @param {bool} options.persistent - Whether notification persists until dismissed - * - * @returns {int} Notification ID, or -1 if notifications disabled - * - * @example - * const id = NotificationService.sendNotification( - * "calendar", - * "Meeting in 5 minutes", - * "Team standup in Conference Room A", - * { - * icon: "qrc:/images/calendar.svg", - * category: "reminder", - * priority: "high", - * persistent: true - * } - * ) - */ - function sendNotification(appId, title, body, options) { - if (!notificationsEnabled) { - console.log("[NotificationService] Notifications disabled, ignoring"); - return -1; - } - console.log("[NotificationService] Sending notification via DBus:", title); - // Build notification object for DBus call - var notification = { - "appId": appId || "system", - "title": title || "", - "body": body || "", - "icon": (options && options.icon) ? options.icon : "", - "category": (options && options.category) ? options.category : "message", - "priority": (options && options.priority) ? options.priority : "normal", - "actions": (options && options.actions) ? options.actions : [], - "persistent": (options && typeof options.persistent !== "undefined") ? options.persistent : false - }; - // Call DBus interface - this will trigger _handleExternalNotification() which does: - // - Add to NotificationModel - // - Add to internal notifications array - // - Play sound/haptic - // - Show UI - _platformNotify(notification); - // Return -1 for now (we'll get the real ID from the DBus callback) - return -1; - } - - function dismissNotification(id) { - console.log("[NotificationService] Dismissing notification:", id); - var found = false; - for (var i = 0; i < notifications.length; i++) { - if (notifications[i].id === id) { - found = true; - var appId = notifications[i].appId; - if (!notifications[i].read) - unreadCount = Math.max(0, unreadCount - 1); - - notifications.splice(i, 1); - notificationDismissed(id); - // Clear the tracked ID for this app - if (appNotificationIds[appId] === id) - delete appNotificationIds[appId]; - - break; - } - } - // CRITICAL: Always dismiss from NotificationModel and DBus, even if not in internal array - // This handles race conditions where notification hasn't been fully processed yet - if (!found) - console.warn("[NotificationService] Notification", id, "not found in internal array (possible race condition)"); - - NotificationModel.dismissNotification(id); - _platformDismissNotification(id); - } - - function dismissAllNotifications() { - console.log("[NotificationService] Dismissing all notifications"); - notifications = []; - unreadCount = 0; - _platformDismissAllNotifications(); - NotificationModel.dismissAllNotifications(); - } - - function markAsRead(id) { - for (var i = 0; i < notifications.length; i++) { - if (notifications[i].id === id && !notifications[i].read) { - notifications[i].read = true; - unreadCount = Math.max(0, unreadCount - 1); - NotificationModel.markAsRead(id); - break; - } - } - } - - function markAllAsRead() { - console.log("[NotificationService] Marking all as read"); - for (var i = 0; i < notifications.length; i++) { - if (!notifications[i].read) { - notifications[i].read = true; - NotificationModel.markAsRead(notifications[i].id); - } - } - unreadCount = 0; - } - - function clearAll() { - console.log("[NotificationService] Clearing all notifications"); - var notifIds = notifications.map(function (n) { - return n.id; - }); - for (var i = 0; i < notifIds.length; i++) { - dismissNotification(notifIds[i]); - } - } - - function clickNotification(id) { - console.log("[NotificationService] Notification clicked:", id); - markAsRead(id); - notificationClicked(id); - _platformNotificationClicked(id); - } - - function triggerAction(id, action) { - console.log("[NotificationService] Action triggered:", id, action); - notificationActionTriggered(id, action); - _platformNotificationAction(id, action); - } - - function getNotification(id) { - for (var i = 0; i < notifications.length; i++) { - if (notifications[i].id === id) - return notifications[i]; - } - return null; - } - - function getNotificationsByApp(appId) { - return notifications.filter(function (n) { - return n.appId === appId; - }); - } - - function getUnreadNotifications() { - return notifications.filter(function (n) { - return !n.read; - }); - } - - function getNotificationCountForApp(appId) { - var count = 0; - for (var i = 0; i < notifications.length; i++) { - if (notifications[i].appId === appId && !notifications[i].read) - count++; - } - return count; - } - - function _platformNotify(notification) { - if (Platform.isLinux) { - // Emit via org.freedesktop.Notifications DBus interface - if (typeof FreedesktopNotifications !== 'undefined') { - console.log("[NotificationService] Emitting D-Bus notification:", notification.title); - // Convert actions array to QStringList format (key, label, key, label, ...) - var actionsList = []; - if (notification.actions && notification.actions.length > 0) { - for (var i = 0; i < notification.actions.length; i++) { - actionsList.push(notification.actions[i]); // key - actionsList.push(notification.actions[i]); // label (same as key for simplicity) - } - } - // Build hints map - var hints = { - "category": notification.category, - "urgency": notification.priority === "high" ? 2 : (notification.priority === "low" ? 0 : 1), - "desktop-entry": notification.appId - }; - // Get the last notification ID for this app (for replacement) - // Per freedesktop spec: if replaces_id > 0, the new notification replaces the old one - var replacesId = appNotificationIds[notification.appId] || 0; - // Call the C++ DBus service - var newId = FreedesktopNotifications.Notify(notification.appId, replacesId, notification.icon || "", notification.title, notification.body, actionsList, hints, notification.persistent ? 0 : 5000); - // Store the new ID for future replacements - appNotificationIds[notification.appId] = newId; - } else { - console.warn("[NotificationService] FreedesktopNotifications not available"); - } - } else if (Platform.isMacOS) { - console.log("[NotificationService] macOS NSUserNotification"); - } - } - - function _platformDismissNotification(id) { - console.log("[NotificationService] _platformDismissNotification called for id:", id); - console.log("[NotificationService] Platform.isLinux:", Platform.isLinux); - console.log("[NotificationService] FreedesktopNotifications available:", typeof FreedesktopNotifications); - if (Platform.isLinux) { - if (typeof FreedesktopNotifications !== 'undefined' && FreedesktopNotifications !== null) { - console.log("[NotificationService] D-Bus CloseNotification:", id); - try { - FreedesktopNotifications.CloseNotification(id); - console.log("[NotificationService] CloseNotification call completed"); - } catch (error) { - console.error("[NotificationService] Error calling CloseNotification:", error); - } - } else { - console.warn("[NotificationService] FreedesktopNotifications not available for CloseNotification"); - } - } else { - console.log("[NotificationService] Not on Linux, skipping CloseNotification"); - } - } - - function _platformDismissAllNotifications() { - if (Platform.isLinux) - console.log("[NotificationService] Dismissing all via D-Bus"); - } - - function _platformNotificationClicked(id) { - if (Platform.isLinux) - console.log("[NotificationService] D-Bus NotificationClosed with reason 2 (clicked)"); - } - - function _platformNotificationAction(id, action) { - if (Platform.isLinux) - console.log("[NotificationService] D-Bus ActionInvoked:", id, action); - } - - function _populateTestNotifications() { - sendNotification("messages", "John Doe", "Hey, are you free tonight?", { - "icon": "message-square", - "category": "message", - "priority": "high", - "actions": ["reply", "dismiss"] - }); - sendNotification("email", "Work Email", "Meeting moved to 3pm", { - "icon": "calendar", - "category": "email", - "priority": "normal" - }); - sendNotification("system", "System Update", "Software update available", { - "icon": "qrc:/images/settings.svg", - "category": "system", - "priority": "low" - }); - } - - // Handler for ALL notifications coming via DBus (FreedesktopNotifications) - // Following freedesktop.org spec: This is the SINGLE point where all notifications are processed - // Both internal Marathon OS apps and external apps go through this path - function _handleExternalNotification(id) { - console.log("[NotificationService] Notification received via DBus:", id); - if (typeof NotificationModel === "undefined" || !NotificationModel) { - console.warn("[NotificationService] NotificationModel not available (isolated app process?)"); - return; - } - // Get the notification from the model (added by FreedesktopNotifications::Notify) - var notification = NotificationModel.getNotification(id); - if (!notification) { - console.warn("[NotificationService] Notification not found:", id); - return; - } - // Create notification object for internal tracking - var notif = { - "id": id, - "appId": notification.appId, - "title": notification.title, - "body": notification.body, - "icon": notification.icon, - "timestamp": new Date(notification.timestamp).toISOString(), - "read": false, - "category": "message", - "priority": "normal" - }; - // Add to internal array - notifications.push(notif); - unreadCount++; - console.log("[NotificationService] Notification processed:", id, notification.title); - // Emit signal for UI updates - notificationReceived(notif); - // Play sound and vibrate (SINGLE source of audio/haptic feedback) - if (soundEnabled && !AudioManager.dndEnabled) - AudioManager.playNotificationSound(); - - if (vibrationEnabled && !AudioManager.dndEnabled) - AudioManager.vibrate([50, 100, 50]); - } - - Component.onCompleted: { - console.log("[NotificationService] Initialized"); - // Connect to NotificationModel signal for external notifications. - // In strict process isolation, apps may import MarathonOS.Shell QML without having the - // shell-only NotificationModel context property; guard to avoid ReferenceError spam. - if (typeof NotificationModel !== "undefined" && NotificationModel) - NotificationModel.notificationAdded.connect(_handleExternalNotification); - - if (!Platform.isLinux && !Platform.isMacOS) - _populateTestNotifications(); - } -} diff --git a/shell/qml/services/Platform.qml b/shell/qml/services/Platform.qml deleted file mode 100644 index 6257833b7..000000000 --- a/shell/qml/services/Platform.qml +++ /dev/null @@ -1,44 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - // Default to true for safety - - id: platform - - readonly property string os: Qt.platform.os - readonly property bool isLinux: os === "linux" - readonly property bool isMacOS: os === "osx" - readonly property bool isAndroid: os === "android" - readonly property bool isIOS: os === "ios" - readonly property bool hasDBus: isLinux || isAndroid - readonly property bool hasSystemdLogind: isLinux - readonly property bool hasUPower: isLinux - readonly property bool hasNetworkManager: isLinux - readonly property bool hasModemManager: isLinux - readonly property bool hasPulseAudio: isLinux - // Hardware keyboard detection (via C++ Platform backend) - readonly property bool hasHardwareKeyboard: typeof PlatformCpp !== 'undefined' ? PlatformCpp.hasHardwareKeyboard : true - readonly property string backend: { - if (isLinux) - return "linux"; - - if (isMacOS) - return "macos"; - - if (isAndroid) - return "android"; - - if (isIOS) - return "ios"; - - return "unknown"; - } - - Component.onCompleted: { - Logger.info("Platform", "Detected OS: " + os); - Logger.info("Platform", "Backend: " + backend); - Logger.info("Platform", "D-Bus available: " + hasDBus); - Logger.info("Platform", "Hardware keyboard: " + hasHardwareKeyboard); - } -} diff --git a/shell/qml/services/PowerBatteryHandler.qml b/shell/qml/services/PowerBatteryHandler.qml deleted file mode 100644 index 43b527a40..000000000 --- a/shell/qml/services/PowerBatteryHandler.qml +++ /dev/null @@ -1,34 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: root - - function handlePowerButtonPress() { - if (typeof PowerPolicyControllerCpp === "undefined" || !PowerPolicyControllerCpp) - return; - - var screenOn = (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) ? DisplayPolicyControllerCpp.screenOn : true; - var isLocked = SessionStore.isLocked; - var action = PowerPolicyControllerCpp.powerButtonAction(screenOn, isLocked); - if (action === PowerPolicyControllerCpp.TurnScreenOn) { - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) - DisplayPolicyControllerCpp.turnScreenOn(); - else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.setScreenState(true); - } else if (action === PowerPolicyControllerCpp.LockAndTurnScreenOff) { - SessionStore.lock(); - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) - DisplayPolicyControllerCpp.turnScreenOff(); - else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.setScreenState(false); - } else if (action === PowerPolicyControllerCpp.TurnScreenOff) { - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) - DisplayPolicyControllerCpp.turnScreenOff(); - else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.setScreenState(false); - } - if (typeof HapticService !== "undefined" && HapticService) - HapticService.medium(); - } -} diff --git a/shell/qml/services/ProximitySensor.qml b/shell/qml/services/ProximitySensor.qml deleted file mode 100644 index d9b7a725e..000000000 --- a/shell/qml/services/ProximitySensor.qml +++ /dev/null @@ -1,45 +0,0 @@ -pragma Singleton -import QtQuick - -Item { - id: proximitySensor - - property bool available: SensorManagerCpp.available - property bool active: true - property bool near: SensorManagerCpp.proximityNear - property real distance: near ? 0 : 100 - property bool autoScreenOff: true // Auto turn off screen when near - - signal proximityChanged(bool near) - - Component.onCompleted: { - Logger.info("ProximitySensor", "Initialized (QtSensors)"); - } - - Connections { - function onProximityNearChanged() { - near = SensorManagerCpp.proximityNear; - distance = near ? 0 : 100; - Logger.info("ProximitySensor", "NEAR = " + near); - proximityChanged(near); - if (autoScreenOff && typeof TelephonyIntegration !== "undefined" && TelephonyIntegration.hasActiveCall) { - var screenOn = (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) ? DisplayPolicyControllerCpp.screenOn : true; - if (near && screenOn) { - Logger.info("ProximitySensor", "Auto screen OFF (during call)"); - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) - DisplayPolicyControllerCpp.turnScreenOff(); - else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.setScreenState(false); - } else if (!near && !screenOn) { - Logger.info("ProximitySensor", "Auto screen ON (away from face)"); - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) - DisplayPolicyControllerCpp.turnScreenOn(); - else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.setScreenState(true); - } - } - } - - target: SensorManagerCpp - } -} diff --git a/shell/qml/services/ScreenshotService.qml b/shell/qml/services/ScreenshotService.qml deleted file mode 100644 index 3d824e5ef..000000000 --- a/shell/qml/services/ScreenshotService.qml +++ /dev/null @@ -1,93 +0,0 @@ -pragma Singleton -import QtCore -import QtQuick - -QtObject { - id: screenshotService - - property string screenshotsPath: { - // Use StandardPaths for proper path resolution - var homePath = StandardPaths.writableLocation(StandardPaths.HomeLocation); - var picturesPath = StandardPaths.writableLocation(StandardPaths.PicturesLocation); - var path = ""; - if (picturesPath && picturesPath !== "") - path = picturesPath + "/Screenshots/"; - else if (homePath && homePath !== "") - path = homePath + "/Pictures/Screenshots/"; - else - path = "/tmp/Screenshots/"; - // Remove file:// prefix if present - if (path.indexOf("file://") === 0) - path = path.substring(7); - - return path; - } - property var shellWindow: null - - signal screenshotCaptured(string filePath, string thumbnailPath) - signal screenshotFailed(string error) - - // Alias for convenience - function takeScreenshot(windowItem) { - return captureScreen(windowItem); - } - - function captureScreen(windowItem) { - console.log("[ScreenshotService] Capturing screenshot"); - // Use provided windowItem, or fallback to stored shellWindow - var targetWindow = windowItem || shellWindow; - if (!targetWindow) { - console.error("[ScreenshotService] No window available for capture"); - screenshotFailed("No window available"); - return; - } - var timestamp = Qt.formatDateTime(new Date(), "yyyy-MM-dd_HH-mm-ss"); - var filename = "Screenshot_" + timestamp + ".png"; - var fullPath = screenshotsPath + filename; - console.log("[ScreenshotService] Screenshots path:", screenshotsPath); - console.log("[ScreenshotService] Full path:", fullPath); - if (targetWindow.grabToImage) { - targetWindow.grabToImage(function (result) { - if (result) { - console.log("[ScreenshotService] Image grabbed, attempting to save..."); - var saved = result.saveToFile(fullPath); - console.log("[ScreenshotService] Save result:", saved); - if (saved) { - console.log("[ScreenshotService] Screenshot saved:", fullPath); - // Emit with file path (not QImage) for preview - screenshotCaptured(fullPath, fullPath); - // Play camera shutter sound - if (typeof AudioManager !== 'undefined') - AudioManager.playNotificationSound(); - - // Haptic feedback - if (typeof HapticService !== 'undefined') - HapticService.medium(); - - // Show notification - if (typeof NotificationService !== 'undefined') - NotificationService.sendNotification("system", "Screenshot captured", filename, { - "icon": "camera", - "category": "system", - "priority": "low" - }); - } else { - console.error("[ScreenshotService] Failed to save screenshot"); - screenshotFailed("Failed to save file"); - } - } else { - console.error("[ScreenshotService] Failed to capture screenshot"); - screenshotFailed("Failed to capture image"); - } - }); - } else { - console.error("[ScreenshotService] grabToImage not supported"); - screenshotFailed("grabToImage not supported"); - } - } - - Component.onCompleted: { - console.log("[ScreenshotService] Initialized"); - console.log("[ScreenshotService] Screenshots path:", screenshotsPath); - } -} diff --git a/shell/qml/services/StateManager.qml b/shell/qml/services/StateManager.qml deleted file mode 100644 index f3a359f7f..000000000 --- a/shell/qml/services/StateManager.qml +++ /dev/null @@ -1,99 +0,0 @@ -pragma Singleton -import Qt.labs.settings -import QtQuick - -Item { - id: root - - property var runningApps: ({}) - property var appStates: ({}) - readonly property Settings settings: Settings { - property string runningAppIds: "" - property string appStateData: "" - - category: "AppState" - } - - readonly property Timer autoSaveTimer: Timer { - interval: 5000 - running: true - repeat: true - onTriggered: root.saveAllStates() - } - - function registerApp(appId: string, isVisible: bool) { - const timestamp = Date.now(); - runningApps[appId] = { - "appId": appId, - "isVisible": isVisible, - "timestamp": timestamp - }; - console.log("StateManager: Registered app", appId, "visible:", isVisible); - } - - function unregisterApp(appId: string) { - if (runningApps[appId]) { - delete runningApps[appId]; - console.log("StateManager: Unregistered app", appId); - } - } - - function updateAppVisibility(appId: string, isVisible: bool) { - if (runningApps[appId]) { - runningApps[appId].isVisible = isVisible; - runningApps[appId].timestamp = Date.now(); - } - } - - function saveAppState(appId: string, state: string) { - appStates[appId] = { - "state": state, - "timestamp": Date.now() - }; - console.log("StateManager: Saved state for", appId, ":", state); - } - - function saveAllStates() { - try { - const appIdArray = Object.keys(runningApps); - settings.runningAppIds = JSON.stringify(appIdArray); - settings.appStateData = JSON.stringify(appStates); - } catch (e) { - console.error("StateManager: Failed to save states:", e); - } - } - - function restoreStates() { - try { - if (settings.runningAppIds) { - const appIds = JSON.parse(settings.runningAppIds); - console.log("StateManager: Found", appIds.length, "running apps to restore"); - return appIds; - } - } catch (e) { - console.error("StateManager: Failed to restore states:", e); - } - return []; - } - - function getAppState(appId: string) { - return appStates[appId] || null; - } - - function clearAppState(appId: string) { - if (appStates[appId]) - delete appStates[appId]; - } - - function clearAllStates() { - runningApps = {}; - appStates = {}; - settings.runningAppIds = ""; - settings.appStateData = ""; - console.log("StateManager: Cleared all states"); - } - - Component.onCompleted: { - console.log("StateManager: Initialized with auto-save every 5 seconds"); - } -} diff --git a/shell/qml/services/StatusBarIconService.qml b/shell/qml/services/StatusBarIconService.qml deleted file mode 100644 index c33a1ba34..000000000 --- a/shell/qml/services/StatusBarIconService.qml +++ /dev/null @@ -1,155 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - // Excellent - // 91-100% - // 4 bars (excellent) - // 3-4 bars (excellent) - // Excellent - // Critical (red) - // Low (orange) - // Medium - // Full - - id: statusBarIconService - - function getBatteryIcon(level, isCharging) { - if (isCharging) - return "battery-charging"; - - // 5-level granular battery display - if (level <= 10) - return "battery-warning"; - else if (level <= 25) - return "battery-low"; - else if (level <= 50) - return "battery-medium"; - else if (level <= 90) - return "battery-full"; - else - return "battery-full"; - } - - function getBatteryColor(level, isCharging) { - if (isCharging) - return "#00CCCC"; - - if (level <= 10) - return "#FF4444"; - else if (level <= 20) - return "#FF8800"; - else - return "#FFFFFF"; - } - - function getSignalIcon(strength) { - // 5-level granular cellular signal display - if (strength === 0) - return "signal-zero"; - - if (strength <= 25) - return "signal-low"; - - // 1 bar - if (strength <= 50) - return "signal-medium"; - - // 2 bars - if (strength <= 75) - return "signal"; - - // 3 bars (good) - return "signal-high"; - } - - function getSignalOpacity(strength) { - // Icon shows signal level, opacity shows overall status - if (strength === 0) - return 0.3; - - // No signal - if (strength <= 25) - return 0.6; - - // Poor - if (strength <= 50) - return 0.8; - - // Fair - if (strength <= 75) - return 0.9; - - // Good - return 1; - } - - function getWifiIcon(isEnabled, strength, isConnected) { - // 4-level granular WiFi signal display - if (!isEnabled) - return "wifi-off"; - - if (!isConnected) - return "wifi-off"; - - if (strength === 0) - return "wifi-zero"; - - // Connected but no signal - if (strength <= 33) - return "wifi-low"; - - // 1-2 bars (weak) - if (strength <= 66) - return "wifi"; - - // 2-3 bars (good) - use base icon - return "wifi-high"; - } - - function getWifiOpacity(isEnabled, strength, isConnected) { - // Icon shows signal level, opacity shows overall status - if (!isEnabled || !isConnected) - return 0.3; - - if (strength === 0) - return 0.4; - - // Connected but no signal - if (strength <= 33) - return 0.6; - - // Weak - if (strength <= 66) - return 0.8; - - // Good - return 1; - } - - function getBluetoothIcon(isEnabled, isConnected) { - return "bluetooth"; - } - - function getBluetoothOpacity(isEnabled, isConnected) { - if (!isEnabled) - return 0.3; - - if (isConnected) - return 1; - - return 0.6; - } - - function shouldShowAirplaneMode(isEnabled) { - return isEnabled; - } - - function shouldShowDND(isEnabled) { - return isEnabled; - } - - function shouldShowBluetooth(isEnabled) { - return isEnabled; - } -} diff --git a/shell/qml/services/TelephonyIntegration.qml b/shell/qml/services/TelephonyIntegration.qml deleted file mode 100644 index 8ab4f58a5..000000000 --- a/shell/qml/services/TelephonyIntegration.qml +++ /dev/null @@ -1,128 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: root - - property string lastCallState: "idle" - property string lastCallerNumber: "" - property bool callWasAnswered: false - property var incomingCallOverlay: null - // True whenever we have an active or ringing call (used by proximity sensor / screen policy). - property bool hasActiveCall: false - - function resolveContactName(number) { - if (typeof ContactsManager !== 'undefined') { - var contact = ContactsManager.getContactByNumber(number); - if (contact && contact.name) - return contact.name; - } - return number; - } - - function createMissedCallNotification(number) { - var contactName = root.resolveContactName(number); - if (typeof NotificationService !== 'undefined') - NotificationService.sendNotification("phone", "Missed Call", "From: " + contactName, { - "icon": "phone-missed", - "category": "call", - "priority": "high", - "actions": [ - { - "id": "call_back", - "label": "Call Back" - }, - { - "id": "message", - "label": "Message" - } - ] - }); - - if (typeof HapticService !== 'undefined') - HapticService.heavy(); - } - - function handleIncomingCall(number) { - Logger.info("TelephonyIntegration", "📞 INCOMING CALL from: " + number); - root.lastCallerNumber = number; - root.callWasAnswered = false; - if (typeof PowerPolicyControllerCpp !== "undefined" && PowerPolicyControllerCpp) - PowerPolicyControllerCpp.wake("call"); - else if (typeof PowerManagerService !== "undefined" && PowerManagerService) - PowerManagerService.acquireWakelock("call"); - if (typeof DisplayPolicyControllerCpp !== "undefined" && DisplayPolicyControllerCpp) - DisplayPolicyControllerCpp.turnScreenOn(); - else if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.setScreenState(true); - // Play ringtone - if (typeof AudioManager !== 'undefined') - AudioManager.playRingtone(); - - var contactName = root.resolveContactName(number); - if (root.incomingCallOverlay) - root.incomingCallOverlay.show(number, contactName); - - if (typeof HapticService !== 'undefined') - HapticService.vibratePattern([1000, 500], -1); - } - - function handleCallStateChanged(state) { - Logger.info("TelephonyIntegration", "📞 Call state changed: " + state); - root.hasActiveCall = (state === "incoming" || state === "ringing" || state === "active"); - if (root.lastCallState === "incoming" && (state === "idle" || state === "terminated")) { - if (!root.callWasAnswered) { - Logger.info("TelephonyIntegration", "📞 MISSED CALL from: " + root.lastCallerNumber); - root.createMissedCallNotification(root.lastCallerNumber); - } - } - if (state === "active" || state === "idle" || state === "terminated") { - // Stop ringtone when call is answered/ended - if (typeof AudioManager !== 'undefined') - AudioManager.stopRingtone(); - - if (root.incomingCallOverlay && root.incomingCallOverlay.visible) - root.incomingCallOverlay.hide(); - - if (typeof HapticService !== 'undefined') - HapticService.stopVibration(); - - // If the session is already unlocked (grace period), don't strand the user on the lock screen - // after dismissing the ringer UI. - if (typeof SessionStore !== "undefined" && SessionStore && !SessionStore.isLocked && SessionStore.showLockScreen) - SessionStore.unlock(); - } - root.lastCallState = state; - } - - function handleMessageReceived(sender, text, timestamp) { - Logger.info("TelephonyIntegration", "💬 SMS RECEIVED from: " + sender); - if (typeof PowerPolicyControllerCpp !== "undefined" && PowerPolicyControllerCpp) - PowerPolicyControllerCpp.wake("notification"); - else if (typeof PowerManagerService !== "undefined" && PowerManagerService) - PowerManagerService.acquireWakelock("notification"); - var contactName = root.resolveContactName(sender); - if (typeof NotificationService !== 'undefined') - NotificationService.sendNotification("messages", contactName, text, { - "icon": "message-circle", - "category": "message", - "priority": "high", - "actions": [ - { - "id": "reply", - "label": "Reply" - }, - { - "id": "mark_read", - "label": "Mark Read" - } - ] - }); - - if (typeof HapticService !== 'undefined') - HapticService.medium(); - - if (typeof AudioManager !== 'undefined') - AudioManager.playNotificationSound(); - } -} diff --git a/shell/qml/services/UnifiedSearchService.qml b/shell/qml/services/UnifiedSearchService.qml deleted file mode 100644 index 7ff42a672..000000000 --- a/shell/qml/services/UnifiedSearchService.qml +++ /dev/null @@ -1,290 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - // 2. Title starts with query (very high priority) - // 3. Exact keyword match - // 4. Any keyword starts with query - - id: searchService - - property var searchIndex: [] - property var recentSearches: [] - property bool isIndexing: false - property int maxRecentSearches: 10 - - signal searchCompleted(var results) - signal indexingComplete - - function buildSearchIndex() { - Logger.info("UnifiedSearch", "Building search index..."); - isIndexing = true; - searchIndex = []; - // Get apps from C++ AppModel using getAppAtIndex - var appCount = AppModel.count; - Logger.info("UnifiedSearch", "Indexing " + appCount + " apps from AppModel"); - var actualAppsAdded = 0; - for (var i = 0; i < appCount; i++) { - var app = AppModel.getAppAtIndex(i); - if (!app) { - Logger.warning("UnifiedSearch", "Failed to get app at index " + i); - continue; - } - var appId = app.id; - var appName = app.name; - var appIcon = app.icon; - var appType = app.type; - // Skip native apps if searchNativeApps is disabled - if (appType === "native" && !SettingsManagerCpp.searchNativeApps) - continue; - - if (appName && appId) { - var keywords = [appName.toLowerCase(), appId.toLowerCase()]; - // Add word fragments for better matching - var nameParts = appName.toLowerCase().split(/\s+/); - for (var j = 0; j < nameParts.length; j++) { - if (nameParts[j].length > 0) - keywords.push(nameParts[j]); - } - searchIndex.push({ - "type": "app", - "id": appId, - "title": appName, - "subtitle": appType === "native" ? "Native App" : "Marathon App", - "icon": appIcon, - "keywords": keywords, - "searchText": appName.toLowerCase() + " " + appId.toLowerCase(), - "data": { - "id": appId, - "name": appName, - "icon": appIcon, - "type": appType - }, - "score": 0 - }); - actualAppsAdded++; - } - } - Logger.info("UnifiedSearch", "Added " + actualAppsAdded + " apps to search index"); - // Index deep links from all apps in MarathonAppRegistry - var deepLinkCount = 0; - if (typeof MarathonAppRegistry !== 'undefined') { - var registryCount = MarathonAppRegistry.count; - Logger.info("UnifiedSearch", "Indexing deep links from " + registryCount + " apps in registry"); - for (var j = 0; j < registryCount; j++) { - // Use getApp() instead of data() - much simpler! - var allAppIds = MarathonAppRegistry.getAllAppIds(); - if (j >= allAppIds.length) - break; - - var appId = allAppIds[j]; - var appData = MarathonAppRegistry.getApp(appId); - if (!appData || !appData.id) { - console.error("Failed to get app data for index", j); - continue; - } - var registryApp = appData.id; - var appName = appData.name; - var appIcon = appData.icon; - var deepLinksJson = appData.deepLinks; - Logger.info("UnifiedSearch", "App " + j + ": " + registryApp + " (" + appName + ") - deepLinks JSON length: " + (deepLinksJson ? deepLinksJson.length : 0)); - if (deepLinksJson && deepLinksJson.length > 0) { - Logger.info("UnifiedSearch", "Deep links JSON for " + registryApp + ": " + deepLinksJson); - try { - var deepLinks = JSON.parse(deepLinksJson); - var linkCount = 0; - for (var route in deepLinks) { - var link = deepLinks[route]; - var linkKeywords = link.keywords || []; - // Add app name as extra context - linkKeywords.push(appName.toLowerCase()); - linkKeywords.push(route.toLowerCase()); - Logger.info("UnifiedSearch", " Adding deep link: " + link.title + " (route: " + route + ")"); - searchIndex.push({ - "type": "deeplink", - "id": route, - "appId": registryApp, - "title": link.title || route, - "subtitle": link.description || appName, - "icon": appIcon || "qrc:/images/app-icon-placeholder.svg", - "keywords": linkKeywords, - "searchText": (link.title || route).toLowerCase() + " " + linkKeywords.join(" "), - "data": { - "appId": registryApp, - "route": route, - "appName": appName - }, - "score": 0 - }); - deepLinkCount++; - linkCount++; - } - Logger.info("UnifiedSearch", " Added " + linkCount + " deep links for " + registryApp); - } catch (e) { - Logger.error("UnifiedSearch", "Failed to parse deep links for " + registryApp + ": " + e); - } - } else { - Logger.info("UnifiedSearch", " No deep links for " + registryApp); - } - } - Logger.info("UnifiedSearch", "Indexed " + deepLinkCount + " deep links"); - } else { - Logger.warning("UnifiedSearch", "MarathonAppRegistry is undefined!"); - } - isIndexing = false; - indexingComplete(); - Logger.info("UnifiedSearch", "Index built: " + searchIndex.length + " items (" + actualAppsAdded + " apps + " + deepLinkCount + " deep links)"); - } - - function search(query) { - if (!query || query.trim().length === 0) - return []; - - var normalizedQuery = query.toLowerCase().trim(); - var results = []; - // Fast path: exact matches first - for (var i = 0; i < searchIndex.length; i++) { - var item = searchIndex[i]; - var score = 0; - // 1. Exact title match (highest priority) - if (item.title.toLowerCase() === normalizedQuery) { - score = 10000; - } else if (item.title.toLowerCase().startsWith(normalizedQuery)) { - score = 5000; - } else if (item.keywords.indexOf(normalizedQuery) !== -1) { - score = 3000; - } else { - for (var j = 0; j < item.keywords.length; j++) { - if (item.keywords[j].startsWith(normalizedQuery)) { - score = Math.max(score, 2000); - break; - } - } - } - // 5. Title contains query - if (score === 0 && item.title.toLowerCase().indexOf(normalizedQuery) !== -1) - score = 1000; - - // 6. Any keyword contains query - if (score === 0) { - for (var m = 0; m < item.keywords.length; m++) { - if (item.keywords[m].indexOf(normalizedQuery) !== -1) - score = Math.max(score, 500); - } - } - // 7. Fuzzy match on searchText - if (score === 0) { - var fuzzyScore = fuzzyMatch(item.searchText, normalizedQuery); - if (fuzzyScore > 0) - score = fuzzyScore * 100; - } - // Boost apps over settings - if (score > 0 && item.type === "app") - score += 100; - - if (score > 0) { - var result = Object.assign({}, item); - result.score = score; - results.push(result); - } - } - // Sort by score (highest first), then alphabetically - results.sort(function (a, b) { - if (b.score !== a.score) - return b.score - a.score; - - return a.title.localeCompare(b.title); - }); - searchCompleted(results); - Logger.info("UnifiedSearch", "Search for '" + query + "' returned " + results.length + " results"); - return results; - } - - function fuzzyMatch(text, pattern) { - var patternIdx = 0; - var score = 0; - var consecutiveMatches = 0; - var textIdx = 0; - while (textIdx < text.length && patternIdx < pattern.length) { - if (text[textIdx] === pattern[patternIdx]) { - score += 1 + consecutiveMatches * 2; // Bonus for consecutive chars - consecutiveMatches++; - patternIdx++; - } else { - consecutiveMatches = 0; - } - textIdx++; - } - // Full match required - if (patternIdx === pattern.length) - return score / pattern.length; - - return 0; - } - - function addToRecentSearches(query) { - if (!query || query.trim().length === 0) - return; - - var normalized = query.trim(); - var existingIndex = recentSearches.indexOf(normalized); - if (existingIndex !== -1) - recentSearches.splice(existingIndex, 1); - - recentSearches.unshift(normalized); - if (recentSearches.length > maxRecentSearches) - recentSearches = recentSearches.slice(0, maxRecentSearches); - } - - function clearRecentSearches() { - recentSearches = []; - } - - function executeSearchResult(result) { - Logger.info("UnifiedSearch", "Executing result: " + result.type + " - " + result.title); - if (result.type === "app") { - // Launch app through UIStore - var app = result.data; - UIStore.openApp(app.id, app.name, app.icon); - } else if (result.type === "setting") { - // Legacy setting support (deprecated - use deeplink instead) - UIStore.openSettings(); - if (typeof Router !== 'undefined') - Router.navigateToSetting(result.id); - } else if (result.type === "deeplink") { - // NEW: Deep link navigation (core Marathon app pattern) - var linkData = result.data; - Logger.info("UnifiedSearch", "Navigating to deep link: " + linkData.appId + " → " + linkData.route); - if (typeof NavigationRouter !== 'undefined') - NavigationRouter.navigateToDeepLink(linkData.appId, linkData.route, {}); - else - Logger.error("UnifiedSearch", "NavigationRouter not available"); - } - } - - Component.onCompleted: { - Logger.info("UnifiedSearch", "Unified Search Service initialized"); - // Wait for app scanner to complete before building index - if (typeof MarathonAppScanner !== 'undefined') { - MarathonAppScanner.scanComplete.connect(function (count) { - Logger.info("UnifiedSearch", "App scan complete with " + count + " apps, building search index..."); - buildSearchIndex(); - }); - // If scan already happened, build immediately - if (typeof MarathonAppRegistry !== 'undefined' && MarathonAppRegistry.count > 0) { - Logger.info("UnifiedSearch", "Apps already loaded, building index immediately"); - buildSearchIndex(); - } - } else { - // Fallback: build index immediately - Logger.warning("UnifiedSearch", "MarathonAppScanner not available, building index immediately"); - buildSearchIndex(); - } - // Rebuild index when apps change - if (typeof AppModel !== 'undefined') - AppModel.countChanged.connect(function () { - Logger.info("UnifiedSearch", "App count changed, rebuilding index"); - buildSearchIndex(); - }); - } -} diff --git a/shell/qml/stores/AppStore.qml b/shell/qml/stores/AppStore.qml index da75d66a8..5c7d4d2d8 100644 --- a/shell/qml/stores/AppStore.qml +++ b/shell/qml/stores/AppStore.qml @@ -68,10 +68,8 @@ QtObject { ] property var nativeApps: [] property var apps: [] - // Listen for native apps from DesktopEntryParser property Connections desktopEntryConnection - // Helper function to get app metadata by ID function getApp(appId) { for (var i = 0; i < apps.length; i++) { if (apps[i].id === appId) @@ -80,31 +78,26 @@ QtObject { return null; } - // Get app name by ID function getAppName(appId) { var app = getApp(appId); return app ? app.name : appId; } - // Get app icon by ID function getAppIcon(appId) { var app = getApp(appId); return app ? app.icon : ""; } - // Check if app is Marathon app function isInternalApp(appId) { var app = getApp(appId); return app ? (app.type === "marathon") : true; } - // Check if app is native Wayland app function isNativeApp(appId) { var app = getApp(appId); return app ? (app.type === "native") : false; } - // Merge Marathon apps and native apps function refreshAppList() { var merged = []; for (var i = 0; i < marathonApps.length; i++) { diff --git a/shell/qml/stores/SessionStore.qml b/shell/qml/stores/SessionStore.qml index 8ef760523..d1cb9f872 100644 --- a/shell/qml/stores/SessionStore.qml +++ b/shell/qml/stores/SessionStore.qml @@ -1,84 +1,22 @@ pragma Singleton import QtQuick -/** - * @singleton - * @brief Manages session lock state and transitions - * - * Tracks whether the session is locked and coordinates lock/unlock - * animations across the UI (status bar lock icon, lock screen, etc.) - */ QtObject { - // ===== SESSION STATE ===== - // ===== ANIMATION STATE ===== - // ===== SIGNALS ===== - // ===== PUBLIC API ===== - // ===== INTERNAL ===== - id: sessionStore - /** - * @brief Whether the session is currently locked - * @type {bool} - * @default true - */ property bool isLocked: true - /** - * @brief Whether we're currently viewing the lock screen - * Used to show lock icon in status bar - * @type {bool} - * @default false - */ property bool isOnLockScreen: false - /** - * @brief Whether the lock screen should be visible - * True when session is locked OR when returning from screen-off with valid grace period - * @type {bool} - * @default true - */ property bool showLockScreen: true - /** - * @brief Whether a lock state transition animation is in progress - * @type {bool} - * @default false - */ property bool isAnimatingLock: false - /** - * @brief Current lock transition direction - * @type {string} - * @values "locking" | "unlocking" | "" - */ property string lockTransition: "" - /** - * @brief Timestamp until which session is valid (grace period) - * @type {number} - * @private - */ property real sessionValidUntil: 0 property Timer lockTimer - /** - * @brief Emitted when lock state is about to change - * @param {bool} toLocked - Target lock state - */ signal lockStateChanging(bool toLocked) - /** - * @brief Emitted when lock state has changed (after animation) - * @param {bool} isLocked - New lock state - */ signal lockStateChanged(bool isLocked) - /** - * @brief Trigger shake animation on lock icon (invalid PIN) - */ signal triggerShakeAnimation - /** - * @brief Trigger unlock animation on lock icon (valid PIN) - */ signal triggerUnlockAnimation - /** - * @brief Lock the session with animation - */ function lock() { console.log("[SessionStore] lock() called - current isLocked:", isLocked, "showLockScreen:", showLockScreen); if (isLocked) { @@ -86,24 +24,18 @@ QtObject { return; } console.log("[SessionStore] Locking session..."); - showLockScreen = true; // Show lock screen when locking + showLockScreen = true; console.log("[SessionStore] Set showLockScreen to true"); lockTransition = "locking"; isAnimatingLock = true; lockStateChanging(true); - // Complete animation after 300ms lockTimer.interval = 300; lockTimer.targetLocked = true; lockTimer.restart(); } - /** - * @brief Unlock the session with animation - */ function unlock() { console.log("[SessionStore] unlock() called - current isLocked:", isLocked); - // If an unlock transition is already in progress, ignore duplicate calls. - // (Multiple UI components may react to the same unlock trigger.) if (isAnimatingLock && lockTransition === "unlocking") { console.log("[SessionStore] Unlock already in progress, ignoring duplicate"); showLockScreen = false; @@ -111,34 +43,24 @@ QtObject { return; } if (!isLocked) { - // Common case during grace-period: session is already unlocked but lock screen is showing - // (e.g. screen turned on, or user tapped a notification on the lock screen). - // In this case we still need to dismiss the lock screen. console.log("[SessionStore] Already unlocked - dismissing lock screen"); showLockScreen = false; isOnLockScreen = false; return; } console.log("[SessionStore] Unlocking session..."); - showLockScreen = false; // Hide lock screen when unlocking + showLockScreen = false; lockTransition = "unlocking"; isAnimatingLock = true; lockStateChanging(false); - // Mark session as valid for 5 minutes (grace period) sessionValidUntil = Date.now() + (5 * 60 * 1000); console.log("[SessionStore] Grace period set until:", new Date(sessionValidUntil).toLocaleTimeString()); - // Complete animation after 300ms lockTimer.interval = 300; lockTimer.targetLocked = false; lockTimer.restart(); } - /** - * @brief Check if current session is still valid (grace period) - * @returns {bool} True if session valid, false if PIN required - */ function checkSession() { - // Grace period: unlock without PIN if < 5 minutes since last unlock var now = Date.now(); var isValid = sessionValidUntil > now; if (!isValid) @@ -147,10 +69,6 @@ QtObject { return isValid; } - /** - * @brief Show lock screen (without locking session) - * Used when returning from screen-off with valid grace period - */ function showLock() { console.log("[SessionStore] Showing lock screen (session may still be unlocked)"); showLockScreen = true; diff --git a/shell/qml/stores/SystemControlStore.qml b/shell/qml/stores/SystemControlStore.qml index ebdcc8981..4cf3cec02 100644 --- a/shell/qml/stores/SystemControlStore.qml +++ b/shell/qml/stores/SystemControlStore.qml @@ -2,16 +2,6 @@ pragma Singleton import QtQuick QtObject { - // Don't set brightness property here - let the binding update from DisplayManager - // Don't set volume property here - let the binding update from AudioManager - // Bindings automatically update from NetworkManagerCpp/BluetoothManagerCpp properties and Binding objects above - // No need for Connections - the Binding objects with restoreMode handle everything - // Binding automatically updates from PowerManager.isPowerSaveMode (line 32) - // Don't assign - let the binding update automatically - // Don't assign - let the binding update automatically - // Don't assign - let the binding update automatically - // Default hotspot config - can be customized later - id: systemControl property bool isWifiOn: typeof NetworkManagerCpp !== 'undefined' && NetworkManagerCpp ? NetworkManagerCpp.wifiEnabled : true @@ -21,32 +11,20 @@ QtObject { property bool isFlashlightOn: (typeof FlashlightManagerCpp !== "undefined" && FlashlightManagerCpp) ? FlashlightManagerCpp.enabled : false property bool isCellularOn: typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp ? ModemManagerCpp.modemEnabled : false property bool isCellularDataOn: typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp ? ModemManagerCpp.dataEnabled : false - property bool isDndMode: AudioManager.dndEnabled - property bool isAlarmOn: typeof AlarmManager !== 'undefined' ? (AlarmManager.hasActiveAlarm || _hasEnabledAlarm()) : false + property bool isDndMode: typeof SettingsManagerCpp !== 'undefined' && SettingsManagerCpp ? SettingsManagerCpp.dndEnabled : false + property bool isAlarmOn: typeof AlarmManagerCpp !== 'undefined' && AlarmManagerCpp ? (AlarmManagerCpp.hasActiveAlarm || AlarmManagerCpp.hasEnabledAlarm) : false property bool isAutoBrightnessOn: (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? DisplayManagerCpp.autoBrightnessEnabled : false property bool isLocationOn: typeof LocationManager !== 'undefined' ? LocationManager.active : false property bool isHotspotOn: typeof NetworkManagerCpp !== 'undefined' ? NetworkManagerCpp.isHotspotActive() : false property bool isVibrationOn: typeof HapticManager !== 'undefined' ? HapticManager.enabled : true property bool isNightLightOn: (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? DisplayManagerCpp.nightLightEnabled : false - property int brightness: 50 // Managed by binding below - property int volume: 50 // Managed by binding below - // Two-way bindings with restore mode (as properties) + property int brightness: 50 + property int volume: 50 property Binding brightnessBinding property Binding volumeBinding property bool isLowPowerMode: (typeof PowerManagerService !== "undefined" && PowerManagerService) ? PowerManagerService.isPowerSaveMode : false - // Two-way binding for rotation lock (as property) property Binding rotationLockBinding - function _hasEnabledAlarm() { - if (typeof AlarmManager !== 'undefined' && AlarmManager.alarms) { - for (var i = 0; i < AlarmManager.alarms.length; i++) { - if (AlarmManager.alarms[i].enabled) - return true; - } - } - return false; - } - function toggleWifi() { if (typeof NetworkManagerCpp !== 'undefined' && NetworkManagerCpp) NetworkManagerCpp.toggleWifi(); @@ -106,7 +84,10 @@ QtObject { function toggleDndMode() { var newMode = !isDndMode; - AudioManager.setDoNotDisturb(newMode); + if (typeof SettingsManagerCpp !== 'undefined' && SettingsManagerCpp) + SettingsManagerCpp.dndEnabled = newMode; + if (typeof AudioPolicyControllerCpp !== 'undefined' && AudioPolicyControllerCpp) + AudioPolicyControllerCpp.setDoNotDisturb(newMode); Logger.info("SystemControl", "DND mode toggled to: " + newMode); } @@ -168,7 +149,6 @@ QtObject { function captureScreenshot() { Logger.info("SystemControl", "Screenshot captured"); - // Screenshot logic will be handled by ScreenshotService if (typeof ScreenshotService !== 'undefined') ScreenshotService.captureScreen(); } @@ -183,7 +163,10 @@ QtObject { function setVolume(value) { var clamped = Math.max(0, Math.min(100, value)); - AudioManager.setVolume(clamped / 100); + if (typeof AudioManagerCpp !== 'undefined' && AudioManagerCpp) + AudioManagerCpp.setVolume(clamped / 100); + else if (typeof AudioPolicyControllerCpp !== 'undefined' && AudioPolicyControllerCpp) + AudioPolicyControllerCpp.setMasterVolume(clamped / 100); Logger.debug("SystemControl", "Volume: " + clamped); } @@ -219,7 +202,7 @@ QtObject { volumeBinding: Binding { target: systemControl property: "volume" - value: Math.round(AudioManager.volume * 100) + value: (typeof AudioManagerCpp !== 'undefined' && AudioManagerCpp) ? Math.round(AudioManagerCpp.volume * 100) : 50 restoreMode: Binding.RestoreBinding } diff --git a/shell/qml/stores/SystemStatusStore.qml b/shell/qml/stores/SystemStatusStore.qml index f84ae21e0..d5c5cd926 100644 --- a/shell/qml/stores/SystemStatusStore.qml +++ b/shell/qml/stores/SystemStatusStore.qml @@ -2,11 +2,6 @@ pragma Singleton import QtQuick QtObject { - // batteryLevel and isCharging are already bound directly (line 8-9) - // No need to update them here - direct bindings are more efficient - // notificationCount is already bound to NotificationService.unreadCount at line 38 - // Switch to 1s updates after first tick - id: systemStatus property int batteryLevel: (typeof PowerManagerService !== "undefined" && PowerManagerService) ? PowerManagerService.batteryLevel : 0 @@ -40,20 +35,17 @@ QtObject { property real storageUsed: 45.2 property real storageTotal: 128 property date currentTime: new Date() - property string timeString // Updated by timer, initialized on first tick - property string dateString // Updated by timer, initialized on first tick + property string timeString + property string dateString property var notifications: [] property int notificationCount: NotificationService.unreadCount property Timer updateTimer - // Listen for time format changes using Connections (proper lifetime management) property Connections timeFormatConnection property Connections powerManagerConnections property Connections notificationServiceConnections function updateTime() { systemStatus.currentTime = new Date(); - // In strict app process isolation, apps may import MarathonOS.Shell QML but won't have - // shell-only context properties like SettingsManagerCpp. Guard to avoid ReferenceError spam. var tf = (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp) ? SettingsManagerCpp.timeFormat : "24h"; systemStatus.timeString = Qt.formatTime(systemStatus.currentTime, tf === "24h" ? "HH:mm" : "h:mm AP"); systemStatus.dateString = Qt.formatDate(systemStatus.currentTime, "dddd, MMMM d"); @@ -85,7 +77,7 @@ QtObject { } updateTimer: Timer { - interval: 16 // 60fps for immediate first update + interval: 16 running: true repeat: true triggeredOnStart: true diff --git a/shell/qml/stores/UIStore.qml b/shell/qml/stores/UIStore.qml index db73dcadf..d1aa06436 100644 --- a/shell/qml/stores/UIStore.qml +++ b/shell/qml/stores/UIStore.qml @@ -15,7 +15,7 @@ QtObject { property bool searchOpen: false property bool shareSheetOpen: false property bool clipboardManagerOpen: false - property var shellRef: null // Reference to shell for dynamic sizing + property var shellRef: null signal showNotificationToast(var notification) signal showSystemHUD(string type, real value) @@ -61,7 +61,7 @@ QtObject { if (shellRef) quickSettingsHeight = shellRef.maxQuickSettingsHeight; else - quickSettingsHeight = 1000; // Fallback + quickSettingsHeight = 1000; Logger.state("UIStore", "quickSettings closed", "open"); } @@ -83,14 +83,12 @@ QtObject { currentAppName = appName; currentAppIcon = appIcon; appWindowOpen = true; - // Also set settingsOpen for Settings app (for layout decisions) if (appId === "settings") settingsOpen = true; } function closeApp() { appWindowOpen = false; - // Also clear settingsOpen if it was Settings if (currentAppId === "settings") settingsOpen = false; @@ -101,23 +99,19 @@ QtObject { function minimizeApp() { appWindowOpen = false; - // Also clear settingsOpen if it was Settings if (currentAppId === "settings") settingsOpen = false; - // CRITICAL: Clear currentAppId so restoring the same app later triggers onCurrentAppIdChanged currentAppId = ""; currentAppName = ""; currentAppIcon = ""; } function restoreApp(appId, appName, appIcon) { - // CRITICAL: Set appWindowOpen BEFORE currentAppId so signal handlers see correct state appWindowOpen = true; currentAppName = appName; currentAppIcon = appIcon; - currentAppId = appId; // Set last so onCurrentAppIdChanged fires with appWindowOpen=true - // Also set settingsOpen for Settings app + currentAppId = appId; if (appId === "settings") settingsOpen = true; } @@ -133,8 +127,6 @@ QtObject { } function minimizeSettings() { - // Settings runs as a regular app window; minimizing it must clear the app window state - // (otherwise the detached AppWindow container can remain visible and cover ActiveFrames). minimizeApp(); } diff --git a/shell/qml/stores/WallpaperStore.qml b/shell/qml/stores/WallpaperStore.qml index 0aa08e2a9..21b076a20 100644 --- a/shell/qml/stores/WallpaperStore.qml +++ b/shell/qml/stores/WallpaperStore.qml @@ -4,9 +4,6 @@ import QtQuick QtObject { id: wallpaperStore - // NOTE: Do NOT bind directly to SettingsManagerCpp.wallpaperPath. - // We want SettingsManagerCpp to be the source of truth, but the store must remain writable - // (Settings app sets wallpapers). Direct bindings + writebacks create binding loops. property string currentWallpaper: "qrc:/wallpapers/wallpaper.jpg" property string path: currentWallpaper property bool isDark: true @@ -62,7 +59,6 @@ QtObject { function setWallpaper(newPath, newIsDark) { if (!newPath || newPath === currentWallpaper) { - // Still update dark flag if the caller provided it (e.g., same wallpaper tapped). if (newIsDark !== undefined) isDark = newIsDark; @@ -72,7 +68,6 @@ QtObject { if (newIsDark !== undefined) isDark = newIsDark; - // Persist immediately via SettingsManager (in-shell or runner proxy). if (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp && SettingsManagerCpp.wallpaperPath !== newPath) SettingsManagerCpp.wallpaperPath = newPath; } @@ -90,7 +85,6 @@ QtObject { if (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp && SettingsManagerCpp.wallpaperPath) currentWallpaper = SettingsManagerCpp.wallpaperPath; - // Keep store in sync with SettingsManager changes coming from elsewhere (e.g. runner → DBus). if (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp && SettingsManagerCpp.wallpaperPathChanged) SettingsManagerCpp.wallpaperPathChanged.connect(function () { if (SettingsManagerCpp && SettingsManagerCpp.wallpaperPath && currentWallpaper !== SettingsManagerCpp.wallpaperPath) From 2657d3aac0fb3d0a9aec053a689c8b9cc59d7b09 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:31:06 +0000 Subject: [PATCH 04/28] chore(ui): refine MarathonUI components and lint setup Update MarathonUI QML components and build configuration to match current layouts, styling, and lint expectations. --- marathon-ui/CMakeLists.txt | 86 +++++++++++++++++++++ marathon-ui/Containers/MApp.qml | 5 -- marathon-ui/Containers/MCard.qml | 14 ++-- marathon-ui/Containers/MFormCard.qml | 1 - marathon-ui/Containers/MListItem.qml | 3 +- marathon-ui/Containers/MPanel.qml | 1 - marathon-ui/Containers/MPullToRefresh.qml | 13 +--- marathon-ui/Containers/MScrollView.qml | 15 +--- marathon-ui/Containers/MSectionHeader.qml | 1 - marathon-ui/Containers/MSwipeDelegate.qml | 9 +-- marathon-ui/Controls/MRadioGroup.qml | 49 +++++++++--- marathon-ui/Controls/MSlider.qml | 4 +- marathon-ui/Controls/MToggle.qml | 15 +--- marathon-ui/Core/Icon.qml | 8 +- marathon-ui/Core/MAppIcon.qml | 25 +++--- marathon-ui/Core/MButton.qml | 10 +-- marathon-ui/Core/MCircularIconButton.qml | 6 +- marathon-ui/Core/MIconButton.qml | 2 +- marathon-ui/Effects/MHaptics.qml | 38 +-------- marathon-ui/Effects/MRipple.qml | 7 +- marathon-ui/Feedback/MActivityIndicator.qml | 3 - marathon-ui/Modals/MConfirmDialog.qml | 1 - marathon-ui/Modals/MModal.qml | 1 - marathon-ui/Modals/MSheet.qml | 14 +++- marathon-ui/Navigation/MPageIndicator.qml | 1 - marathon-ui/Navigation/MSwipeView.qml | 7 -- marathon-ui/Navigation/MTopBar.qml | 1 - marathon-ui/Theme/MColors.qml | 6 +- marathon-ui/Theme/MElevation.qml | 16 ++-- marathon-ui/Theme/MMotion.qml | 17 ++-- marathon-ui/Theme/MRadius.qml | 1 + marathon-ui/Theme/MSpacing.qml | 23 +++--- marathon-ui/Theme/MTypography.qml | 27 +++---- 33 files changed, 212 insertions(+), 218 deletions(-) diff --git a/marathon-ui/CMakeLists.txt b/marathon-ui/CMakeLists.txt index 57a52dc2a..de16e5fa3 100644 --- a/marathon-ui/CMakeLists.txt +++ b/marathon-ui/CMakeLists.txt @@ -52,6 +52,92 @@ add_subdirectory(Modals) add_subdirectory(Effects) add_subdirectory(Containers) # Last - depends on Controls +set(MARATHON_QML_IMPORT_PATH + "${CMAKE_BINARY_DIR}/qmllint" + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/shell/qml" +) +set(MARATHON_UI_QML_TARGETS + marathon-ui-theme + marathon-ui-core + marathon-ui-controls + marathon-ui-lists + marathon-ui-navigation + marathon-ui-feedback + marathon-ui-modals + marathon-ui-effects + marathon-ui-containers +) +foreach(target IN LISTS MARATHON_UI_QML_TARGETS) + if(TARGET ${target}) + set_property(TARGET ${target} APPEND PROPERTY QT_QML_IMPORT_PATH "${MARATHON_QML_IMPORT_PATH}") + endif() +endforeach() + +find_program(QMLPLUGINDUMP_EXECUTABLE + NAMES qmlplugindump-qt6 qmlplugindump + PATHS /usr/lib64/qt6/bin /usr/bin /usr/local/bin +) +find_program(PYTHON_EXECUTABLE + NAMES python3 python + PATHS /usr/bin /usr/local/bin +) +if(QMLPLUGINDUMP_EXECUTABLE) + set(QMLDUMP_ROOT "${CMAKE_BINARY_DIR}/qmldump") + set(QMLDUMP_IMPORT_PATH "${QMLDUMP_ROOT}:/usr/lib64/qt6/qml") + add_custom_target(marathon-ui-qmltypes + COMMAND ${CMAKE_COMMAND} -E make_directory ${QMLDUMP_ROOT}/MarathonUI + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/MarathonUI ${QMLDUMP_ROOT}/MarathonUI + COMMAND ${CMAKE_COMMAND} -E make_directory ${QMLDUMP_ROOT}/MarathonOS + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/shell/qml/MarathonOS/Shell ${QMLDUMP_ROOT}/MarathonOS/Shell + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Theme/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Theme/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Core/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Core/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Controls/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Controls/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Lists/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Lists/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Navigation/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Navigation/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Feedback/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Feedback/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Modals/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Modals/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Effects/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Effects/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonUI/Containers/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonUI/Containers/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLDUMP_ROOT}/MarathonOS/Shell/qmldir -DOUT=${QMLDUMP_ROOT}/MarathonOS/Shell/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldir.cmake + COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/build_qml_dependencies.py ${QMLDUMP_ROOT}/MarathonUI ${QMLDUMP_ROOT}/dependencies.json + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Theme 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Theme/marathon-ui-theme.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Core 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Core/marathon-ui-core.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Controls 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Controls/marathon-ui-controls.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Lists 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Lists/marathon-ui-lists.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Navigation 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Navigation/marathon-ui-navigation.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Feedback 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Feedback/marathon-ui-feedback.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Modals 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Modals/marathon-ui-modals.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Effects 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Effects/marathon-ui-effects.qmltypes + COMMAND ${CMAKE_COMMAND} -E env PATH=/usr/lib64/qt6/libexec:$ENV{PATH} QML2_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} QML_IMPORT_PATH=${QMLDUMP_IMPORT_PATH} ${QMLPLUGINDUMP_EXECUTABLE} -noinstantiate -dependencies ${QMLDUMP_ROOT}/dependencies.json MarathonUI.Containers 2.0 ${QMLDUMP_ROOT} -output ${CMAKE_BINARY_DIR}/MarathonUI/Containers/marathon-ui-containers.qmltypes + DEPENDS marathon-shell ${MARATHON_UI_QML_TARGETS} + ) + foreach(target IN LISTS MARATHON_UI_QML_TARGETS) + if(TARGET ${target}_qmllint) + add_dependencies(${target}_qmllint marathon-ui-qmltypes) + endif() + endforeach() +endif() + +set(QMLLINT_MODULE_ROOT "${CMAKE_BINARY_DIR}/qmllint") +add_custom_target(marathon-ui-qmllint-modules + COMMAND ${CMAKE_COMMAND} -E make_directory ${QMLLINT_MODULE_ROOT}/MarathonUI + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/MarathonUI ${QMLLINT_MODULE_ROOT}/MarathonUI + COMMAND ${CMAKE_COMMAND} -E make_directory ${QMLLINT_MODULE_ROOT}/MarathonOS + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/shell/qml/MarathonOS/Shell ${QMLLINT_MODULE_ROOT}/MarathonOS/Shell + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Theme/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Theme/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Core/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Core/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Controls/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Controls/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Lists/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Lists/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Navigation/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Navigation/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Feedback/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Feedback/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Modals/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Modals/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Effects/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Effects/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonUI/Containers/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonUI/Containers/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + COMMAND ${CMAKE_COMMAND} -DIN=${QMLLINT_MODULE_ROOT}/MarathonOS/Shell/qmldir -DOUT=${QMLLINT_MODULE_ROOT}/MarathonOS/Shell/qmldir -P ${CMAKE_SOURCE_DIR}/cmake/StripQmldirForLint.cmake + DEPENDS marathon-shell ${MARATHON_UI_QML_TARGETS} +) + message(STATUS "==========================================") message(STATUS "MarathonUI Build Configuration") message(STATUS "Version: ${PROJECT_VERSION}") diff --git a/marathon-ui/Containers/MApp.qml b/marathon-ui/Containers/MApp.qml index 2c97a523d..0f8144dcf 100644 --- a/marathon-ui/Containers/MApp.qml +++ b/marathon-ui/Containers/MApp.qml @@ -143,7 +143,6 @@ Item { } } - // Signal emitted when app wants to register with lifecycle manager signal requestRegister(string appId, var appInstance) signal requestUnregister(string appId) @@ -162,12 +161,10 @@ Item { appStarted(); appResumed(); - // Try direct registration first (works in shell context) if (typeof AppLifecycleManager !== 'undefined' && appId) { console.log(" Calling AppLifecycleManager.registerApp() directly"); AppLifecycleManager.registerApp(appId, root); } else { - // Fallback: emit signal for external registration (works in app loader context) console.log(" ℹ AppLifecycleManager not available, emitting requestRegister signal"); requestRegister(appId, root); } @@ -178,11 +175,9 @@ Item { appWillTerminate(); appClosed(); - // Try direct unregistration first if (typeof AppLifecycleManager !== 'undefined' && appId) { AppLifecycleManager.unregisterApp(appId); } else { - // Fallback: emit signal requestUnregister(appId); } } diff --git a/marathon-ui/Containers/MCard.qml b/marathon-ui/Containers/MCard.qml index 90cd73cc1..c350b5c55 100644 --- a/marathon-ui/Containers/MCard.qml +++ b/marathon-ui/Containers/MCard.qml @@ -28,9 +28,8 @@ Rectangle { border.width: borderWidth border.color: MElevation.getBorderOuter(elevation) - scale: pressed && interactive ? 0.96 : 1.0 // Press only, no hover + scale: pressed && interactive ? 0.96 : 1.0 - // Performant shadow using layered rectangles Rectangle { id: shadowLayer anchors.fill: parent @@ -39,7 +38,7 @@ Rectangle { anchors.rightMargin: -shadowMargin1 anchors.bottomMargin: -shadowMargin4 z: -1 - radius: parent.radius + radius: root.radius opacity: 0.4 gradient: Gradient { GradientStop { @@ -57,13 +56,12 @@ Rectangle { } } - // Additional crisp shadow layer for more depth Rectangle { anchors.fill: parent anchors.topMargin: shadowMargin1 anchors.bottomMargin: -shadowMargin1 z: -2 - radius: parent.radius + radius: root.radius color: Qt.rgba(0, 0, 0, 0.8) opacity: 0.3 } @@ -82,21 +80,19 @@ Rectangle { } } - // Inner highlight border Rectangle { anchors.fill: parent anchors.margins: shadowMargin1 - radius: parent.radius > shadowMargin1 ? parent.radius - shadowMargin1 : 0 + radius: root.radius > shadowMargin1 ? root.radius - shadowMargin1 : 0 color: "transparent" border.width: borderWidth border.color: MElevation.getBorderInner(elevation) } - // Secondary inner border for extra depth Rectangle { anchors.fill: parent anchors.margins: shadowMargin2 - radius: parent.radius > shadowMargin2 ? parent.radius - shadowMargin2 : 0 + radius: root.radius > shadowMargin2 ? root.radius - shadowMargin2 : 0 color: "transparent" border.width: borderWidth border.color: Qt.rgba(1, 1, 1, 0.02) diff --git a/marathon-ui/Containers/MFormCard.qml b/marathon-ui/Containers/MFormCard.qml index 1d8eb9257..5f5d1cae4 100644 --- a/marathon-ui/Containers/MFormCard.qml +++ b/marathon-ui/Containers/MFormCard.qml @@ -23,7 +23,6 @@ Rectangle { Accessible.name: title Accessible.description: description - // Performant shadow Rectangle { anchors.fill: parent anchors.topMargin: 2 diff --git a/marathon-ui/Containers/MListItem.qml b/marathon-ui/Containers/MListItem.qml index ffb6b2cdf..306fadc0f 100644 --- a/marathon-ui/Containers/MListItem.qml +++ b/marathon-ui/Containers/MListItem.qml @@ -9,7 +9,7 @@ Rectangle { required property string title property string subtitle: "" property string time: "" - property int animationIndex: 0 // For staggered entrance animation + property int animationIndex: 0 property bool enableEntrance: true signal clicked @@ -21,7 +21,6 @@ Rectangle { property bool pressed: false property real entranceProgress: enableEntrance ? 0 : 1 - // Staggered entrance animation using transform instead of y position opacity: entranceProgress transform: Translate { y: (1 - entranceProgress) * 20 diff --git a/marathon-ui/Containers/MPanel.qml b/marathon-ui/Containers/MPanel.qml index 33abc60cc..0e8c4ac9f 100644 --- a/marathon-ui/Containers/MPanel.qml +++ b/marathon-ui/Containers/MPanel.qml @@ -32,7 +32,6 @@ Rectangle { } } - // Performant shadow using simple rectangle (60fps on PinePhone!) Rectangle { anchors.fill: parent anchors.topMargin: 1 diff --git a/marathon-ui/Containers/MPullToRefresh.qml b/marathon-ui/Containers/MPullToRefresh.qml index abf2e2898..2bf852918 100644 --- a/marathon-ui/Containers/MPullToRefresh.qml +++ b/marathon-ui/Containers/MPullToRefresh.qml @@ -1,4 +1,5 @@ import QtQuick +import MarathonUI.Feedback import MarathonUI.Theme import MarathonUI.Effects @@ -6,7 +7,7 @@ Item { id: root property alias contentItem: contentFlickable - property real threshold: 80 // Distance to trigger refresh + property real threshold: 80 property bool refreshing: false property bool enabled: true @@ -14,7 +15,6 @@ Item { anchors.fill: parent - // Pull-to-refresh indicator Item { id: indicator anchors.horizontalCenter: parent.horizontalCenter @@ -23,7 +23,6 @@ Item { height: 40 visible: contentFlickable.contentY < 0 || refreshing - // Spinner MActivityIndicator { anchors.centerIn: parent size: 32 @@ -31,7 +30,6 @@ Item { color: MColors.marathonTeal } - // Pull indicator (when not refreshing) Rectangle { visible: !root.refreshing anchors.centerIn: parent @@ -43,7 +41,6 @@ Item { border.color: MColors.marathonTeal opacity: Math.min(-contentFlickable.contentY / threshold, 1.0) - // Arrow Canvas { anchors.fill: parent opacity: parent.opacity @@ -56,7 +53,6 @@ Item { ctx.lineCap = "round"; ctx.lineJoin = "round"; - // Down arrow ctx.beginPath(); ctx.moveTo(width * 0.3, height * 0.4); ctx.lineTo(width * 0.5, height * 0.6); @@ -67,7 +63,6 @@ Item { } } - // Main content area Flickable { id: contentFlickable anchors.fill: parent @@ -76,12 +71,10 @@ Item { boundsMovement: Flickable.StopAtBounds boundsBehavior: Flickable.DragAndOvershootBounds - // Enable pull-to-refresh behavior onContentYChanged: { if (!root.enabled || root.refreshing) return; - // Trigger refresh when threshold exceeded and released if (contentY < -threshold && !dragging && atYBeginning) { root.refreshing = true; if (MHaptics.enabled) @@ -93,11 +86,9 @@ Item { Column { id: contentColumn width: parent.width - // Content goes here as children } } - // Reset refreshing state function stopRefreshing() { root.refreshing = false; } diff --git a/marathon-ui/Containers/MScrollView.qml b/marathon-ui/Containers/MScrollView.qml index d30f15e60..e1cd5b47d 100644 --- a/marathon-ui/Containers/MScrollView.qml +++ b/marathon-ui/Containers/MScrollView.qml @@ -13,20 +13,17 @@ Flickable { maximumFlickVelocity: 2500 boundsBehavior: Flickable.DragAndOvershootBounds - // API for Global Input Handler readonly property bool isScrollable: true function scrollBy(pixels) { - flick(0, pixels * 5); // Multiply for better feel + flick(0, pixels * 5); } - // Enable mouse wheel scrolling WheelHandler { onWheel: event => { root.flick(0, -event.angleDelta.y * 5); } } - // Enable Keyboard Scrolling (Arrow Keys, Page Up/Down) focus: true Keys.onUpPressed: root.flick(0, 500) Keys.onDownPressed: root.flick(0, -500) @@ -47,30 +44,26 @@ Flickable { active: root.moving || root.flicking || edgeScrollArea.containsMouse } - // Software Edge Scrolling for Q20 Trackpad - // Allows scrolling by moving cursor along the right edge MouseArea { id: edgeScrollArea anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom - width: 20 // Active zone width + width: 20 hoverEnabled: true preventStealing: true - z: 100 // Ensure it's on top + z: 100 property real lastY: 0 onEntered: { lastY = mouseY; - vbar.active = true; // Show scrollbar when entering edge + vbar.active = true; } onPositionChanged: { var delta = mouseY - lastY; if (Math.abs(delta) > 2) { - // Threshold to prevent jitter - // Multiply delta for faster scrolling root.flick(0, -delta * 100); lastY = mouseY; } diff --git a/marathon-ui/Containers/MSectionHeader.qml b/marathon-ui/Containers/MSectionHeader.qml index 433ccbf06..c2b3a2721 100644 --- a/marathon-ui/Containers/MSectionHeader.qml +++ b/marathon-ui/Containers/MSectionHeader.qml @@ -9,7 +9,6 @@ Rectangle { height: 44 color: MColors.glassHeader - // No blur - clean and crisp border.width: 1 border.color: MColors.borderGlass diff --git a/marathon-ui/Containers/MSwipeDelegate.qml b/marathon-ui/Containers/MSwipeDelegate.qml index fa45a3d21..f51391978 100644 --- a/marathon-ui/Containers/MSwipeDelegate.qml +++ b/marathon-ui/Containers/MSwipeDelegate.qml @@ -9,7 +9,7 @@ Item { property alias contentItem: contentContainer.data property list leftActions property list rightActions - property real threshold: width * 0.3 // 30% swipe to trigger + property real threshold: width * 0.3 property bool hapticEnabled: true signal leftActionTriggered(int index) @@ -21,7 +21,6 @@ Item { height: 88 clip: true - // Background actions (left side) Row { id: leftActionsRow anchors.left: parent.left @@ -58,7 +57,6 @@ Item { } } - // Background actions (right side) Row { id: rightActionsRow anchors.right: parent.right @@ -96,7 +94,6 @@ Item { } } - // Main content (swipeable) Rectangle { id: contentContainer anchors.top: parent.top @@ -114,7 +111,6 @@ Item { } } - // Drag area for swiping MouseArea { id: dragArea anchors.fill: parent @@ -134,15 +130,12 @@ Item { var swipeDistance = contentContainer.x - startX; var absDistance = Math.abs(swipeDistance); - // Snap to actions if threshold exceeded if (absDistance > root.threshold) { if (swipeDistance > 0 && leftActions.length > 0) { - // Swiped right - show left actions contentContainer.x = leftActionsRow.width; if (root.hapticEnabled) MHaptics.light(); } else if (swipeDistance < 0 && rightActions.length > 0) { - // Swiped left - show right actions contentContainer.x = -rightActionsRow.width; if (root.hapticEnabled) MHaptics.light(); diff --git a/marathon-ui/Controls/MRadioGroup.qml b/marathon-ui/Controls/MRadioGroup.qml index ac8daafe4..494ee4449 100644 --- a/marathon-ui/Controls/MRadioGroup.qml +++ b/marathon-ui/Controls/MRadioGroup.qml @@ -8,11 +8,11 @@ Column { property variant selectedValue: undefined property int selectedIndex: -1 property alias options: optionsRepeater.model - property alias spacing: root.spacing + property int spacingValue: MSpacing.sm signal selectionChanged(variant value, int index) - spacing: MSpacing.sm + spacing: spacingValue Accessible.role: Accessible.RadioButton Accessible.name: "Radio button group" @@ -40,19 +40,46 @@ Column { } function selectByIndex(index) { - if (index >= 0 && index < optionsRepeater.count) { - var item = optionsRepeater.itemAt(index); - if (item) { - item.select(); - } - } + if (index < 0 || index >= optionsRepeater.count) + return; + + var data = optionsRepeater.model; + var entry = data; + if (data && typeof data.get === "function") + entry = data.get(index); + else if (Array.isArray(data)) + entry = data[index]; + else if (data && data.length !== undefined) + entry = data[index]; + + var value = entry; + if (entry && typeof entry === "object" && entry.value !== undefined) + value = entry.value; + + root.selectedIndex = index; + root.selectedValue = value; + root.selectionChanged(value, index); } function selectByValue(value) { + var data = optionsRepeater.model; for (var i = 0; i < optionsRepeater.count; i++) { - var item = optionsRepeater.itemAt(i); - if (item && item.value === value) { - item.select(); + var entry = data; + if (data && typeof data.get === "function") + entry = data.get(i); + else if (Array.isArray(data)) + entry = data[i]; + else if (data && data.length !== undefined) + entry = data[i]; + + var entryValue = entry; + if (entry && typeof entry === "object" && entry.value !== undefined) + entryValue = entry.value; + + if (entryValue === value) { + root.selectedIndex = i; + root.selectedValue = entryValue; + root.selectionChanged(entryValue, i); return; } } diff --git a/marathon-ui/Controls/MSlider.qml b/marathon-ui/Controls/MSlider.qml index 1645c4fe4..f3d78f023 100644 --- a/marathon-ui/Controls/MSlider.qml +++ b/marathon-ui/Controls/MSlider.qml @@ -75,7 +75,6 @@ Item { } } - // Inner highlight Rectangle { anchors.fill: parent anchors.margins: handleInnerMargin @@ -86,14 +85,13 @@ Item { } } - // Extended touch target for easier grabbing Rectangle { id: touchTarget x: handle.x - touchTargetOffset anchors.verticalCenter: parent.verticalCenter width: touchTargetSize height: touchTargetSize - color: "transparent" // Invisible + color: "transparent" } MouseArea { diff --git a/marathon-ui/Controls/MToggle.qml b/marathon-ui/Controls/MToggle.qml index a492468a3..fab2f3025 100644 --- a/marathon-ui/Controls/MToggle.qml +++ b/marathon-ui/Controls/MToggle.qml @@ -4,8 +4,6 @@ import MarathonUI.Theme import QtQuick Item { - // Thumb (handle) with proper MUIstyling - id: root property bool checked: false @@ -56,7 +54,6 @@ Item { toggle(); } - // Track background with proper MUIstyling Rectangle { id: track @@ -67,7 +64,6 @@ Item { border.color: root.checked ? MColors.marathonTealBright : MColors.borderGlass layer.enabled: false - // Performant subtle shadow (no GPU blur needed) Rectangle { anchors.fill: parent anchors.topMargin: shadowMargin @@ -80,7 +76,6 @@ Item { opacity: 0.3 } - // Inner border for dual-border depth Rectangle { anchors.fill: parent anchors.margins: innerMargin @@ -96,7 +91,6 @@ Item { } } - // Subtle inner glow when checked Rectangle { visible: root.checked anchors.fill: parent @@ -130,9 +124,6 @@ Item { } } - // IMPORTANT: - // Avoid `layer.effect` (MultiEffect) here. On some platforms/backends it can fail and - // the thumb disappears entirely, leaving only the track (what you reported). Rectangle { id: thumbOuter @@ -142,11 +133,10 @@ Item { height: thumbHeight radius: MRadius.md color: "transparent" - // Teal border ring for consistency + border.width: borderWidthThick border.color: Qt.rgba(0, 191 / 255, 165 / 255, 0.35) - // Simple shadow under the thumb (no shader/effect dependency) Rectangle { anchors.centerIn: parent width: thumbInnerWidth @@ -165,11 +155,10 @@ Item { width: thumbInnerWidth height: thumbInnerHeight radius: MRadius.md > innerMargin ? MRadius.md - innerMargin : 0 - // Outer border + border.width: borderWidth border.color: Qt.rgba(0, 0, 0, 0.15) - // Inner highlight for polish Rectangle { anchors.fill: parent anchors.margins: innerMargin diff --git a/marathon-ui/Core/Icon.qml b/marathon-ui/Core/Icon.qml index 58c9d3e5d..41bb43452 100644 --- a/marathon-ui/Core/Icon.qml +++ b/marathon-ui/Core/Icon.qml @@ -4,19 +4,15 @@ import QtQuick import QtQuick.Effects Text { - // Performance optimization: Text is much lighter than Image+Shader - // No layer.enabled needed for coloring! - id: root property string name: "" property int size: 24 - // Map icon name to glyph character text: Lucide.Glyphs[name] || "" font.family: lucideFont.name font.pixelSize: size - // Default color (can be overridden) + color: MColors.textPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -24,8 +20,6 @@ Text { FontLoader { id: lucideFont - // Keep this module self-contained: the Lucide font is packaged as a MarathonUI.Core resource. - // Using Qt.resolvedUrl makes it work from any process (shell or isolated runner). source: Qt.resolvedUrl("fonts/lucide.ttf") } } diff --git a/marathon-ui/Core/MAppIcon.qml b/marathon-ui/Core/MAppIcon.qml index 4da7e5cbb..c32e3da00 100644 --- a/marathon-ui/Core/MAppIcon.qml +++ b/marathon-ui/Core/MAppIcon.qml @@ -6,40 +6,35 @@ Item { property string source: "" property int size: 24 - property bool isMask: false // If true, image is used as a mask (not implemented here but kept for API compat) + property bool isMask: false property color color: MColors.textPrimary - // Logic to determine if source is an image path or an icon name + readonly property bool isImage: source.indexOf("/") >= 0 || source.indexOf("file:") >= 0 || source.indexOf("qrc:") >= 0 - // Check if this is an SVG file that should use LunaSVG + readonly property bool isSvg: source.endsWith(".svg") - // Get the actual image source - use lunasvg:// scheme for SVG files + readonly property int status: imageItem.status + readonly property string imageSource: { if (!isImage) return ""; - // For SVG files, use LunaSVG image provider for proper clipPath support if (isSvg) { - // The LunaSVG provider takes the path portion of the URL as `id`. - // Normalize common schemes so we don't accidentally create a new provider name - // (e.g. "image://lunasvg" + "qrc:/foo.svg" => "image://lunasvgqrc:/foo.svg"). var svgPath = source; if (svgPath.startsWith("file://")) svgPath = svgPath.substring("file://".length); - // absolute filesystem path + if (svgPath.startsWith("qrc:/")) svgPath = svgPath.substring("qrc:".length); - // qrc:/foo.svg -> /foo.svg + if (svgPath.startsWith(":/")) svgPath = svgPath.substring(1); - // :/foo.svg -> /foo.svg - // Ensure there is a '/' between provider name and path. + if (!svgPath.startsWith("/")) svgPath = "/" + svgPath; return "image://lunasvg" + svgPath; } - // For non-SVG files (PNG, etc.), ensure we have proper file:// prefix - // for absolute filesystem paths, otherwise Qt interprets them relative to qrc + if (source.startsWith("/") && !source.startsWith("file://")) return "file://" + source; @@ -49,7 +44,6 @@ Item { width: size height: size - // 1. Image Handler (for native apps / external paths) Image { id: imageItem @@ -63,7 +57,6 @@ Item { cache: true } - // 2. Font Icon Handler (for internal apps / Lucide names) Icon { anchors.centerIn: parent name: !root.isImage ? root.source : "" diff --git a/marathon-ui/Core/MButton.qml b/marathon-ui/Core/MButton.qml index 260cf9cd7..46a1d26fb 100644 --- a/marathon-ui/Core/MButton.qml +++ b/marathon-ui/Core/MButton.qml @@ -13,7 +13,7 @@ Item { property bool iconLeft: true property bool disabled: false property string state: "default" - property string shape: "rounded" // "rounded" or "circular" + property string shape: "rounded" signal clicked signal pressed @@ -58,8 +58,6 @@ Item { implicitWidth: contentRow.width + MSpacing.xl * 2 implicitHeight: MSpacing.touchTargetMin - // Always subtract glow offset to ensure containment and stability - // This prevents jumping when switching variants and clipping in tight layouts width: (root.width > 0 ? root.width : root.implicitWidth) - borderGlowOffset * 2 height: (root.height > 0 ? root.height : root.implicitHeight) - borderGlowOffset * 2 @@ -136,7 +134,7 @@ Item { visible: iconName !== "" && root.state === "default" name: iconName size: iconSize - color: variant === "primary" ? "#000000" : MColors.textPrimary // Black icon on teal + color: variant === "primary" ? "#000000" : MColors.textPrimary anchors.verticalCenter: parent.verticalCenter } @@ -146,12 +144,12 @@ Item { if (disabled) return MColors.textHint; if (variant === "primary") - return "#000000"; // Pure black for maximum contrast on teal + return "#000000"; if (variant === "secondary") return MColors.textPrimary; return MColors.textPrimary; } - font.pixelSize: MTypography.sizeBody // Larger for readability + font.pixelSize: MTypography.sizeBody font.weight: variant === "primary" ? MTypography.weightBold : MTypography.weightDemiBold font.family: MTypography.fontFamily anchors.verticalCenter: parent.verticalCenter diff --git a/marathon-ui/Core/MCircularIconButton.qml b/marathon-ui/Core/MCircularIconButton.qml index 899f6c426..597c569ea 100644 --- a/marathon-ui/Core/MCircularIconButton.qml +++ b/marathon-ui/Core/MCircularIconButton.qml @@ -13,7 +13,7 @@ Item { property color iconColor: variant === "primary" ? "#000000" : MColors.textOnAccent property color textColor: MColors.textPrimary property bool disabled: false - property string variant: "primary" // "primary" or "secondary" + property string variant: "primary" property int buttonSize: 62 signal clicked @@ -27,7 +27,6 @@ Item { implicitWidth: scaledButtonSize + (scaledBorderSize * 2) + Math.round(6 * scaleFactor) implicitHeight: scaledButtonSize + (scaledBorderSize * 2) + Math.round(6 * scaleFactor) - // Outer glow border (like action bar) Rectangle { anchors.centerIn: parent width: scaledButtonSize + (scaledBorderSize * 2) + Math.round(6 * scaleFactor) @@ -39,7 +38,6 @@ Item { opacity: variant === "primary" ? 1.0 : 0.6 } - // Main button Rectangle { id: mainButton anchors.centerIn: parent @@ -94,7 +92,6 @@ Item { } } - // Inner highlight border (for primary) Rectangle { visible: variant === "primary" anchors.fill: parent @@ -105,7 +102,6 @@ Item { border.color: Qt.rgba(1, 1, 1, 0.1) } - // Top highlight gradient (for primary) Rectangle { visible: variant === "primary" anchors.fill: parent diff --git a/marathon-ui/Core/MIconButton.qml b/marathon-ui/Core/MIconButton.qml index e7776a4da..42506b097 100644 --- a/marathon-ui/Core/MIconButton.qml +++ b/marathon-ui/Core/MIconButton.qml @@ -82,7 +82,7 @@ Item { Icon { name: iconName size: scaledIconSize - color: disabled ? MColors.textHint : (variant === "primary" ? "#000000" : iconColor) // Black icon on teal + color: disabled ? MColors.textHint : (variant === "primary" ? "#000000" : iconColor) anchors.centerIn: parent } diff --git a/marathon-ui/Effects/MHaptics.qml b/marathon-ui/Effects/MHaptics.qml index 7732aeae5..987d60a53 100644 --- a/marathon-ui/Effects/MHaptics.qml +++ b/marathon-ui/Effects/MHaptics.qml @@ -4,21 +4,16 @@ import QtQuick QtObject { id: root - // Haptic feedback intensity levels (0.0 - 1.0) readonly property real lightIntensity: 0.3 readonly property real mediumIntensity: 0.6 readonly property real heavyIntensity: 1 - // Haptic feedback durations (ms) + readonly property int shortDuration: 10 readonly property int mediumDuration: 20 readonly property int longDuration: 40 - // Enable/disable haptics globally + property bool enabled: false - /** - * Light haptic feedback - * Use for: Buttons, toggles, selection changes - */ function light() { if (!enabled) return; @@ -34,10 +29,6 @@ QtObject { selection(); } - /** - * Medium haptic feedback - * Use for: Confirmations, important actions, slider steps - */ function medium() { if (!enabled) return; @@ -49,10 +40,6 @@ QtObject { medium(); } - /** - * Heavy haptic feedback - * Use for: Errors, critical actions, notifications - */ function heavy() { if (!enabled) return; @@ -60,10 +47,6 @@ QtObject { console.log("[Haptics] Heavy tap"); } - /** - * Selection feedback (light, rapid) - * Use for: Scrolling through picker items, scrubbing sliders - */ function selection() { if (!enabled) return; @@ -71,11 +54,6 @@ QtObject { console.log("[Haptics] Selection"); } - /** - * Impact feedback with custom intensity - * @param intensity: 0.0 - 1.0 - * @param duration: milliseconds - */ function impact(intensity, duration) { if (!enabled) return; @@ -83,10 +61,6 @@ QtObject { console.log("[Haptics] Impact - intensity:", intensity, "duration:", duration); } - /** - * Success pattern (double light tap) - * Use for: Successful operations, confirmations - */ function success() { if (!enabled) return; @@ -97,10 +71,6 @@ QtObject { }); } - /** - * Error pattern (long heavy vibration) - * Use for: Failed operations, invalid input - */ function error() { if (!enabled) return; @@ -108,10 +78,6 @@ QtObject { impact(heavyIntensity, longDuration); } - /** - * Warning pattern (two medium taps) - * Use for: Warnings, important notices - */ function warning() { if (!enabled) return; diff --git a/marathon-ui/Effects/MRipple.qml b/marathon-ui/Effects/MRipple.qml index a8347a218..6af6f6945 100644 --- a/marathon-ui/Effects/MRipple.qml +++ b/marathon-ui/Effects/MRipple.qml @@ -25,10 +25,9 @@ Item { name: "active" when: root.active PropertyChanges { - target: rippleCircle - width: Math.max(root.width, root.height) * MMotion.rippleMaxRadius - height: width - opacity: 0 + rippleCircle.width: Math.max(root.width, root.height) * MMotion.rippleMaxRadius + rippleCircle.height: Math.max(root.width, root.height) * MMotion.rippleMaxRadius + rippleCircle.opacity: 0 } } diff --git a/marathon-ui/Feedback/MActivityIndicator.qml b/marathon-ui/Feedback/MActivityIndicator.qml index 6f38c2ae2..6b05166ee 100644 --- a/marathon-ui/Feedback/MActivityIndicator.qml +++ b/marathon-ui/Feedback/MActivityIndicator.qml @@ -12,14 +12,12 @@ Item { implicitWidth: size implicitHeight: size - // Modern arc-based spinner with gradient trail Shape { id: spinner anchors.centerIn: parent width: root.size height: root.size - // Main spinning arc with gradient ShapePath { strokeColor: "transparent" fillColor: "transparent" @@ -36,7 +34,6 @@ Item { } } - // Gradient overlay using multiple arcs with decreasing opacity Repeater { model: 8 diff --git a/marathon-ui/Modals/MConfirmDialog.qml b/marathon-ui/Modals/MConfirmDialog.qml index 73f425989..f9c6d5faa 100644 --- a/marathon-ui/Modals/MConfirmDialog.qml +++ b/marathon-ui/Modals/MConfirmDialog.qml @@ -53,7 +53,6 @@ Rectangle { border.width: 1 border.color: MColors.borderGlass - // Performant shadow for dialogs Rectangle { anchors.fill: parent anchors.topMargin: 8 diff --git a/marathon-ui/Modals/MModal.qml b/marathon-ui/Modals/MModal.qml index 1b8245e54..b1c9019a0 100644 --- a/marathon-ui/Modals/MModal.qml +++ b/marathon-ui/Modals/MModal.qml @@ -58,7 +58,6 @@ Rectangle { border.width: borderWidth border.color: MColors.borderGlass - // Performant shadow for modals (looks great, runs smooth on PinePhone) Rectangle { anchors.fill: parent anchors.topMargin: shadowTopMargin diff --git a/marathon-ui/Modals/MSheet.qml b/marathon-ui/Modals/MSheet.qml index c7e8ab0c6..ad277ef55 100644 --- a/marathon-ui/Modals/MSheet.qml +++ b/marathon-ui/Modals/MSheet.qml @@ -51,7 +51,6 @@ Rectangle { border.width: 1 border.color: MColors.borderGlass - // Performant shadow for sheets (upward shadow) Rectangle { anchors.fill: parent anchors.topMargin: -4 @@ -128,11 +127,20 @@ Rectangle { } } - function show() { + function open() { showing = true; } - function hide() { + function close() { showing = false; + closed(); + } + + function show() { + open(); + } + + function hide() { + close(); } } diff --git a/marathon-ui/Navigation/MPageIndicator.qml b/marathon-ui/Navigation/MPageIndicator.qml index e81950aaf..86af7af8e 100644 --- a/marathon-ui/Navigation/MPageIndicator.qml +++ b/marathon-ui/Navigation/MPageIndicator.qml @@ -36,7 +36,6 @@ Row { } } - // Inner highlight on active dot Rectangle { visible: index === root.currentIndex anchors.centerIn: parent diff --git a/marathon-ui/Navigation/MSwipeView.qml b/marathon-ui/Navigation/MSwipeView.qml index 066a1d781..69032a6b3 100644 --- a/marathon-ui/Navigation/MSwipeView.qml +++ b/marathon-ui/Navigation/MSwipeView.qml @@ -6,22 +6,18 @@ import MarathonUI.Effects SwipeView { id: root - // Custom properties property bool hapticEnabled: true property bool bounceEnabled: true property real bounceIntensity: 0.1 - // Signals signal pageChanged(int index) signal swipeStarted signal swipeFinished clip: true - // Spring physics for page transitions interactive: true - // Haptic feedback on page change onCurrentIndexChanged: { if (hapticEnabled && MHaptics.enabled) { MHaptics.light(); @@ -29,7 +25,6 @@ SwipeView { pageChanged(currentIndex); } - // Track swipe state onMovingChanged: { if (moving) { swipeStarted(); @@ -38,7 +33,6 @@ SwipeView { } } - // Custom page transition with spring physics Behavior on contentItem.x { enabled: !moving SpringAnimation { @@ -48,7 +42,6 @@ SwipeView { } } - // Rubber-band overscroll effect onContentItemChanged: { if (contentItem && bounceEnabled) { contentItem.boundsBehavior = Flickable.DragAndOvershootBounds; diff --git a/marathon-ui/Navigation/MTopBar.qml b/marathon-ui/Navigation/MTopBar.qml index 5ef40c8ae..f5f8ab80f 100644 --- a/marathon-ui/Navigation/MTopBar.qml +++ b/marathon-ui/Navigation/MTopBar.qml @@ -17,7 +17,6 @@ Rectangle { height: barHeight color: MColors.glassTitlebar - // Glass morphism without blur (blur effect would blur content UNDER the bar, not desired) border.width: borderWidth border.color: MColors.borderGlass diff --git a/marathon-ui/Theme/MColors.qml b/marathon-ui/Theme/MColors.qml index 724dafff8..b343fb74e 100644 --- a/marathon-ui/Theme/MColors.qml +++ b/marathon-ui/Theme/MColors.qml @@ -49,15 +49,16 @@ QtObject { readonly property color infoDim: "#2563EB" readonly property color background: bb10Black + readonly property color backgroundLight: bb10Elevated readonly property color surface: bb10Surface readonly property color elevated: bb10Elevated readonly property color text: textPrimary + readonly property color textInverse: bb10Black readonly property color accent: marathonTeal readonly property color accentBright: marathonTealBright readonly property color accentDark: marathonTealDark readonly property color border: borderGlass - // Pre-computed colors for gradients and effects (used frequently in UI) readonly property color marathonTealHoverGradient: Qt.rgba(0, 191 / 255, 165 / 255, 0.03) readonly property color marathonTealPressGradient: Qt.rgba(0, 191 / 255, 165 / 255, 0.12) readonly property color marathonTealGlowTop: Qt.rgba(0, 191 / 255, 165 / 255, 0.18) @@ -66,12 +67,10 @@ QtObject { readonly property color marathonTealBorder: Qt.rgba(0, 191 / 255, 165 / 255, 0.35) readonly property color marathonTealBorderHover: Qt.rgba(0, 191 / 255, 165 / 255, 0.4) - // Shadow colors (precomputed for performance) readonly property color shadowDefault: Qt.rgba(0, 0, 0, 0.4) readonly property color shadowStrong: Qt.rgba(0, 0, 0, 0.6) readonly property color shadowHeavy: Qt.rgba(0, 0, 0, 0.7) - // White overlays (used in gradients) readonly property color whiteOverlay02: Qt.rgba(1, 1, 1, 0.02) readonly property color whiteOverlay03: Qt.rgba(1, 1, 1, 0.03) readonly property color whiteOverlay04: Qt.rgba(1, 1, 1, 0.04) @@ -84,7 +83,6 @@ QtObject { readonly property color whiteOverlay30: Qt.rgba(1, 1, 1, 0.30) readonly property color whiteOverlay40: Qt.rgba(1, 1, 1, 0.40) - // Black overlays (used in shadows) readonly property color blackOverlay15: Qt.rgba(0, 0, 0, 0.15) readonly property color blackOverlay40: Qt.rgba(0, 0, 0, 0.4) } diff --git a/marathon-ui/Theme/MElevation.qml b/marathon-ui/Theme/MElevation.qml index f9aad226f..e42fd8b6e 100644 --- a/marathon-ui/Theme/MElevation.qml +++ b/marathon-ui/Theme/MElevation.qml @@ -2,27 +2,26 @@ pragma Singleton import QtQuick QtObject { - // More dramatic elevation steps for better depth perception + function getSurface(level) { switch (level) { case 0: - return "#040404"; // Base/background + return "#040404"; case 1: - return "#0a0a0b"; // Very subtle lift + return "#0a0a0b"; case 2: - return "#121213"; // Standard surface + return "#121213"; case 3: - return "#1c1c1d"; // Elevated surface + return "#1c1c1d"; case 4: - return "#282829"; // Higher elevation + return "#282829"; case 5: - return "#353536"; // Highest elevation + return "#353536"; default: return "#121213"; } } - // Sharper, more defined outer borders function getBorderOuter(level) { switch (level) { case 0: @@ -42,7 +41,6 @@ QtObject { } } - // More visible inner borders for layered effect function getBorderInner(level) { switch (level) { case 0: diff --git a/marathon-ui/Theme/MMotion.qml b/marathon-ui/Theme/MMotion.qml index e8a283235..ba61c3d57 100644 --- a/marathon-ui/Theme/MMotion.qml +++ b/marathon-ui/Theme/MMotion.qml @@ -2,14 +2,13 @@ pragma Singleton import QtQuick QtObject { - // Aligned with marathon-config.json animations (snappy for 60Hz) + readonly property int instant: 0 - readonly property int fast: 150 // fast: 150 - readonly property int normal: 200 // normal: 200 - readonly property int slow: 300 // slow: 300 - readonly property int slower: 400 // slower: 400 (was durationSlow) + readonly property int fast: 150 + readonly property int normal: 200 + readonly property int slow: 300 + readonly property int slower: 400 - // Legacy aliases for compatibility readonly property int xs: fast readonly property int sm: normal readonly property int md: slow @@ -19,7 +18,6 @@ QtObject { readonly property int quick: 160 readonly property int moderate: 240 - // Spring physics readonly property real springLight: 1.5 readonly property real springMedium: 2.0 readonly property real springHeavy: 3.0 @@ -29,7 +27,6 @@ QtObject { readonly property real dampingHeavy: 0.4 readonly property real epsilon: 0.01 - // Easing curves (BB10-inspired) readonly property int easingStandard: Easing.Bezier readonly property var easingStandardCurve: [0.2, 0, 0.2, 1] @@ -42,22 +39,18 @@ QtObject { readonly property int easingSpring: Easing.Bezier readonly property var easingSpringCurve: [0.34, 1.56, 0.64, 1] - // Choreography readonly property int staggerMicro: 20 readonly property int staggerShort: 50 readonly property int staggerMedium: 80 readonly property int staggerLong: 120 - // Page transitions readonly property real pageParallaxOffset: 0.3 readonly property real pageScaleOut: 0.92 - // Ripple effect readonly property int rippleDuration: slow readonly property real rippleMaxRadius: 2.5 readonly property real rippleOpacity: 0.12 - // Scroll physics (from marathon-config.json) readonly property int flickDecelerationFast: 8000 readonly property int flickVelocityMax: 5000 readonly property int touchFlickDeceleration: 25000 diff --git a/marathon-ui/Theme/MRadius.qml b/marathon-ui/Theme/MRadius.qml index e615a53e6..251086db4 100644 --- a/marathon-ui/Theme/MRadius.qml +++ b/marathon-ui/Theme/MRadius.qml @@ -11,4 +11,5 @@ QtObject { readonly property int pill: 999 readonly property int circle: 999 + readonly property int full: pill } diff --git a/marathon-ui/Theme/MSpacing.qml b/marathon-ui/Theme/MSpacing.qml index 857bad23a..a61d87cbe 100644 --- a/marathon-ui/Theme/MSpacing.qml +++ b/marathon-ui/Theme/MSpacing.qml @@ -3,17 +3,16 @@ import QtQuick import MarathonOS.Shell QtObject { - // Aligned with marathon-config.json spacing system - scaled with Constants.scaleFactor - readonly property int xs: Math.round(5 * (Constants.scaleFactor || 1.0)) // xsmall: 5 - readonly property int sm: Math.round(10 * (Constants.scaleFactor || 1.0)) // small: 10 - readonly property int md: Math.round(16 * (Constants.scaleFactor || 1.0)) // medium: 16 - readonly property int lg: Math.round(20 * (Constants.scaleFactor || 1.0)) // large: 20 - readonly property int xl: Math.round(32 * (Constants.scaleFactor || 1.0)) // xlarge: 32 - readonly property int xxl: Math.round(40 * (Constants.scaleFactor || 1.0)) // xxlarge: 40 - // Touch targets aligned with marathon-config.json - scaled with Constants.scaleFactor - readonly property int touchTargetMin: Math.round(45 * (Constants.scaleFactor || 1.0)) // minimum: 45 - readonly property int touchTargetSmall: Math.round(60 * (Constants.scaleFactor || 1.0)) // small: 60 - readonly property int touchTargetMedium: Math.round(70 * (Constants.scaleFactor || 1.0)) // medium: 70 - readonly property int touchTargetLarge: Math.round(90 * (Constants.scaleFactor || 1.0)) // large: 90 + readonly property int xs: Math.round(5 * (Constants.scaleFactor || 1.0)) + readonly property int sm: Math.round(10 * (Constants.scaleFactor || 1.0)) + readonly property int md: Math.round(16 * (Constants.scaleFactor || 1.0)) + readonly property int lg: Math.round(20 * (Constants.scaleFactor || 1.0)) + readonly property int xl: Math.round(32 * (Constants.scaleFactor || 1.0)) + readonly property int xxl: Math.round(40 * (Constants.scaleFactor || 1.0)) + + readonly property int touchTargetMin: Math.round(45 * (Constants.scaleFactor || 1.0)) + readonly property int touchTargetSmall: Math.round(60 * (Constants.scaleFactor || 1.0)) + readonly property int touchTargetMedium: Math.round(70 * (Constants.scaleFactor || 1.0)) + readonly property int touchTargetLarge: Math.round(90 * (Constants.scaleFactor || 1.0)) } diff --git a/marathon-ui/Theme/MTypography.qml b/marathon-ui/Theme/MTypography.qml index 533942728..4168a6e4b 100644 --- a/marathon-ui/Theme/MTypography.qml +++ b/marathon-ui/Theme/MTypography.qml @@ -3,22 +3,23 @@ import QtQuick import MarathonOS.Shell QtObject { - // Aligned with marathon-config.json typography - readonly property string fontFamily: "Slate" // Shell uses Slate font - readonly property string fontFamilyMono: "JetBrains Mono" // Shell uses JetBrains Mono - // Font sizes aligned with marathon-config.json - scaled with Constants.scaleFactor - readonly property int sizeXSmall: Math.round(12 * (Constants.scaleFactor || 1.0)) // xsmall: 12 - readonly property int sizeSmall: Math.round(14 * (Constants.scaleFactor || 1.0)) // small: 14 - readonly property int sizeBody: Math.round(16 * (Constants.scaleFactor || 1.0)) // medium: 16 - readonly property int sizeLarge: Math.round(18 * (Constants.scaleFactor || 1.0)) // large: 18 - readonly property int sizeXLarge: Math.round(24 * (Constants.scaleFactor || 1.0)) // xlarge: 24 - readonly property int sizeXXLarge: Math.round(32 * (Constants.scaleFactor || 1.0)) // xxlarge: 32 - readonly property int sizeHuge: Math.round(48 * (Constants.scaleFactor || 1.0)) // huge: 48 - readonly property int sizeGigantic: Math.round(96 * (Constants.scaleFactor || 1.0)) // gigantic: 96 (lock screen clock) + readonly property string fontFamily: "Slate" + readonly property string fontFamilyMono: "JetBrains Mono" + readonly property string fontMonospace: fontFamilyMono + + readonly property int sizeXSmall: Math.round(12 * (Constants.scaleFactor || 1.0)) + readonly property int sizeSmall: Math.round(14 * (Constants.scaleFactor || 1.0)) + readonly property int sizeBody: Math.round(16 * (Constants.scaleFactor || 1.0)) + readonly property int sizeLarge: Math.round(18 * (Constants.scaleFactor || 1.0)) + readonly property int sizeXLarge: Math.round(24 * (Constants.scaleFactor || 1.0)) + readonly property int sizeXXLarge: Math.round(32 * (Constants.scaleFactor || 1.0)) + readonly property int sizeDisplay: Math.round(40 * (Constants.scaleFactor || 1.0)) + readonly property int sizeHuge: Math.round(48 * (Constants.scaleFactor || 1.0)) + readonly property int sizeGigantic: Math.round(96 * (Constants.scaleFactor || 1.0)) - // Font weights readonly property int weightLight: Font.Light + readonly property int weightRegular: Font.Normal readonly property int weightNormal: Font.Normal readonly property int weightMedium: Font.Medium readonly property int weightDemiBold: Font.DemiBold From 8ad2f79755d1af4a106142510d900de0b7dade3a Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:31:19 +0000 Subject: [PATCH 05/28] refactor(services): sync telephony, contacts, and media helpers Update Marathon services managers to align APIs and cleanup related call history, contacts, SMS, and media handling. --- marathon-services/src/callhistorymanager.cpp | 6 +-- marathon-services/src/callhistorymanager.h | 6 +-- marathon-services/src/contactsmanager.cpp | 16 ++------ marathon-services/src/contactsmanager.h | 2 +- marathon-services/src/medialibrarymanager.cpp | 32 ++++------------ marathon-services/src/medialibrarymanager.h | 7 ++-- marathon-services/src/musiclibrarymanager.cpp | 24 ++---------- marathon-services/src/musiclibrarymanager.h | 7 ++-- marathon-services/src/smsservice.cpp | 37 ++++--------------- marathon-services/src/smsservice.h | 5 +-- marathon-services/src/telephonyservice.cpp | 32 +++------------- marathon-services/src/telephonyservice.h | 3 +- 12 files changed, 41 insertions(+), 136 deletions(-) diff --git a/marathon-services/src/callhistorymanager.cpp b/marathon-services/src/callhistorymanager.cpp index dfcc068e7..8c017ebf5 100644 --- a/marathon-services/src/callhistorymanager.cpp +++ b/marathon-services/src/callhistorymanager.cpp @@ -23,7 +23,7 @@ CallHistoryManager::~CallHistoryManager() { void CallHistoryManager::setContactsManager(ContactsManager *contactsManager) { m_contactsManager = contactsManager; - // Reload history to resolve contact names + loadHistory(); } @@ -62,7 +62,6 @@ void CallHistoryManager::addCall(const QString &number, const QString &type, qin saveCall(record); - // Reload to get the ID loadHistory(); qDebug() << "[CallHistoryManager] Added call:" << type << number << duration << "seconds"; @@ -143,7 +142,6 @@ void CallHistoryManager::initDatabase() { return; } - // Create table if it doesn't exist QSqlQuery query(m_database); bool success = query.exec("CREATE TABLE IF NOT EXISTS call_history (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -201,7 +199,6 @@ void CallHistoryManager::saveCall(const CallRecord &record) { qWarning() << "[CallHistoryManager] Failed to save call:" << query.lastError().text(); } - // Clean up old records if exceeding limit QSqlQuery countQuery(m_database); if (countQuery.exec("SELECT COUNT(*) FROM call_history")) { if (countQuery.next() && countQuery.value(0).toInt() > MAX_HISTORY_SIZE) { @@ -219,7 +216,6 @@ QString CallHistoryManager::resolveContactName(const QString &number) { return "Unknown"; } - // Search contacts by phone number QVariantList results = m_contactsManager->searchContacts(number); if (!results.isEmpty()) { QVariantMap contact = results.first().toMap(); diff --git a/marathon-services/src/callhistorymanager.h b/marathon-services/src/callhistorymanager.h index e19bac70d..deb333bf2 100644 --- a/marathon-services/src/callhistorymanager.h +++ b/marathon-services/src/callhistorymanager.h @@ -14,9 +14,9 @@ struct CallRecord { int id; QString number; QString contactName; - QString type; // "incoming", "outgoing", "missed" + QString type; qint64 timestamp; - int duration; // seconds + int duration; }; class CallHistoryManager : public QObject { @@ -56,4 +56,4 @@ class CallHistoryManager : public QObject { static const int MAX_HISTORY_SIZE = 500; }; -#endif // CALLHISTORYMANAGER_H +#endif diff --git a/marathon-services/src/contactsmanager.cpp b/marathon-services/src/contactsmanager.cpp index e9302764e..2ce9516b8 100644 --- a/marathon-services/src/contactsmanager.cpp +++ b/marathon-services/src/contactsmanager.cpp @@ -9,7 +9,6 @@ ContactsManager::ContactsManager(QObject *parent) , m_nextId(1) { m_contactsDir = getContactsDir(); - // Create contacts directory if it doesn't exist QDir dir; if (!dir.exists(m_contactsDir)) { dir.mkpath(m_contactsDir); @@ -203,10 +202,8 @@ void ContactsManager::loadFromVCards() { QString content = in.readAll(); file.close(); - // Simple vCard parsing (supports v3.0 and v4.0) - Contact contact; + Contact contact; - // Extract ID from filename QRegularExpression idRegex("_(\\d+)\\.vcf$"); QRegularExpressionMatch match = idRegex.match(fileName); if (match.hasMatch()) { @@ -218,28 +215,24 @@ void ContactsManager::loadFromVCards() { contact.id = m_nextId++; } - // Parse FN (formatted name) QRegularExpression fnRegex("FN:(.*?)\\r?\\n"); match = fnRegex.match(content); if (match.hasMatch()) { contact.name = match.captured(1).trimmed(); } - // Parse TEL (phone) QRegularExpression telRegex("TEL[^:]*:(.*?)\\r?\\n"); match = telRegex.match(content); if (match.hasMatch()) { contact.phone = match.captured(1).trimmed(); } - // Parse EMAIL QRegularExpression emailRegex("EMAIL[^:]*:(.*?)\\r?\\n"); match = emailRegex.match(content); if (match.hasMatch()) { contact.email = match.captured(1).trimmed(); } - // Parse ORG (organization) QRegularExpression orgRegex("ORG:(.*?)\\r?\\n"); match = orgRegex.match(content); if (match.hasMatch()) { @@ -266,7 +259,6 @@ void ContactsManager::saveToVCard(const Contact &contact) { QTextStream out(&file); - // Write vCard v3.0 format out << "BEGIN:VCARD\n"; out << "VERSION:3.0\n"; out << "FN:" << contact.name << "\n"; @@ -307,7 +299,6 @@ void ContactsManager::importVCard(const QString &path) { const QString content = in.readAll(); file.close(); - // Very small parser: FN/TEL/EMAIL/ORG (vCard v3/v4) QString name; QString phone; QString email; @@ -340,11 +331,10 @@ void ContactsManager::importVCard(const QString &path) { return; } - // Use existing addContact flow (persists to our contacts dir) addContact(name, phone, email); if (!organization.isEmpty()) { - // Update last-added contact's organization field + const int newId = m_nextId - 1; QVariantMap update; update["organization"] = organization; @@ -397,7 +387,7 @@ void ContactsManager::exportVCard(int contactId, const QString &path) { QString ContactsManager::sanitizeFileName(const QString &name) { QString sanitized = name; - // Remove or replace characters that are invalid in filenames + sanitized.replace(QRegularExpression("[/\\\\:*?\"<>|]"), "_"); sanitized.replace(" ", "_"); return sanitized; diff --git a/marathon-services/src/contactsmanager.h b/marathon-services/src/contactsmanager.h index 590f45d55..969833255 100644 --- a/marathon-services/src/contactsmanager.h +++ b/marathon-services/src/contactsmanager.h @@ -60,4 +60,4 @@ class ContactsManager : public QObject { QString m_contactsDir; }; -#endif // CONTACTSMANAGER_H +#endif diff --git a/marathon-services/src/medialibrarymanager.cpp b/marathon-services/src/medialibrarymanager.cpp index ec3d39281..adf137a4c 100644 --- a/marathon-services/src/medialibrarymanager.cpp +++ b/marathon-services/src/medialibrarymanager.cpp @@ -12,7 +12,6 @@ #include #include -// Static const for extensions const QStringList MediaScanWorker::IMAGE_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "bmp", "webp", "heic", "heif"}; @@ -25,8 +24,6 @@ const QStringList MediaLibraryManager::IMAGE_EXTENSIONS = {"jpg", "jpeg", "png", const QStringList MediaLibraryManager::VIDEO_EXTENSIONS = {"mp4", "mov", "avi", "mkv", "webm", "m4v", "3gp"}; -// ===== MediaScanWorker Implementation ===== - MediaScanWorker::MediaScanWorker(const QStringList &paths, QObject *parent) : QObject(parent) , m_paths(paths) {} @@ -38,7 +35,6 @@ void MediaScanWorker::process() { int totalFiles = 0; int currentFile = 0; - // First pass: count files for (const QString &path : m_paths) { QDirIterator countIt(path, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); @@ -50,7 +46,6 @@ void MediaScanWorker::process() { qDebug() << "[MediaScanWorker] Found" << totalFiles << "total files to scan"; - // Second pass: scan files for (const QString &path : m_paths) { QDirIterator it(path, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); @@ -66,7 +61,7 @@ void MediaScanWorker::process() { } currentFile++; - if (currentFile % 50 == 0) { // Emit progress every 50 files + if (currentFile % 50 == 0) { emit scanProgress(currentFile, totalFiles); } } @@ -74,13 +69,13 @@ void MediaScanWorker::process() { } qDebug() << "[MediaScanWorker] Scan complete. Found" << items.size() << "media items"; - emit scanProgress(totalFiles, totalFiles); // 100% + emit scanProgress(totalFiles, totalFiles); emit scanFinished(items); } MediaItem MediaScanWorker::scanFile(const QString &filePath) { MediaItem item; - item.id = -1; // Will be set by database + item.id = -1; item.path = filePath; QFileInfo fileInfo(filePath); @@ -91,7 +86,6 @@ MediaItem MediaScanWorker::scanFile(const QString &filePath) { item.width = 0; item.height = 0; - // Extract image dimensions if it's a photo if (item.type == "photo") { QImageReader reader(filePath); if (reader.canRead()) { @@ -114,8 +108,6 @@ bool MediaScanWorker::isVideoFile(const QString &path) { return VIDEO_EXTENSIONS.contains(extension); } -// ===== MediaLibraryManager Implementation ===== - MediaLibraryManager::MediaLibraryManager(QObject *parent) : QObject(parent) , m_watcher(new QFileSystemWatcher(this)) @@ -196,7 +188,7 @@ QStringList MediaLibraryManager::getScanPaths() { } void MediaLibraryManager::scanLibrary() { - // Synchronous version for backwards compatibility + if (m_isScanning) { qDebug() << "[MediaLibraryManager] Scan already in progress"; return; @@ -253,7 +245,6 @@ void MediaLibraryManager::scanLibraryAsync() { return; } - // Clean up previous thread if exists if (m_scanThread) { m_scanThread->quit(); m_scanThread->wait(); @@ -263,16 +254,14 @@ void MediaLibraryManager::scanLibraryAsync() { m_isScanning = true; m_scanProgress = 0; - emit scanningChanged(true); - emit scanProgressChanged(0); + emit scanningChanged(true); + emit scanProgressChanged(0); - // Create worker and thread QStringList paths = getScanPaths(); m_scanWorker = new MediaScanWorker(paths); m_scanThread = new QThread(); m_scanWorker->moveToThread(m_scanThread); - // Connect signals connect(m_scanThread, &QThread::started, m_scanWorker, &MediaScanWorker::process); connect(m_scanWorker, &MediaScanWorker::scanProgress, this, &MediaLibraryManager::onScanProgress); @@ -284,12 +273,10 @@ void MediaLibraryManager::scanLibraryAsync() { emit scanningChanged(false); }); - // Clean up thread when done connect(m_scanWorker, &MediaScanWorker::scanFinished, m_scanThread, &QThread::quit); connect(m_scanThread, &QThread::finished, m_scanWorker, &QObject::deleteLater); connect(m_scanThread, &QThread::finished, m_scanThread, &QObject::deleteLater); - // Start scanning m_scanThread->start(); } @@ -303,10 +290,8 @@ void MediaLibraryManager::onScanProgress(int current, int total) { void MediaLibraryManager::onScanFinished(QList items) { qDebug() << "[MediaLibraryManager] Async scan finished. Processing" << items.size() << "items"; - // Batch insert all items addMediaItemBatch(items); - // Update counts and albums loadAlbums(); QSqlQuery countQuery(m_database); @@ -618,8 +603,7 @@ QString MediaLibraryManager::createThumbnail(const QString &sourcePath) { QImageReader reader(sourcePath); reader.setAutoTransform(true); - // Decode at (roughly) thumbnail size while preserving aspect ratio. - // Avoid forcing a square scaledSize, which can distort and look "grainy" when rendered. + const QSize origSize = reader.size(); if (origSize.isValid() && origSize.width() > 0 && origSize.height() > 0) { const QSize scaled = origSize.scaled(QSize(targetSize, targetSize), Qt::KeepAspectRatio); @@ -635,7 +619,7 @@ QString MediaLibraryManager::createThumbnail(const QString &sourcePath) { return thumbPath; } } else { - // Higher quality helps avoid "grainy" thumbnails, especially for UI screenshots. + if (image.save(thumbPath, "JPG", 92)) { return thumbPath; } diff --git a/marathon-services/src/medialibrarymanager.h b/marathon-services/src/medialibrarymanager.h index f83de0396..bc3916e54 100644 --- a/marathon-services/src/medialibrarymanager.h +++ b/marathon-services/src/medialibrarymanager.h @@ -30,7 +30,6 @@ struct Album { qint64 lastModified; }; -// Worker thread for async scanning class MediaScanWorker : public QObject { Q_OBJECT public: @@ -73,7 +72,7 @@ class MediaLibraryManager : public QObject { int scanProgress() const; Q_INVOKABLE void scanLibrary(); - Q_INVOKABLE void scanLibraryAsync(); // New async method + Q_INVOKABLE void scanLibraryAsync(); Q_INVOKABLE QVariantList getPhotos(const QString &albumId); Q_INVOKABLE QVariantList getVideos(); Q_INVOKABLE QVariantList getAllPhotos(); @@ -98,7 +97,7 @@ class MediaLibraryManager : public QObject { void initDatabase(); void scanDirectory(const QString &path); void addMediaItem(const QString &filePath); - void addMediaItemBatch(const QList &items); // Batch insert + void addMediaItemBatch(const QList &items); void extractPhotoMetadata(const QString &path, MediaItem &item); QString createThumbnail(const QString &sourcePath); void loadAlbums(); @@ -125,4 +124,4 @@ class MediaLibraryManager : public QObject { static const QStringList VIDEO_EXTENSIONS; }; -#endif // MEDIALIBRARYMANAGER_H +#endif diff --git a/marathon-services/src/musiclibrarymanager.cpp b/marathon-services/src/musiclibrarymanager.cpp index f22554e0b..93ec7e239 100644 --- a/marathon-services/src/musiclibrarymanager.cpp +++ b/marathon-services/src/musiclibrarymanager.cpp @@ -9,15 +9,12 @@ #include #include -// Static const for extensions const QStringList MusicScanWorker::AUDIO_EXTENSIONS = {"mp3", "m4a", "flac", "ogg", "opus", "wav", "aac", "wma"}; const QStringList MusicLibraryManager::AUDIO_EXTENSIONS = {"mp3", "m4a", "flac", "ogg", "opus", "wav", "aac", "wma"}; -// ===== MusicScanWorker Implementation ===== - MusicScanWorker::MusicScanWorker(const QStringList &paths, QObject *parent) : QObject(parent) , m_paths(paths) {} @@ -29,7 +26,6 @@ void MusicScanWorker::process() { int totalFiles = 0; int currentFile = 0; - // First pass: count files for (const QString &path : m_paths) { QDirIterator countIt(path, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); @@ -41,7 +37,6 @@ void MusicScanWorker::process() { qDebug() << "[MusicScanWorker] Found" << totalFiles << "total files to scan"; - // Second pass: scan files for (const QString &path : m_paths) { QDirIterator it(path, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); @@ -57,7 +52,7 @@ void MusicScanWorker::process() { } currentFile++; - if (currentFile % 50 == 0) { // Emit progress every 50 files + if (currentFile % 50 == 0) { emit scanProgress(currentFile, totalFiles); } } @@ -65,18 +60,17 @@ void MusicScanWorker::process() { } qDebug() << "[MusicScanWorker] Scan complete. Found" << tracks.size() << "music tracks"; - emit scanProgress(totalFiles, totalFiles); // 100% + emit scanProgress(totalFiles, totalFiles); emit scanFinished(tracks); } Track MusicScanWorker::scanFile(const QString &filePath) { Track track; - track.id = -1; // Will be set by database + track.id = -1; track.path = filePath; QFileInfo fileInfo(filePath); - // Basic metadata from filename/path track.title = fileInfo.completeBaseName(); track.artist = "Unknown Artist"; track.album = "Unknown Album"; @@ -84,7 +78,6 @@ Track MusicScanWorker::scanFile(const QString &filePath) { track.trackNumber = 0; track.year = ""; - // Try to extract from path structure: .../Artist/Album/Track.mp3 QStringList pathParts = fileInfo.absolutePath().split('/'); if (pathParts.size() >= 2) { track.album = pathParts[pathParts.size() - 1]; @@ -99,8 +92,6 @@ bool MusicScanWorker::isAudioFile(const QString &path) { return AUDIO_EXTENSIONS.contains(extension); } -// ===== MusicLibraryManager Implementation ===== - MusicLibraryManager::MusicLibraryManager(QObject *parent) : QObject(parent) , m_watcher(new QFileSystemWatcher(this)) @@ -203,13 +194,11 @@ void MusicLibraryManager::scanLibraryAsync() { qDebug() << "[MusicLibraryManager] Starting async library scan..."; - // Create worker and thread m_scanWorker = new MusicScanWorker(getScanPaths()); m_scanThread = new QThread(); m_scanWorker->moveToThread(m_scanThread); - // Connect signals connect(m_scanThread, &QThread::started, m_scanWorker, &MusicScanWorker::process); connect(m_scanWorker, &MusicScanWorker::scanProgress, this, &MusicLibraryManager::onScanProgress); @@ -221,7 +210,6 @@ void MusicLibraryManager::scanLibraryAsync() { emit scanningChanged(false); }); - // Cleanup when done connect(m_scanWorker, &MusicScanWorker::scanFinished, m_scanThread, &QThread::quit); connect(m_scanThread, &QThread::finished, m_scanWorker, &QObject::deleteLater); connect(m_scanThread, &QThread::finished, m_scanThread, &QObject::deleteLater); @@ -230,7 +218,6 @@ void MusicLibraryManager::scanLibraryAsync() { m_scanWorker = nullptr; }); - // Start the scan m_scanThread->start(); } @@ -251,17 +238,14 @@ void MusicLibraryManager::onScanFinished(QList tracks) { qDebug() << "[MusicLibraryManager] Async scan finished. Processing" << tracks.size() << "tracks"; - // Add all tracks to database in a batch addTrackBatch(tracks); - // Update counts QSqlQuery countQuery(m_database); countQuery.exec("SELECT COUNT(*) FROM tracks"); if (countQuery.next()) { m_trackCount = countQuery.value(0).toInt(); } - // Reload artists loadArtists(); m_isScanning = false; @@ -315,8 +299,6 @@ QStringList MusicLibraryManager::getScanPaths() { paths << musicPath; - // Could add additional paths here (e.g., SD card, custom music folders) - return paths; } diff --git a/marathon-services/src/musiclibrarymanager.h b/marathon-services/src/musiclibrarymanager.h index dbee4e266..df38bf8a4 100644 --- a/marathon-services/src/musiclibrarymanager.h +++ b/marathon-services/src/musiclibrarymanager.h @@ -28,7 +28,6 @@ struct Artist { int trackCount; }; -// Worker thread for async music scanning class MusicScanWorker : public QObject { Q_OBJECT public: @@ -67,7 +66,7 @@ class MusicLibraryManager : public QObject { int scanProgress() const; Q_INVOKABLE void scanLibrary(); - Q_INVOKABLE void scanLibraryAsync(); // New async method + Q_INVOKABLE void scanLibraryAsync(); Q_INVOKABLE QVariantList getAlbums(const QString &artistName); Q_INVOKABLE QVariantList getTracks(const QString &albumName); Q_INVOKABLE QVariantList getAllTracks(); @@ -89,7 +88,7 @@ class MusicLibraryManager : public QObject { void initDatabase(); void scanDirectory(const QString &path); void addTrack(const QString &filePath); - void addTrackBatch(const QList &tracks); // Batch insert + void addTrackBatch(const QList &tracks); void extractMetadata(const QString &path, Track &track); void loadArtists(); bool isAudioFile(const QString &path); @@ -109,4 +108,4 @@ class MusicLibraryManager : public QObject { static const QStringList AUDIO_EXTENSIONS; }; -#endif // MUSICLIBRARYMANAGER_H +#endif diff --git a/marathon-services/src/smsservice.cpp b/marathon-services/src/smsservice.cpp index 31d61dcd3..d310d4e91 100644 --- a/marathon-services/src/smsservice.cpp +++ b/marathon-services/src/smsservice.cpp @@ -23,7 +23,6 @@ SMSService::SMSService(QObject *parent) initDatabase(); connectToModemManager(); - // Poll for new messages every 5 seconds m_pollTimer->setInterval(5000); connect(m_pollTimer, &QTimer::timeout, this, &SMSService::checkForNewMessages); m_pollTimer->start(); @@ -73,7 +72,6 @@ void SMSService::sendMessage(const QString &recipient, const QString &text) { if (!m_modemManager || !m_modemManager->isValid()) { qWarning() << "[SMSService] ModemManager not available"; - // Store in database as pending Message msg; msg.conversationId = generateConversationId(recipient); msg.sender = "me"; @@ -90,7 +88,6 @@ void SMSService::sendMessage(const QString &recipient, const QString &text) { return; } - // Get list of modems QDBusReply reply = m_modemManager->call("GetManagedObjects"); if (!reply.isValid()) { qWarning() << "[SMSService] Failed to get modems:" << reply.error().message(); @@ -101,7 +98,6 @@ void SMSService::sendMessage(const QString &recipient, const QString &text) { QVariantMap objects = reply.value(); QString modemPath; - // Find first modem with Messaging capability for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) { QString path = it.key(); QVariantMap interfaces = qdbus_cast(it.value()); @@ -118,7 +114,6 @@ void SMSService::sendMessage(const QString &recipient, const QString &text) { return; } - // Create SMS QVariantMap properties; properties["number"] = recipient; properties["text"] = text; @@ -145,7 +140,6 @@ void SMSService::sendMessage(const QString &recipient, const QString &text) { QString smsPath = createReply.value().path(); qDebug() << "[SMSService] SMS created:" << smsPath; - // Send SMS QDBusInterface smsInterface("org.freedesktop.ModemManager1", smsPath, "org.freedesktop.ModemManager1.Sms", QDBusConnection::systemBus()); @@ -162,7 +156,6 @@ void SMSService::sendMessage(const QString &recipient, const QString &text) { return; } - // Store in database Message msg; msg.conversationId = generateConversationId(recipient); msg.sender = "me"; @@ -237,7 +230,7 @@ void SMSService::markAsRead(const QString &conversationId) { } QString SMSService::generateConversationId(const QString &number) { - // Normalize phone number (remove spaces, dashes, etc.) + QString normalized = number; normalized.remove(QRegularExpression("[^0-9+]")); return normalized; @@ -247,9 +240,8 @@ void SMSService::simulateIncomingSMS(const QString &sender, const QString &text) qInfo() << "[SMSService] [SIMULATION] Simulating incoming SMS from:" << sender << "text:" << text; - qint64 timestamp = QDateTime::currentMSecsSinceEpoch(); + qint64 timestamp = QDateTime::currentMSecsSinceEpoch(); - // Store message in database Message msg; msg.conversationId = generateConversationId(sender); msg.sender = sender; @@ -262,7 +254,6 @@ void SMSService::simulateIncomingSMS(const QString &sender, const QString &text) storeMessage(msg); loadConversations(); - // Emit signal emit messageReceived(sender, text, timestamp); qInfo() << "[SMSService] [SIMULATION] ✓ Incoming SMS simulated and stored"; @@ -280,7 +271,6 @@ void SMSService::initDatabase() { return; } - // Create messages table QSqlQuery query(m_database); query.exec("CREATE TABLE IF NOT EXISTS messages (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -302,7 +292,6 @@ void SMSService::initDatabase() { void SMSService::loadConversations() { m_conversations.clear(); - // Get conversations with last message and unread count QSqlQuery query(m_database); query.exec("SELECT conversationId, " "MAX(timestamp) as lastTimestamp, " @@ -312,11 +301,10 @@ void SMSService::loadConversations() { "ORDER BY lastTimestamp DESC"); while (query.next()) { - QString convId = query.value("conversationId").toString(); - qint64 lastTimestamp = query.value("lastTimestamp").toLongLong(); - int unreadCount = query.value("unreadCount").toInt(); + QString convId = query.value("conversationId").toString(); + qint64 lastTimestamp = query.value("lastTimestamp").toLongLong(); + int unreadCount = query.value("unreadCount").toInt(); - // Get last message text QSqlQuery msgQuery(m_database); msgQuery.prepare( "SELECT text, recipient FROM messages WHERE conversationId = ? AND timestamp = ?"); @@ -330,8 +318,6 @@ void SMSService::loadConversations() { lastMessage = msgQuery.value("text").toString(); contactNumber = msgQuery.value("recipient").toString(); - // If message is outgoing, recipient is the contact - // If incoming, sender is the contact (which is convId) if (contactNumber == "me") { contactNumber = convId; } @@ -386,7 +372,6 @@ void SMSService::connectToModemManager() { qInfo() << "[SMSService] ✓ Connected to ModemManager"; - // Monitor for new SMS messages QDBusConnection::systemBus().connect("org.freedesktop.ModemManager1", "", "org.freedesktop.ModemManager1.Modem.Messaging", "Added", this, SLOT(checkForNewMessages())); @@ -397,7 +382,6 @@ void SMSService::checkForNewMessages() { return; } - // Get list of modems QDBusReply reply = m_modemManager->call("GetManagedObjects"); if (!reply.isValid()) { return; @@ -405,7 +389,6 @@ void SMSService::checkForNewMessages() { QVariantMap objects = reply.value(); - // Check each modem for new messages for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) { QString path = it.key(); QVariantMap interfaces = qdbus_cast(it.value()); @@ -419,7 +402,6 @@ void SMSService::checkForNewMessages() { continue; } - // List all messages QDBusReply> listReply = messagingInterface.call("List"); if (!listReply.isValid()) { continue; @@ -442,7 +424,6 @@ void SMSService::processIncomingSMS(const QString &smsPath) { return; } - // Get SMS properties QDBusReply numberReply = smsInterface.call("Get", "org.freedesktop.ModemManager1.Sms", "Number"); QDBusReply textReply = @@ -466,7 +447,6 @@ void SMSService::processIncomingSMS(const QString &smsPath) { } } - // Check if we already have this message (by comparing timestamp and sender) QSqlQuery checkQuery(m_database); checkQuery.prepare( "SELECT COUNT(*) FROM messages WHERE sender = ? AND timestamp = ? AND text = ?"); @@ -476,12 +456,11 @@ void SMSService::processIncomingSMS(const QString &smsPath) { if (checkQuery.exec() && checkQuery.next()) { if (checkQuery.value(0).toInt() > 0) { - // Message already exists + return; } } - // Store new message Message msg; msg.conversationId = generateConversationId(sender); msg.sender = sender; @@ -498,7 +477,6 @@ void SMSService::processIncomingSMS(const QString &smsPath) { qInfo() << "[SMSService] ✓ New SMS received from:" << sender; - // Delete from modem (to save SIM storage) QDBusInterface smsDeleteInterface("org.freedesktop.ModemManager1", smsPath, "org.freedesktop.ModemManager1.Sms", QDBusConnection::systemBus()); @@ -510,8 +488,7 @@ void SMSService::processIncomingSMS(const QString &smsPath) { QString SMSService::resolveContactName(const QString &number) const { if (m_contactsManager) { - // Look up contact name from ContactsManager - // This would need to be implemented in ContactsManager + return number; } return number; diff --git a/marathon-services/src/smsservice.h b/marathon-services/src/smsservice.h index 64d2c5950..df80cb2ac 100644 --- a/marathon-services/src/smsservice.h +++ b/marathon-services/src/smsservice.h @@ -48,8 +48,7 @@ class SMSService : public QObject { Q_INVOKABLE void markAsRead(const QString &conversationId); Q_INVOKABLE QString generateConversationId(const QString &number); - // Simulation method for testing - Q_INVOKABLE void simulateIncomingSMS(const QString &sender, const QString &text); + Q_INVOKABLE void simulateIncomingSMS(const QString &sender, const QString &text); signals: void messageReceived(const QString &sender, const QString &text, qint64 timestamp); @@ -80,4 +79,4 @@ class SMSService : public QObject { #endif }; -#endif // SMSSERVICE_H +#endif diff --git a/marathon-services/src/telephonyservice.cpp b/marathon-services/src/telephonyservice.cpp index a8d807b92..c1c62d127 100644 --- a/marathon-services/src/telephonyservice.cpp +++ b/marathon-services/src/telephonyservice.cpp @@ -17,8 +17,7 @@ TelephonyService::TelephonyService(QObject *parent) connectToModemManager(); - // Setup reconnect timer for modem detection - m_reconnectTimer->setInterval(10000); // Check every 10 seconds + m_reconnectTimer->setInterval(10000); m_reconnectTimer->setSingleShot(false); connect(m_reconnectTimer, &QTimer::timeout, this, &TelephonyService::checkModemStatus); if (QDBusConnection::systemBus().isConnected()) { @@ -63,7 +62,6 @@ void TelephonyService::dial(const QString &number) { return; } - // Call ModemManager D-Bus method to create voice call QDBusInterface voiceInterface("org.freedesktop.ModemManager1", m_modemPath, "org.freedesktop.ModemManager1.Modem.Voice", QDBusConnection::systemBus()); @@ -75,7 +73,6 @@ void TelephonyService::dial(const QString &number) { return; } - // Create call QVariantMap properties; properties["number"] = number; @@ -91,7 +88,6 @@ void TelephonyService::dial(const QString &number) { QString callPath = reply.value().path(); qDebug() << "[TelephonyService] Call created:" << callPath; - // Start the call QDBusInterface callInterface("org.freedesktop.ModemManager1", callPath, "org.freedesktop.ModemManager1.Call", QDBusConnection::systemBus()); @@ -113,7 +109,6 @@ void TelephonyService::dial(const QString &number) { m_activeNumber = number; m_callState = "dialing"; - // Monitor call state changes setupCallMonitoring(callPath); emit callStateChanged("dialing"); @@ -130,7 +125,6 @@ void TelephonyService::answer() { return; } - // Handle simulation mode if (m_activeCallPath.contains("simulate")) { m_callState = "active"; emit callStateChanged("active"); @@ -169,7 +163,6 @@ void TelephonyService::hangup() { return; } - // Handle simulation mode if (m_activeCallPath.contains("simulate")) { m_callState = "idle"; m_activeCallPath.clear(); @@ -243,7 +236,7 @@ void TelephonyService::simulateIncomingCall(const QString &number) { m_activeNumber = number; m_callState = "incoming"; - m_activeCallPath = "/org/freedesktop/ModemManager1/Call/simulate"; // Mock path for simulation + m_activeCallPath = "/org/freedesktop/ModemManager1/Call/simulate"; emit incomingCall(number); emit callStateChanged("incoming"); @@ -285,7 +278,7 @@ void TelephonyService::connectToModemManager() { } void TelephonyService::setupDBusConnections() { - // Monitor for modems being added/removed + QDBusConnection::systemBus().connect( "org.freedesktop.ModemManager1", "/org/freedesktop/ModemManager1", "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", this, SLOT(checkModemStatus())); @@ -304,7 +297,6 @@ void TelephonyService::checkModemStatus() { return; } - // Get list of modems - use correct DBus type typedef QMap InterfaceList; typedef QMap ManagedObjectList; @@ -322,7 +314,6 @@ void TelephonyService::checkModemStatus() { ManagedObjectList objects; arg >> objects; - // Find first modem with Voice capability for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) { QString path = it.key().path(); InterfaceList interfaces = it.value(); @@ -338,13 +329,11 @@ void TelephonyService::checkModemStatus() { emit modemChanged(true); } - // Monitor for incoming calls monitorIncomingCalls(); return; } } - // No modem found if (m_hasModem) { m_hasModem = false; m_modemPath.clear(); @@ -354,7 +343,7 @@ void TelephonyService::checkModemStatus() { } void TelephonyService::setupCallMonitoring(const QString &callPath) { - // Monitor call property changes + QDBusConnection::systemBus().connect( "org.freedesktop.ModemManager1", callPath, "org.freedesktop.DBus.Properties", "PropertiesChanged", this, @@ -365,7 +354,6 @@ void TelephonyService::monitorIncomingCalls() { if (m_modemPath.isEmpty()) return; - // Monitor for new calls being added QDBusConnection::systemBus().connect("org.freedesktop.ModemManager1", m_modemPath, "org.freedesktop.ModemManager1.Modem.Voice", "CallAdded", this, SLOT(onCallAdded(QDBusObjectPath))); @@ -375,7 +363,6 @@ void TelephonyService::onCallAdded(const QDBusObjectPath &callPath) { QString path = callPath.path(); qInfo() << "[TelephonyService] New call detected:" << path; - // Get call properties QDBusInterface callInterface("org.freedesktop.ModemManager1", path, "org.freedesktop.DBus.Properties", QDBusConnection::systemBus()); @@ -384,16 +371,14 @@ void TelephonyService::onCallAdded(const QDBusObjectPath &callPath) { return; } - // Get call direction (incoming/outgoing) QDBusReply directionReply = callInterface.call("Get", "org.freedesktop.ModemManager1.Call", "Direction"); if (directionReply.isValid()) { uint direction = directionReply.value().toUInt(); - // 0 = unknown, 1 = incoming, 2 = outgoing if (direction == 1) { - // Incoming call + QDBusReply numberReply = callInterface.call("Get", "org.freedesktop.ModemManager1.Call", "Number"); @@ -432,7 +417,6 @@ void TelephonyService::onModemManagerPropertiesChanged(const QString &interf emit callStateChanged(newState); qDebug() << "[TelephonyService] Call state changed to:" << newState; - // Call ended if (newState == "idle" || newState == "terminated") { m_activeCallPath.clear(); m_activeNumber.clear(); @@ -444,9 +428,6 @@ void TelephonyService::onModemManagerPropertiesChanged(const QString &interf } QString TelephonyService::callStateFromModemManager(uint mmState) { - // ModemManager call states: - // 0 = Unknown, 1 = Dialing, 2 = Ringing-out, 3 = Ringing-in - // 4 = Active, 5 = Held, 6 = Waiting, 7 = Terminated switch (mmState) { case 0: return "unknown"; @@ -462,8 +443,7 @@ QString TelephonyService::callStateFromModemManager(uint mmState) { } QString TelephonyService::extractNumberFromPath(const QString &path) { - // Extract number from call path if embedded - // This is a fallback; normally we get it from properties + Q_UNUSED(path) return "Unknown"; } diff --git a/marathon-services/src/telephonyservice.h b/marathon-services/src/telephonyservice.h index 8e52a75b2..093dddfd5 100644 --- a/marathon-services/src/telephonyservice.h +++ b/marathon-services/src/telephonyservice.h @@ -28,7 +28,6 @@ class TelephonyService : public QObject { Q_INVOKABLE void hangup(); Q_INVOKABLE void sendDTMF(const QString &digit); - // Simulation methods for testing Q_INVOKABLE void simulateIncomingCall(const QString &number); Q_INVOKABLE void simulateCallStateChange(const QString &state); @@ -63,4 +62,4 @@ class TelephonyService : public QObject { QTimer *m_reconnectTimer; }; -#endif // TELEPHONYSERVICE_H +#endif From 1a0c19b69c55f8c0d47b1579674061e06101f990 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:31:36 +0000 Subject: [PATCH 06/28] refactor(keyboard): tighten layouts and input handling Update marathon-keyboard QML and engine bindings for safer layout sizing, input context handling, and haptic calls. --- .../qml/Core/KeyboardPerformance.qml | 32 +--- .../qml/Core/MarathonKeyboard.qml | 7 - marathon-keyboard/qml/Data/AutoCorrect.qml | 19 +-- marathon-keyboard/qml/Data/Dictionary.qml | 16 -- marathon-keyboard/qml/Input/InputContext.qml | 28 +--- .../qml/Layouts/QwertyLayout.qml | 24 +-- .../qml/Layouts/SymbolLayout.qml | 22 +-- marathon-keyboard/qml/UI/Key.qml | 66 ++------ marathon-keyboard/qml/UI/LongPressPopup.qml | 5 - marathon-keyboard/qml/UI/PredictionBar.qml | 11 +- .../src/marathoninputmethodengine.cpp | 30 +--- .../src/marathoninputmethodengine.h | 105 +++---------- marathon-keyboard/src/marathonkeyboardime.cpp | 54 ++----- marathon-keyboard/src/marathonkeyboardime.h | 141 ++++-------------- 14 files changed, 87 insertions(+), 473 deletions(-) diff --git a/marathon-keyboard/qml/Core/KeyboardPerformance.qml b/marathon-keyboard/qml/Core/KeyboardPerformance.qml index c575d7884..c6464fabb 100644 --- a/marathon-keyboard/qml/Core/KeyboardPerformance.qml +++ b/marathon-keyboard/qml/Core/KeyboardPerformance.qml @@ -1,46 +1,21 @@ -// Marathon Keyboard - Performance Optimizations -// QML-based optimizations for zero-latency input pragma Singleton import QtQuick QtObject { id: keyboardPerf - /** - * CRITICAL OPTIMIZATION 1: Pre-render key glyphs - * Cache rendered text to avoid re-layout on every frame - */ readonly property bool useTextMetricsCache: true - /** - * CRITICAL OPTIMIZATION 2: Batch property updates - * Use PropertyAnimation instead of direct property changes - */ - readonly property int keyPressAnimationDuration: 50 // ms + readonly property int keyPressAnimationDuration: 50 - /** - * CRITICAL OPTIMIZATION 3: Lazy load prediction bar - * Only create when needed - */ readonly property bool lazyLoadPredictions: true - /** - * CRITICAL OPTIMIZATION 4: Touch event priority - * Process keyboard touches before other UI elements - */ readonly property int touchEventPriority: Qt.HighEventPriority - /** - * CRITICAL OPTIMIZATION 5: Minimize QML re-evaluation - * Use const/readonly where possible - */ readonly property bool optimizeBindings: true - /** - * Performance monitoring - */ property var frameTimings: [] - property real averageFrameTime: 16.67 // 60 FPS target + property real averageFrameTime: 16.67 function recordFrameTime(time) { frameTimings.push(time); @@ -59,9 +34,6 @@ QtObject { } } - /** - * Touch latency monitoring - */ property var touchLatencies: [] property real averageTouchLatency: 0 diff --git a/marathon-keyboard/qml/Core/MarathonKeyboard.qml b/marathon-keyboard/qml/Core/MarathonKeyboard.qml index bc6b6db7a..2def76d53 100644 --- a/marathon-keyboard/qml/Core/MarathonKeyboard.qml +++ b/marathon-keyboard/qml/Core/MarathonKeyboard.qml @@ -1,6 +1,3 @@ -// Marathon Virtual Keyboard - Main Container -// BlackBerry 10-inspired keyboard with Marathon design -// OPTIMIZED FOR ZERO LATENCY INPUT import QtQuick import MarathonKeyboard.UI 1.0 import MarathonKeyboard.Layouts 1.0 @@ -8,10 +5,8 @@ import MarathonKeyboard.Layouts 1.0 Rectangle { id: keyboard - // Abstraction properties - set by parent/shell property real scaleFactor: 1.0 - // Theme properties - passed from shell property color backgroundColor: "#1E1E1E" property color keyBackgroundColor: "#2D2D30" property color keyPressedColor: "#007ACC" @@ -21,11 +16,9 @@ Rectangle { property real borderRadius: 4 property real keySpacing: 4 - // Signals for abstraction signal logMessage(string category, string message) signal hapticRequested(string intensity) - // State property bool active: false property string currentLayout: "qwerty" property bool shifted: false diff --git a/marathon-keyboard/qml/Data/AutoCorrect.qml b/marathon-keyboard/qml/Data/AutoCorrect.qml index 525c7af5b..e36354e4f 100644 --- a/marathon-keyboard/qml/Data/AutoCorrect.qml +++ b/marathon-keyboard/qml/Data/AutoCorrect.qml @@ -1,12 +1,9 @@ -// Marathon Virtual Keyboard - Auto Correct Engine -// Handles typo detection and auto-correction pragma Singleton import QtQuick QtObject { id: autoCorrect - // Common typo mappings property var commonTypos: ({ "teh": "the", "adn": "and", @@ -46,10 +43,8 @@ QtObject { "alot": "a lot" }) - // Auto-correction enabled property bool enabled: true - // Auto-correct a word function correct(word) { if (!enabled || !word || word.length === 0) { return word; @@ -57,21 +52,17 @@ QtObject { var lowerWord = word.toLowerCase(); - // Check common typos first if (commonTypos.hasOwnProperty(lowerWord)) { var correction = commonTypos[lowerWord]; keyboard.logMessage("AutoCorrect", "Correcting '" + word + "' to '" + correction + "'"); return correction; } - // Check if word exists in dictionary if (!Dictionary.hasWord(lowerWord)) { - // Try to find close matches using edit distance var suggestions = Dictionary.predict(lowerWord); if (suggestions.length > 0) { var bestMatch = suggestions[0]; - // Only auto-correct if edit distance is 1 or 2 var distance = levenshteinDistance(lowerWord, bestMatch.toLowerCase()); if (distance <= 2) { keyboard.logMessage("AutoCorrect", "Correcting '" + word + "' to '" + bestMatch + "' (distance: " + distance + ")"); @@ -83,7 +74,6 @@ QtObject { return word; } - // Calculate Levenshtein distance between two strings function levenshteinDistance(a, b) { if (a.length === 0) return b.length; @@ -92,7 +82,6 @@ QtObject { var matrix = []; - // Initialize matrix for (var i = 0; i <= b.length; i++) { matrix[i] = [i]; } @@ -101,16 +90,12 @@ QtObject { matrix[0][j] = j; } - // Fill matrix for (var i = 1; i <= b.length; i++) { for (var j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { - matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1 // substitution - , matrix[i][j - 1] + 1 // insertion - , matrix[i - 1][j] + 1 // deletion - ); + matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); } } } @@ -118,7 +103,6 @@ QtObject { return matrix[b.length][a.length]; } - // Check if a correction should be suggested function shouldCorrect(word) { if (!enabled || !word || word.length < 3) { return null; @@ -132,7 +116,6 @@ QtObject { return null; } - // Learn a typo correction from user function learnCorrection(typo, correction) { commonTypos[typo.toLowerCase()] = correction; keyboard.logMessage("AutoCorrect", "Learned: '" + typo + "' -> '" + correction + "'"); diff --git a/marathon-keyboard/qml/Data/Dictionary.qml b/marathon-keyboard/qml/Data/Dictionary.qml index 932e507b0..f4ec5140c 100644 --- a/marathon-keyboard/qml/Data/Dictionary.qml +++ b/marathon-keyboard/qml/Data/Dictionary.qml @@ -1,13 +1,9 @@ -// Marathon Virtual Keyboard - Dictionary -// Simple dictionary with frequency-based word suggestions pragma Singleton import QtQuick QtObject { id: dictionary - // Top 1000 most common English words with frequencies - // Format: {word: "the", freq: 10000} property var words: [ { word: "the", @@ -503,10 +499,8 @@ QtObject { } ] - // User's personal dictionary (learned words) property var userWords: [] - // Get predictions for a given prefix function predict(prefix) { if (!prefix || prefix.length === 0) { return []; @@ -514,41 +508,33 @@ QtObject { const lowerPrefix = prefix.toLowerCase(); - // Combine system and user dictionaries const allWords = words.concat(userWords); - // Filter words that start with prefix const matches = allWords.filter(function (entry) { return entry.word.toLowerCase().startsWith(lowerPrefix); }); - // Sort by frequency (higher first) matches.sort(function (a, b) { return b.freq - a.freq; }); - // Return top 3 matches return matches.slice(0, 3).map(function (entry) { return entry.word; }); } - // Learn a new word from user input function learnWord(word) { if (!word || word.length < 2) { return; } - // Check if word already exists in user dictionary const existing = userWords.findIndex(function (entry) { return entry.word.toLowerCase() === word.toLowerCase(); }); if (existing >= 0) { - // Increase frequency userWords[existing].freq += 10; } else { - // Add new word with medium frequency userWords.push({ word: word, freq: 100 @@ -558,7 +544,6 @@ QtObject { keyboard.logMessage("Dictionary", "Learned word: " + word); } - // Check if a word exists in dictionary function hasWord(word) { const lowerWord = word.toLowerCase(); @@ -574,7 +559,6 @@ QtObject { }); } - // Get word frequency (for auto-correction scoring) function getFrequency(word) { const lowerWord = word.toLowerCase(); diff --git a/marathon-keyboard/qml/Input/InputContext.qml b/marathon-keyboard/qml/Input/InputContext.qml index 5757e7fee..dd96c4ee8 100644 --- a/marathon-keyboard/qml/Input/InputContext.qml +++ b/marathon-keyboard/qml/Input/InputContext.qml @@ -1,27 +1,20 @@ -// Marathon Virtual Keyboard - Input Context -// Manages input field focus and provides proper key event handling -// OPTIMIZED for low-latency text input import QtQuick Item { id: inputContext - // PERFORMANCE: visible false reduces overhead when not managing focus visible: false width: 0 height: 0 - // Track if we're in an input field using Qt.inputMethod readonly property bool hasActiveFocus: Qt.inputMethod && Qt.inputMethod.visible - // Input mode (for context-aware predictions) - property string inputMode: "text" // text, email, url, number + property string inputMode: "text" signal textInserted(string text) signal backspacePressed signal enterPressed - // Monitor Qt.inputMethod visible state Connections { target: Qt.inputMethod function onVisibleChanged() { @@ -34,60 +27,41 @@ Item { } } - // Insert text at cursor position function insertText(text) { - // Qt.inputMethod.commit() in Qt6 doesn't take arguments - // Text input is handled by C++ IME backend (InputMethodEngine) - // This function is kept for API compatibility textInserted(text); keyboard.logMessage("InputContext", "Text input signaled: " + text); } - // Handle backspace function handleBackspace() { - // Send backspace through the C++ IME backend (already wired in VirtualKeyboard.qml) backspacePressed(); } - // Handle enter/return function handleEnter() { - // Send enter through the C++ IME backend enterPressed(); } - // Replace current word with suggestion function replaceCurrentWord(newWord) { var currentWord = getCurrentWord(); if (currentWord.length === 0) { - // No current word, just insert the new word insertText(newWord); return; } - // Delete the current word (send backspace for each character) for (var i = 0; i < currentWord.length; i++) { handleBackspace(); } - // Insert the new word insertText(newWord); keyboard.logMessage("InputContext", "Replaced '" + currentWord + "' with: " + newWord); } - // Get current word being typed function getCurrentWord() { - // NOTE: This is a simplified version - in reality we'd need to query - // the actual input field's text. For now, we track it in MarathonKeyboard. - // This function is kept for API compatibility but may return empty string. - keyboard.logMessage("InputContext", "getCurrentWord() called - should use keyboard.currentWord instead"); return ""; } - // Detect input mode from focused input function detectInputMode() { - // For now, default to "text" - could be enhanced to detect from Qt.inputMethod hints inputMode = "text"; keyboard.logMessage("InputContext", "Input mode: " + inputMode); } diff --git a/marathon-keyboard/qml/Layouts/QwertyLayout.qml b/marathon-keyboard/qml/Layouts/QwertyLayout.qml index 6ac121a28..f029c8404 100644 --- a/marathon-keyboard/qml/Layouts/QwertyLayout.qml +++ b/marathon-keyboard/qml/Layouts/QwertyLayout.qml @@ -1,5 +1,3 @@ -// Marathon Virtual Keyboard - QWERTY Layout -// BlackBerry 10-style QWERTY keyboard layout import QtQuick import "../UI" import "../Data" @@ -7,14 +5,11 @@ import "../Data" Item { id: layout - // Properties property bool shifted: false property bool capsLock: false - // Expose Column's implicit height implicitHeight: layoutColumn.implicitHeight - // Signals signal keyClicked(string text) signal backspaceClicked signal enterClicked @@ -23,7 +18,6 @@ Item { signal layoutSwitchClicked(string layout) signal dismissClicked - // Key definitions with alternates readonly property var row1Keys: [ { char: "q", @@ -140,7 +134,6 @@ Item { width: parent.width spacing: 0 - // Row 1: Q W E R T Y U I O P Row { width: parent.width spacing: Math.round(1 * scaleFactor) @@ -166,15 +159,13 @@ Item { } } - // Separator line between Row 1 and Row 2 Rectangle { width: parent.width height: Math.round(2 * scaleFactor) - color: "#666666" // BRIGHT grey line - very visible + color: "#666666" opacity: 1.0 } - // Row 2: A S D F G H J K L Row { width: parent.width spacing: Math.round(1 * scaleFactor) @@ -200,7 +191,6 @@ Item { } } - // Separator line between Row 2 and Row 3 Rectangle { width: parent.width height: Math.round(2 * scaleFactor) @@ -208,14 +198,12 @@ Item { opacity: 1.0 } - // Row 3: Shift Z X C V B N M Backspace Row { id: row3 width: parent.width spacing: Math.round(1 * scaleFactor) property real availableWidth: width - spacing * 8 - // Shift key (wider) Key { width: row3.availableWidth * 0.15 text: "shift" @@ -228,7 +216,6 @@ Item { } } - // Letter keys (equal width) Repeater { model: row3Keys @@ -248,7 +235,6 @@ Item { } } - // Backspace key (wider) Key { width: row3.availableWidth * 0.15 text: "backspace" @@ -261,7 +247,6 @@ Item { } } - // Separator line between Row 3 and Row 4 Rectangle { width: parent.width height: Math.round(2 * scaleFactor) @@ -269,14 +254,12 @@ Item { opacity: 1.0 } - // Row 4: 123, Comma, Space, Dismiss, Period, Enter (BlackBerry 10 exact style) Row { id: row4 width: parent.width spacing: Math.round(1 * scaleFactor) property real availableWidth: width - spacing * 5 - // 123 key (switch to numbers) Key { width: row4.availableWidth * 0.12 text: "123" @@ -288,7 +271,6 @@ Item { } } - // Comma key Key { width: row4.availableWidth * 0.08 text: "," @@ -299,7 +281,6 @@ Item { } } - // Space bar (MASSIVE - 50% of row) Key { width: row4.availableWidth * 0.50 text: " " @@ -311,7 +292,6 @@ Item { } } - // Dismiss key (keyboard down icon) - LEFT OF ENTER Key { width: row4.availableWidth * 0.08 text: "dismiss" @@ -323,7 +303,6 @@ Item { } } - // Period key Key { width: row4.availableWidth * 0.08 text: "." @@ -334,7 +313,6 @@ Item { } } - // Enter key (rightmost) Key { width: row4.availableWidth * 0.14 text: "enter" diff --git a/marathon-keyboard/qml/Layouts/SymbolLayout.qml b/marathon-keyboard/qml/Layouts/SymbolLayout.qml index 591d92c4b..e5ce98d39 100644 --- a/marathon-keyboard/qml/Layouts/SymbolLayout.qml +++ b/marathon-keyboard/qml/Layouts/SymbolLayout.qml @@ -1,5 +1,3 @@ -// Marathon Virtual Keyboard - Symbol Layout -// Special characters and symbols import QtQuick import "../UI" @@ -8,7 +6,6 @@ Item { property bool shifted: false - // Expose Column's implicit height implicitHeight: layoutColumn.implicitHeight signal keyClicked(string text) @@ -18,7 +15,6 @@ Item { signal layoutSwitchClicked(string layout) signal dismissClicked - // Symbol key definitions readonly property var row1Keys: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] readonly property var row2Keys: ["@", "#", "$", "_", "&", "-", "+", "(", ")", "/"] readonly property var row3Keys: ["*", "\"", "'", ":", ";", "!", "?", "~", "`"] @@ -28,7 +24,6 @@ Item { width: parent.width spacing: 0 - // Row 1: 1 2 3 4 5 6 7 8 9 0 Row { width: parent.width spacing: Math.round(1 * scaleFactor) @@ -49,15 +44,13 @@ Item { } } - // Separator line between Row 1 and Row 2 Rectangle { width: parent.width height: Math.round(2 * scaleFactor) - color: "#666666" // BRIGHT grey line - very visible + color: "#666666" opacity: 1.0 } - // Row 2: @ # $ _ & - + ( ) / Row { width: parent.width spacing: Math.round(1 * scaleFactor) @@ -78,7 +71,6 @@ Item { } } - // Separator line between Row 2 and Row 3 Rectangle { width: parent.width height: 1 @@ -86,14 +78,12 @@ Item { opacity: 0.3 } - // Row 3: =\< * " ' : ; ! ? ~ ` Backspace Row { id: row3 width: parent.width spacing: Math.round(1 * scaleFactor) property real availableWidth: width - spacing * 10 - // Shift key (switches to alternate symbols) Key { width: row3.availableWidth * 0.15 text: "=\\<" @@ -105,7 +95,6 @@ Item { } } - // Symbol keys Repeater { model: row3Keys @@ -120,7 +109,6 @@ Item { } } - // Backspace key Key { width: row3.availableWidth * 0.15 text: "backspace" @@ -133,7 +121,6 @@ Item { } } - // Separator line between Row 3 and Row 4 Rectangle { width: parent.width height: 1 @@ -141,14 +128,12 @@ Item { opacity: 0.3 } - // Row 4: ABC, Comma, Space, Dismiss, Period, Enter (matching QWERTY layout) Row { id: row4 width: parent.width spacing: Math.round(1 * scaleFactor) property real availableWidth: width - spacing * 5 - // ABC key (back to letters) Key { width: row4.availableWidth * 0.12 text: "ABC" @@ -160,7 +145,6 @@ Item { } } - // Comma key Key { width: row4.availableWidth * 0.08 text: "," @@ -171,7 +155,6 @@ Item { } } - // Space bar (MASSIVE - 50% of row) Key { width: row4.availableWidth * 0.50 text: " " @@ -183,7 +166,6 @@ Item { } } - // Dismiss key (keyboard down icon) - LEFT OF ENTER Key { width: row4.availableWidth * 0.08 text: "dismiss" @@ -195,7 +177,6 @@ Item { } } - // Period key Key { width: row4.availableWidth * 0.08 text: "." @@ -206,7 +187,6 @@ Item { } } - // Enter key (rightmost) Key { width: row4.availableWidth * 0.14 text: "enter" diff --git a/marathon-keyboard/qml/UI/Key.qml b/marathon-keyboard/qml/UI/Key.qml index dd8a21fcb..5eb053218 100644 --- a/marathon-keyboard/qml/UI/Key.qml +++ b/marathon-keyboard/qml/UI/Key.qml @@ -1,5 +1,3 @@ -// Marathon Virtual Keyboard - Key Component -// Optimized for zero-latency input import QtQuick import QtQuick.Effects import MarathonUI.Core 2.0 @@ -7,35 +5,29 @@ import MarathonUI.Core 2.0 Rectangle { id: key - // Reference to parent keyboard for theme access property var keyboard: null property real scaleFactor: keyboard ? keyboard.scaleFactor : 1.0 - // Key properties property string text: "" property string displayText: text - property string alternateText: "" // For long-press (e.g., "1" shows "!") - property var alternateChars: [] // Array of alternate characters - property bool isSpecial: false // Shift, Enter, Backspace - property string iconName: "" // Lucide icon for special keys + property string alternateText: "" + property var alternateChars: [] + property bool isSpecial: false + property string iconName: "" property int keyCode: Qt.Key_unknown - // State property bool pressed: false property bool highlighted: false property bool showingAlternates: false - // PERFORMANCE: Cache text metrics to avoid re-layout property real cachedTextWidth: 0 property real cachedTextHeight: 0 - // Signals signal clicked signal pressAndHold signal released signal alternateSelected(string character) - // Styling - BlackBerry style: BLACK keys, dark grey special keys width: Math.round(60 * scaleFactor) height: Math.round(45 * scaleFactor) radius: keyboard ? keyboard.borderRadius : 4 @@ -43,39 +35,35 @@ Rectangle { if (pressed) return keyboard ? keyboard.keyPressedColor : "#007ACC"; if (isSpecial) - return "#1a1a1a"; // Dark grey for special keys - return keyboard ? keyboard.keyBackgroundColor : "#000000"; // Key background + return "#1a1a1a"; + return keyboard ? keyboard.keyBackgroundColor : "#000000"; } - // Physical button styling - DARK borders, partial (bottom/right only) - border.width: 0 // No full border + border.width: 0 antialiasing: true - // Bottom border (darker, creates depth) Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: Math.round(2 * scaleFactor) - color: "#0a0a0a" // Very dark, almost black + color: "#0a0a0a" radius: 0 } - // Right border (darker, creates depth) Rectangle { anchors.top: parent.top anchors.right: parent.right anchors.bottom: parent.bottom width: Math.round(2 * scaleFactor) - color: "#0a0a0a" // Very dark, almost black + color: "#0a0a0a" radius: 0 } - // PERFORMANCE: Use fast color animation instead of general Behavior Behavior on color { ColorAnimation { - duration: 50 // Fixed duration for predictability - easing.type: Easing.Linear // Linear is fastest + duration: 50 + easing.type: Easing.Linear } } @@ -86,7 +74,6 @@ Rectangle { } } - // PERFORMANCE: Use NumberAnimation for scale and translate Behavior on scale { NumberAnimation { duration: 80 @@ -103,29 +90,25 @@ Rectangle { } } - // Pop over effect when pressed: scale up slightly and lift scale: pressed ? 1.05 : 1.0 y: pressed ? -Math.round(3 * scaleFactor) : 0 z: pressed ? 100 : 1 - // Inner border for physical button depth (shine effect) Rectangle { anchors.fill: parent anchors.margins: Math.round(2 * scaleFactor) radius: parent.radius > 0 ? parent.radius - 2 : 0 color: "transparent" border.width: Math.round(1 * scaleFactor) - border.color: key.pressed ? (keyboard ? keyboard.keyPressedColor : "#007ACC") : "#555555" // Lighter grey for depth + border.color: key.pressed ? (keyboard ? keyboard.keyPressedColor : "#007ACC") : "#555555" antialiasing: parent.antialiasing } - // Content: Either icon or text Item { anchors.centerIn: parent width: parent.width - Math.round(12 * scaleFactor) height: parent.height - Math.round(8 * scaleFactor) - // Icon for special keys Icon { visible: key.iconName !== "" name: key.iconName @@ -135,7 +118,6 @@ Rectangle { opacity: key.pressed ? 1.0 : 0.9 } - // Text for regular keys Text { visible: key.iconName === "" text: key.displayText @@ -148,7 +130,6 @@ Rectangle { opacity: key.pressed ? 1.0 : 0.9 } - // Alternate text (top-right corner for long-press hint) Text { visible: key.alternateText !== "" && !key.pressed text: key.alternateText @@ -161,7 +142,6 @@ Rectangle { } } - // Character preview popup (BB10 style) - HIDDEN when showing alternates Rectangle { id: preview visible: key.pressed && !key.isSpecial && !key.showingAlternates @@ -177,18 +157,16 @@ Rectangle { border.color: keyboard ? keyboard.borderColor : "#3E3E42" antialiasing: true - // Inner border - Marathon teal highlight Rectangle { anchors.fill: parent anchors.margins: 1 radius: parent.radius - 1 color: "transparent" border.width: Math.round(1 * scaleFactor) - border.color: keyboard ? keyboard.keyPressedColor : "#00D4AA" // Marathon teal + border.color: keyboard ? keyboard.keyPressedColor : "#00D4AA" antialiasing: true } - // Shadow effect layer.enabled: true layer.effect: MultiEffect { shadowEnabled: true @@ -197,7 +175,6 @@ Rectangle { shadowOpacity: 0.6 } - // Preview text (larger) Text { text: key.displayText color: keyboard ? keyboard.textColor : "#FFFFFF" @@ -207,31 +184,25 @@ Rectangle { } } - // Long-press alternate popup - DYNAMIC positioning to prevent off-screen Loader { id: alternatePopup active: key.showingAlternates && key.alternateChars.length > 0 z: 2000 - // Dynamic positioning based on key location property real popupWidth: Math.round((60 * key.alternateChars.length + 4 * (key.alternateChars.length - 1)) * scaleFactor) property real keyGlobalX: key.mapToItem(null, 0, 0).x property real screenWidth: keyboard ? keyboard.width : 540 - // Center by default, but shift if too close to edge x: { var centerX = (key.width - popupWidth) / 2; var leftEdge = keyGlobalX + centerX; var rightEdge = leftEdge + popupWidth; if (leftEdge < 0) { - // Too far left, shift right return -keyGlobalX; } else if (rightEdge > screenWidth) { - // Too far right, shift left return screenWidth - keyGlobalX - popupWidth; } else { - // Centered is fine return centerX; } } @@ -245,7 +216,7 @@ Rectangle { radius: keyboard ? keyboard.borderRadius : 4 color: keyboard ? keyboard.keyBackgroundColor : "#2D2D30" border.width: Math.round(2 * scaleFactor) - border.color: keyboard ? keyboard.keyPressedColor : "#00D4AA" // Marathon teal + border.color: keyboard ? keyboard.keyPressedColor : "#00D4AA" antialiasing: true layer.enabled: true @@ -292,27 +263,22 @@ Rectangle { } } - // Touch handling - OPTIMIZED FOR ZERO LATENCY MouseArea { id: mouseArea anchors.fill: parent property bool longPressTriggered: false - // CRITICAL: onPressed fires IMMEDIATELY (synchronous) - // This gives instant visual feedback before event propagation onPressed: function (mouse) { - key.pressed = true; // INSTANT visual change + key.pressed = true; longPressTriggered = false; if (keyboard) keyboard.hapticRequested("light"); - // Start long-press timer if alternates exist if (key.alternateChars.length > 0) { longPressTimer.restart(); } - // Accept event to prevent propagation delay mouse.accepted = true; } @@ -322,7 +288,6 @@ Rectangle { if (!longPressTriggered && containsMouse) { if (!key.showingAlternates) { - // Emit clicked immediately key.clicked(); } } @@ -339,7 +304,6 @@ Rectangle { } } - // Long-press timer Timer { id: longPressTimer interval: 500 diff --git a/marathon-keyboard/qml/UI/LongPressPopup.qml b/marathon-keyboard/qml/UI/LongPressPopup.qml index 38eb4e44a..6cdb993ac 100644 --- a/marathon-keyboard/qml/UI/LongPressPopup.qml +++ b/marathon-keyboard/qml/UI/LongPressPopup.qml @@ -1,11 +1,8 @@ -// Marathon Virtual Keyboard - Long Press Handler -// Handles long-press for alternate characters import QtQuick Item { id: longPressHandler - // Alternate character mappings readonly property var alternates: ({ "a": ["à", "á", "â", "ã", "ä", "å", "æ", "ā"], "e": ["è", "é", "ê", "ë", "ē", "ė", "ę"], @@ -39,7 +36,6 @@ Item { signal alternateSelected(string character) - // Get alternates for a character function getAlternates(character) { var lowerChar = character.toLowerCase(); if (alternates.hasOwnProperty(lowerChar)) { @@ -48,7 +44,6 @@ Item { return []; } - // Check if character has alternates function hasAlternates(character) { var lowerChar = character.toLowerCase(); return alternates.hasOwnProperty(lowerChar); diff --git a/marathon-keyboard/qml/UI/PredictionBar.qml b/marathon-keyboard/qml/UI/PredictionBar.qml index fba191f67..60df6e362 100644 --- a/marathon-keyboard/qml/UI/PredictionBar.qml +++ b/marathon-keyboard/qml/UI/PredictionBar.qml @@ -1,26 +1,21 @@ -// Marathon Virtual Keyboard - Prediction Bar -// Word suggestions bar above keyboard (BlackBerry style) import QtQuick Rectangle { id: predictionBar - property var predictions: [] // Array of suggested words + property var predictions: [] property string currentWord: "" signal predictionSelected(string word) - // Always expose a stable height via implicitHeight implicitHeight: Math.round(40 * scaleFactor) - // HIDE when no predictions (user request) visible: predictions.length > 0 height: visible ? implicitHeight : 0 color: MColors.surface border.width: 0 border.color: "transparent" - // Predictions display Row { anchors.centerIn: parent spacing: Math.round(12 * scaleFactor) @@ -29,7 +24,6 @@ Rectangle { Repeater { model: predictionBar.predictions - // Prediction button Rectangle { width: Math.round(100 * scaleFactor) height: Math.round(32 * scaleFactor) @@ -56,7 +50,6 @@ Rectangle { scale: predictionMouseArea.pressed ? 0.95 : 1.0 - // Inner border Rectangle { anchors.fill: parent anchors.margins: 1 @@ -67,7 +60,6 @@ Rectangle { antialiasing: parent.antialiasing } - // Prediction text Text { anchors.centerIn: parent text: modelData @@ -89,7 +81,6 @@ Rectangle { } } - // Placeholder when no predictions Text { anchors.centerIn: parent text: predictionBar.currentWord ? "..." : "" diff --git a/marathon-keyboard/src/marathoninputmethodengine.cpp b/marathon-keyboard/src/marathoninputmethodengine.cpp index a524cfd59..e19df992f 100644 --- a/marathon-keyboard/src/marathoninputmethodengine.cpp +++ b/marathon-keyboard/src/marathoninputmethodengine.cpp @@ -1,4 +1,4 @@ -// Marathon Input Method Engine - Implementation + #include "marathoninputmethodengine.h" #include #include @@ -10,7 +10,7 @@ MarathonInputMethodEngine::MarathonInputMethodEngine(QObject *parent) , m_active(false) , m_preeditText("") , m_inputMethod(nullptr) { - // Get Qt's input method instance + m_inputMethod = QGuiApplication::inputMethod(); if (m_inputMethod) { @@ -31,7 +31,6 @@ void MarathonInputMethodEngine::connectToInputMethod() { if (!m_inputMethod) return; - // Connect to input method signals connect(m_inputMethod, &QInputMethod::visibleChanged, this, &MarathonInputMethodEngine::onInputMethodVisibleChanged); connect(m_inputMethod, &QInputMethod::animatingChanged, this, @@ -67,7 +66,6 @@ bool MarathonInputMethodEngine::hasActiveFocus() const { if (!m_inputMethod) return false; - // Check if there's an active input item return m_inputMethod->isVisible() || m_inputMethod->inputDirection() != Qt::LayoutDirectionAuto; } @@ -75,9 +73,6 @@ int MarathonInputMethodEngine::cursorPosition() const { if (!m_inputMethod) return 0; - // Get cursor position from input method - // Note: This is a simplified implementation - // Real cursor position would need to be queried from the focused input item return m_inputMethod->cursorRectangle().x(); } @@ -96,13 +91,8 @@ void MarathonInputMethodEngine::commitText(const QString &text) { qDebug() << "[MarathonIME] Committing text:" << text; - // Qt 6 API: commit() takes no arguments - // We need to send key events or use Qt.inputMethod.commit() from QML - // For now, simulate typing by sending key events for (const QChar &ch : text) { - QKeyEvent *pressEvent = new QKeyEvent(QEvent::KeyPress, - 0, // Key code (0 for text input) - Qt::NoModifier, QString(ch)); + QKeyEvent *pressEvent = new QKeyEvent(QEvent::KeyPress, 0, Qt::NoModifier, QString(ch)); QKeyEvent *releaseEvent = new QKeyEvent(QEvent::KeyRelease, 0, Qt::NoModifier, QString(ch)); @@ -115,7 +105,6 @@ void MarathonInputMethodEngine::commitText(const QString &text) { delete releaseEvent; } - // Clear preedit if (!m_preeditText.isEmpty()) { m_preeditText.clear(); emit preeditTextChanged(); @@ -125,13 +114,11 @@ void MarathonInputMethodEngine::commitText(const QString &text) { void MarathonInputMethodEngine::sendBackspace() { qDebug() << "[MarathonIME] Sending backspace"; - // Create and send a backspace key event QKeyEvent *pressEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier, ""); QKeyEvent *releaseEvent = new QKeyEvent(QEvent::KeyRelease, Qt::Key_Backspace, Qt::NoModifier, ""); - // Send to the application's focused widget if (QGuiApplication::focusObject()) { QGuiApplication::sendEvent(QGuiApplication::focusObject(), pressEvent); QGuiApplication::sendEvent(QGuiApplication::focusObject(), releaseEvent); @@ -144,13 +131,11 @@ void MarathonInputMethodEngine::sendBackspace() { void MarathonInputMethodEngine::sendEnter() { qDebug() << "[MarathonIME] Sending enter"; - // Create and send an enter key event QKeyEvent *pressEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier, "\n"); QKeyEvent *releaseEvent = new QKeyEvent(QEvent::KeyRelease, Qt::Key_Return, Qt::NoModifier, "\n"); - // Send to the application's focused widget if (QGuiApplication::focusObject()) { QGuiApplication::sendEvent(QGuiApplication::focusObject(), pressEvent); QGuiApplication::sendEvent(QGuiApplication::focusObject(), releaseEvent); @@ -163,32 +148,29 @@ void MarathonInputMethodEngine::sendEnter() { void MarathonInputMethodEngine::replacePreedit(const QString &word) { qDebug() << "[MarathonIME] Replacing preedit with:" << word; - // First, delete the current preedit text if (!m_preeditText.isEmpty()) { for (int i = 0; i < m_preeditText.length(); ++i) { sendBackspace(); } } - // Then commit the new word commitText(word); } QString MarathonInputMethodEngine::getTextBeforeCursor(int length) { - // This would require querying the input item directly - // For now, return empty - real implementation would use QInputMethodQueryEvent + qDebug() << "[MarathonIME] getTextBeforeCursor requested (length:" << length << ")"; return ""; } QString MarathonInputMethodEngine::getTextAfterCursor(int length) { - // This would require querying the input item directly + qDebug() << "[MarathonIME] getTextAfterCursor requested (length:" << length << ")"; return ""; } QString MarathonInputMethodEngine::getCurrentWord() { - // Return the current preedit text as the current word + return m_preeditText; } diff --git a/marathon-keyboard/src/marathoninputmethodengine.h b/marathon-keyboard/src/marathoninputmethodengine.h index a50da2b09..23e7184eb 100644 --- a/marathon-keyboard/src/marathoninputmethodengine.h +++ b/marathon-keyboard/src/marathoninputmethodengine.h @@ -1,5 +1,5 @@ -// Marathon Input Method Engine - Qt Platform Integration -// Proper IME implementation for system-level text input + + #ifndef MARATHONINPUTMETHODENGINE_H #define MARATHONINPUTMETHODENGINE_H @@ -9,23 +9,9 @@ #include #include -/** - * @brief Marathon Input Method Engine - * - * This class integrates with Qt's input method system to provide - * proper text input from the virtual keyboard to any text field. - * - * Key responsibilities: - * - Register as system input method - * - Handle focus changes - * - Commit text to focused input fields - * - Manage cursor position - * - Provide input method hints - */ class MarathonInputMethodEngine : public QObject { Q_OBJECT - // Properties exposed to QML Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) Q_PROPERTY(QString preeditText READ preeditText WRITE setPreeditText NOTIFY preeditTextChanged) Q_PROPERTY(bool hasActiveFocus READ hasActiveFocus NOTIFY hasActiveFocusChanged) @@ -36,75 +22,34 @@ class MarathonInputMethodEngine : public QObject { explicit MarathonInputMethodEngine(QObject *parent = nullptr); ~MarathonInputMethodEngine(); - // Property getters bool active() const { return m_active; } QString preeditText() const { return m_preeditText; } - bool hasActiveFocus() const; - int cursorPosition() const; - QRect inputItemRect() const; - - // Property setters - void setActive(bool active); - void setPreeditText(const QString &text); - - /** - * @brief Commit text to the currently focused input field - * @param text The text to commit - * - * This is the primary method for inserting text from the keyboard. - * It uses Qt.inputMethod to commit text properly. - */ - Q_INVOKABLE void commitText(const QString &text); - - /** - * @brief Send a backspace key event - * - * Deletes the character before the cursor. - */ - Q_INVOKABLE void sendBackspace(); - - /** - * @brief Send an enter/return key event - */ - Q_INVOKABLE void sendEnter(); - - /** - * @brief Replace the current preedit text with a new word - * @param word The new word to commit - * - * Used for accepting predictions/autocorrections. - */ - Q_INVOKABLE void replacePreedit(const QString &word); - - /** - * @brief Get the text before the cursor - * @param length Maximum length to retrieve - * @return Text before cursor - */ + bool hasActiveFocus() const; + int cursorPosition() const; + QRect inputItemRect() const; + + void setActive(bool active); + void setPreeditText(const QString &text); + + Q_INVOKABLE void commitText(const QString &text); + + Q_INVOKABLE void sendBackspace(); + + Q_INVOKABLE void sendEnter(); + + Q_INVOKABLE void replacePreedit(const QString &word); + Q_INVOKABLE QString getTextBeforeCursor(int length = 100); - /** - * @brief Get the text after the cursor - * @param length Maximum length to retrieve - * @return Text after cursor - */ Q_INVOKABLE QString getTextAfterCursor(int length = 100); - /** - * @brief Get the current word being typed - * @return Current word (text from last space/newline to cursor) - */ Q_INVOKABLE QString getCurrentWord(); - /** - * @brief Show/hide the virtual keyboard - * @param show True to show, false to hide - */ - Q_INVOKABLE void showKeyboard(bool show); + Q_INVOKABLE void showKeyboard(bool show); signals: void activeChanged(); @@ -113,24 +58,12 @@ class MarathonInputMethodEngine : public QObject { void cursorPositionChanged(); void inputItemRectChanged(); - /** - * @brief Emitted when an input field gains focus - */ void inputItemFocused(); - /** - * @brief Emitted when an input field loses focus - */ void inputItemUnfocused(); - /** - * @brief Emitted when the keyboard should be shown - */ void keyboardRequested(); - /** - * @brief Emitted when the keyboard should be hidden - */ void keyboardHideRequested(); private slots: @@ -147,4 +80,4 @@ class MarathonInputMethodEngine : public QObject { QInputMethod *m_inputMethod; }; -#endif // MARATHONINPUTMETHODENGINE_H +#endif diff --git a/marathon-keyboard/src/marathonkeyboardime.cpp b/marathon-keyboard/src/marathonkeyboardime.cpp index 1ca628600..17c09ec19 100644 --- a/marathon-keyboard/src/marathonkeyboardime.cpp +++ b/marathon-keyboard/src/marathonkeyboardime.cpp @@ -1,4 +1,4 @@ -// Marathon Keyboard Input Method Engine - Implementation + #include "marathonkeyboardime.h" #include #include @@ -6,8 +6,6 @@ #include #include -// ========== MarathonKeyboardIME Implementation ========== - MarathonKeyboardIME::MarathonKeyboardIME(QObject *parent) : QObject(parent) , m_autoCorrectEnabled(true) @@ -15,18 +13,15 @@ MarathonKeyboardIME::MarathonKeyboardIME(QObject *parent) , m_predictionThread(new QThread(this)) , m_predictionEngine(new PredictionEngine()) , m_dictionaryLoader(new DictionaryLoader()) { - // Move prediction engine to background thread + m_predictionEngine->moveToThread(m_predictionThread); m_dictionaryLoader->moveToThread(m_predictionThread); - // Connect signals connect(m_predictionEngine, &PredictionEngine::predictionsReady, this, &MarathonKeyboardIME::onPredictionsReady); - // Start background thread m_predictionThread->start(); - // Load dictionary asynchronously QMetaObject::invokeMethod(m_dictionaryLoader, "loadDictionary", Qt::QueuedConnection); qDebug() << "[MarathonKeyboardIME] Initialized with background prediction engine"; @@ -48,20 +43,16 @@ void MarathonKeyboardIME::setAutoCorrectEnabled(bool enabled) { } bool MarathonKeyboardIME::processKeyPress(const QString &character) { - // Start latency timer + m_latencyTimer.start(); - // Update current word immediately (< 0.1ms) m_currentWord.append(character); emit currentWordChanged(); - // Commit text immediately for zero perceived latency emit commitText(character); - // Trigger async prediction update updatePredictionsAsync(); - // Record latency qint64 latency = m_latencyTimer.elapsed(); updateLatencyMetrics(latency); @@ -75,7 +66,6 @@ void MarathonKeyboardIME::processBackspace() { m_currentWord.chop(1); emit currentWordChanged(); - // Update predictions if word still exists if (!m_currentWord.isEmpty()) { updatePredictionsAsync(); } else { @@ -94,21 +84,19 @@ void MarathonKeyboardIME::processSpace() { m_latencyTimer.start(); if (!m_currentWord.isEmpty()) { - // Apply auto-correct if enabled + QString finalWord = m_currentWord; if (m_autoCorrectEnabled) { QString corrected = applyAutoCorrect(m_currentWord); if (corrected != m_currentWord) { - // Replace current word with corrected version + emit replaceWord(m_currentWord, corrected); finalWord = corrected; } } - // Learn the word learnWord(finalWord); - // Clear current word m_currentWord.clear(); m_predictions.clear(); emit currentWordChanged(); @@ -138,13 +126,10 @@ void MarathonKeyboardIME::acceptPrediction(const QString &word) { return; } - // Replace current word with prediction emit replaceWord(m_currentWord, word); - // Learn the accepted word learnWord(word); - // Clear state m_currentWord.clear(); m_predictions.clear(); emit currentWordChanged(); @@ -156,7 +141,6 @@ QStringList MarathonKeyboardIME::getAlternates(const QString &character) const { return QStringList(); } - // Check cache first QChar ch = character.at(0).toLower(); QString key = QString(ch); @@ -164,7 +148,6 @@ QStringList MarathonKeyboardIME::getAlternates(const QString &character) const { return m_alternatesCache.value(key); } - // Return empty list if not found (alternates defined in QML for now) return QStringList(); } @@ -173,7 +156,6 @@ void MarathonKeyboardIME::learnWord(const QString &word) { return; } - // Update frequency asynchronously QMetaObject::invokeMethod( m_dictionaryLoader, [this, word]() { m_dictionaryLoader->updateFrequency(word, 10); }, Qt::QueuedConnection); @@ -204,7 +186,6 @@ void MarathonKeyboardIME::updatePredictionsAsync() { return; } - // Trigger prediction generation on background thread QMetaObject::invokeMethod(m_predictionEngine, "generatePredictions", Qt::QueuedConnection, Q_ARG(QString, m_currentWord)); } @@ -212,12 +193,10 @@ void MarathonKeyboardIME::updatePredictionsAsync() { void MarathonKeyboardIME::updateLatencyMetrics(qint64 latencyMs) { m_latencySamples.append(latencyMs); - // Keep only last 100 samples if (m_latencySamples.size() > 100) { m_latencySamples.removeFirst(); } - // Calculate average qint64 sum = 0; for (qint64 sample : m_latencySamples) { sum += sample; @@ -236,18 +215,14 @@ void MarathonKeyboardIME::updateLatencyMetrics(qint64 latencyMs) { } QString MarathonKeyboardIME::applyAutoCorrect(const QString &word) { - // Check cache first + if (m_autoCorrectCache.contains(word.toLower())) { return m_autoCorrectCache.value(word.toLower()); } - // Common typos (defined in QML AutoCorrect for now) - // Return word unchanged - QML layer handles this return word; } -// ========== PredictionEngine Implementation ========== - PredictionEngine::PredictionEngine(QObject *parent) : QObject(parent) , m_root(new TrieNode()) { @@ -260,15 +235,13 @@ void PredictionEngine::generatePredictions(const QString &prefix) { return; } - // Search trie for predictions QStringList predictions = searchTrie(prefix.toLower(), 3); emit predictionsReady(predictions); } void PredictionEngine::buildTrie() { - // Build trie from common words - // This is a placeholder - real implementation would load from file + QStringList commonWords = {"the", "be", "to", "of", "and", "a", "in", "that", "have", "I", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at", "this", "but", "his", "by", "from", "they"}; @@ -283,24 +256,21 @@ void PredictionEngine::buildTrie() { current = current->children[ch]; } current->isWord = true; - current->frequency = 100; // Default frequency + current->frequency = 100; } } QStringList PredictionEngine::searchTrie(const QString &prefix, int maxResults) { QStringList results; - // Navigate to prefix node - TrieNode *current = m_root; + TrieNode *current = m_root; for (const QChar &ch : prefix) { if (!current->children.contains(ch)) { - return results; // Prefix not found + return results; } current = current->children[ch]; } - // DFS to find words (simplified - real implementation would use priority queue) - // For now, just return if current is a word if (current->isWord) { results.append(prefix); } @@ -308,16 +278,12 @@ QStringList PredictionEngine::searchTrie(const QString &prefix, int maxResults) return results; } -// ========== DictionaryLoader Implementation ========== - DictionaryLoader::DictionaryLoader(QObject *parent) : QObject(parent) {} void DictionaryLoader::loadDictionary() { qDebug() << "[DictionaryLoader] Loading dictionary..."; - // Load common words with frequencies - // This is a placeholder - real implementation would load from file QStringList commonWords = {"the", "be", "to", "of", "and", "a", "in", "that", "have", "I", "it", "for", "not", "on", "with", "he", "as", "you"}; diff --git a/marathon-keyboard/src/marathonkeyboardime.h b/marathon-keyboard/src/marathonkeyboardime.h index 7c17ad209..37ad665fc 100644 --- a/marathon-keyboard/src/marathonkeyboardime.h +++ b/marathon-keyboard/src/marathonkeyboardime.h @@ -1,5 +1,5 @@ -// Marathon Keyboard Input Method Engine - C++ Backend -// High-performance keyboard with zero-latency input processing + + #ifndef MARATHONKEYBOARDIME_H #define MARATHONKEYBOARDIME_H @@ -14,19 +14,9 @@ #include #include -// Forward declarations class PredictionEngine; class DictionaryLoader; -/** - * @brief High-performance Input Method Engine for Marathon Keyboard - * - * This class handles all keyboard input with minimal latency by: - * - Processing touch events directly in C++ - * - Running predictions on background thread - * - Caching frequently used data - * - Using lock-free data structures where possible - */ class MarathonKeyboardIME : public QObject { Q_OBJECT Q_PROPERTY(QString currentWord READ currentWord NOTIFY currentWordChanged) @@ -39,7 +29,6 @@ class MarathonKeyboardIME : public QObject { explicit MarathonKeyboardIME(QObject *parent = nullptr); ~MarathonKeyboardIME(); - // Property getters QString currentWord() const { return m_currentWord; } @@ -53,63 +42,24 @@ class MarathonKeyboardIME : public QObject { return m_averageLatency; } - // Property setters - void setAutoCorrectEnabled(bool enabled); - - /** - * @brief Process a key press with minimal latency - * @param character The character to insert - * @return true if processed successfully - * - * This method is optimized for speed: - * - Updates current word immediately - * - Triggers async prediction update - * - Returns in < 1ms - */ - Q_INVOKABLE bool processKeyPress(const QString &character); - - /** - * @brief Process backspace with word tracking - */ - Q_INVOKABLE void processBackspace(); - - /** - * @brief Process space key (word completion) - */ - Q_INVOKABLE void processSpace(); - - /** - * @brief Process enter key - */ - Q_INVOKABLE void processEnter(); - - /** - * @brief Replace current word with prediction - * @param word The predicted word to use - */ - Q_INVOKABLE void acceptPrediction(const QString &word); - - /** - * @brief Get alternate characters for long-press - * @param character The base character - * @return List of alternate characters - */ + void setAutoCorrectEnabled(bool enabled); + + Q_INVOKABLE bool processKeyPress(const QString &character); + + Q_INVOKABLE void processBackspace(); + + Q_INVOKABLE void processSpace(); + + Q_INVOKABLE void processEnter(); + + Q_INVOKABLE void acceptPrediction(const QString &word); + Q_INVOKABLE QStringList getAlternates(const QString &character) const; - /** - * @brief Learn a new word or increase frequency - * @param word The word to learn - */ - Q_INVOKABLE void learnWord(const QString &word); + Q_INVOKABLE void learnWord(const QString &word); - /** - * @brief Clear current word state - */ - Q_INVOKABLE void clearCurrentWord(); + Q_INVOKABLE void clearCurrentWord(); - /** - * @brief Get performance metrics - */ Q_INVOKABLE QVariantMap getPerformanceMetrics() const; signals: @@ -118,63 +68,38 @@ class MarathonKeyboardIME : public QObject { void autoCorrectEnabledChanged(); void averageLatencyChanged(); - /** - * @brief Emitted when text should be committed to input field - * @param text The text to commit - * - * This is emitted AFTER visual feedback is shown, - * ensuring perceived zero latency - */ void commitText(const QString &text); - /** - * @brief Emitted when backspace should be applied - */ void commitBackspace(); - /** - * @brief Emitted when current word should be replaced - * @param oldWord The word to replace - * @param newWord The new word - */ void replaceWord(const QString &oldWord, const QString &newWord); private slots: void onPredictionsReady(const QStringList &predictions); private: - void updatePredictionsAsync(); - void updateLatencyMetrics(qint64 latencyMs); - QString applyAutoCorrect(const QString &word); + void updatePredictionsAsync(); + void updateLatencyMetrics(qint64 latencyMs); + QString applyAutoCorrect(const QString &word); - // Current state - QString m_currentWord; - QStringList m_predictions; - bool m_autoCorrectEnabled; + QString m_currentWord; + QStringList m_predictions; + bool m_autoCorrectEnabled; - // Performance tracking - int m_averageLatency; - QList m_latencySamples; - QElapsedTimer m_latencyTimer; + int m_averageLatency; + QList m_latencySamples; + QElapsedTimer m_latencyTimer; - // Background processing - QThread *m_predictionThread; - PredictionEngine *m_predictionEngine; - DictionaryLoader *m_dictionaryLoader; + QThread *m_predictionThread; + PredictionEngine *m_predictionEngine; + DictionaryLoader *m_dictionaryLoader; - // Thread safety - mutable QMutex m_mutex; + mutable QMutex m_mutex; - // Caches for performance QHash m_alternatesCache; QHash m_autoCorrectCache; }; -/** - * @brief Background prediction engine - * - * Runs on separate thread to avoid blocking UI - */ class PredictionEngine : public QObject { Q_OBJECT @@ -188,7 +113,6 @@ class PredictionEngine : public QObject { void predictionsReady(const QStringList &predictions); private: - // Trie or similar fast structure for predictions struct TrieNode { QChar character; int frequency; @@ -209,11 +133,6 @@ class PredictionEngine : public QObject { QStringList searchTrie(const QString &prefix, int maxResults = 3); }; -/** - * @brief Async dictionary loader - * - * Loads and indexes dictionary in background - */ class DictionaryLoader : public QObject { Q_OBJECT @@ -234,4 +153,4 @@ class DictionaryLoader : public QObject { mutable QMutex m_mutex; }; -#endif // MARATHONKEYBOARDIME_H +#endif From 0b3b521ddefffb06b256031ff94e9ef1fedecc6f Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:31:49 +0000 Subject: [PATCH 07/28] refactor(apps): align app UIs with updated services Update app QML and supporting C++ to use revised service APIs, improve bindings, and resolve runtime/lint issues across bundled apps. --- apps/CMakeLists.txt | 17 + apps/browser/BrowserApp.qml | 98 +----- apps/browser/components/BrowserDrawer.qml | 49 +-- apps/browser/components/TabCard.qml | 36 +-- apps/browser/components/WebView.qml | 12 +- apps/browser/pages/BookmarksPage.qml | 62 ++-- apps/browser/pages/BrowserSettingsPage.qml | 71 ++--- apps/browser/pages/HistoryPage.qml | 47 ++- apps/browser/pages/HomepagePage.qml | 20 +- apps/browser/pages/SearchEnginePage.qml | 17 +- apps/browser/pages/TabsPage.qml | 2 +- apps/browser/src/BrowserData.cpp | 1 - apps/browser/src/BrowserData.h | 4 +- apps/calculator/CalculatorApp.qml | 3 +- apps/calendar/CalendarApp.qml | 16 +- apps/calendar/components/EventListItem.qml | 19 +- apps/calendar/pages/CalendarGridPage.qml | 45 ++- apps/calendar/pages/EventCreationPage.qml | 52 ++-- apps/calendar/pages/EventDetailPage.qml | 28 +- apps/calendar/pages/EventListPage.qml | 31 +- apps/calendar/stores/CalendarStorage.qml | 173 ++++------- apps/camera/CameraApp.qml | 1 + apps/camera/src/CameraController.cpp | 9 +- apps/camera/src/CameraController.h | 6 +- apps/camera/src/SoftwareVideoOutput.cpp | 2 +- apps/camera/src/SoftwareVideoOutput.h | 8 +- apps/clock/ClockApp.qml | 187 ++++------- apps/clock/components/AddCityDialog.qml | 210 ++++++------- apps/clock/components/AlarmItem.qml | 13 +- apps/clock/components/ClockIcon.qml | 6 +- apps/clock/pages/AlarmEditorDialog.qml | 26 +- apps/clock/pages/AlarmPage.qml | 74 ++--- apps/clock/pages/ClockPage.qml | 124 +++----- apps/clock/pages/StopwatchPage.qml | 19 +- apps/clock/pages/TimerPage.qml | 2 +- apps/clock/pages/WorldClockPage.qml | 148 +++++---- apps/gallery/GalleryApp.qml | 9 +- apps/gallery/pages/AlbumDetailPage.qml | 1 - apps/gallery/pages/PhotoViewerPage.qml | 25 +- apps/maps/MapsApp.qml | 23 +- apps/messages/MessagesApp.qml | 130 ++++---- .../components/ConversationListItem.qml | 103 +++--- apps/messages/components/DateSeparator.qml | 50 ++- apps/messages/components/MessageBubble.qml | 58 ++-- apps/messages/components/MessageInputBar.qml | 11 +- apps/messages/pages/ChatPage.qml | 195 ++++++------ apps/messages/pages/ConversationsListPage.qml | 139 ++++----- apps/messages/pages/NewConversationPage.qml | 109 ++++--- apps/music/MusicApp.qml | 206 ++++++------ apps/notes/NotesApp.qml | 94 +++--- apps/notes/components/NoteItem.qml | 18 +- apps/notes/pages/NoteEditorPage.qml | 27 +- apps/notes/pages/NotesListPage.qml | 90 +++--- apps/phone/PhoneApp.qml | 35 +-- apps/phone/pages/ActiveCallPage.qml | 91 +++--- apps/phone/pages/ContactEditorPage.qml | 71 +++-- apps/phone/pages/ContactsPage.qml | 9 +- apps/phone/pages/DialerPage.qml | 72 ++--- apps/phone/pages/IncomingCallScreen.qml | 25 +- apps/settings/SettingsApp.qml | 29 -- .../components/BluetoothPairDialog.qml | 275 +++++++--------- .../components/SettingsPageTemplate.qml | 21 +- .../components/WiFiPasswordDialog.qml | 119 +++---- apps/settings/manifest.json | 2 +- apps/settings/pages/AppManagerPage.qml | 193 ++++++------ apps/settings/pages/AppSortPage.qml | 10 +- apps/settings/pages/BatteryPage.qml | 9 +- apps/settings/pages/BluetoothPage.qml | 247 +++++++-------- apps/settings/pages/CellularPage.qml | 4 - apps/settings/pages/DefaultAppsPage.qml | 235 +++++++------- apps/settings/pages/HiddenAppsPage.qml | 11 +- apps/settings/pages/KeyboardPage.qml | 56 +++- apps/settings/pages/NotificationsPage.qml | 6 +- apps/settings/pages/QuickSettingsPage.qml | 149 +++++---- apps/settings/pages/ScalePage.qml | 22 +- apps/settings/pages/SecurityPage.qml | 293 +++++++++--------- apps/settings/pages/SettingsMainPage.qml | 8 - apps/settings/pages/SoundPage.qml | 89 +++--- apps/settings/pages/SoundPickerPage.qml | 25 +- apps/settings/pages/StoragePage.qml | 18 +- apps/settings/pages/WiFiPage.qml | 67 ++-- apps/store/StoreApp.qml | 30 +- apps/store/components/AppListItem.qml | 38 +-- apps/store/components/FeaturedAppCard.qml | 45 +-- apps/store/components/InfoRow.qml | 12 +- apps/store/pages/AppDetailPage.qml | 271 +--------------- apps/store/pages/InstalledAppsPage.qml | 9 +- apps/store/pages/StoreFrontPage.qml | 49 ++- apps/store/pages/UpdatesPage.qml | 9 +- apps/template/TemplateApp.qml | 77 +++-- apps/template/pages/MainPage.qml | 7 +- apps/terminal/TerminalApp.qml | 70 ++--- apps/terminal/components/TerminalSession.qml | 124 +++----- .../terminal/components/TerminalTabButton.qml | 6 +- apps/terminal/components/VirtualKeyRow.qml | 124 ++++---- apps/terminal/src/TerminalEngine.cpp | 48 +-- apps/terminal/src/TerminalEngine.h | 8 +- apps/terminal/src/TerminalRenderer.cpp | 44 +-- apps/terminal/src/TerminalScreen.cpp | 39 +-- apps/terminal/src/TerminalScreen.h | 71 ++--- apps/test/TestApp.qml | 4 +- apps/test/manifest.json | 13 +- apps/test/pages/AlarmTestsPage.qml | 84 +++-- apps/test/pages/MediaTestsPage.qml | 139 ++++++--- apps/test/pages/NotificationTestsPage.qml | 41 +-- apps/test/pages/SensorTestsPage.qml | 66 +--- 106 files changed, 2952 insertions(+), 3593 deletions(-) diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index 8b4befb26..bb982f179 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -50,6 +50,19 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/QMLLint.cmake") include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/QMLLint.cmake) endif() +get_filename_component(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) +set(QML_IMPORT_PATH + "${REPO_ROOT}/build/qmllint" + "${REPO_ROOT}/build" + "${REPO_ROOT}/build/shell/qml" + CACHE STRING "" FORCE +) +set(MARATHON_QML_IMPORT_PATH + "${REPO_ROOT}/build/qmllint" + "${REPO_ROOT}/build" + "${REPO_ROOT}/build/shell/qml" +) + function(add_marathon_app APP_NAME) set(options "") @@ -119,6 +132,10 @@ function(add_marathon_app APP_NAME) "${MODULE_OUTPUT_DIR}" NO_PLUGIN_OPTIONAL) + set_property(TARGET ${APP_NAME} APPEND PROPERTY QT_QML_IMPORT_PATH + "${MARATHON_QML_IMPORT_PATH}" + ) + if(APPLE) install( CODE " diff --git a/apps/browser/BrowserApp.qml b/apps/browser/BrowserApp.qml index c528441db..e8d270d4a 100644 --- a/apps/browser/BrowserApp.qml +++ b/apps/browser/BrowserApp.qml @@ -25,33 +25,27 @@ MApp { property var history: [] property bool isPrivateMode: false property bool isAppActive: true - property var tabViewsById: ({}) // tabId(string) -> WebView + property var tabViewsById: ({}) property real drawerProgress: 0 property bool isDrawerOpen: false property bool isDragging: false - // Reference to drawer component (set after drawer is created) property var drawerRef: null property var webView: null property bool updatingTabUrl: false - // content: Rectangle { ... } is its own component boundary in Qt6; keep refs for cross-scope access. property var urlInputRef: null - // Address bar UX state property bool isEditingAddress: false property string addressEditText: "" - // Search Engine Settings property string searchEngineName: "DuckDuckGo" property string searchEngineUrl: "https://duckduckgo.com/?q=" property string homepageUrl: "https://duckduckgo.com" property string transientMessage: "" - // WebEngine can steal focus right after we highlight/select the address bar. - // Keep re-asserting focus briefly so typing always goes into the URL bar. property var _focusRetryItem: null property bool _focusRetrySelectAll: false property int _focusRetryAttempts: 0 property bool _pendingAddressBarFocus: false property bool _pendingAddressBarSelectAll: false property int _pendingAddressBarAttempts: 0 - property bool addressBarWantsFocus: false // Prevents WebView from stealing focus on startup + property bool addressBarWantsFocus: false function updateCurrentWebView() { var currentTab = getCurrentTab(); @@ -82,14 +76,10 @@ MApp { return; var wasPrivate = isPrivateMode; - // Preserve normal tabs on disk before entering private mode. if (!wasPrivate && enabled) saveTabs(); closeDrawer(); - // Keep “real tabs” behavior, but don’t mix profiles across a live session. - // Switching mode resets the in-memory tab set. - // Clear tabs BEFORE toggling isPrivateMode to avoid switching WebEngineProfile on live WebViews. tabs.clear(); nextTabId = 1; currentTabIndex = 0; @@ -114,7 +104,6 @@ MApp { view.goBack(); return true; } - // Default behavior (minimize) minimizeRequested(); return true; } @@ -297,7 +286,6 @@ MApp { var maxId = 0; for (var i = 0; i < parsedTabs.length; i++) { var tab = parsedTabs[i]; - // Ensure all required properties are present tabs.append({ "tabId": tab.id, "url": (tab.url && tab.url !== "about:blank") ? tab.url : homepageUrl, @@ -375,11 +363,9 @@ MApp { for (var i = 0; i < tabs.count; i++) { if (tabs.get(i).tabId === tabId) { var wasCurrent = (i === currentTabIndex); - // Safely stop the WebEngineView before removal to prevent crashes var viewToRemove = browserApp.tabViewsById["" + tabId]; if (viewToRemove) { try { - // Encourage WebEngine to drop resources for this tab before we destroy the view. try { viewToRemove.lifecycleState = WebEngineView.LifecycleState.Discarded; } catch (e) {} @@ -388,7 +374,6 @@ MApp { Logger.warn("BrowserApp", "Error stopping view: " + e); } } - // If closing the current tab, clear the webView reference first to avoid crashes if (wasCurrent) webView = null; @@ -396,15 +381,12 @@ MApp { if (tabs.count === 0) { createNewTab(); } else { - // Adjust index if we closed a tab before the current one if (i < currentTabIndex) currentTabIndex--; - // Clamp index if (currentTabIndex >= tabs.count) currentTabIndex = tabs.count - 1; - // Force update of webView if we closed the current tab if (wasCurrent) Qt.callLater(updateCurrentWebView); } @@ -444,16 +426,13 @@ MApp { return ""; partialText = partialText.toLowerCase(); - // Search Bookmarks first (higher priority) for (var i = 0; i < bookmarks.length; i++) { var url = bookmarks[i].url.toLowerCase(); - // Strip protocol for matching var cleanUrl = url.replace("https://", "").replace("http://", "").replace("www.", ""); var hostOnly = cleanUrl.split("/")[0]; if (hostOnly.startsWith(partialText)) return hostOnly; } - // Search History for (var j = 0; j < history.length; j++) { var hUrl = history[j].url.toLowerCase(); var hCleanUrl = hUrl.replace("https://", "").replace("http://", "").replace("www.", ""); @@ -492,7 +471,6 @@ MApp { _addSuggestion("go", "Go to " + goUrl, goUrl); } _addSuggestion("search", "Search " + searchEngineName + " for \"" + q + "\"", q); - // Simple ranked matches: bookmarks then history, host-prefix first. var seen = {}; for (var i = 0; i < bookmarks.length; i++) { var bUrl = bookmarks[i].url || ""; @@ -537,7 +515,6 @@ MApp { else finalUrl = searchEngineUrl + encodeURIComponent(url); } - // Only allow http/https/about at the point of navigation. var finalLower = finalUrl.toLowerCase(); if (!(finalLower.startsWith("http://") || finalLower.startsWith("https://") || finalLower.startsWith("about:"))) { Logger.warn("BrowserApp", "Blocked unsupported URL: " + finalUrl); @@ -600,7 +577,6 @@ MApp { if (browserApp._focusRetrySelectAll) browserApp._focusRetryItem.selectAll(); - // Explicitly request the virtual keyboard to show via Wayland text-input if (Qt.inputMethod) Qt.inputMethod.show(); @@ -608,12 +584,10 @@ MApp { }); } - focus: true // Ensure focus chain is established for keyboard input + focus: true appId: "browser" appName: "Browser" appIcon: "assets/icon.svg" - // Tell the shell whether “system back/forward” should be routed into the web history. - // This is what AppLifecycleManager.handleSystemBack/Forward consults via MApp.handleBack/Forward. canNavigateBack: { var t = getCurrentTab(); return t ? (t.canGoBack === true) : false; @@ -627,8 +601,6 @@ MApp { } onAppLaunched: { isAppActive = true; - // WebEngine takes ~800ms to initialize and will steal focus. - // Delay our focus claim until after it's likely done. launchFocusTimer.restart(); } onAppResumed: { @@ -636,7 +608,6 @@ MApp { } Component.onCompleted: { Logger.info("BrowserApp", "Initializing browser..."); - // Prevent WebView from grabbing focus during startup addressBarWantsFocus = true; loadSettings(); loadBookmarks(); @@ -646,7 +617,6 @@ MApp { Logger.info("BrowserApp", "No tabs found, creating default tab"); createNewTab(); } - // Defer to ensure children are created Qt.callLater(updateCurrentWebView); Qt.callLater(function () { browserApp._syncAddressBarFromCurrentTab(); @@ -660,7 +630,6 @@ MApp { saveHistory(); } onAppMinimized: { - // Prevent OOM kills by discarding WebEngine renderers while backgrounded. isAppActive = false; try { var keys = Object.keys(tabViewsById); @@ -675,7 +644,6 @@ MApp { _clearAddressSuggestions(); } onAppRestored: { - // Reactivate; active binding will unfreeze the current tab. isAppActive = true; try { var keys = Object.keys(tabViewsById); @@ -710,15 +678,10 @@ MApp { id: tabsModel } - // Explicit profiles so we can do “real tabs” but still manage resources sanely. WebEngineProfile { id: normalProfile httpUserAgent: "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36" - // Qt 6.9: property initialization order during QML construction can momentarily leave - // storageName empty while disk-based options are applied, producing: - // "Storage name is empty... Switching to disk-based behavior" - // Initialize in onCompleted to ensure storageName is set before disk-based behavior. Component.onCompleted: { storageName = "normal"; offTheRecord = false; @@ -770,7 +733,6 @@ MApp { } item.forceActiveFocus(); browserApp._focusRetryAttempts++; - // WebEngine takes ~800ms to init and steals focus. Retry for 1.6s total. if (browserApp._focusRetryAttempts >= 100) { if (browserApp._focusRetrySelectAll) item.selectAll(); @@ -784,7 +746,7 @@ MApp { Timer { id: launchFocusTimer - interval: 900 // WebEngine init takes ~800ms, give it a moment to settle + interval: 900 repeat: false onTriggered: { Logger.info("BrowserApp", "Post-launch focus: claiming address bar"); @@ -818,7 +780,6 @@ MApp { } } - // Match Settings behavior: consume nav-bar swipe back/forward for in-app history Connections { function onBackPressed() { browserApp.handleBack(); @@ -837,7 +798,6 @@ MApp { function onDeepLinkRequested(appId, route, params) { if (appId === "browser") { Logger.info("BrowserApp", "Deep link requested with params: " + JSON.stringify(params)); - // Handle URL parameter if (params && params.url) { Logger.info("BrowserApp", "Opening URL from deep link: " + params.url); navigateTo(params.url); @@ -876,8 +836,6 @@ MApp { model: browserApp.tabs - // Wrapper to avoid ListModel role name collisions with WebEngineView properties - // (e.g. role 'url' vs WebEngineView.url). Item { id: tabDelegate @@ -890,20 +848,12 @@ MApp { Layout.fillHeight: true WebView { - // Create and switch to the new tab using requestedUrl. - // Handle failure - id: webView anchors.fill: parent - // Prevent WebEngine from stealing focus when the URL bar is editing/highlighted - // or when we want the address bar focused on startup. focus: !urlInput.activeFocus && !browserApp.addressBarWantsFocus - // Bind URL to model role url: tabDelegate.url profile: isPrivateMode ? privateProfile : normalProfile - // Only freeze *background* tabs. QtWebEngine refuses to freeze a visible page. - // (We still render the current tab even when the drawer is open.) active: isAppActive && (tabDelegate.index === currentTabIndex) crashed: !!tabDelegate.isCrashed Component.onCompleted: { @@ -922,7 +872,6 @@ MApp { browserApp.tabs.setProperty(tabDelegate.index, "isCrashed", crashed); } } - // Handle render process crashes gracefully onRenderProcessTerminated: function (terminationStatus, exitCode) { var status = ""; switch (terminationStatus) { @@ -953,8 +902,6 @@ MApp { if (!request) return; - // Stability-first: treat new window as “open URL in new tab”. - // Avoid deferring request.openIn(...) across delegate lifetimes; it can destabilize tab close. var reqUrl = request.requestedUrl ? request.requestedUrl.toString() : ""; if (reqUrl) createNewTab(reqUrl); @@ -975,12 +922,10 @@ MApp { request.accept(); } onCertificateError: error => { - // Security-first default: do not allow TLS bypass without an explicit UI. Logger.warn("BrowserApp", "TLS certificate error for " + error.url + " (" + error.type + "): " + error.description); error.rejectCertificate(); } onFeaturePermissionRequested: (securityOrigin, feature) => { - // Security-first default: deny. (We can add a prompt UI later.) Logger.info("BrowserApp", "Feature permission requested (" + feature + ") for " + securityOrigin); webView.grantFeaturePermission(securityOrigin, feature, false); } @@ -991,7 +936,6 @@ MApp { if (updatingTabUrl) return; - // Update model when URL changes (navigation) if (tabDelegate.index >= 0 && tabDelegate.index < browserApp.tabs.count) { if (browserApp.tabs.get(tabDelegate.index).url !== url.toString()) browserApp.tabs.setProperty(tabDelegate.index, "url", url.toString()); @@ -1003,7 +947,6 @@ MApp { if (tabDelegate.index >= 0 && tabDelegate.index < browserApp.tabs.count) { if (browserApp.tabs.get(tabDelegate.index).title !== title) { browserApp.tabs.setProperty(tabDelegate.index, "title", title); - // Keep history titles fresh without incrementing visits. if (!isPrivateMode) updateHistoryTitle(url.toString(), title); } @@ -1133,7 +1076,6 @@ MApp { if (browserApp.updatingTabUrl) return parent.width * 0.2; - // Fake 20% progress during preparation var currentTab = getCurrentTab(); if (currentTab && currentTab.isLoading && currentTab.loadProgress) return parent.width * (currentTab.loadProgress / 100); @@ -1159,7 +1101,6 @@ MApp { anchors.rightMargin: MSpacing.sm spacing: MSpacing.xs - // Back Button Rectangle { Layout.preferredWidth: Constants.touchTargetSmall Layout.preferredHeight: Constants.touchTargetSmall @@ -1195,7 +1136,6 @@ MApp { } } - // Forward Button Rectangle { Layout.preferredWidth: Constants.touchTargetSmall Layout.preferredHeight: Constants.touchTargetSmall @@ -1225,7 +1165,6 @@ MApp { } } - // Address Bar Rectangle { Layout.fillWidth: true Layout.preferredHeight: parent.height - MSpacing.sm * 2 @@ -1236,7 +1175,6 @@ MApp { border.color: urlInput.activeFocus ? MColors.accent : MColors.border clip: true - // Ensure tapping the address bar actually focuses it (selectByMouse can highlight without focus). MouseArea { anchors.fill: parent propagateComposedEvents: true @@ -1251,7 +1189,6 @@ MApp { TextInput { id: urlInput - // Inline autocomplete state property bool inlineCompletionActive: false property string inlineTypedPrefix: "" property bool suppressAutocompleteOnce: false @@ -1277,7 +1214,7 @@ MApp { onActiveFocusChanged: { if (activeFocus) { browserApp.isEditingAddress = true; - browserApp.addressBarWantsFocus = false; // Mission accomplished + browserApp.addressBarWantsFocus = false; urlInput.userTypedSinceFocus = false; var currentTab = getCurrentTab(); urlInput.text = currentTab ? currentTab.url : ""; @@ -1302,10 +1239,8 @@ MApp { browserApp.urlInputRef = null; } Keys.onPressed: event => { - // Track deletion so we don’t re-autocomplete while backspacing. if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete) { urlInput.lastEditWasDeletion = true; - // If the suggested suffix is selected, backspace should remove *only* the suggestion. if (urlInput.inlineCompletionActive && urlInput.text.startsWith(urlInput.inlineTypedPrefix)) { var expectedStart = urlInput.inlineTypedPrefix.length; if (selectionStart === expectedStart && selectionEnd === urlInput.text.length) { @@ -1324,7 +1259,6 @@ MApp { return; } urlInput.lastEditWasDeletion = false; - // Accept completion (Right/Tab) or cancel (Left/Escape) if (urlInput.inlineCompletionActive) { if (event.key === Qt.Key_Right || event.key === Qt.Key_Tab) { event.accepted = true; @@ -1359,10 +1293,6 @@ MApp { urlInput.suppressAutocompleteOnce = false; return; } - // Inline completion rules (Chrome/Safari-like): - // - only when inserting at end (caret at end, no selection) - // - never while deleting - // - never when empty/too short if (!activeFocus) return; @@ -1404,7 +1334,6 @@ MApp { browserApp.ensureAddressBarFocus(true); } - // Fix: Restore binding when tabs change (navigation) function onTabsChanged() { if (!urlInput.activeFocus) { var currentTab = getCurrentTab(); @@ -1437,7 +1366,6 @@ MApp { } } - // Actions inside Address Bar Row { id: actionRow @@ -1447,7 +1375,6 @@ MApp { anchors.rightMargin: MSpacing.xs spacing: 0 - // Clear Button Rectangle { width: Constants.touchTargetSmall * 0.8 height: parent.height @@ -1481,7 +1408,6 @@ MApp { } } - // Star Button Rectangle { width: Constants.touchTargetSmall * 0.8 height: parent.height @@ -1519,7 +1445,6 @@ MApp { } } - // Refresh/Stop Button Rectangle { width: Constants.touchTargetSmall * 0.8 height: parent.height @@ -1551,7 +1476,6 @@ MApp { if (currentTab && currentTab.isLoading) { view.triggerWebAction(WebEngineView.Stop); } else { - // “Hard reload” (bypass cache). Also call reload() as a fallback. view.triggerWebAction(WebEngineView.ReloadAndBypassCache); view.reload(); } @@ -1561,7 +1485,6 @@ MApp { } } - // Tabs Button Rectangle { Layout.preferredWidth: Constants.touchTargetSmall * 1.6 Layout.preferredHeight: Constants.touchTargetSmall @@ -1606,7 +1529,6 @@ MApp { } } - // Address suggestions overlay (anchored above the URL bar) Rectangle { id: addressSuggestionsPopup @@ -1622,14 +1544,11 @@ MApp { radius: MRadius.md clip: true opacity: 0.98 - // urlBar is nested under Column (not a sibling), so anchors won't work. - // Position relative to the bottom of the app using urlBar.height. x: 0 width: rootContent.width anchors.bottom: rootContent.bottom anchors.bottomMargin: urlBar.height height: Math.min(maxHeight, rowHeight * addressSuggestions.count) - // Match MarathonUI dropdown feel: subtle pop + shadow scale: visible ? 1 : 0.97 layer.enabled: true @@ -1684,7 +1603,7 @@ MApp { if (model.kind === "history") return "history"; - return "arrow-right"; // go/default + return "arrow-right"; } } @@ -1736,7 +1655,6 @@ MApp { urlInput.suppressAutocompleteOnce = true; urlInput.inlineCompletionActive = false; urlInput.inlineTypedPrefix = ""; - // ListModel roles are directly available in the delegate: kind / value / display var selectedValue = value; var selectedKind = kind; urlInput.text = selectedValue; @@ -1770,7 +1688,6 @@ MApp { } } - // MultiEffect isn't available in all Qt builds; use a portable drop shadow. layer.effect: DropShadow { horizontalOffset: 0 verticalOffset: 4 @@ -1809,7 +1726,6 @@ MApp { anchors.fill: parent Component.onCompleted: { - // Store reference for safe access from other components browserApp.drawerRef = drawer; if (drawer.tabsPage) { drawer.tabsPage.tabs = Qt.binding(function () { @@ -1831,7 +1747,6 @@ MApp { }); if (drawer.settingsPage) { - // Bind settings page to app properties drawer.settingsPage.searchEngine = Qt.binding(function () { return browserApp.searchEngineName; }); @@ -1848,7 +1763,6 @@ MApp { browserApp.setPrivateMode(drawer.settingsPage.isPrivateMode); }); drawer.settingsPage.searchEngineChanged.connect(function () { - // Only update if changed from UI if (browserApp.searchEngineName !== drawer.settingsPage.searchEngine) { browserApp.searchEngineName = drawer.settingsPage.searchEngine; saveSettings(); diff --git a/apps/browser/components/BrowserDrawer.qml b/apps/browser/components/BrowserDrawer.qml index e0a3a162d..e00a8d2c2 100644 --- a/apps/browser/components/BrowserDrawer.qml +++ b/apps/browser/components/BrowserDrawer.qml @@ -1,21 +1,13 @@ -import QtQuick import MarathonApp.Browser -import QtQuick.Layouts import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Core import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Layouts Rectangle { id: drawer - anchors.fill: parent - color: MColors.background - - signal closed - signal tabSelected(int tabId) - signal newTabRequested - signal bookmarkSelected(string url) - signal historySelected(string url) property int selectedTabIndex: 0 property alias contentStack: contentStack @@ -24,33 +16,42 @@ Rectangle { property alias historyPage: historyPage property alias settingsPage: settingsPage + signal closed + signal tabSelected(int tabId) + signal newTabRequested + signal bookmarkSelected(string url) + signal historySelected(string url) + + anchors.fill: parent + color: MColors.background + Column { anchors.fill: parent spacing: 0 MTabBar { id: drawerTabs + width: parent.width activeTab: drawer.selectedTabIndex tabs: [ { - label: "Tabs", - icon: "layers" + "label": "Tabs", + "icon": "layers" }, { - label: "Bookmarks", - icon: "star" + "label": "Bookmarks", + "icon": "star" }, { - label: "History", - icon: "clock" + "label": "History", + "icon": "clock" }, { - label: "Settings", - icon: "settings" + "label": "Settings", + "icon": "settings" } ] - onTabSelected: index => { drawer.selectedTabIndex = index; Logger.info("BrowserDrawer", "Switched to tab: " + index); @@ -59,18 +60,23 @@ Rectangle { StackLayout { id: contentStack + width: parent.width height: parent.height - drawerTabs.height currentIndex: drawer.selectedTabIndex TabsPage { id: tabsPage - onTabSelected: tabId => drawer.tabSelected(tabId) + + onTabSelected: tabId => { + return drawer.tabSelected(tabId); + } onNewTabRequested: drawer.newTabRequested() } BookmarksPage { id: bookmarksPage + onBookmarkSelected: url => { drawer.bookmarkSelected(url); drawer.closed(); @@ -79,6 +85,7 @@ Rectangle { HistoryPage { id: historyPage + onHistorySelected: url => { drawer.historySelected(url); drawer.closed(); diff --git a/apps/browser/components/TabCard.qml b/apps/browser/components/TabCard.qml index dd7d93155..09a32c9bf 100644 --- a/apps/browser/components/TabCard.qml +++ b/apps/browser/components/TabCard.qml @@ -1,32 +1,30 @@ -import QtQuick import MarathonApp.Browser import MarathonOS.Shell -import MarathonUI.Theme -import MarathonUI.Core import MarathonUI.Containers +import MarathonUI.Core +import MarathonUI.Theme +import QtQuick MCard { id: tabCard - height: Constants.cardHeight - elevation: isCurrentTab ? 2 : 1 - interactive: false // Disable built-in MouseArea to allow custom z-ordering - - signal tabClicked - signal closeRequested property var tabData: null property bool isCurrentTab: false - border.color: isCurrentTab ? MColors.accentBright : MColors.border + signal tabClicked + signal closeRequested + height: Constants.cardHeight + elevation: isCurrentTab ? 2 : 1 + interactive: false + border.color: isCurrentTab ? MColors.accentBright : MColors.border onClicked: { tabCard.tabClicked(); } - // Manual MouseArea for tab clicking, placed behind content but filling card MouseArea { anchors.fill: parent - z: 0 // Bottom layer + z: 0 onClicked: { tabCard.tabClicked(); } @@ -35,7 +33,7 @@ MCard { Column { anchors.fill: parent anchors.margins: MSpacing.md - anchors.rightMargin: MSpacing.md + Constants.touchTargetSmall // Make room for close button + anchors.rightMargin: MSpacing.md + Constants.touchTargetSmall spacing: MSpacing.sm Item { @@ -44,6 +42,7 @@ MCard { Icon { id: globeIcon + anchors.left: parent.left anchors.top: parent.top name: "globe" @@ -100,29 +99,27 @@ MCard { Item { id: closeButtonContainer + anchors.right: parent.right anchors.top: parent.top width: Constants.touchTargetSmall height: Constants.touchTargetSmall - z: 1000 // High z-index to sit above card content + z: 1000 MIconButton { anchors.centerIn: parent iconName: "x" - // Disable internal mouse handling to prevent conflicts, we handle it manually enabled: false - opacity: closeMouseArea.pressed ? 0.7 : 1.0 // Visual feedback - // color property removed as it caused a crash + opacity: closeMouseArea.pressed ? 0.7 : 1 } MouseArea { id: closeMouseArea + anchors.fill: parent hoverEnabled: true preventStealing: true propagateComposedEvents: false - - // Explicitly accept all events to prevent propagation to MCard onPressed: mouse => { mouse.accepted = true; } @@ -135,7 +132,6 @@ MCard { onPressAndHold: mouse => { mouse.accepted = true; } - onClicked: mouse => { mouse.accepted = true; tabCard.closeRequested(); diff --git a/apps/browser/components/WebView.qml b/apps/browser/components/WebView.qml index 2bd2a430e..1100115ba 100644 --- a/apps/browser/components/WebView.qml +++ b/apps/browser/components/WebView.qml @@ -1,25 +1,19 @@ -import QtQuick import MarathonApp.Browser -import QtWebEngine import MarathonOS.Shell import MarathonUI.Core +import QtQuick +import QtWebEngine -// Separate file for WebEngineView to enable true lazy loading -// This file is only loaded when needed, avoiding QtWebEngine initialization on app launch WebEngineView { id: webView property bool updatingTabUrl: false property bool active: true property bool crashed: false - // Allows BrowserApp to discard WebEngine state while minimized without breaking bindings. property bool forceDiscarded: false - zoomFactor: 1.0 - - // Keep per-tab state, but freeze background tabs (no reload) on low-end devices. + zoomFactor: 1 lifecycleState: forceDiscarded ? WebEngineView.LifecycleState.Discarded : (active ? WebEngineView.LifecycleState.Active : WebEngineView.LifecycleState.Frozen) - settings.accelerated2dCanvasEnabled: false settings.webGLEnabled: false settings.pluginsEnabled: false diff --git a/apps/browser/pages/BookmarksPage.qml b/apps/browser/pages/BookmarksPage.qml index 19e33d1a3..fe7cd4f46 100644 --- a/apps/browser/pages/BookmarksPage.qml +++ b/apps/browser/pages/BookmarksPage.qml @@ -1,43 +1,47 @@ -import QtQuick import MarathonApp.Browser import MarathonOS.Shell -import MarathonUI.Theme -import MarathonUI.Core import MarathonUI.Containers +import MarathonUI.Core +import MarathonUI.Theme +import QtQuick Rectangle { id: bookmarksPage - color: MColors.background + + property var bookmarks: [] signal bookmarkSelected(string url) signal deleteBookmark(string url) - property var bookmarks: [] + color: MColors.background ListView { id: bookmarksList + anchors.fill: parent clip: true - model: bookmarksPage.bookmarks + Text { + visible: bookmarksPage.bookmarks.length === 0 + anchors.centerIn: parent + text: "No bookmarks yet" + font.pixelSize: MTypography.sizeLarge + color: MColors.textTertiary + } + delegate: MSettingsListItem { + property real swipeX: 0 + title: modelData.title || modelData.url subtitle: modelData.url iconName: "star" showChevron: true - onSettingClicked: { Logger.info("BookmarksPage", "Bookmark clicked: " + modelData.url); bookmarksPage.bookmarkSelected(modelData.url); } - property real swipeX: 0 - - transform: Translate { - x: swipeX - } - Rectangle { anchors.right: parent.right anchors.top: parent.top @@ -56,42 +60,40 @@ Rectangle { MouseArea { id: swipeArea - anchors.fill: parent - propagateComposedEvents: true property real startX: 0 property real currentX: 0 + anchors.fill: parent + propagateComposedEvents: true onPressed: mouse => { startX = mouse.x; currentX = mouse.x; } - onPositionChanged: mouse => { currentX = mouse.x; var deltaX = currentX - startX; - - if (deltaX < 0 && deltaX > -Constants.touchTargetLarge) { + if (deltaX < 0 && deltaX > -Constants.touchTargetLarge) parent.swipeX = deltaX; - } } - onReleased: { - if (parent.swipeX < -Constants.touchTargetMedium) { + if (parent.swipeX < -Constants.touchTargetMedium) bookmarksPage.deleteBookmark(modelData.url); - } + parent.swipeX = 0; } - onClicked: mouse => { - if (parent.swipeX === 0) { + if (parent.swipeX === 0) mouse.accepted = false; - } else { + else parent.swipeX = 0; - } } } + transform: Translate { + x: swipeX + } + Behavior on swipeX { NumberAnimation { duration: 200 @@ -99,13 +101,5 @@ Rectangle { } } } - - Text { - visible: bookmarksPage.bookmarks.length === 0 - anchors.centerIn: parent - text: "No bookmarks yet" - font.pixelSize: MTypography.sizeLarge - color: MColors.textTertiary - } } } diff --git a/apps/browser/pages/BrowserSettingsPage.qml b/apps/browser/pages/BrowserSettingsPage.qml index c95d3af4d..214d7c657 100644 --- a/apps/browser/pages/BrowserSettingsPage.qml +++ b/apps/browser/pages/BrowserSettingsPage.qml @@ -1,24 +1,26 @@ -import QtQuick import MarathonApp.Browser -import QtQuick.Controls import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Rectangle { id: settingsPage - color: MColors.background - - signal clearHistoryRequested - signal clearCookiesRequested property bool isPrivateMode: false property string searchEngine: "Google" property string searchEngineUrl: "https://www.google.com/search?q=" property string homepage: "https://www.google.com" + signal clearHistoryRequested + signal clearCookiesRequested + + color: MColors.background + StackView { id: settingsStack + anchors.fill: parent initialItem: mainSettingsComponent @@ -28,38 +30,37 @@ Rectangle { ListView { clip: true spacing: 0 - model: [ { - type: "toggle", - title: "Private Browsing", - subtitle: "Don't save history or cookies", - iconName: "eye-off", - value: settingsPage.isPrivateMode + "type": "toggle", + "title": "Private Browsing", + "subtitle": "Don't save history or cookies", + "iconName": "eye-off", + "value": settingsPage.isPrivateMode }, { - type: "button", - title: "Clear History", - subtitle: "Remove all browsing history", - iconName: "trash-2" + "type": "button", + "title": "Clear History", + "subtitle": "Remove all browsing history", + "iconName": "trash-2" }, { - type: "button", - title: "Clear Cookies", - subtitle: "Remove all stored website data", - iconName: "trash-2" + "type": "button", + "title": "Clear Cookies", + "subtitle": "Remove all stored website data", + "iconName": "trash-2" }, { - type: "chevron", - title: "Search Engine", - subtitle: settingsPage.searchEngine, - iconName: "search" + "type": "chevron", + "title": "Search Engine", + "subtitle": settingsPage.searchEngine, + "iconName": "search" }, { - type: "chevron", - title: "Homepage", - subtitle: settingsPage.homepage, - iconName: "home" + "type": "chevron", + "title": "Homepage", + "subtitle": settingsPage.homepage, + "iconName": "home" } ] @@ -70,21 +71,17 @@ Rectangle { showChevron: modelData.type === "chevron" showToggle: modelData.type === "toggle" toggleValue: modelData.type === "toggle" ? modelData.value : false - onSettingClicked: { Logger.info("BrowserSettings", "Clicked: " + modelData.title); - - if (modelData.title === "Clear History") { + if (modelData.title === "Clear History") settingsPage.clearHistoryRequested(); - } else if (modelData.title === "Clear Cookies") { + else if (modelData.title === "Clear Cookies") settingsPage.clearCookiesRequested(); - } else if (modelData.title === "Search Engine") { + else if (modelData.title === "Search Engine") settingsStack.push(searchEngineComponent); - } else if (modelData.title === "Homepage") { + else if (modelData.title === "Homepage") settingsStack.push(homepageComponent); - } } - onToggleChanged: value => { if (modelData.title === "Private Browsing") { settingsPage.isPrivateMode = value; diff --git a/apps/browser/pages/HistoryPage.qml b/apps/browser/pages/HistoryPage.qml index 49f299aec..ab6abfcce 100644 --- a/apps/browser/pages/HistoryPage.qml +++ b/apps/browser/pages/HistoryPage.qml @@ -1,30 +1,31 @@ -import QtQuick import MarathonApp.Browser import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Core +import MarathonUI.Theme +import QtQuick Rectangle { id: historyPage - color: MColors.background + + property var history: [] signal historySelected(string url) signal clearHistory - property var history: [] - function getDateGroup(timestamp) { var now = Date.now(); var diff = now - timestamp; var dayMs = 24 * 60 * 60 * 1000; - if (diff < dayMs) return "Today"; + if (diff < dayMs * 2) return "Yesterday"; + if (diff < dayMs * 7) return "This Week"; + return "Older"; } @@ -35,49 +36,55 @@ Rectangle { "This Week": [], "Older": [] }; - for (var i = 0; i < history.length; i++) { var item = history[i]; var group = getDateGroup(item.timestamp); groups[group].push(item); } - return groups; } + color: MColors.background + Column { anchors.fill: parent ListView { id: historyList + width: parent.width height: parent.height - clearButton.height clip: true - model: { var groups = historyPage.groupHistoryByDate(); var result = []; - var order = ["Today", "Yesterday", "This Week", "Older"]; for (var i = 0; i < order.length; i++) { var groupName = order[i]; if (groups[groupName].length > 0) { result.push({ - type: "header", - title: groupName + "type": "header", + "title": groupName }); for (var j = 0; j < groups[groupName].length; j++) { result.push({ - type: "item", - data: groups[groupName][j] + "type": "item", + "data": groups[groupName][j] }); } } } - return result; } + Text { + visible: historyPage.history.length === 0 + anchors.centerIn: parent + text: "No history yet" + font.pixelSize: MTypography.sizeLarge + color: MColors.textTertiary + } + delegate: Item { width: historyList.width height: modelData.type === "header" ? Constants.touchTargetSmall : (Constants.touchTargetLarge + MSpacing.xs) @@ -97,7 +104,6 @@ Rectangle { iconName: "clock" showChevron: true value: modelData.data ? (modelData.data.visitCount > 1 ? modelData.data.visitCount + " visits" : "") : "" - onSettingClicked: { if (modelData.data) { Logger.info("HistoryPage", "History clicked: " + modelData.data.url); @@ -106,18 +112,11 @@ Rectangle { } } } - - Text { - visible: historyPage.history.length === 0 - anchors.centerIn: parent - text: "No history yet" - font.pixelSize: MTypography.sizeLarge - color: MColors.textTertiary - } } Rectangle { id: clearButton + width: parent.width height: Constants.touchTargetMedium + MSpacing.md * 2 color: MColors.surface diff --git a/apps/browser/pages/HomepagePage.qml b/apps/browser/pages/HomepagePage.qml index 0cf8b3136..4142ab129 100644 --- a/apps/browser/pages/HomepagePage.qml +++ b/apps/browser/pages/HomepagePage.qml @@ -1,17 +1,18 @@ -import QtQuick import MarathonApp.Browser import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Core +import MarathonUI.Theme +import QtQuick Rectangle { id: homepagePage - color: MColors.background + + property string currentHomepage: "https://www.google.com" signal homepageChanged(string url) signal backRequested - property string currentHomepage: "https://www.google.com" + color: MColors.background Column { anchors.fill: parent @@ -51,6 +52,7 @@ Rectangle { MouseArea { id: backMouseArea + anchors.fill: parent onClicked: { HapticService.light(); @@ -84,14 +86,17 @@ Rectangle { name: "Google" url: "https://www.google.com" } + ListElement { name: "DuckDuckGo" url: "https://duckduckgo.com" } + ListElement { name: "Bing" url: "https://www.bing.com" } + ListElement { name: "Blank Page" url: "about:blank" @@ -156,6 +161,7 @@ Rectangle { MouseArea { id: delegateMouseArea + anchors.fill: parent onClicked: { HapticService.medium(); @@ -199,6 +205,7 @@ Rectangle { TextInput { id: customUrlInput + anchors.fill: parent anchors.leftMargin: MSpacing.md anchors.rightMargin: MSpacing.md @@ -211,12 +218,11 @@ Rectangle { selectionColor: MColors.accent inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoPredictiveText text: homepagePage.currentHomepage - onAccepted: { HapticService.medium(); - if (text.length > 0) { + if (text.length > 0) homepagePage.homepageChanged(text); - } + focus = false; } diff --git a/apps/browser/pages/SearchEnginePage.qml b/apps/browser/pages/SearchEnginePage.qml index 46bbc5868..4a894af45 100644 --- a/apps/browser/pages/SearchEnginePage.qml +++ b/apps/browser/pages/SearchEnginePage.qml @@ -1,18 +1,19 @@ -import QtQuick import MarathonApp.Browser -import QtQuick.Layouts import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Core +import MarathonUI.Theme +import QtQuick +import QtQuick.Layouts Rectangle { id: searchEnginePage - color: MColors.background + + property string currentSearchEngine: "Google" signal searchEngineSelected(string name, string url) signal backRequested - property string currentSearchEngine: "Google" + color: MColors.background Column { anchors.fill: parent @@ -52,6 +53,7 @@ Rectangle { MouseArea { id: backMouseArea + anchors.fill: parent onClicked: { HapticService.light(); @@ -81,18 +83,22 @@ Rectangle { name: "Google" url: "https://www.google.com/search?q=" } + ListElement { name: "DuckDuckGo" url: "https://duckduckgo.com/?q=" } + ListElement { name: "Bing" url: "https://www.bing.com/search?q=" } + ListElement { name: "Brave Search" url: "https://search.brave.com/search?q=" } + ListElement { name: "Ecosia" url: "https://www.ecosia.org/search?q=" @@ -147,6 +153,7 @@ Rectangle { MouseArea { id: delegateMouseArea + anchors.fill: parent onClicked: { HapticService.medium(); diff --git a/apps/browser/pages/TabsPage.qml b/apps/browser/pages/TabsPage.qml index eb31c525b..e93f9ed32 100644 --- a/apps/browser/pages/TabsPage.qml +++ b/apps/browser/pages/TabsPage.qml @@ -8,7 +8,7 @@ import QtQuick Rectangle { id: tabsPage - property var tabs: null // ListModel + property var tabs: null property int currentTabId: -1 property int maxTabs: 12 diff --git a/apps/browser/src/BrowserData.cpp b/apps/browser/src/BrowserData.cpp index e447c798c..f613c8ed9 100644 --- a/apps/browser/src/BrowserData.cpp +++ b/apps/browser/src/BrowserData.cpp @@ -22,7 +22,6 @@ bool BrowserData::clearCookiesAndCache(QObject *webEngineProfileObject) { store->deleteAllCookies(); } - // Async; QML can listen to clearHttpCacheCompleted if it cares. profile->clearHttpCache(); return true; } diff --git a/apps/browser/src/BrowserData.h b/apps/browser/src/BrowserData.h index 178ab2671..43747cda3 100644 --- a/apps/browser/src/BrowserData.h +++ b/apps/browser/src/BrowserData.h @@ -15,7 +15,5 @@ class BrowserData : public QObject { static BrowserData *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine); - // Clears cookies + HTTP cache for the given QML WebEngineProfile (QtWebEngine Quick). - // Returns false if the passed object is not a WebEngineProfile. - Q_INVOKABLE bool clearCookiesAndCache(QObject *webEngineProfileObject); + Q_INVOKABLE bool clearCookiesAndCache(QObject *webEngineProfileObject); }; diff --git a/apps/calculator/CalculatorApp.qml b/apps/calculator/CalculatorApp.qml index e88429737..2581c1070 100644 --- a/apps/calculator/CalculatorApp.qml +++ b/apps/calculator/CalculatorApp.qml @@ -54,7 +54,7 @@ MApp { result = value !== 0 ? currentValue / value : 0; break; } - display = result.toString(); + display = String(result); currentOperator = ""; newNumber = true; } @@ -68,7 +68,6 @@ MApp { appId: "calculator" appName: "Calculator" - // Prefer app-local asset path so the icon works both in-process and in marathon-app-runner. appIcon: "assets/icon.svg" content: Rectangle { diff --git a/apps/calendar/CalendarApp.qml b/apps/calendar/CalendarApp.qml index 519ea2361..aacfedbc1 100644 --- a/apps/calendar/CalendarApp.qml +++ b/apps/calendar/CalendarApp.qml @@ -14,7 +14,9 @@ MApp { property date currentDate: new Date() property var selectedDate: null property int currentView: 0 - property Item navStack: null + property StackView navStack: null + + signal eventsChanged function createEvent(title, date, time, allDay, recurring) { var event = { @@ -32,11 +34,14 @@ MApp { return calendarStorage.getEventsForDate(date); } + function getAllEvents() { + return calendarStorage.events; + } + function deleteEvent(id) { return calendarStorage.deleteEvent(id); } - // Helper to open detail page function openEventDetail(event) { if (navStack) navStack.push("pages/EventDetailPage.qml", { @@ -55,10 +60,6 @@ MApp { } navigationDepth: navStack ? navStack.depth : 0 onBackPressed: { - // If filtering by date, clear filter - // If in list view (without filter), go back to month view - // Let the shell handle closing the app if at root - if (navStack && navStack.depth > 1) navStack.pop(); else if (calendarApp.selectedDate !== null) @@ -71,6 +72,8 @@ MApp { CalendarStorage { id: calendarStorage + + onDataChanged: calendarApp.eventsChanged() } content: StackView { @@ -140,7 +143,6 @@ MApp { } } - // Transitions pushEnter: Transition { PropertyAnimation { property: "x" diff --git a/apps/calendar/components/EventListItem.qml b/apps/calendar/components/EventListItem.qml index 610c03528..0d601eb3b 100644 --- a/apps/calendar/components/EventListItem.qml +++ b/apps/calendar/components/EventListItem.qml @@ -1,15 +1,12 @@ -import QtQuick import MarathonApp.Calendar -import QtQuick.Layouts -import MarathonUI.Theme -import MarathonUI.Core import MarathonOS.Shell +import MarathonUI.Core +import MarathonUI.Theme +import QtQuick +import QtQuick.Layouts Rectangle { id: root - width: parent.width - height: 72 - color: "transparent" property string title: "" property string time: "" @@ -18,19 +15,21 @@ Rectangle { signal clicked + width: parent.width + height: 72 + color: "transparent" + Rectangle { anchors.fill: parent anchors.leftMargin: MSpacing.md anchors.rightMargin: MSpacing.md anchors.topMargin: MSpacing.xs anchors.bottomMargin: MSpacing.xs - color: MColors.surface radius: Constants.borderRadiusSmall border.color: MColors.border border.width: 1 - // Color strip Rectangle { width: 4 height: parent.height - 16 @@ -43,7 +42,7 @@ Rectangle { ColumnLayout { anchors.fill: parent - anchors.leftMargin: MSpacing.xl // Make room for strip + anchors.leftMargin: MSpacing.xl anchors.rightMargin: MSpacing.md anchors.topMargin: MSpacing.sm anchors.bottomMargin: MSpacing.sm diff --git a/apps/calendar/pages/CalendarGridPage.qml b/apps/calendar/pages/CalendarGridPage.qml index 9d0acb0a2..1cd467f63 100644 --- a/apps/calendar/pages/CalendarGridPage.qml +++ b/apps/calendar/pages/CalendarGridPage.qml @@ -1,12 +1,11 @@ -import QtQuick import MarathonApp.Calendar import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick Rectangle { id: calendarGridPage - color: MColors.background property date selectedDate: new Date() property int currentMonth: selectedDate.getMonth() @@ -20,6 +19,8 @@ Rectangle { return new Date(year, month, 1).getDay(); } + color: MColors.background + Column { anchors.fill: parent spacing: 0 @@ -90,6 +91,7 @@ Rectangle { model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] Rectangle { + id: dayCell width: calendarGridPage.width / 7 height: Constants.touchTargetMedium color: MColors.elevated @@ -108,16 +110,9 @@ Rectangle { model: 42 Rectangle { - width: calendarGridPage.width / 7 - height: (calendarGridPage.height - Constants.touchTargetLarge - MSpacing.lg - Constants.touchTargetMedium) / 6 - color: "transparent" - border.width: Constants.borderWidthThin - border.color: MColors.border - property int dayNumber: { var firstDay = getFirstDayOfMonth(currentYear, currentMonth); var dayIndex = index - firstDay; - if (dayIndex < 0) { var prevMonth = currentMonth === 0 ? 11 : currentMonth - 1; var prevYear = currentMonth === 0 ? currentYear - 1 : currentYear; @@ -128,27 +123,32 @@ Rectangle { return dayIndex + 1; } } - property bool isCurrentMonth: { var firstDay = getFirstDayOfMonth(currentYear, currentMonth); var dayIndex = index - firstDay; return dayIndex >= 0 && dayIndex < getDaysInMonth(currentYear, currentMonth); } - property bool isToday: { if (!isCurrentMonth) return false; + var today = new Date(); return today.getDate() === dayNumber && today.getMonth() === currentMonth && today.getFullYear() === currentYear; } - property var dayEvents: { if (!isCurrentMonth) return []; + var dayDate = new Date(currentYear, currentMonth, dayNumber); return calendarApp.getEventsForDate(dayDate); } + width: calendarGridPage.width / 7 + height: (calendarGridPage.height - Constants.touchTargetLarge - MSpacing.lg - Constants.touchTargetMedium) / 6 + color: "transparent" + border.width: Constants.borderWidthThin + border.color: MColors.border + Column { anchors.fill: parent anchors.margins: MSpacing.xs @@ -158,20 +158,20 @@ Rectangle { width: Constants.touchTargetMedium height: Constants.touchTargetMedium radius: Constants.borderRadiusSharp - color: parent.parent.isToday ? MColors.accent : "transparent" + color: dayCell.isToday ? MColors.accent : "transparent" anchors.horizontalCenter: parent.horizontalCenter Text { anchors.centerIn: parent - text: parent.parent.parent.dayNumber + text: dayCell.dayNumber font.pixelSize: MTypography.sizeSmall - font.weight: parent.parent.parent.isToday ? Font.Bold : Font.Normal - color: parent.parent.parent.isCurrentMonth ? (parent.parent.parent.isToday ? MColors.text : MColors.text) : MColors.textTertiary + font.weight: dayCell.isToday ? Font.Bold : Font.Normal + color: dayCell.isCurrentMonth ? (dayCell.isToday ? MColors.text : MColors.text) : MColors.textTertiary } } Repeater { - model: Math.min(parent.parent.dayEvents.length, 3) + model: Math.min(dayCell.dayEvents.length, 3) Rectangle { width: parent.width @@ -182,18 +182,18 @@ Rectangle { Text { anchors.fill: parent anchors.margins: 1 - text: parent.parent.parent.dayEvents[index].title + text: dayCell.dayEvents[index].title font.pixelSize: MTypography.sizeXSmall color: MColors.text elide: Text.ElideRight verticalAlignment: Text.AlignVCenter leftPadding: 2 } + MouseArea { anchors.fill: parent onClicked: { - // Open event detail - calendarApp.openEventDetail(parent.parent.parent.dayEvents[index]); + calendarApp.openEventDetail(dayCell.dayEvents[index]); } } } @@ -201,10 +201,10 @@ Rectangle { Text { anchors.horizontalCenter: parent.horizontalCenter - text: parent.parent.dayEvents.length > 3 ? "+" + (parent.parent.dayEvents.length - 3) : "" + text: dayCell.dayEvents.length > 3 ? "+" + (dayCell.dayEvents.length - 3) : "" font.pixelSize: MTypography.sizeXSmall color: MColors.textSecondary - visible: parent.parent.dayEvents.length > 3 + visible: dayCell.dayEvents.length > 3 } } @@ -212,7 +212,6 @@ Rectangle { anchors.fill: parent onClicked: { if (parent.isCurrentMonth) { - // Switch to list view with selected date var date = new Date(currentYear, currentMonth, parent.dayNumber); calendarApp.selectedDate = date; calendarApp.currentView = 1; diff --git a/apps/calendar/pages/EventCreationPage.qml b/apps/calendar/pages/EventCreationPage.qml index 7ea6d26ee..b487ac27b 100644 --- a/apps/calendar/pages/EventCreationPage.qml +++ b/apps/calendar/pages/EventCreationPage.qml @@ -1,26 +1,26 @@ -import QtQuick import MarathonApp.Calendar -import QtQuick.Layouts -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts MPage { id: root + + property var onSave: null + title: "New Event" showBackButton: true onBackClicked: root.StackView.view.pop() - property var onSave: null - content: ColumnLayout { width: parent.width spacing: MSpacing.lg - // Title Field MFormField { Layout.fillWidth: true Layout.margins: MSpacing.lg @@ -29,22 +29,23 @@ MPage { content: TextField { id: titleField + width: parent.width placeholderText: "Enter event title" font.pixelSize: MTypography.sizeBody font.family: MTypography.fontFamily color: MColors.text + padding: MSpacing.md + background: Rectangle { color: MColors.surface border.color: MColors.border border.width: 1 radius: Constants.borderRadiusSmall } - padding: MSpacing.md } } - // Date Field (Simplified for now, using text input) MFormField { Layout.fillWidth: true Layout.margins: MSpacing.lg @@ -53,23 +54,24 @@ MPage { content: TextField { id: dateField + width: parent.width text: Qt.formatDate(new Date(), "yyyy-MM-dd") placeholderText: "YYYY-MM-DD" font.pixelSize: MTypography.sizeBody font.family: MTypography.fontFamily color: MColors.text + padding: MSpacing.md + background: Rectangle { color: MColors.surface border.color: MColors.border border.width: 1 radius: Constants.borderRadiusSmall } - padding: MSpacing.md } } - // Time Field MFormField { Layout.fillWidth: true Layout.margins: MSpacing.lg @@ -77,23 +79,24 @@ MPage { content: TextField { id: timeField + width: parent.width text: Qt.formatTime(new Date(), "HH:mm") placeholderText: "HH:MM" font.pixelSize: MTypography.sizeBody font.family: MTypography.fontFamily color: MColors.text + padding: MSpacing.md + background: Rectangle { color: MColors.surface border.color: MColors.border border.width: 1 radius: Constants.borderRadiusSmall } - padding: MSpacing.md } } - // All Day Toggle RowLayout { Layout.fillWidth: true Layout.margins: MSpacing.lg @@ -107,39 +110,36 @@ MPage { Switch { id: allDaySwitch + checked: false } } - // Save Button MButton { Layout.fillWidth: true Layout.margins: MSpacing.lg text: "Save Event" variant: "primary" onClicked: { - if (titleField.text === "") { - // Show error (TODO: proper validation feedback) + if (titleField.text === "") return; - } var newEvent = { - title: titleField.text, - date: dateField.text, - time: timeField.text, - allDay: allDaySwitch.checked, - recurring: "none" // Default for now + "title": titleField.text, + "date": dateField.text, + "time": timeField.text, + "allDay": allDaySwitch.checked, + "recurring": "none" }; - - if (root.onSave) { + if (root.onSave) root.onSave(newEvent); - } + root.StackView.view.pop(); } } Item { Layout.fillHeight: true - } // Spacer + } } } diff --git a/apps/calendar/pages/EventDetailPage.qml b/apps/calendar/pages/EventDetailPage.qml index f4abf0442..29f86349e 100644 --- a/apps/calendar/pages/EventDetailPage.qml +++ b/apps/calendar/pages/EventDetailPage.qml @@ -1,27 +1,27 @@ -import QtQuick import MarathonApp.Calendar -import QtQuick.Layouts -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts MPage { id: root - title: "Event Details" - showBackButton: true - onBackClicked: root.StackView.view.pop() property var event: ({}) property var onDelete: null + title: "Event Details" + showBackButton: true + onBackClicked: root.StackView.view.pop() + content: ColumnLayout { width: parent.width spacing: MSpacing.lg - // Title Text { Layout.fillWidth: true Layout.margins: MSpacing.lg @@ -32,7 +32,6 @@ MPage { wrapMode: Text.WordWrap } - // Date & Time RowLayout { Layout.fillWidth: true Layout.margins: MSpacing.lg @@ -60,25 +59,22 @@ MPage { } } - // Delete Button MButton { Layout.fillWidth: true Layout.margins: MSpacing.lg Layout.topMargin: MSpacing.xl text: "Delete Event" - variant: "secondary" // Or danger if available, using secondary for now - // TODO: Add danger variant support or style manually - + variant: "secondary" onClicked: { - if (root.onDelete) { + if (root.onDelete) root.onDelete(root.event.id); - } + root.StackView.view.pop(); } } Item { Layout.fillHeight: true - } // Spacer + } } } diff --git a/apps/calendar/pages/EventListPage.qml b/apps/calendar/pages/EventListPage.qml index d6ff17bf9..1f7219c79 100644 --- a/apps/calendar/pages/EventListPage.qml +++ b/apps/calendar/pages/EventListPage.qml @@ -1,26 +1,23 @@ -import QtQuick import MarathonApp.Calendar -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Containers -import MarathonUI.Theme import MarathonUI.Core -import "../stores" +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Page { - background: Rectangle { - color: MColors.background - } - property int updateTrigger: 0 MScrollView { id: scrollView + anchors.fill: parent contentHeight: calendarContent.height + 40 Column { id: calendarContent + width: parent.width spacing: MSpacing.xl leftPadding: 24 @@ -42,11 +39,11 @@ Page { var count = eventListRepeater.count; if (count === 0) return "No events scheduled."; + return count + " event" + (count === 1 ? "" : "s"); } width: parent.width - 48 - // Clear selection button MButton { visible: calendarApp.selectedDate !== null text: "Show All Events" @@ -57,14 +54,13 @@ Page { Repeater { id: eventListRepeater + model: { - // Dependency on dataChanged signal var _ = updateTrigger; + if (calendarApp.selectedDate) + return calendarApp.getEventsForDate(calendarApp.selectedDate); - if (calendarApp.selectedDate) { - return CalendarStorage.getEventsForDate(calendarApp.selectedDate); - } - return CalendarStorage.events; + return calendarApp.getAllEvents(); } EventListItem { @@ -80,10 +76,11 @@ Page { } Connections { - target: CalendarStorage function onDataChanged() { updateTrigger++; } + + target: calendarApp } Item { @@ -91,4 +88,8 @@ Page { } } } + + background: Rectangle { + color: MColors.background + } } diff --git a/apps/calendar/stores/CalendarStorage.qml b/apps/calendar/stores/CalendarStorage.qml index 1e76a64ff..d709d1b9a 100644 --- a/apps/calendar/stores/CalendarStorage.qml +++ b/apps/calendar/stores/CalendarStorage.qml @@ -1,3 +1,4 @@ +pragma Singleton import MarathonOS.Shell import QtQuick @@ -8,148 +9,84 @@ QtObject { property int nextEventId: 1 property Connections calendarConnection - calendarConnection: Connections { - function onEventsLoaded() { - root.events = CalendarManager.events; - root.dataChanged(); - } - - function onEventCreated(event) { - root.events = CalendarManager.events; - root.dataChanged(); - } - - function onEventUpdated(event) { - root.events = CalendarManager.events; - root.dataChanged(); - } - - function onEventDeleted(eventId) { - root.events = CalendarManager.events; - root.dataChanged(); - } - - target: typeof CalendarManager !== 'undefined' ? CalendarManager : null - enabled: typeof CalendarManager !== 'undefined' - } - - // Signal to notify listeners of changes signal dataChanged function init() { - if (typeof CalendarManager !== 'undefined') { - events = CalendarManager.events; - nextEventId = CalendarManager.nextEventId; - } else { - var savedEvents = SettingsManagerCpp.get("calendar/events", "[]"); - try { - events = JSON.parse(savedEvents); - if (events.length > 0) { - // Ensure IDs are numbers - events.forEach(e => { - return e.id = Number(e.id); - }); - nextEventId = Math.max(events.map(e => { - return e.id; - })) + 1; - } - } catch (e) { - Logger.error("CalendarStorage", "Failed to load events: " + e); - events = []; - } - } + if (typeof CalendarManager === "undefined") + return; + + events = CalendarManager.events; + nextEventId = CalendarManager.nextEventId; dataChanged(); } function save() { - // No-op if using CalendarManager, it handles saving - if (typeof CalendarManager === 'undefined') { - var data = JSON.stringify(events); - SettingsManagerCpp.set("calendar/events", data); - dataChanged(); - } + dataChanged(); } function addEvent(event) { - if (typeof CalendarManager !== 'undefined') { - var newEvent = CalendarManager.createEvent(event); - events = CalendarManager.events; - dataChanged(); - return newEvent; - } else { - event.id = nextEventId++; - event.timestamp = Date.now(); - events.push(event); - save(); - return event; - } + if (typeof CalendarManager === "undefined") + return null; + + var newEvent = CalendarManager.createEvent(event); + events = CalendarManager.events; + dataChanged(); + return newEvent; } function updateEvent(event) { - if (typeof CalendarManager !== 'undefined') { - var result = CalendarManager.updateEvent(event); - events = CalendarManager.events; - dataChanged(); - return result; - } else { - for (var i = 0; i < events.length; i++) { - if (events[i].id === event.id) { - events[i] = event; - save(); - return true; - } - } + if (typeof CalendarManager === "undefined") return false; - } + + var result = CalendarManager.updateEvent(event); + events = CalendarManager.events; + dataChanged(); + return result; } function deleteEvent(id) { - if (typeof CalendarManager !== 'undefined') { - var result = CalendarManager.deleteEvent(id); - events = CalendarManager.events; - dataChanged(); - return result; - } else { - for (var i = 0; i < events.length; i++) { - if (events[i].id === id) { - events.splice(i, 1); - save(); - return true; - } - } + if (typeof CalendarManager === "undefined") return false; - } + + var result = CalendarManager.deleteEvent(id); + events = CalendarManager.events; + dataChanged(); + return result; } function getEventsForDate(date) { - if (typeof CalendarManager !== 'undefined') - return CalendarManager.getEventsForDate(date); - - var dateStr = Qt.formatDate(date, "yyyy-MM-dd"); - var result = []; - for (var i = 0; i < events.length; i++) { - var event = events[i]; - if (event.date === dateStr) { - result.push(event); - } else if (event.recurring !== "none") { - var eventDate = new Date(event.date); - var checkDate = new Date(date); - if (event.recurring === "daily" && checkDate >= eventDate) { - result.push(event); - } else if (event.recurring === "weekly" && checkDate >= eventDate) { - var daysDiff = Math.floor((checkDate - eventDate) / (1000 * 60 * 60 * 24)); - if (daysDiff % 7 === 0) - result.push(event); - } else if (event.recurring === "monthly" && checkDate >= eventDate) { - if (checkDate.getDate() === eventDate.getDate()) - result.push(event); - } - } - } - return result; + if (typeof CalendarManager === "undefined") + return []; + + return CalendarManager.getEventsForDate(date); } function getAllEvents() { return events; } + + calendarConnection: Connections { + function onEventsLoaded() { + root.events = CalendarManager.events; + root.dataChanged(); + } + + function onEventCreated(event) { + root.events = CalendarManager.events; + root.dataChanged(); + } + + function onEventUpdated(event) { + root.events = CalendarManager.events; + root.dataChanged(); + } + + function onEventDeleted(eventId) { + root.events = CalendarManager.events; + root.dataChanged(); + } + + target: typeof CalendarManager !== "undefined" ? CalendarManager : null + enabled: typeof CalendarManager !== "undefined" + } } diff --git a/apps/camera/CameraApp.qml b/apps/camera/CameraApp.qml index 99a91c121..1d8111baa 100644 --- a/apps/camera/CameraApp.qml +++ b/apps/camera/CameraApp.qml @@ -7,6 +7,7 @@ import MarathonUI.Theme import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls +import QtMultimedia 6.0 MApp { id: cameraApp diff --git a/apps/camera/src/CameraController.cpp b/apps/camera/src/CameraController.cpp index 6b6632b0b..3940d103a 100644 --- a/apps/camera/src/CameraController.cpp +++ b/apps/camera/src/CameraController.cpp @@ -190,15 +190,16 @@ void CameraController::focusOnPoint(qreal x, qreal y) { } } -QVideoSink *CameraController::videoSink() const { +QObject *CameraController::videoSink() const { return m_videoSink; } -void CameraController::setVideoSink(QVideoSink *sink) { - if (m_videoSink == sink) +void CameraController::setVideoSink(QObject *sink) { + auto *videoSink = qobject_cast(sink); + if (m_videoSink == videoSink) return; - m_videoSink = sink; + m_videoSink = videoSink; m_captureSession->setVideoOutput(m_videoSink); emit videoSinkChanged(); } diff --git a/apps/camera/src/CameraController.h b/apps/camera/src/CameraController.h index 15a481763..8f405757c 100644 --- a/apps/camera/src/CameraController.h +++ b/apps/camera/src/CameraController.h @@ -15,7 +15,7 @@ class CameraController : public QObject { Q_OBJECT QML_ELEMENT - Q_PROPERTY(QVideoSink *videoSink READ videoSink WRITE setVideoSink NOTIFY videoSinkChanged) + Q_PROPERTY(QObject *videoSink READ videoSink WRITE setVideoSink NOTIFY videoSinkChanged) Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged) Q_PROPERTY(bool active READ isActive NOTIFY activeChanged) Q_PROPERTY(int cameraCount READ cameraCount NOTIFY cameraCountChanged) @@ -46,8 +46,8 @@ class CameraController : public QObject { explicit CameraController(QObject *parent = nullptr); ~CameraController() override; - QVideoSink *videoSink() const; - void setVideoSink(QVideoSink *sink); + QObject *videoSink() const; + void setVideoSink(QObject *sink); bool isReady() const; bool isActive() const; diff --git a/apps/camera/src/SoftwareVideoOutput.cpp b/apps/camera/src/SoftwareVideoOutput.cpp index 4ab3408a2..c336852bf 100644 --- a/apps/camera/src/SoftwareVideoOutput.cpp +++ b/apps/camera/src/SoftwareVideoOutput.cpp @@ -12,7 +12,7 @@ SoftwareVideoOutput::SoftwareVideoOutput(QQuickItem *parent) SoftwareVideoOutput::~SoftwareVideoOutput() {} -QVideoSink *SoftwareVideoOutput::videoSink() const { +QObject *SoftwareVideoOutput::videoSink() const { return m_videoSink; } diff --git a/apps/camera/src/SoftwareVideoOutput.h b/apps/camera/src/SoftwareVideoOutput.h index 3adcd1071..30786f6dd 100644 --- a/apps/camera/src/SoftwareVideoOutput.h +++ b/apps/camera/src/SoftwareVideoOutput.h @@ -12,7 +12,7 @@ class SoftwareVideoOutput : public QQuickItem { Q_OBJECT QML_ELEMENT - Q_PROPERTY(QVideoSink *videoSink READ videoSink CONSTANT) + Q_PROPERTY(QObject *videoSink READ videoSink CONSTANT) Q_PROPERTY(int fillMode READ fillMode WRITE setFillMode NOTIFY fillModeChanged) public: @@ -26,10 +26,10 @@ class SoftwareVideoOutput : public QQuickItem { explicit SoftwareVideoOutput(QQuickItem *parent = nullptr); ~SoftwareVideoOutput() override; - QVideoSink *videoSink() const; + QObject *videoSink() const; - int fillMode() const; - void setFillMode(int mode); + int fillMode() const; + void setFillMode(int mode); protected: QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; diff --git a/apps/clock/ClockApp.qml b/apps/clock/ClockApp.qml index 5b3e87bd5..468f82edb 100644 --- a/apps/clock/ClockApp.qml +++ b/apps/clock/ClockApp.qml @@ -7,140 +7,83 @@ import QtQuick import QtQuick.Layouts MApp { - // appIcon loaded from registry - don't override here - id: clockApp - property var alarms // No initial binding - set by loadAlarms() - property int nextAlarmId: 1 + property var alarms: [] function loadAlarms() { - if (typeof AlarmManager !== 'undefined') { - alarms = AlarmManager.alarms; - } else { - var savedAlarms = SettingsManagerCpp.get("clock/alarms", "[]"); - try { - alarms = JSON.parse(savedAlarms); - if (alarms.length > 0) - nextAlarmId = Math.max(alarms.map(a => { - return parseInt(a.id) || 0; - })) + 1; - } catch (e) { - Logger.error("ClockApp", "Failed to load alarms: " + e); - alarms = []; // Fallback to empty array on error - } - } - alarmsChanged(); - } + if (typeof AlarmService === 'undefined') + return; - function saveAlarms() { - var data = JSON.stringify(alarms); - SettingsManagerCpp.set("clock/alarms", data); + alarms = AlarmService.alarms; + alarmsChanged(); } function createAlarm(hour, minute, label, enabled) { + if (typeof AlarmService === 'undefined') + return null; + var timeString = _padZero(hour) + ":" + _padZero(minute); var repeat = []; - if (typeof AlarmManager !== 'undefined') { - var alarmId = AlarmManager.createAlarm(timeString, label, repeat, { - "vibrate": true, - "snoozeEnabled": true, - "snoozeDuration": 10 - }); - loadAlarms(); - return { - "id": alarmId, - "hour": hour, - "minute": minute, - "label": label, - "enabled": true, - "repeat": repeat - }; - } else { - var alarm = { - "id": String(nextAlarmId++), - "time": timeString, - "hour": hour, - "minute": minute, - "label": label || "Alarm", - "enabled": enabled !== undefined ? enabled : true, - "repeat": [] - }; - alarms.push(alarm); - alarmsChanged(); - saveAlarms(); - return alarm; - } + var alarmId = AlarmService.createAlarm(timeString, label, repeat, { + "vibrate": true, + "snoozeEnabled": true, + "snoozeDuration": 10 + }); + loadAlarms(); + return { + "id": alarmId, + "hour": hour, + "minute": minute, + "label": label, + "enabled": enabled !== undefined ? enabled : true, + "repeat": repeat + }; } function updateAlarm(id, hour, minute, label, enabled, repeat) { + if (typeof AlarmService === 'undefined') + return false; + var timeString = _padZero(hour) + ":" + _padZero(minute); - if (typeof AlarmManager !== 'undefined') { - var repeatDays = []; - if (repeat) { - for (var i = 0; i < repeat.length; i++) { - if (repeat[i]) - repeatDays.push(i); - } - } - AlarmManager.updateAlarm(id, { - "time": timeString, - "label": label, - "enabled": enabled, - "repeat": repeatDays - }); - loadAlarms(); - } else { - for (var i = 0; i < alarms.length; i++) { - if (alarms[i].id === id) { - alarms[i].time = timeString; - alarms[i].hour = hour; - alarms[i].minute = minute; - alarms[i].label = label; - alarms[i].enabled = enabled; - if (repeat) - alarms[i].repeat = repeat; - - alarmsChanged(); - saveAlarms(); - return true; - } + var repeatDays = []; + if (repeat) { + for (var i = 0; i < repeat.length; i++) { + if (repeat[i]) + repeatDays.push(i); } } + AlarmService.updateAlarm(id, { + "time": timeString, + "label": label, + "enabled": enabled, + "repeat": repeatDays + }); + loadAlarms(); return true; } function deleteAlarm(id) { - if (typeof AlarmManager !== 'undefined') { - AlarmManager.deleteAlarm(id); - loadAlarms(); - } else { - for (var i = 0; i < alarms.length; i++) { - if (alarms[i].id === id) { - alarms.splice(i, 1); - alarmsChanged(); - saveAlarms(); - return true; - } - } - } + if (typeof AlarmService === 'undefined') + return false; + + AlarmService.deleteAlarm(id); + loadAlarms(); return true; } function toggleAlarm(id) { - if (typeof AlarmManager !== 'undefined') { - AlarmManager.toggleAlarm(id); - loadAlarms(); - } else { - for (var i = 0; i < alarms.length; i++) { - if (alarms[i].id === id) { - alarms[i].enabled = !alarms[i].enabled; - alarmsChanged(); - saveAlarms(); - return true; - } - } - } + if (typeof AlarmService === 'undefined') + return false; + + var alarm = _getAlarmById(id); + if (!alarm) + return false; + + AlarmService.updateAlarm(id, { + "enabled": !alarm.enabled + }); + loadAlarms(); return true; } @@ -148,6 +91,16 @@ MApp { return (num < 10 ? "0" : "") + num; } + function _getAlarmById(id) { + if (!alarms) + return null; + for (var i = 0; i < alarms.length; i++) { + if (alarms[i].id === id) + return alarms[i]; + } + return null; + } + appId: "clock" appName: "Clock" Component.onCompleted: { @@ -155,20 +108,12 @@ MApp { } Connections { - function onAlarmCreated() { - loadAlarms(); - } - - function onAlarmUpdated() { - loadAlarms(); - } - - function onAlarmDeleted() { + function onAlarmsChanged() { loadAlarms(); } - target: typeof AlarmManager !== 'undefined' ? AlarmManager : null - enabled: typeof AlarmManager !== 'undefined' + target: typeof AlarmService !== 'undefined' ? AlarmService : null + enabled: typeof AlarmService !== 'undefined' } content: Rectangle { diff --git a/apps/clock/components/AddCityDialog.qml b/apps/clock/components/AddCityDialog.qml index a213d8669..2bfe17783 100644 --- a/apps/clock/components/AddCityDialog.qml +++ b/apps/clock/components/AddCityDialog.qml @@ -1,154 +1,151 @@ -import QtQuick import MarathonApp.Clock import MarathonOS.Shell -import MarathonUI.Core import MarathonUI.Containers +import MarathonUI.Core import MarathonUI.Theme +import QtQuick Item { id: addCityDialog - visible: false - anchors.fill: parent - z: Constants.zIndexModalOverlay - - signal cityAdded(string name, int offset) - - function open() { - visible = true; - searchQuery = ""; - filterCities(); - if (searchInput && searchInput.forceActiveFocus) { - searchInput.forceActiveFocus(); - } - } - - function close() { - visible = false; - } - - function filterCities() { - filteredCities.clear(); - var query = searchQuery.toLowerCase(); - for (var i = 0; i < availableCities.length; i++) { - var city = availableCities[i]; - if (query === "" || city.name.toLowerCase().indexOf(query) !== -1) { - filteredCities.append({ - cityName: city.name, - cityOffset: city.offset - }); - } - } - } property var availableCities: [ { - name: "New York", - offset: -5 + "name": "New York", + "offset": -5 }, { - name: "Los Angeles", - offset: -8 + "name": "Los Angeles", + "offset": -8 }, { - name: "Chicago", - offset: -6 + "name": "Chicago", + "offset": -6 }, { - name: "Toronto", - offset: -5 + "name": "Toronto", + "offset": -5 }, { - name: "Mexico City", - offset: -6 + "name": "Mexico City", + "offset": -6 }, { - name: "São Paulo", - offset: -3 + "name": "São Paulo", + "offset": -3 }, { - name: "London", - offset: 0 + "name": "London", + "offset": 0 }, { - name: "Paris", - offset: 1 + "name": "Paris", + "offset": 1 }, { - name: "Berlin", - offset: 1 + "name": "Berlin", + "offset": 1 }, { - name: "Rome", - offset: 1 + "name": "Rome", + "offset": 1 }, { - name: "Madrid", - offset: 1 + "name": "Madrid", + "offset": 1 }, { - name: "Amsterdam", - offset: 1 + "name": "Amsterdam", + "offset": 1 }, { - name: "Stockholm", - offset: 1 + "name": "Stockholm", + "offset": 1 }, { - name: "Moscow", - offset: 3 + "name": "Moscow", + "offset": 3 }, { - name: "Dubai", - offset: 4 + "name": "Dubai", + "offset": 4 }, { - name: "Mumbai", - offset: 5.5 + "name": "Mumbai", + "offset": 5.5 }, { - name: "Bangkok", - offset: 7 + "name": "Bangkok", + "offset": 7 }, { - name: "Singapore", - offset: 8 + "name": "Singapore", + "offset": 8 }, { - name: "Hong Kong", - offset: 8 + "name": "Hong Kong", + "offset": 8 }, { - name: "Shanghai", - offset: 8 + "name": "Shanghai", + "offset": 8 }, { - name: "Beijing", - offset: 8 + "name": "Beijing", + "offset": 8 }, { - name: "Tokyo", - offset: 9 + "name": "Tokyo", + "offset": 9 }, { - name: "Seoul", - offset: 9 + "name": "Seoul", + "offset": 9 }, { - name: "Sydney", - offset: 10 + "name": "Sydney", + "offset": 10 }, { - name: "Melbourne", - offset: 10 + "name": "Melbourne", + "offset": 10 }, { - name: "Auckland", - offset: 12 + "name": "Auckland", + "offset": 12 } ] - property string searchQuery: "" + signal cityAdded(string name, int offset) + + function open() { + visible = true; + searchQuery = ""; + filterCities(); + if (searchInput && searchInput.forceActiveFocus) + searchInput.forceActiveFocus(); + } + + function close() { + visible = false; + } + + function filterCities() { + filteredCities.clear(); + var query = searchQuery.toLowerCase(); + for (var i = 0; i < availableCities.length; i++) { + var city = availableCities[i]; + if (query === "" || city.name.toLowerCase().indexOf(query) !== -1) + filteredCities.append({ + "cityName": city.name, + "cityOffset": city.offset + }); + } + } + + visible: false + anchors.fill: parent + z: Constants.zIndexModalOverlay onSearchQueryChanged: filterCities() Rectangle { @@ -189,6 +186,7 @@ Item { MIconButton { id: closeButton + width: Constants.touchTargetMedium height: Constants.touchTargetMedium anchors.verticalCenter: parent.verticalCenter @@ -204,6 +202,7 @@ Item { MTextInput { id: searchInput + width: parent.width placeholderText: "Search cities..." onTextChanged: { @@ -215,10 +214,28 @@ Item { width: parent.width height: parent.height - parent.spacing * 2 - Constants.touchTargetMedium - Math.round(48 * Constants.scaleFactor) clip: true + spacing: 0 + + Rectangle { + anchors.centerIn: parent + width: parent.width * 0.8 + height: noResultsText.height + color: "transparent" + visible: parent.count === 0 + + Text { + id: noResultsText + + anchors.centerIn: parent + text: "No cities found" + color: MColors.textSecondary + font.pixelSize: MTypography.sizeBody + } + } + model: ListModel { id: filteredCities } - spacing: 0 delegate: Item { width: ListView.view.width @@ -257,6 +274,7 @@ Item { Text { id: offsetText + anchors.verticalCenter: parent.verticalCenter text: (model.cityOffset >= 0 ? "+" : "") + model.cityOffset + "h" color: MColors.textSecondary @@ -266,22 +284,6 @@ Item { } } } - - Rectangle { - anchors.centerIn: parent - width: parent.width * 0.8 - height: noResultsText.height - color: "transparent" - visible: parent.count === 0 - - Text { - id: noResultsText - anchors.centerIn: parent - text: "No cities found" - color: MColors.textSecondary - font.pixelSize: MTypography.sizeBody - } - } } } } diff --git a/apps/clock/components/AlarmItem.qml b/apps/clock/components/AlarmItem.qml index def6e2852..a3a20ce7c 100644 --- a/apps/clock/components/AlarmItem.qml +++ b/apps/clock/components/AlarmItem.qml @@ -1,15 +1,14 @@ -import QtQuick import MarathonApp.Clock import MarathonOS.Shell -import MarathonUI.Controls import MarathonUI.Containers +import MarathonUI.Controls import MarathonUI.Theme +import QtQuick Item { id: alarmItem - height: card.height + MSpacing.md - property int alarmId: -1 + property string alarmId: "" property int alarmHour: 0 property int alarmMinute: 0 property string alarmLabel: "Alarm" @@ -29,18 +28,21 @@ Item { } if (h === 0) h = 12; + return h + ":" + (minute < 10 ? "0" : "") + minute + " " + suffix; } + height: card.height + MSpacing.md + MCard { id: card + anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: MSpacing.lg anchors.rightMargin: MSpacing.lg elevation: 1 interactive: true - onClicked: alarmItem.clicked() Row { @@ -71,6 +73,7 @@ Item { MToggle { id: toggleSwitch + anchors.verticalCenter: parent.verticalCenter checked: alarmEnabled onToggled: { diff --git a/apps/clock/components/ClockIcon.qml b/apps/clock/components/ClockIcon.qml index d91498ec1..eaa96b550 100644 --- a/apps/clock/components/ClockIcon.qml +++ b/apps/clock/components/ClockIcon.qml @@ -1,8 +1,9 @@ -import QtQuick import MarathonApp.Clock +import QtQuick Image { id: icon + property string name: "clock" property color color: "#FFFFFF" property int size: Constants.iconSizeMedium @@ -15,8 +16,5 @@ Image { smooth: true asynchronous: true cache: true - - // Color filtering disabled - requires Qt 6.5+ MultiEffect or Qt 5.15 ColorOverlay - // For now, use icon as-is. TODO: Add colorization support for Qt 6.4 layer.enabled: false } diff --git a/apps/clock/pages/AlarmEditorDialog.qml b/apps/clock/pages/AlarmEditorDialog.qml index 18f20ca40..a55ae053d 100644 --- a/apps/clock/pages/AlarmEditorDialog.qml +++ b/apps/clock/pages/AlarmEditorDialog.qml @@ -1,16 +1,12 @@ -import QtQuick import MarathonApp.Clock -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Rectangle { id: dialog - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.85) - visible: false - z: Constants.zIndexModalOverlay property int editingAlarmId: -1 property bool isEditMode: false @@ -38,6 +34,11 @@ Rectangle { dialog.visible = false; } + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.85) + visible: false + z: Constants.zIndexModalOverlay + MouseArea { anchors.fill: parent onClicked: dialog.close() @@ -76,9 +77,11 @@ Rectangle { Tumbler { id: hourTumbler + width: 100 height: 200 model: 24 + delegate: Text { text: { var str = modelData.toString(); @@ -86,7 +89,7 @@ Rectangle { } font.pixelSize: MTypography.sizeLarge color: MColors.text - opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) + opacity: 1 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } @@ -102,9 +105,11 @@ Rectangle { Tumbler { id: minuteTumbler + width: 100 height: 200 model: 60 + delegate: Text { text: { var str = modelData.toString(); @@ -112,7 +117,7 @@ Rectangle { } font.pixelSize: MTypography.sizeLarge color: MColors.text - opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) + opacity: 1 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } @@ -141,11 +146,10 @@ Rectangle { variant: "primary" onClicked: { HapticService.light(); - if (isEditMode) { + if (isEditMode) dialog.alarmUpdated(editingAlarmId, hourTumbler.currentIndex, minuteTumbler.currentIndex); - } else { + else dialog.alarmCreated(hourTumbler.currentIndex, minuteTumbler.currentIndex); - } dialog.close(); } } diff --git a/apps/clock/pages/AlarmPage.qml b/apps/clock/pages/AlarmPage.qml index cf892f9c8..e8355be44 100644 --- a/apps/clock/pages/AlarmPage.qml +++ b/apps/clock/pages/AlarmPage.qml @@ -1,8 +1,8 @@ -import QtQuick import MarathonApp.Clock import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick Item { id: alarmPage @@ -17,47 +17,13 @@ Item { ListView { id: alarmsList + anchors.fill: parent anchors.topMargin: MSpacing.md clip: true model: clockApp.alarms spacing: 0 - delegate: AlarmItem { - width: alarmsList.width - alarmId: modelData.id - alarmHour: modelData.time ? _getHour(modelData.time) : (modelData.hour || 0) - alarmMinute: modelData.time ? _getMinute(modelData.time) : (modelData.minute || 0) - alarmLabel: modelData.label - alarmEnabled: modelData.enabled - - onClicked: { - alarmEditorDialog.openForEdit(alarmId, alarmHour, alarmMinute, alarmLabel); - } - - onToggled: { - clockApp.toggleAlarm(alarmId); - } - - onDeleted: { - clockApp.deleteAlarm(alarmId); - } - - function _getHour(timeStr) { - if (!timeStr) - return 0; - var parts = timeStr.split(":"); - return parseInt(parts[0]); - } - - function _getMinute(timeStr) { - if (!timeStr) - return 0; - var parts = timeStr.split(":"); - return parseInt(parts[1]); - } - } - Rectangle { anchors.centerIn: parent width: Math.min(parent.width * 0.8, Constants.screenWidth * 0.6) @@ -67,6 +33,7 @@ Item { Column { id: emptyColumn + anchors.centerIn: parent spacing: MSpacing.lg @@ -97,6 +64,40 @@ Item { } } } + + delegate: AlarmItem { + function _getHour(timeStr) { + if (!timeStr) + return 0; + + var parts = timeStr.split(":"); + return parseInt(parts[0]); + } + + function _getMinute(timeStr) { + if (!timeStr) + return 0; + + var parts = timeStr.split(":"); + return parseInt(parts[1]); + } + + width: alarmsList.width + alarmId: modelData && modelData.id !== undefined ? modelData.id : "" + alarmHour: modelData && modelData.time ? _getHour(modelData.time) : (modelData && modelData.hour !== undefined ? modelData.hour : 0) + alarmMinute: modelData && modelData.time ? _getMinute(modelData.time) : (modelData && modelData.minute !== undefined ? modelData.minute : 0) + alarmLabel: modelData && modelData.label !== undefined ? modelData.label : "Alarm" + alarmEnabled: modelData && modelData.enabled !== undefined ? modelData.enabled : false + onClicked: { + alarmEditorDialog.openForEdit(alarmId, alarmHour, alarmMinute, alarmLabel); + } + onToggled: { + clockApp.toggleAlarm(alarmId); + } + onDeleted: { + clockApp.deleteAlarm(alarmId); + } + } } } } @@ -116,6 +117,7 @@ Item { AlarmEditorDialog { id: alarmEditorDialog + onAlarmCreated: function (hour, minute) { clockApp.createAlarm(hour, minute, "Alarm", true); } diff --git a/apps/clock/pages/ClockPage.qml b/apps/clock/pages/ClockPage.qml index 58e924cdd..4909b4edb 100644 --- a/apps/clock/pages/ClockPage.qml +++ b/apps/clock/pages/ClockPage.qml @@ -1,7 +1,7 @@ -import QtQuick import MarathonApp.Clock import MarathonOS.Shell import MarathonUI.Theme +import QtQuick Item { id: clockPage @@ -23,8 +23,6 @@ Item { hours = now.getHours(); minutes = now.getMinutes(); seconds = now.getSeconds(); - - // Format date for 3 o'clock position dayOfMonth = now.getDate().toString(); var days = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; dayOfWeek = days[now.getDay()]; @@ -35,64 +33,37 @@ Item { anchors.fill: parent color: MColors.background - // Main analog clock - centered and large, accounting for alarm bar Item { anchors.centerIn: parent width: Math.min(parent.width * 0.7, parent.height * 0.55) height: width - // Account for alarm bar when centering anchors.verticalCenterOffset: (clockApp.alarms && clockApp.alarms.length > 0) ? -Constants.actionBarHeight / 2 : 0 - // Squircle clock face with neumorphic design - raised from background Item { id: clockFaceContainer + + property real contentScale: 1 / 1.1 + anchors.centerIn: parent - width: parent.width * 1.10 - height: parent.height * 1.10 - - // Dark shadow layer (bottom-right) - disabled (requires Qt 6.5+ MultiEffect) - // Rectangle { - // anchors.fill: parent - // anchors.margins: -20 - // radius: width * 0.22 - // color: "transparent" - // - // layer.enabled: false - // } - - // Light shadow layer (top-left) - disabled (requires Qt 6.5+ MultiEffect) - // Rectangle { - // anchors.fill: parent - // anchors.margins: -20 - // radius: width * 0.22 - // color: "transparent" - // - // layer.enabled: false - // } - - // Main clock face + width: parent.width * 1.1 + height: parent.height * 1.1 + Rectangle { id: clockFace + anchors.fill: parent color: MColors.surface radius: width * 0.22 - - // Subtle inner border for definition border.width: Constants.borderWidthThin border.color: Qt.rgba(0, 0, 0, 0.05) } - // Scale factor to keep clock content same size inside larger frame - property real contentScale: 1.0 / 1.10 - - // Container to scale clock content to original size inside the face Item { parent: clockFaceContainer anchors.centerIn: parent width: clockFaceContainer.width * clockFaceContainer.contentScale height: clockFaceContainer.height * clockFaceContainer.contentScale - // Hour markers (all 60 ticks, with emphasis on hours) Repeater { model: 60 @@ -107,7 +78,6 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: MSpacing.md - width: isHourMarker ? Constants.borderWidthThick : Constants.borderWidthThin height: isHourMarker ? MSpacing.md : MSpacing.sm color: MColors.marathonTeal @@ -115,7 +85,6 @@ Item { } } - // Number: 12 Text { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top @@ -126,7 +95,6 @@ Item { color: MColors.marathonTeal } - // Number: 3 with date Column { anchors.right: parent.right anchors.rightMargin: MSpacing.xl @@ -158,7 +126,6 @@ Item { } } - // Number: 6 Text { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom @@ -169,7 +136,6 @@ Item { color: MColors.marathonTeal } - // Number: 9 Text { anchors.left: parent.left anchors.leftMargin: MSpacing.xl @@ -180,7 +146,6 @@ Item { color: MColors.marathonTeal } - // PM indicator Text { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter @@ -191,50 +156,33 @@ Item { color: MColors.marathonTeal } - // Hour hand - darker gray with inner baton stripe (40% from center) Item { id: hourHand + + property real handLength: parent.height * 0.25 + width: parent.width height: parent.height rotation: (hours % 12) * 30 + minutes * 0.5 - Behavior on rotation { - RotationAnimation { - duration: Constants.animationSlow - direction: RotationAnimation.Shortest - } - } - - property real handLength: parent.height * 0.25 - - // Outer hand Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: -height / 2 width: Constants.borderWidthThick + 4 height: hourHand.handLength - color: "#4A4A4A" // Darker gray + color: "#4A4A4A" radius: width / 2 } - // Inner baton (lighter stripe - only 40% from center) Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: -(hourHand.handLength * 0.4) / 2 width: Constants.borderWidthThin height: hourHand.handLength * 0.4 - color: "#707070" // Lighter gray baton + color: "#707070" } - } - - // Minute hand - even darker gray with inner baton (FULL LENGTH) - Item { - id: minuteHand - width: parent.width - height: parent.height - rotation: minutes * 6 + seconds * 0.1 Behavior on rotation { RotationAnimation { @@ -242,45 +190,51 @@ Item { direction: RotationAnimation.Shortest } } + } + + Item { + id: minuteHand property real handLength: parent.height * 0.38 - // Outer hand + width: parent.width + height: parent.height + rotation: minutes * 6 + seconds * 0.1 + Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: -height / 2 width: Constants.borderWidthThick + 2 height: minuteHand.handLength - color: "#2A2A2A" // Even darker gray + color: "#2A2A2A" radius: width / 2 } - // Inner baton (lighter stripe - FULL LENGTH to tip) Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: -height / 2 width: Constants.borderWidthThin height: minuteHand.handLength - color: "#505050" // Lighter gray baton + color: "#505050" + } + + Behavior on rotation { + RotationAnimation { + duration: Constants.animationSlow + direction: RotationAnimation.Shortest + } } } - // Second hand - teal accent, thin Item { id: secondHand + width: parent.width height: parent.height rotation: seconds * 6 - Behavior on rotation { - RotationAnimation { - duration: Constants.animationFast - direction: RotationAnimation.Shortest - } - } - Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter @@ -289,24 +243,29 @@ Item { height: parent.height * 0.42 color: MColors.marathonTeal } + + Behavior on rotation { + RotationAnimation { + duration: Constants.animationFast + direction: RotationAnimation.Shortest + } + } } - // Center pivot point - circular Rectangle { anchors.centerIn: parent width: MSpacing.md height: MSpacing.md radius: width / 2 - color: "#404040" // Dark gray + color: "#404040" border.width: 1 border.color: "#606060" z: 10 } - } // End of scaled content container + } } } - // Bottom alarm info bar (if alarms exist) Rectangle { anchors.left: parent.left anchors.right: parent.right @@ -328,10 +287,12 @@ Item { text: { if (!clockApp.alarms || clockApp.alarms.length === 0) return ""; + var alarm = clockApp.alarms[0]; var h = (alarm.hour !== undefined ? alarm.hour : 0) % 12; if (h === 0) h = 12; + var m = alarm.minute !== undefined ? alarm.minute : 0; var mStr = m < 10 ? "0" + m : m.toString(); return h + ":" + mStr; @@ -356,7 +317,6 @@ Item { } } - // Alarm toggle Rectangle { anchors.right: parent.right anchors.rightMargin: MSpacing.lg diff --git a/apps/clock/pages/StopwatchPage.qml b/apps/clock/pages/StopwatchPage.qml index 434028982..ab37cb0b3 100644 --- a/apps/clock/pages/StopwatchPage.qml +++ b/apps/clock/pages/StopwatchPage.qml @@ -1,8 +1,8 @@ -import QtQuick import MarathonApp.Clock import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick Item { id: stopwatchPage @@ -11,8 +11,17 @@ Item { property bool isRunning: false property var laps: [] + function formatTime(ms) { + var totalSeconds = Math.floor(ms / 1000); + var minutes = Math.floor(totalSeconds / 60); + var seconds = totalSeconds % 60; + var centiseconds = Math.floor((ms % 1000) / 10); + return (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds + "." + (centiseconds < 10 ? "0" : "") + centiseconds; + } + Timer { id: stopwatchTimer + interval: 10 running: isRunning repeat: true @@ -21,14 +30,6 @@ Item { } } - function formatTime(ms) { - var totalSeconds = Math.floor(ms / 1000); - var minutes = Math.floor(totalSeconds / 60); - var seconds = totalSeconds % 60; - var centiseconds = Math.floor((ms % 1000) / 10); - return (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds + "." + (centiseconds < 10 ? "0" : "") + centiseconds; - } - Column { anchors.fill: parent spacing: 0 diff --git a/apps/clock/pages/TimerPage.qml b/apps/clock/pages/TimerPage.qml index 02aa87b34..9be026224 100644 --- a/apps/clock/pages/TimerPage.qml +++ b/apps/clock/pages/TimerPage.qml @@ -36,7 +36,7 @@ Item { remainingSeconds--; } else { isRunning = false; - HapticService.strong(); + HapticService.heavy(); } } } diff --git a/apps/clock/pages/WorldClockPage.qml b/apps/clock/pages/WorldClockPage.qml index 0a2162a59..34b0afbd2 100644 --- a/apps/clock/pages/WorldClockPage.qml +++ b/apps/clock/pages/WorldClockPage.qml @@ -1,19 +1,16 @@ -import QtQuick import MarathonApp.Clock -import QtQml.Models import MarathonOS.Shell -import MarathonUI.Core import MarathonUI.Containers +import MarathonUI.Core import MarathonUI.Theme +import QtQml.Models +import QtQuick Item { id: worldClockPage property var cities: [] - - Component.onCompleted: { - loadCities(); - } + property AddCityDialog addCityDialog: null function loadCities() { var saved = SettingsManagerCpp.get("clock/worldCities", ""); @@ -39,33 +36,33 @@ Item { function getDefaultCities() { return [ { - name: "New York", - offset: -5, - dst: true + "name": "New York", + "offset": -5, + "dst": true }, { - name: "London", - offset: 0, - dst: false + "name": "London", + "offset": 0, + "dst": false }, { - name: "Tokyo", - offset: 9, - dst: false + "name": "Tokyo", + "offset": 9, + "dst": false }, { - name: "Sydney", - offset: 10, - dst: false + "name": "Sydney", + "offset": 10, + "dst": false } ]; } function addCity(name, offset, dst) { cities.push({ - name: name, - offset: offset, - dst: dst + "name": name, + "offset": offset, + "dst": dst }); citiesChanged(); saveCities(); @@ -82,6 +79,7 @@ Item { function moveCity(from, to) { if (from === to || from < 0 || to < 0 || from >= cities.length || to >= cities.length) return; + var item = cities.splice(from, 1)[0]; cities.splice(to, 0, item); citiesChanged(); @@ -93,24 +91,28 @@ Item { var now = new Date(); for (var i = 0; i < cities.length; i++) { var city = cities[i]; - var cityTime = new Date(now.getTime() + (city.offset * 3600000)); + var cityTime = new Date(now.getTime() + (city.offset * 3.6e+06)); var hours = cityTime.getUTCHours(); var minutes = cityTime.getUTCMinutes(); var ampm = hours >= 12 ? "PM" : "AM"; hours = hours % 12; if (hours === 0) hours = 12; - var timeStr = hours + ":" + (minutes < 10 ? "0" : "") + minutes + " " + ampm; + var timeStr = hours + ":" + (minutes < 10 ? "0" : "") + minutes + " " + ampm; visualModel.model.append({ - cityName: city.name, - cityTime: timeStr, - timeDiff: (city.offset >= 0 ? "+" : "") + city.offset + "h", - cityIndex: i + "cityName": city.name, + "cityTime": timeStr, + "timeDiff": (city.offset >= 0 ? "+" : "") + city.offset + "h", + "cityIndex": i }); } } + Component.onCompleted: { + loadCities(); + } + Timer { interval: 1000 running: true @@ -123,53 +125,34 @@ Item { DelegateModel { id: visualModel + model: ListModel { id: timeModel } delegate: Item { id: delegateRoot - width: cityList.width - height: Constants.touchTargetLarge property int heldIndex: -1 property int dropIndex: -1 + width: cityList.width + height: Constants.touchTargetLarge + Rectangle { id: content - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } + width: delegateRoot.width height: delegateRoot.height color: dragArea.drag.active ? MColors.elevated : "transparent" - - Behavior on color { - ColorAnimation { - duration: 200 - } - } - Drag.active: dragArea.drag.active Drag.source: delegateRoot Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 - states: State { - when: dragArea.drag.active - - ParentChange { - target: content - parent: cityList - } - AnchorChanges { - target: content - anchors { - horizontalCenter: undefined - verticalCenter: undefined - } - } + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter } Row { @@ -199,6 +182,7 @@ Item { Text { id: timeText + anchors.verticalCenter: parent.verticalCenter text: model.cityTime color: MColors.marathonTeal @@ -208,6 +192,7 @@ Item { MIconButton { id: deleteButton + width: Constants.touchTargetMedium height: Constants.touchTargetMedium anchors.verticalCenter: parent.verticalCenter @@ -233,14 +218,13 @@ Item { MouseArea { id: dragArea + anchors.fill: parent drag.target: content drag.axis: Drag.YAxis - onPressed: { delegateRoot.heldIndex = delegateRoot.DelegateModel.itemsIndex; } - onReleased: { if (delegateRoot.dropIndex !== -1 && delegateRoot.heldIndex !== -1) { var from = delegateRoot.heldIndex; @@ -254,21 +238,43 @@ Item { delegateRoot.dropIndex = -1; } } + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + + states: State { + when: dragArea.drag.active + + ParentChange { + target: content + parent: cityList + } + + AnchorChanges { + target: content + + anchors { + horizontalCenter: undefined + verticalCenter: undefined + } + } + } } DropArea { anchors.fill: parent - onEntered: function (drag) { - if (drag && drag.source && drag.source !== delegateRoot && drag.source.heldIndex !== -1) { - drag.source.dropIndex = delegateRoot.DelegateModel.itemsIndex; - } + var sourceItem = drag ? drag.source : null; + if (sourceItem && sourceItem !== delegateRoot) + sourceItem.dropIndex = delegateRoot.DelegateModel.itemsIndex; } - - onExited: function (drag) { - if (drag && drag.source && drag.source !== delegateRoot) { - drag.source.dropIndex = -1; - } + onExited: { + var sourceItem = drag ? drag.source : null; + if (sourceItem && sourceItem !== delegateRoot) + sourceItem.dropIndex = -1; } } } @@ -276,6 +282,7 @@ Item { ListView { id: cityList + anchors.fill: parent anchors.topMargin: MSpacing.md clip: true @@ -300,6 +307,7 @@ Item { Column { id: emptyColumn + anchors.centerIn: parent spacing: MSpacing.lg @@ -340,22 +348,26 @@ Item { shape: "circular" onClicked: { HapticService.light(); - addCityDialogLoader.item.open(); + if (worldClockPage.addCityDialog) + worldClockPage.addCityDialog.open(); } } Loader { id: addCityDialogLoader + anchors.fill: parent active: true asynchronous: true source: "../components/AddCityDialog.qml" + onLoaded: worldClockPage.addCityDialog = item Connections { - target: addCityDialogLoader.item function onCityAdded(name, offset) { worldClockPage.addCity(name, offset, false); } + + target: addCityDialogLoader.item } } } diff --git a/apps/gallery/GalleryApp.qml b/apps/gallery/GalleryApp.qml index b6ea6d7ab..6313e0152 100644 --- a/apps/gallery/GalleryApp.qml +++ b/apps/gallery/GalleryApp.qml @@ -75,12 +75,10 @@ MApp { Layout.fillWidth: true Layout.fillHeight: true clip: true - // Update parent's navigationDepth when stack changes onDepthChanged: { galleryApp.navigationDepth = depth - 1; } - // Handle back button Connections { function onBackPressed() { if (albumsStackView.depth > 1) @@ -90,7 +88,6 @@ MApp { target: galleryApp } - // Transitions pushEnter: Transition { NumberAnimation { property: "x" @@ -163,15 +160,15 @@ MApp { contentWidth: width Column { - width: parent.width - padding: MSpacing.md + width: parent.width - (MSpacing.md * 2) + anchors.horizontalCenter: parent.horizontalCenter spacing: MSpacing.md Repeater { model: albums MCard { - width: parent.width - parent.padding * 2 + width: parent.width height: Constants.touchTargetLarge * 1.5 elevation: 1 interactive: true diff --git a/apps/gallery/pages/AlbumDetailPage.qml b/apps/gallery/pages/AlbumDetailPage.qml index 1f9f09238..7224b4fa7 100644 --- a/apps/gallery/pages/AlbumDetailPage.qml +++ b/apps/gallery/pages/AlbumDetailPage.qml @@ -39,7 +39,6 @@ Rectangle { anchors.fill: parent spacing: 0 - // Custom Header matching MPage style Rectangle { Layout.fillWidth: true Layout.preferredHeight: 56 diff --git a/apps/gallery/pages/PhotoViewerPage.qml b/apps/gallery/pages/PhotoViewerPage.qml index d1ae589b8..8de801164 100644 --- a/apps/gallery/pages/PhotoViewerPage.qml +++ b/apps/gallery/pages/PhotoViewerPage.qml @@ -1,18 +1,14 @@ -import QtQuick import MarathonApp.Gallery -import QtQuick.Layouts import MarathonApp.Gallery import MarathonOS.Shell +import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme -import MarathonUI.Containers +import QtQuick +import QtQuick.Layouts Rectangle { id: photoViewer - anchors.fill: parent - color: "#000000" - visible: false - z: 2000 property var photo: null @@ -25,17 +21,22 @@ Rectangle { visible = false; } + anchors.fill: parent + color: "#000000" + visible: false + z: 2000 + MouseArea { anchors.fill: parent onClicked: { - if (mouse.y > parent.height * 0.1 && mouse.y < parent.height * 0.9) { + if (mouse.y > parent.height * 0.1 && mouse.y < parent.height * 0.9) photoViewer.hide(); - } } } Image { id: photoImage + anchors.centerIn: parent width: parent.width height: parent.height @@ -48,7 +49,7 @@ Rectangle { anchors.fill: parent pinch.target: photoImage pinch.minimumScale: 0.5 - pinch.maximumScale: 3.0 + pinch.maximumScale: 3 pinch.dragAxis: Pinch.XAndYAxis } } @@ -68,7 +69,7 @@ Rectangle { Layout.alignment: Qt.AlignVCenter iconName: "x" iconSize: Constants.iconSizeLarge - iconColor: "#FFFFFF" // White icon on dark background + iconColor: "#FFFFFF" onClicked: { photoViewer.hide(); } @@ -82,7 +83,7 @@ Rectangle { Layout.alignment: Qt.AlignVCenter iconName: "trash" iconSize: Constants.iconSizeLarge - iconColor: "#FF4444" // Red delete icon + iconColor: "#FF4444" onClicked: { if (photo && typeof MediaLibraryManager !== 'undefined') { MediaLibraryManager.deletePhoto(photo.id); diff --git a/apps/maps/MapsApp.qml b/apps/maps/MapsApp.qml index b71146d75..368bc6125 100644 --- a/apps/maps/MapsApp.qml +++ b/apps/maps/MapsApp.qml @@ -14,9 +14,7 @@ MApp { property bool isSearching: false property bool mapLoaded: false property bool hasLocationPermission: false - // The Map object is created inside the content Loader, so store a reference here - // for access from non-content scopes (e.g. PositionSource). - property var mapObject: null + property Map mapObject: null function searchLocation(query) { if (query.length === 0) { @@ -70,7 +68,6 @@ MApp { appIcon: "assets/icon.svg" onAppLaunched: { loadTimer.start(); - // Check location permission if (typeof PermissionManager !== 'undefined') { if (PermissionManager.hasPermission(appId, "location")) { Logger.info("Maps", "Location permission already granted"); @@ -85,7 +82,6 @@ MApp { } } - // Listen for permission responses Connections { function onPermissionGranted(grantedAppId, permission) { if (grantedAppId === appId && permission === "location") { @@ -136,9 +132,6 @@ MApp { color: MColors.background Loader { - // Gestures are enabled by default in Qt 6 - // gesture.enabled: true removed - not a valid property - id: mapLoader anchors.fill: parent @@ -378,8 +371,8 @@ MApp { variant: "secondary" onClicked: { HapticService.light(); - if (mapLoader.item) - mapLoader.item.zoomLevel = Math.min(mapLoader.item.zoomLevel + 1, mapLoader.item.maximumZoomLevel); + if (mapsApp.mapObject) + mapsApp.mapObject.zoomLevel = Math.min(mapsApp.mapObject.zoomLevel + 1, mapsApp.mapObject.maximumZoomLevel); } } @@ -390,8 +383,8 @@ MApp { variant: "secondary" onClicked: { HapticService.light(); - if (mapLoader.item) - mapLoader.item.zoomLevel = Math.max(mapLoader.item.zoomLevel - 1, mapLoader.item.minimumZoomLevel); + if (mapsApp.mapObject) + mapsApp.mapObject.zoomLevel = Math.max(mapsApp.mapObject.zoomLevel - 1, mapsApp.mapObject.minimumZoomLevel); } } } @@ -408,9 +401,9 @@ MApp { variant: "primary" onClicked: { HapticService.medium(); - if (positionSource.position.valid && mapLoader.item) { - mapLoader.item.center = positionSource.position.coordinate; - mapLoader.item.zoomLevel = 15; + if (positionSource.position.valid && mapsApp.mapObject) { + mapsApp.mapObject.center = positionSource.position.coordinate; + mapsApp.mapObject.zoomLevel = 15; Logger.info("Maps", "Centered on current location"); } else { Logger.warn("Maps", "Position not available"); diff --git a/apps/messages/MessagesApp.qml b/apps/messages/MessagesApp.qml index 619651951..290944452 100644 --- a/apps/messages/MessagesApp.qml +++ b/apps/messages/MessagesApp.qml @@ -1,24 +1,62 @@ -import QtQuick -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Theme +import QtQuick +import QtQuick.Controls MApp { id: messagesApp - appId: "messages" - appName: "Messages" - appIcon: "assets/icon.svg" property var conversations: typeof SMSService !== 'undefined' ? SMSService.conversations : [] - property int selectedConversationId: -1 + function getConversation(id) { + if (!id) + return null; + + for (var i = 0; i < conversations.length; i++) { + if (conversations[i].id === id) + return conversations[i]; + } + Logger.warn("Messages", "Conversation not found: " + id); + return null; + } + + function refreshConversations() { + if (typeof SMSService !== 'undefined') + conversations = SMSService.conversations; + } + + function formatTimestamp(timestamp) { + var now = Date.now(); + var diff = now - timestamp; + if (diff < 1000 * 60 * 60) + return Math.floor(diff / (1000 * 60)) + "m"; + else if (diff < 1000 * 60 * 60 * 24) + return Math.floor(diff / (1000 * 60 * 60)) + "h"; + else + return Math.floor(diff / (1000 * 60 * 60 * 24)) + "d"; + } + + appId: "messages" + appName: "Messages" + appIcon: "assets/icon.svg" + Connections { - target: typeof SMSService !== 'undefined' ? SMSService : null function onMessageReceived(sender, text, timestamp) { Logger.info("Messages", "New message from: " + sender); } + + target: typeof SMSService !== 'undefined' ? SMSService : null + } + + Connections { + function onConversationsChanged() { + Logger.info("Messages", "Conversations updated"); + refreshConversations(); + } + + target: typeof SMSService !== 'undefined' ? SMSService : null } content: Rectangle { @@ -27,29 +65,24 @@ MApp { StackView { id: navigationStack - anchors.fill: parent - initialItem: conversationsListPage property var backConnection: null + anchors.fill: parent + initialItem: conversationsListPage onDepthChanged: { messagesApp.navigationDepth = depth - 1; } - Component.onCompleted: { messagesApp.navigationDepth = depth - 1; - backConnection = messagesApp.backPressed.connect(function () { - if (depth > 1) { + if (depth > 1) pop(); - } }); } - Component.onDestruction: { - if (backConnection) { + if (backConnection) messagesApp.backPressed.disconnect(backConnection); - } } pushEnter: Transition { @@ -70,10 +103,11 @@ MApp { duration: Constants.animationDurationNormal easing.type: Easing.OutCubic } + NumberAnimation { property: "opacity" - from: 1.0 - to: 0.0 + from: 1 + to: 0 duration: Constants.animationDurationNormal } } @@ -86,10 +120,11 @@ MApp { duration: Constants.animationDurationNormal easing.type: Easing.OutCubic } + NumberAnimation { property: "opacity" - from: 0.0 - to: 1.0 + from: 0 + to: 1 duration: Constants.animationDurationNormal } } @@ -107,17 +142,17 @@ MApp { Component { id: conversationsListPage + ConversationsListPage { onOpenConversation: function (conversationId) { selectedConversationId = conversationId; var conversation = getConversation(conversationId); - if (conversation) { + if (conversation) navigationStack.push(chatPage, { - conversation: conversation + "conversation": conversation }); - } else { + else Logger.warn("Messages", "Conversation not found: " + conversationId); - } } onNewMessage: function () { navigationStack.push(newConversationPage); @@ -127,6 +162,7 @@ MApp { Component { id: chatPage + ChatPage { onNavigateBack: { navigationStack.pop(); @@ -136,11 +172,11 @@ MApp { Component { id: newConversationPage + NewConversationPage { onConversationStarted: function (recipient, recipientName) { Logger.info("Messages", "Starting conversation with: " + recipient); var conversationId = typeof SMSService !== 'undefined' ? SMSService.generateConversationId(recipient) : "conv_" + recipient; - var conversation = { "id": conversationId, "contactName": recipientName, @@ -149,10 +185,9 @@ MApp { "timestamp": Date.now(), "unread": false }; - navigationStack.pop(); navigationStack.push(chatPage, { - conversation: conversation + "conversation": conversation }); } onCancelled: function () { @@ -161,45 +196,4 @@ MApp { } } } - - function getConversation(id) { - if (!id) - return null; - - for (var i = 0; i < conversations.length; i++) { - if (conversations[i].id === id) { - return conversations[i]; - } - } - - Logger.warn("Messages", "Conversation not found: " + id); - return null; - } - - function refreshConversations() { - if (typeof SMSService !== 'undefined') { - conversations = SMSService.conversations; - } - } - - Connections { - target: typeof SMSService !== 'undefined' ? SMSService : null - function onConversationsChanged() { - Logger.info("Messages", "Conversations updated"); - refreshConversations(); - } - } - - function formatTimestamp(timestamp) { - var now = Date.now(); - var diff = now - timestamp; - - if (diff < 1000 * 60 * 60) { - return Math.floor(diff / (1000 * 60)) + "m"; - } else if (diff < 1000 * 60 * 60 * 24) { - return Math.floor(diff / (1000 * 60 * 60)) + "h"; - } else { - return Math.floor(diff / (1000 * 60 * 60 * 24)) + "d"; - } - } } diff --git a/apps/messages/components/ConversationListItem.qml b/apps/messages/components/ConversationListItem.qml index d13044221..a301580f1 100644 --- a/apps/messages/components/ConversationListItem.qml +++ b/apps/messages/components/ConversationListItem.qml @@ -1,30 +1,46 @@ -import QtQuick import MarathonApp.Messages import MarathonOS.Shell -import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Containers +import MarathonUI.Core import MarathonUI.Feedback +import MarathonUI.Theme +import QtQuick Item { id: root property var conversation - property bool isUnread: conversation?.unreadCount > 0 + property bool isUnread: (conversation && conversation.unreadCount > 0) signal conversationClicked signal conversationDeleted - width: parent ? parent.width : 0 - height: 80 + function formatTimestamp(timestamp) { + if (!timestamp) + return ""; - Behavior on scale { - NumberAnimation { - duration: MMotion.fast - easing.bezierCurve: MMotion.easingStandardCurve + var now = Date.now(); + var diff = now - timestamp; + var date = new Date(timestamp); + var today = new Date(); + if (diff < 1000 * 60) { + return "Now"; + } else if (diff < 1000 * 60 * 60) { + var mins = Math.floor(diff / (1000 * 60)); + return mins + "m"; + } else if (date.toDateString() === today.toDateString()) { + return date.toLocaleTimeString(Qt.locale(), "h:mm AP"); + } else if (diff < 1000 * 60 * 60 * 24 * 7) { + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + return days[date.getDay()]; + } else { + return date.toLocaleDateString(Qt.locale(), "M/d/yy"); } } + width: parent ? parent.width : 0 + height: 80 + Rectangle { anchors.fill: parent anchors.margins: MSpacing.xs @@ -33,18 +49,6 @@ Item { border.width: isUnread ? 1 : 0 border.color: Qt.rgba(0.24, 0.82, 0.73, 0.3) - Behavior on color { - ColorAnimation { - duration: MMotion.fast - } - } - - Behavior on border.color { - ColorAnimation { - duration: MMotion.fast - } - } - Row { anchors.fill: parent anchors.margins: MSpacing.md @@ -52,6 +56,7 @@ Item { Rectangle { id: avatar + anchors.verticalCenter: parent.verticalCenter width: 48 height: 48 @@ -60,13 +65,13 @@ Item { MLabel { anchors.centerIn: parent - text: conversation?.contactName ? conversation.contactName.charAt(0).toUpperCase() : "?" + text: (conversation && conversation.contactName) ? conversation.contactName.charAt(0).toUpperCase() : "?" font.pixelSize: MTypography.sizeLarge font.weight: MTypography.weightBold } Rectangle { - visible: isUnread && conversation?.unreadCount > 0 + visible: isUnread && (conversation && conversation.unreadCount > 0) anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: -4 @@ -78,7 +83,7 @@ Item { Text { anchors.centerIn: parent - text: conversation?.unreadCount > 9 ? "9+" : (conversation?.unreadCount.toString() || "") + text: (conversation && conversation.unreadCount > 9) ? "9+" : ((conversation && conversation.unreadCount.toString()) || "") font.pixelSize: MTypography.sizeXSmall font.weight: MTypography.weightBold font.family: MTypography.fontFamily @@ -92,7 +97,7 @@ Item { spacing: MSpacing.xs MLabel { - text: conversation?.contactName || conversation?.contactNumber || "Unknown" + text: (conversation && conversation.contactName) || (conversation && conversation.contactNumber) || "Unknown" font.pixelSize: MTypography.sizeBody font.weight: isUnread ? MTypography.weightBold : MTypography.weightMedium elide: Text.ElideRight @@ -100,7 +105,7 @@ Item { } MLabel { - text: conversation?.lastMessage || "No messages yet" + text: (conversation && conversation.lastMessage) || "No messages yet" variant: isUnread ? "secondary" : "tertiary" font.pixelSize: MTypography.sizeSmall font.weight: isUnread ? MTypography.weightMedium : MTypography.weightRegular @@ -112,13 +117,26 @@ Item { MLabel { id: timestampLabel + anchors.verticalCenter: parent.verticalCenter - text: formatTimestamp(conversation?.lastTimestamp || Date.now()) + text: formatTimestamp((conversation && conversation.lastTimestamp) || Date.now()) variant: isUnread ? "secondary" : "tertiary" font.pixelSize: MTypography.sizeXSmall font.weight: isUnread ? MTypography.weightDemiBold : MTypography.weightRegular } } + + Behavior on color { + ColorAnimation { + duration: MMotion.fast + } + } + + Behavior on border.color { + ColorAnimation { + duration: MMotion.fast + } + } } MouseArea { @@ -128,37 +146,20 @@ Item { HapticService.light(); } onReleased: { - root.scale = 1.0; + root.scale = 1; } onCanceled: { - root.scale = 1.0; + root.scale = 1; } onClicked: { root.conversationClicked(); } } - function formatTimestamp(timestamp) { - if (!timestamp) - return ""; - - var now = Date.now(); - var diff = now - timestamp; - var date = new Date(timestamp); - var today = new Date(); - - if (diff < 1000 * 60) { - return "Now"; - } else if (diff < 1000 * 60 * 60) { - var mins = Math.floor(diff / (1000 * 60)); - return mins + "m"; - } else if (date.toDateString() === today.toDateString()) { - return date.toLocaleTimeString(Qt.locale(), "h:mm AP"); - } else if (diff < 1000 * 60 * 60 * 24 * 7) { - var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - return days[date.getDay()]; - } else { - return date.toLocaleDateString(Qt.locale(), "M/d/yy"); + Behavior on scale { + NumberAnimation { + duration: MMotion.fast + easing.bezierCurve: MMotion.easingStandardCurve } } } diff --git a/apps/messages/components/DateSeparator.qml b/apps/messages/components/DateSeparator.qml index f20df3c22..e1efe73cf 100644 --- a/apps/messages/components/DateSeparator.qml +++ b/apps/messages/components/DateSeparator.qml @@ -1,13 +1,35 @@ -import QtQuick import MarathonApp.Messages import MarathonUI.Core import MarathonUI.Theme +import QtQuick Item { id: root property date messageDate + function formatDate(date) { + if (!date) + return ""; + + var now = new Date(); + var msgDate = new Date(date); + if (msgDate.toDateString() === now.toDateString()) + return "Today"; + + var yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + if (msgDate.toDateString() === yesterday.toDateString()) + return "Yesterday"; + + var diffDays = Math.floor((now - msgDate) / (1000 * 60 * 60 * 24)); + if (diffDays < 7) { + var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + return days[msgDate.getDay()]; + } + return msgDate.toLocaleDateString(Qt.locale(), "MMMM d, yyyy"); + } + width: parent.width height: 40 @@ -37,30 +59,4 @@ Item { anchors.verticalCenter: parent.verticalCenter } } - - function formatDate(date) { - if (!date) - return ""; - - var now = new Date(); - var msgDate = new Date(date); - - if (msgDate.toDateString() === now.toDateString()) { - return "Today"; - } - - var yesterday = new Date(now); - yesterday.setDate(yesterday.getDate() - 1); - if (msgDate.toDateString() === yesterday.toDateString()) { - return "Yesterday"; - } - - var diffDays = Math.floor((now - msgDate) / (1000 * 60 * 60 * 24)); - if (diffDays < 7) { - var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - return days[msgDate.getDay()]; - } - - return msgDate.toLocaleDateString(Qt.locale(), "MMMM d, yyyy"); - } } diff --git a/apps/messages/components/MessageBubble.qml b/apps/messages/components/MessageBubble.qml index 11e9a61f8..05b681857 100644 --- a/apps/messages/components/MessageBubble.qml +++ b/apps/messages/components/MessageBubble.qml @@ -1,24 +1,49 @@ -import QtQuick import MarathonApp.Messages import MarathonOS.Shell +import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme -import MarathonUI.Containers +import QtQuick Item { id: root property var message - property bool isOutgoing: message?.isOutgoing || false + property bool isOutgoing: (message && message.isOutgoing) || false property bool showTimestamp: true property bool isFirstInGroup: false property bool isLastInGroup: false + function formatMessageTime(timestamp) { + if (!timestamp) + return ""; + + var date = new Date(timestamp); + return date.toLocaleTimeString(Qt.locale(), "h:mm AP"); + } + + function getStatusIcon() { + if (!message) + return "check"; + + if (message.isFailed) + return "x"; + + if (message.isRead) + return "check-check"; + + if (message.isDelivered) + return "check-check"; + + return "check"; + } + width: parent.width height: bubbleContainer.height + MSpacing.xs Column { id: bubbleContainer + anchors.left: isOutgoing ? undefined : parent.left anchors.right: isOutgoing ? parent.right : undefined anchors.leftMargin: MSpacing.md @@ -27,6 +52,7 @@ Item { Rectangle { id: bubble + width: Math.min(bubbleText.contentWidth + MSpacing.md * 2, root.width * 0.75) height: bubbleText.contentHeight + MSpacing.md * 2 radius: MRadius.lg @@ -36,9 +62,10 @@ Item { MLabel { id: bubbleText + anchors.fill: parent anchors.margins: MSpacing.md - text: message?.text || "" + text: (message && message.text) || "" variant: "primary" font.pixelSize: MTypography.sizeBody color: isOutgoing ? MColors.textPrimary : MColors.textPrimary @@ -64,32 +91,13 @@ Item { MLabel { id: timestampLabel + visible: showTimestamp - text: formatMessageTime(message?.timestamp) + text: formatMessageTime(message && message.timestamp) variant: "tertiary" font.pixelSize: MTypography.sizeXSmall anchors.left: isOutgoing ? undefined : parent.left anchors.right: isOutgoing ? parent.right : undefined } } - - function formatMessageTime(timestamp) { - if (!timestamp) - return ""; - var date = new Date(timestamp); - return date.toLocaleTimeString(Qt.locale(), "h:mm AP"); - } - - function getStatusIcon() { - if (!message) - return "check"; - - if (message.isFailed) - return "x"; - if (message.isRead) - return "check-check"; - if (message.isDelivered) - return "check-check"; - return "check"; - } } diff --git a/apps/messages/components/MessageInputBar.qml b/apps/messages/components/MessageInputBar.qml index 11ec5f43b..b8d8a2e00 100644 --- a/apps/messages/components/MessageInputBar.qml +++ b/apps/messages/components/MessageInputBar.qml @@ -1,10 +1,10 @@ -import QtQuick import MarathonApp.Messages -import QtQuick.Controls import MarathonOS.Shell +import MarathonUI.Controls import MarathonUI.Core import MarathonUI.Theme -import MarathonUI.Controls +import QtQuick +import QtQuick.Controls Rectangle { id: root @@ -30,6 +30,7 @@ Rectangle { Column { id: contentColumn + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -42,6 +43,7 @@ Rectangle { MIconButton { id: attachButton + anchors.verticalCenter: parent.verticalCenter iconName: "paperclip" iconSize: 20 @@ -54,10 +56,10 @@ Rectangle { MTextInput { id: messageInput + width: parent.width - attachButton.width - sendButton.width - parent.spacing * 2 anchors.verticalCenter: parent.verticalCenter placeholderText: "Type a message..." - Keys.onReturnPressed: { if (event.modifiers & Qt.ShiftModifier) { event.accepted = false; @@ -71,6 +73,7 @@ Rectangle { MIconButton { id: sendButton + anchors.verticalCenter: parent.verticalCenter iconName: "send" iconSize: 20 diff --git a/apps/messages/pages/ChatPage.qml b/apps/messages/pages/ChatPage.qml index 06f477a08..45638e407 100644 --- a/apps/messages/pages/ChatPage.qml +++ b/apps/messages/pages/ChatPage.qml @@ -1,16 +1,15 @@ -import QtQuick import MarathonApp.Messages -import QtQuick.Controls import MarathonApp.Messages import MarathonOS.Shell -import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Containers +import MarathonUI.Core import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Rectangle { id: chatPage - color: MColors.background property var conversation property var messages: [] @@ -18,20 +17,78 @@ Rectangle { signal navigateBack + function loadMessages() { + if (!conversation) + return; + + if (typeof SMSService !== 'undefined') + messages = SMSService.getMessages(conversation.id); + else + messages = []; + groupMessages(); + } + + function groupMessages() { + var groups = []; + var currentGroup = null; + var currentDate = null; + var lastSender = null; + var lastTime = 0; + var sortedMessages = messages.slice().reverse(); + for (var i = 0; i < sortedMessages.length; i++) { + var msg = sortedMessages[i]; + var msgDate = new Date(msg.timestamp); + var msgDateStr = msgDate.toDateString(); + if (msgDateStr !== currentDate) { + if (currentGroup) + groups.push(currentGroup); + + currentGroup = { + "date": msgDate, + "showDate": true, + "messages": [] + }; + currentDate = msgDateStr; + lastSender = null; + lastTime = 0; + } + var timeDiff = msg.timestamp - lastTime; + var isNewGroup = (msg.isOutgoing !== (lastSender === "me")) || (timeDiff > 5 * 60 * 1000); + if (isNewGroup && currentGroup.messages.length > 0) { + currentGroup.messages[currentGroup.messages.length - 1].isLast = true; + currentGroup.messages[currentGroup.messages.length - 1].showTime = true; + } + var msgCopy = Object.assign({}, msg); + msgCopy.isFirst = isNewGroup; + msgCopy.isLast = false; + msgCopy.showTime = false; + currentGroup.messages.push(msgCopy); + lastSender = msg.isOutgoing ? "me" : msg.sender; + lastTime = msg.timestamp; + } + if (currentGroup && currentGroup.messages.length > 0) { + currentGroup.messages[currentGroup.messages.length - 1].isLast = true; + currentGroup.messages[currentGroup.messages.length - 1].showTime = true; + groups.push(currentGroup); + } + groupedMessages = groups; + } + + function scrollToBottom() { + messagesList.positionViewAtBeginning(); + } + + color: MColors.background Component.onCompleted: { loadMessages(); - if (typeof SMSService !== 'undefined' && conversation) { + if (typeof SMSService !== 'undefined' && conversation) SMSService.markAsRead(conversation.id); - } } Connections { - target: typeof SMSService !== 'undefined' ? SMSService : null - function onMessageSent(recipient, timestamp) { - if (conversation) { + if (conversation) loadMessages(); - } } function onMessageReceived(sender, text, timestamp) { @@ -40,6 +97,8 @@ Rectangle { scrollToBottom(); } } + + target: typeof SMSService !== 'undefined' ? SMSService : null } Column { @@ -48,9 +107,9 @@ Rectangle { MActionBar { id: header + width: parent.width showBack: true - onBackClicked: { HapticService.light(); chatPage.navigateBack(); @@ -71,7 +130,7 @@ Rectangle { MLabel { anchors.centerIn: parent - text: conversation?.contactName ? conversation.contactName.charAt(0).toUpperCase() : "?" + text: (conversation && conversation.contactName) ? conversation.contactName.charAt(0).toUpperCase() : "?" color: MColors.textInverse font.pixelSize: MTypography.sizeBody font.weight: MTypography.weightBold @@ -83,14 +142,14 @@ Rectangle { spacing: 2 MLabel { - text: conversation?.contactName || conversation?.contactNumber || "" + text: (conversation && conversation.contactName) || (conversation && conversation.contactNumber) || "" variant: "primary" font.pixelSize: MTypography.sizeBody font.weight: MTypography.weightBold } MLabel { - text: conversation?.contactNumber || "" + text: (conversation && conversation.contactNumber) || "" variant: "tertiary" font.pixelSize: MTypography.sizeXSmall } @@ -100,6 +159,11 @@ Rectangle { ListView { id: messagesList + + function scrollToBottom() { + messagesList.positionViewAtBeginning(); + } + width: parent.width height: parent.height - header.height - messageInputBar.height clip: true @@ -107,9 +171,17 @@ Rectangle { spacing: 0 topMargin: MSpacing.md bottomMargin: MSpacing.md - model: groupedMessages + MEmptyState { + visible: messagesList.count === 0 + anchors.centerIn: parent + width: parent.width - MSpacing.xl * 2 + iconName: "message-circle" + title: "No messages yet" + message: "Send a message to start the conversation" + } + delegate: Column { width: messagesList.width spacing: 0 @@ -132,110 +204,23 @@ Rectangle { } } } - - MEmptyState { - visible: messagesList.count === 0 - anchors.centerIn: parent - width: parent.width - MSpacing.xl * 2 - iconName: "message-circle" - title: "No messages yet" - message: "Send a message to start the conversation" - } - - function scrollToBottom() { - messagesList.positionViewAtBeginning(); - } } MessageInputBar { id: messageInputBar - width: parent.width + width: parent.width onSendMessage: text => { if (text.trim().length > 0 && conversation) { Logger.info("Messages", "Sending message to: " + conversation.contactName); var recipientNumber = conversation.contactNumber || conversation.id.replace("conv_", ""); - if (typeof SMSService !== 'undefined') { + if (typeof SMSService !== 'undefined') SMSService.sendMessage(recipientNumber, text.trim()); - } } } - onAttachPressed: { Logger.info("Messages", "Attach pressed"); } } } - - function loadMessages() { - if (!conversation) - return; - if (typeof SMSService !== 'undefined') { - messages = SMSService.getMessages(conversation.id); - } else { - messages = []; - } - - groupMessages(); - } - - function groupMessages() { - var groups = []; - var currentGroup = null; - var currentDate = null; - var lastSender = null; - var lastTime = 0; - - var sortedMessages = messages.slice().reverse(); - - for (var i = 0; i < sortedMessages.length; i++) { - var msg = sortedMessages[i]; - var msgDate = new Date(msg.timestamp); - var msgDateStr = msgDate.toDateString(); - - if (msgDateStr !== currentDate) { - if (currentGroup) { - groups.push(currentGroup); - } - - currentGroup = { - date: msgDate, - showDate: true, - messages: [] - }; - currentDate = msgDateStr; - lastSender = null; - lastTime = 0; - } - - var timeDiff = msg.timestamp - lastTime; - var isNewGroup = (msg.isOutgoing !== (lastSender === "me")) || (timeDiff > 5 * 60 * 1000); - - if (isNewGroup && currentGroup.messages.length > 0) { - currentGroup.messages[currentGroup.messages.length - 1].isLast = true; - currentGroup.messages[currentGroup.messages.length - 1].showTime = true; - } - - var msgCopy = Object.assign({}, msg); - msgCopy.isFirst = isNewGroup; - msgCopy.isLast = false; - msgCopy.showTime = false; - - currentGroup.messages.push(msgCopy); - lastSender = msg.isOutgoing ? "me" : msg.sender; - lastTime = msg.timestamp; - } - - if (currentGroup && currentGroup.messages.length > 0) { - currentGroup.messages[currentGroup.messages.length - 1].isLast = true; - currentGroup.messages[currentGroup.messages.length - 1].showTime = true; - groups.push(currentGroup); - } - - groupedMessages = groups; - } - - function scrollToBottom() { - messagesList.positionViewAtBeginning(); - } } diff --git a/apps/messages/pages/ConversationsListPage.qml b/apps/messages/pages/ConversationsListPage.qml index 701bec949..3e400051c 100644 --- a/apps/messages/pages/ConversationsListPage.qml +++ b/apps/messages/pages/ConversationsListPage.qml @@ -1,28 +1,45 @@ -import QtQuick import MarathonApp.Messages -import QtQuick.Controls import MarathonApp.Messages import MarathonOS.Shell -import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Controls +import MarathonUI.Core import MarathonUI.Feedback import MarathonUI.Modals import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Page { id: conversationsPage - signal openConversation(string conversationId) - signal newMessage - property var filteredConversations: messagesApp.conversations property bool showUnreadOnly: false property string searchQuery: "" - background: Rectangle { - color: MColors.background + signal openConversation(string conversationId) + signal newMessage + + function updateFilter() { + var conversations = messagesApp.conversations; + if (showUnreadOnly) + conversations = conversations.filter(function (c) { + return c.unreadCount > 0; + }); + + if (searchQuery.length > 0) { + var query = searchQuery.toLowerCase(); + conversations = conversations.filter(function (c) { + return (c.contactName && c.contactName.toLowerCase().includes(query)) || (c.contactNumber && c.contactNumber.includes(query)) || (c.lastMessage && c.lastMessage.toLowerCase().includes(query)); + }); + } + filteredConversations = conversations; + } + + function deleteConversation(conversationId) { + if (typeof SMSService !== 'undefined') + SMSService.deleteConversation(conversationId); } Component.onCompleted: { @@ -35,9 +52,9 @@ Page { MActionBar { id: actionBar + showBack: false width: parent.width - onSignatureClicked: { HapticService.medium(); newMessage(); @@ -53,6 +70,7 @@ Page { MLabel { id: titleText + text: "Messages" variant: "primary" font.pixelSize: MTypography.sizeLarge @@ -63,29 +81,23 @@ Page { Item { id: searchBar + width: parent.width height: searchInput.visible ? 60 : 0 visible: height > 0 clip: true - Behavior on height { - NumberAnimation { - duration: MMotion.fast - easing.bezierCurve: MMotion.easingStandardCurve - } - } - Rectangle { anchors.fill: parent color: MColors.surface MTextInput { id: searchInput + anchors.fill: parent anchors.margins: MSpacing.md placeholderText: "Search conversations..." visible: false - onTextChanged: { searchQuery = text; searchTimer.restart(); @@ -93,15 +105,24 @@ Page { Timer { id: searchTimer + interval: 300 onTriggered: updateFilter() } } } + + Behavior on height { + NumberAnimation { + duration: MMotion.fast + easing.bezierCurve: MMotion.easingStandardCurve + } + } } Item { id: filterRow + width: parent.width height: 52 @@ -151,13 +172,22 @@ Page { ListView { id: conversationsList + anchors.fill: parent topMargin: MSpacing.md bottomMargin: MSpacing.md spacing: MSpacing.sm - model: filteredConversations + MEmptyState { + visible: conversationsList.count === 0 + anchors.centerIn: parent + width: parent.width - MSpacing.xl * 2 + iconName: "message-circle" + title: searchQuery.length > 0 ? "No conversations found" : (showUnreadOnly ? "No unread messages" : "No conversations yet") + message: searchQuery.length > 0 ? "Try a different search term" : "Start a new conversation to begin messaging" + } + delegate: Item { width: conversationsList.width height: 88 @@ -169,6 +199,7 @@ Page { Rectangle { id: deleteButton + anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom @@ -196,8 +227,15 @@ Page { ConversationListItem { id: conversationItem + anchors.fill: parent conversation: modelData + onConversationClicked: { + if (conversationItem.x === 0) + openConversation(modelData.id); + else + conversationItem.x = 0; + } Behavior on x { NumberAnimation { @@ -205,30 +243,21 @@ Page { easing.bezierCurve: MMotion.easingStandardCurve } } - - onConversationClicked: { - if (conversationItem.x === 0) { - openConversation(modelData.id); - } else { - conversationItem.x = 0; - } - } } MouseArea { id: swipeArea - anchors.fill: parent - z: -1 property real startX: 0 property bool longPressActive: false + anchors.fill: parent + z: -1 onPressed: mouse => { startX = mouse.x; longPressActive = false; HapticService.light(); } - onPressAndHold: { longPressActive = true; HapticService.medium(); @@ -236,41 +265,27 @@ Page { contextMenu.isUnread = modelData.unreadCount > 0; contextMenu.visible = true; } - onPositionChanged: mouse => { if (pressed && !longPressActive) { var delta = mouse.x - startX; - if (delta < 0) { + if (delta < 0) conversationItem.x = Math.max(delta, -80); - } } } - onReleased: mouse => { if (!longPressActive) { - if (conversationItem.x < -40) { + if (conversationItem.x < -40) conversationItem.x = -80; - } else { + else conversationItem.x = 0; - } } } - onCanceled: { conversationItem.x = 0; } } } } - - MEmptyState { - visible: conversationsList.count === 0 - anchors.centerIn: parent - width: parent.width - MSpacing.xl * 2 - iconName: "message-circle" - title: searchQuery.length > 0 ? "No conversations found" : (showUnreadOnly ? "No unread messages" : "No conversations yet") - message: searchQuery.length > 0 ? "Try a different search term" : "Start a new conversation to begin messaging" - } } } } @@ -291,11 +306,11 @@ Page { MSheet { id: contextMenu - visible: false property string conversationId: "" property bool isUnread: false + visible: false title: "Conversation Options" Column { @@ -308,9 +323,8 @@ Page { showChevron: false onSettingClicked: { if (typeof SMSService !== 'undefined') { - if (contextMenu.isUnread) { + if (contextMenu.isUnread) SMSService.markAsRead(contextMenu.conversationId); - } } HapticService.light(); contextMenu.visible = false; @@ -341,28 +355,7 @@ Page { } } - function updateFilter() { - var conversations = messagesApp.conversations; - - if (showUnreadOnly) { - conversations = conversations.filter(function (c) { - return c.unreadCount > 0; - }); - } - - if (searchQuery.length > 0) { - var query = searchQuery.toLowerCase(); - conversations = conversations.filter(function (c) { - return (c.contactName && c.contactName.toLowerCase().includes(query)) || (c.contactNumber && c.contactNumber.includes(query)) || (c.lastMessage && c.lastMessage.toLowerCase().includes(query)); - }); - } - - filteredConversations = conversations; - } - - function deleteConversation(conversationId) { - if (typeof SMSService !== 'undefined') { - SMSService.deleteConversation(conversationId); - } + background: Rectangle { + color: MColors.background } } diff --git a/apps/messages/pages/NewConversationPage.qml b/apps/messages/pages/NewConversationPage.qml index e8e8172c7..8a7681e5b 100644 --- a/apps/messages/pages/NewConversationPage.qml +++ b/apps/messages/pages/NewConversationPage.qml @@ -1,25 +1,45 @@ -import QtQuick import MarathonApp.Messages -import QtQuick.Controls import MarathonApp.Messages import MarathonOS.Shell -import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Containers +import MarathonUI.Core import MarathonUI.Feedback import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Rectangle { id: newConversationPage - color: MColors.background - - signal conversationStarted(string recipient, string recipientName) - signal cancelled property string selectedContact: "" property string selectedContactName: "" property bool isValidNumber: validatePhoneNumber(recipientInput.text) + signal conversationStarted(string recipient, string recipientName) + signal cancelled + + function startConversation() { + var recipient = selectedContact.length > 0 ? selectedContact : recipientInput.text; + var name = selectedContactName.length > 0 ? selectedContactName : recipient; + if (recipient.length > 0 && isValidNumber) { + Logger.info("NewConversation", "Starting conversation with: " + recipient); + conversationStarted(recipient, name); + } else { + Logger.warn("NewConversation", "Invalid phone number: " + recipient); + } + } + + function validatePhoneNumber(number) { + if (!number || number.length === 0) + return false; + + var cleaned = number.replace(/\D/g, ''); + return cleaned.length >= 10; + } + + color: MColors.background + Column { anchors.fill: parent spacing: 0 @@ -27,7 +47,6 @@ Rectangle { MActionBar { width: parent.width showBack: true - onBackClicked: { HapticService.light(); cancelled(); @@ -43,6 +62,7 @@ Rectangle { MLabel { id: titleText + text: "New Message" variant: "primary" font.pixelSize: MTypography.sizeLarge @@ -83,16 +103,15 @@ Rectangle { MTextInput { id: recipientInput + anchors.verticalCenter: parent.verticalCenter width: parent.width - parent.children[0].width - parent.children[2].width - parent.spacing * 2 placeholderText: "Phone number or contact name" text: selectedContactName || selectedContact enabled: selectedContact.length === 0 - onTextChanged: { - if (selectedContact.length === 0) { + if (selectedContact.length === 0) searchTimer.restart(); - } } } @@ -112,12 +131,12 @@ Rectangle { Timer { id: searchTimer + interval: 300 repeat: false onTriggered: { - if (recipientInput.text.length > 0 && typeof ContactsManager !== 'undefined') { + if (recipientInput.text.length > 0 && typeof ContactsManager !== 'undefined') contactsList.model = ContactsManager.searchContacts(recipientInput.text); - } } } @@ -132,12 +151,12 @@ Rectangle { ListView { id: contactsList + width: parent.width height: parent.height - parent.children[0].height clip: true spacing: MSpacing.xs topMargin: MSpacing.sm - model: typeof ContactsManager !== 'undefined' ? ContactsManager.contacts : [] delegate: Item { @@ -150,6 +169,26 @@ Rectangle { elevation: 0 radius: MRadius.lg + MouseArea { + anchors.fill: parent + onPressed: { + parent.scale = 0.98; + HapticService.light(); + } + onReleased: { + parent.scale = 1; + } + onCanceled: { + parent.scale = 1; + } + onClicked: { + selectedContact = modelData.phone; + selectedContactName = modelData.name; + recipientInput.text = modelData.name; + Logger.info("NewConversation", "Selected contact: " + modelData.name); + } + } + content: Row { anchors.fill: parent anchors.margins: MSpacing.md @@ -193,26 +232,6 @@ Rectangle { } } - MouseArea { - anchors.fill: parent - onPressed: { - parent.scale = 0.98; - HapticService.light(); - } - onReleased: { - parent.scale = 1.0; - } - onCanceled: { - parent.scale = 1.0; - } - onClicked: { - selectedContact = modelData.phone; - selectedContactName = modelData.name; - recipientInput.text = modelData.name; - Logger.info("NewConversation", "Selected contact: " + modelData.name); - } - } - Behavior on scale { NumberAnimation { duration: MMotion.fast @@ -223,24 +242,4 @@ Rectangle { } } } - - function startConversation() { - var recipient = selectedContact.length > 0 ? selectedContact : recipientInput.text; - var name = selectedContactName.length > 0 ? selectedContactName : recipient; - - if (recipient.length > 0 && isValidNumber) { - Logger.info("NewConversation", "Starting conversation with: " + recipient); - conversationStarted(recipient, name); - } else { - Logger.warn("NewConversation", "Invalid phone number: " + recipient); - } - } - - function validatePhoneNumber(number) { - if (!number || number.length === 0) - return false; - - var cleaned = number.replace(/\D/g, ''); - return cleaned.length >= 10; - } } diff --git a/apps/music/MusicApp.qml b/apps/music/MusicApp.qml index 6c23c7eed..6337faf78 100644 --- a/apps/music/MusicApp.qml +++ b/apps/music/MusicApp.qml @@ -1,92 +1,26 @@ -import QtQuick -import QtQuick.Layouts -import QtMultimedia import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Controls import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Navigation +import MarathonUI.Theme +import QtMultimedia +import QtQuick +import QtQuick.Layouts MApp { id: musicApp - appId: "music" - appName: "Music" - appIcon: "assets/icon.svg" property var currentTrack: null property bool isPlaying: audioPlayer.playbackState === MediaPlayer.PlayingState property bool shuffle: false property string repeatMode: "off" - property var playlist // No initial binding - set by library scan - - Component.onCompleted: { - // Initialize with empty playlist first (instant load) - playlist = []; - - // Defer heavy operations to avoid blocking app launch - Qt.callLater(function () { - if (typeof MusicLibraryManager !== 'undefined') { - // Get cached tracks first (fast) - playlist = MusicLibraryManager.getAllTracks(); - if (playlist.length > 0) { - currentTrack = playlist[0]; - } - // Then scan in background (slow, non-blocking) - MusicLibraryManager.scanLibrary(); - } - }); - } - - Connections { - target: typeof MusicLibraryManager !== 'undefined' ? MusicLibraryManager : null - function onScanComplete(trackCount) { - Logger.info("Music", "Library scan complete: " + trackCount + " tracks"); - playlist = MusicLibraryManager.getAllTracks(); - if (playlist.length > 0 && !currentTrack) { - currentTrack = playlist[0]; - } - } - } - - MediaPlayer { - id: audioPlayer - audioOutput: AudioOutput { - id: audioOutput - } - - onPositionChanged: { - if (currentTrack && currentTrack.duration) { - var newPos = position / 1000; - if (!isNaN(newPos) && isFinite(newPos)) { - currentTrack.position = newPos; - } - } - } - - onDurationChanged: { - if (currentTrack && duration > 0) { - var newDur = duration / 1000; - if (!isNaN(newDur) && isFinite(newDur)) { - currentTrack.duration = newDur; - } - } - } - - onPlaybackStateChanged: { - if (playbackState === MediaPlayer.StoppedState && currentTrack) { - playNext(); - } - } - - onErrorOccurred: function (error, errorString) { - Logger.error("Music", "Playback error: " + errorString); - } - } + property var playlist function playTrack(track) { if (!track) return; + currentTrack = track; currentTrack.position = 0; audioPlayer.source = track.path; @@ -97,6 +31,7 @@ MApp { function playNext() { if (playlist.length === 0) return; + var currentIndex = -1; for (var i = 0; i < playlist.length; i++) { if (playlist[i].id === currentTrack.id) { @@ -104,32 +39,28 @@ MApp { break; } } - var nextIndex; if (shuffle) { - // True random shuffle - exclude current track do { nextIndex = Math.floor(Math.random() * playlist.length); } while (nextIndex === currentIndex && playlist.length > 1) } else { nextIndex = (currentIndex + 1) % playlist.length; } - if (repeatMode === "off" && nextIndex <= currentIndex && !shuffle) { audioPlayer.stop(); return; } - - if (repeatMode === "single") { + if (repeatMode === "single") playTrack(currentTrack); - } else { + else playTrack(playlist[nextIndex]); - } } function playPrevious() { if (playlist.length === 0) return; + var currentIndex = -1; for (var i = 0; i < playlist.length; i++) { if (playlist[i].id === currentTrack.id) { @@ -137,7 +68,6 @@ MApp { break; } } - var prevIndex = (currentIndex - 1 + playlist.length) % playlist.length; playTrack(playlist[prevIndex]); } @@ -148,16 +78,73 @@ MApp { return mins + ":" + (secs < 10 ? "0" : "") + secs; } + appId: "music" + appName: "Music" + appIcon: "assets/icon.svg" + Component.onCompleted: { + playlist = []; + Qt.callLater(function () { + if (typeof MusicLibraryManager !== 'undefined') { + playlist = MusicLibraryManager.getAllTracks(); + if (playlist.length > 0) + currentTrack = playlist[0]; + + MusicLibraryManager.scanLibrary(); + } + }); + } + + Connections { + function onScanComplete(trackCount) { + Logger.info("Music", "Library scan complete: " + trackCount + " tracks"); + playlist = MusicLibraryManager.getAllTracks(); + if (playlist.length > 0 && !currentTrack) + currentTrack = playlist[0]; + } + + target: typeof MusicLibraryManager !== 'undefined' ? MusicLibraryManager : null + } + + MediaPlayer { + id: audioPlayer + + onPositionChanged: { + if (currentTrack && currentTrack.duration) { + var newPos = position / 1000; + if (!isNaN(newPos) && isFinite(newPos)) + currentTrack.position = newPos; + } + } + onDurationChanged: { + if (currentTrack && duration > 0) { + var newDur = duration / 1000; + if (!isNaN(newDur) && isFinite(newDur)) + currentTrack.duration = newDur; + } + } + onPlaybackStateChanged: { + if (playbackState === MediaPlayer.StoppedState && currentTrack) + playNext(); + } + onErrorOccurred: function (error, errorString) { + Logger.error("Music", "Playback error: " + errorString); + } + + audioOutput: AudioOutput { + id: audioOutput + } + } + content: Rectangle { anchors.fill: parent color: MColors.background Column { + property int currentView: 0 + anchors.fill: parent spacing: 0 - property int currentView: 0 - StackLayout { width: parent.width height: parent.height - tabBar.height @@ -169,7 +156,7 @@ MApp { Column { anchors.fill: parent anchors.margins: MSpacing.xl - anchors.bottomMargin: Constants.navBarHeight + MSpacing.xl // Account for system nav bar + anchors.bottomMargin: Constants.navBarHeight + MSpacing.xl spacing: MSpacing.lg Item { @@ -253,9 +240,8 @@ MApp { to: (currentTrack && currentTrack.duration) ? currentTrack.duration : 100 value: (currentTrack && currentTrack.position) ? currentTrack.position : 0 onMoved: { - if (currentTrack && currentTrack.duration) { + if (currentTrack && currentTrack.duration) audioPlayer.position = value * 1000; - } } } @@ -322,9 +308,9 @@ MApp { if (isPlaying) { audioPlayer.pause(); } else { - if (currentTrack && audioPlayer.playbackState === MediaPlayer.StoppedState) { + if (currentTrack && audioPlayer.playbackState === MediaPlayer.StoppedState) audioPlayer.source = currentTrack.path; - } + audioPlayer.play(); } } @@ -348,13 +334,12 @@ MApp { variant: repeatMode !== "off" ? "primary" : "secondary" onClicked: { HapticService.light(); - if (repeatMode === "off") { + if (repeatMode === "off") repeatMode = "all"; - } else if (repeatMode === "all") { + else if (repeatMode === "all") repeatMode = "one"; - } else { + else repeatMode = "off"; - } } } } @@ -366,9 +351,17 @@ MApp { Layout.fillHeight: true clip: true topMargin: MSpacing.md - model: playlist + MEmptyState { + anchors.fill: parent + visible: playlist.length === 0 + iconName: "music-2" + iconSize: 96 + title: "No Music Yet" + message: "Your music library is empty. Add some music files to get started!" + } + delegate: Item { width: ListView.view.width height: Constants.touchTargetLarge + MSpacing.sm @@ -379,6 +372,10 @@ MApp { anchors.topMargin: 0 interactive: true elevation: index === 0 ? 2 : 1 + onClicked: { + HapticService.light(); + playTrack(modelData); + } Row { anchors.fill: parent @@ -446,41 +443,26 @@ MApp { color: index === 0 ? MColors.marathonTeal : MColors.textTertiary } } - - onClicked: { - HapticService.light(); - playTrack(modelData); - } } } - - MEmptyState { - anchors.fill: parent - visible: playlist.length === 0 - iconName: "music-2" - iconSize: 96 - title: "No Music Yet" - message: "Your music library is empty. Add some music files to get started!" - } } } MTabBar { id: tabBar + width: parent.width activeTab: parent.currentView - tabs: [ { - label: "Now Playing", - icon: "disc" + "label": "Now Playing", + "icon": "disc" }, { - label: "Library", - icon: "library" + "label": "Library", + "icon": "library" } ] - onTabSelected: index => { HapticService.light(); tabBar.parent.currentView = index; diff --git a/apps/notes/NotesApp.qml b/apps/notes/NotesApp.qml index 141a9c8dc..4efdac2d9 100644 --- a/apps/notes/NotesApp.qml +++ b/apps/notes/NotesApp.qml @@ -1,37 +1,29 @@ -import QtQuick -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Theme +import QtQuick +import QtQuick.Controls MApp { id: notesApp - appId: "notes" - appName: "Notes" - appIcon: "assets/icon.svg" property var notes: [] property int nextId: 1 - property string sortMode: "newest" // "newest", "oldest", "alphabetical" - - Component.onCompleted: { - loadNotes(); - } + property string sortMode: "newest" function sortNotes() { - if (sortMode === "newest") { + if (sortMode === "newest") notes.sort(function (a, b) { return b.timestamp - a.timestamp; }); - } else if (sortMode === "oldest") { + else if (sortMode === "oldest") notes.sort(function (a, b) { return a.timestamp - b.timestamp; }); - } else if (sortMode === "alphabetical") { + else if (sortMode === "alphabetical") notes.sort(function (a, b) { return a.title.toLowerCase().localeCompare(b.title.toLowerCase()); }); - } notesChanged(); } @@ -39,9 +31,11 @@ MApp { var savedNotes = SettingsManagerCpp.get("notes/data", "[]"); try { notes = JSON.parse(savedNotes); - if (notes.length > 0) { - nextId = Math.max(...notes.map(n => n.id)) + 1; - } + if (notes.length > 0) + nextId = Math.max(notes.map(n => { + return n.id; + })) + 1; + sortNotes(); } catch (e) { Logger.error("NotesApp", "Failed to load notes: " + e); @@ -56,10 +50,10 @@ MApp { function createNote(title, content) { var note = { - id: nextId++, - title: title || "Untitled", - content: content || "", - timestamp: Date.now() + "id": nextId++, + "title": title || "Untitled", + "content": content || "", + "timestamp": Date.now() }; notes.push(note); notesChanged(); @@ -95,17 +89,15 @@ MApp { function getNote(id) { for (var i = 0; i < notes.length; i++) { - if (notes[i].id === id) { + if (notes[i].id === id) return notes[i]; - } } return null; } function searchNotes(query) { - if (!query || query.length === 0) { + if (!query || query.length === 0) return notes; - } var lowerQuery = query.toLowerCase(); return notes.filter(function (note) { @@ -113,35 +105,37 @@ MApp { }); } + appId: "notes" + appName: "Notes" + appIcon: "assets/icon.svg" + Component.onCompleted: { + loadNotes(); + } + content: Rectangle { anchors.fill: parent color: MColors.background StackView { id: navigationStack - anchors.fill: parent - initialItem: notesListPage property var backConnection: null + anchors.fill: parent + initialItem: notesListPage onDepthChanged: { notesApp.navigationDepth = depth - 1; } - Component.onCompleted: { notesApp.navigationDepth = depth - 1; - backConnection = notesApp.backPressed.connect(function () { - if (depth > 1) { + if (depth > 1) pop(); - } }); } - Component.onDestruction: { - if (backConnection) { + if (backConnection) notesApp.backPressed.disconnect(backConnection); - } } pushEnter: Transition { @@ -162,10 +156,11 @@ MApp { duration: Constants.animationDurationNormal easing.type: Easing.OutCubic } + NumberAnimation { property: "opacity" - from: 1.0 - to: 0.0 + from: 1 + to: 0 duration: Constants.animationDurationNormal } } @@ -178,10 +173,11 @@ MApp { duration: Constants.animationDurationNormal easing.type: Easing.OutCubic } + NumberAnimation { property: "opacity" - from: 0.0 - to: 1.0 + from: 0 + to: 1 duration: Constants.animationDurationNormal } } @@ -199,39 +195,37 @@ MApp { Component { id: notesListPage + NotesListPage { onCreateNewNote: { navigationStack.push(noteEditorPage, { - isNewNote: true + "isNewNote": true }); } - onOpenNote: function (noteId) { var note = notesApp.getNote(noteId); - if (note) { + if (note) navigationStack.push(noteEditorPage, { - isNewNote: false, - noteId: noteId, - initialTitle: note.title, - initialContent: note.content + "isNewNote": false, + "noteId": noteId, + "initialTitle": note.title, + "initialContent": note.content }); - } } } } Component { id: noteEditorPage + NoteEditorPage { onSaveNote: function (title, content) { - if (isNewNote) { + if (isNewNote) notesApp.createNote(title, content); - } else { + else notesApp.updateNote(noteId, title, content); - } navigationStack.pop(); } - onDeleteNote: function (id) { notesApp.deleteNote(id); navigationStack.pop(); diff --git a/apps/notes/components/NoteItem.qml b/apps/notes/components/NoteItem.qml index 9b8ab5771..c55d91ed4 100644 --- a/apps/notes/components/NoteItem.qml +++ b/apps/notes/components/NoteItem.qml @@ -1,11 +1,10 @@ -import QtQuick import MarathonApp.Notes import MarathonOS.Shell import MarathonUI.Theme +import QtQuick Item { id: noteItem - height: Constants.touchTargetLarge + MSpacing.lg property int noteId: -1 property string noteTitle: "" @@ -18,23 +17,24 @@ Item { var date = new Date(timestamp); var now = new Date(); var diff = now - date; - if (diff < 60000) { return "Just now"; - } else if (diff < 3600000) { + } else if (diff < 3.6e+06) { var mins = Math.floor(diff / 60000); return mins + (mins === 1 ? " min ago" : " mins ago"); - } else if (diff < 86400000) { - var hours = Math.floor(diff / 3600000); + } else if (diff < 8.64e+07) { + var hours = Math.floor(diff / 3.6e+06); return hours + (hours === 1 ? " hour ago" : " hours ago"); - } else if (diff < 604800000) { - var days = Math.floor(diff / 86400000); + } else if (diff < 6.048e+08) { + var days = Math.floor(diff / 8.64e+07); return days + (days === 1 ? " day ago" : " days ago"); } else { return Qt.formatDate(date, "MMM d, yyyy"); } } + height: Constants.touchTargetLarge + MSpacing.lg + Rectangle { anchors.fill: parent anchors.leftMargin: MSpacing.lg @@ -73,6 +73,7 @@ Item { Text { id: timestampText + text: formatTimestamp(noteTimestamp) color: MColors.textSecondary font.pixelSize: MTypography.sizeSmall @@ -103,6 +104,7 @@ Item { MouseArea { id: mouseArea + anchors.fill: parent onClicked: { noteItem.clicked(); diff --git a/apps/notes/pages/NoteEditorPage.qml b/apps/notes/pages/NoteEditorPage.qml index 0d4a81c7b..9c473b5e4 100644 --- a/apps/notes/pages/NoteEditorPage.qml +++ b/apps/notes/pages/NoteEditorPage.qml @@ -1,10 +1,10 @@ -import QtQuick import MarathonApp.Notes -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Modals import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Page { id: editorPage @@ -17,16 +17,11 @@ Page { signal saveNote(string title, string content) signal deleteNote(int noteId) - background: Rectangle { - color: MColors.background - } - Component.onCompleted: { titleInput.text = initialTitle; contentInput.text = initialContent; - if (isNewNote) { + if (isNewNote) titleInput.forceActiveFocus(); - } } Column { @@ -63,6 +58,7 @@ Page { MButton { id: deleteBtn + anchors.verticalCenter: parent.verticalCenter text: !isNewNote ? "Delete" : "" variant: "danger" @@ -104,6 +100,7 @@ Page { Column { id: editorContent + width: parent.width padding: MSpacing.lg spacing: MSpacing.md @@ -119,6 +116,7 @@ Page { TextInput { id: titleInput + anchors.fill: parent anchors.margins: MSpacing.md color: MColors.text @@ -140,6 +138,7 @@ Page { MTextArea { id: contentInput + width: parent.width - parent.padding * 2 height: Math.max(Constants.screenHeight * 0.4, 300) placeholderText: "Start typing..." @@ -151,10 +150,6 @@ Page { MConfirmDialog { id: deleteDialog - title: "Delete Note?" - message: "This note will be permanently deleted." - confirmText: "Delete" - cancelText: "Cancel" function open() { show(); @@ -164,8 +159,16 @@ Page { hide(); } + title: "Delete Note?" + message: "This note will be permanently deleted." + confirmText: "Delete" + cancelText: "Cancel" onConfirmed: { editorPage.deleteNote(noteId); } } + + background: Rectangle { + color: MColors.background + } } diff --git a/apps/notes/pages/NotesListPage.qml b/apps/notes/pages/NotesListPage.qml index 13c65a5b5..4247940bc 100644 --- a/apps/notes/pages/NotesListPage.qml +++ b/apps/notes/pages/NotesListPage.qml @@ -1,10 +1,10 @@ -import QtQuick import MarathonApp.Notes -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Page { id: listPage @@ -12,17 +12,35 @@ Page { signal createNewNote signal openNote(int noteId) - background: Rectangle { - color: MColors.background + function formatTimestamp(timestamp) { + var date = new Date(timestamp); + var now = new Date(); + var diff = now - date; + if (diff < 60000) { + return "Just now"; + } else if (diff < 3.6e+06) { + var mins = Math.floor(diff / 60000); + return mins + "m ago"; + } else if (diff < 8.64e+07) { + var hours = Math.floor(diff / 3.6e+06); + return hours + "h ago"; + } else if (diff < 6.048e+08) { + var days = Math.floor(diff / 8.64e+07); + return days + "d ago"; + } else { + return Qt.formatDate(date, "MMM d"); + } } MScrollView { id: scrollView + anchors.fill: parent contentHeight: notesContent.height + 40 Column { id: notesContent + width: parent.width spacing: MSpacing.xl leftPadding: 24 @@ -52,16 +70,16 @@ Page { Repeater { model: [ { - label: "Newest", - value: "newest" + "label": "Newest", + "value": "newest" }, { - label: "Oldest", - value: "oldest" + "label": "Oldest", + "value": "oldest" }, { - label: "A-Z", - value: "alphabetical" + "label": "A-Z", + "value": "alphabetical" } ] @@ -113,6 +131,7 @@ Page { Rectangle { id: deleteButton + anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom @@ -139,6 +158,7 @@ Page { Rectangle { id: noteItem + anchors.fill: parent anchors.margins: MSpacing.sm color: MColors.surface @@ -146,13 +166,6 @@ Page { border.width: Constants.borderWidthThin border.color: MColors.border - Behavior on x { - NumberAnimation { - duration: 200 - easing.type: Easing.OutQuad - } - } - Row { anchors.fill: parent anchors.margins: MSpacing.md @@ -210,9 +223,9 @@ Page { } MouseArea { - anchors.fill: parent property real startX: 0 + anchors.fill: parent onPressed: { startX = mouse.x; noteItem.color = MColors.elevated; @@ -220,11 +233,10 @@ Page { } onReleased: { noteItem.color = MColors.surface; - if (noteItem.x < -100) { + if (noteItem.x < -100) notesApp.deleteNote(modelData.id); - } else { + else noteItem.x = 0; - } } onCanceled: { noteItem.color = MColors.surface; @@ -233,17 +245,22 @@ Page { onPositionChanged: { if (pressed) { var delta = mouse.x - startX; - if (delta < 0) { + if (delta < 0) noteItem.x = Math.max(delta, -120); - } } } onClicked: { - if (noteItem.x === 0) { + if (noteItem.x === 0) openNote(modelData.id); - } else { + else noteItem.x = 0; - } + } + } + + Behavior on x { + NumberAnimation { + duration: 200 + easing.type: Easing.OutQuad } } } @@ -281,24 +298,7 @@ Page { } } - function formatTimestamp(timestamp) { - var date = new Date(timestamp); - var now = new Date(); - var diff = now - date; - - if (diff < 60000) { - return "Just now"; - } else if (diff < 3600000) { - var mins = Math.floor(diff / 60000); - return mins + "m ago"; - } else if (diff < 86400000) { - var hours = Math.floor(diff / 3600000); - return hours + "h ago"; - } else if (diff < 604800000) { - var days = Math.floor(diff / 86400000); - return days + "d ago"; - } else { - return Qt.formatDate(date, "MMM d"); - } + background: Rectangle { + color: MColors.background } } diff --git a/apps/phone/PhoneApp.qml b/apps/phone/PhoneApp.qml index e0caa17e2..49f80d951 100644 --- a/apps/phone/PhoneApp.qml +++ b/apps/phone/PhoneApp.qml @@ -18,6 +18,7 @@ MApp { property string editingContactName: "" property string editingContactPhone: "" property string editingContactEmail: "" + property var activeCallPageRef: null function resolveContactName(number) { for (var i = 0; i < contacts.length; i++) { @@ -30,7 +31,6 @@ MApp { function formatTimestamp(timestamp) { var now = Date.now(); var ts = Number(timestamp); - // Allow seconds-precision timestamps (convert to ms). if (Number.isFinite(ts) && ts > 0 && ts < 1e+11) ts = ts * 1000; @@ -94,7 +94,8 @@ MApp { if (typeof TelephonyService !== 'undefined') { TelephonyService.dial(dialedNumber); var contactName = resolveContactName(dialedNumber); - activeCallPage.show(dialedNumber, contactName); + if (activeCallPageRef) + activeCallPageRef.show(dialedNumber, contactName); } HapticService.medium(); } @@ -103,15 +104,14 @@ MApp { appId: "phone" appName: "Phone" appIcon: "assets/icon.svg" - // Check contacts permission on launch Component.onCompleted: { if (typeof TelephonyService !== 'undefined' && TelephonyService.callState === "active") { var number = TelephonyService.activeNumber; var contactName = resolveContactName(number); - activeCallPage.show(number, contactName); + if (activeCallPageRef) + activeCallPageRef.show(number, contactName); Logger.info("Phone", "Phone app opened with active call: " + contactName + " (" + number + ")"); } - // Check contacts permission if (typeof PermissionManager !== 'undefined') { if (PermissionManager.hasPermission(appId, "contacts")) { Logger.info("Phone", "Contacts permission already granted"); @@ -126,7 +126,6 @@ MApp { } } - // Listen for permission responses Connections { function onPermissionGranted(grantedAppId, permission) { if (grantedAppId === appId && permission === "contacts") { @@ -155,24 +154,24 @@ MApp { function onCallStateChanged(state) { Logger.info("Phone", "Call state changed: " + state); if (state === "idle") { - // Call ended if (dialedNumber.length > 0) dialedNumber = ""; if (activeCallPage.visible) - activeCallPage.hide(); + if (activeCallPageRef) + activeCallPageRef.hide(); if (incomingCallScreen.visible) incomingCallScreen.hide(); } else if (state === "active") { - // Call answered - hide incoming screen, show active call screen if (incomingCallScreen.visible) incomingCallScreen.hide(); if (!activeCallPage.visible && typeof TelephonyService !== 'undefined') { var number = TelephonyService.activeNumber; var contactName = resolveContactName(number); - activeCallPage.show(number, contactName); + if (activeCallPageRef) + activeCallPageRef.show(number, contactName); } } } @@ -196,7 +195,6 @@ MApp { height: parent.height - tabBar.height currentIndex: parent.currentIndex - // Dialer Page Rectangle { color: MColors.background @@ -205,7 +203,6 @@ MApp { anchors.margins: MSpacing.lg spacing: MSpacing.lg - // Display Rectangle { width: parent.width height: Constants.touchTargetLarge @@ -225,8 +222,8 @@ MApp { } } - // Keypad Grid { + id: dialPadGrid width: parent.width height: parent.height - Constants.touchTargetLarge - Constants.touchTargetLarge - MSpacing.lg * 3 columns: 3 @@ -286,8 +283,8 @@ MApp { ] Rectangle { - width: (parent.width - parent.spacing * 2) / 3 - height: (parent.height - parent.spacing * 3) / 4 + width: (dialPadGrid.width - dialPadGrid.spacing * 2) / 3 + height: (dialPadGrid.height - dialPadGrid.spacing * 3) / 4 color: "transparent" border.width: Constants.borderWidthThin border.color: MColors.border @@ -334,7 +331,6 @@ MApp { } } - // Call/Delete buttons Row { width: parent.width spacing: MSpacing.lg @@ -416,7 +412,6 @@ MApp { } } - // Call History Page ListView { Layout.fillWidth: true Layout.fillHeight: true @@ -509,7 +504,6 @@ MApp { } } - // Contacts Page Rectangle { color: MColors.background @@ -667,6 +661,11 @@ MApp { id: activeCallPage anchors.fill: parent + Component.onCompleted: phoneApp.activeCallPageRef = activeCallPage + Component.onDestruction: { + if (phoneApp.activeCallPageRef === activeCallPage) + phoneApp.activeCallPageRef = null; + } } } } diff --git a/apps/phone/pages/ActiveCallPage.qml b/apps/phone/pages/ActiveCallPage.qml index def66e707..0942e6b9d 100644 --- a/apps/phone/pages/ActiveCallPage.qml +++ b/apps/phone/pages/ActiveCallPage.qml @@ -1,14 +1,11 @@ -import QtQuick import MarathonApp.Phone import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick Rectangle { id: activeCallPage - color: MColors.background - visible: false - z: 1000 property string callNumber: "" property string callName: "Unknown" @@ -16,16 +13,6 @@ Rectangle { property bool isMuted: false property bool isSpeakerOn: false - Timer { - id: durationTimer - interval: 1000 - running: activeCallPage.visible - repeat: true - onTriggered: { - callDuration++; - } - } - function show(number, name) { callNumber = number; callName = name || "Unknown"; @@ -44,13 +31,27 @@ Rectangle { var hours = Math.floor(seconds / 3600); var minutes = Math.floor((seconds % 3600) / 60); var secs = seconds % 60; - - if (hours > 0) { + if (hours > 0) return hours + ":" + (minutes < 10 ? "0" : "") + minutes + ":" + (secs < 10 ? "0" : "") + secs; - } + return minutes + ":" + (secs < 10 ? "0" : "") + secs; } + color: MColors.background + visible: false + z: 1000 + + Timer { + id: durationTimer + + interval: 1000 + running: activeCallPage.visible + repeat: true + onTriggered: { + callDuration++; + } + } + Column { anchors.centerIn: parent spacing: MSpacing.xl * 2 @@ -114,34 +115,34 @@ Rectangle { Repeater { model: [ { - icon: isMuted ? "volume-x" : "volume-2", - label: "Mute", - action: "mute" + "icon": isMuted ? "volume-x" : "volume-2", + "label": "Mute", + "action": "mute" }, { - icon: "user-plus", - label: "Add", - action: "add" + "icon": "user-plus", + "label": "Add", + "action": "add" }, { - icon: isSpeakerOn ? "volume-2" : "smartphone", - label: "Speaker", - action: "speaker" + "icon": isSpeakerOn ? "volume-2" : "smartphone", + "label": "Speaker", + "action": "speaker" }, { - icon: "grid", - label: "Keypad", - action: "keypad" + "icon": "grid", + "label": "Keypad", + "action": "keypad" }, { - icon: "pause", - label: "Hold", - action: "hold" + "icon": "pause", + "label": "Hold", + "action": "hold" }, { - icon: "arrow-left", - label: "Transfer", - action: "transfer" + "icon": "arrow-left", + "label": "Transfer", + "action": "transfer" } ] @@ -172,23 +173,23 @@ Rectangle { HapticService.light(); } onReleased: { - parent.scale = 1.0; + parent.scale = 1; } onCanceled: { - parent.scale = 1.0; + parent.scale = 1; } onClicked: { if (modelData.action === "mute") { isMuted = !isMuted; - if (typeof AudioRoutingManagerCpp !== 'undefined') { + if (typeof AudioRoutingManagerCpp !== 'undefined') AudioRoutingManagerCpp.setMuted(isMuted); - } + Logger.info("Phone", "Mute toggled: " + isMuted); } else if (modelData.action === "speaker") { isSpeakerOn = !isSpeakerOn; - if (typeof AudioRoutingManagerCpp !== 'undefined') { + if (typeof AudioRoutingManagerCpp !== 'undefined') AudioRoutingManagerCpp.setSpeakerphone(isSpeakerOn); - } + Logger.info("Phone", "Speaker toggled: " + isSpeakerOn); } else { Logger.info("Phone", "Action: " + modelData.action); @@ -239,15 +240,15 @@ Rectangle { HapticService.medium(); } onReleased: { - parent.scale = 1.0; + parent.scale = 1; } onCanceled: { - parent.scale = 1.0; + parent.scale = 1; } onClicked: { - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined') TelephonyService.hangup(); - } + hide(); } } diff --git a/apps/phone/pages/ContactEditorPage.qml b/apps/phone/pages/ContactEditorPage.qml index d5a8db855..25449529c 100644 --- a/apps/phone/pages/ContactEditorPage.qml +++ b/apps/phone/pages/ContactEditorPage.qml @@ -1,12 +1,11 @@ -import QtQuick import MarathonApp.Phone import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick Rectangle { id: contactEditorPage - color: MColors.background property int contactId: -1 property string contactName: "" @@ -17,6 +16,38 @@ Rectangle { signal contactSaved signal cancelled + function saveContact() { + if (nameInput.text.length === 0 || phoneInput.text.length === 0) + return; + + if (typeof ContactsManager !== 'undefined') { + if (isNewContact) { + ContactsManager.addContact(nameInput.text, phoneInput.text, emailInput.text); + Logger.info("ContactEditor", "Created contact: " + nameInput.text); + } else { + ContactsManager.updateContact(contactId, { + "name": nameInput.text, + "phone": phoneInput.text, + "email": emailInput.text + }); + Logger.info("ContactEditor", "Updated contact: " + nameInput.text); + } + } + HapticService.medium(); + contactSaved(); + } + + function deleteContact() { + if (typeof ContactsManager !== 'undefined' && contactId !== -1) { + ContactsManager.deleteContact(contactId); + Logger.info("ContactEditor", "Deleted contact ID: " + contactId); + } + HapticService.medium(); + contactSaved(); + } + + color: MColors.background + Column { anchors.fill: parent spacing: 0 @@ -76,6 +107,7 @@ Rectangle { Column { id: contentColumn + width: parent.width spacing: MSpacing.lg padding: MSpacing.lg @@ -111,6 +143,7 @@ Rectangle { MTextInput { id: nameInput + width: parent.width placeholderText: "Enter name" text: contactName @@ -130,6 +163,7 @@ Rectangle { MTextInput { id: phoneInput + width: parent.width placeholderText: "+1 (555) 123-4567" text: contactPhone @@ -152,6 +186,7 @@ Rectangle { MTextInput { id: emailInput + width: parent.width placeholderText: "email@example.com" text: contactEmail @@ -197,36 +232,4 @@ Rectangle { } } } - - function saveContact() { - if (nameInput.text.length === 0 || phoneInput.text.length === 0) { - return; - } - - if (typeof ContactsManager !== 'undefined') { - if (isNewContact) { - ContactsManager.addContact(nameInput.text, phoneInput.text, emailInput.text); - Logger.info("ContactEditor", "Created contact: " + nameInput.text); - } else { - ContactsManager.updateContact(contactId, { - "name": nameInput.text, - "phone": phoneInput.text, - "email": emailInput.text - }); - Logger.info("ContactEditor", "Updated contact: " + nameInput.text); - } - } - - HapticService.medium(); - contactSaved(); - } - - function deleteContact() { - if (typeof ContactsManager !== 'undefined' && contactId !== -1) { - ContactsManager.deleteContact(contactId); - Logger.info("ContactEditor", "Deleted contact ID: " + contactId); - } - HapticService.medium(); - contactSaved(); - } } diff --git a/apps/phone/pages/ContactsPage.qml b/apps/phone/pages/ContactsPage.qml index 8d87240c9..5ee0be8b2 100644 --- a/apps/phone/pages/ContactsPage.qml +++ b/apps/phone/pages/ContactsPage.qml @@ -1,29 +1,28 @@ -import QtQuick import MarathonApp.Phone import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Core +import MarathonUI.Theme +import QtQuick Rectangle { color: MColors.background ListView { id: contactsList + anchors.fill: parent anchors.leftMargin: MSpacing.md anchors.rightMargin: MSpacing.md anchors.bottomMargin: MSpacing.md spacing: MSpacing.sm clip: true + model: phoneApp.contacts - // Header spacer for top padding header: Item { width: parent.width height: MSpacing.lg } - model: phoneApp.contacts - delegate: Rectangle { width: contactsList.width height: Constants.touchTargetLarge diff --git a/apps/phone/pages/DialerPage.qml b/apps/phone/pages/DialerPage.qml index ca301f680..2c8350bdf 100644 --- a/apps/phone/pages/DialerPage.qml +++ b/apps/phone/pages/DialerPage.qml @@ -1,15 +1,15 @@ -import QtQuick import MarathonApp.Phone -import QtQuick.Layouts import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Layouts Rectangle { - color: MColors.background - property string dialedNumber: "" + color: MColors.background + ColumnLayout { anchors.fill: parent anchors.margins: MSpacing.md @@ -51,77 +51,75 @@ Rectangle { Repeater { model: [ { - digit: "1", - letters: "" + "digit": "1", + "letters": "" }, { - digit: "2", - letters: "ABC" + "digit": "2", + "letters": "ABC" }, { - digit: "3", - letters: "DEF" + "digit": "3", + "letters": "DEF" }, { - digit: "4", - letters: "GHI" + "digit": "4", + "letters": "GHI" }, { - digit: "5", - letters: "JKL" + "digit": "5", + "letters": "JKL" }, { - digit: "6", - letters: "MNO" + "digit": "6", + "letters": "MNO" }, { - digit: "7", - letters: "PQRS" + "digit": "7", + "letters": "PQRS" }, { - digit: "8", - letters: "TUV" + "digit": "8", + "letters": "TUV" }, { - digit: "9", - letters: "WXYZ" + "digit": "9", + "letters": "WXYZ" }, { - digit: "*", - letters: "" + "digit": "*", + "letters": "" }, { - digit: "0", - letters: "+" + "digit": "0", + "letters": "+" }, { - digit: "#", - letters: "" + "digit": "#", + "letters": "" } ] Item { required property var modelData + width: (parent.width - MSpacing.md * 2) / 3 height: Constants.touchTargetLarge MCircularIconButton { + property string subtitle: modelData.letters + anchors.centerIn: parent text: modelData.digit buttonSize: Math.min(parent.width, parent.height) - 10 iconSize: 24 variant: "secondary" - - // Show letters as subtitle if present - property string subtitle: modelData.letters - onClicked: { HapticService.light(); - if (modelData.digit === "0" && dialedNumber.length === 0) { + if (modelData.digit === "0" && dialedNumber.length === 0) dialedNumber = "+"; - } else { + else dialedNumber += modelData.digit; - } } } } @@ -143,9 +141,8 @@ Rectangle { variant: "secondary" disabled: dialedNumber.length === 0 onClicked: { - if (dialedNumber.length > 0) { + if (dialedNumber.length > 0) dialedNumber = dialedNumber.slice(0, -1); - } } } @@ -154,7 +151,6 @@ Rectangle { iconName: "phone" iconLeft: true variant: "primary" - size: "large" disabled: dialedNumber.length === 0 implicitWidth: Constants.touchTargetLarge * 2 onClicked: { diff --git a/apps/phone/pages/IncomingCallScreen.qml b/apps/phone/pages/IncomingCallScreen.qml index 2b7e44311..51ca0e781 100644 --- a/apps/phone/pages/IncomingCallScreen.qml +++ b/apps/phone/pages/IncomingCallScreen.qml @@ -1,15 +1,11 @@ -import QtQuick import MarathonApp.Phone import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick Rectangle { id: incomingCallScreen - anchors.fill: parent - color: MColors.background - z: 1000 - visible: false property string callerNumber: "" property string callerName: "Unknown" @@ -24,6 +20,11 @@ Rectangle { visible = false; } + anchors.fill: parent + color: MColors.background + z: 1000 + visible: false + Column { anchors.centerIn: parent spacing: MSpacing.xl * 2 @@ -53,15 +54,17 @@ Rectangle { SequentialAnimation on scale { running: incomingCallScreen.visible loops: Animation.Infinite + NumberAnimation { - from: 1.0 + from: 1 to: 1.1 duration: 800 easing.type: Easing.InOutQuad } + NumberAnimation { from: 1.1 - to: 1.0 + to: 1 duration: 800 easing.type: Easing.InOutQuad } @@ -120,9 +123,9 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined') TelephonyService.hangup(); - } + hide(); } } @@ -146,9 +149,9 @@ Rectangle { MouseArea { anchors.fill: parent onClicked: { - if (typeof TelephonyService !== 'undefined') { + if (typeof TelephonyService !== 'undefined') TelephonyService.answer(); - } + hide(); } } diff --git a/apps/settings/SettingsApp.qml b/apps/settings/SettingsApp.qml index cfd07de97..abf4f2abb 100644 --- a/apps/settings/SettingsApp.qml +++ b/apps/settings/SettingsApp.qml @@ -5,8 +5,6 @@ import QtQuick import QtQuick.Controls MApp { - // End Rectangle (content) - id: settingsApp appId: "settings" @@ -14,11 +12,6 @@ MApp { appIcon: "assets/icon.svg" content: Rectangle { - /** - * Navigate to a specific settings page - * @param pageName {string} - Page identifier (wifi, bluetooth, etc) - * @param params {object} - Optional parameters to pass to page - */ function navigateToSettingsPage(pageName, params) { Logger.info("SettingsApp", "Navigate to page: " + pageName); var component = null; @@ -80,9 +73,6 @@ MApp { navigationStack.push(component, pageParams); } - /** - * Navigate back in stack - */ function navigateBack() { if (navigationStack.depth > 1) { navigationStack.pop(); @@ -91,26 +81,14 @@ MApp { return false; } - /** - * Get current page name - */ - function getCurrentPage() { - if (navigationStack.currentItem) - return navigationStack.currentItem.pageName || "main"; - - return "main"; - } - anchors.fill: parent color: MColors.background - // Navigation stack StackView { id: navigationStack anchors.fill: parent initialItem: settingsMainPage - // Update parent's navigationDepth when stack changes onDepthChanged: { var newDepth = depth - 1; Logger.info("SettingsApp", "StackView depth changed: " + depth + " → navigationDepth: " + newDepth); @@ -120,7 +98,6 @@ MApp { settingsApp.navigationDepth = depth - 1; } - // Use Connections for proper lifetime management Connections { function onBackPressed() { if (navigationStack.depth > 1) @@ -130,7 +107,6 @@ MApp { target: settingsApp } - // BB10-inspired slide transitions pushEnter: Transition { NumberAnimation { property: "x" @@ -200,7 +176,6 @@ MApp { } } - // Main settings page component Component { id: settingsMainPage @@ -214,7 +189,6 @@ MApp { } } - // Page components Component { id: wifiPageComponent @@ -343,9 +317,7 @@ MApp { } } - // Listen for deep link navigation Connections { - // Modern deep link handler function onDeepLinkRequested(appId, route, params) { if (appId === "settings") { Logger.info("SettingsApp", "Deep link requested: " + route); @@ -353,7 +325,6 @@ MApp { } } - // Legacy handler (deprecated but kept for compatibility) function onSettingsNavigationRequested(page, subpage, params) { Logger.info("SettingsApp", "Legacy navigation: " + page); navigateToSettingsPage(page, params); diff --git a/apps/settings/components/BluetoothPairDialog.qml b/apps/settings/components/BluetoothPairDialog.qml index 931df7ec7..79acd86f7 100644 --- a/apps/settings/components/BluetoothPairDialog.qml +++ b/apps/settings/components/BluetoothPairDialog.qml @@ -1,34 +1,19 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Controls import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Core +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls -/** - * Polished Bluetooth Pairing Dialog - * - * Handles various Bluetooth pairing types: - * - Just Works (no input needed) - * - PIN Entry (user enters 4-6 digit PIN) - * - Passkey Entry (user enters 6-digit passkey) - * - Passkey Confirmation (user confirms displayed passkey) - * - Pairing in progress - * - Success/failure states - */ Item { id: bluetoothPairDialog - anchors.fill: parent - visible: false - z: Constants.zIndexModalOverlay + 10 - // Public API property string deviceName: "" property string deviceAddress: "" - property string deviceType: "" // "headphones", "keyboard", "mouse", "phone", etc. - property string pairingMode: "" // "pin", "passkey", "confirm", "justworks" - property string displayedPasskey: "" // For confirmation mode + property string deviceType: "" + property string pairingMode: "" + property string displayedPasskey: "" property bool isPairing: false property string errorMessage: "" @@ -36,7 +21,6 @@ Item { signal pairConfirmed(bool accepted) signal cancelled - // Show the dialog for pairing function show(name, address, type, mode) { deviceName = name; deviceAddress = address; @@ -46,22 +30,18 @@ Item { errorMessage = ""; pinInput.text = ""; passkeyInput.text = ""; - if (pairingMode === "pin" || pairingMode === "passkey") { - if (pairingMode === "pin") { + if (pairingMode === "pin") pinInput.forceActiveFocus(); - } else { + else passkeyInput.forceActiveFocus(); - } } - bluetoothPairDialog.visible = true; showAnimation.start(); HapticService.light(); Logger.info("BluetoothPairDialog", "Showing pairing dialog for: " + name); } - // Show passkey for confirmation function showPasskeyConfirmation(name, address, type, passkey) { deviceName = name; deviceAddress = address; @@ -70,34 +50,101 @@ Item { displayedPasskey = passkey; isPairing = false; errorMessage = ""; - bluetoothPairDialog.visible = true; showAnimation.start(); HapticService.light(); Logger.info("BluetoothPairDialog", "Showing passkey confirmation: " + passkey); } - // Hide the dialog function hide() { hideAnimation.start(); } - // Show error function showError(message) { errorMessage = message; isPairing = false; HapticService.medium(); } - // Show pairing in progress function showPairing() { isPairing = true; errorMessage = ""; } - // Background overlay + function getDeviceIcon(type) { + switch (type.toLowerCase()) { + case "headphones": + case "headset": + return "headphones"; + case "keyboard": + return "keyboard"; + case "mouse": + return "mouse"; + case "phone": + case "smartphone": + return "smartphone"; + case "computer": + case "laptop": + return "monitor"; + case "speaker": + return "volume-2"; + default: + return "bluetooth"; + } + } + + function getPairingModeText(mode) { + switch (mode) { + case "pin": + return "Enter PIN to pair"; + case "passkey": + return "Enter passkey displayed on device"; + case "confirm": + return "Confirm pairing code"; + case "justworks": + default: + return "Ready to pair"; + } + } + + function getHelpText(mode) { + switch (mode) { + case "pin": + return "Enter the PIN shown on your device (usually 0000 or 1234)"; + case "passkey": + return "Type the 6-digit passkey displayed on the device"; + case "confirm": + return "Make sure the code above matches what's shown on your device"; + case "justworks": + default: + return "No PIN required for this device"; + } + } + + function canPair() { + if (pairingMode === "confirm" || pairingMode === "justworks") + return true; + + if (pairingMode === "pin") + return pinInput.text.length >= 4; + + if (pairingMode === "passkey") + return passkeyInput.text.length === 6; + + return false; + } + + anchors.fill: parent + visible: false + z: Constants.zIndexModalOverlay + 10 + Keys.onEscapePressed: { + if (!isPairing) + bluetoothPairDialog.hide(); + } + Rectangle { id: overlay + anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.6) opacity: 0 @@ -105,16 +152,15 @@ Item { MouseArea { anchors.fill: parent onClicked: { - if (!isPairing) { + if (!isPairing) bluetoothPairDialog.hide(); - } } } } - // Dialog card Rectangle { id: dialogCard + anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 0 @@ -124,31 +170,21 @@ Item { color: MColors.surface border.width: Constants.borderWidthThin border.color: MColors.border - transform: Translate { - id: translateTransform - y: dialogCard.height - } - - // Glass morphism effect layer.enabled: true - layer.effect: ShaderEffect { - property real blur: 32 - } Column { id: contentColumn + anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: MSpacing.lg spacing: MSpacing.lg - // Header row Row { width: parent.width spacing: MSpacing.md - // Device icon Rectangle { width: Constants.touchTargetMedium height: Constants.touchTargetMedium @@ -164,7 +200,6 @@ Item { } } - // Device info Column { width: parent.width - Constants.touchTargetMedium - MSpacing.md spacing: MSpacing.xs @@ -190,7 +225,6 @@ Item { } } - // PIN/Passkey input (for pin/passkey modes) Rectangle { width: parent.width height: Constants.inputHeight @@ -200,12 +234,6 @@ Item { border.color: errorMessage !== "" ? MColors.error : ((pinInput.activeFocus || passkeyInput.activeFocus) ? MColors.marathonTeal : MColors.border) visible: pairingMode === "pin" || pairingMode === "passkey" - Behavior on border.color { - ColorAnimation { - duration: 150 - } - } - Row { anchors.fill: parent anchors.margins: MSpacing.md @@ -220,6 +248,7 @@ Item { TextInput { id: pinInput + visible: pairingMode === "pin" width: parent.width - Constants.iconSizeMedium - MSpacing.md anchors.verticalCenter: parent.verticalCenter @@ -230,8 +259,11 @@ Item { maximumLength: 6 enabled: !isPairing selectByMouse: true + Keys.onReturnPressed: { + if (pinInput.text.length >= 4) + pairButton.clicked(); + } - // Placeholder Text { text: "Enter PIN (4-6 digits)" font: pinInput.font @@ -239,16 +271,11 @@ Item { visible: pinInput.text.length === 0 && !pinInput.activeFocus anchors.verticalCenter: parent.verticalCenter } - - Keys.onReturnPressed: { - if (pinInput.text.length >= 4) { - pairButton.clicked(); - } - } } TextInput { id: passkeyInput + visible: pairingMode === "passkey" width: parent.width - Constants.iconSizeMedium - MSpacing.md anchors.verticalCenter: parent.verticalCenter @@ -259,8 +286,11 @@ Item { maximumLength: 6 enabled: !isPairing selectByMouse: true + Keys.onReturnPressed: { + if (passkeyInput.text.length === 6) + pairButton.clicked(); + } - // Placeholder Text { text: "Enter 6-digit passkey" font: passkeyInput.font @@ -268,17 +298,16 @@ Item { visible: passkeyInput.text.length === 0 && !passkeyInput.activeFocus anchors.verticalCenter: parent.verticalCenter } + } + } - Keys.onReturnPressed: { - if (passkeyInput.text.length === 6) { - pairButton.clicked(); - } - } + Behavior on border.color { + ColorAnimation { + duration: 150 } } } - // Passkey confirmation display Rectangle { width: parent.width height: Math.round(100 * Constants.scaleFactor) @@ -312,7 +341,6 @@ Item { } } - // Error message Rectangle { width: parent.width height: errorText.height + MSpacing.md @@ -337,6 +365,7 @@ Item { Text { id: errorText + text: errorMessage font.pixelSize: MTypography.sizeSmall font.family: MTypography.fontFamily @@ -347,7 +376,6 @@ Item { } } - // Pairing progress Column { width: parent.width spacing: MSpacing.sm @@ -374,14 +402,12 @@ Item { } } - // Action buttons Row { width: parent.width height: Constants.touchTargetMedium spacing: MSpacing.md visible: !isPairing - // Cancel/Reject button Rectangle { width: (parent.width - MSpacing.md) / 2 height: parent.height @@ -403,28 +429,25 @@ Item { onClicked: { Logger.info("BluetoothPairDialog", "Pairing cancelled/rejected"); HapticService.light(); - bluetoothPairDialog.hide(); - - if (pairingMode === "confirm") { + if (pairingMode === "confirm") bluetoothPairDialog.pairConfirmed(false); - } else { + else bluetoothPairDialog.cancelled(); - } } } } - // Pair/Accept button Rectangle { id: pairButton + + signal clicked + width: (parent.width - MSpacing.md) / 2 height: parent.height radius: Constants.borderRadiusSmall color: canPair() ? MColors.marathonTeal : Qt.darker(MColors.marathonTeal, 1.5) - opacity: canPair() ? 1.0 : 0.5 - - signal clicked + opacity: canPair() ? 1 : 0.5 Text { text: pairingMode === "confirm" ? "Accept" : "Pair" @@ -442,23 +465,19 @@ Item { Logger.info("BluetoothPairDialog", "Pair/Accept clicked"); HapticService.medium(); bluetoothPairDialog.showPairing(); - - if (pairingMode === "confirm") { + if (pairingMode === "confirm") bluetoothPairDialog.pairConfirmed(true); - } else if (pairingMode === "pin") { + else if (pairingMode === "pin") bluetoothPairDialog.pairRequested(pinInput.text); - } else if (pairingMode === "passkey") { + else if (pairingMode === "passkey") bluetoothPairDialog.pairRequested(passkeyInput.text); - } else { - // Just works mode + else bluetoothPairDialog.pairRequested(""); - } } } } } - // Help text Text { text: getHelpText(pairingMode) font.pixelSize: MTypography.sizeXSmall @@ -470,76 +489,18 @@ Item { visible: !isPairing } } - } - // Helper functions - function getDeviceIcon(type) { - switch (type.toLowerCase()) { - case "headphones": - case "headset": - return "headphones"; - case "keyboard": - return "keyboard"; - case "mouse": - return "mouse"; - case "phone": - case "smartphone": - return "smartphone"; - case "computer": - case "laptop": - return "monitor"; - case "speaker": - return "volume-2"; - default: - return "bluetooth"; - } - } - - function getPairingModeText(mode) { - switch (mode) { - case "pin": - return "Enter PIN to pair"; - case "passkey": - return "Enter passkey displayed on device"; - case "confirm": - return "Confirm pairing code"; - case "justworks": - default: - return "Ready to pair"; - } - } - - function getHelpText(mode) { - switch (mode) { - case "pin": - return "Enter the PIN shown on your device (usually 0000 or 1234)"; - case "passkey": - return "Type the 6-digit passkey displayed on the device"; - case "confirm": - return "Make sure the code above matches what's shown on your device"; - case "justworks": - default: - return "No PIN required for this device"; - } - } - - function canPair() { - if (pairingMode === "confirm" || pairingMode === "justworks") { - return true; - } + transform: Translate { + id: translateTransform - if (pairingMode === "pin") { - return pinInput.text.length >= 4; + y: dialogCard.height } - if (pairingMode === "passkey") { - return passkeyInput.text.length === 6; + layer.effect: ShaderEffect { + property real blur: 32 } - - return false; } - // Show animation ParallelAnimation { id: showAnimation @@ -562,7 +523,6 @@ Item { } } - // Hide animation SequentialAnimation { id: hideAnimation @@ -594,11 +554,4 @@ Item { } } } - - // Handle escape key - Keys.onEscapePressed: { - if (!isPairing) { - bluetoothPairDialog.hide(); - } - } } diff --git a/apps/settings/components/SettingsPageTemplate.qml b/apps/settings/components/SettingsPageTemplate.qml index 8bd6eb2c0..74a0ed23a 100644 --- a/apps/settings/components/SettingsPageTemplate.qml +++ b/apps/settings/components/SettingsPageTemplate.qml @@ -1,10 +1,10 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Controls import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Core +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls Page { id: pageTemplate @@ -15,13 +15,9 @@ Page { signal navigateBack - background: Rectangle { - color: MColors.background - } - - // Header with back button (BB10 style) Rectangle { id: header + anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -61,11 +57,11 @@ Page { pageTemplate.navigateBack(); } - // Press feedback Rectangle { anchors.fill: parent color: MColors.textPrimary opacity: parent.pressed ? 0.1 : 0 + Behavior on opacity { NumberAnimation { duration: 150 @@ -74,7 +70,6 @@ Page { } } - // Bottom border Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left @@ -84,12 +79,16 @@ Page { } } - // Content area Loader { id: contentLoader + anchors.top: showBackButton ? header.bottom : parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom } + + background: Rectangle { + color: MColors.background + } } diff --git a/apps/settings/components/WiFiPasswordDialog.qml b/apps/settings/components/WiFiPasswordDialog.qml index 9e9dcf7a2..527938093 100644 --- a/apps/settings/components/WiFiPasswordDialog.qml +++ b/apps/settings/components/WiFiPasswordDialog.qml @@ -1,29 +1,14 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Controls import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Core +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls -/** - * Polished WiFi Password Dialog - * - * Slide-up modal for entering WiFi passwords with: - * - Auto-focus on input field - * - Show/hide password toggle - * - Signal strength indicator - * - Security type badge - * - Loading state during connection - * - Error handling with retry - */ Item { id: wifiDialog - anchors.fill: parent - visible: false - z: Constants.zIndexModalOverlay + 10 - // Public API property string networkSsid: "" property int signalStrength: 0 property string securityType: "" @@ -34,7 +19,6 @@ Item { signal connectRequested(string ssid, string password) signal cancelled - // Show the dialog function show(ssid, strength, security, isSecured) { networkSsid = ssid; signalStrength = strength; @@ -50,27 +34,32 @@ Item { Logger.info("WiFiDialog", "Showing dialog for: " + ssid); } - // Hide the dialog function hide() { hideAnimation.start(); } - // Show error function showError(message) { errorMessage = message; isConnecting = false; HapticService.medium(); } - // Show connecting state function showConnecting() { isConnecting = true; errorMessage = ""; } - // Background overlay + anchors.fill: parent + visible: false + z: Constants.zIndexModalOverlay + 10 + Keys.onEscapePressed: { + if (!isConnecting) + wifiDialog.hide(); + } + Rectangle { id: overlay + anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.6) opacity: 0 @@ -78,16 +67,15 @@ Item { MouseArea { anchors.fill: parent onClicked: { - if (!isConnecting) { + if (!isConnecting) wifiDialog.hide(); - } } } } - // Dialog card Rectangle { id: dialogCard + anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 0 @@ -97,31 +85,21 @@ Item { color: MColors.surface border.width: Constants.borderWidthThin border.color: MColors.border - transform: Translate { - id: translateTransform - y: dialogCard.height - } - - // Glass morphism effect layer.enabled: true - layer.effect: ShaderEffect { - property real blur: 32 - } Column { id: contentColumn + anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: MSpacing.lg spacing: MSpacing.lg - // Header row Row { width: parent.width spacing: MSpacing.md - // Network icon with signal strength Rectangle { width: Constants.touchTargetMedium height: Constants.touchTargetMedium @@ -138,7 +116,6 @@ Item { } } - // Network info Column { width: parent.width - Constants.touchTargetMedium - MSpacing.md spacing: MSpacing.xs @@ -157,7 +134,6 @@ Item { Row { spacing: MSpacing.sm - // Security badge Rectangle { width: securityBadgeText.width + Math.round(16 * Constants.scaleFactor) height: Math.round(24 * Constants.scaleFactor) @@ -166,6 +142,7 @@ Item { Text { id: securityBadgeText + text: secured ? securityType : "Open" font.pixelSize: MTypography.sizeXSmall font.weight: Font.Medium @@ -175,7 +152,6 @@ Item { } } - // Signal strength text Text { text: signalStrength >= 75 ? "Excellent" : signalStrength >= 50 ? "Good" : signalStrength >= 25 ? "Fair" : "Weak" font.pixelSize: MTypography.sizeXSmall @@ -187,7 +163,6 @@ Item { } } - // Password input Rectangle { width: parent.width height: Constants.inputHeight @@ -197,12 +172,6 @@ Item { border.color: errorMessage !== "" ? MColors.error : (passwordInput.activeFocus ? MColors.marathonTeal : MColors.border) visible: secured - Behavior on border.color { - ColorAnimation { - duration: 150 - } - } - Row { anchors.fill: parent anchors.margins: MSpacing.md @@ -217,6 +186,7 @@ Item { TextInput { id: passwordInput + width: parent.width - Constants.iconSizeMedium - Constants.touchTargetSmall - MSpacing.md * 2 anchors.verticalCenter: parent.verticalCenter font.pixelSize: MTypography.sizeBody @@ -227,8 +197,11 @@ Item { enabled: !isConnecting selectByMouse: true clip: true + Keys.onReturnPressed: { + if (passwordInput.text.length >= 8) + connectButton.clicked(); + } - // Placeholder text (TextInput doesn't render placeholderText, so we fake it) Text { text: "Enter Password" font.pixelSize: MTypography.sizeBody @@ -238,18 +211,13 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left } - - Keys.onReturnPressed: { - if (passwordInput.text.length >= 8) { - connectButton.clicked(); - } - } } - // Show/hide password toggle Rectangle { id: showPasswordToggle + property bool checked: false + width: Constants.touchTargetSmall height: Constants.touchTargetSmall radius: Constants.borderRadiusSmall @@ -272,9 +240,14 @@ Item { } } } + + Behavior on border.color { + ColorAnimation { + duration: 150 + } + } } - // Error message Rectangle { width: parent.width height: errorText.height + MSpacing.md @@ -299,6 +272,7 @@ Item { Text { id: errorText + text: errorMessage font.pixelSize: MTypography.sizeSmall font.family: MTypography.fontFamily @@ -309,7 +283,6 @@ Item { } } - // Connection progress Column { width: parent.width spacing: MSpacing.sm @@ -336,13 +309,11 @@ Item { } } - // Action buttons Row { width: parent.width height: Constants.touchTargetMedium spacing: MSpacing.md - // Cancel button Rectangle { width: (parent.width - MSpacing.md) / 2 height: parent.height @@ -350,7 +321,7 @@ Item { color: "transparent" border.width: Constants.borderWidthThin border.color: MColors.border - opacity: isConnecting ? 0.5 : 1.0 + opacity: isConnecting ? 0.5 : 1 Text { text: "Cancel" @@ -372,16 +343,16 @@ Item { } } - // Connect button Rectangle { id: connectButton + + signal clicked + width: (parent.width - MSpacing.md) / 2 height: parent.height radius: Constants.borderRadiusSmall color: (secured && passwordInput.text.length < 8) || isConnecting ? Qt.darker(MColors.marathonTeal, 1.5) : MColors.marathonTeal - opacity: (secured && passwordInput.text.length < 8) || isConnecting ? 0.5 : 1.0 - - signal clicked + opacity: (secured && passwordInput.text.length < 8) || isConnecting ? 0.5 : 1 Text { text: "Connect" @@ -405,7 +376,6 @@ Item { } } - // Help text Text { text: secured ? "Password must be at least 8 characters" : "This network is not secured" font.pixelSize: MTypography.sizeXSmall @@ -417,9 +387,18 @@ Item { visible: !isConnecting } } + + transform: Translate { + id: translateTransform + + y: dialogCard.height + } + + layer.effect: ShaderEffect { + property real blur: 32 + } } - // Show animation ParallelAnimation { id: showAnimation @@ -442,7 +421,6 @@ Item { } } - // Hide animation SequentialAnimation { id: hideAnimation @@ -473,11 +451,4 @@ Item { } } } - - // Handle back button/escape key - Keys.onEscapePressed: { - if (!isConnecting) { - wifiDialog.hide(); - } - } } diff --git a/apps/settings/manifest.json b/apps/settings/manifest.json index 6d46f93aa..37da82011 100644 --- a/apps/settings/manifest.json +++ b/apps/settings/manifest.json @@ -5,7 +5,7 @@ "entryPoint": "SettingsApp.qml", "icon": "assets/icon.svg", "author": "Marathon OS", - "permissions": ["system"], + "permissions": ["system", "network"], "minShellVersion": "1.0.0", "protected": true, "categories": ["System"], diff --git a/apps/settings/pages/AppManagerPage.qml b/apps/settings/pages/AppManagerPage.qml index b9d18ed2e..5606a74ab 100644 --- a/apps/settings/pages/AppManagerPage.qml +++ b/apps/settings/pages/AppManagerPage.qml @@ -1,17 +1,109 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Layouts import MarathonApp.Settings import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Layouts SettingsPageTemplate { id: appManagerPage - pageTitle: "App Manager" property string pageName: "appmanager" + pageTitle: "App Manager" + + Rectangle { + id: uninstallDialog + + property string appId: "" + property string appName: "" + + function open() { + visible = true; + } + + function close() { + visible = false; + } + + anchors.centerIn: parent + width: Math.min(Constants.screenWidth * 0.85, parent.width - MSpacing.xl * 2) + height: dialogContent.height + MSpacing.xl * 2 + color: MColors.surface + radius: Constants.borderRadiusLarge + visible: false + z: 1000 + + ColumnLayout { + id: dialogContent + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: MSpacing.xl + spacing: MSpacing.lg + + Text { + Layout.fillWidth: true + text: "Uninstall " + uninstallDialog.appName + "?" + color: MColors.textPrimary + font.pixelSize: MTypography.sizeLarge + font.weight: Font.Bold + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Text { + Layout.fillWidth: true + text: "This app will be permanently removed from your device." + color: MColors.textSecondary + font.pixelSize: MTypography.sizeBody + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + spacing: MSpacing.md + + MButton { + Layout.fillWidth: true + text: "Cancel" + variant: "secondary" + onClicked: { + uninstallDialog.close(); + } + } + + MButton { + Layout.fillWidth: true + text: "Uninstall" + variant: "danger" + onClicked: { + Logger.info("AppManagerPage", "Uninstalling: " + uninstallDialog.appId); + MarathonAppInstaller.uninstallApp(uninstallDialog.appId); + uninstallDialog.close(); + } + } + } + } + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.7) + visible: uninstallDialog.visible + z: 999 + + MouseArea { + anchors.fill: parent + onClicked: { + uninstallDialog.close(); + } + } + } + content: Flickable { contentHeight: contentColumn.height + Constants.navBarHeight + MSpacing.xl * 3 clip: true @@ -19,6 +111,7 @@ SettingsPageTemplate { Column { id: contentColumn + width: parent.width spacing: 0 @@ -65,11 +158,9 @@ SettingsPageTemplate { source: model.icon || "qrc:/images/app-icon-placeholder.svg" fillMode: Image.PreserveAspectFit asynchronous: true - onStatusChanged: { - if (status === Image.Error) { + if (status === Image.Error) source = "qrc:/images/app-icon-placeholder.svg"; - } } } @@ -100,6 +191,7 @@ SettingsPageTemplate { Text { id: systemBadgeText + anchors.centerIn: parent text: "System" color: MColors.marathonTeal @@ -146,93 +238,4 @@ SettingsPageTemplate { } } } - - Rectangle { - id: uninstallDialog - anchors.centerIn: parent - width: Math.min(Constants.screenWidth * 0.85, parent.width - MSpacing.xl * 2) - height: dialogContent.height + MSpacing.xl * 2 - color: MColors.surface - radius: Constants.borderRadiusLarge - visible: false - z: 1000 - - property string appId: "" - property string appName: "" - - function open() { - visible = true; - } - - function close() { - visible = false; - } - - ColumnLayout { - id: dialogContent - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: MSpacing.xl - spacing: MSpacing.lg - - Text { - Layout.fillWidth: true - text: "Uninstall " + uninstallDialog.appName + "?" - color: MColors.textPrimary - font.pixelSize: MTypography.sizeLarge - font.weight: Font.Bold - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - } - - Text { - Layout.fillWidth: true - text: "This app will be permanently removed from your device." - color: MColors.textSecondary - font.pixelSize: MTypography.sizeBody - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - } - - RowLayout { - Layout.fillWidth: true - spacing: MSpacing.md - - MButton { - Layout.fillWidth: true - text: "Cancel" - variant: "secondary" - onClicked: { - uninstallDialog.close(); - } - } - - MButton { - Layout.fillWidth: true - text: "Uninstall" - variant: "danger" - onClicked: { - Logger.info("AppManagerPage", "Uninstalling: " + uninstallDialog.appId); - MarathonAppInstaller.uninstallApp(uninstallDialog.appId); - uninstallDialog.close(); - } - } - } - } - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.7) - visible: uninstallDialog.visible - z: 999 - - MouseArea { - anchors.fill: parent - onClicked: { - uninstallDialog.close(); - } - } - } } diff --git a/apps/settings/pages/AppSortPage.qml b/apps/settings/pages/AppSortPage.qml index 85788a68f..b324f890e 100644 --- a/apps/settings/pages/AppSortPage.qml +++ b/apps/settings/pages/AppSortPage.qml @@ -1,24 +1,26 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Controls import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Core +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls SettingsPageTemplate { id: appSortPage - pageTitle: "App Sorting & Layout" property string pageName: "appsort" + pageTitle: "App Sorting & Layout" + content: Flickable { contentHeight: contentColumn.height + 40 clip: true Column { id: contentColumn + width: parent.width spacing: MSpacing.xl leftPadding: 24 diff --git a/apps/settings/pages/BatteryPage.qml b/apps/settings/pages/BatteryPage.qml index 9848cd562..b64a728f6 100644 --- a/apps/settings/pages/BatteryPage.qml +++ b/apps/settings/pages/BatteryPage.qml @@ -1,29 +1,30 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Controls import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: batteryPage - pageTitle: "Battery" property string pageName: "battery" + pageTitle: "Battery" + content: Flickable { contentHeight: batteryContent.height + 40 clip: true Column { id: batteryContent + width: parent.width spacing: MSpacing.xl leftPadding: 24 rightPadding: 24 topPadding: 24 - // Battery Status Header Column { width: parent.width spacing: MSpacing.sm @@ -41,6 +42,7 @@ SettingsPageTemplate { text: { if (PowerManagerService.isCharging) return "Charging"; + if (PowerManagerService.isPluggedIn) return "Plugged In, Not Charging"; @@ -50,6 +52,7 @@ SettingsPageTemplate { var minutes = Math.floor((time % 3600) / 60); if (hours > 0) return hours + "h " + minutes + "m remaining"; + return minutes + "m remaining"; } return "Discharging"; diff --git a/apps/settings/pages/BluetoothPage.qml b/apps/settings/pages/BluetoothPage.qml index 372ea4870..11d7ab208 100644 --- a/apps/settings/pages/BluetoothPage.qml +++ b/apps/settings/pages/BluetoothPage.qml @@ -1,16 +1,126 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Controls import MarathonUI.Core +import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: bluetoothPage - pageTitle: "Bluetooth" property string pageName: "bluetooth" + property BluetoothPairDialog activePairDialog: null + property var pendingPairRequest: null + + pageTitle: "Bluetooth" + Component.onCompleted: { + Logger.info("BluetoothPage", "Initialized"); + if (BluetoothManagerCpp.enabled) + BluetoothManagerCpp.startScan(); + } + + Loader { + id: bluetoothPairDialogLoader + + function show(name, address, type, mode) { + active = true; + bluetoothPage.pendingPairRequest = { + "name": name, + "address": address, + "type": type, + "mode": mode + }; + if (bluetoothPage.activePairDialog) { + bluetoothPage.activePairDialog.show(name, address, type, mode); + bluetoothPage.pendingPairRequest = null; + } + } + + anchors.fill: parent + active: false + z: 1000 + onLoaded: { + bluetoothPage.activePairDialog = item; + if (bluetoothPage.pendingPairRequest) { + var request = bluetoothPage.pendingPairRequest; + bluetoothPage.pendingPairRequest = null; + bluetoothPage.activePairDialog.show(request.name, request.address, request.type, request.mode); + } + } + onActiveChanged: { + if (!active) + bluetoothPage.activePairDialog = null; + } + + sourceComponent: Component { + BluetoothPairDialog { + id: pairDialog + + anchors.fill: parent + onPairRequested: pin => { + Logger.info("BluetoothPage", "Pairing requested with PIN: " + (pin ? "****" : "none")); + BluetoothManagerCpp.pairDevice(deviceAddress, pin); + } + onPairConfirmed: accepted => { + Logger.info("BluetoothPage", "Pairing confirmation: " + accepted); + if (accepted) { + BluetoothManagerCpp.confirmPairing(deviceAddress, true); + } else { + BluetoothManagerCpp.confirmPairing(deviceAddress, false); + bluetoothPairDialogLoader.item.hide(); + } + } + onCancelled: { + Logger.info("BluetoothPage", "Pairing cancelled"); + BluetoothManagerCpp.cancelPairing(deviceAddress); + } + } + } + } + + Connections { + function onEnabledChanged() { + if (BluetoothManagerCpp.enabled) + BluetoothManagerCpp.startScan(); + else + BluetoothManagerCpp.stopScan(); + } + + function onPairingSucceeded(address) { + Logger.info("BluetoothPage", "✓ Successfully paired with: " + address); + if (bluetoothPage.activePairDialog) + bluetoothPage.activePairDialog.hide(); + + HapticService.medium(); + } + + function onPairingFailed(address, error) { + Logger.error("BluetoothPage", "✗ Failed to pair with " + address + ": " + error); + if (bluetoothPage.activePairDialog) + bluetoothPage.activePairDialog.showError(error || "Pairing failed. Try again."); + } + + function onPinRequested(address, deviceName) { + Logger.info("BluetoothPage", "PIN requested for device: " + deviceName); + if (bluetoothPage.activePairDialog) + bluetoothPage.activePairDialog.show(deviceName, address, "device", "pin"); + } + + function onPasskeyRequested(address, deviceName) { + Logger.info("BluetoothPage", "Passkey requested for device: " + deviceName); + if (bluetoothPage.activePairDialog) + bluetoothPage.activePairDialog.show(deviceName, address, "device", "passkey"); + } + + function onPasskeyConfirmation(address, deviceName, passkey) { + Logger.info("BluetoothPage", "Passkey confirmation requested: " + passkey); + if (bluetoothPage.activePairDialog) + bluetoothPage.activePairDialog.showPasskeyConfirmation(deviceName, address, "device", passkey); + } + + target: BluetoothManagerCpp + } content: Flickable { contentHeight: bluetoothContent.height + Constants.navBarHeight + MSpacing.xl * 3 @@ -19,6 +129,7 @@ SettingsPageTemplate { Column { id: bluetoothContent + width: parent.width spacing: MSpacing.lg leftPadding: MSpacing.lg @@ -35,6 +146,7 @@ SettingsPageTemplate { Icon { id: bluetoothIcon + anchors.left: parent.left anchors.leftMargin: MSpacing.md anchors.verticalCenter: parent.verticalCenter @@ -69,6 +181,7 @@ SettingsPageTemplate { MToggle { id: bluetoothToggle + anchors.right: parent.right anchors.rightMargin: MSpacing.md anchors.verticalCenter: parent.verticalCenter @@ -124,6 +237,7 @@ SettingsPageTemplate { Column { id: deviceColumn + anchors.verticalCenter: parent.verticalCenter width: parent.width - Constants.iconSizeMedium - Constants.iconSizeSmall - MSpacing.md * 4 @@ -156,13 +270,13 @@ SettingsPageTemplate { MouseArea { id: deviceMouseArea + anchors.fill: parent onClicked: { - if (modelData.connected) { + if (modelData.connected) BluetoothManagerCpp.disconnectDevice(modelData.address); - } else { + else BluetoothManagerCpp.connectDevice(modelData.address); - } } } } @@ -182,15 +296,13 @@ SettingsPageTemplate { MButton { anchors.horizontalCenter: parent.horizontalCenter - // width: parent.width - removed to allow implicit sizing text: BluetoothManagerCpp.scanning ? "Stop Scanning" : "Scan for Devices" variant: BluetoothManagerCpp.scanning ? "primary" : "default" onClicked: { - if (BluetoothManagerCpp.scanning) { + if (BluetoothManagerCpp.scanning) BluetoothManagerCpp.stopScan(); - } else { + else BluetoothManagerCpp.startScan(); - } } } @@ -259,15 +371,12 @@ SettingsPageTemplate { MouseArea { id: availableDeviceMouseArea + anchors.fill: parent onClicked: { Logger.info("BluetoothPage", "Selected device for pairing: " + modelData.name); HapticService.light(); - - // Show pairing dialog - // Most devices use "justworks" pairing, but some may require PIN - bluetoothPairDialogLoader.show(modelData.name, modelData.address, modelData.type || "device", "justworks" // Will be updated if device requests PIN/passkey - ); + bluetoothPairDialogLoader.show(modelData.name, modelData.address, modelData.type || "device", "justworks"); } } } @@ -293,112 +402,4 @@ SettingsPageTemplate { } } } - - // Bluetooth pairing dialog - Loader { - id: bluetoothPairDialogLoader - anchors.fill: parent - active: false - z: 1000 - - sourceComponent: Component { - BluetoothPairDialog { - id: pairDialog - anchors.fill: parent - - // Use direct signal handlers instead of .connect() - onPairRequested: pin => { - Logger.info("BluetoothPage", "Pairing requested with PIN: " + (pin ? "****" : "none")); - BluetoothManagerCpp.pairDevice(deviceAddress, pin); - } - - onPairConfirmed: accepted => { - Logger.info("BluetoothPage", "Pairing confirmation: " + accepted); - if (accepted) { - BluetoothManagerCpp.confirmPairing(deviceAddress, true); - } else { - BluetoothManagerCpp.confirmPairing(deviceAddress, false); - bluetoothPairDialogLoader.item.hide(); - } - } - - onCancelled: { - Logger.info("BluetoothPage", "Pairing cancelled"); - BluetoothManagerCpp.cancelPairing(deviceAddress); - } - } - } - - function show(name, address, type, mode) { - active = true; - if (item) { - item.show(name, address, type, mode); - } - } - } - - Connections { - target: BluetoothManagerCpp - - function onEnabledChanged() { - if (BluetoothManagerCpp.enabled) { - BluetoothManagerCpp.startScan(); - } else { - BluetoothManagerCpp.stopScan(); - } - } - - function onPairingSucceeded(address) { - Logger.info("BluetoothPage", "✓ Successfully paired with: " + address); - - if (bluetoothPairDialogLoader.active && bluetoothPairDialogLoader.item) { - bluetoothPairDialogLoader.item.hide(); - } - - HapticService.medium(); - } - - function onPairingFailed(address, error) { - Logger.error("BluetoothPage", "✗ Failed to pair with " + address + ": " + error); - - if (bluetoothPairDialogLoader.active && bluetoothPairDialogLoader.item) { - bluetoothPairDialogLoader.item.showError(error || "Pairing failed. Try again."); - } - } - - function onPinRequested(address, deviceName) { - Logger.info("BluetoothPage", "PIN requested for device: " + deviceName); - - if (bluetoothPairDialogLoader.active && bluetoothPairDialogLoader.item) { - // Update dialog to PIN entry mode - bluetoothPairDialogLoader.item.show(deviceName, address, "device", "pin"); - } - } - - function onPasskeyRequested(address, deviceName) { - Logger.info("BluetoothPage", "Passkey requested for device: " + deviceName); - - if (bluetoothPairDialogLoader.active && bluetoothPairDialogLoader.item) { - // Update dialog to passkey entry mode - bluetoothPairDialogLoader.item.show(deviceName, address, "device", "passkey"); - } - } - - function onPasskeyConfirmation(address, deviceName, passkey) { - Logger.info("BluetoothPage", "Passkey confirmation requested: " + passkey); - - if (bluetoothPairDialogLoader.active && bluetoothPairDialogLoader.item) { - // Update dialog to confirmation mode - bluetoothPairDialogLoader.item.showPasskeyConfirmation(deviceName, address, "device", passkey); - } - } - } - - Component.onCompleted: { - Logger.info("BluetoothPage", "Initialized"); - // Start scanning if Bluetooth is enabled - if (BluetoothManagerCpp.enabled) { - BluetoothManagerCpp.startScan(); - } - } } diff --git a/apps/settings/pages/CellularPage.qml b/apps/settings/pages/CellularPage.qml index 0248859e3..4f3237fc9 100644 --- a/apps/settings/pages/CellularPage.qml +++ b/apps/settings/pages/CellularPage.qml @@ -68,8 +68,6 @@ SettingsPageTemplate { title: "Data Roaming" subtitle: (typeof ModemManagerCpp !== "undefined" && ModemManagerCpp && ModemManagerCpp.roaming) ? "Currently roaming" : "Use data when traveling" showToggle: true - // ModemManagerCpp exposes roaming state but doesn't currently provide a setter. - // Keep as read-only indicator for now. toggleValue: typeof ModemManagerCpp !== "undefined" && ModemManagerCpp ? ModemManagerCpp.roaming : false enabled: false visible: typeof ModemManagerCpp !== "undefined" && ModemManagerCpp && ModemManagerCpp.available @@ -83,13 +81,11 @@ SettingsPageTemplate { MSettingsListItem { title: "SIM Operator" - // ModemManagerCpp doesn't currently expose a SIM operator string; fall back to network operator name. value: (typeof ModemManagerCpp !== "undefined" && ModemManagerCpp && ModemManagerCpp.operatorName) || "Unknown" } MSettingsListItem { title: "Phone Number" - // Not exposed yet by ModemManagerCpp value: "Not available" } } diff --git a/apps/settings/pages/DefaultAppsPage.qml b/apps/settings/pages/DefaultAppsPage.qml index e73cb07a6..e652b8921 100644 --- a/apps/settings/pages/DefaultAppsPage.qml +++ b/apps/settings/pages/DefaultAppsPage.qml @@ -1,16 +1,15 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Controls import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Modals +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls SettingsPageTemplate { id: defaultAppsPage - pageTitle: "Default Apps" property string pageName: "defaultapps" @@ -18,16 +17,14 @@ SettingsPageTemplate { var eligible = []; for (var i = 0; i < AppModel.rowCount(); i++) { var app = AppModel.getApp(i); - if (app && app.defaultFor && app.defaultFor.indexOf(handler) >= 0) { + if (app && app.defaultFor && app.defaultFor.indexOf(handler) >= 0) eligible.push(app); - } } return eligible; } function getDefaultAppName(handler) { var defaultAppId = SettingsManagerCpp.defaultApps[handler] || ""; - if (!defaultAppId) { var eligible = getAppsForHandler(handler); if (eligible.length > 0) { @@ -40,123 +37,19 @@ SettingsPageTemplate { return "None"; } } - for (var i = 0; i < AppModel.rowCount(); i++) { var app = AppModel.getApp(i); - if (app && app.id === defaultAppId) { + if (app && app.id === defaultAppId) return app.name; - } } return "None"; } - content: Flickable { - contentHeight: contentColumn.height + 40 - clip: true - - Column { - id: contentColumn - width: parent.width - spacing: MSpacing.xl - leftPadding: 24 - rightPadding: 24 - topPadding: 24 - - MSection { - title: "Communication" - width: parent.width - 48 - - MSettingsListItem { - title: "Browser" - value: getDefaultAppName("browser") - showChevron: true - iconName: "globe" - onSettingClicked: browserSheet.visible = true - } - - MSettingsListItem { - title: "Phone" - value: getDefaultAppName("dialer") - showChevron: true - iconName: "phone" - onSettingClicked: dialerSheet.visible = true - } - - MSettingsListItem { - title: "Messaging" - value: getDefaultAppName("messaging") - showChevron: true - iconName: "message-circle" - onSettingClicked: messagingSheet.visible = true - } - - MSettingsListItem { - title: "Email" - value: getDefaultAppName("email") - showChevron: true - iconName: "mail" - onSettingClicked: emailSheet.visible = true - } - } - - MSection { - title: "Media" - width: parent.width - 48 - - MSettingsListItem { - title: "Camera" - value: getDefaultAppName("camera") - showChevron: true - iconName: "camera" - onSettingClicked: cameraSheet.visible = true - } - - MSettingsListItem { - title: "Gallery" - value: getDefaultAppName("gallery") - showChevron: true - iconName: "image" - onSettingClicked: gallerySheet.visible = true - } - - MSettingsListItem { - title: "Music" - value: getDefaultAppName("music") - showChevron: true - iconName: "music" - onSettingClicked: musicSheet.visible = true - } - - MSettingsListItem { - title: "Video" - value: getDefaultAppName("video") - showChevron: true - iconName: "video" - onSettingClicked: videoSheet.visible = true - } - } - - MSection { - title: "Utilities" - width: parent.width - 48 - - MSettingsListItem { - title: "File Manager" - value: getDefaultAppName("files") - showChevron: true - iconName: "folder" - onSettingClicked: filesSheet.visible = true - } - } - - Item { - height: Constants.navBarHeight - } - } - } + pageTitle: "Default Apps" MSheet { id: browserSheet + title: "Choose Browser" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -184,6 +77,7 @@ SettingsPageTemplate { MSheet { id: dialerSheet + title: "Choose Phone App" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -211,6 +105,7 @@ SettingsPageTemplate { MSheet { id: messagingSheet + title: "Choose Messaging App" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -238,6 +133,7 @@ SettingsPageTemplate { MSheet { id: emailSheet + title: "Choose Email App" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -265,6 +161,7 @@ SettingsPageTemplate { MSheet { id: cameraSheet + title: "Choose Camera App" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -292,6 +189,7 @@ SettingsPageTemplate { MSheet { id: gallerySheet + title: "Choose Gallery App" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -319,6 +217,7 @@ SettingsPageTemplate { MSheet { id: musicSheet + title: "Choose Music App" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -346,6 +245,7 @@ SettingsPageTemplate { MSheet { id: videoSheet + title: "Choose Video App" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -373,6 +273,7 @@ SettingsPageTemplate { MSheet { id: filesSheet + title: "Choose File Manager" height: Math.min(600, defaultAppsPage.height * 0.75) @@ -397,4 +298,110 @@ SettingsPageTemplate { } } } + + content: Flickable { + contentHeight: contentColumn.height + 40 + clip: true + + Column { + id: contentColumn + + width: parent.width + spacing: MSpacing.xl + leftPadding: 24 + rightPadding: 24 + topPadding: 24 + + MSection { + title: "Communication" + width: parent.width - 48 + + MSettingsListItem { + title: "Browser" + value: getDefaultAppName("browser") + showChevron: true + iconName: "globe" + onSettingClicked: browserSheet.visible = true + } + + MSettingsListItem { + title: "Phone" + value: getDefaultAppName("dialer") + showChevron: true + iconName: "phone" + onSettingClicked: dialerSheet.visible = true + } + + MSettingsListItem { + title: "Messaging" + value: getDefaultAppName("messaging") + showChevron: true + iconName: "message-circle" + onSettingClicked: messagingSheet.visible = true + } + + MSettingsListItem { + title: "Email" + value: getDefaultAppName("email") + showChevron: true + iconName: "mail" + onSettingClicked: emailSheet.visible = true + } + } + + MSection { + title: "Media" + width: parent.width - 48 + + MSettingsListItem { + title: "Camera" + value: getDefaultAppName("camera") + showChevron: true + iconName: "camera" + onSettingClicked: cameraSheet.visible = true + } + + MSettingsListItem { + title: "Gallery" + value: getDefaultAppName("gallery") + showChevron: true + iconName: "image" + onSettingClicked: gallerySheet.visible = true + } + + MSettingsListItem { + title: "Music" + value: getDefaultAppName("music") + showChevron: true + iconName: "music" + onSettingClicked: musicSheet.visible = true + } + + MSettingsListItem { + title: "Video" + value: getDefaultAppName("video") + showChevron: true + iconName: "video" + onSettingClicked: videoSheet.visible = true + } + } + + MSection { + title: "Utilities" + width: parent.width - 48 + + MSettingsListItem { + title: "File Manager" + value: getDefaultAppName("files") + showChevron: true + iconName: "folder" + onSettingClicked: filesSheet.visible = true + } + } + + Item { + height: Constants.navBarHeight + } + } + } } diff --git a/apps/settings/pages/HiddenAppsPage.qml b/apps/settings/pages/HiddenAppsPage.qml index c5495eeed..1a29277bd 100644 --- a/apps/settings/pages/HiddenAppsPage.qml +++ b/apps/settings/pages/HiddenAppsPage.qml @@ -1,24 +1,26 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Controls import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Core +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls SettingsPageTemplate { id: hiddenAppsPage - pageTitle: "Hidden Apps" property string pageName: "hiddenapps" + pageTitle: "Hidden Apps" + content: Flickable { contentHeight: contentColumn.height + 40 clip: true Column { id: contentColumn + width: parent.width spacing: MSpacing.xl leftPadding: 24 @@ -51,7 +53,6 @@ SettingsPageTemplate { onToggleChanged: value => { var hiddenList = SettingsManagerCpp.hiddenApps; var index = hiddenList.indexOf(model.id); - if (value && index < 0) { hiddenList.push(model.id); SettingsManagerCpp.hiddenApps = hiddenList; diff --git a/apps/settings/pages/KeyboardPage.qml b/apps/settings/pages/KeyboardPage.qml index f4489922d..9eee9b832 100644 --- a/apps/settings/pages/KeyboardPage.qml +++ b/apps/settings/pages/KeyboardPage.qml @@ -1,3 +1,4 @@ +import MarathonApp.Settings import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core @@ -9,9 +10,36 @@ Page { id: keyboardPage property string pageName: "keyboard" + property var languageNames: ({ + "en_US": "English (US)", + "fr_FR": "Français", + "de_DE": "Deutsch", + "es_ES": "Español", + "pt_BR": "Português", + "it_IT": "Italiano", + "ru_RU": "Русский", + "ar_SA": "العربية", + "zh_CN": "中文", + "ja_JP": "日本語" + }) signal navigateBack + function keyboardValue(key, fallbackValue) { + if (KeyboardSettingsStore && KeyboardSettingsStore[key] !== undefined) + return KeyboardSettingsStore[key]; + return fallbackValue; + } + + function setKeyboardValue(key, value) { + if (KeyboardSettingsStore && KeyboardSettingsStore[key] !== undefined) + KeyboardSettingsStore[key] = value; + } + + function languageNameFor(languageId) { + return languageNames[languageId] || languageId; + } + Component.onCompleted: { Logger.info("KeyboardPage", "Initialized"); } @@ -62,15 +90,15 @@ Page { width: parent.width - 48 Repeater { - model: KeyboardSettingsStore.availableLanguages + model: keyboardValue("availableLanguages", []) MSettingsListItem { - title: KeyboardSettingsStore.getLanguageName(modelData) + title: languageNameFor(modelData) subtitle: modelData - iconName: KeyboardSettingsStore.currentLanguage === modelData ? "check" : "" + iconName: keyboardValue("currentLanguage", "") === modelData ? "check" : "" showChevron: false onSettingClicked: { - KeyboardSettingsStore.currentLanguage = modelData; + setKeyboardValue("currentLanguage", modelData); } Rectangle { @@ -80,8 +108,8 @@ Page { width: 32 height: 32 radius: 16 - color: KeyboardSettingsStore.currentLanguage === modelData ? MColors.accentBright : "transparent" - visible: KeyboardSettingsStore.currentLanguage === modelData + color: keyboardValue("currentLanguage", "") === modelData ? MColors.accentBright : "transparent" + visible: keyboardValue("currentLanguage", "") === modelData Text { anchors.centerIn: parent @@ -105,9 +133,9 @@ Page { subtitle: "Automatically correct misspelled words" iconName: "spell-check" showToggle: true - toggleValue: KeyboardSettingsStore.autoCorrectEnabled + toggleValue: keyboardValue("autoCorrectEnabled", false) onToggleChanged: value => { - KeyboardSettingsStore.autoCorrectEnabled = value; + setKeyboardValue("autoCorrectEnabled", value); } } @@ -116,9 +144,9 @@ Page { subtitle: "Capitalize first letter of sentences" iconName: "arrow-up" showToggle: true - toggleValue: KeyboardSettingsStore.autoCapitalizeEnabled + toggleValue: keyboardValue("autoCapitalizeEnabled", false) onToggleChanged: value => { - KeyboardSettingsStore.autoCapitalizeEnabled = value; + setKeyboardValue("autoCapitalizeEnabled", value); } } @@ -127,9 +155,9 @@ Page { subtitle: "Vibrate on key press" iconName: "vibrate" showToggle: true - toggleValue: KeyboardSettingsStore.hapticFeedbackEnabled + toggleValue: keyboardValue("hapticFeedbackEnabled", false) onToggleChanged: value => { - KeyboardSettingsStore.hapticFeedbackEnabled = value; + setKeyboardValue("hapticFeedbackEnabled", value); } } @@ -138,9 +166,9 @@ Page { subtitle: "Show word suggestions while typing" iconName: "message-square" showToggle: true - toggleValue: KeyboardSettingsStore.predictionsEnabled + toggleValue: keyboardValue("predictionsEnabled", false) onToggleChanged: value => { - KeyboardSettingsStore.predictionsEnabled = value; + setKeyboardValue("predictionsEnabled", value); } } } diff --git a/apps/settings/pages/NotificationsPage.qml b/apps/settings/pages/NotificationsPage.qml index 5a450feff..2a1905bad 100644 --- a/apps/settings/pages/NotificationsPage.qml +++ b/apps/settings/pages/NotificationsPage.qml @@ -1,21 +1,23 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: notificationsPage - pageTitle: "Notifications" property string pageName: "notifications" + pageTitle: "Notifications" + content: Flickable { contentHeight: notificationsContent.height + 40 clip: true Column { id: notificationsContent + width: parent.width spacing: MSpacing.xl leftPadding: 24 diff --git a/apps/settings/pages/QuickSettingsPage.qml b/apps/settings/pages/QuickSettingsPage.qml index 1f66272d2..036cf3838 100644 --- a/apps/settings/pages/QuickSettingsPage.qml +++ b/apps/settings/pages/QuickSettingsPage.qml @@ -1,19 +1,18 @@ -import QtQuick import MarathonApp.Settings -import QtQuick.Controls import MarathonApp.Settings import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Controls SettingsPageTemplate { id: quickSettingsPage - pageTitle: "Quick Settings" property string pageName: "quicksettings" - // navigateBack signal already provided by SettingsPageTemplate + pageTitle: "Quick Settings" content: Flickable { contentHeight: quickSettingsContent.height + 40 @@ -21,6 +20,7 @@ SettingsPageTemplate { Column { id: quickSettingsContent + width: parent.width spacing: MSpacing.xl leftPadding: 24 @@ -36,115 +36,115 @@ SettingsPageTemplate { width: parent.width spacing: 0 - // Define all possible tiles with descriptions Repeater { model: [ { - id: "wifi", - label: "Wi-Fi", - icon: "wifi", - desc: "Toggle wireless network" + "id": "wifi", + "label": "Wi-Fi", + "icon": "wifi", + "desc": "Toggle wireless network" }, { - id: "bluetooth", - label: "Bluetooth", - icon: "bluetooth", - desc: "Connect to devices" + "id": "bluetooth", + "label": "Bluetooth", + "icon": "bluetooth", + "desc": "Connect to devices" }, { - id: "flight", - label: "Flight Mode", - icon: "plane", - desc: "Disable all radios" + "id": "flight", + "label": "Flight Mode", + "icon": "plane", + "desc": "Disable all radios" }, { - id: "cellular", - label: "Mobile Network", - icon: "signal", - desc: "Toggle cellular data" + "id": "cellular", + "label": "Mobile Network", + "icon": "signal", + "desc": "Toggle cellular data" }, { - id: "rotation", - label: "Rotation Lock", - icon: "rotate-ccw", - desc: "Lock screen orientation" + "id": "rotation", + "label": "Rotation Lock", + "icon": "rotate-ccw", + "desc": "Lock screen orientation" }, { - id: "autobrightness", - label: "Auto-brightness", - icon: "sun-moon", - desc: "Automatic brightness adjustment" + "id": "autobrightness", + "label": "Auto-brightness", + "icon": "sun-moon", + "desc": "Automatic brightness adjustment" }, { - id: "location", - label: "Location", - icon: "map-pin", - desc: "GPS and location services" + "id": "location", + "label": "Location", + "icon": "map-pin", + "desc": "GPS and location services" }, { - id: "hotspot", - label: "Hotspot", - icon: "wifi-tethering", - desc: "Share internet connection" + "id": "hotspot", + "label": "Hotspot", + "icon": "wifi-tethering", + "desc": "Share internet connection" }, { - id: "vibration", - label: "Vibration", - icon: "vibrate", - desc: "Haptic feedback" + "id": "vibration", + "label": "Vibration", + "icon": "vibrate", + "desc": "Haptic feedback" }, { - id: "nightlight", - label: "Night Light", - icon: "moon", - desc: "Reduce blue light" + "id": "nightlight", + "label": "Night Light", + "icon": "moon", + "desc": "Reduce blue light" }, { - id: "torch", - label: "Torch", - icon: "flashlight", - desc: "Toggle flashlight" + "id": "torch", + "label": "Torch", + "icon": "flashlight", + "desc": "Toggle flashlight" }, { - id: "notifications", - label: "DND Mode", - icon: "bell", - desc: "Do Not Disturb" + "id": "notifications", + "label": "DND Mode", + "icon": "bell", + "desc": "Do Not Disturb" }, { - id: "battery", - label: "Battery Saver", - icon: "battery", - desc: "Low power mode" + "id": "battery", + "label": "Battery Saver", + "icon": "battery", + "desc": "Low power mode" }, { - id: "screenshot", - label: "Screenshot", - icon: "camera", - desc: "Capture screen" + "id": "screenshot", + "label": "Screenshot", + "icon": "camera", + "desc": "Capture screen" }, { - id: "settings", - label: "Settings", - icon: "settings", - desc: "Open Settings app" + "id": "settings", + "label": "Settings", + "icon": "settings", + "desc": "Open Settings app" }, { - id: "lock", - label: "Lock Device", - icon: "lock", - desc: "Lock the screen" + "id": "lock", + "label": "Lock Device", + "icon": "lock", + "desc": "Lock the screen" }, { - id: "power", - label: "Power Menu", - icon: "power", - desc: "Show power options" + "id": "power", + "label": "Power Menu", + "icon": "power", + "desc": "Show power options" } ] delegate: MSettingsListItem { required property var modelData + title: modelData.label subtitle: modelData.desc showToggle: true @@ -152,14 +152,11 @@ SettingsPageTemplate { onToggleChanged: value => { var tiles = SettingsManagerCpp.enabledQuickSettingsTiles; var idx = tiles.indexOf(modelData.id); - if (value && idx === -1) { - // Enable tile tiles.push(modelData.id); SettingsManagerCpp.enabledQuickSettingsTiles = tiles; Logger.info("QuickSettings", "Enabled tile: " + modelData.id); } else if (!value && idx !== -1) { - // Disable tile tiles.splice(idx, 1); SettingsManagerCpp.enabledQuickSettingsTiles = tiles; Logger.info("QuickSettings", "Disabled tile: " + modelData.id); diff --git a/apps/settings/pages/ScalePage.qml b/apps/settings/pages/ScalePage.qml index 29aceddb5..3573cda12 100644 --- a/apps/settings/pages/ScalePage.qml +++ b/apps/settings/pages/ScalePage.qml @@ -1,15 +1,16 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers +import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: scalePage - pageTitle: "UI Scale" property string pageName: "scale" + pageTitle: "UI Scale" + content: Flickable { contentHeight: scaleContent.height + 40 clip: true @@ -17,6 +18,7 @@ SettingsPageTemplate { Column { id: scaleContent + width: parent.width spacing: MSpacing.xl leftPadding: MSpacing.lg @@ -106,8 +108,8 @@ SettingsPageTemplate { width: parent.width height: Constants.touchTargetMedium radius: Constants.borderRadiusSmall - color: Constants.userScaleFactor === 1.0 ? Qt.rgba(20, 184, 166, 0.08) : "transparent" - border.width: Constants.userScaleFactor === 1.0 ? 1 : 0 + color: Constants.userScaleFactor === 1 ? Qt.rgba(20, 184, 166, 0.08) : "transparent" + border.width: Constants.userScaleFactor === 1 ? 1 : 0 border.color: Qt.rgba(20, 184, 166, 0.3) Row { @@ -119,13 +121,13 @@ SettingsPageTemplate { width: Math.round(28 * Constants.userScaleFactor) height: Math.round(28 * Constants.userScaleFactor) radius: Math.round(14 * Constants.userScaleFactor) - color: Constants.userScaleFactor === 1.0 ? MColors.marathonTeal : "transparent" + color: Constants.userScaleFactor === 1 ? MColors.marathonTeal : "transparent" border.width: Math.round(2 * Constants.userScaleFactor) - border.color: Constants.userScaleFactor === 1.0 ? MColors.marathonTeal : MColors.textSecondary + border.color: Constants.userScaleFactor === 1 ? MColors.marathonTeal : MColors.textSecondary anchors.verticalCenter: parent.verticalCenter Rectangle { - visible: Constants.userScaleFactor === 1.0 + visible: Constants.userScaleFactor === 1 width: Math.round(12 * Constants.userScaleFactor) height: Math.round(12 * Constants.userScaleFactor) radius: Math.round(6 * Constants.userScaleFactor) @@ -158,8 +160,8 @@ SettingsPageTemplate { MouseArea { anchors.fill: parent onClicked: { - Constants.userScaleFactor = 1.0; - SettingsManagerCpp.userScaleFactor = 1.0; + Constants.userScaleFactor = 1; + SettingsManagerCpp.userScaleFactor = 1; } } } diff --git a/apps/settings/pages/SecurityPage.qml b/apps/settings/pages/SecurityPage.qml index d34cc653d..bd3bd8091 100644 --- a/apps/settings/pages/SecurityPage.qml +++ b/apps/settings/pages/SecurityPage.qml @@ -1,163 +1,29 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Controls import MarathonUI.Core import MarathonUI.Modals +import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: securityPage - pageTitle: "Security" property string pageName: "security" property bool showQuickPINDialog: false property bool showRemovePINDialog: false + pageTitle: "Security" Component.onCompleted: { Logger.info("SecurityPage", "Initialized"); } - content: Flickable { - contentHeight: securityContent.height + 40 - clip: true - - Column { - id: securityContent - width: parent.width - spacing: MSpacing.xl - leftPadding: MSpacing.lg - rightPadding: MSpacing.lg - topPadding: MSpacing.lg - - // Authentication Method Section - MSection { - title: "Authentication" - width: parent.width - parent.leftPadding - parent.rightPadding - - MSettingsListItem { - title: "Lock Method" - subtitle: { - if (!SecurityManagerCpp) - return "System Password"; - - switch (SecurityManagerCpp.authMode) { - case 0: - return "System Password (PAM)"; - case 1: - return "Quick PIN"; - case 2: - return "Fingerprint Only"; - case 3: - return "Fingerprint + PIN"; - default: - return "Unknown"; - } - } - iconName: "lock" - enabled: false - } - - MSettingsListItem { - title: SecurityManagerCpp && SecurityManagerCpp.hasQuickPIN ? "Change Quick PIN" : "Set Quick PIN" - subtitle: SecurityManagerCpp && SecurityManagerCpp.hasQuickPIN ? "Update your convenience PIN" : "Set a quick 4-6 digit PIN" - iconName: "hash" - onSettingClicked: showQuickPINDialog = true - } - - MSettingsListItem { - title: "Remove Quick PIN" - subtitle: "Use system password only" - iconName: "trash-2" - visible: SecurityManagerCpp && SecurityManagerCpp.hasQuickPIN - onSettingClicked: showRemovePINDialog = true - } - } - - // Biometric Section - MSection { - title: "Biometric" - width: parent.width - parent.leftPadding - parent.rightPadding - - MSettingsListItem { - title: "Fingerprint" - subtitle: SecurityManagerCpp && SecurityManagerCpp.fingerprintAvailable ? "Enrolled and ready" : "Not enrolled" - iconName: "fingerprint" - onSettingClicked: { - // Launch fprintd enrollment - Qt.openUrlExternally("fprintd://enroll"); - } - } - } - - // Security Status Section - MSection { - title: "Security Status" - width: parent.width - parent.leftPadding - parent.rightPadding - - MSettingsListItem { - title: "Failed Attempts" - subtitle: SecurityManagerCpp ? SecurityManagerCpp.failedAttempts.toString() : "0" - iconName: "shield-alert" - enabled: false - } - - MSettingsListItem { - title: "Account Status" - subtitle: { - if (!SecurityManagerCpp) - return "Active"; - if (SecurityManagerCpp.isLockedOut) { - var secs = SecurityManagerCpp.lockoutSecondsRemaining; - return "Locked (" + secs + "s remaining)"; - } - return "Active"; - } - iconName: SecurityManagerCpp && SecurityManagerCpp.isLockedOut ? "lock" : "check-circle" - enabled: false - } - } - - // Information Section - MSection { - title: "How It Works" - width: parent.width - parent.leftPadding - parent.rightPadding - - Text { - width: parent.width - text: "Marathon uses your system password (PAM) for secure authentication. " + "Quick PIN is an optional convenience feature stored encrypted. " + "Fingerprint authentication is provided by fprintd." - font.pixelSize: MTypography.sizeSmall - font.family: MTypography.fontFamily - color: MColors.textSecondary - wrapMode: Text.WordWrap - topPadding: MSpacing.sm - leftPadding: MSpacing.md - rightPadding: MSpacing.md - } - - Text { - width: parent.width - text: "Security features:\n" + "• PAM-based authentication\n" + "• Rate limiting (5 attempts)\n" + "• Exponential lockout\n" + "• Audit logging" - font.pixelSize: MTypography.sizeXSmall - font.family: MTypography.fontFamily - color: MColors.textTertiary - wrapMode: Text.WordWrap - topPadding: MSpacing.sm - bottomPadding: MSpacing.sm - leftPadding: MSpacing.md - rightPadding: MSpacing.md - } - } - } - } - - // Quick PIN Setup Dialog MModal { id: quickPINModal + showing: showQuickPINDialog title: SecurityManagerCpp && SecurityManagerCpp.hasQuickPIN ? "Change Quick PIN" : "Set Quick PIN" - onClosed: { showQuickPINDialog = false; } @@ -175,7 +41,6 @@ SettingsPageTemplate { wrapMode: Text.WordWrap } - // PIN input Rectangle { width: parent.width height: 48 @@ -186,6 +51,7 @@ SettingsPageTemplate { TextInput { id: newPINField + anchors.fill: parent anchors.leftMargin: MSpacing.md anchors.rightMargin: MSpacing.md @@ -207,7 +73,6 @@ SettingsPageTemplate { } } - // System password input Rectangle { width: parent.width height: 48 @@ -218,6 +83,7 @@ SettingsPageTemplate { TextInput { id: systemPasswordField + anchors.fill: parent anchors.leftMargin: MSpacing.md anchors.rightMargin: MSpacing.md @@ -240,6 +106,7 @@ SettingsPageTemplate { Text { id: pinErrorText + width: parent.width text: "" font.pixelSize: MTypography.sizeSmall @@ -271,16 +138,11 @@ SettingsPageTemplate { pinErrorText.text = "PIN must be 4-6 digits"; return; } - if (systemPasswordField.text.trim().length === 0) { pinErrorText.text = "System password required"; return; } - - // Set Quick PIN via SecurityManager SecurityManagerCpp.setQuickPIN(newPINField.text, systemPasswordField.text); - - // Dialog will close on success via signal showQuickPINDialog = false; newPINField.text = ""; systemPasswordField.text = ""; @@ -291,30 +153,23 @@ SettingsPageTemplate { } } - // Remove PIN Confirmation Dialog MConfirmDialog { id: removePINDialog + showing: showRemovePINDialog title: "Remove Quick PIN?" message: "You will need to enter your system password to unlock. This action requires your system password." confirmText: "Remove" cancelText: "Cancel" - onConfirmed: { - // TODO: Need to prompt for system password - // For now, just show message showRemovePINDialog = false; } - onCancelled: { showRemovePINDialog = false; } } - // SecurityManager signal handlers Connections { - target: SecurityManagerCpp - function onQuickPINChanged() { Logger.info("SecurityPage", "Quick PIN changed"); } @@ -322,5 +177,137 @@ SettingsPageTemplate { function onAuthenticationFailed(reason) { pinErrorText.text = reason; } + + target: SecurityManagerCpp + } + + content: Flickable { + contentHeight: securityContent.height + 40 + clip: true + + Column { + id: securityContent + + width: parent.width + spacing: MSpacing.xl + leftPadding: MSpacing.lg + rightPadding: MSpacing.lg + topPadding: MSpacing.lg + + MSection { + title: "Authentication" + width: parent.width - parent.leftPadding - parent.rightPadding + + MSettingsListItem { + title: "Lock Method" + subtitle: { + if (!SecurityManagerCpp) + return "System Password"; + + switch (SecurityManagerCpp.authMode) { + case 0: + return "System Password (PAM)"; + case 1: + return "Quick PIN"; + case 2: + return "Fingerprint Only"; + case 3: + return "Fingerprint + PIN"; + default: + return "Unknown"; + } + } + iconName: "lock" + enabled: false + } + + MSettingsListItem { + title: SecurityManagerCpp && SecurityManagerCpp.hasQuickPIN ? "Change Quick PIN" : "Set Quick PIN" + subtitle: SecurityManagerCpp && SecurityManagerCpp.hasQuickPIN ? "Update your convenience PIN" : "Set a quick 4-6 digit PIN" + iconName: "hash" + onSettingClicked: showQuickPINDialog = true + } + + MSettingsListItem { + title: "Remove Quick PIN" + subtitle: "Use system password only" + iconName: "trash-2" + visible: SecurityManagerCpp && SecurityManagerCpp.hasQuickPIN + onSettingClicked: showRemovePINDialog = true + } + } + + MSection { + title: "Biometric" + width: parent.width - parent.leftPadding - parent.rightPadding + + MSettingsListItem { + title: "Fingerprint" + subtitle: SecurityManagerCpp && SecurityManagerCpp.fingerprintAvailable ? "Enrolled and ready" : "Not enrolled" + iconName: "fingerprint" + onSettingClicked: { + Qt.openUrlExternally("fprintd://enroll"); + } + } + } + + MSection { + title: "Security Status" + width: parent.width - parent.leftPadding - parent.rightPadding + + MSettingsListItem { + title: "Failed Attempts" + subtitle: SecurityManagerCpp ? SecurityManagerCpp.failedAttempts.toString() : "0" + iconName: "shield-alert" + enabled: false + } + + MSettingsListItem { + title: "Account Status" + subtitle: { + if (!SecurityManagerCpp) + return "Active"; + + if (SecurityManagerCpp.isLockedOut) { + var secs = SecurityManagerCpp.lockoutSecondsRemaining; + return "Locked (" + secs + "s remaining)"; + } + return "Active"; + } + iconName: SecurityManagerCpp && SecurityManagerCpp.isLockedOut ? "lock" : "check-circle" + enabled: false + } + } + + MSection { + title: "How It Works" + width: parent.width - parent.leftPadding - parent.rightPadding + + Text { + width: parent.width + text: "Marathon uses your system password (PAM) for secure authentication. " + "Quick PIN is an optional convenience feature stored encrypted. " + "Fingerprint authentication is provided by fprintd." + font.pixelSize: MTypography.sizeSmall + font.family: MTypography.fontFamily + color: MColors.textSecondary + wrapMode: Text.WordWrap + topPadding: MSpacing.sm + leftPadding: MSpacing.md + rightPadding: MSpacing.md + } + + Text { + width: parent.width + text: "Security features:\n" + "• PAM-based authentication\n" + "• Rate limiting (5 attempts)\n" + "• Exponential lockout\n" + "• Audit logging" + font.pixelSize: MTypography.sizeXSmall + font.family: MTypography.fontFamily + color: MColors.textTertiary + wrapMode: Text.WordWrap + topPadding: MSpacing.sm + bottomPadding: MSpacing.sm + leftPadding: MSpacing.md + rightPadding: MSpacing.md + } + } + } } } diff --git a/apps/settings/pages/SettingsMainPage.qml b/apps/settings/pages/SettingsMainPage.qml index e4e0ab01b..f74e30e53 100644 --- a/apps/settings/pages/SettingsMainPage.qml +++ b/apps/settings/pages/SettingsMainPage.qml @@ -38,7 +38,6 @@ Page { topPadding: 24 bottomPadding: 24 - // Page title Text { text: "Settings" color: MColors.textPrimary @@ -47,7 +46,6 @@ Page { font.family: MTypography.fontFamily } - // Network & Connectivity MSection { title: "Network & Connectivity" subtitle: "Manage your network connections" @@ -95,7 +93,6 @@ Page { } } - // Display & Brightness MSection { title: "Display & Brightness" subtitle: "Customize your screen settings" @@ -112,7 +109,6 @@ Page { } } - // Sound & Notifications MSection { title: "Sound & Notifications" subtitle: "Manage audio and notification settings" @@ -150,7 +146,6 @@ Page { } } - // Storage & Battery MSection { title: "Storage & Battery" subtitle: "Manage device resources" @@ -213,7 +208,6 @@ Page { } } - // Security & Privacy MSection { title: "Security & Privacy" subtitle: "Lock screen, authentication, and security" @@ -230,7 +224,6 @@ Page { } } - // Apps & Filters MSection { title: "Customization" subtitle: "Personalize your Marathon experience" @@ -341,7 +334,6 @@ Page { } } - // Swipe down to close gesture (BB10 style) MouseArea { property real startY: 0 property bool isDragging: false diff --git a/apps/settings/pages/SoundPage.qml b/apps/settings/pages/SoundPage.qml index 57dbec9c1..51821b430 100644 --- a/apps/settings/pages/SoundPage.qml +++ b/apps/settings/pages/SoundPage.qml @@ -1,23 +1,67 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Core import MarathonUI.Containers import MarathonUI.Controls +import MarathonUI.Core import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: soundPage - pageTitle: "Sound" property string pageName: "sound" + pageTitle: "Sound" + + Component { + id: ringtonePickerComponent + + SoundPickerPage { + soundType: "ringtone" + currentSound: AudioManager.currentRingtone + availableSounds: AudioManager.availableRingtones + onSoundSelected: path => { + AudioManager.setRingtone(path); + } + onNavigateBack: soundPage.parent.pop() + } + } + + Component { + id: notificationSoundPickerComponent + + SoundPickerPage { + soundType: "notification" + currentSound: AudioManager.currentNotificationSound + availableSounds: AudioManager.availableNotificationSounds + onSoundSelected: path => { + AudioManager.setNotificationSound(path); + } + onNavigateBack: soundPage.parent.pop() + } + } + + Component { + id: alarmSoundPickerComponent + + SoundPickerPage { + soundType: "alarm" + currentSound: AudioManager.currentAlarmSound + availableSounds: AudioManager.availableAlarmSounds + onSoundSelected: path => { + AudioManager.setAlarmSound(path); + } + onNavigateBack: soundPage.parent.pop() + } + } + content: Flickable { contentHeight: soundContent.height + 40 clip: true Column { id: soundContent + width: parent.width spacing: MSpacing.xl leftPadding: 24 @@ -142,43 +186,4 @@ SettingsPageTemplate { } } } - - Component { - id: ringtonePickerComponent - SoundPickerPage { - soundType: "ringtone" - currentSound: AudioManager.currentRingtone - availableSounds: AudioManager.availableRingtones - onSoundSelected: path => { - AudioManager.setRingtone(path); - } - onNavigateBack: soundPage.parent.pop() - } - } - - Component { - id: notificationSoundPickerComponent - SoundPickerPage { - soundType: "notification" - currentSound: AudioManager.currentNotificationSound - availableSounds: AudioManager.availableNotificationSounds - onSoundSelected: path => { - AudioManager.setNotificationSound(path); - } - onNavigateBack: soundPage.parent.pop() - } - } - - Component { - id: alarmSoundPickerComponent - SoundPickerPage { - soundType: "alarm" - currentSound: AudioManager.currentAlarmSound - availableSounds: AudioManager.availableAlarmSounds - onSoundSelected: path => { - AudioManager.setAlarmSound(path); - } - onNavigateBack: soundPage.parent.pop() - } - } } diff --git a/apps/settings/pages/SoundPickerPage.qml b/apps/settings/pages/SoundPickerPage.qml index 03a5a3f25..8c8f0de41 100644 --- a/apps/settings/pages/SoundPickerPage.qml +++ b/apps/settings/pages/SoundPickerPage.qml @@ -1,9 +1,9 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers import MarathonUI.Core +import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: soundPickerPage @@ -11,21 +11,23 @@ SettingsPageTemplate { property string soundType: "ringtone" property string currentSound: "" property var availableSounds: [] + property string pageName: soundType signal soundSelected(string path) pageTitle: { if (soundType === "ringtone") return "Ringtone"; + if (soundType === "notification") return "Notification Sound"; + if (soundType === "alarm") return "Alarm Sound"; + return "Sound"; } - property string pageName: soundType - content: Flickable { contentHeight: soundContent.height + MSpacing.xl * 3 clip: true @@ -33,6 +35,7 @@ SettingsPageTemplate { Column { id: soundContent + width: parent.width spacing: MSpacing.lg leftPadding: MSpacing.lg @@ -84,6 +87,7 @@ SettingsPageTemplate { Icon { id: soundIcon + anchors.left: parent.left anchors.leftMargin: MSpacing.md anchors.verticalCenter: parent.verticalCenter @@ -110,6 +114,7 @@ SettingsPageTemplate { Rectangle { id: checkBox + anchors.right: parent.right anchors.rightMargin: MSpacing.md anchors.verticalCenter: parent.verticalCenter @@ -139,22 +144,18 @@ SettingsPageTemplate { MouseArea { id: soundMouseArea + anchors.fill: parent onClicked: { Logger.info("SoundPickerPage", "Selected sound: " + modelData); - - // Save selection soundPickerPage.currentSound = modelData; soundPickerPage.soundSelected(modelData); - - // Preview the sound using dedicated preview player - if (soundPickerPage.soundType === "ringtone") { + if (soundPickerPage.soundType === "ringtone") AudioManager.previewRingtone(modelData); - } else if (soundPickerPage.soundType === "notification") { + else if (soundPickerPage.soundType === "notification") AudioManager.previewNotificationSound(modelData); - } else if (soundPickerPage.soundType === "alarm") { + else if (soundPickerPage.soundType === "alarm") AudioManager.previewAlarmSound(modelData); - } } } } diff --git a/apps/settings/pages/StoragePage.qml b/apps/settings/pages/StoragePage.qml index 7125424f8..d42354f04 100644 --- a/apps/settings/pages/StoragePage.qml +++ b/apps/settings/pages/StoragePage.qml @@ -1,25 +1,27 @@ -import QtQuick import MarathonApp.Settings import MarathonOS.Shell -import MarathonUI.Theme import MarathonUI.Containers +import MarathonUI.Theme +import QtQuick SettingsPageTemplate { id: storagePage + + property string pageName: "storage" + pageTitle: "Storage" StorageInfo { id: storageInfo } - property string pageName: "storage" - content: Flickable { contentHeight: storageContent.height + 40 clip: true Column { id: storageContent + width: parent.width spacing: MSpacing.xl leftPadding: 24 @@ -64,10 +66,12 @@ SettingsPageTemplate { radius: parent.radius color: { if (storageInfo.usedPercentage > 0.9) - return Qt.rgba(255, 59, 48, 0.8); // Red when >90% + return Qt.rgba(255, 59, 48, 0.8); + if (storageInfo.usedPercentage > 0.75) - return Qt.rgba(255, 149, 0, 0.8); // Orange when >75% - return Qt.rgba(20, 184, 166, 0.8); // Teal when <75% + return Qt.rgba(255, 149, 0, 0.8); + + return Qt.rgba(20, 184, 166, 0.8); } } } diff --git a/apps/settings/pages/WiFiPage.qml b/apps/settings/pages/WiFiPage.qml index c697fa83f..e1c12a898 100644 --- a/apps/settings/pages/WiFiPage.qml +++ b/apps/settings/pages/WiFiPage.qml @@ -1,5 +1,4 @@ import MarathonApp.Settings -import MarathonApp.Settings import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core @@ -12,11 +11,12 @@ SettingsPageTemplate { id: wifiPage property string pageName: "wifi" + property WiFiPasswordDialog activePasswordDialog: null + property var pendingPasswordRequest: null pageTitle: "WiFi" Component.onCompleted: { Logger.info("WiFiPage", "Initialized"); - // Scan for networks on page load if WiFi is on if (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp && NetworkManagerCpp.wifiEnabled) NetworkManagerCpp.scanWifi(); } @@ -70,26 +70,44 @@ SettingsPageTemplate { } } - // WiFi password dialog loader (using shell component) Loader { id: wifiPasswordDialogLoader function show(ssid, strength, security, secured) { active = true; - if (item) - item.show(ssid, strength, security, secured); + wifiPage.pendingPasswordRequest = { + "ssid": ssid, + "strength": strength, + "security": security, + "secured": secured + }; + if (wifiPage.activePasswordDialog) { + wifiPage.activePasswordDialog.show(ssid, strength, security, secured); + wifiPage.pendingPasswordRequest = null; + } } anchors.fill: parent active: false z: 1000 + onLoaded: { + wifiPage.activePasswordDialog = item; + if (wifiPage.pendingPasswordRequest) { + var request = wifiPage.pendingPasswordRequest; + wifiPage.pendingPasswordRequest = null; + wifiPage.activePasswordDialog.show(request.ssid, request.strength, request.security, request.secured); + } + } + onActiveChanged: { + if (!active) + wifiPage.activePasswordDialog = null; + } sourceComponent: Component { WiFiPasswordDialog { id: passwordDialog anchors.fill: parent - // Use direct signal handlers instead of .connect() onConnectRequested: (ssid, password) => { Logger.info("WiFiPage", "Attempting WiFi connection to:", ssid); if (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) @@ -102,21 +120,19 @@ SettingsPageTemplate { } } - // Wire NetworkManagerCpp signals to password dialog Connections { function onConnectionSuccess() { Logger.info("WiFiPage", "WiFi connection successful!"); - if (wifiPasswordDialogLoader.active && wifiPasswordDialogLoader.item) { - wifiPasswordDialogLoader.item.hide(); - wifiPasswordDialogLoader.active = false; - } + if (wifiPage.activePasswordDialog) + wifiPage.activePasswordDialog.hide(); + wifiPasswordDialogLoader.active = false; HapticService.medium(); } function onConnectionFailed(message) { Logger.warn("WiFiPage", "WiFi connection failed: " + message); - if (wifiPasswordDialogLoader.active && wifiPasswordDialogLoader.item) - wifiPasswordDialogLoader.item.showError(message); + if (wifiPage.activePasswordDialog) + wifiPage.activePasswordDialog.showError(message); } target: typeof NetworkManagerCpp !== "undefined" ? NetworkManagerCpp : null @@ -136,7 +152,6 @@ SettingsPageTemplate { rightPadding: 24 topPadding: 24 - // WiFi toggle MSettingsListItem { width: parent.width - 48 title: "WiFi" @@ -145,8 +160,6 @@ SettingsPageTemplate { showToggle: true toggleValue: (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) ? NetworkManagerCpp.wifiEnabled : false onToggleChanged: { - // Start scanning when WiFi is turned on - if (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) NetworkManagerCpp.toggleWifi(); @@ -158,7 +171,6 @@ SettingsPageTemplate { } } - // Current network (if connected) MSection { title: "Current Network" width: parent.width - 48 @@ -173,16 +185,11 @@ SettingsPageTemplate { border.color: Qt.rgba(20, 184, 166, 0.3) Row { - // Spacer - anchors.fill: parent anchors.margins: 16 spacing: MSpacing.md Icon { - // 3-4 bars (excellent) - - // Use proper signal bar icons based on current connection strength name: { var strength = (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) ? NetworkManagerCpp.wifiSignalStrength : 0; if (strength === 0) @@ -191,11 +198,9 @@ SettingsPageTemplate { if (strength <= 33) return "wifi-low"; - // 1-2 bars (weak) if (strength <= 66) return "wifi"; - // 2-3 bars (good) return "wifi-high"; } size: 28 @@ -250,18 +255,16 @@ SettingsPageTemplate { } } - // Available networks MSection { title: (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp && NetworkManagerCpp.wifiEnabled) ? "Available Networks" : "Turn on WiFi to see networks" width: parent.width - 48 visible: (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) ? NetworkManagerCpp.wifiEnabled : false - // Scanning indicator Row { width: parent.width height: 48 spacing: MSpacing.md - visible: false // NetworkManagerCpp doesn't currently expose scanning state + visible: false BusyIndicator { running: parent.visible @@ -279,7 +282,6 @@ SettingsPageTemplate { } } - // Network list Column { width: parent.width spacing: MSpacing.sm @@ -289,12 +291,9 @@ SettingsPageTemplate { model: (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) ? NetworkManagerCpp.availableNetworks : [] MSettingsListItem { - // 3-4 bars (excellent) - width: parent.width title: modelData.ssid subtitle: (modelData.security || "Open") + " • " + modelData.strength + "% signal" + (modelData.frequency ? (" • " + modelData.frequency + " GHz") : "") - // Use proper signal bar icons based on strength, not opacity iconName: { if (modelData.strength === 0) return "wifi-zero"; @@ -302,23 +301,19 @@ SettingsPageTemplate { if (modelData.strength <= 33) return "wifi-low"; - // 1-2 bars (weak) if (modelData.strength <= 66) return "wifi"; - // 2-3 bars (good) return "wifi-high"; } showChevron: true onSettingClicked: { HapticService.light(); - // If this is the currently connected network, show disconnect dialog if ((typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) && NetworkManagerCpp.wifiConnected && modelData.ssid === NetworkManagerCpp.wifiSsid) { Logger.info("WiFiPage", "Show disconnect dialog for: " + modelData.ssid); disconnectSheet.show(); } else { Logger.info("WiFiPage", "Connect to: " + modelData.ssid); - // Show password dialog (works for both secured and open networks) wifiPasswordDialogLoader.show(modelData.ssid, modelData.strength, modelData.security || "Open", modelData.secured); } } @@ -326,7 +321,6 @@ SettingsPageTemplate { } } - // No networks found Text { width: parent.width text: "No networks found" @@ -339,7 +333,6 @@ SettingsPageTemplate { visible: (typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp) ? (NetworkManagerCpp.availableNetworks.length === 0) : true } - // Rescan button MButton { width: parent.width text: "Scan for networks" diff --git a/apps/store/StoreApp.qml b/apps/store/StoreApp.qml index 1d424d9a4..fc1f25a59 100644 --- a/apps/store/StoreApp.qml +++ b/apps/store/StoreApp.qml @@ -1,12 +1,13 @@ -import QtQuick -import QtQuick.Controls -import MarathonUI.Core +import MarathonOS.Shell import MarathonUI.Containers +import MarathonUI.Core import MarathonUI.Theme -import MarathonOS.Shell +import QtQuick +import QtQuick.Controls MApp { id: root + appId: "store" appName: "App Store" appIcon: "assets/icon.svg" @@ -17,50 +18,45 @@ MApp { StackView { id: navigationStack + anchors.fill: parent initialItem: storeFrontPage - - // Update parent's navigationDepth when stack changes onDepthChanged: { root.navigationDepth = depth - 1; } - // Handle back button Connections { - target: root function onBackPressed() { - if (navigationStack.depth > 1) { + if (navigationStack.depth > 1) navigationStack.pop(); - } } + + target: root } } Component { id: storeFrontPage + StoreFrontPage {} } Component { id: appDetailPage + AppDetailPage {} } Component { id: installedAppsPage + InstalledAppsPage {} } Component { id: updatesPage + UpdatesPage {} } - - // Refresh catalog on startup - // Component.onCompleted: { - // if (!AppStoreService.catalogLoaded) { - // AppStoreService.refreshCatalog(); - // } - // } } } diff --git a/apps/store/components/AppListItem.qml b/apps/store/components/AppListItem.qml index 7e627661f..e0b892c10 100644 --- a/apps/store/components/AppListItem.qml +++ b/apps/store/components/AppListItem.qml @@ -1,51 +1,51 @@ -import QtQuick -import QtQuick.Controls +import MarathonUI.Containers import MarathonUI.Core -import MarathonUI.Controls -import MarathonUI.Lists +import MarathonUI.Theme +import QtQuick -MListItem { +MListTile { id: root property var appData: ({}) + title: appData.name || "Unknown" + subtitle: appData.description || "No description" + leading: Rectangle { width: 48 height: 48 radius: 12 - color: MTheme.surfaceVariant + color: MColors.elevated - MIcon { + Icon { anchors.centerIn: parent - source: appData.icon || "apps" + name: appData.icon || "apps" size: 28 - color: MTheme.primary + color: MColors.accent } } - title: appData.name || "Unknown" - subtitle: appData.description || "No description" - trailing: Row { spacing: 8 - MIcon { - source: "star" + Icon { + name: "star" size: 16 color: "#FFC107" anchors.verticalCenter: parent.verticalCenter } - MText { + MLabel { text: (appData.rating || 0).toFixed(1) - type: MText.Caption + variant: "xsmall" + color: MColors.textSecondary anchors.verticalCenter: parent.verticalCenter } - MIcon { - source: "chevron_right" + Icon { + name: "chevron-right" size: 20 - color: MTheme.onSurfaceVariant + color: MColors.textSecondary anchors.verticalCenter: parent.verticalCenter } } diff --git a/apps/store/components/FeaturedAppCard.qml b/apps/store/components/FeaturedAppCard.qml index f50955ea0..63f622590 100644 --- a/apps/store/components/FeaturedAppCard.qml +++ b/apps/store/components/FeaturedAppCard.qml @@ -1,16 +1,16 @@ -import QtQuick -import QtQuick.Controls import MarathonUI.Core -import MarathonUI.Controls +import MarathonUI.Theme +import QtQuick Rectangle { id: root property var appData: ({}) + signal clicked radius: 16 - color: MTheme.surfaceVariant + color: MColors.elevated MouseArea { anchors.fill: parent @@ -18,8 +18,8 @@ Rectangle { Rectangle { anchors.fill: parent - radius: parent.parent.radius - color: MTheme.primary + radius: root.radius + color: MColors.accent opacity: parent.pressed ? 0.1 : 0 Behavior on opacity { @@ -30,7 +30,7 @@ Rectangle { } } - MColumn { + Column { anchors.fill: parent anchors.margins: 20 spacing: 12 @@ -43,50 +43,51 @@ Rectangle { width: 56 height: 56 radius: 14 - color: MTheme.surface + color: MColors.surface - MIcon { + Icon { anchors.centerIn: parent - source: appData.icon || "apps" + name: appData.icon || "apps" size: 36 - color: MTheme.primary + color: MColors.accent } } - MColumn { + Column { width: parent.width - 71 spacing: 4 anchors.verticalCenter: parent.verticalCenter - MText { + MLabel { text: appData.name || "Unknown" - type: MText.Subtitle + variant: "large" + color: MColors.textPrimary elide: Text.ElideRight } Row { spacing: 5 - MIcon { - source: "star" + Icon { + name: "star" size: 14 color: "#FFC107" } - MText { + MLabel { text: (appData.rating || 0).toFixed(1) - type: MText.Caption - color: MTheme.onSurfaceVariant + variant: "xsmall" + color: MColors.textSecondary } } } } - MText { + MLabel { width: parent.width text: appData.description || "" - type: MText.Body - color: MTheme.onSurfaceVariant + variant: "small" + color: MColors.textSecondary wrapMode: Text.WordWrap maximumLineCount: 3 elide: Text.ElideRight diff --git a/apps/store/components/InfoRow.qml b/apps/store/components/InfoRow.qml index 277e38bd6..8d920e966 100644 --- a/apps/store/components/InfoRow.qml +++ b/apps/store/components/InfoRow.qml @@ -1,5 +1,6 @@ -import QtQuick import MarathonUI.Core +import MarathonUI.Theme +import QtQuick Row { property string label: "" @@ -8,14 +9,17 @@ Row { width: parent.width spacing: 12 - MText { + MLabel { text: label + ":" width: 100 - color: MTheme.onSurfaceVariant + variant: "small" + color: MColors.textSecondary } - MText { + MLabel { text: value width: parent.width - 112 + variant: "small" + color: MColors.textPrimary } } diff --git a/apps/store/pages/AppDetailPage.qml b/apps/store/pages/AppDetailPage.qml index 95ca84fdb..41d2a1ccb 100644 --- a/apps/store/pages/AppDetailPage.qml +++ b/apps/store/pages/AppDetailPage.qml @@ -1,18 +1,19 @@ +import MarathonOS.Shell +import MarathonUI.Containers +import MarathonUI.Core +import MarathonUI.Theme import QtQuick import QtQuick.Controls import QtQuick.Layouts -import MarathonUI.Core -import MarathonUI.Containers -import MarathonUI.Theme -import MarathonOS.Shell Rectangle { id: page - anchors.fill: parent - color: MColors.background property string appId: "" + anchors.fill: parent + color: MColors.background + ColumnLayout { anchors.fill: parent spacing: 0 @@ -22,262 +23,4 @@ Rectangle { Layout.margins: MSpacing.md } } - - /*Connections { - target: AppStoreService - - function onDownloadProgress(id, bytesReceived, bytesTotal) { - if (id === appId) { - isDownloading = true - downloadProgress = bytesReceived / bytesTotal - } - } - - function onDownloadComplete(id, packagePath) { - if (id === appId) { - isDownloading = false - } - } - - function onDownloadFailed(id, error) { - if (id === appId) { - isDownloading = false - errorText.text = error - } - } - } - - Connections { - target: AppInstaller - - function onInstallComplete(id) { - if (id === appId) { - isInstalled = true - } - } - - function onUninstallComplete(id) { - if (id === appId) { - isInstalled = false - } - } - } - - Flickable { - anchors.fill: parent - contentHeight: contentColumn.height - clip: true - - MColumn { - id: contentColumn - width: parent.width - spacing: 25 - padding: 20 - - // App Header - Row { - width: parent.width - spacing: 20 - - // App Icon - Rectangle { - width: 80 - height: 80 - radius: 20 - color: MTheme.surfaceVariant - border.color: MTheme.outline - border.width: 1 - - MIcon { - anchors.centerIn: parent - source: appData.icon || "apps" - size: 48 - color: MTheme.primary - } - } - - MColumn { - width: parent.width - 100 - spacing: 8 - anchors.verticalCenter: parent.verticalCenter - - MText { - text: appData.name || "Unknown" - type: MText.Title - } - - MText { - text: "by " + (appData.author || "Unknown Developer") - color: MTheme.onSurfaceVariant - } - - Row { - spacing: 10 - - MIcon { - source: "star" - size: 16 - color: "#FFC107" - } - - MText { - text: (appData.rating || 0).toFixed(1) + " (" + (appData.downloads || 0) + " downloads)" - type: MText.Caption - } - } - } - } - - // Install/Uninstall Button - MColumn { - width: parent.width - spacing: 10 - - MButton { - text: { - if (isDownloading) return "Downloading..." - if (isInstalled) return "Uninstall" - return "Install" - } - type: isInstalled ? MButton.Outlined : MButton.Filled - width: parent.width - enabled: !isDownloading - - onClicked: { - if (isInstalled) { - AppInstaller.uninstallApp(appId) - } else { - AppStoreService.downloadApp(appId) - } - } - } - - // Download progress - Rectangle { - width: parent.width - height: 4 - radius: 2 - color: MTheme.surfaceVariant - visible: isDownloading - - Rectangle { - width: parent.width * downloadProgress - height: parent.height - radius: 2 - color: MTheme.primary - - Behavior on width { - NumberAnimation { duration: 150 } - } - } - } - - // Error message - MText { - id: errorText - width: parent.width - color: MTheme.error - wrapMode: Text.WordWrap - visible: text.length > 0 - } - } - - // Description - MColumn { - width: parent.width - spacing: 10 - - MText { - text: "About" - type: MText.Subtitle - } - - MText { - width: parent.width - text: appData.description || "No description available." - wrapMode: Text.WordWrap - } - } - - // Permissions - MColumn { - width: parent.width - spacing: 10 - visible: appData.permissions && appData.permissions.length > 0 - - MText { - text: "Permissions" - type: MText.Subtitle - } - - Repeater { - model: appData.permissions || [] - - delegate: Row { - width: parent.width - spacing: 12 - - MIcon { - source: getPermissionIcon(modelData) - size: 20 - color: MTheme.primary - anchors.verticalCenter: parent.verticalCenter - } - - MText { - text: PermissionManager.getPermissionDescription(modelData) - width: parent.width - 32 - wrapMode: Text.WordWrap - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - - // App Info - MColumn { - width: parent.width - spacing: 10 - - MText { - text: "Information" - type: MText.Subtitle - } - - InfoRow { - label: "Version" - value: appData.version || "1.0.0" - } - - InfoRow { - label: "Size" - value: formatSize(appData.size || 0) - } - - InfoRow { - label: "Category" - value: appData.categories ? appData.categories[0] : "Uncategorized" - } - } - } - } - - function getPermissionIcon(permission) { - const icons = { - "network": "wifi", - "location": "location_on", - "camera": "camera_alt", - "microphone": "mic", - "contacts": "contacts", - "storage": "folder" - }; - return icons[permission] || "security"; - } - - function formatSize(bytes) { - if (bytes < 1024) return bytes + " B"; - if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; - return (bytes / (1024 * 1024)).toFixed(1) + " MB"; - } - */ } diff --git a/apps/store/pages/InstalledAppsPage.qml b/apps/store/pages/InstalledAppsPage.qml index 5846bd776..e51d4fb28 100644 --- a/apps/store/pages/InstalledAppsPage.qml +++ b/apps/store/pages/InstalledAppsPage.qml @@ -1,13 +1,14 @@ +import MarathonOS.Shell +import MarathonUI.Containers +import MarathonUI.Core +import MarathonUI.Theme import QtQuick import QtQuick.Controls import QtQuick.Layouts -import MarathonUI.Core -import MarathonUI.Containers -import MarathonUI.Theme -import MarathonOS.Shell Rectangle { id: page + anchors.fill: parent color: MColors.background diff --git a/apps/store/pages/StoreFrontPage.qml b/apps/store/pages/StoreFrontPage.qml index cb0b30e10..0500d9fd9 100644 --- a/apps/store/pages/StoreFrontPage.qml +++ b/apps/store/pages/StoreFrontPage.qml @@ -1,13 +1,14 @@ +import MarathonOS.Shell +import MarathonUI.Containers +import MarathonUI.Core +import MarathonUI.Theme import QtQuick import QtQuick.Controls import QtQuick.Layouts -import MarathonUI.Core -import MarathonUI.Containers -import MarathonUI.Theme -import MarathonOS.Shell Rectangle { id: page + anchors.fill: parent color: MColors.background @@ -18,7 +19,6 @@ Rectangle { width: page.width spacing: 0 - // Hero Section Rectangle { Layout.fillWidth: true Layout.preferredHeight: 280 @@ -58,7 +58,6 @@ Rectangle { } } - // Features Grid ColumnLayout { Layout.fillWidth: true Layout.margins: MSpacing.xl @@ -79,39 +78,40 @@ Rectangle { Repeater { model: [ { - title: "Curated Catalog", - desc: "Browse hand-picked Marathon apps", - icon: "📱" + "title": "Curated Catalog", + "desc": "Browse hand-picked Marathon apps", + "icon": "📱" }, { - title: "Secure Installation", - desc: "GPG-verified packages with signature checking", - icon: "🔒" + "title": "Secure Installation", + "desc": "GPG-verified packages with signature checking", + "icon": "🔒" }, { - title: "Smart Permissions", - desc: "Runtime permission prompts for user privacy", - icon: "🛡" + "title": "Smart Permissions", + "desc": "Runtime permission prompts for user privacy", + "icon": "🛡" }, { - title: "Automatic Updates", - desc: "Keep your apps up-to-date automatically", - icon: "🔄" + "title": "Automatic Updates", + "desc": "Keep your apps up-to-date automatically", + "icon": "🔄" }, { - title: "Developer Tools", - desc: "CLI tools for packaging and signing apps", - icon: "" + "title": "Developer Tools", + "desc": "CLI tools for packaging and signing apps", + "icon": "" }, { - title: "Fast & Native", - desc: "QML apps optimized for ARM performance", - icon: "" + "title": "Fast & Native", + "desc": "QML apps optimized for ARM performance", + "icon": "" } ] delegate: Rectangle { required property var modelData + Layout.fillWidth: true Layout.preferredHeight: 90 color: MColors.surface @@ -152,7 +152,6 @@ Rectangle { } } - // Developer Info Rectangle { Layout.fillWidth: true Layout.preferredHeight: 120 diff --git a/apps/store/pages/UpdatesPage.qml b/apps/store/pages/UpdatesPage.qml index b2cbfd846..5de378a56 100644 --- a/apps/store/pages/UpdatesPage.qml +++ b/apps/store/pages/UpdatesPage.qml @@ -1,13 +1,14 @@ +import MarathonOS.Shell +import MarathonUI.Containers +import MarathonUI.Core +import MarathonUI.Theme import QtQuick import QtQuick.Controls import QtQuick.Layouts -import MarathonUI.Core -import MarathonUI.Containers -import MarathonUI.Theme -import MarathonOS.Shell Rectangle { id: page + anchors.fill: parent color: MColors.background diff --git a/apps/template/TemplateApp.qml b/apps/template/TemplateApp.qml index 7f98f5141..17c40d775 100644 --- a/apps/template/TemplateApp.qml +++ b/apps/template/TemplateApp.qml @@ -1,51 +1,63 @@ -import QtQuick -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Controls MApp { id: templateApp - appId: "template" - appName: "Template App" property var appData: [] + function loadAppData() { + var savedData = SettingsManagerCpp.get("template/data", "[]"); + try { + appData = JSON.parse(savedData); + } catch (e) { + Logger.error("TemplateApp", "Failed to load data: " + e); + appData = []; + } + appDataChanged(); + } + + function saveAppData() { + var data = JSON.stringify(appData); + SettingsManagerCpp.set("template/data", data); + } + + appId: "template" + appName: "Template App" onAppLaunched: { Logger.info("TemplateApp", "App launched"); loadAppData(); } - onAppResumed: { Logger.info("TemplateApp", "App resumed"); } - onAppPaused: { Logger.info("TemplateApp", "App paused"); saveAppData(); } - onAppWillTerminate: { Logger.info("TemplateApp", "App terminating"); saveAppData(); } - function loadAppData() { - var savedData = SettingsManagerCpp.get("template/data", "[]"); - try { - appData = JSON.parse(savedData); - } catch (e) { - Logger.error("TemplateApp", "Failed to load data: " + e); - appData = []; + Connections { + function onBackPressed() { + if (navigationStack.depth > 1) + navigationStack.pop(); } - appDataChanged(); + + target: templateApp } - function saveAppData() { - var data = JSON.stringify(appData); - SettingsManagerCpp.set("template/data", data); + Component { + id: mainPage + + MainPage {} } content: Rectangle { @@ -54,13 +66,12 @@ MApp { StackView { id: navigationStack + anchors.fill: parent initialItem: mainPage - onDepthChanged: { templateApp.navigationDepth = depth - 1; } - Component.onCompleted: { templateApp.navigationDepth = depth - 1; } @@ -83,10 +94,11 @@ MApp { duration: Constants.animationDurationNormal easing.type: Easing.OutCubic } + NumberAnimation { property: "opacity" - from: 1.0 - to: 0.0 + from: 1 + to: 0 duration: Constants.animationDurationNormal } } @@ -99,10 +111,11 @@ MApp { duration: Constants.animationDurationNormal easing.type: Easing.OutCubic } + NumberAnimation { property: "opacity" - from: 0.0 - to: 1.0 + from: 0 + to: 1 duration: Constants.animationDurationNormal } } @@ -118,18 +131,4 @@ MApp { } } } - - Connections { - target: templateApp - function onBackPressed() { - if (navigationStack.depth > 1) { - navigationStack.pop(); - } - } - } - - Component { - id: mainPage - MainPage {} - } } diff --git a/apps/template/pages/MainPage.qml b/apps/template/pages/MainPage.qml index 08186d019..cf030b222 100644 --- a/apps/template/pages/MainPage.qml +++ b/apps/template/pages/MainPage.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core -import MarathonUI.Theme import MarathonUI.Navigation +import MarathonUI.Theme +import QtQuick +import QtQuick.Layouts MPage { id: mainPage @@ -51,7 +51,6 @@ MPage { width: parent.width - parent.padding * 2 elevation: 1 interactive: true - onClicked: { HapticService.light(); Logger.info("TemplateApp", "Card clicked"); diff --git a/apps/terminal/TerminalApp.qml b/apps/terminal/TerminalApp.qml index f3fddd202..52406fd86 100644 --- a/apps/terminal/TerminalApp.qml +++ b/apps/terminal/TerminalApp.qml @@ -1,41 +1,28 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import MarathonOS.Shell import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts MApp { id: terminalApp - appId: "terminal" - appName: "Terminal" - appIcon: "assets/icon.svg" property int currentTabIndex: 0 property var tabs: [] property int nextTabId: 1 - onCurrentTabIndexChanged: { - Qt.callLater(() => { - var tab = tabs[currentTabIndex]; - if (tab && tab.session && terminalApp.Window.window?.active) { - tab.session.forceFocus(); - } - }); - } - function createNewTab() { var tabId = nextTabId++; var tab = { - id: tabId, - title: "Terminal " + tabId, - session: null + "id": tabId, + "title": "Terminal " + tabId, + "session": null }; tabs.push(tab); currentTabIndex = tabs.length - 1; tabsChanged(); - Logger.info("Terminal", "Created new tab: " + currentTabIndex); } @@ -44,28 +31,34 @@ MApp { Logger.info("Terminal", "Cannot close last tab"); return; } - if (index >= 0 && index < tabs.length) { var tab = tabs[index]; - // Session cleanup is handled by the Item destruction - tabs.splice(index, 1); - - if (currentTabIndex >= tabs.length) { + if (currentTabIndex >= tabs.length) currentTabIndex = tabs.length - 1; - } tabsChanged(); Logger.info("Terminal", "Closed tab: " + index); } } + appId: "terminal" + appName: "Terminal" + appIcon: "assets/icon.svg" + onCurrentTabIndexChanged: { + Qt.callLater(() => { + var tab = tabs[currentTabIndex]; + if (tab && tab.session && terminalApp.Window.window && terminalApp.Window.window.active) + tab.session.forceFocus(); + }); + } Component.onCompleted: { createNewTab(); } content: Rectangle { id: contentRoot + anchors.fill: parent color: MColors.background @@ -73,7 +66,6 @@ MApp { anchors.fill: parent spacing: 0 - // Terminal Content Area Item { Layout.fillWidth: true Layout.fillHeight: true @@ -84,28 +76,24 @@ MApp { TerminalSession { anchors.fill: parent visible: index === currentTabIndex - Component.onCompleted: { var tab = terminalApp.tabs[index]; - if (tab) { + if (tab) tab.session = this; - } + start(); } - onSessionFinished: { Logger.info("Terminal", "Session finished for tab " + index); - if (terminalApp.tabs.length > 1) { + if (terminalApp.tabs.length > 1) terminalApp.closeTab(index); - } else { - start(); // Restart if last tab - } + else + start(); } } } } - // Tab Bar (Bottom) Rectangle { Layout.fillWidth: true Layout.preferredHeight: 56 @@ -123,7 +111,6 @@ MApp { anchors.margins: MSpacing.sm spacing: MSpacing.sm - // Scrollable tab area ScrollView { Layout.fillWidth: true Layout.fillHeight: true @@ -143,14 +130,13 @@ MApp { anchors.verticalCenter: parent.verticalCenter title: { var tab = terminalApp.tabs[index]; - if (tab && tab.session) { + if (tab && tab.session) return tab.session.title || tab.title; - } + return tab ? tab.title : ""; } active: index === currentTabIndex canClose: terminalApp.tabs.length > 1 - onClicked: currentTabIndex = index onCloseClicked: terminalApp.closeTab(index) } @@ -158,7 +144,6 @@ MApp { } } - // New Tab Button Wrapper for Centering Item { Layout.preferredWidth: 48 Layout.fillHeight: true @@ -175,16 +160,13 @@ MApp { } } - // Virtual Key Row (Bottom) VirtualKeyRow { Layout.fillWidth: true Layout.preferredHeight: 48 - onKeyTriggered: (key, modifiers) => { var tab = terminalApp.tabs[currentTabIndex]; - if (tab && tab.session) { + if (tab && tab.session) tab.session.sendKey(key, "", modifiers); - } } } } diff --git a/apps/terminal/components/TerminalSession.qml b/apps/terminal/components/TerminalSession.qml index da8072e25..c04f9a9ed 100644 --- a/apps/terminal/components/TerminalSession.qml +++ b/apps/terminal/components/TerminalSession.qml @@ -1,11 +1,11 @@ -import QtQuick import MarathonApp.Terminal -import QtQuick.Layouts import MarathonApp.Terminal -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts Item { id: root @@ -49,133 +49,72 @@ Item { anchors.fill: parent spacing: 0 - // Output display Flickable { id: outputFlickable + Layout.fillWidth: true Layout.fillHeight: true contentWidth: terminalRenderer.width contentHeight: terminalRenderer.height clip: true boundsBehavior: Flickable.StopAtBounds - - // Padding leftMargin: MSpacing.md rightMargin: MSpacing.md topMargin: MSpacing.md bottomMargin: MSpacing.md - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - active: outputFlickable.moving || outputFlickable.flicking - } - TerminalRenderer { id: terminalRenderer + + function updateTerminalSize() { + if (charWidth > 0 && charHeight > 0) { + var cols = Math.floor(width / charWidth); + var rows = Math.floor(height / charHeight); + if (cols > 0 && rows > 0) + terminalEngine.resize(cols, rows); + } + } + width: outputFlickable.width - (outputFlickable.leftMargin + outputFlickable.rightMargin) height: outputFlickable.height - (outputFlickable.topMargin + outputFlickable.bottomMargin) terminal: terminalEngine - font.family: MTypography.fontFamilyMono font.pixelSize: 14 - textColor: MColors.text backgroundColor: "transparent" selectionColor: Qt.rgba(MColors.accent.r, MColors.accent.g, MColors.accent.b, 0.4) - onCharSizeChanged: updateTerminalSize() onWidthChanged: updateTerminalSize() onHeightChanged: updateTerminalSize() - - function updateTerminalSize() { - if (charWidth > 0 && charHeight > 0) { - var cols = Math.floor(width / charWidth); - var rows = Math.floor(height / charHeight); - if (cols > 0 && rows > 0) { - terminalEngine.resize(cols, rows); - } - } - } - - MouseArea { - anchors.fill: parent - preventStealing: true - - property point startPos - property bool selecting: false - - onPressed: function (mouse) { - terminalRenderer.clearSelection(); - startPos = Qt.point(mouse.x, mouse.y); - selecting = true; - - // Focus the hidden input to ensure keyboard works - terminalRenderer.forceActiveFocus(); // Assuming terminalRenderer is the input field - } - - onPositionChanged: function (mouse) { - if (selecting) { - var startGrid = terminalRenderer.positionToGrid(startPos.x, startPos.y); - var endGrid = terminalRenderer.positionToGrid(mouse.x, mouse.y); - terminalRenderer.select(startGrid.x, startGrid.y, endGrid.x, endGrid.y); - } - } - - onReleased: function (mouse) { - selecting = false; - var text = terminalRenderer.selectedText(); - if (text.length > 0) { - // Auto-copy to clipboard on selection end (Linux terminal style) - // Or show a menu? For now, let's just log it or keep it selected. - // Ideally we'd have a context menu. - console.log("Selected text:", text); - } - } - - onPressAndHold: function (mouse) { - // TODO: Show context menu (Copy/Paste) - // HapticService.light() // HapticService is not defined in this context - } - } - - // Handle keyboard input Keys.onPressed: event => { var text = event.text; var key = event.key; var modifiers = event.modifiers; - - // Handle special keys if (key === Qt.Key_Backspace) { terminalEngine.sendKey(key, "", modifiers); event.accepted = true; return; } - if (key === Qt.Key_Return || key === Qt.Key_Enter) { terminalEngine.sendKey(key, "", modifiers); event.accepted = true; return; } - if (key === Qt.Key_Up || key === Qt.Key_Down || key === Qt.Key_Left || key === Qt.Key_Right) { terminalEngine.sendKey(key, "", modifiers); event.accepted = true; return; } - if (key === Qt.Key_Tab) { terminalEngine.sendKey(key, "", modifiers); event.accepted = true; return; } - if (key === Qt.Key_Escape) { terminalEngine.sendKey(key, "", modifiers); event.accepted = true; return; } - - // Ctrl+C, etc. if (modifiers & Qt.ControlModifier) { if (key >= Qt.Key_A && key <= Qt.Key_Z) { terminalEngine.sendKey(key, text, modifiers); @@ -183,13 +122,44 @@ Item { return; } } - - // Normal text input if (text.length > 0) { terminalEngine.sendInput(text); event.accepted = true; } } + + MouseArea { + property point startPos + property bool selecting: false + + anchors.fill: parent + preventStealing: true + onPressed: function (mouse) { + terminalRenderer.clearSelection(); + startPos = Qt.point(mouse.x, mouse.y); + selecting = true; + terminalRenderer.forceActiveFocus(); + } + onPositionChanged: function (mouse) { + if (selecting) { + var startGrid = terminalRenderer.positionToGrid(startPos.x, startPos.y); + var endGrid = terminalRenderer.positionToGrid(mouse.x, mouse.y); + terminalRenderer.select(startGrid.x, startGrid.y, endGrid.x, endGrid.y); + } + } + onReleased: function (mouse) { + selecting = false; + var text = terminalRenderer.selectedText(); + if (text.length > 0) + console.log("Selected text:", text); + } + onPressAndHold: function (mouse) {} + } + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + active: outputFlickable.moving || outputFlickable.flicking } } } diff --git a/apps/terminal/components/TerminalTabButton.qml b/apps/terminal/components/TerminalTabButton.qml index 926ec770e..a976a9135 100644 --- a/apps/terminal/components/TerminalTabButton.qml +++ b/apps/terminal/components/TerminalTabButton.qml @@ -1,11 +1,11 @@ -import QtQuick import MarathonApp.Terminal -import QtQuick.Layouts import MarathonApp.Terminal -import QtQuick.Controls import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts Rectangle { id: root diff --git a/apps/terminal/components/VirtualKeyRow.qml b/apps/terminal/components/VirtualKeyRow.qml index ae273c89b..22581b5ad 100644 --- a/apps/terminal/components/VirtualKeyRow.qml +++ b/apps/terminal/components/VirtualKeyRow.qml @@ -1,89 +1,89 @@ -import QtQuick import MarathonApp.Terminal -import QtQuick.Layouts import MarathonApp.Terminal -import QtQuick.Controls import MarathonUI.Core import MarathonUI.Theme +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts Rectangle { id: root property bool ctrlActive: false property bool altActive: false - - signal keyTriggered(int key, int modifiers) - - color: MColors.surface - property var keyModel: [ { - label: "Esc", - key: Qt.Key_Escape, - isModifier: false, - modifier: 0, - requireCtrl: false + "label": "Esc", + "key": Qt.Key_Escape, + "isModifier": false, + "modifier": 0, + "requireCtrl": false }, { - label: "Tab", - key: Qt.Key_Tab, - isModifier: false, - modifier: 0, - requireCtrl: false + "label": "Tab", + "key": Qt.Key_Tab, + "isModifier": false, + "modifier": 0, + "requireCtrl": false }, { - label: "Ctrl", - key: -1, - isModifier: true, - modifier: Qt.ControlModifier, - requireCtrl: false + "label": "Ctrl", + "key": -1, + "isModifier": true, + "modifier": Qt.ControlModifier, + "requireCtrl": false }, { - label: "Alt", - key: -1, - isModifier: true, - modifier: Qt.AltModifier, - requireCtrl: false + "label": "Alt", + "key": -1, + "isModifier": true, + "modifier": Qt.AltModifier, + "requireCtrl": false }, { - label: "▲", - key: Qt.Key_Up, - isModifier: false, - modifier: 0, - requireCtrl: false + "label": "▲", + "key": Qt.Key_Up, + "isModifier": false, + "modifier": 0, + "requireCtrl": false }, { - label: "▼", - key: Qt.Key_Down, - isModifier: false, - modifier: 0, - requireCtrl: false + "label": "▼", + "key": Qt.Key_Down, + "isModifier": false, + "modifier": 0, + "requireCtrl": false }, { - label: "◀", - key: Qt.Key_Left, - isModifier: false, - modifier: 0, - requireCtrl: false + "label": "◀", + "key": Qt.Key_Left, + "isModifier": false, + "modifier": 0, + "requireCtrl": false }, { - label: "▶", - key: Qt.Key_Right, - isModifier: false, - modifier: 0, - requireCtrl: false + "label": "▶", + "key": Qt.Key_Right, + "isModifier": false, + "modifier": 0, + "requireCtrl": false }, { - label: "C", - key: Qt.Key_C, - isModifier: false, - modifier: 0, - requireCtrl: true + "label": "C", + "key": Qt.Key_C, + "isModifier": false, + "modifier": 0, + "requireCtrl": true } ] + signal keyTriggered(int key, int modifiers) + + color: MColors.surface + ScrollView { id: scrollView + anchors.fill: parent contentHeight: parent.height clip: true @@ -92,6 +92,7 @@ Rectangle { Row { id: keyRow + padding: MSpacing.sm spacing: MSpacing.xs height: parent.height @@ -100,43 +101,42 @@ Rectangle { model: root.keyModel MButton { - width: 48 - height: keyRow.height - (keyRow.padding * 2) - anchors.verticalCenter: parent.verticalCenter - required property int index required property var modelData + width: 48 + height: keyRow.height - (keyRow.padding * 2) + anchors.verticalCenter: parent.verticalCenter text: modelData.label - variant: { if (modelData.isModifier) { if (modelData.modifier === Qt.ControlModifier && root.ctrlActive) return "primary"; + if (modelData.modifier === Qt.AltModifier && root.altActive) return "primary"; } return "secondary"; } - onClicked: { if (modelData.isModifier) { if (modelData.modifier === Qt.ControlModifier) root.ctrlActive = !root.ctrlActive; + if (modelData.modifier === Qt.AltModifier) root.altActive = !root.altActive; + return; } - var modifiers = 0; if (root.ctrlActive) modifiers |= Qt.ControlModifier; + if (root.altActive) modifiers |= Qt.AltModifier; - if (modelData.requireCtrl) { + if (modelData.requireCtrl) modifiers |= Qt.ControlModifier; - } root.keyTriggered(modelData.key, modifiers); } diff --git a/apps/terminal/src/TerminalEngine.cpp b/apps/terminal/src/TerminalEngine.cpp index 3f3699c91..8d18768f4 100644 --- a/apps/terminal/src/TerminalEngine.cpp +++ b/apps/terminal/src/TerminalEngine.cpp @@ -101,12 +101,12 @@ void TerminalEngine::processOutput(const QByteArray &data) { } else if (ch == '\b') { m_screen->backspace(); } else if (ch == '\t') { - // Simple tab handling (every 8 chars) + int x = m_screen->cursorX(); int nextTab = (x / 8 + 1) * 8; m_screen->setCursorX(nextTab); } else if (ch == '\a') { - // Bell - ignore + } else if (static_cast(ch) >= 32) { m_screen->putChar(ch); } @@ -128,7 +128,7 @@ void TerminalEngine::parseEscapeSequence(char ch) { } else if (ch == '(' || ch == ')') { m_state = Charset; } else { - // Unknown escape sequence, reset + m_state = Normal; } } else if (m_state == CSI) { @@ -138,20 +138,20 @@ void TerminalEngine::parseEscapeSequence(char ch) { m_params.append(m_sequenceBuffer.toInt()); m_sequenceBuffer.clear(); } else if (ch >= 0x40 && ch <= 0x7E) { - // Final byte + m_params.append(m_sequenceBuffer.toInt()); handleCSI(QString(ch)); m_state = Normal; } } else if (m_state == OSC) { - if (ch == '\a' || ch == '\x9c') { // BEL or ST + if (ch == '\a' || ch == '\x9c') { handleOSC(m_sequenceBuffer); m_state = Normal; } else { m_sequenceBuffer.append(ch); } } else if (m_state == Charset) { - // Ignore charset selection + m_state = Normal; } } @@ -160,7 +160,7 @@ void TerminalEngine::handleCSI(const QString &seq) { int p1 = m_params.value(0, 0); int p2 = m_params.value(1, 0); - if (seq == "m") { // SGR - Select Graphic Rendition + if (seq == "m") { if (m_params.isEmpty()) { m_screen->resetStyle(); } else { @@ -176,46 +176,46 @@ void TerminalEngine::handleCSI(const QString &seq) { else if (param == 27) m_screen->setInverse(false); else if (param >= 30 && param <= 37) { - // Standard FG + const uint32_t colors[] = {0xFF000000, 0xFFCC0000, 0xFF4E9A06, 0xFFC4A000, 0xFF3465A4, 0xFF75507B, 0xFF06989A, 0xFFD3D7CF}; m_screen->setFgColor(colors[param - 30]); } else if (param >= 40 && param <= 47) { - // Standard BG + const uint32_t colors[] = {0xFF000000, 0xFFCC0000, 0xFF4E9A06, 0xFFC4A000, 0xFF3465A4, 0xFF75507B, 0xFF06989A, 0xFFD3D7CF}; m_screen->setBgColor(colors[param - 40]); } else if (param == 39) - m_screen->setFgColor(0xFFFFFFFF); // Default FG + m_screen->setFgColor(0xFFFFFFFF); else if (param == 49) - m_screen->setBgColor(0xFF000000); // Default BG + m_screen->setBgColor(0xFF000000); else if (param >= 90 && param <= 97) { - // Bright FG + const uint32_t colors[] = {0xFF555753, 0xFFEF2929, 0xFF8AE234, 0xFFFCE94F, 0xFF729FCF, 0xFFAD7FA8, 0xFF34E2E2, 0xFFEEEEEC}; m_screen->setFgColor(colors[param - 90]); } } } - } else if (seq == "A") { // Cursor Up + } else if (seq == "A") { m_screen->moveCursorRelative(0, -std::max(1, p1)); - } else if (seq == "B") { // Cursor Down + } else if (seq == "B") { m_screen->moveCursorRelative(0, std::max(1, p1)); - } else if (seq == "C") { // Cursor Forward + } else if (seq == "C") { m_screen->moveCursorRelative(std::max(1, p1), 0); - } else if (seq == "D") { // Cursor Back + } else if (seq == "D") { m_screen->moveCursorRelative(-std::max(1, p1), 0); - } else if (seq == "H" || seq == "f") { // Cursor Position + } else if (seq == "H" || seq == "f") { int row = std::max(1, p1) - 1; int col = std::max(1, p2) - 1; m_screen->moveCursor(col, row); - } else if (seq == "J") { // Erase in Display + } else if (seq == "J") { m_screen->clearScreen(p1); - } else if (seq == "K") { // Erase in Line + } else if (seq == "K") { m_screen->clearLine(p1); - } else if (seq == "P") { // Delete Characters + } else if (seq == "P") { m_screen->deleteChars(std::max(1, p1)); - } else if (seq == "@") { // Insert Characters + } else if (seq == "@") { m_screen->insertChars(std::max(1, p1)); } } @@ -323,6 +323,6 @@ void TerminalEngine::sendSignal(int signal) { kill(m_pid, signal); } -void TerminalEngine::sendMousePress(int x, int y, int button) { /* TODO */ } -void TerminalEngine::sendMouseRelease(int x, int y, int button) { /* TODO */ } -void TerminalEngine::sendMouseMove(int x, int y, int buttons) { /* TODO */ } +void TerminalEngine::sendMousePress(int x, int y, int button) {} +void TerminalEngine::sendMouseRelease(int x, int y, int button) {} +void TerminalEngine::sendMouseMove(int x, int y, int buttons) {} diff --git a/apps/terminal/src/TerminalEngine.h b/apps/terminal/src/TerminalEngine.h index f0427ba01..9780e06d1 100644 --- a/apps/terminal/src/TerminalEngine.h +++ b/apps/terminal/src/TerminalEngine.h @@ -37,7 +37,6 @@ class TerminalEngine : public QObject { Q_INVOKABLE void resize(int cols, int rows); Q_INVOKABLE void sendSignal(int signal); - // Mouse handling Q_INVOKABLE void sendMousePress(int x, int y, int button); Q_INVOKABLE void sendMouseRelease(int x, int y, int button); Q_INVOKABLE void sendMouseMove(int x, int y, int buttons); @@ -62,13 +61,12 @@ class TerminalEngine : public QObject { QString m_title; TerminalScreen *m_screen; - // ANSI parser state enum State { Normal, Escape, - CSI, // Control Sequence Introducer [ - OSC, // Operating System Command ] - Charset // ( or ) + CSI, + OSC, + Charset }; State m_state; diff --git a/apps/terminal/src/TerminalRenderer.cpp b/apps/terminal/src/TerminalRenderer.cpp index 5c569ae54..315680942 100644 --- a/apps/terminal/src/TerminalRenderer.cpp +++ b/apps/terminal/src/TerminalRenderer.cpp @@ -14,7 +14,6 @@ TerminalRenderer::TerminalRenderer(QQuickItem *parent) setFlag(ItemHasContents, true); setAntialiasing(true); - // Default font - try to find a good monospace font QStringList fonts = {"Cascadia Code", "Fira Code", "Roboto Mono", "Courier New", "Monospace"}; for (const QString &family : fonts) { QFont f(family); @@ -26,10 +25,9 @@ TerminalRenderer::TerminalRenderer(QQuickItem *parent) m_font.setPixelSize(14); m_font.setStyleHint(QFont::Monospace); - // Default colors (can be overridden by QML) m_textColor = QColor(Qt::white); m_backgroundColor = QColor(Qt::black); - m_selectionColor = QColor(255, 255, 255, 64); // Semi-transparent white + m_selectionColor = QColor(255, 255, 255, 64); updateCharSize(); } @@ -49,8 +47,7 @@ void TerminalRenderer::setTerminal(TerminalEngine *terminal) { m_screen = m_terminal ? m_terminal->screen() : nullptr; if (m_terminal && m_screen) { - // Connect screen updates to QQuickItem::update() - // This schedules a repaint for the next VSync + connect(m_screen, &TerminalScreen::screenChanged, this, &QQuickItem::update); connect(m_screen, &TerminalScreen::cursorChanged, this, &QQuickItem::update); } @@ -134,11 +131,6 @@ QPoint TerminalRenderer::positionToGrid(qreal x, qreal y) { int col = static_cast(x / m_charWidth); int row = static_cast(y / m_charHeight); - // Clamp? Or let caller handle? - // Let's clamp to screen bounds if possible, but we don't know max cols/rows easily without locking - // Caller (QML) usually handles bounds or passes raw coords. - // We'll just return raw grid coords. - return QPoint(col, row); } @@ -148,21 +140,18 @@ void TerminalRenderer::paint(QPainter *painter) { painter->setFont(m_font); - // Lock the screen while we read from it QMutexLocker locker(m_screen->mutex()); int cols = m_screen->cols(); int rows = m_screen->rows(); - // Draw background painter->fillRect(boundingRect(), m_backgroundColor); - // Batch drawing variables QString lineBuffer; lineBuffer.reserve(cols); for (int y = 0; y < rows; ++y) { - // Optimization: Skip rows outside view + if (y * m_charHeight > height()) break; @@ -170,10 +159,7 @@ void TerminalRenderer::paint(QPainter *painter) { while (x < cols) { const TerminalCell &startCell = m_screen->cell(x, y); - // 1. Draw Background (if not default) - // We can batch background too, but for now let's do per-cell or small runs - // Actually, let's find a run of identical styles - int runEnd = x + 1; + int runEnd = x + 1; while (runEnd < cols) { const TerminalCell &next = m_screen->cell(runEnd, y); if (next != startCell) @@ -181,15 +167,14 @@ void TerminalRenderer::paint(QPainter *painter) { runEnd++; } - // Draw BG for the run if (startCell.bgColor != 0xFF000000 || startCell.inverse) { QColor bg; if (startCell.inverse) { - // Inverse: Use FG as BG + bg = (startCell.fgColor == 0xFFFFFFFF) ? m_textColor : QColor(startCell.fgColor); } else { - // Normal: Use BG + bg = QColor(startCell.bgColor); } @@ -198,11 +183,6 @@ void TerminalRenderer::paint(QPainter *painter) { painter->fillRect(bgRect, bg); } - // Draw Selection Overlay - // We need to check each cell in the run for selection - // Optimization: Check if the whole run is selected or not - // But selection boundaries might be in the middle of a style run. - // So we iterate. if (m_screen->hasSelection()) { int selStart = x; while (selStart < runEnd) { @@ -211,7 +191,7 @@ void TerminalRenderer::paint(QPainter *painter) { while (selEnd < runEnd && m_screen->isSelected(selEnd, y)) { selEnd++; } - // Draw selection rect + QRectF selRect(selStart * m_charWidth, y * m_charHeight, (selEnd - selStart) * m_charWidth, m_charHeight); painter->fillRect(selRect, m_selectionColor); @@ -222,21 +202,17 @@ void TerminalRenderer::paint(QPainter *painter) { } } - // Draw Cursor (if in this run) if (y == m_screen->cursorY() && m_screen->cursorX() >= x && m_screen->cursorX() < runEnd) { QRectF cursorRect(m_screen->cursorX() * m_charWidth, y * m_charHeight, m_charWidth, m_charHeight); - // Cursor color is inverse of text color or a specific cursor color - // For now, let's use semi-transparent inverse of background + QColor cursorColor = (m_backgroundColor.lightness() > 128) ? QColor(0, 0, 0, 128) : QColor(255, 255, 255, 128); painter->fillRect(cursorRect, cursorColor); } - // 2. Draw Text Batch - // Collect characters for this run lineBuffer.clear(); bool hasText = false; for (int i = x; i < runEnd; ++i) { @@ -252,11 +228,11 @@ void TerminalRenderer::paint(QPainter *painter) { if (hasText) { QColor fg; if (startCell.inverse) { - // Inverse: Use BG as FG + fg = (startCell.bgColor == 0xFF000000) ? m_backgroundColor : QColor(startCell.bgColor); } else { - // Normal: Use FG + fg = (startCell.fgColor == 0xFFFFFFFF) ? m_textColor : QColor(startCell.fgColor); } diff --git a/apps/terminal/src/TerminalScreen.cpp b/apps/terminal/src/TerminalScreen.cpp index 1b7a94306..6439603b0 100644 --- a/apps/terminal/src/TerminalScreen.cpp +++ b/apps/terminal/src/TerminalScreen.cpp @@ -9,7 +9,7 @@ TerminalScreen::TerminalScreen(QObject *parent) , m_cursorX(0) , m_cursorY(0) , m_topRow(0) - , m_historySize(1000) // 1000 lines of history + , m_historySize(1000) , m_currentFg(0xFFFFFFFF) , m_currentBg(0xFF000000) , m_currentBold(false) @@ -19,7 +19,7 @@ TerminalScreen::TerminalScreen(QObject *parent) , m_selStartY(0) , m_selEndX(0) , m_selEndY(0) { - // Initialize grid with history capacity + m_grid.resize(m_historySize); for (int i = 0; i < m_historySize; ++i) { m_grid[i].resize(m_cols); @@ -27,8 +27,7 @@ TerminalScreen::TerminalScreen(QObject *parent) } TerminalCell &TerminalScreen::cellAt(int x, int y) { - // y is visual row (0..m_rows-1) - // map to buffer row + int bufferRow = (m_topRow + y) % m_historySize; return m_grid[bufferRow][x]; } @@ -44,16 +43,11 @@ void TerminalScreen::resize(int cols, int rows) { if (cols == m_cols && rows == m_rows) return; - // Resizing a ring buffer is complex. - // For simplicity in this version, we'll clear history on resize or do a naive copy. - // Let's do a naive copy to a new grid to keep it clean. - QVector> newGrid(m_historySize); for (int i = 0; i < m_historySize; ++i) { newGrid[i].resize(cols); } - // Copy visible portion int copyRows = std::min(rows, m_rows); int copyCols = std::min(cols, m_cols); @@ -67,9 +61,8 @@ void TerminalScreen::resize(int cols, int rows) { m_grid = newGrid; m_cols = cols; m_rows = rows; - m_topRow = 0; // Reset top to 0 after resize + m_topRow = 0; - // Clamp cursor if (m_cursorX >= m_cols) m_cursorX = m_cols - 1; if (m_cursorY >= m_rows) @@ -159,9 +152,9 @@ void TerminalScreen::clearLine(int mode) { int start = 0; int end = m_cols; - if (mode == 0) { // Cursor to end + if (mode == 0) { start = m_cursorX; - } else if (mode == 1) { // Start to cursor + } else if (mode == 1) { end = m_cursorX + 1; } @@ -178,13 +171,13 @@ void TerminalScreen::clearScreen(int mode) { int startRow = 0; int endRow = m_rows; - if (mode == 0) { // Cursor to end + if (mode == 0) { clearLine(0); startRow = m_cursorY + 1; - } else if (mode == 1) { // Start to cursor + } else if (mode == 1) { clearLine(1); endRow = m_cursorY; - } else if (mode == 2) { // All + } else if (mode == 2) { startRow = 0; endRow = m_rows; m_cursorX = 0; @@ -258,18 +251,16 @@ void TerminalScreen::resetStyle() { const TerminalCell &TerminalScreen::cell(int x, int y) const { static TerminalCell empty; if (y >= 0 && y < m_rows && x >= 0 && x < m_cols) { - // Const cast to call helper, but we know we are reading + return const_cast(this)->cellAt(x, y); } return empty; } void TerminalScreen::scrollUp() { - // Ring buffer scroll: just increment top pointer + m_topRow = (m_topRow + 1) % m_historySize; - // Clear the new bottom row (which was the old top row) - // The bottom row is at visual index m_rows - 1 int bottomRowIndex = (m_topRow + m_rows - 1) % m_historySize; for (int x = 0; x < m_cols; ++x) { @@ -284,7 +275,6 @@ void TerminalScreen::setSelection(int startX, int startY, int endX, int endY) { QMutexLocker locker(&m_mutex); m_hasSelection = true; - // Normalize coordinates (start should be before end) if (startY > endY || (startY == endY && startX > endX)) { std::swap(startX, endX); std::swap(startY, endY); @@ -322,16 +312,13 @@ bool TerminalScreen::isSelected(int x, int y) const { if (y == m_selEndY) return x <= m_selEndX; - return true; // In between rows + return true; } QString TerminalScreen::getSelectedText() const { if (!m_hasSelection) return QString(); - // Note: We can't easily lock here if this is called from GUI thread while worker is writing - // Ideally, caller locks mutex() - QString text; for (int y = m_selStartY; y <= m_selEndY; ++y) { int startX = (y == m_selStartY) ? m_selStartX : 0; @@ -342,7 +329,7 @@ QString TerminalScreen::getSelectedText() const { if (c.codePoint) { text.append(QChar(c.codePoint)); } else { - text.append(' '); // Empty cells are spaces + text.append(' '); } } diff --git a/apps/terminal/src/TerminalScreen.h b/apps/terminal/src/TerminalScreen.h index fb4fd4c54..f3fd38206 100644 --- a/apps/terminal/src/TerminalScreen.h +++ b/apps/terminal/src/TerminalScreen.h @@ -7,8 +7,8 @@ struct TerminalCell { uint32_t codePoint = ' '; - uint32_t fgColor = 0xFFFFFFFF; // ARGB - uint32_t bgColor = 0xFF000000; // ARGB + uint32_t fgColor = 0xFFFFFFFF; + uint32_t bgColor = 0xFF000000; bool bold = false; bool italic = false; bool underline = false; @@ -34,32 +34,27 @@ class TerminalScreen : public QObject { void resize(int cols, int rows); void clear(); - // Character manipulation void putChar(uint32_t codePoint); void newLine(); void backspace(); - // Cursor movement - void moveCursor(int x, int y); // Absolute + void moveCursor(int x, int y); void moveCursorRelative(int dx, int dy); void setCursorX(int x); void setCursorY(int y); - // Editing - void clearLine(int mode); // 0=end, 1=start, 2=all - void clearScreen(int mode); // 0=end, 1=start, 2=all + void clearLine(int mode); + void clearScreen(int mode); void deleteChars(int count); void insertChars(int count); - // Style void setFgColor(uint32_t color); void setBgColor(uint32_t color); void setBold(bool bold); void setInverse(bool inverse); void resetStyle(); - // Accessors for Renderer - int cols() const { + int cols() const { return m_cols; } int rows() const { @@ -73,16 +68,14 @@ class TerminalScreen : public QObject { } const TerminalCell &cell(int x, int y) const; - // Selection - void setSelection(int startX, int startY, int endX, int endY); - void clearSelection(); - bool hasSelection() const { + void setSelection(int startX, int startY, int endX, int endY); + void clearSelection(); + bool hasSelection() const { return m_hasSelection; } bool isSelected(int x, int y) const; QString getSelectedText() const; - // Thread safety QMutex *mutex() { return &m_mutex; } @@ -92,32 +85,26 @@ class TerminalScreen : public QObject { void cursorChanged(); private: - void scrollUp(); - TerminalCell &cellAt(int x, int y); - - int m_cols; - int m_rows; - int m_cursorX; - int m_cursorY; - - // Ring Buffer State - int m_topRow; // Index of the visual top row in the buffer - int m_historySize; // Total rows in buffer (screen + history) - - // Style state - uint32_t m_currentFg; - uint32_t m_currentBg; - bool m_currentBold; - bool m_currentInverse; - - // Selection State - bool m_hasSelection; - int m_selStartX, m_selStartY; - int m_selEndX, m_selEndY; - - // Grid: [row][col] - // We use a flat vector for better cache locality? Or vector of vectors? - // Vector of vectors is easier for resizing. + void scrollUp(); + TerminalCell &cellAt(int x, int y); + + int m_cols; + int m_rows; + int m_cursorX; + int m_cursorY; + + int m_topRow; + int m_historySize; + + uint32_t m_currentFg; + uint32_t m_currentBg; + bool m_currentBold; + bool m_currentInverse; + + bool m_hasSelection; + int m_selStartX, m_selStartY; + int m_selEndX, m_selEndY; + QVector> m_grid; QMutex m_mutex; diff --git a/apps/test/TestApp.qml b/apps/test/TestApp.qml index 65d64d98b..55fcde293 100644 --- a/apps/test/TestApp.qml +++ b/apps/test/TestApp.qml @@ -21,8 +21,8 @@ MApp { Logger.info("TestApp", " - TelephonyService: " + (typeof TelephonyService !== 'undefined')); Logger.info("TestApp", " - SMSService: " + (typeof SMSService !== 'undefined')); Logger.info("TestApp", " - NotificationService: " + (typeof NotificationService !== 'undefined')); - Logger.info("TestApp", " - AlarmManager: " + (typeof AlarmManager !== 'undefined')); - Logger.info("TestApp", " - AudioManager: " + (typeof AudioManager !== 'undefined')); + Logger.info("TestApp", " - AlarmService: " + (typeof AlarmService !== 'undefined')); + Logger.info("TestApp", " - AudioManagerCpp: " + (typeof AudioManagerCpp !== 'undefined')); Logger.info("TestApp", " - PowerManager: " + (typeof PowerManagerService !== 'undefined')); Logger.info("TestApp", " - NetworkManagerCpp: " + (typeof NetworkManagerCpp !== 'undefined')); Logger.info("TestApp", " - DisplayManager: " + (typeof DisplayManagerCpp !== 'undefined')); diff --git a/apps/test/manifest.json b/apps/test/manifest.json index 21430d5ff..3ed8aed85 100644 --- a/apps/test/manifest.json +++ b/apps/test/manifest.json @@ -11,14 +11,11 @@ "system", "storage", "network", - "system.telephony", - "system.notifications", - "system.audio", - "system.network", - "system.power", - "system.display", - "system.location", - "system.contacts" + "telephony", + "sms", + "notifications", + "contacts", + "location" ], "capabilities": { "backgroundExecution": false, diff --git a/apps/test/pages/AlarmTestsPage.qml b/apps/test/pages/AlarmTestsPage.qml index e5ee556c2..8e3f3ae89 100644 --- a/apps/test/pages/AlarmTestsPage.qml +++ b/apps/test/pages/AlarmTestsPage.qml @@ -6,6 +6,9 @@ import MarathonUI.Theme import QtQuick Item { + property var alarmService: typeof AlarmService !== "undefined" ? AlarmService : null + property string lastAlarmId: "" + Flickable { anchors.fill: parent contentHeight: alarmColumn.height @@ -49,7 +52,7 @@ Item { } MLabel { - text: "Current Alarms: " + AlarmManager.alarms.length + text: "Current Alarms: " + (alarmService ? alarmService.alarms.length : 0) variant: "secondary" } @@ -61,24 +64,31 @@ Item { variant: "primary" onClicked: { HapticService.medium(); - var testAlarm = { - "id": "test_" + Date.now(), - "time": Qt.formatTime(new Date(), "HH:mm"), - "enabled": true, - "label": "Test Alarm", - "repeat": [], - "sound": "default", - "vibrate": true, - "snoozeEnabled": true, - "snoozeDuration": 10 - }; - // Use AlarmManager's real trigger path (plays sound, wakes screen, emits alarmTriggered) - if (typeof AlarmManager !== "undefined" && AlarmManager.triggerAlarmForTesting) - AlarmManager.triggerAlarmForTesting(testAlarm); - - Logger.info("TestApp", "Triggered test alarm"); - if (testApp) { - testApp.passedTests++; + if (alarmService && alarmService.triggerAlarmNow) { + alarmService.triggerAlarmNow("Test Alarm"); + Logger.info("TestApp", "Triggered alarm now"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (alarmService && alarmService.createAlarm) { + var futureTime = new Date(); + futureTime.setMinutes(futureTime.getMinutes() + 1); + lastAlarmId = alarmService.createAlarm(Qt.formatTime(futureTime, "HH:mm"), "Test Alarm (auto trigger)", [], {}); + if (lastAlarmId) { + Logger.info("TestApp", "Created trigger alarm: " + lastAlarmId); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "Failed to create trigger alarm"); + testApp.failedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AlarmService not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -89,12 +99,24 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - var futureTime = new Date(); - futureTime.setMinutes(futureTime.getMinutes() + 1); - var alarmId = AlarmManager.createAlarm(Qt.formatTime(futureTime, "HH:mm"), "Test Alarm (+1 min)", []); - Logger.info("TestApp", "Created alarm: " + alarmId); - if (testApp) { - testApp.passedTests++; + if (alarmService && alarmService.createAlarm) { + var futureTime = new Date(); + futureTime.setMinutes(futureTime.getMinutes() + 1); + var alarmId = alarmService.createAlarm(Qt.formatTime(futureTime, "HH:mm"), "Test Alarm (+1 min)", [], {}); + if (alarmId) { + Logger.info("TestApp", "Created alarm: " + alarmId); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "Failed to create alarm"); + testApp.failedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AlarmService not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -103,6 +125,18 @@ Item { } } + Connections { + target: alarmService + enabled: alarmService !== null + function onAlarmTriggered(id, label) { + Logger.info("TestApp", "Alarm triggered: " + label); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } + } + MCard { width: parent.width - parent.padding * 2 diff --git a/apps/test/pages/MediaTestsPage.qml b/apps/test/pages/MediaTestsPage.qml index 5fbfa03fa..0e5ccc433 100644 --- a/apps/test/pages/MediaTestsPage.qml +++ b/apps/test/pages/MediaTestsPage.qml @@ -1,11 +1,13 @@ -import QtQuick import MarathonApp.Test import MarathonOS.Shell +import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme -import MarathonUI.Containers +import QtQuick Item { + property var audioManager: typeof AudioManagerCpp !== "undefined" ? AudioManagerCpp : null + Flickable { anchors.fill: parent contentHeight: mediaColumn.height @@ -13,18 +15,21 @@ Item { Column { id: mediaColumn + width: parent.width spacing: MSpacing.md padding: MSpacing.lg Row { spacing: MSpacing.sm + Icon { name: "music" size: 24 color: MColors.accent anchors.verticalCenter: parent.verticalCenter } + MLabel { text: "Media & Audio" variant: "headline" @@ -46,7 +51,7 @@ Item { } MLabel { - text: "Volume: " + Math.round(AudioManager.volume * 100) + "%" + text: "Volume: " + (audioManager ? Math.round(audioManager.volume * 100) : 0) + "%" variant: "secondary" } @@ -59,10 +64,16 @@ Item { variant: "primary" onClicked: { HapticService.light(); - AudioManager.playRingtone(); - Logger.info("TestApp", "Playing ringtone"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.playRingtone) { + audioManager.playRingtone(); + Logger.info("TestApp", "Playing ringtone"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.playRingtone not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -73,10 +84,16 @@ Item { variant: "danger" onClicked: { HapticService.light(); - AudioManager.stopRingtone(); - Logger.info("TestApp", "Stopped ringtone"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.stopRingtone) { + audioManager.stopRingtone(); + Logger.info("TestApp", "Stopped ringtone"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.stopRingtone not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -87,10 +104,16 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - AudioManager.playNotificationSound(); - Logger.info("TestApp", "Played notification sound"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.playNotificationSound) { + audioManager.playNotificationSound(); + Logger.info("TestApp", "Played notification sound"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.playNotificationSound not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -101,10 +124,16 @@ Item { variant: "accent" onClicked: { HapticService.light(); - AudioManager.playAlarmSound(); - Logger.info("TestApp", "Playing alarm sound"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.playAlarmSound) { + audioManager.playAlarmSound(); + Logger.info("TestApp", "Playing alarm sound"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.playAlarmSound not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -115,10 +144,16 @@ Item { variant: "danger" onClicked: { HapticService.light(); - AudioManager.stopAlarmSound(); - Logger.info("TestApp", "Stopped alarm sound"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.stopAlarmSound) { + audioManager.stopAlarmSound(); + Logger.info("TestApp", "Stopped alarm sound"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.stopAlarmSound not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -141,7 +176,7 @@ Item { } MLabel { - text: "Current: " + AudioManager.audioProfile + text: "Current: " + (audioManager && audioManager.audioProfile ? audioManager.audioProfile : "unknown") variant: "secondary" } @@ -154,10 +189,16 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - AudioManager.setAudioProfile("silent"); - Logger.info("TestApp", "Profile: silent"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.setAudioProfile) { + audioManager.setAudioProfile("silent"); + Logger.info("TestApp", "Profile: silent"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.setAudioProfile not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -168,10 +209,16 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - AudioManager.setAudioProfile("vibrate"); - Logger.info("TestApp", "Profile: vibrate"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.setAudioProfile) { + audioManager.setAudioProfile("vibrate"); + Logger.info("TestApp", "Profile: vibrate"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.setAudioProfile not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -182,10 +229,16 @@ Item { variant: "primary" onClicked: { HapticService.light(); - AudioManager.setAudioProfile("normal"); - Logger.info("TestApp", "Profile: normal"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.setAudioProfile) { + audioManager.setAudioProfile("normal"); + Logger.info("TestApp", "Profile: normal"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.setAudioProfile not available"); + testApp.failedTests++; testApp.totalTests++; } } @@ -196,10 +249,16 @@ Item { variant: "accent" onClicked: { HapticService.light(); - AudioManager.setAudioProfile("loud"); - Logger.info("TestApp", "Profile: loud"); - if (testApp) { - testApp.passedTests++; + if (audioManager && audioManager.setAudioProfile) { + audioManager.setAudioProfile("loud"); + Logger.info("TestApp", "Profile: loud"); + if (testApp) { + testApp.passedTests++; + testApp.totalTests++; + } + } else if (testApp) { + Logger.warn("TestApp", "AudioManagerCpp.setAudioProfile not available"); + testApp.failedTests++; testApp.totalTests++; } } diff --git a/apps/test/pages/NotificationTestsPage.qml b/apps/test/pages/NotificationTestsPage.qml index c1efb85f3..51acd0df5 100644 --- a/apps/test/pages/NotificationTestsPage.qml +++ b/apps/test/pages/NotificationTestsPage.qml @@ -1,9 +1,9 @@ -import QtQuick import MarathonApp.Test import MarathonOS.Shell +import MarathonUI.Containers import MarathonUI.Core import MarathonUI.Theme -import MarathonUI.Containers +import QtQuick Item { Flickable { @@ -13,18 +13,21 @@ Item { Column { id: notifColumn + width: parent.width spacing: MSpacing.md padding: MSpacing.lg Row { spacing: MSpacing.sm + Icon { name: "bell" size: 24 color: MColors.accent anchors.verticalCenter: parent.verticalCenter } + MLabel { text: "Notifications" variant: "headline" @@ -69,8 +72,8 @@ Item { onClicked: { HapticService.light(); NotificationService.sendNotification("test", "Marathon Test", "Notification with custom icon", { - icon: "bell", - category: "test" + "icon": "bell", + "category": "test" }); Logger.info("TestApp", "Sent notification with icon"); if (testApp) { @@ -86,9 +89,9 @@ Item { onClicked: { HapticService.light(); NotificationService.sendNotification("test", "Important", "This is a high priority notification", { - priority: "high", - persistent: true, - icon: "alert-triangle" + "priority": "high", + "persistent": true, + "icon": "alert-triangle" }); Logger.info("TestApp", "Sent high priority notification"); if (testApp) { @@ -104,9 +107,9 @@ Item { onClicked: { HapticService.light(); NotificationService.sendNotification("test", "Interactive", "Tap an action below", { - actions: ["reply", "dismiss", "snooze"], - category: "message", - icon: "message-circle" + "actions": ["reply", "dismiss", "snooze"], + "category": "message", + "icon": "message-circle" }); Logger.info("TestApp", "Sent notification with actions"); if (testApp) { @@ -142,8 +145,8 @@ Item { onClicked: { HapticService.light(); NotificationService.sendNotification("messages", "John Doe", "Hey, how are you doing?", { - category: "message", - icon: "message-circle" + "category": "message", + "icon": "message-circle" }); if (testApp) { testApp.passedTests++; @@ -158,8 +161,8 @@ Item { onClicked: { HapticService.light(); NotificationService.sendNotification("mail", "New Email", "You have 3 new emails", { - category: "email", - icon: "mail" + "category": "email", + "icon": "mail" }); if (testApp) { testApp.passedTests++; @@ -174,8 +177,8 @@ Item { onClicked: { HapticService.light(); NotificationService.sendNotification("social", "Friend Request", "Jane wants to connect with you", { - category: "social", - icon: "users" + "category": "social", + "icon": "users" }); if (testApp) { testApp.passedTests++; @@ -190,8 +193,8 @@ Item { onClicked: { HapticService.light(); NotificationService.sendNotification("system", "System Update", "A new system update is available", { - category: "system", - icon: "download" + "category": "system", + "icon": "download" }); if (testApp) { testApp.passedTests++; @@ -226,7 +229,7 @@ Item { HapticService.medium(); for (var i = 0; i < 10; i++) { NotificationService.sendNotification("test", "Burst Test " + (i + 1), "Testing notification system under load", { - icon: "zap" + "icon": "zap" }); } Logger.info("TestApp", "Sent 10 burst notifications"); diff --git a/apps/test/pages/SensorTestsPage.qml b/apps/test/pages/SensorTestsPage.qml index fab0601e3..7407c3d96 100644 --- a/apps/test/pages/SensorTestsPage.qml +++ b/apps/test/pages/SensorTestsPage.qml @@ -53,12 +53,12 @@ Item { spacing: MSpacing.xs MLabel { - text: "Available: " + (AmbientLightSensor.available ? "Yes" : "No") + text: "Available: " + (SensorService.available ? "Yes" : "No") variant: "secondary" } MLabel { - text: "Light Level: " + AmbientLightSensor.lightLevel + " lux" + text: "Light Level: " + SensorService.ambientLight + " lux" variant: "secondary" } } @@ -71,15 +71,7 @@ Item { variant: "primary" onClicked: { HapticService.light(); - if (typeof AmbientLightSensor !== 'undefined' && typeof AmbientLightSensor.enable === 'function') - AmbientLightSensor.enable(); - else if (typeof AmbientLightSensor !== 'undefined') - AmbientLightSensor.active = true; - Logger.info("TestApp", "Enabled ambient light sensor"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; - } + Logger.info("TestApp", "Enabled ambient light sensor (noop)"); } } @@ -88,15 +80,7 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - if (typeof AmbientLightSensor !== 'undefined' && typeof AmbientLightSensor.disable === 'function') - AmbientLightSensor.disable(); - else if (typeof AmbientLightSensor !== 'undefined') - AmbientLightSensor.active = false; - Logger.info("TestApp", "Disabled ambient light sensor"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; - } + Logger.info("TestApp", "Disabled ambient light sensor (noop)"); } } } @@ -121,12 +105,12 @@ Item { spacing: MSpacing.xs MLabel { - text: "Available: " + (ProximitySensor.available ? "Yes" : "No") + text: "Available: " + (SensorService.available ? "Yes" : "No") variant: "secondary" } MLabel { - text: "Near: " + (ProximitySensor.near ? "Yes" : "No") + text: "Near: " + (SensorService.proximityNear ? "Yes" : "No") variant: "secondary" } } @@ -139,15 +123,7 @@ Item { variant: "primary" onClicked: { HapticService.light(); - if (typeof ProximitySensor !== 'undefined' && typeof ProximitySensor.enable === 'function') - ProximitySensor.enable(); - else if (typeof ProximitySensor !== 'undefined') - ProximitySensor.active = true; - Logger.info("TestApp", "Enabled proximity sensor"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; - } + Logger.info("TestApp", "Enabled proximity sensor (noop)"); } } @@ -156,15 +132,7 @@ Item { variant: "secondary" onClicked: { HapticService.light(); - if (typeof ProximitySensor !== 'undefined' && typeof ProximitySensor.disable === 'function') - ProximitySensor.disable(); - else if (typeof ProximitySensor !== 'undefined') - ProximitySensor.active = false; - Logger.info("TestApp", "Disabled proximity sensor"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; - } + Logger.info("TestApp", "Disabled proximity sensor (noop)"); } } } @@ -189,7 +157,7 @@ Item { spacing: MSpacing.xs MLabel { - text: "Enabled: " + (LocationService.enabled ? "Yes" : "No") + text: "Active: " + (LocationService.active ? "Yes" : "No") variant: "secondary" } @@ -213,11 +181,11 @@ Item { spacing: MSpacing.md MButton { - text: "Enable" + text: "Start" variant: "primary" onClicked: { HapticService.light(); - LocationService.enable(); + LocationService.start(); Logger.info("TestApp", "Enabled location service"); if (testApp) { testApp.passedTests++; @@ -227,16 +195,12 @@ Item { } MButton { - text: "Get Location" - variant: "accent" + text: "Stop" + variant: "secondary" onClicked: { HapticService.light(); - LocationService.startUpdating(); - Logger.info("TestApp", "Requested location update"); - if (testApp) { - testApp.passedTests++; - testApp.totalTests++; - } + LocationService.stop(); + Logger.info("TestApp", "Stopped location service"); } } } From d42b5ca20681945a9f7ef1f9910e6f1cfe21a043 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:32:00 +0000 Subject: [PATCH 08/28] refactor(core): adjust app registry and scanning metadata Tighten Marathon core app metadata, scanning, and verification helpers to support updated app behaviors. --- marathon-core/src/marathonappinstaller.cpp | 26 ++----------------- marathon-core/src/marathonapppackager.h | 7 ++--- marathon-core/src/marathonappregistry.h | 20 ++++++++++++--- marathon-core/src/marathonappscanner.cpp | 30 ++++++++-------------- marathon-core/src/marathonappscanner.h | 10 ++++---- marathon-core/src/marathonappverifier.h | 18 +++++-------- 6 files changed, 42 insertions(+), 69 deletions(-) diff --git a/marathon-core/src/marathonappinstaller.cpp b/marathon-core/src/marathonappinstaller.cpp index 23c46be25..3c142bde1 100644 --- a/marathon-core/src/marathonappinstaller.cpp +++ b/marathon-core/src/marathonappinstaller.cpp @@ -20,14 +20,11 @@ MarathonAppInstaller::MarathonAppInstaller(MarathonAppRegistry *registry, , m_verifier(new MarathonAppVerifier(this)) { qDebug() << "[MarathonAppInstaller] Initialized"; - // Connect packager signals connect(m_packager, &MarathonAppPackager::packagingProgress, this, [this](int percent) { - // Forward packaging progress qDebug() << "[MarathonAppInstaller] Packaging progress:" << percent << "%"; }); connect(m_packager, &MarathonAppPackager::extractionProgress, this, [this](int percent) { - // Forward extraction progress qDebug() << "[MarathonAppInstaller] Extraction progress:" << percent << "%"; }); } @@ -66,7 +63,6 @@ bool MarathonAppInstaller::validateManifest(const QString &manifestPath) { QJsonObject obj = doc.object(); - // Validate required fields if (!obj.contains("id") || !obj.contains("name") || !obj.contains("entryPoint")) { qWarning() << "[MarathonAppInstaller] Manifest missing required fields"; return false; @@ -126,14 +122,12 @@ bool MarathonAppInstaller::removeDirectory(const QString &path) { bool MarathonAppInstaller::installFromDirectory(const QString &sourcePath) { qDebug() << "[MarathonAppInstaller] Installing from directory:" << sourcePath; - // Validate manifest QString manifestPath = sourcePath + "/manifest.json"; if (!validateManifest(manifestPath)) { emit installFailed("unknown", "Invalid or missing manifest.json"); return false; } - // Parse manifest to get app ID QFile file(manifestPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { emit installFailed("unknown", "Failed to open manifest.json"); @@ -204,7 +198,6 @@ bool MarathonAppInstaller::installFromDirectory(const QString &sourcePath) { emit installProgress(appId, 90); - // Rescan applications m_scanner->scanApplications(); emit installProgress(appId, 100); @@ -217,13 +210,11 @@ bool MarathonAppInstaller::installFromDirectory(const QString &sourcePath) { bool MarathonAppInstaller::installFromPackage(const QString &packagePath) { qDebug() << "[MarathonAppInstaller] Installing from package:" << packagePath; - // Check if package exists if (!QFile::exists(packagePath)) { emit installFailed("unknown", "Package file does not exist"); return false; } - // Create temporary directory for extraction QTemporaryDir tempDir; if (!tempDir.isValid()) { emit installFailed("unknown", "Failed to create temporary directory"); @@ -234,7 +225,6 @@ bool MarathonAppInstaller::installFromPackage(const QString &packagePath) { qDebug() << "[MarathonAppInstaller] Extracting to temp:" << tempExtractPath; - // Extract package if (!m_packager->extractPackage(packagePath, tempExtractPath)) { QString error = "Failed to extract package: " + m_packager->lastError(); emit installFailed("unknown", error); @@ -243,7 +233,6 @@ bool MarathonAppInstaller::installFromPackage(const QString &packagePath) { emit installProgress("extracting", 30); - // Verify package signature qDebug() << "[MarathonAppInstaller] Verifying package signature..."; MarathonAppVerifier::VerificationResult verifyResult = m_verifier->verifyDirectory(tempExtractPath); @@ -255,9 +244,8 @@ bool MarathonAppInstaller::installFromPackage(const QString &packagePath) { return false; } - emit installProgress("verifying", 50); + emit installProgress("verifying", 50); - // Read manifest to get app ID QString manifestPath = tempExtractPath + "/manifest.json"; QFile manifestFile(manifestPath); if (!manifestFile.open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -285,16 +273,13 @@ bool MarathonAppInstaller::installFromPackage(const QString &packagePath) { emit installStarted(appId); emit installProgress(appId, 60); - // Check if already installed if (m_registry->hasApp(appId)) { qDebug() << "[MarathonAppInstaller] App already installed, updating:" << appId; } - // Determine destination QString installBase = getTargetInstallPath(); QString destPath = installBase + "/" + appId; - // Remove existing installation if (QDir(destPath).exists()) { if (!removeDirectory(destPath)) { emit installFailed(appId, "Failed to remove existing installation"); @@ -304,9 +289,8 @@ bool MarathonAppInstaller::installFromPackage(const QString &packagePath) { emit installProgress(appId, 75); - // Move extracted files to destination if (!QDir().rename(tempExtractPath, destPath)) { - // If rename fails, try copy + if (!copyDirectory(tempExtractPath, destPath)) { emit installFailed(appId, "Failed to copy app files to installation directory"); return false; @@ -315,7 +299,6 @@ bool MarathonAppInstaller::installFromPackage(const QString &packagePath) { emit installProgress(appId, 90); - // Rescan applications m_scanner->scanApplications(); emit installProgress(appId, 100); @@ -332,7 +315,6 @@ bool MarathonAppInstaller::canUninstall(const QString &appId) { bool MarathonAppInstaller::uninstallApp(const QString &appId) { qDebug() << "[MarathonAppInstaller] Uninstalling app:" << appId; - // Check if protected if (m_registry->isProtected(appId)) { QString error = "Cannot uninstall protected system app"; qWarning() << "[MarathonAppInstaller]" << error << ":" << appId; @@ -340,13 +322,11 @@ bool MarathonAppInstaller::uninstallApp(const QString &appId) { return false; } - // Check if installed if (!m_registry->hasApp(appId)) { emit uninstallFailed(appId, "App not found"); return false; } - // Get app info QVariantMap appInfo = m_registry->getApp(appId); QString appPath = appInfo.value("absolutePath").toString(); @@ -355,13 +335,11 @@ bool MarathonAppInstaller::uninstallApp(const QString &appId) { return false; } - // Remove directory if (!removeDirectory(appPath)) { emit uninstallFailed(appId, "Failed to remove app files"); return false; } - // Rescan to update registry m_scanner->scanApplications(); emit uninstallComplete(appId); diff --git a/marathon-core/src/marathonapppackager.h b/marathon-core/src/marathonapppackager.h index dc2eb1b75..bf9bdf7c5 100644 --- a/marathon-core/src/marathonapppackager.h +++ b/marathon-core/src/marathonapppackager.h @@ -9,13 +9,10 @@ class MarathonAppPackager : public QObject { public: explicit MarathonAppPackager(QObject *parent = nullptr); - // Create a .marathon package from an app directory - Q_INVOKABLE bool createPackage(const QString &appDir, const QString &outputPath); + Q_INVOKABLE bool createPackage(const QString &appDir, const QString &outputPath); - // Extract a .marathon package to a destination directory - Q_INVOKABLE bool extractPackage(const QString &packagePath, const QString &destDir); + Q_INVOKABLE bool extractPackage(const QString &packagePath, const QString &destDir); - // Get the last error message Q_INVOKABLE QString lastError() const { return m_lastError; } diff --git a/marathon-core/src/marathonappregistry.h b/marathon-core/src/marathonappregistry.h index 1442b3bae..88e8abee5 100644 --- a/marathon-core/src/marathonappregistry.h +++ b/marathon-core/src/marathonappregistry.h @@ -38,17 +38,31 @@ class MarathonAppRegistry : public QAbstractListModel { QString id; QString name; QString icon; - AppType type; + AppType type = Marathon; QString absolutePath; QString entryPoint; QString version; - bool isProtected; + bool isProtected = false; QStringList permissions; QStringList searchKeywords; - QString deepLinksJson; // JSON string for QML parsing + QString deepLinksJson; QStringList categories; QStringList handlesUriSchemes; QStringList defaultFor; + + AppInfo() = default; + AppInfo(const QString &id, const QString &name, const QString &icon, AppType type, + const QString &absolutePath, const QString &entryPoint, const QString &version, + bool isProtected, const QStringList &permissions) + : id(id) + , name(name) + , icon(icon) + , type(type) + , absolutePath(absolutePath) + , entryPoint(entryPoint) + , version(version) + , isProtected(isProtected) + , permissions(permissions) {} }; explicit MarathonAppRegistry(QObject *parent = nullptr); diff --git a/marathon-core/src/marathonappscanner.cpp b/marathon-core/src/marathonappscanner.cpp index c204ec2fd..d2e9714d5 100644 --- a/marathon-core/src/marathonappscanner.cpp +++ b/marathon-core/src/marathonappscanner.cpp @@ -22,7 +22,7 @@ MarathonAppScanner::MarathonAppScanner(MarathonAppRegistry *registry, QObject *p qDebug() << "[MarathonAppScanner] Initialized"; #ifdef HAVE_QT_CONCURRENT - // Connect async scan completion + connect(m_scanWatcher, &QFutureWatcher::finished, this, [this]() { int count = m_scanWatcher->result(); qDebug() << "[MarathonAppScanner] Async scan complete. Discovered:" << count << "apps"; @@ -35,14 +35,13 @@ QStringList MarathonAppScanner::getSearchPaths() { QStringList paths; #ifdef Q_OS_MACOS - // macOS: use /usr/local/share (user-writable) + paths << "/usr/local/share/marathon-apps"; #else - // Linux: use /usr/share (system apps) + paths << "/usr/share/marathon-apps"; #endif - // User apps directory (works on both platforms) QString homeDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); paths << homeDir + "/.local/share/marathon-apps"; @@ -51,7 +50,7 @@ QStringList MarathonAppScanner::getSearchPaths() { } void MarathonAppScanner::scanApplications() { - // Keep synchronous version for backwards compatibility + emit scanStarted(); int count = performScan(); emit scanComplete(count); @@ -62,17 +61,17 @@ void MarathonAppScanner::scanApplicationsAsync() { emit scanStarted(); #ifdef HAVE_QT_CONCURRENT - // Run scan in background thread using QtConcurrent + if (m_scanWatcher) { QFuture future = QtConcurrent::run([this]() { return this->performScan(); }); m_scanWatcher->setFuture(future); } else { - // Fallback to synchronous if Concurrent not available + int count = performScan(); emit scanComplete(count); } #else - // Fallback to synchronous if Concurrent not available + int count = performScan(); emit scanComplete(count); #endif @@ -95,7 +94,7 @@ int MarathonAppScanner::performScan() { qDebug() << "[MarathonAppScanner] Scanning directory:" << searchPath; QStringList appDirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - appDirs.sort(); // Sort alphabetically for consistent ordering + appDirs.sort(); for (const QString &appDir : appDirs) { QString appPath = dir.absoluteFilePath(appDir); @@ -111,9 +110,7 @@ int MarathonAppScanner::performScan() { MarathonAppRegistry::AppInfo appInfo = parseManifest(manifestPath, appPath); if (validateManifest(appInfo)) { - // absolutePath already set in parseManifest - // Register with registry if (!m_registry->hasApp(appInfo.id)) { m_registry->registerAppInfo(appInfo); emit appDiscovered(appInfo.id); @@ -168,14 +165,13 @@ MarathonAppRegistry::AppInfo MarathonAppScanner::parseManifest(const QString &ma info.name = obj.value("name").toString(); info.entryPoint = obj.value("entryPoint").toString(); - // Convert relative icon path to absolute with file:// prefix QString iconPath = obj.value("icon").toString(); if (!iconPath.isEmpty() && !iconPath.startsWith("qrc:") && !iconPath.startsWith("file://")) { if (!iconPath.startsWith("/")) { - // Relative path - make it absolute + iconPath = info.absolutePath + "/" + iconPath; } - // Add file:// prefix for QML Image component + info.icon = "file://" + iconPath; } else { info.icon = iconPath; @@ -184,35 +180,29 @@ MarathonAppRegistry::AppInfo MarathonAppScanner::parseManifest(const QString &ma info.version = obj.value("version").toString("1.0.0"); info.isProtected = obj.value("protected").toBool(false); - // Parse permissions array QJsonArray permissionsArray = obj.value("permissions").toArray(); for (const QJsonValue &value : permissionsArray) { info.permissions.append(value.toString()); } - // Parse searchKeywords array QJsonArray keywordsArray = obj.value("searchKeywords").toArray(); for (const QJsonValue &value : keywordsArray) { info.searchKeywords.append(value.toString()); } - // Parse deepLinks object QJsonObject deepLinksObj = obj.value("deepLinks").toObject(); info.deepLinksJson = QJsonDocument(deepLinksObj).toJson(QJsonDocument::Compact); - // Parse categories array QJsonArray categoriesArray = obj.value("categories").toArray(); for (const QJsonValue &value : categoriesArray) { info.categories.append(value.toString()); } - // Parse handlesUriSchemes array QJsonArray uriSchemesArray = obj.value("handlesUriSchemes").toArray(); for (const QJsonValue &value : uriSchemesArray) { info.handlesUriSchemes.append(value.toString()); } - // Parse defaultFor array QJsonArray defaultForArray = obj.value("defaultFor").toArray(); for (const QJsonValue &value : defaultForArray) { info.defaultFor.append(value.toString()); diff --git a/marathon-core/src/marathonappscanner.h b/marathon-core/src/marathonappscanner.h index 28ccb2020..2068da04a 100644 --- a/marathon-core/src/marathonappscanner.h +++ b/marathon-core/src/marathonappscanner.h @@ -15,13 +15,13 @@ class MarathonAppScanner : public QObject { explicit MarathonAppScanner(MarathonAppRegistry *registry, QObject *parent = nullptr); Q_INVOKABLE void scanApplications(); - Q_INVOKABLE void scanApplicationsAsync(); // New async method + Q_INVOKABLE void scanApplicationsAsync(); Q_INVOKABLE QString getManifestPath(const QString &appPath); signals: void scanStarted(); void appDiscovered(const QString &appId); - void scanProgress(int current, int total); // New progress signal + void scanProgress(int current, int total); void scanComplete(int count); void scanError(const QString &error); @@ -30,12 +30,12 @@ class MarathonAppScanner : public QObject { MarathonAppRegistry::AppInfo parseManifest(const QString &manifestPath, const QString &appDirPath); bool validateManifest(const MarathonAppRegistry::AppInfo &info); - int performScan(); // Actual scan logic (for thread), returns count + int performScan(); MarathonAppRegistry *m_registry; #ifdef HAVE_QT_CONCURRENT - QFutureWatcher *m_scanWatcher; // For async scanning + QFutureWatcher *m_scanWatcher; #else - void *m_scanWatcher; // Placeholder when Concurrent not available + void *m_scanWatcher; #endif }; diff --git a/marathon-core/src/marathonappverifier.h b/marathon-core/src/marathonappverifier.h index 5797c3b4e..003547a3f 100644 --- a/marathon-core/src/marathonappverifier.h +++ b/marathon-core/src/marathonappverifier.h @@ -22,26 +22,20 @@ class MarathonAppVerifier : public QObject { explicit MarathonAppVerifier(QObject *parent = nullptr); - // Verify a .marathon package or extracted app directory Q_INVOKABLE VerificationResult verifyPackage(const QString &packagePath); Q_INVOKABLE VerificationResult verifyDirectory(const QString &appDir); - // Sign a manifest file - Q_INVOKABLE bool signManifest(const QString &manifestPath, const QString &signaturePath, - const QString &keyId = QString()); + Q_INVOKABLE bool signManifest(const QString &manifestPath, const QString &signaturePath, + const QString &keyId = QString()); - // Check if a key is trusted - Q_INVOKABLE bool isTrustedKey(const QString &keyFingerprint); + Q_INVOKABLE bool isTrustedKey(const QString &keyFingerprint); - // Add/remove trusted keys - Q_INVOKABLE bool addTrustedKey(const QString &keyPath); - Q_INVOKABLE bool removeTrustedKey(const QString &keyFingerprint); + Q_INVOKABLE bool addTrustedKey(const QString &keyPath); + Q_INVOKABLE bool removeTrustedKey(const QString &keyFingerprint); - // Get list of trusted keys Q_INVOKABLE QStringList getTrustedKeys(); - // Get the last error message - Q_INVOKABLE QString lastError() const { + Q_INVOKABLE QString lastError() const { return m_lastError; } From 4764ed48b091bef9c7e3fad1ac6be8c1223f6d70 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:33:34 +0000 Subject: [PATCH 09/28] feat(app-runner): add calendar manager and IPC updates Add calendar manager support and expand shell IPC clients, with related runner wiring and tool entrypoint adjustments. --- tools/marathon-app-runner/CMakeLists.txt | 1 + .../calendarmanagercpp.cpp | 249 +++++++++ .../marathon-app-runner/calendarmanagercpp.h | 62 +++ .../ipc/shellipcclients.cpp | 482 ++++++++++++++++-- .../marathon-app-runner/ipc/shellipcclients.h | 217 +++++++- tools/marathon-app-runner/main.cpp | 118 ++--- tools/marathon-dev/main.cpp | 22 +- tools/marathon-sandbox/main.c | 45 +- 8 files changed, 1043 insertions(+), 153 deletions(-) create mode 100644 tools/marathon-app-runner/calendarmanagercpp.cpp create mode 100644 tools/marathon-app-runner/calendarmanagercpp.h diff --git a/tools/marathon-app-runner/CMakeLists.txt b/tools/marathon-app-runner/CMakeLists.txt index 1fabeab97..b9d289e65 100644 --- a/tools/marathon-app-runner/CMakeLists.txt +++ b/tools/marathon-app-runner/CMakeLists.txt @@ -17,6 +17,7 @@ find_package(Qt6 REQUIRED COMPONENTS set(SOURCES main.cpp ipc/shellipcclients.cpp + calendarmanagercpp.cpp ) qt6_add_executable(marathon-app-runner ${SOURCES}) diff --git a/tools/marathon-app-runner/calendarmanagercpp.cpp b/tools/marathon-app-runner/calendarmanagercpp.cpp new file mode 100644 index 000000000..0bf3785c7 --- /dev/null +++ b/tools/marathon-app-runner/calendarmanagercpp.cpp @@ -0,0 +1,249 @@ +#include "calendarmanagercpp.h" +#include "ipc/shellipcclients.h" + +#include +#include +#include +#include +#include + +CalendarManagerCpp::CalendarManagerCpp(SettingsClient *settings, NotificationClient *notifications, + QObject *parent) + : QObject(parent) + , m_settings(settings) + , m_notifications(notifications) { + load(); + updateNextEventId(); + emit eventsLoaded(); + + m_checkTimer = new QTimer(this); + m_checkTimer->setInterval(60 * 1000); + connect(m_checkTimer, &QTimer::timeout, this, &CalendarManagerCpp::checkReminders); + m_checkTimer->start(); + checkReminders(); +} + +QVariantMap CalendarManagerCpp::createEvent(const QVariantMap &event) { + QVariantMap normalized = normalizeEvent(event); + normalized["id"] = m_nextEventId++; + normalized["timestamp"] = QDateTime::currentMSecsSinceEpoch(); + m_events.append(normalized); + save(); + emit eventsChanged(); + emit nextEventIdChanged(); + emit eventCreated(normalized); + return normalized; +} + +bool CalendarManagerCpp::updateEvent(const QVariantMap &event) { + if (!event.contains("id")) + return false; + + const int id = event.value("id").toInt(); + for (int i = 0; i < m_events.size(); ++i) { + const QVariantMap existing = m_events[i].toMap(); + if (existing.value("id").toInt() == id) { + QVariantMap normalized = normalizeEvent(event); + normalized["id"] = id; + m_events[i] = normalized; + save(); + emit eventsChanged(); + emit eventUpdated(normalized); + return true; + } + } + return false; +} + +bool CalendarManagerCpp::deleteEvent(int id) { + for (int i = 0; i < m_events.size(); ++i) { + const QVariantMap existing = m_events[i].toMap(); + if (existing.value("id").toInt() == id) { + m_events.removeAt(i); + save(); + emit eventsChanged(); + emit eventDeleted(id); + return true; + } + } + return false; +} + +QVariantList CalendarManagerCpp::getEventsForDate(const QDateTime &date) const { + const QString dateStr = date.date().toString("yyyy-MM-dd"); + QVariantList result; + for (const QVariant &entry : m_events) { + const QVariantMap event = entry.toMap(); + const QString eventDateStr = event.value("date").toString(); + if (eventDateStr == dateStr) { + result.append(event); + continue; + } + + const QString recurring = event.value("recurring", "none").toString(); + if (recurring == "none") + continue; + + const QDate eventDate = QDate::fromString(eventDateStr, "yyyy-MM-dd"); + const QDate checkDate = date.date(); + if (!eventDate.isValid() || checkDate < eventDate) + continue; + + if (recurring == "daily") { + result.append(event); + } else if (recurring == "weekly") { + const int daysDiff = eventDate.daysTo(checkDate); + if (daysDiff % 7 == 0) + result.append(event); + } else if (recurring == "monthly") { + if (eventDate.day() == checkDate.day()) + result.append(event); + } + } + return result; +} + +QVariantList CalendarManagerCpp::getAllEvents() const { + return m_events; +} + +void CalendarManagerCpp::checkReminders() { + const QDateTime now = QDateTime::currentDateTime(); + for (const QVariant &entry : m_events) { + const QVariantMap event = entry.toMap(); + if (!shouldTriggerEvent(event, now)) + continue; + + const QString key = triggerKeyForEvent(event, now); + if (m_triggered.contains(key)) + continue; + + triggerNotification(event); + m_triggered.append(key); + if (m_triggered.size() > 50) + m_triggered.removeFirst(); + } +} + +void CalendarManagerCpp::load() { + if (!m_settings) { + m_events.clear(); + return; + } + + const QVariant raw = m_settings->get("calendar/events", QVariantList()); + if (raw.canConvert()) { + m_events = raw.toList(); + return; + } + + if (raw.typeId() == QMetaType::QString) { + const QJsonDocument doc = QJsonDocument::fromJson(raw.toString().toUtf8()); + if (!doc.isArray()) { + m_events.clear(); + return; + } + const QJsonArray arr = doc.array(); + m_events.clear(); + m_events.reserve(arr.size()); + for (const QJsonValue &value : arr) { + if (value.isObject()) + m_events.append(value.toObject().toVariantMap()); + } + return; + } + + m_events.clear(); +} + +void CalendarManagerCpp::save() { + if (!m_settings) + return; + m_settings->set("calendar/events", m_events); +} + +void CalendarManagerCpp::updateNextEventId() { + int maxId = 0; + for (const QVariant &entry : m_events) { + const QVariantMap event = entry.toMap(); + maxId = std::max(maxId, event.value("id").toInt()); + } + if (maxId + 1 != m_nextEventId) { + m_nextEventId = maxId + 1; + emit nextEventIdChanged(); + } +} + +QVariantMap CalendarManagerCpp::normalizeEvent(const QVariantMap &event) const { + QVariantMap normalized = event; + if (!normalized.contains("title")) + normalized["title"] = ""; + if (!normalized.contains("date")) + normalized["date"] = ""; + if (!normalized.contains("time")) + normalized["time"] = ""; + if (!normalized.contains("allDay")) + normalized["allDay"] = false; + if (!normalized.contains("recurring")) + normalized["recurring"] = "none"; + if (!normalized.contains("description")) + normalized["description"] = ""; + return normalized; +} + +bool CalendarManagerCpp::shouldTriggerEvent(const QVariantMap &event, const QDateTime &now) const { + const QString dateStr = now.date().toString("yyyy-MM-dd"); + const QString timeStr = now.time().toString("HH:mm"); + const QString eventDate = event.value("date").toString(); + const QString eventTime = event.value("time").toString(); + if (eventTime != timeStr) + return false; + + const QString recurring = event.value("recurring", "none").toString(); + if (eventDate == dateStr) + return true; + + if (recurring == "none") + return false; + + const QDate baseDate = QDate::fromString(eventDate, "yyyy-MM-dd"); + if (!baseDate.isValid() || now.date() < baseDate) + return false; + + if (recurring == "daily") + return true; + if (recurring == "weekly") { + const int daysDiff = baseDate.daysTo(now.date()); + return daysDiff % 7 == 0; + } + if (recurring == "monthly") + return baseDate.day() == now.date().day(); + + return false; +} + +void CalendarManagerCpp::triggerNotification(const QVariantMap &event) { + if (!m_notifications) + return; + + const QString title = event.value("title").toString(); + const QString time = event.value("time").toString(); + const bool allDay = event.value("allDay").toBool(); + const QString body = allDay ? QStringLiteral("%1 (All Day)").arg(time) : time; + + QVariantMap options; + options.insert("icon", "qrc:/images/calendar.svg"); + options.insert("category", "reminder"); + options.insert("priority", "high"); + options.insert("actions", QVariantList{"Dismiss"}); + + m_notifications->sendNotification("calendar", title, body, options); +} + +QString CalendarManagerCpp::triggerKeyForEvent(const QVariantMap &event, + const QDateTime &now) const { + return QString("%1_%2_%3") + .arg(event.value("id").toInt()) + .arg(now.date().toString("yyyy-MM-dd")) + .arg(now.time().toString("HH:mm")); +} diff --git a/tools/marathon-app-runner/calendarmanagercpp.h b/tools/marathon-app-runner/calendarmanagercpp.h new file mode 100644 index 000000000..295ee97d1 --- /dev/null +++ b/tools/marathon-app-runner/calendarmanagercpp.h @@ -0,0 +1,62 @@ +#ifndef CALENDARMANAGERCPP_H +#define CALENDARMANAGERCPP_H + +#include +#include +#include +#include + +class SettingsClient; +class NotificationClient; + +class CalendarManagerCpp : public QObject { + Q_OBJECT + Q_PROPERTY(QVariantList events READ events NOTIFY eventsChanged) + Q_PROPERTY(int nextEventId READ nextEventId NOTIFY nextEventIdChanged) + + public: + explicit CalendarManagerCpp(SettingsClient *settings, NotificationClient *notifications, + QObject *parent = nullptr); + + QVariantList events() const { + return m_events; + } + int nextEventId() const { + return m_nextEventId; + } + + Q_INVOKABLE QVariantMap createEvent(const QVariantMap &event); + Q_INVOKABLE bool updateEvent(const QVariantMap &event); + Q_INVOKABLE bool deleteEvent(int id); + Q_INVOKABLE QVariantList getEventsForDate(const QDateTime &date) const; + Q_INVOKABLE QVariantList getAllEvents() const; + + signals: + void eventsChanged(); + void nextEventIdChanged(); + void eventCreated(const QVariantMap &event); + void eventUpdated(const QVariantMap &event); + void eventDeleted(int eventId); + void eventsLoaded(); + + private slots: + void checkReminders(); + + private: + void load(); + void save(); + void updateNextEventId(); + QVariantMap normalizeEvent(const QVariantMap &event) const; + bool shouldTriggerEvent(const QVariantMap &event, const QDateTime &now) const; + void triggerNotification(const QVariantMap &event); + QString triggerKeyForEvent(const QVariantMap &event, const QDateTime &now) const; + + SettingsClient *m_settings = nullptr; + NotificationClient *m_notifications = nullptr; + QVariantList m_events; + int m_nextEventId = 1; + QTimer *m_checkTimer = nullptr; + QStringList m_triggered; +}; + +#endif diff --git a/tools/marathon-app-runner/ipc/shellipcclients.cpp b/tools/marathon-app-runner/ipc/shellipcclients.cpp index 4a1ebd7cc..5b65acbb7 100644 --- a/tools/marathon-app-runner/ipc/shellipcclients.cpp +++ b/tools/marathon-app-runner/ipc/shellipcclients.cpp @@ -1,6 +1,7 @@ #include "shellipcclients.h" #include +#include #include #include #include @@ -9,7 +10,6 @@ #include #include -// Helper to reduce noise when access is denied: that's an expected state until permissions are granted. static bool isAccessDenied(const QDBusError &e) { return e.type() == QDBusError::AccessDenied; } @@ -37,28 +37,23 @@ static QVariantMap normalizeMapDeep(const QVariantMap &in) { } static QVariant normalizeDbusVariantDeep(QVariant v) { - // Peel DBus "variant" wrappers first. + if (v.canConvert()) v = v.value().variant(); - // If this value is still a QDBusArgument, try to coerce into a Qt container type. - // This is critical for arrays/maps that otherwise become `undefined` in QML. if (v.userType() == qMetaTypeId()) { - // Try map first (for a{sv}). + const QVariantMap asMap = qdbus_cast(v); if (!asMap.isEmpty()) return normalizeMapDeep(asMap); - // Then list (for arrays). const QVariantList asList = qdbus_cast(v); if (!asList.isEmpty()) return normalizeListDeep(asList); - // Fall back to original (best effort). return v; } - // Recurse into containers. if (v.metaType().id() == QMetaType::QVariantList) return normalizeListDeep(v.toList()); if (v.metaType().id() == QMetaType::QVariantMap) @@ -68,7 +63,7 @@ static QVariant normalizeDbusVariantDeep(QVariant v) { } static QVariantList unwrapDbusVariantList(const QVariant &v) { - // Convert payload to a plain QVariantList of plain QVariantMap values for QML consumption. + QVariantList raw; if (v.canConvert()) { raw = unwrapDbusVariantList(v.value().variant()); @@ -88,7 +83,13 @@ static QDBusInterface makePermissionsIface(QObject *parent) { parent); } -// ---- PermissionClient ---- +static QString notificationServiceName() { + auto *iface = QDBusConnection::sessionBus().interface(); + if (iface && iface->isServiceRegistered("org.marathon.Notifications")) + return QStringLiteral("org.marathon.Notifications"); + return QStringLiteral("org.freedesktop.Notifications"); +} + PermissionClient::PermissionClient(QObject *parent) : QObject(parent) , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Permissions", @@ -132,7 +133,6 @@ void PermissionClient::setPermission(const QString &appId, const QString &permis qWarning() << "[PermissionClient] SetPermission failed:" << r.errorMessage(); } -// ---- ContactsClient ---- ContactsClient::ContactsClient(QObject *parent) : QObject(parent) , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Contacts", @@ -216,7 +216,6 @@ QVariantMap ContactsClient::getContactByNumber(const QString &phoneNumber) { return r.value(); } -// ---- CallHistoryClient ---- CallHistoryClient::CallHistoryClient(QObject *parent) : QObject(parent) , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/CallHistory", @@ -287,11 +286,12 @@ QVariantList CallHistoryClient::getCallsByNumber(const QString &number) { return r.value(); } -// ---- TelephonyClient ---- -TelephonyClient::TelephonyClient(QObject *parent) +TelephonyClient::TelephonyClient(const QString &appId, QObject *parent) : QObject(parent) + , m_appId(appId) , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Telephony", - "org.marathonos.Shell.Telephony1", QDBusConnection::sessionBus()) { + "org.marathonos.Shell.Telephony1", QDBusConnection::sessionBus()) + , m_permIface(makePermissionsIface(this)) { if (!m_iface.isValid()) { qWarning() << "[TelephonyClient] DBus interface invalid:" << m_iface.lastError().message(); return; @@ -315,6 +315,29 @@ TelephonyClient::TelephonyClient(QObject *parent) refresh(); } +bool TelephonyClient::ensurePermission() { + if (m_appId.isEmpty()) + return false; + QDBusReply rTelephony = m_permIface.call("HasPermission", m_appId, "telephony"); + if (rTelephony.isValid() && rTelephony.value()) { + m_permissionRequested = false; + return true; + } + QDBusReply rPhone = m_permIface.call("HasPermission", m_appId, "phone"); + if (rPhone.isValid() && rPhone.value()) { + m_permissionRequested = false; + return true; + } + if (!rTelephony.isValid() && !rPhone.isValid()) + return false; + if (!m_permissionRequested) { + m_permissionRequested = true; + m_permIface.call("RequestPermission", m_appId, "telephony"); + } + QTimer::singleShot(750, this, &TelephonyClient::refresh); + return false; +} + void TelephonyClient::setCallState(const QString &s) { if (m_callState == s) return; @@ -335,6 +358,8 @@ void TelephonyClient::setActiveNumber(const QString &n) { } void TelephonyClient::refresh() { + if (!ensurePermission()) + return; QDBusReply cs = m_iface.call("CallState"); if (cs.isValid()) setCallState(cs.value()); @@ -347,31 +372,56 @@ void TelephonyClient::refresh() { } void TelephonyClient::dial(const QString &number) { + if (!ensurePermission()) + return; auto r = m_iface.call("Dial", number); if (r.type() == QDBusMessage::ErrorMessage) qWarning() << "[TelephonyClient] Dial failed:" << r.errorMessage(); } void TelephonyClient::answer() { + if (!ensurePermission()) + return; auto r = m_iface.call("Answer"); if (r.type() == QDBusMessage::ErrorMessage) qWarning() << "[TelephonyClient] Answer failed:" << r.errorMessage(); } void TelephonyClient::hangup() { + if (!ensurePermission()) + return; auto r = m_iface.call("Hangup"); if (r.type() == QDBusMessage::ErrorMessage) qWarning() << "[TelephonyClient] Hangup failed:" << r.errorMessage(); } void TelephonyClient::sendDTMF(const QString &digit) { + if (!ensurePermission()) + return; auto r = m_iface.call("SendDTMF", digit); if (r.type() == QDBusMessage::ErrorMessage) qWarning() << "[TelephonyClient] SendDTMF failed:" << r.errorMessage(); } -// ---- SmsClient ---- -SmsClient::SmsClient(QObject *parent) +void TelephonyClient::simulateIncomingCall(const QString &number) { + if (!ensurePermission()) + return; + auto r = m_iface.call("SimulateIncomingCall", number); + if (r.type() == QDBusMessage::ErrorMessage) + qWarning() << "[TelephonyClient] SimulateIncomingCall failed:" << r.errorMessage(); +} + +void TelephonyClient::simulateCallStateChange(const QString &state) { + if (!ensurePermission()) + return; + auto r = m_iface.call("SimulateCallStateChange", state); + if (r.type() == QDBusMessage::ErrorMessage) + qWarning() << "[TelephonyClient] SimulateCallStateChange failed:" << r.errorMessage(); +} + +SmsClient::SmsClient(const QString &appId, QObject *parent) : QObject(parent) + , m_appId(appId) , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Sms", "org.marathonos.Shell.Sms1", - QDBusConnection::sessionBus()) { + QDBusConnection::sessionBus()) + , m_permIface(makePermissionsIface(this)) { if (!m_iface.isValid()) { qWarning() << "[SmsClient] DBus interface invalid:" << m_iface.lastError().message(); return; @@ -391,7 +441,28 @@ SmsClient::SmsClient(QObject *parent) refresh(); } -// ---- MediaLibraryClient ---- +bool SmsClient::ensurePermission() { + if (m_appId.isEmpty()) + return false; + QDBusReply rSms = m_permIface.call("HasPermission", m_appId, "sms"); + if (rSms.isValid() && rSms.value()) { + m_permissionRequested = false; + return true; + } + QDBusReply rPhone = m_permIface.call("HasPermission", m_appId, "phone"); + if (rPhone.isValid() && rPhone.value()) { + m_permissionRequested = false; + return true; + } + if (!rSms.isValid() && !rPhone.isValid()) + return false; + if (!m_permissionRequested) { + m_permissionRequested = true; + m_permIface.call("RequestPermission", m_appId, "sms"); + } + QTimer::singleShot(750, this, &SmsClient::refresh); + return false; +} MediaLibraryClient::MediaLibraryClient(const QString &appId, QObject *parent) : QObject(parent) @@ -466,7 +537,7 @@ void MediaLibraryClient::setScanProgress(int progress) { } void MediaLibraryClient::onLibraryChanged() { - // Update counts on library changes + QDBusReply p = m_iface.call("PhotoCount"); if (p.isValid()) m_photoCount = p.value(); @@ -573,8 +644,6 @@ void MediaLibraryClient::deleteMedia(int mediaId) { qWarning() << "[MediaLibraryClient] DeleteMedia failed:" << r.errorMessage(); } -// ---- SettingsClient ---- - SettingsClient::SettingsClient(const QString &appId, QObject *parent) : QObject(parent) , m_appId(appId) @@ -614,7 +683,6 @@ bool SettingsClient::ensureSystemPermission() { << req.errorMessage(); } - // Retry later; this becomes valid once the user grants permission. QTimer::singleShot(750, this, &SettingsClient::refresh); return false; } @@ -662,13 +730,11 @@ void SettingsClient::refresh() { return; m_refreshInFlight = true; - // Global Settings state is only available to "system" apps. if (!ensureSystemPermission()) { m_refreshInFlight = false; return; } - // Avoid blocking the UI thread with long DBus timeouts during startup. m_iface.setTimeout(2000); auto async = m_iface.asyncCall("GetState"); @@ -682,13 +748,13 @@ void SettingsClient::refresh() { if (!isAccessDenied(r.error()) && debugEnabled()) qWarning() << "[SettingsClient] GetState failed:" << r.error().name() << r.error().message(); - // Retry quickly; Settings needs this for core functionality. + QTimer::singleShot(250, this, &SettingsClient::refresh); return; } m_state = r.value(); - // Emit a broad refresh for key properties. + emit userScaleFactorChanged(); emit wallpaperPathChanged(); emit deviceNameChanged(); @@ -744,7 +810,7 @@ void SettingsClient::setProp(const QString &name, const QVariant &value) { qWarning() << "[SettingsClient] SetProperty failed:" << msg.errorMessage(); return; } - // Optimistic local update; shell will also broadcast PropertyChanged. + m_state.insert(name, value); onPropertyChanged(name, QDBusVariant(value)); } @@ -753,7 +819,6 @@ void SettingsClient::onPropertyChanged(const QString &name, const QDBusVariant & const QVariant v = value.variant(); m_state.insert(name, v); - // Emit per-property change signals (QML relies on these). if (name == "userScaleFactor") emit userScaleFactorChanged(); else if (name == "wallpaperPath") @@ -835,7 +900,6 @@ void SettingsClient::onPropertyChanged(const QString &name, const QDBusVariant & Q_UNUSED(v); } -// Getters qreal SettingsClient::userScaleFactor() const { return prop("userScaleFactor", 1.0).toReal(); } @@ -961,7 +1025,6 @@ QString SettingsClient::keyboardHapticStrength() const { return prop("keyboardHapticStrength", "medium").toString(); } -// Setters void SettingsClient::setUserScaleFactor(qreal v) { setProp("userScaleFactor", v); } @@ -1088,22 +1151,32 @@ void SettingsClient::setKeyboardHapticStrength(const QString &v) { } QStringList SettingsClient::availableRingtones() { + if (!ensureSystemPermission()) + return {}; QDBusReply r = m_iface.call("AvailableRingtones"); return r.isValid() ? r.value() : QStringList{}; } QStringList SettingsClient::availableNotificationSounds() { + if (!ensureSystemPermission()) + return {}; QDBusReply r = m_iface.call("AvailableNotificationSounds"); return r.isValid() ? r.value() : QStringList{}; } QStringList SettingsClient::availableAlarmSounds() { + if (!ensureSystemPermission()) + return {}; QDBusReply r = m_iface.call("AvailableAlarmSounds"); return r.isValid() ? r.value() : QStringList{}; } QStringList SettingsClient::screenTimeoutOptions() { + if (!ensureSystemPermission()) + return {}; QDBusReply r = m_iface.call("ScreenTimeoutOptions"); return r.isValid() ? r.value() : QStringList{}; } int SettingsClient::screenTimeoutValue(const QString &option) { + if (!ensureSystemPermission()) + return 0; QDBusReply r = m_iface.call("ScreenTimeoutValue", option); return r.isValid() ? r.value() : 0; } @@ -1134,7 +1207,7 @@ void SettingsClient::set(const QString &key, const QVariant &value) { qWarning() << "[SettingsClient] Set failed:" << r.errorMessage(); } void SettingsClient::sync() { - // Shell allows Sync() for either "system" or "storage" callers. + if (!ensureSystemPermission()) { if (!ensureStoragePermission()) return; @@ -1144,8 +1217,6 @@ void SettingsClient::sync() { qWarning() << "[SettingsClient] Sync failed:" << r.errorMessage(); } -// ---- BluetoothClient ---- - BluetoothClient::BluetoothClient(const QString &appId, QObject *parent) : QObject(parent) , m_appId(appId) @@ -1247,7 +1318,7 @@ void BluetoothClient::refresh() { void BluetoothClient::onStateChanged(const QVariantMap &state) { QVariantMap normalized = state; - // Normalize nested list payloads so QML gets real JS objects (not QDBusArgument/QDBusVariant). + normalized["devices"] = unwrapDbusVariantList(normalized.value("devices")); normalized["pairedDevices"] = unwrapDbusVariantList(normalized.value("pairedDevices")); m_state = normalized; @@ -1319,8 +1390,6 @@ void BluetoothClient::cancelPairing(const QString &address) { qWarning() << "[BluetoothClient] CancelPairing failed:" << r.errorMessage(); } -// ---- DisplayClient ---- - DisplayClient::DisplayClient(const QString &appId, QObject *parent) : QObject(parent) , m_appId(appId) @@ -1475,8 +1544,6 @@ void DisplayClient::setScreenState(bool on) { qWarning() << "[DisplayClient] SetScreenState failed:" << r.errorMessage(); } -// ---- RemoteAudioStreamModel ---- - RemoteAudioStreamModel::RemoteAudioStreamModel(QObject *parent) : QAbstractListModel(parent) {} @@ -1519,8 +1586,6 @@ void RemoteAudioStreamModel::setStreams(const QVariantList &streams) { endResetModel(); } -// ---- AudioClient ---- - AudioClient::AudioClient(const QString &appId, QObject *parent) : QObject(parent) , m_appId(appId) @@ -1624,6 +1689,10 @@ bool AudioClient::perAppVolumeSupported() const { return v("perAppVolumeSupported").toBool(); } +QString AudioClient::audioProfile() const { + return v("audioProfile").toString(); +} + void AudioClient::setVolume(double v) { auto r = m_iface.call("SetVolume", v); if (r.type() == QDBusMessage::ErrorMessage) @@ -1695,8 +1764,6 @@ void AudioClient::vibratePattern(const QVariantList &pattern) { qWarning() << "[AudioClient] VibratePattern failed:" << r.errorMessage(); } -// ---- PowerClient ---- - PowerClient::PowerClient(const QString &appId, QObject *parent) : QObject(parent) , m_appId(appId) @@ -1834,8 +1901,6 @@ void PowerClient::shutdown() { qWarning() << "[PowerClient] Shutdown failed:" << r.errorMessage(); } -// ---- NetworkClient ---- - NetworkClient::NetworkClient(const QString &appId, QObject *parent) : QObject(parent) , m_appId(appId) @@ -2088,6 +2153,8 @@ void SmsClient::setConversations(const QVariantList &v) { } void SmsClient::refresh() { + if (!ensurePermission()) + return; QDBusReply r = m_iface.call("GetConversations"); if (!r.isValid()) { if (r.error().type() != QDBusError::AccessDenied) @@ -2098,12 +2165,16 @@ void SmsClient::refresh() { } void SmsClient::sendMessage(const QString &recipient, const QString &text) { + if (!ensurePermission()) + return; auto r = m_iface.call("SendMessage", recipient, text); if (r.type() == QDBusMessage::ErrorMessage) qWarning() << "[SmsClient] SendMessage failed:" << r.errorMessage(); } QVariantList SmsClient::getMessages(const QString &conversationId) { + if (!ensurePermission()) + return {}; QDBusReply r = m_iface.call("GetMessages", conversationId); if (!r.isValid()) { qWarning() << "[SmsClient] GetMessages failed:" << r.error().message(); @@ -2113,18 +2184,24 @@ QVariantList SmsClient::getMessages(const QString &conversationId) { } void SmsClient::deleteConversation(const QString &conversationId) { + if (!ensurePermission()) + return; auto r = m_iface.call("DeleteConversation", conversationId); if (r.type() == QDBusMessage::ErrorMessage) qWarning() << "[SmsClient] DeleteConversation failed:" << r.errorMessage(); } void SmsClient::markAsRead(const QString &conversationId) { + if (!ensurePermission()) + return; auto r = m_iface.call("MarkAsRead", conversationId); if (r.type() == QDBusMessage::ErrorMessage) qWarning() << "[SmsClient] MarkAsRead failed:" << r.errorMessage(); } QString SmsClient::generateConversationId(const QString &number) { + if (!ensurePermission()) + return {}; QDBusReply r = m_iface.call("GenerateConversationId", number); if (!r.isValid()) { qWarning() << "[SmsClient] GenerateConversationId failed:" << r.error().message(); @@ -2133,6 +2210,14 @@ QString SmsClient::generateConversationId(const QString &number) { return r.value(); } +void SmsClient::simulateIncomingSMS(const QString &sender, const QString &message) { + if (!ensurePermission()) + return; + auto r = m_iface.call("SimulateIncomingSMS", sender, message); + if (r.type() == QDBusMessage::ErrorMessage) + qWarning() << "[SmsClient] SimulateIncomingSMS failed:" << r.errorMessage(); +} + NavigationClient::NavigationClient(QObject *parent) : QObject(parent) , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Navigation", @@ -2191,3 +2276,310 @@ bool NavigationClient::launchAppWithRoute(const QString &appId, const QString &r } return r.value(); } + +HapticClient::HapticClient(QObject *parent) + : QObject(parent) + , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Haptic", + "org.marathonos.Shell.Haptic1", QDBusConnection::sessionBus()) { + if (!m_iface.isValid()) + qWarning() << "[HapticClient] DBus interface invalid:" << m_iface.lastError().message(); + refresh(); +} + +void HapticClient::refresh() { + QDBusReply r = m_iface.call("IsAvailable"); + if (r.isValid() && r.value() != m_available) { + m_available = r.value(); + emit availableChanged(); + } +} + +void HapticClient::light() { + m_iface.call("Light"); +} + +void HapticClient::medium() { + m_iface.call("Medium"); +} + +void HapticClient::heavy() { + m_iface.call("Heavy"); +} + +void HapticClient::vibrate(int duration) { + m_iface.call("Vibrate", duration); +} + +void HapticClient::vibratePattern(const QVariantList &pattern, int repeat) { + Q_UNUSED(repeat) + m_iface.call("VibratePattern", pattern); +} + +void HapticClient::stopVibration() { + m_iface.call("Stop"); +} + +NotificationClient::NotificationClient(const QString &appId, QObject *parent) + : QObject(parent) + , m_appId(appId) + , m_iface(notificationServiceName(), "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", QDBusConnection::sessionBus()) + , m_permIface(makePermissionsIface(this)) { + if (!m_iface.isValid()) + qWarning() << "[NotificationClient] DBus interface invalid:" + << m_iface.lastError().message(); + + QDBusConnection::sessionBus().connect(m_iface.service(), "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", "NotificationClosed", + this, SLOT(notificationClosed(int, int))); + QDBusConnection::sessionBus().connect(m_iface.service(), "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", "ActionInvoked", this, + SLOT(actionInvoked(int, QString))); +} + +bool NotificationClient::ensurePermission() { + if (m_appId.isEmpty()) + return false; + QDBusReply r = m_permIface.call("HasPermission", m_appId, "notifications"); + if (!r.isValid()) + return false; + if (r.value()) { + m_permissionRequested = false; + return true; + } + if (!m_permissionRequested) { + m_permissionRequested = true; + m_permIface.call("RequestPermission", m_appId, "notifications"); + } + return false; +} + +int NotificationClient::sendNotification(const QString &appId, const QString &title, + const QString &body, const QVariantMap &options) { + if (!ensurePermission()) + return -1; + + QStringList actions; + if (options.contains("actions")) { + QVariantList actionList = options.value("actions").toList(); + for (const QVariant &a : actionList) { + actions << a.toString() << a.toString(); + } + } + + QVariantMap hints; + if (options.contains("category")) + hints["category"] = options.value("category"); + if (options.contains("priority")) { + QString priority = options.value("priority").toString(); + hints["urgency"] = (priority == "high") ? 2 : ((priority == "low") ? 0 : 1); + } + hints["desktop-entry"] = appId; + + QString icon = options.value("icon").toString(); + int timeout = options.value("persistent", false).toBool() ? 0 : 5000; + + QDBusReply r = + m_iface.call("Notify", appId, 0u, icon, title, body, actions, hints, timeout); + if (!r.isValid()) { + qWarning() << "[NotificationClient] Notify failed:" << r.error().message(); + return -1; + } + return static_cast(r.value()); +} + +void NotificationClient::dismissNotification(int id) { + m_iface.call("CloseNotification", static_cast(id)); +} + +void NotificationClient::dismissAllNotifications() { + + qWarning() << "[NotificationClient] dismissAllNotifications not supported by freedesktop spec"; +} + +SensorClient::SensorClient(QObject *parent) + : QObject(parent) + , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Sensor", + "org.marathonos.Shell.Sensor1", QDBusConnection::sessionBus()) { + if (!m_iface.isValid()) + qWarning() << "[SensorClient] DBus interface invalid:" << m_iface.lastError().message(); + + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Sensor", + "org.marathonos.Shell.Sensor1", "ProximityChanged", this, + SIGNAL(proximityChanged())); + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Sensor", + "org.marathonos.Shell.Sensor1", "AmbientLightChanged", + this, SIGNAL(ambientLightChanged())); + + refresh(); +} + +void SensorClient::refresh() { + if (m_iface.isValid()) { + bool avail = m_iface.property("available").toBool(); + bool prox = m_iface.property("proximityNear").toBool(); + int light = m_iface.property("ambientLight").toInt(); + + if (avail != m_available) { + m_available = avail; + emit availableChanged(); + } + if (prox != m_proximityNear) { + m_proximityNear = prox; + emit proximityChanged(); + } + if (light != m_ambientLight) { + m_ambientLight = light; + emit ambientLightChanged(); + } + } +} + +LocationClient::LocationClient(QObject *parent) + : QObject(parent) + , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Location", + "org.marathonos.Shell.Location1", QDBusConnection::sessionBus()) { + if (!m_iface.isValid()) + qWarning() << "[LocationClient] DBus interface invalid:" << m_iface.lastError().message(); + + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Location", + "org.marathonos.Shell.Location1", "LocationChanged", this, + SLOT(updateLocation(QVariantMap))); + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Location", + "org.marathonos.Shell.Location1", "activeChanged", this, + SIGNAL(activeChanged())); + refresh(); +} + +void LocationClient::refresh() { + if (m_iface.isValid()) { + bool avail = m_iface.property("available").toBool(); + bool active = m_iface.property("active").toBool(); + + if (avail != m_available) { + m_available = avail; + emit availableChanged(); + } + if (active != m_active) { + m_active = active; + emit activeChanged(); + } + updateLocation(m_iface.call("GetLocation").arguments().at(0).value()); + } +} + +void LocationClient::updateLocation(const QVariantMap &loc) { + if (loc.isEmpty()) + return; + + m_latitude = loc.value("latitude").toDouble(); + m_longitude = loc.value("longitude").toDouble(); + m_accuracy = loc.value("accuracy").toDouble(); + m_altitude = loc.value("altitude").toDouble(); + m_speed = loc.value("speed").toDouble(); + m_heading = loc.value("heading").toDouble(); + m_timestamp = loc.value("timestamp").toLongLong(); + emit locationChanged(); +} + +void LocationClient::start() { + m_iface.call("Start"); +} + +void LocationClient::stop() { + m_iface.call("Stop"); +} + +AlarmClient::AlarmClient(QObject *parent) + : QObject(parent) + , m_iface("org.marathonos.Shell", "/org/marathonos/Shell/Alarm", "org.marathonos.Shell.Alarm1", + QDBusConnection::sessionBus()) { + if (!m_iface.isValid()) + qWarning() << "[AlarmClient] DBus interface invalid:" << m_iface.lastError().message(); + + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Alarm", + "org.marathonos.Shell.Alarm1", "AlarmsChanged", this, + SLOT(onAlarmsChanged())); + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Alarm", + "org.marathonos.Shell.Alarm1", "ActiveAlarmsChanged", + this, SLOT(onActiveAlarmsChanged())); + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Alarm", + "org.marathonos.Shell.Alarm1", "AlarmTriggered", this, + SIGNAL(alarmTriggered(QString, QString))); + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Alarm", + "org.marathonos.Shell.Alarm1", "AlarmDismissed", this, + SIGNAL(alarmDismissed(QString))); + QDBusConnection::sessionBus().connect("org.marathonos.Shell", "/org/marathonos/Shell/Alarm", + "org.marathonos.Shell.Alarm1", "AlarmSnoozed", this, + SIGNAL(alarmSnoozed(QString, int))); + + refresh(); +} + +void AlarmClient::refresh() { + if (m_iface.isValid()) { + onAlarmsChanged(); + onActiveAlarmsChanged(); + } +} + +void AlarmClient::onAlarmsChanged() { + QDBusReply r = m_iface.call("GetAlarms"); + if (r.isValid()) { + m_alarms = r.value(); + emit alarmsChanged(); + } +} + +void AlarmClient::onActiveAlarmsChanged() { + QDBusReply r = m_iface.call("GetActiveAlarms"); + if (r.isValid()) { + m_activeAlarms = r.value(); + emit activeAlarmsChanged(); + } +} + +QString AlarmClient::createAlarm(const QString &time, const QString &label, + const QList &repeat, const QVariantMap &options) { + QDBusReply r = + m_iface.call("CreateAlarm", time, label, QVariant::fromValue(repeat), options); + return r.isValid() ? r.value() : QString(); +} + +bool AlarmClient::updateAlarm(const QString &id, const QVariantMap &updates) { + QDBusReply r = m_iface.call("UpdateAlarm", id, updates); + return r.isValid() && r.value(); +} + +bool AlarmClient::deleteAlarm(const QString &id) { + QDBusReply r = m_iface.call("DeleteAlarm", id); + return r.isValid() && r.value(); +} + +bool AlarmClient::enableAlarm(const QString &id) { + QDBusReply r = m_iface.call("EnableAlarm", id); + return r.isValid() && r.value(); +} + +bool AlarmClient::disableAlarm(const QString &id) { + QDBusReply r = m_iface.call("DisableAlarm", id); + return r.isValid() && r.value(); +} + +bool AlarmClient::snoozeAlarm(const QString &id) { + QDBusReply r = m_iface.call("SnoozeAlarm", id); + return r.isValid() && r.value(); +} + +bool AlarmClient::dismissAlarm(const QString &id) { + QDBusReply r = m_iface.call("DismissAlarm", id); + return r.isValid() && r.value(); +} + +void AlarmClient::stopAll() { + m_iface.call("StopAll"); +} + +void AlarmClient::triggerAlarmNow(const QString &label) { + m_iface.call("TriggerAlarmNow", label); +} diff --git a/tools/marathon-app-runner/ipc/shellipcclients.h b/tools/marathon-app-runner/ipc/shellipcclients.h index b53c636ed..04e5d654a 100644 --- a/tools/marathon-app-runner/ipc/shellipcclients.h +++ b/tools/marathon-app-runner/ipc/shellipcclients.h @@ -104,7 +104,8 @@ class TelephonyClient : public QObject { Q_PROPERTY(QString activeNumber READ activeNumber NOTIFY activeNumberChanged) public: - explicit TelephonyClient(QObject *parent = nullptr); + public: + explicit TelephonyClient(const QString &appId, QObject *parent = nullptr); QString callState() const { return m_callState; @@ -122,6 +123,9 @@ class TelephonyClient : public QObject { Q_INVOKABLE void hangup(); Q_INVOKABLE void sendDTMF(const QString &digit); + Q_INVOKABLE void simulateIncomingCall(const QString &number); + Q_INVOKABLE void simulateCallStateChange(const QString &state); + signals: void callStateChanged(const QString &state); void incomingCall(const QString &number); @@ -135,10 +139,14 @@ class TelephonyClient : public QObject { void setActiveNumber(const QString &n); private: + bool ensurePermission(); + QString m_appId; QDBusInterface m_iface; + QDBusInterface m_permIface; QString m_callState; bool m_hasModem = false; QString m_activeNumber; + bool m_permissionRequested = false; }; class SmsClient : public QObject { @@ -146,7 +154,8 @@ class SmsClient : public QObject { Q_PROPERTY(QVariantList conversations READ conversations NOTIFY conversationsChanged) public: - explicit SmsClient(QObject *parent = nullptr); + public: + explicit SmsClient(const QString &appId, QObject *parent = nullptr); QVariantList conversations() const { return m_conversations; @@ -159,6 +168,8 @@ class SmsClient : public QObject { Q_INVOKABLE void markAsRead(const QString &conversationId); Q_INVOKABLE QString generateConversationId(const QString &number); + Q_INVOKABLE void simulateIncomingSMS(const QString &sender, const QString &message); + signals: void messageReceived(const QString &sender, const QString &text, qint64 timestamp); void messageSent(const QString &recipient, qint64 timestamp); @@ -166,9 +177,13 @@ class SmsClient : public QObject { void conversationsChanged(); private: + bool ensurePermission(); void setConversations(const QVariantList &v); + QString m_appId; QDBusInterface m_iface; + QDBusInterface m_permIface; QVariantList m_conversations; + bool m_permissionRequested = false; }; class MediaLibraryClient : public QObject { @@ -637,6 +652,7 @@ class AudioClient : public QObject { Q_PROPERTY(double volume READ volume NOTIFY stateChanged) Q_PROPERTY(bool muted READ muted NOTIFY stateChanged) Q_PROPERTY(bool perAppVolumeSupported READ perAppVolumeSupported NOTIFY stateChanged) + Q_PROPERTY(QString audioProfile READ audioProfile NOTIFY stateChanged) Q_PROPERTY(RemoteAudioStreamModel *streams READ streams CONSTANT) public: @@ -646,6 +662,7 @@ class AudioClient : public QObject { double volume() const; bool muted() const; bool perAppVolumeSupported() const; + QString audioProfile() const; RemoteAudioStreamModel *streams() { return m_streamModel; } @@ -839,3 +856,199 @@ class NavigationClient : public QObject { private: QDBusInterface m_iface; }; + +class HapticClient : public QObject { + Q_OBJECT + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + + public: + explicit HapticClient(QObject *parent = nullptr); + + bool available() const { + return m_available; + } + + Q_INVOKABLE void light(); + Q_INVOKABLE void medium(); + Q_INVOKABLE void heavy(); + Q_INVOKABLE void vibrate(int duration); + Q_INVOKABLE void vibratePattern(const QVariantList &pattern, int repeat = 1); + Q_INVOKABLE void stopVibration(); + + signals: + void availableChanged(); + + private: + void refresh(); + QDBusInterface m_iface; + bool m_available = false; +}; + +class NotificationClient : public QObject { + Q_OBJECT + + public: + public: + explicit NotificationClient(const QString &appId, QObject *parent = nullptr); + + Q_INVOKABLE int sendNotification(const QString &appId, const QString &title, + const QString &body, + const QVariantMap &options = QVariantMap()); + Q_INVOKABLE void dismissNotification(int id); + Q_INVOKABLE void dismissAllNotifications(); + + signals: + void notificationClosed(int id, int reason); + void actionInvoked(int id, const QString &action); + + private: + bool ensurePermission(); + QString m_appId; + QDBusInterface m_iface; + QDBusInterface m_permIface; + bool m_permissionRequested = false; +}; + +class SensorClient : public QObject { + Q_OBJECT + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + Q_PROPERTY(bool proximityNear READ proximityNear NOTIFY proximityChanged) + Q_PROPERTY(int ambientLight READ ambientLight NOTIFY ambientLightChanged) + + public: + explicit SensorClient(QObject *parent = nullptr); + + bool available() const { + return m_available; + } + bool proximityNear() const { + return m_proximityNear; + } + int ambientLight() const { + return m_ambientLight; + } + + signals: + void availableChanged(); + void proximityChanged(); + void ambientLightChanged(); + + private: + void refresh(); + QDBusInterface m_iface; + bool m_available = false; + bool m_proximityNear = false; + int m_ambientLight = 0; +}; + +class LocationClient : public QObject { + Q_OBJECT + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + Q_PROPERTY(bool active READ active NOTIFY activeChanged) + Q_PROPERTY(double latitude READ latitude NOTIFY locationChanged) + Q_PROPERTY(double longitude READ longitude NOTIFY locationChanged) + Q_PROPERTY(double accuracy READ accuracy NOTIFY locationChanged) + Q_PROPERTY(double altitude READ altitude NOTIFY locationChanged) + Q_PROPERTY(double speed READ speed NOTIFY locationChanged) + Q_PROPERTY(double heading READ heading NOTIFY locationChanged) + Q_PROPERTY(qint64 timestamp READ timestamp NOTIFY locationChanged) + + public: + explicit LocationClient(QObject *parent = nullptr); + + bool available() const { + return m_available; + } + bool active() const { + return m_active; + } + double latitude() const { + return m_latitude; + } + double longitude() const { + return m_longitude; + } + double accuracy() const { + return m_accuracy; + } + double altitude() const { + return m_altitude; + } + double speed() const { + return m_speed; + } + double heading() const { + return m_heading; + } + qint64 timestamp() const { + return m_timestamp; + } + + Q_INVOKABLE void start(); + Q_INVOKABLE void stop(); + + signals: + void availableChanged(); + void activeChanged(); + void locationChanged(); + + private slots: + void updateLocation(const QVariantMap &loc); + + private: + void refresh(); + QDBusInterface m_iface; + bool m_available = false; + bool m_active = false; + double m_latitude = 0.0; + double m_longitude = 0.0; + double m_accuracy = 0.0; + double m_altitude = 0.0; + double m_speed = 0.0; + double m_heading = 0.0; + qint64 m_timestamp = 0; +}; + +class AlarmClient : public QObject { + Q_OBJECT + Q_PROPERTY(QVariantList alarms READ alarms NOTIFY alarmsChanged) + Q_PROPERTY(QVariantList activeAlarms READ activeAlarms NOTIFY activeAlarmsChanged) + + public: + explicit AlarmClient(QObject *parent = nullptr); + + QVariantList alarms() const { + return m_alarms; + } + QVariantList activeAlarms() const { + return m_activeAlarms; + } + + Q_INVOKABLE QString createAlarm(const QString &time, const QString &label, + const QList &repeat, const QVariantMap &options); + Q_INVOKABLE bool updateAlarm(const QString &id, const QVariantMap &updates); + Q_INVOKABLE bool deleteAlarm(const QString &id); + Q_INVOKABLE bool enableAlarm(const QString &id); + Q_INVOKABLE bool disableAlarm(const QString &id); + Q_INVOKABLE bool snoozeAlarm(const QString &id); + Q_INVOKABLE bool dismissAlarm(const QString &id); + Q_INVOKABLE void stopAll(); + Q_INVOKABLE void triggerAlarmNow(const QString &label); + + signals: + void alarmsChanged(); + void activeAlarmsChanged(); + void alarmTriggered(const QString &id, const QString &label); + void alarmDismissed(const QString &id); + void alarmSnoozed(const QString &id, int minutes); + + private slots: + void onAlarmsChanged(); + void onActiveAlarmsChanged(); + + private: + void refresh(); + QDBusInterface m_iface; + QVariantList m_alarms; + QVariantList m_activeAlarms; +}; diff --git a/tools/marathon-app-runner/main.cpp b/tools/marathon-app-runner/main.cpp index 17b894028..c153df541 100644 --- a/tools/marathon-app-runner/main.cpp +++ b/tools/marathon-app-runner/main.cpp @@ -27,6 +27,7 @@ #include "marathonappscanner.h" #include "ipc/shellipcclients.h" +#include "calendarmanagercpp.h" class AppRunnerLifecycleObject : public QObject, protected QDBusContext { Q_OBJECT @@ -85,13 +86,11 @@ class AppRunnerLifecycleObject : public QObject, protected QDBusContext { return false; } - // QML methods report return types as QVariant in metaobject (even when the function returns true/false), - // so we must marshal via QVariant to avoid "return type mismatch" warnings. QVariant ret; bool ok = QMetaObject::invokeMethod(m_root.data(), method, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret)); if (!ok) { - // Fall back to emitting the signal if method isn't invokable (best-effort). + QMetaObject::invokeMethod(m_root.data(), QByteArray(method) == "handleBack" ? "backPressed" : "forwardPressed", @@ -138,7 +137,7 @@ static bool copyDirRecursively(const QString &srcPath, const QString &dstPath) { if (!copyDirRecursively(srcItem, dstItem)) return false; } else { - // Overwrite if it already exists (this is a temp dir; correctness > preserving) + if (QFileInfo::exists(dstItem)) QFile::remove(dstItem); if (!QFile::copy(srcItem, dstItem)) @@ -159,8 +158,6 @@ static QString createPatchedQmldirWithoutPrefer(const QString &importRoot, if (qmldirText.isEmpty()) return {}; - // Only patch when the qmldir is explicitly preferring qrc resources; that breaks in the runner - // because this process does not embed the shell's qrc. if (!qmldirText.contains("prefer :/qt/qml/")) return {}; @@ -180,7 +177,6 @@ static QString createPatchedQmldirWithoutPrefer(const QString &importRoot, if (line.isEmpty() || line.startsWith('#')) continue; - // Drop prefer line so it loads from filesystem (this process does not embed shell qrc). if (line.startsWith("prefer ")) continue; @@ -193,8 +189,6 @@ static QString createPatchedQmldirWithoutPrefer(const QString &importRoot, f.write(patched.toUtf8()); f.close(); - // Copy module payload so qmldir relative paths remain valid. - // (Absolute paths in qmldir produce warnings and can fail with "Network error".) const QString srcQmlDir = QDir(moduleAbsDir).filePath("qml"); const QString dstQmlDir = QDir(tmpModuleDir).filePath("qml"); if (!QDir(dstQmlDir).exists()) { @@ -212,7 +206,7 @@ static QString createPatchedQmldirWithoutPrefer(const QString &importRoot, struct QmldirInfo { QString moduleUri; - QString componentVersion; // e.g. "1.0" (may be empty) + QString componentVersion; }; static QmldirInfo parseQmldirForComponent(const QString &qmldirPath, const QString &componentName) { @@ -227,13 +221,11 @@ static QmldirInfo parseQmldirForComponent(const QString &qmldirPath, const QStri if (line.isEmpty() || line.startsWith('#')) continue; - // Example: "module MarathonApp.Calculator" if (line.startsWith("module ")) { out.moduleUri = line.mid(QString("module ").size()).trimmed(); continue; } - // Example: "CalculatorApp 1.0 CalculatorApp.qml" if (line.startsWith(componentName + ' ')) { const QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); if (parts.size() >= 2) @@ -245,14 +237,12 @@ static QmldirInfo parseQmldirForComponent(const QString &qmldirPath, const QStri } static QString findAppQmldirPath(const QString &appAbsolutePath, const QString &appId) { - // Common install layout: - // /MarathonApp//qmldir + QString p1 = QDir(appAbsolutePath) .filePath(QStringLiteral("MarathonApp/%1/qmldir").arg(capitalizeAppId(appId))); if (QFileInfo::exists(p1)) return p1; - // Slightly more defensive: scan /MarathonApp/*/qmldir const QDir root(QDir(appAbsolutePath).filePath("MarathonApp")); if (!root.exists()) return {}; @@ -289,11 +279,8 @@ int main(int argc, char *argv[]) { return 2; } - // Help Qt/Wayland set a stable xdg-shell app_id (often derived from desktopFileName). - // This is metadata only; the compositor should still primarily identify clients by PID. QGuiApplication::setDesktopFileName(appId); - // Discover installed apps using the same scanner as the shell. MarathonAppRegistry registry; MarathonAppScanner scanner(®istry); scanner.scanApplications(); @@ -307,13 +294,9 @@ int main(int argc, char *argv[]) { QQuickView view; view.setResizeMode(QQuickView::SizeRootObjectToView); view.setTitle(info->name); - // Clear to transparent (not white) while the scene is warming up. + view.setColor(Qt::transparent); - // NOTE: Do not call QQuickView::setOpacity() here. - // The Wayland client plugin may not support window opacity and will log warnings. - // The shell compositor already gates visibility until first frame, so we don't need it. - // Default phone-like size for desktop testing; can be overridden by env. const int w = qEnvironmentVariableIntValue("MARATHON_APP_WIDTH") > 0 ? qEnvironmentVariableIntValue("MARATHON_APP_WIDTH") : 540; @@ -324,21 +307,17 @@ int main(int argc, char *argv[]) { QQmlEngine *engine = view.engine(); - // Import paths: - // - Marathon app directory itself (for relative imports/assets) if (QDir(info->absolutePath).exists()) engine->addImportPath(info->absolutePath); - // - MarathonUI module root (dev build + user install + system install) - // The import path must be the *parent* that contains MarathonUI/*/qmldir. { const QDir binDir(QCoreApplication::applicationDirPath()); const QDir cwd(QDir::currentPath()); const QStringList devCandidates = { - // Typical build tree: /build/tools/marathon-app-runner -> /build-ui + binDir.filePath("../../../build-ui"), - // Fallbacks for other layouts + binDir.filePath("../../build-ui"), binDir.filePath("../build-ui"), cwd.filePath("build-ui"), @@ -354,7 +333,6 @@ int main(int argc, char *argv[]) { } } - // User install path (optional) const QString userUi = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/marathon-ui"; if (QDir(userUi + "/MarathonUI").exists()) { @@ -362,7 +340,6 @@ int main(int argc, char *argv[]) { foundUi = true; } - // System install paths (optional) if (QDir("/usr/lib/qt6/qml/MarathonUI").exists()) { engine->addImportPath("/usr/lib/qt6/qml"); foundUi = true; @@ -386,19 +363,19 @@ int main(int argc, char *argv[]) { } } - // - Shell QML module (MarathonOS.Shell) for Logger/Constants/etc. - // Prefer an explicit env override, otherwise try dev-build sibling path. const QByteArray shellQmlEnv = qgetenv("MARATHON_SHELL_QML_IMPORT_PATH").trimmed(); if (!shellQmlEnv.isEmpty()) { const QString p = QString::fromLocal8Bit(shellQmlEnv); - if (QDir(p).exists()) - engine->addImportPath(p); + if (QDir(p).exists()) { + const QString patchedRoot = createPatchedQmldirWithoutPrefer(p, "MarathonOS/Shell"); + if (!patchedRoot.isEmpty()) + engine->addImportPath(patchedRoot); + else + engine->addImportPath(p); + } } else { - const QDir binDir(QCoreApplication::applicationDirPath()); - // Common layouts: - // - build/tools/marathon-app-runner/ -> build/shell/qml (../../shell/qml) - // - build/ -> build/shell/qml (shell/qml) - // - /usr/local/bin/ -> /usr/local/lib/qt6/qml (system install) + const QDir binDir(QCoreApplication::applicationDirPath()); + const QString p1 = binDir.filePath("../../shell/qml"); const QString p2 = binDir.filePath("../shell/qml"); const QString p3 = binDir.filePath("shell/qml"); @@ -409,12 +386,10 @@ int main(int argc, char *argv[]) { for (const QString &cand : candidates) { if (!QDir(cand).exists()) continue; - // Patch out "prefer :/qt/qml/..." in the generated qmldir so MarathonOS.Shell works in this process. + const QString patchedRoot = createPatchedQmldirWithoutPrefer(cand, "MarathonOS/Shell"); if (!patchedRoot.isEmpty()) { - // IMPORTANT: Do NOT also add the original import root here. - // If it is searched first, the qmldir "prefer :/qt/qml/..." will route loads to qrc, - // which this process does not embed. + engine->addImportPath(patchedRoot); } else { engine->addImportPath(cand); @@ -422,17 +397,11 @@ int main(int argc, char *argv[]) { } } - // Allow qrc imports if present in this process (apps should prefer filesystem assets). engine->addImportPath("qrc:/"); engine->addImportPath(":/"); - auto *ctx = view.rootContext(); + auto *ctx = view.rootContext(); - // Real IPC clients (no stubs): apps talk to the shell over DBus. - // - // IMPORTANT: Only create clients for permissions the app declares. - // Otherwise, "initial refresh" will hit AccessDenied and spam stderr on every app launch - // (e.g. Gallery should not talk to Network/Bluetooth/Audio/etc). const auto hasPerm = [&](const QString &perm) -> bool { return std::find(info->permissions.begin(), info->permissions.end(), perm) != info->permissions.end(); @@ -441,17 +410,17 @@ int main(int argc, char *argv[]) { ctx->setContextProperty("PermissionManager", new PermissionClient(&app)); ctx->setContextProperty("ContactsManager", new ContactsClient(&app)); ctx->setContextProperty("CallHistoryManager", new CallHistoryClient(&app)); - ctx->setContextProperty("TelephonyService", new TelephonyClient(&app)); - ctx->setContextProperty("SMSService", new SmsClient(&app)); + ctx->setContextProperty("TelephonyService", new TelephonyClient(appId, &app)); + ctx->setContextProperty("SMSService", new SmsClient(appId, &app)); if (hasPerm("storage")) ctx->setContextProperty("MediaLibraryManager", new MediaLibraryClient(appId, &app)); - // SettingsManagerCpp is used for: - // - system/global settings (requires "system") - // - app-scoped settings (keys under "/...") which only require "storage" - if (hasPerm("system") || hasPerm("storage")) - ctx->setContextProperty("SettingsManagerCpp", new SettingsClient(appId, &app)); + SettingsClient *settingsClient = nullptr; + if (hasPerm("system") || hasPerm("storage")) { + settingsClient = new SettingsClient(appId, &app); + ctx->setContextProperty("SettingsManagerCpp", settingsClient); + } if (hasPerm("system")) { ctx->setContextProperty("BluetoothManagerCpp", new BluetoothClient(appId, &app)); @@ -464,6 +433,17 @@ int main(int argc, char *argv[]) { ctx->setContextProperty("NetworkManagerCpp", new NetworkClient(appId, &app)); ctx->setContextProperty("NavigationService", new NavigationClient(&app)); + ctx->setContextProperty("HapticService", new HapticClient(&app)); + auto *notificationClient = new NotificationClient(appId, &app); + ctx->setContextProperty("NativeNotificationService", notificationClient); + ctx->setContextProperty("SensorService", new SensorClient(&app)); + ctx->setContextProperty("LocationService", new LocationClient(&app)); + ctx->setContextProperty("AlarmService", new AlarmClient(&app)); + + if (appId == "calendar") { + ctx->setContextProperty("CalendarManager", + new CalendarManagerCpp(settingsClient, notificationClient, &app)); + } const QString entryAbs = QDir(info->absolutePath).filePath(info->entryPoint); const QUrl entryUrl = QUrl::fromLocalFile(entryAbs); @@ -472,14 +452,12 @@ int main(int argc, char *argv[]) { qInfo() << "[marathon-app-runner] Starting app" << appId << "entryPoint" << info->entryPoint << "entryAbs" << entryAbs << "appPath" << info->absolutePath; - // Show a window immediately so the Wayland client creates a surface quickly. view.show(); - // Runner-side DBus lifecycle server: shell can send Back/Forward to this app instance. AppRunnerLifecycleObject lifecycleObj; { - const qint64 pid = QCoreApplication::applicationPid(); - // D-Bus service name elements cannot start with a digit, so prefix the PID. + const qint64 pid = QCoreApplication::applicationPid(); + const QString serviceName = QStringLiteral("org.marathonos.AppRunner.pid%1").arg(pid); auto bus = QDBusConnection::sessionBus(); if (bus.isConnected()) { @@ -498,24 +476,14 @@ int main(int argc, char *argv[]) { } } - // Create and attach the app root item on the next tick. const QString appAbsPath = info->absolutePath; QTimer::singleShot( 0, &view, [&view, engine, ctx, entryUrl, entryAbs, appId, componentName, appAbsPath, &lifecycleObj]() { - // NOTE: QQmlComponent can be asynchronously "Loading" even for local files/modules. - // If we call create() while not Ready, Qt prints "Component is not ready" and returns null. - // That was causing apps to fail to launch intermittently. - QQmlComponent *component = nullptr; - // Two supported layouts: - // 1) "unpacked" app directory contains the entrypoint QML file on disk - // 2) "compiled module" app directory contains MarathonApp//qmldir + libplugin.so - // In that case, the entryPoint in the manifest is the *type* (e.g. CalculatorApp.qml), - // but the actual QML lives in the module's internal resources (:/qt/qml/...). if (QFileInfo::exists(entryAbs)) { component = new QQmlComponent(engine, entryUrl, &view); } else { @@ -530,8 +498,6 @@ int main(int argc, char *argv[]) { return; } - // Create a tiny wrapper QML file that imports the module and instantiates the root type. - // This forces the module plugin to load (and thus register its qrc resources). QString wrapper; wrapper += "import QtQuick\n"; if (!qmldir.componentVersion.isEmpty()) @@ -589,8 +555,6 @@ int main(int argc, char *argv[]) { rootItem->setWidth(view.width()); rootItem->setHeight(view.height()); - // Ensure QML focus is given to the root item so forceActiveFocus() on - // child items (e.g. browser address bar) properly routes keyboard input. rootItem->forceActiveFocus(); QObject::connect(&view, &QQuickView::widthChanged, rootItem, @@ -602,7 +566,7 @@ int main(int argc, char *argv[]) { }; if (component->status() == QQmlComponent::Loading) { - // Wait until the imports/plugins are loaded. + QObject::connect(component, &QQmlComponent::statusChanged, &view, [component, createAndAttach](QQmlComponent::Status st) { if (st == QQmlComponent::Ready || st == QQmlComponent::Error) diff --git a/tools/marathon-dev/main.cpp b/tools/marathon-dev/main.cpp index 4a8725190..e057025b8 100644 --- a/tools/marathon-dev/main.cpp +++ b/tools/marathon-dev/main.cpp @@ -41,7 +41,7 @@ int packageCommand(const QStringList &args) { if (args.size() >= 4) { outputFile = args.at(3); } else { - // Generate output filename from app directory + QFileInfo dirInfo(appDir); QString appDirName = dirInfo.fileName(); outputFile = appDirName + ".marathon"; @@ -55,7 +55,6 @@ int packageCommand(const QStringList &args) { if (packager.createPackage(appDir, outputFile)) { printSuccess("Package created successfully: " + outputFile); - // Show package info QFileInfo packageInfo(outputFile); qint64 sizeKB = packageInfo.size() / 1024; printInfo(QString("Package size: %1 KB").arg(sizeKB)); @@ -121,10 +120,10 @@ int validateCommand(const QStringList &args) { MarathonAppVerifier::VerificationResult result; if (pathInfo.isDir()) { - // Validate directory + result = verifier.verifyDirectory(path); } else if (path.endsWith(".marathon")) { - // For packages, we need to extract first + printError("Direct package validation not supported yet"); printInfo("Extract the package first, then validate the directory"); return 1; @@ -166,7 +165,6 @@ int installCommand(const QStringList &args) { printInfo("Installing: " + path); - // Create installer components MarathonAppRegistry registry; MarathonAppScanner scanner(®istry); MarathonAppInstaller installer(®istry, &scanner); @@ -174,10 +172,10 @@ int installCommand(const QStringList &args) { bool success = false; if (pathInfo.isDir()) { - // Install from directory + success = installer.installFromDirectory(path); } else if (path.endsWith(".marathon")) { - // Install from package + success = installer.installFromPackage(path); } else { printError("Unknown file type. Expected directory or .marathon package"); @@ -203,20 +201,17 @@ int initCommand(const QStringList &args) { QString appName = args.at(2); QString appId = appName.toLower().replace(" ", "-"); - // Create app directory - QDir dir; + QDir dir; if (!dir.mkdir(appId)) { printError("Failed to create directory: " + appId); return 1; } - // Create subdirectories dir.cd(appId); dir.mkdir("pages"); dir.mkdir("components"); dir.mkdir("assets"); - // Create manifest.json QJsonObject manifest; manifest["id"] = appId; manifest["name"] = appName; @@ -234,7 +229,6 @@ int initCommand(const QStringList &args) { manifestFile.close(); } - // Create main QML file QString appFileName = appId.replace("-", ""); appFileName[0] = appFileName[0].toUpper(); appFileName += "App.qml"; @@ -285,7 +279,6 @@ MApplicationWindow { qmlFile.close(); } - // Create qmldir QString qmldirContent = QString("module %1\n").arg(appId); QFile qmldirFile(appId + "/qmldir"); if (qmldirFile.open(QIODevice::WriteOnly)) { @@ -293,9 +286,8 @@ MApplicationWindow { qmldirFile.close(); } - // Create placeholder icon QString iconSVG = R"( - + A )"; diff --git a/tools/marathon-sandbox/main.c b/tools/marathon-sandbox/main.c index 66d2dfd73..2743e0692 100644 --- a/tools/marathon-sandbox/main.c +++ b/tools/marathon-sandbox/main.c @@ -83,6 +83,18 @@ static int add_net_port_rule(int ruleset_fd, __u16 port, __u64 allowed_access) { return landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, &net_port, 0); } +static int join_home_path(char *out, size_t out_size, const char *home, const char *suffix) { + size_t home_len = strlen(home); + size_t suffix_len = strlen(suffix); + if (home_len + suffix_len + 1 > out_size) + return 0; + for (size_t i = 0; i < home_len; ++i) + out[i] = home[i]; + for (size_t j = 0; j <= suffix_len; ++j) + out[home_len + j] = suffix[j]; + return 1; +} + static void add_standard_net_rules(int ruleset_fd) { add_net_port_rule(ruleset_fd, 80, LANDLOCK_ACCESS_NET_CONNECT_TCP); add_net_port_rule(ruleset_fd, 443, LANDLOCK_ACCESS_NET_CONNECT_TCP); @@ -91,7 +103,7 @@ static void add_standard_net_rules(int ruleset_fd) { static int setup_seccomp(void) { if (prctl(PR_GET_SECCOMP) == -1 && errno == EINVAL) { - fprintf(stderr, "[marathon-sandbox] Seccomp not available\n"); + fputs("[marathon-sandbox] Seccomp not available\n", stderr); return 0; } @@ -215,7 +227,7 @@ static int setup_landlock(int has_network_permission, int has_storage_permission int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); if (abi < 0) { if (errno == ENOSYS || errno == EOPNOTSUPP) { - fprintf(stderr, "[marathon-sandbox] Landlock not available\n"); + fputs("[marathon-sandbox] Landlock not available\n", stderr); return 0; } return -1; @@ -295,16 +307,19 @@ static int setup_landlock(int has_network_permission, int has_storage_permission add_path_rule(ruleset_fd, home, access_ro); char config_path[PATH_MAX]; - snprintf(config_path, sizeof(config_path), "%s/.config", home); - add_path_rule(ruleset_fd, config_path, access_rw); + if (join_home_path(config_path, sizeof(config_path), home, "/.config")) { + add_path_rule(ruleset_fd, config_path, access_rw); + } char cache_path[PATH_MAX]; - snprintf(cache_path, sizeof(cache_path), "%s/.cache", home); - add_path_rule(ruleset_fd, cache_path, access_rw); + if (join_home_path(cache_path, sizeof(cache_path), home, "/.cache")) { + add_path_rule(ruleset_fd, cache_path, access_rw); + } char local_path[PATH_MAX]; - snprintf(local_path, sizeof(local_path), "%s/.local/share", home); - add_path_rule(ruleset_fd, local_path, access_rw); + if (join_home_path(local_path, sizeof(local_path), home, "/.local/share")) { + add_path_rule(ruleset_fd, local_path, access_rw); + } } } @@ -336,10 +351,12 @@ static int env_has_permission(const char *env_var) { int main(int argc, char *argv[]) { if (argc < 2) { - fprintf(stderr, "Usage: %s [args...]\n", argv[0]); - fprintf(stderr, "Environment variables:\n"); - fprintf(stderr, " MARATHON_PERM_NETWORK=1 - Allow unrestricted network access\n"); - fprintf(stderr, " MARATHON_PERM_STORAGE=1 - Allow full home directory write access\n"); + fputs("Usage: ", stderr); + fputs(argv[0], stderr); + fputs(" [args...]\n", stderr); + fputs("Environment variables:\n", stderr); + fputs(" MARATHON_PERM_NETWORK=1 - Allow unrestricted network access\n", stderr); + fputs(" MARATHON_PERM_STORAGE=1 - Allow full home directory write access\n", stderr); return 1; } @@ -355,13 +372,13 @@ int main(int argc, char *argv[]) { if (setup_landlock(has_network, has_storage) < 0) { syslog(LOG_ERR, "[SECURITY] Landlock setup failed - Aborting"); - fprintf(stderr, "[marathon-sandbox] Error: Landlock setup failed. Aborting.\n"); + fputs("[marathon-sandbox] Error: Landlock setup failed. Aborting.\n", stderr); return 1; } if (setup_seccomp() < 0) { syslog(LOG_ERR, "[SECURITY] Seccomp setup failed - Aborting"); - fprintf(stderr, "[marathon-sandbox] Error: Seccomp setup failed. Aborting.\n"); + fputs("[marathon-sandbox] Error: Seccomp setup failed. Aborting.\n", stderr); return 1; } From ed8d4b9f26973793165626f4e45afca677cef9ce Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:34:02 +0000 Subject: [PATCH 10/28] chore(build): update lint plumbing and run script Tighten QML lint configuration and build/run defaults to match the current project layout. --- CMakeLists.txt | 6 ++++++ cmake/QMLLint.cmake | 22 ++++++++++++++++++++++ run.sh | 1 + 3 files changed, 29 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d0febc5b..6f25daba8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,12 @@ endif() # Enable QML Language Server support for IDE/VS Code set(QT_QML_GENERATE_QMLLS_INI ON) +set(QML_IMPORT_PATH + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/shell/qml" + CACHE STRING "" FORCE +) + set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) diff --git a/cmake/QMLLint.cmake b/cmake/QMLLint.cmake index b5a1c9f96..a48133d51 100644 --- a/cmake/QMLLint.cmake +++ b/cmake/QMLLint.cmake @@ -52,6 +52,18 @@ function(add_qmllint_target target_name) endif() # Find MarathonUI build directory (multiple possible locations) + if(EXISTS "${CMAKE_BINARY_DIR}/qmllint") + list(APPEND import_paths "${CMAKE_BINARY_DIR}/qmllint") + endif() + + if(EXISTS "${CMAKE_SOURCE_DIR}/build/qmllint") + list(APPEND import_paths "${CMAKE_SOURCE_DIR}/build/qmllint") + endif() + + if(EXISTS "${CMAKE_SOURCE_DIR}/../build/qmllint") + list(APPEND import_paths "${CMAKE_SOURCE_DIR}/../build/qmllint") + endif() + # 1. If we're building marathon-ui, use current binary dir if(CMAKE_CURRENT_BINARY_DIR MATCHES "marathon-ui") list(APPEND import_paths "${CMAKE_BINARY_DIR}/MarathonUI") @@ -72,6 +84,10 @@ function(add_qmllint_target target_name) if(EXISTS "${CMAKE_BINARY_DIR}/MarathonUI") list(APPEND import_paths "${CMAKE_BINARY_DIR}/MarathonUI") endif() + + if(EXISTS "${CMAKE_BINARY_DIR}/shell/qml") + list(APPEND import_paths "${CMAKE_BINARY_DIR}/shell/qml") + endif() # 4. Check for installed MarathonUI (user-local) set(marathon_ui_import_path "$ENV{HOME}/.local/share/marathon-ui") @@ -135,6 +151,12 @@ function(add_qmllint_target target_name) if(TARGET marathon-ui-core) list(APPEND qmllint_dependencies marathon-ui-core) endif() + if(TARGET marathon-shell) + list(APPEND qmllint_dependencies marathon-shell) + endif() + if(TARGET marathon-ui-qmllint-modules) + list(APPEND qmllint_dependencies marathon-ui-qmllint-modules) + endif() add_custom_target(${target_name}_qmllint COMMAND ${CMAKE_COMMAND} -E echo "🔍 Running qmllint validation for ${target_name}..." diff --git a/run.sh b/run.sh index 808cebd93..db758657e 100755 --- a/run.sh +++ b/run.sh @@ -176,6 +176,7 @@ if [ $? -eq 0 ]; then # Set QML import path for MarathonUI modules export QML_IMPORT_PATH="$PROJECT_DIR/build/shell/qml:$QML_IMPORT_PATH" + export MARATHON_SHELL_QML_IMPORT_PATH="$PROJECT_DIR/build/shell/qml" export MARATHON_DATA_DIR="${MARATHON_DATA_DIR:-$PROJECT_DIR/shell/resources}" From ff05e41af33c9e6bc03146475aa3c33396bef2e3 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 15:34:13 +0000 Subject: [PATCH 11/28] chore(scripts): add QML build helpers Add scripts to build QML dependency stubs and strip comments for linting. --- scripts/build_qml_dependencies.py | 70 +++++++++++++++++++++++++++++++ scripts/strip_comments.py | 49 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 scripts/build_qml_dependencies.py create mode 100644 scripts/strip_comments.py diff --git a/scripts/build_qml_dependencies.py b/scripts/build_qml_dependencies.py new file mode 100644 index 000000000..b056be831 --- /dev/null +++ b/scripts/build_qml_dependencies.py @@ -0,0 +1,70 @@ +import json +import os +import re +import sys + + +IMPORT_RE = re.compile(r"^import\\s+([A-Za-z0-9_.]+)(?:\\s+([0-9]+\\.[0-9]+))?\\s*$") + +VERSION_MAP = { + "QtQuick": "2.0", + "QtQuick.Controls": "2.0", + "QtQuick.Layouts": "1.0", + "QtQml": "2.0", + "QtQml.Models": "2.0", + "MarathonOS.Shell": "1.0", +} + + +def version_for(module: str) -> str | None: + if module.startswith("MarathonUI."): + return "2.0" + return VERSION_MAP.get(module) + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: build_qml_dependencies.py ROOT OUT", file=sys.stderr) + return 1 + + root = sys.argv[1] + out_path = sys.argv[2] + modules: dict[str, str] = {} + + for dirpath, _, filenames in os.walk(root): + for filename in filenames: + if not filename.endswith(".qml"): + continue + path = os.path.join(dirpath, filename) + with open(path, "r", encoding="utf-8") as handle: + for line in handle: + line = line.strip() + if not line.startswith("import "): + continue + match = IMPORT_RE.match(line) + if not match: + continue + module = match.group(1) + version = match.group(2) + if module.startswith('"') or module.startswith("."): + continue + if version: + modules[module] = version + continue + mapped = version_for(module) + if mapped: + modules.setdefault(module, mapped) + + data = [ + {"name": name, "type": "module", "version": modules[name]} + for name in sorted(modules) + ] + with open(out_path, "w", encoding="utf-8") as handle: + json.dump(data, handle, indent=4, sort_keys=True) + handle.write("\n") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/strip_comments.py b/scripts/strip_comments.py new file mode 100644 index 000000000..8b8cc7854 --- /dev/null +++ b/scripts/strip_comments.py @@ -0,0 +1,49 @@ +import re +import sys +import os + +def remove_comments(text): + def replacer(match): + s = match.group(0) + if s.startswith('/'): + return " " # note: a space and not an empty string + else: + return s + + # Regex to capture: + # 1. Strings (double or single quoted) to preserve them + # 2. Block comments /* ... */ + # 3. Line comments // ... + pattern = re.compile( + r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.DOTALL | re.MULTILINE + ) + return re.sub(pattern, replacer, text) + +def process_file(filepath): + try: + with open(filepath, 'r') as f: + content = f.read() + + new_content = remove_comments(content) + + # Simple cleanup of multiple empty lines potentially created + new_content = re.sub(r'\n\s*\n\s*\n', '\n\n', new_content) + + if content != new_content: + with open(filepath, 'w') as f: + f.write(new_content) + print(f"Processed: {filepath}") + except Exception as e: + print(f"Error processing {filepath}: {e}") + +if __name__ == "__main__": + for root, dirs, files in os.walk("."): + if "build" in dirs: + dirs.remove("build") + if ".git" in dirs: + dirs.remove(".git") + + for file in files: + if file.endswith(('.cpp', '.h', '.qml', '.js')): + process_file(os.path.join(root, file)) From 5cb6311e0388620a017db52e44f5ba4faae4e1ab Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 16:03:54 +0000 Subject: [PATCH 12/28] feat(shell): move SessionStore to C++ singleton Replace the QML SessionStore with a C++ QML_SINGLETON and update shell components to import MarathonOS.Shell for session state access. --- shell/CMakeLists.txt | 4 +- shell/qml/MarathonShell.qml | 1 + shell/qml/components/IncomingCallOverlay.qml | 1 + shell/qml/components/MarathonLockScreen.qml | 1 + shell/qml/components/MarathonPinScreen.qml | 1 + .../qml/components/MarathonQuickSettings.qml | 1 + shell/qml/components/MarathonStatusBar.qml | 1 + shell/qml/stores/SessionStore.qml | 93 ------------------ shell/src/services/sessionstore.cpp | 96 +++++++++++++++++++ shell/src/services/sessionstore.h | 83 ++++++++++++++++ 10 files changed, 187 insertions(+), 95 deletions(-) delete mode 100644 shell/qml/stores/SessionStore.qml create mode 100644 shell/src/services/sessionstore.cpp create mode 100644 shell/src/services/sessionstore.h diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index bca837ebc..560d6d035 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -28,6 +28,8 @@ set(SOURCES src/services/notificationservicecpp.cpp src/services/telephonyintegrationcpp.h src/services/telephonyintegrationcpp.cpp + src/services/sessionstore.h + src/services/sessionstore.cpp src/components/crashhandler.h src/components/crashhandler.cpp qml/keyboard/Data/WordEngine.h @@ -220,7 +222,6 @@ set(QML_FILES qml/stores/WallpaperStore.qml qml/stores/SystemStatusStore.qml qml/stores/SystemControlStore.qml - qml/stores/SessionStore.qml qml/stores/UIStore.qml qml/core/Constants.qml qml/core/Logger.qml @@ -258,7 +259,6 @@ set(SINGLETON_FILES qml/stores/WallpaperStore.qml qml/stores/SystemStatusStore.qml qml/stores/SystemControlStore.qml - qml/stores/SessionStore.qml qml/stores/UIStore.qml qml/core/Constants.qml qml/core/Logger.qml diff --git a/shell/qml/MarathonShell.qml b/shell/qml/MarathonShell.qml index fc776cb31..40fc5514e 100644 --- a/shell/qml/MarathonShell.qml +++ b/shell/qml/MarathonShell.qml @@ -1,6 +1,7 @@ import "./components" as Comp import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick import QtQuick.Window diff --git a/shell/qml/components/IncomingCallOverlay.qml b/shell/qml/components/IncomingCallOverlay.qml index 4d480c720..a0cf5ea0f 100644 --- a/shell/qml/components/IncomingCallOverlay.qml +++ b/shell/qml/components/IncomingCallOverlay.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick import QtQuick.Controls diff --git a/shell/qml/components/MarathonLockScreen.qml b/shell/qml/components/MarathonLockScreen.qml index d5d4a77fb..11d096826 100644 --- a/shell/qml/components/MarathonLockScreen.qml +++ b/shell/qml/components/MarathonLockScreen.qml @@ -1,4 +1,5 @@ import "./ui" +import MarathonOS.Shell 1.0 import MarathonOS.Shell 1.0 as Shell import MarathonUI.Core import MarathonUI.Theme diff --git a/shell/qml/components/MarathonPinScreen.qml b/shell/qml/components/MarathonPinScreen.qml index 6b667a5fe..f807acf49 100644 --- a/shell/qml/components/MarathonPinScreen.qml +++ b/shell/qml/components/MarathonPinScreen.qml @@ -1,6 +1,7 @@ import MarathonUI.Core import MarathonUI.Feedback import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick import QtQuick.Effects diff --git a/shell/qml/components/MarathonQuickSettings.qml b/shell/qml/components/MarathonQuickSettings.qml index 69670812b..9bb9186c1 100644 --- a/shell/qml/components/MarathonQuickSettings.qml +++ b/shell/qml/components/MarathonQuickSettings.qml @@ -2,6 +2,7 @@ import MarathonUI.Controls import MarathonUI.Core import MarathonUI.Navigation import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick import QtQuick.Controls diff --git a/shell/qml/components/MarathonStatusBar.qml b/shell/qml/components/MarathonStatusBar.qml index 8feb74f09..be3c4b09d 100644 --- a/shell/qml/components/MarathonStatusBar.qml +++ b/shell/qml/components/MarathonStatusBar.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Item { diff --git a/shell/qml/stores/SessionStore.qml b/shell/qml/stores/SessionStore.qml deleted file mode 100644 index d1cb9f872..000000000 --- a/shell/qml/stores/SessionStore.qml +++ /dev/null @@ -1,93 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: sessionStore - - property bool isLocked: true - property bool isOnLockScreen: false - property bool showLockScreen: true - property bool isAnimatingLock: false - property string lockTransition: "" - property real sessionValidUntil: 0 - property Timer lockTimer - - signal lockStateChanging(bool toLocked) - signal lockStateChanged(bool isLocked) - signal triggerShakeAnimation - signal triggerUnlockAnimation - - function lock() { - console.log("[SessionStore] lock() called - current isLocked:", isLocked, "showLockScreen:", showLockScreen); - if (isLocked) { - console.log("[SessionStore] Already locked, ignoring"); - return; - } - console.log("[SessionStore] Locking session..."); - showLockScreen = true; - console.log("[SessionStore] Set showLockScreen to true"); - lockTransition = "locking"; - isAnimatingLock = true; - lockStateChanging(true); - lockTimer.interval = 300; - lockTimer.targetLocked = true; - lockTimer.restart(); - } - - function unlock() { - console.log("[SessionStore] unlock() called - current isLocked:", isLocked); - if (isAnimatingLock && lockTransition === "unlocking") { - console.log("[SessionStore] Unlock already in progress, ignoring duplicate"); - showLockScreen = false; - isOnLockScreen = false; - return; - } - if (!isLocked) { - console.log("[SessionStore] Already unlocked - dismissing lock screen"); - showLockScreen = false; - isOnLockScreen = false; - return; - } - console.log("[SessionStore] Unlocking session..."); - showLockScreen = false; - lockTransition = "unlocking"; - isAnimatingLock = true; - lockStateChanging(false); - sessionValidUntil = Date.now() + (5 * 60 * 1000); - console.log("[SessionStore] Grace period set until:", new Date(sessionValidUntil).toLocaleTimeString()); - lockTimer.interval = 300; - lockTimer.targetLocked = false; - lockTimer.restart(); - } - - function checkSession() { - var now = Date.now(); - var isValid = sessionValidUntil > now; - if (!isValid) - console.log("[SessionStore] Session expired, PIN required"); - - return isValid; - } - - function showLock() { - console.log("[SessionStore] Showing lock screen (session may still be unlocked)"); - showLockScreen = true; - } - - Component.onCompleted: { - console.log("[SessionStore] Initialized - session locked by default"); - } - - lockTimer: Timer { - property bool targetLocked: true - - onTriggered: { - console.log("[SessionStore] Lock transition complete - setting isLocked to:", targetLocked); - isLocked = targetLocked; - console.log("[SessionStore] isLocked is now:", isLocked); - isAnimatingLock = false; - lockTransition = ""; - lockStateChanged(isLocked); - } - } -} diff --git a/shell/src/services/sessionstore.cpp b/shell/src/services/sessionstore.cpp new file mode 100644 index 000000000..d13a8da00 --- /dev/null +++ b/shell/src/services/sessionstore.cpp @@ -0,0 +1,96 @@ +#include "sessionstore.h" + +#include + +SessionStore::SessionStore(QObject *parent) + : QObject(parent) { + m_lockTimer.setSingleShot(true); + connect(&m_lockTimer, &QTimer::timeout, this, [this]() { + setIsLocked(m_targetLocked); + setIsAnimatingLock(false); + setLockTransition(QString()); + emit lockStateChanged(m_isLocked); + }); +} + +void SessionStore::setIsLocked(bool locked) { + if (m_isLocked == locked) + return; + m_isLocked = locked; + emit isLockedChanged(); +} + +void SessionStore::setIsOnLockScreen(bool onLockScreen) { + if (m_isOnLockScreen == onLockScreen) + return; + m_isOnLockScreen = onLockScreen; + emit isOnLockScreenChanged(); +} + +void SessionStore::setShowLockScreen(bool show) { + if (m_showLockScreen == show) + return; + m_showLockScreen = show; + emit showLockScreenChanged(); +} + +void SessionStore::setIsAnimatingLock(bool animating) { + if (m_isAnimatingLock == animating) + return; + m_isAnimatingLock = animating; + emit isAnimatingLockChanged(); +} + +void SessionStore::setLockTransition(const QString &transition) { + if (m_lockTransition == transition) + return; + m_lockTransition = transition; + emit lockTransitionChanged(); +} + +void SessionStore::lock() { + if (m_isLocked) + return; + + setShowLockScreen(true); + setLockTransition("locking"); + setIsAnimatingLock(true); + emit lockStateChanging(true); + startLockTimer(true); +} + +void SessionStore::unlock() { + if (m_isAnimatingLock && m_lockTransition == "unlocking") { + setShowLockScreen(false); + setIsOnLockScreen(false); + return; + } + + if (!m_isLocked) { + setShowLockScreen(false); + setIsOnLockScreen(false); + return; + } + + setShowLockScreen(false); + setLockTransition("unlocking"); + setIsAnimatingLock(true); + emit lockStateChanging(false); + m_sessionValidUntil = QDateTime::currentMSecsSinceEpoch() + 5 * 60 * 1000; + startLockTimer(false); +} + +bool SessionStore::checkSession() const { + const qint64 now = QDateTime::currentMSecsSinceEpoch(); + return m_sessionValidUntil > now; +} + +void SessionStore::showLock() { + setShowLockScreen(true); +} + +void SessionStore::startLockTimer(bool targetLocked) { + m_targetLocked = targetLocked; + m_lockTimer.setInterval(300); + m_lockTimer.start(); +} diff --git a/shell/src/services/sessionstore.h b/shell/src/services/sessionstore.h new file mode 100644 index 000000000..51466ba4c --- /dev/null +++ b/shell/src/services/sessionstore.h @@ -0,0 +1,83 @@ +#ifndef SESSIONSTORE_H +#define SESSIONSTORE_H + +#include +#include +#include + +#include + +class SessionStore : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool isLocked READ isLocked WRITE setIsLocked NOTIFY isLockedChanged) + Q_PROPERTY(bool isOnLockScreen READ isOnLockScreen WRITE setIsOnLockScreen NOTIFY + isOnLockScreenChanged) + Q_PROPERTY(bool showLockScreen READ showLockScreen WRITE setShowLockScreen NOTIFY + showLockScreenChanged) + Q_PROPERTY(bool isAnimatingLock READ isAnimatingLock WRITE setIsAnimatingLock NOTIFY + isAnimatingLockChanged) + Q_PROPERTY(QString lockTransition READ lockTransition WRITE setLockTransition NOTIFY + lockTransitionChanged) + + public: + explicit SessionStore(QObject *parent = nullptr); + + bool isLocked() const { + return m_isLocked; + } + void setIsLocked(bool locked); + + bool isOnLockScreen() const { + return m_isOnLockScreen; + } + void setIsOnLockScreen(bool onLockScreen); + + bool showLockScreen() const { + return m_showLockScreen; + } + void setShowLockScreen(bool show); + + bool isAnimatingLock() const { + return m_isAnimatingLock; + } + void setIsAnimatingLock(bool animating); + + QString lockTransition() const { + return m_lockTransition; + } + void setLockTransition(const QString &transition); + + Q_INVOKABLE void lock(); + Q_INVOKABLE void unlock(); + Q_INVOKABLE bool checkSession() const; + Q_INVOKABLE void showLock(); + + signals: + void isLockedChanged(); + void isOnLockScreenChanged(); + void showLockScreenChanged(); + void isAnimatingLockChanged(); + void lockTransitionChanged(); + + void lockStateChanging(bool toLocked); + void lockStateChanged(bool isLocked); + void triggerShakeAnimation(); + void triggerUnlockAnimation(); + + private: + void startLockTimer(bool targetLocked); + + bool m_isLocked = true; + bool m_isOnLockScreen = false; + bool m_showLockScreen = true; + bool m_isAnimatingLock = false; + QString m_lockTransition; + qint64 m_sessionValidUntil = 0; + QTimer m_lockTimer; + bool m_targetLocked = true; +}; + +#endif From 4d7898abba3c626cd9264666ed02d5df0874c75e Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 17:50:32 +0000 Subject: [PATCH 13/28] fix(shell): restore SessionStore singleton wiring Ensure SessionStore is registered from C++ and reset on startup so the lock flow initializes consistently. --- shell/CMakeLists.txt | 7 +++++-- shell/main.cpp | 5 +++++ shell/qml/MarathonShell.qml | 4 ++++ shell/src/services/sessionstore.cpp | 10 ++++++++++ shell/src/services/sessionstore.h | 1 + 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 560d6d035..712e76e94 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -28,8 +28,6 @@ set(SOURCES src/services/notificationservicecpp.cpp src/services/telephonyintegrationcpp.h src/services/telephonyintegrationcpp.cpp - src/services/sessionstore.h - src/services/sessionstore.cpp src/components/crashhandler.h src/components/crashhandler.cpp qml/keyboard/Data/WordEngine.h @@ -222,6 +220,8 @@ set(QML_FILES qml/stores/WallpaperStore.qml qml/stores/SystemStatusStore.qml qml/stores/SystemControlStore.qml + src/services/sessionstore.h + src/services/sessionstore.cpp qml/stores/UIStore.qml qml/core/Constants.qml qml/core/Logger.qml @@ -303,6 +303,9 @@ qt6_add_qml_module(marathon-shell URI MarathonOS.Shell VERSION 1.0 QML_FILES ${QML_FILES} + SOURCES + src/services/sessionstore.cpp + src/services/sessionstore.h RESOURCES ${JSON_FILES} OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/shell/qml/MarathonOS/Shell ) diff --git a/shell/main.cpp b/shell/main.cpp index 52be5cd8d..4f1bace96 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -73,6 +73,7 @@ #include "src/services/unifiedsearchservicecpp.h" #include "src/services/telephonyintegrationcpp.h" #include "src/services/screenshotservicecpp.h" +#include "src/services/sessionstore.h" #include "qml/keyboard/Data/WordEngine.h" #include "src/dbus/freedesktopnotifications.h" #include "src/dbus/notificationdatabase.h" @@ -298,6 +299,10 @@ int main(int argc, char *argv[]) { QQmlApplicationEngine engine; + auto *sessionStore = new SessionStore(&app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "SessionStore", + sessionStore); + engine.addImageProvider("lunasvg", new LunaSvgImageProvider()); engine.addImageProvider("lunasvgqrc", new LunaSvgImageProvider()); diff --git a/shell/qml/MarathonShell.qml b/shell/qml/MarathonShell.qml index 40fc5514e..acf31b230 100644 --- a/shell/qml/MarathonShell.qml +++ b/shell/qml/MarathonShell.qml @@ -118,6 +118,10 @@ Item { AppLaunchService.uiStore = UIStore; AppLaunchService.appLifecycleManager = AppLifecycleManager; ScreenshotService.shellWindow = shell; + SessionStore.reset(); + showPinScreen = false; + pinScreen.reset(); + lockScreen.swipeProgress = 0; if (compositor) { compositor.systemBackTriggered.connect(handleBackKey); compositor.systemHomeTriggered.connect(handleHomeKey); diff --git a/shell/src/services/sessionstore.cpp b/shell/src/services/sessionstore.cpp index d13a8da00..0fe4beb0d 100644 --- a/shell/src/services/sessionstore.cpp +++ b/shell/src/services/sessionstore.cpp @@ -89,6 +89,16 @@ void SessionStore::showLock() { setShowLockScreen(true); } +void SessionStore::reset() { + m_lockTimer.stop(); + m_sessionValidUntil = 0; + setIsAnimatingLock(false); + setLockTransition(QString()); + setShowLockScreen(true); + setIsLocked(true); + setIsOnLockScreen(false); +} + void SessionStore::startLockTimer(bool targetLocked) { m_targetLocked = targetLocked; m_lockTimer.setInterval(300); diff --git a/shell/src/services/sessionstore.h b/shell/src/services/sessionstore.h index 51466ba4c..a6ad44a74 100644 --- a/shell/src/services/sessionstore.h +++ b/shell/src/services/sessionstore.h @@ -54,6 +54,7 @@ class SessionStore : public QObject { Q_INVOKABLE void unlock(); Q_INVOKABLE bool checkSession() const; Q_INVOKABLE void showLock(); + Q_INVOKABLE void reset(); signals: void isLockedChanged(); From 412ebbd3add2bcfb9df8b97e8cff6060329a8f21 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 17:50:48 +0000 Subject: [PATCH 14/28] fix(messages): handle conversation taps and ids Accept conversation taps with missing ids and guard unread badges to avoid runtime errors in test notifications. --- apps/messages/MessagesApp.qml | 9 ++++--- .../components/ConversationListItem.qml | 25 ++++++++----------- apps/messages/pages/ConversationsListPage.qml | 2 +- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/apps/messages/MessagesApp.qml b/apps/messages/MessagesApp.qml index 290944452..7dad006c1 100644 --- a/apps/messages/MessagesApp.qml +++ b/apps/messages/MessagesApp.qml @@ -8,15 +8,16 @@ MApp { id: messagesApp property var conversations: typeof SMSService !== 'undefined' ? SMSService.conversations : [] - property int selectedConversationId: -1 + property string selectedConversationId: "" function getConversation(id) { - if (!id) + if (id === undefined || id === null) return null; for (var i = 0; i < conversations.length; i++) { - if (conversations[i].id === id) - return conversations[i]; + var conversation = conversations[i]; + if (conversation.id === id || conversation.contactNumber === id) + return conversation; } Logger.warn("Messages", "Conversation not found: " + id); return null; diff --git a/apps/messages/components/ConversationListItem.qml b/apps/messages/components/ConversationListItem.qml index a301580f1..fd8c1168a 100644 --- a/apps/messages/components/ConversationListItem.qml +++ b/apps/messages/components/ConversationListItem.qml @@ -83,7 +83,7 @@ Item { Text { anchors.centerIn: parent - text: (conversation && conversation.unreadCount > 9) ? "9+" : ((conversation && conversation.unreadCount.toString()) || "") + text: (conversation && conversation.unreadCount > 9) ? "9+" : (conversation && conversation.unreadCount !== undefined ? String(conversation.unreadCount) : "") font.pixelSize: MTypography.sizeXSmall font.weight: MTypography.weightBold font.family: MTypography.fontFamily @@ -139,21 +139,16 @@ Item { } } - MouseArea { - anchors.fill: parent - onPressed: { - root.scale = 0.98; - HapticService.light(); - } - onReleased: { - root.scale = 1; - } - onCanceled: { - root.scale = 1; - } - onClicked: { - root.conversationClicked(); + TapHandler { + onPressedChanged: { + if (pressed) { + root.scale = 0.98; + HapticService.light(); + } else { + root.scale = 1; + } } + onTapped: root.conversationClicked() } Behavior on scale { diff --git a/apps/messages/pages/ConversationsListPage.qml b/apps/messages/pages/ConversationsListPage.qml index 3e400051c..b3389983a 100644 --- a/apps/messages/pages/ConversationsListPage.qml +++ b/apps/messages/pages/ConversationsListPage.qml @@ -232,7 +232,7 @@ Page { conversation: modelData onConversationClicked: { if (conversationItem.x === 0) - openConversation(modelData.id); + openConversation(modelData.id || modelData.contactNumber || ""); else conversationItem.x = 0; } From 93561eca7ce8ee30e1a23bc3a4afe56fe8633294 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 22:30:16 +0000 Subject: [PATCH 15/28] feat(shell): move core stores to C++ Replace QML system stores with C++ singletons, wire them in main, and update build sources so store logic lives in C++. --- shell/CMakeLists.txt | 32 +- shell/main.cpp | 43 ++- shell/qml/stores/AppStore.qml | 127 ------- shell/qml/stores/SystemControlStore.qml | 215 ----------- shell/qml/stores/SystemStatusStore.qml | 118 ------ shell/qml/stores/UIStore.qml | 141 ------- shell/qml/stores/WallpaperStore.qml | 94 ----- shell/src/services/appstore.cpp | 60 +++ shell/src/services/appstore.h | 38 ++ shell/src/services/systemcontrolstore.cpp | 425 ++++++++++++++++++++++ shell/src/services/systemcontrolstore.h | 200 ++++++++++ shell/src/services/systemstatusstore.cpp | 387 ++++++++++++++++++++ shell/src/services/systemstatusstore.h | 273 ++++++++++++++ shell/src/services/uistore.cpp | 222 +++++++++++ shell/src/services/uistore.h | 138 +++++++ shell/src/services/wallpaperstore.cpp | 129 +++++++ shell/src/services/wallpaperstore.h | 54 +++ 17 files changed, 1979 insertions(+), 717 deletions(-) delete mode 100644 shell/qml/stores/AppStore.qml delete mode 100644 shell/qml/stores/SystemControlStore.qml delete mode 100644 shell/qml/stores/SystemStatusStore.qml delete mode 100644 shell/qml/stores/UIStore.qml delete mode 100644 shell/qml/stores/WallpaperStore.qml create mode 100644 shell/src/services/appstore.cpp create mode 100644 shell/src/services/appstore.h create mode 100644 shell/src/services/systemcontrolstore.cpp create mode 100644 shell/src/services/systemcontrolstore.h create mode 100644 shell/src/services/systemstatusstore.cpp create mode 100644 shell/src/services/systemstatusstore.h create mode 100644 shell/src/services/uistore.cpp create mode 100644 shell/src/services/uistore.h create mode 100644 shell/src/services/wallpaperstore.cpp create mode 100644 shell/src/services/wallpaperstore.h diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 712e76e94..58f5fe752 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -94,6 +94,16 @@ set(SOURCES src/managers/alarmmanagercpp.cpp src/services/statusbariconservicecpp.h src/services/statusbariconservicecpp.cpp + src/services/systemstatusstore.h + src/services/systemstatusstore.cpp + src/services/systemcontrolstore.h + src/services/systemcontrolstore.cpp + src/services/uistore.h + src/services/uistore.cpp + src/services/appstore.h + src/services/appstore.cpp + src/services/wallpaperstore.h + src/services/wallpaperstore.cpp src/managers/flashlightmanagercpp.h src/managers/flashlightmanagercpp.cpp src/controllers/audioroutingmanager.h @@ -216,13 +226,6 @@ set(QML_FILES qml/components/MediaPlaybackManager.qml qml/apps/template/TemplateApp.qml qml/apps/native/NativeAppWindow.qml - qml/stores/AppStore.qml - qml/stores/WallpaperStore.qml - qml/stores/SystemStatusStore.qml - qml/stores/SystemControlStore.qml - src/services/sessionstore.h - src/services/sessionstore.cpp - qml/stores/UIStore.qml qml/core/Constants.qml qml/core/Logger.qml qml/core/SurfaceRegistry.qml @@ -255,11 +258,6 @@ qt6_add_executable(marathon-shell ${SOURCES}) # Mark singleton files BEFORE adding QML module set(SINGLETON_FILES - qml/stores/AppStore.qml - qml/stores/WallpaperStore.qml - qml/stores/SystemStatusStore.qml - qml/stores/SystemControlStore.qml - qml/stores/UIStore.qml qml/core/Constants.qml qml/core/Logger.qml qml/core/SurfaceRegistry.qml @@ -306,6 +304,16 @@ qt6_add_qml_module(marathon-shell SOURCES src/services/sessionstore.cpp src/services/sessionstore.h + src/services/systemstatusstore.cpp + src/services/systemstatusstore.h + src/services/systemcontrolstore.cpp + src/services/systemcontrolstore.h + src/services/uistore.cpp + src/services/uistore.h + src/services/appstore.cpp + src/services/appstore.h + src/services/wallpaperstore.cpp + src/services/wallpaperstore.h RESOURCES ${JSON_FILES} OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/shell/qml/MarathonOS/Shell ) diff --git a/shell/main.cpp b/shell/main.cpp index 4f1bace96..3cbe97ac9 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -74,6 +74,11 @@ #include "src/services/telephonyintegrationcpp.h" #include "src/services/screenshotservicecpp.h" #include "src/services/sessionstore.h" +#include "src/services/systemstatusstore.h" +#include "src/services/systemcontrolstore.h" +#include "src/services/uistore.h" +#include "src/services/appstore.h" +#include "src/services/wallpaperstore.h" #include "qml/keyboard/Data/WordEngine.h" #include "src/dbus/freedesktopnotifications.h" #include "src/dbus/notificationdatabase.h" @@ -316,6 +321,9 @@ int main(int argc, char *argv[]) { qInfo() << "[MarathonShell] ✓ MPRIS2 media controller initialized"; auto *settingsManager = createObject(ctx, "SettingsManagerCpp", &app); + auto *wallpaperStore = new WallpaperStore(settingsManager, &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "WallpaperStore", + wallpaperStore); createObject(ctx, "WaylandCompositorManager", &app); if (debugEnabled || profileMode) { @@ -333,6 +341,8 @@ int main(int argc, char *argv[]) { createObject(ctx, "DesktopFileParserCpp", &app); auto *appRegistry = createObject(ctx, "MarathonAppRegistry", &app); + auto *appStore = new AppStore(appRegistry, &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "AppStore", appStore); auto *appScanner = createObject(ctx, "MarathonAppScanner", appRegistry, &app); auto *appInstaller = createObject(ctx, "MarathonAppInstaller", @@ -353,13 +363,13 @@ int main(int argc, char *argv[]) { qmlRegisterUncreatableMetaObject(NotificationModel::staticMetaObject, "MarathonOS.Shell", 1, 0, "NotificationRoles", "Cannot create NotificationRoles enum"); - auto *networkManager = createObject(ctx, "NetworkManagerCpp", &app); - auto *powerManager = createObject(ctx, "PowerManagerService", &app); - auto *rotationManager = createObject(ctx, "RotationManager", &app); - auto *displayManager = createObject(ctx, "DisplayManagerCpp", powerManager, - rotationManager, &app); - auto *audioManager = createObject(ctx, "AudioManagerCpp", &app); - createObject(ctx, "ModemManagerCpp", &app); + auto *networkManager = createObject(ctx, "NetworkManagerCpp", &app); + auto *powerManager = createObject(ctx, "PowerManagerService", &app); + auto *rotationManager = createObject(ctx, "RotationManager", &app); + auto *displayManager = createObject(ctx, "DisplayManagerCpp", powerManager, + rotationManager, &app); + auto *audioManager = createObject(ctx, "AudioManagerCpp", &app); + auto *modemManager = createObject(ctx, "ModemManagerCpp", &app); auto *sensorManager = createObject(ctx, "SensorManagerCpp", &app); auto *bluetoothManager = createObject(ctx, "BluetoothManagerCpp", &app); auto *locationManager = createObject(ctx, "LocationManager", &app); @@ -367,7 +377,7 @@ int main(int argc, char *argv[]) { createObject(ctx, "ClipboardManagerCpp", settingsManager, &app); auto *alarmManager = createObject( ctx, "AlarmManagerCpp", settingsManager, powerManager, audioManager, hapticManager, &app); - createObject(ctx, "FlashlightManagerCpp", &app); + auto *flashlightManager = createObject(ctx, "FlashlightManagerCpp", &app); auto *audioRoutingManager = createObject(ctx, "AudioRoutingManagerCpp", &app); createObject(ctx, "SecurityManagerCpp", &app); @@ -418,8 +428,21 @@ int main(int argc, char *argv[]) { auto *notificationService = createObject( ctx, "NotificationService", notificationModel, settingsManager, audioPolicyController, hapticsObj, &app); - createObject(ctx, "ScreenshotService", audioPolicyController, hapticsObj, - notificationService, &app); + auto *systemStatusStore = + new SystemStatusStore(powerManager, networkManager, bluetoothManager, modemManager, + notificationService, settingsManager, &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "SystemStatusStore", + systemStatusStore); + auto *screenshotService = createObject( + ctx, "ScreenshotService", audioPolicyController, hapticsObj, notificationService, &app); + auto *systemControlStore = new SystemControlStore( + networkManager, bluetoothManager, displayManager, flashlightManager, modemManager, + settingsManager, alarmManager, locationManager, hapticManager, powerManager, audioManager, + audioPolicyController, screenshotService, &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "SystemControlStore", + systemControlStore); + auto *uiStore = new UIStore(&app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "UIStore", uiStore); createObject(ctx, "CursorManager", &app); if (debugEnabled || profileMode) { diff --git a/shell/qml/stores/AppStore.qml b/shell/qml/stores/AppStore.qml deleted file mode 100644 index 5c7d4d2d8..000000000 --- a/shell/qml/stores/AppStore.qml +++ /dev/null @@ -1,127 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: appStore - - property var marathonApps: [ - { - "id": "phone", - "name": "Phone", - "icon": "phone", - "type": "marathon" - }, - { - "id": "messages", - "name": "Messages", - "icon": "message-square", - "type": "marathon" - }, - { - "id": "camera", - "name": "Camera", - "icon": "camera", - "type": "marathon" - }, - { - "id": "gallery", - "name": "Gallery", - "icon": "image", - "type": "marathon" - }, - { - "id": "music", - "name": "Music", - "icon": "qrc:/images/music.svg", - "type": "marathon" - }, - { - "id": "calendar", - "name": "Calendar", - "icon": "qrc:/images/calendar.svg", - "type": "marathon" - }, - { - "id": "clock", - "name": "Clock", - "icon": "qrc:/images/clock.svg", - "type": "marathon" - }, - { - "id": "maps", - "name": "Maps", - "icon": "qrc:/images/maps.svg", - "type": "marathon" - }, - { - "id": "notes", - "name": "Notes", - "icon": "qrc:/images/notes.svg", - "type": "marathon" - }, - { - "id": "settings", - "name": "Settings", - "icon": "qrc:/images/settings.svg", - "type": "marathon" - } - ] - property var nativeApps: [] - property var apps: [] - property Connections desktopEntryConnection - - function getApp(appId) { - for (var i = 0; i < apps.length; i++) { - if (apps[i].id === appId) - return apps[i]; - } - return null; - } - - function getAppName(appId) { - var app = getApp(appId); - return app ? app.name : appId; - } - - function getAppIcon(appId) { - var app = getApp(appId); - return app ? app.icon : ""; - } - - function isInternalApp(appId) { - var app = getApp(appId); - return app ? (app.type === "marathon") : true; - } - - function isNativeApp(appId) { - var app = getApp(appId); - return app ? (app.type === "native") : false; - } - - function refreshAppList() { - var merged = []; - for (var i = 0; i < marathonApps.length; i++) { - merged.push(marathonApps[i]); - } - for (var j = 0; j < nativeApps.length; j++) { - merged.push(nativeApps[j]); - } - apps = merged; - Logger.info("AppStore", "App list refreshed. Total: " + apps.length + " (Marathon: " + marathonApps.length + ", Native: " + nativeApps.length + ")"); - } - - Component.onCompleted: { - Logger.info("AppStore", "AppStore initialized"); - refreshAppList(); - } - - desktopEntryConnection: Connections { - function onScanComplete(count) { - Logger.info("AppStore", "Native apps scan complete: " + count + " apps"); - nativeApps = DesktopEntryParser.nativeApps; - refreshAppList(); - } - - target: typeof DesktopEntryParser !== 'undefined' ? DesktopEntryParser : null - } -} diff --git a/shell/qml/stores/SystemControlStore.qml b/shell/qml/stores/SystemControlStore.qml deleted file mode 100644 index 4cf3cec02..000000000 --- a/shell/qml/stores/SystemControlStore.qml +++ /dev/null @@ -1,215 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: systemControl - - property bool isWifiOn: typeof NetworkManagerCpp !== 'undefined' && NetworkManagerCpp ? NetworkManagerCpp.wifiEnabled : true - property bool isBluetoothOn: typeof BluetoothManagerCpp !== 'undefined' && BluetoothManagerCpp ? BluetoothManagerCpp.enabled : false - property bool isAirplaneModeOn: typeof NetworkManagerCpp !== 'undefined' && NetworkManagerCpp ? NetworkManagerCpp.airplaneModeEnabled : false - property bool isRotationLocked: (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? DisplayManagerCpp.rotationLocked : false - property bool isFlashlightOn: (typeof FlashlightManagerCpp !== "undefined" && FlashlightManagerCpp) ? FlashlightManagerCpp.enabled : false - property bool isCellularOn: typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp ? ModemManagerCpp.modemEnabled : false - property bool isCellularDataOn: typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp ? ModemManagerCpp.dataEnabled : false - property bool isDndMode: typeof SettingsManagerCpp !== 'undefined' && SettingsManagerCpp ? SettingsManagerCpp.dndEnabled : false - property bool isAlarmOn: typeof AlarmManagerCpp !== 'undefined' && AlarmManagerCpp ? (AlarmManagerCpp.hasActiveAlarm || AlarmManagerCpp.hasEnabledAlarm) : false - property bool isAutoBrightnessOn: (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? DisplayManagerCpp.autoBrightnessEnabled : false - property bool isLocationOn: typeof LocationManager !== 'undefined' ? LocationManager.active : false - property bool isHotspotOn: typeof NetworkManagerCpp !== 'undefined' ? NetworkManagerCpp.isHotspotActive() : false - property bool isVibrationOn: typeof HapticManager !== 'undefined' ? HapticManager.enabled : true - property bool isNightLightOn: (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? DisplayManagerCpp.nightLightEnabled : false - property int brightness: 50 - property int volume: 50 - property Binding brightnessBinding - property Binding volumeBinding - property bool isLowPowerMode: (typeof PowerManagerService !== "undefined" && PowerManagerService) ? PowerManagerService.isPowerSaveMode : false - property Binding rotationLockBinding - - function toggleWifi() { - if (typeof NetworkManagerCpp !== 'undefined' && NetworkManagerCpp) - NetworkManagerCpp.toggleWifi(); - - Logger.info("SystemControl", "WiFi toggled to: " + (typeof NetworkManagerCpp !== 'undefined' && NetworkManagerCpp ? NetworkManagerCpp.wifiEnabled : "unknown")); - } - - function toggleBluetooth() { - if (typeof BluetoothManagerCpp !== 'undefined' && BluetoothManagerCpp) - BluetoothManagerCpp.enabled = !BluetoothManagerCpp.enabled; - - Logger.info("SystemControl", "Bluetooth toggled to: " + (typeof BluetoothManagerCpp !== 'undefined' && BluetoothManagerCpp ? BluetoothManagerCpp.enabled : "unknown")); - } - - function toggleAirplaneMode() { - var newMode = !isAirplaneModeOn; - if (typeof NetworkManagerCpp !== 'undefined' && NetworkManagerCpp) - NetworkManagerCpp.setAirplaneMode(newMode); - - Logger.info("SystemControl", "Airplane mode toggled to: " + newMode); - } - - function toggleRotationLock() { - var newLock = !isRotationLocked; - if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.rotationLocked = newLock; - - Logger.info("SystemControl", "Rotation lock: " + newLock); - } - - function toggleFlashlight() { - if (typeof FlashlightManagerCpp !== "undefined" && FlashlightManagerCpp) - FlashlightManagerCpp.toggle(); - - Logger.info("SystemControl", "Flashlight: " + isFlashlightOn); - } - - function toggleCellular() { - if (typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp) { - if (ModemManagerCpp.modemEnabled) - ModemManagerCpp.disable(); - else - ModemManagerCpp.enable(); - } - Logger.info("SystemControl", "Cellular: " + isCellularOn); - } - - function toggleCellularData() { - if (typeof ModemManagerCpp !== 'undefined' && ModemManagerCpp) { - if (ModemManagerCpp.dataEnabled) - ModemManagerCpp.disableData(); - else - ModemManagerCpp.enableData(); - } - Logger.info("SystemControl", "Cellular Data: " + isCellularDataOn); - } - - function toggleDndMode() { - var newMode = !isDndMode; - if (typeof SettingsManagerCpp !== 'undefined' && SettingsManagerCpp) - SettingsManagerCpp.dndEnabled = newMode; - if (typeof AudioPolicyControllerCpp !== 'undefined' && AudioPolicyControllerCpp) - AudioPolicyControllerCpp.setDoNotDisturb(newMode); - Logger.info("SystemControl", "DND mode toggled to: " + newMode); - } - - function toggleAlarm() { - Logger.info("SystemControl", "Alarm quick settings tapped - opening Clock app"); - } - - function toggleLowPowerMode() { - var newMode = !isLowPowerMode; - if (typeof PowerManagerService !== "undefined" && PowerManagerService) - PowerManagerService.setPowerSaveMode(newMode); - - Logger.info("SystemControl", "Low power mode toggled to: " + newMode); - } - - function toggleAutoBrightness() { - var newMode = !isAutoBrightnessOn; - if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.autoBrightnessEnabled = newMode; - - Logger.info("SystemControl", "Auto-brightness toggled to: " + newMode); - } - - function toggleLocation() { - if (typeof LocationManager !== 'undefined') { - if (isLocationOn) - LocationManager.stop(); - else - LocationManager.start(); - Logger.info("SystemControl", "Location toggled to: " + !isLocationOn); - } - } - - function toggleHotspot() { - if (typeof NetworkManagerCpp !== 'undefined') { - if (isHotspotOn) - NetworkManagerCpp.stopHotspot(); - else - NetworkManagerCpp.createHotspot("Marathon Hotspot", "marathon2025"); - Logger.info("SystemControl", "Hotspot toggled"); - } - } - - function toggleVibration() { - if (typeof HapticManager !== 'undefined') { - var newMode = !isVibrationOn; - HapticManager.setEnabled(newMode); - Logger.info("SystemControl", "Vibration toggled to: " + newMode); - } - } - - function toggleNightLight() { - var newMode = !isNightLightOn; - if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.nightLightEnabled = newMode; - - Logger.info("SystemControl", "Night Light toggled to: " + newMode); - } - - function captureScreenshot() { - Logger.info("SystemControl", "Screenshot captured"); - if (typeof ScreenshotService !== 'undefined') - ScreenshotService.captureScreen(); - } - - function setBrightness(value) { - var clamped = Math.max(0, Math.min(100, value)); - if (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) - DisplayManagerCpp.brightness = clamped / 100; - - Logger.debug("SystemControl", "Brightness: " + clamped); - } - - function setVolume(value) { - var clamped = Math.max(0, Math.min(100, value)); - if (typeof AudioManagerCpp !== 'undefined' && AudioManagerCpp) - AudioManagerCpp.setVolume(clamped / 100); - else if (typeof AudioPolicyControllerCpp !== 'undefined' && AudioPolicyControllerCpp) - AudioPolicyControllerCpp.setMasterVolume(clamped / 100); - Logger.debug("SystemControl", "Volume: " + clamped); - } - - function sleep() { - Logger.info("SystemControl", "Sleep triggered"); - if (typeof PowerManagerService !== "undefined" && PowerManagerService) - PowerManagerService.suspend(); - } - - function powerOff() { - Logger.info("SystemControl", "Power off triggered"); - if (typeof PowerManagerService !== "undefined" && PowerManagerService) - PowerManagerService.shutdown(); - } - - function reboot() { - Logger.info("SystemControl", "Reboot triggered"); - if (typeof PowerManagerService !== "undefined" && PowerManagerService) - PowerManagerService.restart(); - } - - Component.onCompleted: { - console.log("[SystemControlStore] Initialized with real services"); - } - - brightnessBinding: Binding { - target: systemControl - property: "brightness" - value: (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? Math.round(DisplayManagerCpp.brightness * 100) : 50 - restoreMode: Binding.RestoreBinding - } - - volumeBinding: Binding { - target: systemControl - property: "volume" - value: (typeof AudioManagerCpp !== 'undefined' && AudioManagerCpp) ? Math.round(AudioManagerCpp.volume * 100) : 50 - restoreMode: Binding.RestoreBinding - } - - rotationLockBinding: Binding { - target: systemControl - property: "isRotationLocked" - value: (typeof DisplayManagerCpp !== "undefined" && DisplayManagerCpp) ? DisplayManagerCpp.rotationLocked : false - restoreMode: Binding.RestoreBinding - } -} diff --git a/shell/qml/stores/SystemStatusStore.qml b/shell/qml/stores/SystemStatusStore.qml deleted file mode 100644 index d5c5cd926..000000000 --- a/shell/qml/stores/SystemStatusStore.qml +++ /dev/null @@ -1,118 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: systemStatus - - property int batteryLevel: (typeof PowerManagerService !== "undefined" && PowerManagerService) ? PowerManagerService.batteryLevel : 0 - property bool isCharging: (typeof PowerManagerService !== "undefined" && PowerManagerService) ? PowerManagerService.isCharging : false - property bool isPluggedIn: (typeof PowerManagerService !== "undefined" && PowerManagerService) ? PowerManagerService.isPluggedIn : false - property string chargingType: isCharging ? "usb" : "none" - property bool isWifiOn: typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.wifiEnabled : true - property int wifiStrength: typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.wifiSignalStrength : 0 - property string wifiNetwork: typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.wifiSsid : "" - property bool wifiConnected: typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.wifiConnected : false - property bool ethernetConnected: typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.ethernetConnected : false - property bool isBluetoothOn: typeof BluetoothManagerCpp !== "undefined" && BluetoothManagerCpp ? BluetoothManagerCpp.enabled : false - property var bluetoothDevices: typeof BluetoothManagerCpp !== "undefined" && BluetoothManagerCpp ? BluetoothManagerCpp.pairedDevices : [] - readonly property int bluetoothConnectedDevicesCount: { - var count = 0; - var list = bluetoothDevices || []; - for (var i = 0; i < list.length; i++) { - if (list[i] && list[i].connected) - count++; - } - return count; - } - readonly property bool isBluetoothConnected: bluetoothConnectedDevicesCount > 0 - property bool isAirplaneMode: typeof NetworkManagerCpp !== "undefined" && NetworkManagerCpp ? NetworkManagerCpp.airplaneModeEnabled : false - property bool isDndMode: NotificationService.isDndEnabled - property int cellularStrength: typeof ModemManagerCpp !== "undefined" && ModemManagerCpp ? ModemManagerCpp.signalStrength : 0 - property string carrier: typeof ModemManagerCpp !== "undefined" && ModemManagerCpp ? ModemManagerCpp.operatorName : "" - property string dataType: typeof ModemManagerCpp !== "undefined" && ModemManagerCpp ? ModemManagerCpp.networkType : "" - property int cpuUsage: 23 - property int memoryUsage: 45 - property real storageUsed: 45.2 - property real storageTotal: 128 - property date currentTime: new Date() - property string timeString - property string dateString - property var notifications: [] - property int notificationCount: NotificationService.unreadCount - property Timer updateTimer - property Connections timeFormatConnection - property Connections powerManagerConnections - property Connections notificationServiceConnections - - function updateTime() { - systemStatus.currentTime = new Date(); - var tf = (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp) ? SettingsManagerCpp.timeFormat : "24h"; - systemStatus.timeString = Qt.formatTime(systemStatus.currentTime, tf === "24h" ? "HH:mm" : "h:mm AP"); - systemStatus.dateString = Qt.formatDate(systemStatus.currentTime, "dddd, MMMM d"); - } - - function addNotification(title, message, app) { - NotificationService.sendNotification(app, title, message, { - "category": "message", - "priority": "normal" - }); - } - - function removeNotification(notificationId) { - NotificationService.dismissNotification(notificationId); - } - - function clearAllNotifications() { - NotificationService.dismissAllNotifications(); - } - - function updateSystemInfo() { - cpuUsage = Math.floor(Math.random() * 30) + 10; - memoryUsage = Math.floor(Math.random() * 20) + 35; - } - - Component.onCompleted: { - console.log("[SystemStatusStore] Initialized with real services"); - notifications = NotificationService.notifications; - } - - updateTimer: Timer { - interval: 16 - running: true - repeat: true - triggeredOnStart: true - onTriggered: { - updateTime(); - if (interval === 16) - interval = 1000; - } - } - - timeFormatConnection: Connections { - function onTimeFormatChanged() { - updateTime(); - } - - target: typeof SettingsManagerCpp !== 'undefined' ? SettingsManagerCpp : null - } - - powerManagerConnections: Connections { - target: typeof PowerManagerService !== "undefined" ? PowerManagerService : null - } - - notificationServiceConnections: Connections { - function onNotificationReceived(notification) { - notifications.push(notification); - notifications = notifications; - } - - function onNotificationDismissed(id) { - notifications = notifications.filter(function (n) { - return n.id !== id; - }); - notificationCount = NotificationService.unreadCount; - } - - target: NotificationService - } -} diff --git a/shell/qml/stores/UIStore.qml b/shell/qml/stores/UIStore.qml deleted file mode 100644 index d1aa06436..000000000 --- a/shell/qml/stores/UIStore.qml +++ /dev/null @@ -1,141 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: uiStore - - property bool quickSettingsOpen: false - property real quickSettingsHeight: 0 - property bool quickSettingsDragging: false - property bool appWindowOpen: false - property string currentAppId: "" - property string currentAppName: "" - property string currentAppIcon: "" - property bool settingsOpen: false - property bool searchOpen: false - property bool shareSheetOpen: false - property bool clipboardManagerOpen: false - property var shellRef: null - - signal showNotificationToast(var notification) - signal showSystemHUD(string type, real value) - signal showConfirmDialog(string title, string message, var onConfirm) - signal showShareSheet(var content, string contentType) - - function openSearch() { - searchOpen = true; - Logger.state("UIStore", "search closed", "open"); - } - - function closeSearch() { - searchOpen = false; - Logger.state("UIStore", "search open", "closed"); - } - - function toggleSearch() { - if (searchOpen) - closeSearch(); - else - openSearch(); - } - - function openShareSheet(content, contentType) { - showShareSheet(content, contentType || "text"); - shareSheetOpen = true; - } - - function closeShareSheet() { - shareSheetOpen = false; - } - - function openClipboardManager() { - clipboardManagerOpen = true; - } - - function closeClipboardManager() { - clipboardManagerOpen = false; - } - - function openQuickSettings() { - quickSettingsOpen = true; - if (shellRef) - quickSettingsHeight = shellRef.maxQuickSettingsHeight; - else - quickSettingsHeight = 1000; - Logger.state("UIStore", "quickSettings closed", "open"); - } - - function closeQuickSettings() { - quickSettingsOpen = false; - quickSettingsHeight = 0; - Logger.state("UIStore", "quickSettings open", "closed"); - } - - function toggleQuickSettings() { - if (quickSettingsOpen) - closeQuickSettings(); - else - openQuickSettings(); - } - - function openApp(appId, appName, appIcon) { - currentAppId = appId; - currentAppName = appName; - currentAppIcon = appIcon; - appWindowOpen = true; - if (appId === "settings") - settingsOpen = true; - } - - function closeApp() { - appWindowOpen = false; - if (currentAppId === "settings") - settingsOpen = false; - - currentAppId = ""; - currentAppName = ""; - currentAppIcon = ""; - } - - function minimizeApp() { - appWindowOpen = false; - if (currentAppId === "settings") - settingsOpen = false; - - currentAppId = ""; - currentAppName = ""; - currentAppIcon = ""; - } - - function restoreApp(appId, appName, appIcon) { - appWindowOpen = true; - currentAppName = appName; - currentAppIcon = appIcon; - currentAppId = appId; - if (appId === "settings") - settingsOpen = true; - } - - function openSettings() { - settingsOpen = true; - Logger.state("UIStore", "settings closed", "open"); - } - - function closeSettings() { - settingsOpen = false; - Logger.state("UIStore", "settings open", "closed"); - } - - function minimizeSettings() { - minimizeApp(); - } - - function closeAll() { - closeQuickSettings(); - closeApp(); - closeSettings(); - closeSearch(); - closeShareSheet(); - closeClipboardManager(); - } -} diff --git a/shell/qml/stores/WallpaperStore.qml b/shell/qml/stores/WallpaperStore.qml deleted file mode 100644 index 21b076a20..000000000 --- a/shell/qml/stores/WallpaperStore.qml +++ /dev/null @@ -1,94 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: wallpaperStore - - property string currentWallpaper: "qrc:/wallpapers/wallpaper.jpg" - property string path: currentWallpaper - property bool isDark: true - property var wallpapers: [ - { - "name": "Gradient 1", - "path": asset("wallpapers/wallpaper.jpg", "qrc:/wallpapers/wallpaper.jpg"), - "isDark": true - }, - { - "name": "Gradient 2", - "path": asset("wallpapers/wallpaper2.jpg", "qrc:/wallpapers/wallpaper2.jpg"), - "isDark": true - }, - { - "name": "Gradient 3", - "path": asset("wallpapers/wallpaper3.jpg", "qrc:/wallpapers/wallpaper3.jpg"), - "isDark": true - }, - { - "name": "Gradient 4", - "path": asset("wallpapers/wallpaper4.jpg", "qrc:/wallpapers/wallpaper4.jpg"), - "isDark": false - }, - { - "name": "Gradient 5", - "path": asset("wallpapers/wallpaper5.jpg", "qrc:/wallpapers/wallpaper5.jpg"), - "isDark": true - }, - { - "name": "Gradient 6", - "path": asset("wallpapers/wallpaper6.jpg", "qrc:/wallpapers/wallpaper6.jpg"), - "isDark": false - }, - { - "name": "Gradient 7", - "path": asset("wallpapers/wallpaper7.jpg", "qrc:/wallpapers/wallpaper7.jpg"), - "isDark": true - }, - { - "name": "Gradient 8", - "path": asset("wallpapers/wallpaper8.jpg", "qrc:/wallpapers/wallpaper8.jpg"), - "isDark": true - } - ] - - function asset(rel, qrcFallback) { - if (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp && SettingsManagerCpp.assetUrl) - return SettingsManagerCpp.assetUrl(rel); - - return qrcFallback; - } - - function setWallpaper(newPath, newIsDark) { - if (!newPath || newPath === currentWallpaper) { - if (newIsDark !== undefined) - isDark = newIsDark; - - return; - } - currentWallpaper = newPath; - if (newIsDark !== undefined) - isDark = newIsDark; - - if (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp && SettingsManagerCpp.wallpaperPath !== newPath) - SettingsManagerCpp.wallpaperPath = newPath; - } - - onCurrentWallpaperChanged: { - Logger.info("WallpaperStore", "Wallpaper changed to: " + currentWallpaper); - for (var i = 0; i < wallpapers.length; i++) { - if (wallpapers[i].path === currentWallpaper) { - isDark = wallpapers[i].isDark; - break; - } - } - } - Component.onCompleted: { - if (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp && SettingsManagerCpp.wallpaperPath) - currentWallpaper = SettingsManagerCpp.wallpaperPath; - - if (typeof SettingsManagerCpp !== "undefined" && SettingsManagerCpp && SettingsManagerCpp.wallpaperPathChanged) - SettingsManagerCpp.wallpaperPathChanged.connect(function () { - if (SettingsManagerCpp && SettingsManagerCpp.wallpaperPath && currentWallpaper !== SettingsManagerCpp.wallpaperPath) - currentWallpaper = SettingsManagerCpp.wallpaperPath; - }); - } -} diff --git a/shell/src/services/appstore.cpp b/shell/src/services/appstore.cpp new file mode 100644 index 000000000..7c59253d7 --- /dev/null +++ b/shell/src/services/appstore.cpp @@ -0,0 +1,60 @@ +#include "appstore.h" +#include "marathonappregistry.h" + +AppStore::AppStore(MarathonAppRegistry *appRegistry, QObject *parent) + : QObject(parent) + , m_appRegistry(appRegistry) { + if (m_appRegistry) { + connect(m_appRegistry, &MarathonAppRegistry::appRegistered, this, + [this]() { refreshApps(); }); + connect(m_appRegistry, &MarathonAppRegistry::appUnregistered, this, + [this]() { refreshApps(); }); + connect(m_appRegistry, &MarathonAppRegistry::countChanged, this, + [this]() { refreshApps(); }); + } + refreshApps(); +} + +QVariantMap AppStore::getApp(const QString &appId) const { + if (!m_appRegistry) { + return {}; + } + return m_appRegistry->getApp(appId); +} + +QString AppStore::getAppName(const QString &appId) const { + const QVariantMap app = getApp(appId); + const QString name = app.value("name").toString(); + return name.isEmpty() ? appId : name; +} + +QString AppStore::getAppIcon(const QString &appId) const { + const QVariantMap app = getApp(appId); + return app.value("icon").toString(); +} + +bool AppStore::isInternalApp(const QString &appId) const { + const QVariantMap app = getApp(appId); + if (app.isEmpty()) { + return true; + } + return app.value("type").toInt() == static_cast(MarathonAppRegistry::Marathon); +} + +bool AppStore::isNativeApp(const QString &appId) const { + const QVariantMap app = getApp(appId); + return app.value("type").toInt() == static_cast(MarathonAppRegistry::Native); +} + +void AppStore::refreshApps() { + if (!m_appRegistry) { + return; + } + QVariantList updated; + const QStringList ids = m_appRegistry->getAllAppIds(); + for (const QString &id : ids) { + updated.append(m_appRegistry->getApp(id)); + } + m_apps = updated; + emit appsChanged(); +} diff --git a/shell/src/services/appstore.h b/shell/src/services/appstore.h new file mode 100644 index 000000000..0aa520ebc --- /dev/null +++ b/shell/src/services/appstore.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +class MarathonAppRegistry; + +class AppStore : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QVariantList apps READ apps NOTIFY appsChanged) + + public: + explicit AppStore(MarathonAppRegistry *appRegistry, QObject *parent = nullptr); + + QVariantList apps() const { + return m_apps; + } + + Q_INVOKABLE QVariantMap getApp(const QString &appId) const; + Q_INVOKABLE QString getAppName(const QString &appId) const; + Q_INVOKABLE QString getAppIcon(const QString &appId) const; + Q_INVOKABLE bool isInternalApp(const QString &appId) const; + Q_INVOKABLE bool isNativeApp(const QString &appId) const; + + signals: + void appsChanged(); + + private: + void refreshApps(); + + MarathonAppRegistry *m_appRegistry; + QVariantList m_apps; +}; diff --git a/shell/src/services/systemcontrolstore.cpp b/shell/src/services/systemcontrolstore.cpp new file mode 100644 index 000000000..9f2100a83 --- /dev/null +++ b/shell/src/services/systemcontrolstore.cpp @@ -0,0 +1,425 @@ +#include "systemcontrolstore.h" +#include "src/controllers/audiopolicycontroller.h" +#include "src/managers/alarmmanagercpp.h" +#include "src/managers/audiomanagercpp.h" +#include "src/managers/bluetoothmanager.h" +#include "src/managers/displaymanagercpp.h" +#include "src/managers/flashlightmanagercpp.h" +#include "src/managers/hapticmanager.h" +#include "src/managers/locationmanager.h" +#include "src/managers/modemmanagercpp.h" +#include "src/managers/networkmanagercpp.h" +#include "src/managers/powermanagercpp.h" +#include "src/managers/settingsmanager.h" +#include "src/services/screenshotservicecpp.h" +#include +#include + +SystemControlStore::SystemControlStore( + NetworkManagerCpp *networkManager, BluetoothManager *bluetoothManager, + DisplayManagerCpp *displayManager, FlashlightManagerCpp *flashlightManager, + ModemManagerCpp *modemManager, SettingsManager *settingsManager, AlarmManagerCpp *alarmManager, + LocationManager *locationManager, HapticManager *hapticManager, PowerManagerCpp *powerManager, + AudioManagerCpp *audioManager, AudioPolicyController *audioPolicyController, + ScreenshotServiceCpp *screenshotService, QObject *parent) + : QObject(parent) + , m_networkManager(networkManager) + , m_bluetoothManager(bluetoothManager) + , m_displayManager(displayManager) + , m_flashlightManager(flashlightManager) + , m_modemManager(modemManager) + , m_settingsManager(settingsManager) + , m_alarmManager(alarmManager) + , m_locationManager(locationManager) + , m_hapticManager(hapticManager) + , m_powerManager(powerManager) + , m_audioManager(audioManager) + , m_audioPolicyController(audioPolicyController) + , m_screenshotService(screenshotService) { + if (m_networkManager) { + setIsWifiOn(m_networkManager->wifiEnabled()); + setIsAirplaneModeOn(m_networkManager->airplaneModeEnabled()); + setIsHotspotOn(m_networkManager->isHotspotActive()); + connect(m_networkManager, &NetworkManagerCpp::wifiEnabledChanged, this, + [this]() { setIsWifiOn(m_networkManager->wifiEnabled()); }); + connect(m_networkManager, &NetworkManagerCpp::airplaneModeEnabledChanged, this, + [this]() { setIsAirplaneModeOn(m_networkManager->airplaneModeEnabled()); }); + } + + if (m_bluetoothManager) { + setIsBluetoothOn(m_bluetoothManager->enabled()); + connect(m_bluetoothManager, &BluetoothManager::enabledChanged, this, + [this]() { setIsBluetoothOn(m_bluetoothManager->enabled()); }); + } + + if (m_displayManager) { + setIsRotationLocked(m_displayManager->rotationLocked()); + setIsAutoBrightnessOn(m_displayManager->autoBrightnessEnabled()); + setIsNightLightOn(m_displayManager->nightLightEnabled()); + setBrightnessValue(static_cast( + std::round(std::clamp(m_displayManager->brightness(), 0.0, 1.0) * 100.0))); + connect(m_displayManager, &DisplayManagerCpp::rotationLockedChanged, this, + [this]() { setIsRotationLocked(m_displayManager->rotationLocked()); }); + connect(m_displayManager, &DisplayManagerCpp::autoBrightnessEnabledChanged, this, + [this]() { setIsAutoBrightnessOn(m_displayManager->autoBrightnessEnabled()); }); + connect(m_displayManager, &DisplayManagerCpp::nightLightEnabledChanged, this, + [this]() { setIsNightLightOn(m_displayManager->nightLightEnabled()); }); + connect(m_displayManager, &DisplayManagerCpp::brightnessChanged, this, [this]() { + setBrightnessValue(static_cast( + std::round(std::clamp(m_displayManager->brightness(), 0.0, 1.0) * 100.0))); + }); + } + + if (m_flashlightManager) { + setIsFlashlightOn(m_flashlightManager->enabled()); + connect(m_flashlightManager, &FlashlightManagerCpp::enabledChanged, this, + [this]() { setIsFlashlightOn(m_flashlightManager->enabled()); }); + } + + if (m_modemManager) { + setIsCellularOn(m_modemManager->modemEnabled()); + setIsCellularDataOn(m_modemManager->dataEnabled()); + connect(m_modemManager, &ModemManagerCpp::modemEnabledChanged, this, + [this]() { setIsCellularOn(m_modemManager->modemEnabled()); }); + connect(m_modemManager, &ModemManagerCpp::dataEnabledChanged, this, + [this]() { setIsCellularDataOn(m_modemManager->dataEnabled()); }); + } + + if (m_settingsManager) { + setIsDndMode(m_settingsManager->dndEnabled()); + connect(m_settingsManager, &SettingsManager::dndEnabledChanged, this, + [this]() { setIsDndMode(m_settingsManager->dndEnabled()); }); + } + + if (m_alarmManager) { + refreshAlarmState(); + connect(m_alarmManager, &AlarmManagerCpp::alarmsChanged, this, + [this]() { refreshAlarmState(); }); + connect(m_alarmManager, &AlarmManagerCpp::activeAlarmsChanged, this, + [this]() { refreshAlarmState(); }); + } + + if (m_locationManager) { + setIsLocationOn(m_locationManager->active()); + connect(m_locationManager, &LocationManager::activeChanged, this, + [this]() { setIsLocationOn(m_locationManager->active()); }); + } + + if (m_hapticManager) { + setIsVibrationOn(m_hapticManager->enabled()); + connect(m_hapticManager, &HapticManager::enabledChanged, this, + [this]() { setIsVibrationOn(m_hapticManager->enabled()); }); + } + + if (m_powerManager) { + setIsLowPowerMode(m_powerManager->isPowerSaveMode()); + connect(m_powerManager, &PowerManagerCpp::isPowerSaveModeChanged, this, + [this]() { setIsLowPowerMode(m_powerManager->isPowerSaveMode()); }); + } + + if (m_audioManager) { + setVolumeValue( + static_cast(std::round(std::clamp(m_audioManager->volume(), 0.0, 1.0) * 100.0))); + connect(m_audioManager, &AudioManagerCpp::volumeChanged, this, [this]() { + setVolumeValue(static_cast( + std::round(std::clamp(m_audioManager->volume(), 0.0, 1.0) * 100.0))); + }); + } +} + +void SystemControlStore::toggleWifi() { + if (m_networkManager) { + m_networkManager->toggleWifi(); + } +} + +void SystemControlStore::toggleBluetooth() { + if (m_bluetoothManager) { + m_bluetoothManager->setEnabled(!m_bluetoothManager->enabled()); + } +} + +void SystemControlStore::toggleAirplaneMode() { + if (m_networkManager) { + m_networkManager->setAirplaneMode(!m_isAirplaneModeOn); + } +} + +void SystemControlStore::toggleRotationLock() { + if (m_displayManager) { + m_displayManager->setRotationLock(!m_isRotationLocked); + } +} + +void SystemControlStore::toggleFlashlight() { + if (m_flashlightManager) { + m_flashlightManager->toggle(); + } +} + +void SystemControlStore::toggleCellular() { + if (!m_modemManager) { + return; + } + if (m_modemManager->modemEnabled()) { + m_modemManager->disable(); + } else { + m_modemManager->enable(); + } +} + +void SystemControlStore::toggleCellularData() { + if (!m_modemManager) { + return; + } + if (m_modemManager->dataEnabled()) { + m_modemManager->disableData(); + } else { + m_modemManager->enableData(); + } +} + +void SystemControlStore::toggleDndMode() { + if (m_settingsManager) { + m_settingsManager->setDndEnabled(!m_isDndMode); + } + if (m_audioPolicyController) { + m_audioPolicyController->setDoNotDisturb(!m_isDndMode); + } +} + +void SystemControlStore::toggleAlarm() {} + +void SystemControlStore::toggleLowPowerMode() { + if (m_powerManager) { + m_powerManager->setPowerSaveMode(!m_isLowPowerMode); + } +} + +void SystemControlStore::toggleAutoBrightness() { + if (m_displayManager) { + m_displayManager->setAutoBrightness(!m_isAutoBrightnessOn); + } +} + +void SystemControlStore::toggleLocation() { + if (!m_locationManager) { + return; + } + if (m_isLocationOn) { + m_locationManager->stop(); + } else { + m_locationManager->start(); + } +} + +void SystemControlStore::toggleHotspot() { + if (!m_networkManager) { + return; + } + if (m_networkManager->isHotspotActive()) { + m_networkManager->stopHotspot(); + setIsHotspotOn(false); + } else { + m_networkManager->createHotspot("Marathon Hotspot", "marathon2025"); + setIsHotspotOn(true); + } +} + +void SystemControlStore::toggleVibration() { + if (m_hapticManager) { + m_hapticManager->setEnabled(!m_isVibrationOn); + } +} + +void SystemControlStore::toggleNightLight() { + if (m_displayManager) { + m_displayManager->setNightLightEnabled(!m_isNightLightOn); + } +} + +void SystemControlStore::captureScreenshot() { + if (m_screenshotService) { + m_screenshotService->captureScreen(); + } +} + +void SystemControlStore::setBrightness(int value) { + int clamped = std::clamp(value, 0, 100); + if (m_displayManager) { + m_displayManager->setBrightness(static_cast(clamped) / 100.0); + } + setBrightnessValue(clamped); +} + +void SystemControlStore::setVolume(int value) { + int clamped = std::clamp(value, 0, 100); + if (m_audioManager) { + m_audioManager->setVolume(static_cast(clamped) / 100.0); + } else if (m_audioPolicyController) { + m_audioPolicyController->setMasterVolume(static_cast(clamped) / 100.0); + } + setVolumeValue(clamped); +} + +void SystemControlStore::sleep() { + if (m_powerManager) { + m_powerManager->suspend(); + } +} + +void SystemControlStore::powerOff() { + if (m_powerManager) { + m_powerManager->shutdown(); + } +} + +void SystemControlStore::reboot() { + if (m_powerManager) { + m_powerManager->restart(); + } +} + +void SystemControlStore::setIsWifiOn(bool enabled) { + if (m_isWifiOn == enabled) { + return; + } + m_isWifiOn = enabled; + emit isWifiOnChanged(); +} + +void SystemControlStore::setIsBluetoothOn(bool enabled) { + if (m_isBluetoothOn == enabled) { + return; + } + m_isBluetoothOn = enabled; + emit isBluetoothOnChanged(); +} + +void SystemControlStore::setIsAirplaneModeOn(bool enabled) { + if (m_isAirplaneModeOn == enabled) { + return; + } + m_isAirplaneModeOn = enabled; + emit isAirplaneModeOnChanged(); +} + +void SystemControlStore::setIsRotationLocked(bool enabled) { + if (m_isRotationLocked == enabled) { + return; + } + m_isRotationLocked = enabled; + emit isRotationLockedChanged(); +} + +void SystemControlStore::setIsFlashlightOn(bool enabled) { + if (m_isFlashlightOn == enabled) { + return; + } + m_isFlashlightOn = enabled; + emit isFlashlightOnChanged(); +} + +void SystemControlStore::setIsCellularOn(bool enabled) { + if (m_isCellularOn == enabled) { + return; + } + m_isCellularOn = enabled; + emit isCellularOnChanged(); +} + +void SystemControlStore::setIsCellularDataOn(bool enabled) { + if (m_isCellularDataOn == enabled) { + return; + } + m_isCellularDataOn = enabled; + emit isCellularDataOnChanged(); +} + +void SystemControlStore::setIsDndMode(bool enabled) { + if (m_isDndMode == enabled) { + return; + } + m_isDndMode = enabled; + emit isDndModeChanged(); +} + +void SystemControlStore::setIsAlarmOn(bool enabled) { + if (m_isAlarmOn == enabled) { + return; + } + m_isAlarmOn = enabled; + emit isAlarmOnChanged(); +} + +void SystemControlStore::setIsAutoBrightnessOn(bool enabled) { + if (m_isAutoBrightnessOn == enabled) { + return; + } + m_isAutoBrightnessOn = enabled; + emit isAutoBrightnessOnChanged(); +} + +void SystemControlStore::setIsLocationOn(bool enabled) { + if (m_isLocationOn == enabled) { + return; + } + m_isLocationOn = enabled; + emit isLocationOnChanged(); +} + +void SystemControlStore::setIsHotspotOn(bool enabled) { + if (m_isHotspotOn == enabled) { + return; + } + m_isHotspotOn = enabled; + emit isHotspotOnChanged(); +} + +void SystemControlStore::setIsVibrationOn(bool enabled) { + if (m_isVibrationOn == enabled) { + return; + } + m_isVibrationOn = enabled; + emit isVibrationOnChanged(); +} + +void SystemControlStore::setIsNightLightOn(bool enabled) { + if (m_isNightLightOn == enabled) { + return; + } + m_isNightLightOn = enabled; + emit isNightLightOnChanged(); +} + +void SystemControlStore::setBrightnessValue(int value) { + if (m_brightness == value) { + return; + } + m_brightness = value; + emit brightnessChanged(); +} + +void SystemControlStore::setVolumeValue(int value) { + if (m_volume == value) { + return; + } + m_volume = value; + emit volumeChanged(); +} + +void SystemControlStore::setIsLowPowerMode(bool enabled) { + if (m_isLowPowerMode == enabled) { + return; + } + m_isLowPowerMode = enabled; + emit isLowPowerModeChanged(); +} + +void SystemControlStore::refreshAlarmState() { + if (!m_alarmManager) { + setIsAlarmOn(false); + return; + } + setIsAlarmOn(m_alarmManager->hasActiveAlarm() || m_alarmManager->hasEnabledAlarm()); +} diff --git a/shell/src/services/systemcontrolstore.h b/shell/src/services/systemcontrolstore.h new file mode 100644 index 000000000..98b86000b --- /dev/null +++ b/shell/src/services/systemcontrolstore.h @@ -0,0 +1,200 @@ +#pragma once + +#include +#include +#include + +class NetworkManagerCpp; +class BluetoothManager; +class DisplayManagerCpp; +class FlashlightManagerCpp; +class ModemManagerCpp; +class SettingsManager; +class AlarmManagerCpp; +class LocationManager; +class HapticManager; +class PowerManagerCpp; +class AudioManagerCpp; +class AudioPolicyController; +class ScreenshotServiceCpp; + +class SystemControlStore : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool isWifiOn READ isWifiOn NOTIFY isWifiOnChanged) + Q_PROPERTY(bool isBluetoothOn READ isBluetoothOn NOTIFY isBluetoothOnChanged) + Q_PROPERTY(bool isAirplaneModeOn READ isAirplaneModeOn NOTIFY isAirplaneModeOnChanged) + Q_PROPERTY(bool isRotationLocked READ isRotationLocked NOTIFY isRotationLockedChanged) + Q_PROPERTY(bool isFlashlightOn READ isFlashlightOn NOTIFY isFlashlightOnChanged) + Q_PROPERTY(bool isCellularOn READ isCellularOn NOTIFY isCellularOnChanged) + Q_PROPERTY(bool isCellularDataOn READ isCellularDataOn NOTIFY isCellularDataOnChanged) + Q_PROPERTY(bool isDndMode READ isDndMode NOTIFY isDndModeChanged) + Q_PROPERTY(bool isAlarmOn READ isAlarmOn NOTIFY isAlarmOnChanged) + Q_PROPERTY(bool isAutoBrightnessOn READ isAutoBrightnessOn NOTIFY isAutoBrightnessOnChanged) + Q_PROPERTY(bool isLocationOn READ isLocationOn NOTIFY isLocationOnChanged) + Q_PROPERTY(bool isHotspotOn READ isHotspotOn NOTIFY isHotspotOnChanged) + Q_PROPERTY(bool isVibrationOn READ isVibrationOn NOTIFY isVibrationOnChanged) + Q_PROPERTY(bool isNightLightOn READ isNightLightOn NOTIFY isNightLightOnChanged) + Q_PROPERTY(int brightness READ brightness NOTIFY brightnessChanged) + Q_PROPERTY(int volume READ volume NOTIFY volumeChanged) + Q_PROPERTY(bool isLowPowerMode READ isLowPowerMode NOTIFY isLowPowerModeChanged) + + public: + explicit SystemControlStore(NetworkManagerCpp *networkManager, + BluetoothManager *bluetoothManager, + DisplayManagerCpp *displayManager, + FlashlightManagerCpp *flashlightManager, + ModemManagerCpp *modemManager, SettingsManager *settingsManager, + AlarmManagerCpp *alarmManager, LocationManager *locationManager, + HapticManager *hapticManager, PowerManagerCpp *powerManager, + AudioManagerCpp *audioManager, + AudioPolicyController *audioPolicyController, + ScreenshotServiceCpp *screenshotService, QObject *parent = nullptr); + + bool isWifiOn() const { + return m_isWifiOn; + } + bool isBluetoothOn() const { + return m_isBluetoothOn; + } + bool isAirplaneModeOn() const { + return m_isAirplaneModeOn; + } + bool isRotationLocked() const { + return m_isRotationLocked; + } + bool isFlashlightOn() const { + return m_isFlashlightOn; + } + bool isCellularOn() const { + return m_isCellularOn; + } + bool isCellularDataOn() const { + return m_isCellularDataOn; + } + bool isDndMode() const { + return m_isDndMode; + } + bool isAlarmOn() const { + return m_isAlarmOn; + } + bool isAutoBrightnessOn() const { + return m_isAutoBrightnessOn; + } + bool isLocationOn() const { + return m_isLocationOn; + } + bool isHotspotOn() const { + return m_isHotspotOn; + } + bool isVibrationOn() const { + return m_isVibrationOn; + } + bool isNightLightOn() const { + return m_isNightLightOn; + } + int brightness() const { + return m_brightness; + } + int volume() const { + return m_volume; + } + bool isLowPowerMode() const { + return m_isLowPowerMode; + } + + Q_INVOKABLE void toggleWifi(); + Q_INVOKABLE void toggleBluetooth(); + Q_INVOKABLE void toggleAirplaneMode(); + Q_INVOKABLE void toggleRotationLock(); + Q_INVOKABLE void toggleFlashlight(); + Q_INVOKABLE void toggleCellular(); + Q_INVOKABLE void toggleCellularData(); + Q_INVOKABLE void toggleDndMode(); + Q_INVOKABLE void toggleAlarm(); + Q_INVOKABLE void toggleLowPowerMode(); + Q_INVOKABLE void toggleAutoBrightness(); + Q_INVOKABLE void toggleLocation(); + Q_INVOKABLE void toggleHotspot(); + Q_INVOKABLE void toggleVibration(); + Q_INVOKABLE void toggleNightLight(); + Q_INVOKABLE void captureScreenshot(); + Q_INVOKABLE void setBrightness(int value); + Q_INVOKABLE void setVolume(int value); + Q_INVOKABLE void sleep(); + Q_INVOKABLE void powerOff(); + Q_INVOKABLE void reboot(); + + signals: + void isWifiOnChanged(); + void isBluetoothOnChanged(); + void isAirplaneModeOnChanged(); + void isRotationLockedChanged(); + void isFlashlightOnChanged(); + void isCellularOnChanged(); + void isCellularDataOnChanged(); + void isDndModeChanged(); + void isAlarmOnChanged(); + void isAutoBrightnessOnChanged(); + void isLocationOnChanged(); + void isHotspotOnChanged(); + void isVibrationOnChanged(); + void isNightLightOnChanged(); + void brightnessChanged(); + void volumeChanged(); + void isLowPowerModeChanged(); + + private: + void setIsWifiOn(bool enabled); + void setIsBluetoothOn(bool enabled); + void setIsAirplaneModeOn(bool enabled); + void setIsRotationLocked(bool enabled); + void setIsFlashlightOn(bool enabled); + void setIsCellularOn(bool enabled); + void setIsCellularDataOn(bool enabled); + void setIsDndMode(bool enabled); + void setIsAlarmOn(bool enabled); + void setIsAutoBrightnessOn(bool enabled); + void setIsLocationOn(bool enabled); + void setIsHotspotOn(bool enabled); + void setIsVibrationOn(bool enabled); + void setIsNightLightOn(bool enabled); + void setBrightnessValue(int value); + void setVolumeValue(int value); + void setIsLowPowerMode(bool enabled); + void refreshAlarmState(); + + NetworkManagerCpp *m_networkManager; + BluetoothManager *m_bluetoothManager; + DisplayManagerCpp *m_displayManager; + FlashlightManagerCpp *m_flashlightManager; + ModemManagerCpp *m_modemManager; + SettingsManager *m_settingsManager; + AlarmManagerCpp *m_alarmManager; + LocationManager *m_locationManager; + HapticManager *m_hapticManager; + PowerManagerCpp *m_powerManager; + AudioManagerCpp *m_audioManager; + AudioPolicyController *m_audioPolicyController; + ScreenshotServiceCpp *m_screenshotService; + + bool m_isWifiOn = true; + bool m_isBluetoothOn = false; + bool m_isAirplaneModeOn = false; + bool m_isRotationLocked = false; + bool m_isFlashlightOn = false; + bool m_isCellularOn = false; + bool m_isCellularDataOn = false; + bool m_isDndMode = false; + bool m_isAlarmOn = false; + bool m_isAutoBrightnessOn = false; + bool m_isLocationOn = false; + bool m_isHotspotOn = false; + bool m_isVibrationOn = true; + bool m_isNightLightOn = false; + int m_brightness = 50; + int m_volume = 50; + bool m_isLowPowerMode = false; +}; diff --git a/shell/src/services/systemstatusstore.cpp b/shell/src/services/systemstatusstore.cpp new file mode 100644 index 000000000..c61ec4869 --- /dev/null +++ b/shell/src/services/systemstatusstore.cpp @@ -0,0 +1,387 @@ +#include "systemstatusstore.h" +#include "src/managers/bluetoothmanager.h" +#include "src/managers/modemmanagercpp.h" +#include "src/managers/networkmanagercpp.h" +#include "src/managers/powermanagercpp.h" +#include "src/managers/settingsmanager.h" +#include "src/services/notificationservicecpp.h" +#include +#include +#include + +SystemStatusStore::SystemStatusStore(PowerManagerCpp *powerManager, + NetworkManagerCpp *networkManager, + BluetoothManager *bluetoothManager, + ModemManagerCpp *modemManager, + NotificationServiceCpp *notificationService, + SettingsManager *settingsManager, QObject *parent) + : QObject(parent) + , m_powerManager(powerManager) + , m_networkManager(networkManager) + , m_bluetoothManager(bluetoothManager) + , m_modemManager(modemManager) + , m_notificationService(notificationService) + , m_settingsManager(settingsManager) { + if (m_powerManager) { + setBatteryLevel(m_powerManager->batteryLevel()); + setIsCharging(m_powerManager->isCharging()); + setIsPluggedIn(m_powerManager->isPluggedIn()); + updateChargingType(); + connect(m_powerManager, &PowerManagerCpp::batteryLevelChanged, this, + [this]() { setBatteryLevel(m_powerManager->batteryLevel()); }); + connect(m_powerManager, &PowerManagerCpp::isChargingChanged, this, [this]() { + setIsCharging(m_powerManager->isCharging()); + updateChargingType(); + }); + connect(m_powerManager, &PowerManagerCpp::isPluggedInChanged, this, + [this]() { setIsPluggedIn(m_powerManager->isPluggedIn()); }); + } + + if (m_networkManager) { + setIsWifiOn(m_networkManager->wifiEnabled()); + setWifiStrength(m_networkManager->wifiSignalStrength()); + setWifiNetwork(m_networkManager->wifiSsid()); + setWifiConnected(m_networkManager->wifiConnected()); + setEthernetConnected(m_networkManager->ethernetConnected()); + setIsAirplaneMode(m_networkManager->airplaneModeEnabled()); + connect(m_networkManager, &NetworkManagerCpp::wifiEnabledChanged, this, + [this]() { setIsWifiOn(m_networkManager->wifiEnabled()); }); + connect(m_networkManager, &NetworkManagerCpp::wifiSignalStrengthChanged, this, + [this]() { setWifiStrength(m_networkManager->wifiSignalStrength()); }); + connect(m_networkManager, &NetworkManagerCpp::wifiSsidChanged, this, + [this]() { setWifiNetwork(m_networkManager->wifiSsid()); }); + connect(m_networkManager, &NetworkManagerCpp::wifiConnectedChanged, this, + [this]() { setWifiConnected(m_networkManager->wifiConnected()); }); + connect(m_networkManager, &NetworkManagerCpp::ethernetConnectedChanged, this, + [this]() { setEthernetConnected(m_networkManager->ethernetConnected()); }); + connect(m_networkManager, &NetworkManagerCpp::airplaneModeEnabledChanged, this, + [this]() { setIsAirplaneMode(m_networkManager->airplaneModeEnabled()); }); + } + + if (m_bluetoothManager) { + setIsBluetoothOn(m_bluetoothManager->enabled()); + updateBluetoothDevices(); + connect(m_bluetoothManager, &BluetoothManager::enabledChanged, this, + [this]() { setIsBluetoothOn(m_bluetoothManager->enabled()); }); + connect(m_bluetoothManager, &BluetoothManager::pairedDevicesChanged, this, + [this]() { updateBluetoothDevices(); }); + } + + if (m_modemManager) { + setCellularStrength(m_modemManager->signalStrength()); + setCarrier(m_modemManager->operatorName()); + setDataType(m_modemManager->networkType()); + connect(m_modemManager, &ModemManagerCpp::signalStrengthChanged, this, + [this]() { setCellularStrength(m_modemManager->signalStrength()); }); + connect(m_modemManager, &ModemManagerCpp::operatorNameChanged, this, + [this]() { setCarrier(m_modemManager->operatorName()); }); + connect(m_modemManager, &ModemManagerCpp::networkTypeChanged, this, + [this]() { setDataType(m_modemManager->networkType()); }); + } + + if (m_notificationService) { + updateNotifications(); + setNotificationCount(m_notificationService->unreadCount()); + setIsDndMode(m_notificationService->isDndEnabled()); + connect(m_notificationService, &NotificationServiceCpp::notificationsChanged, this, + [this]() { updateNotifications(); }); + connect(m_notificationService, &NotificationServiceCpp::unreadCountChanged, this, + [this]() { setNotificationCount(m_notificationService->unreadCount()); }); + connect(m_notificationService, &NotificationServiceCpp::isDndEnabledChanged, this, + [this]() { setIsDndMode(m_notificationService->isDndEnabled()); }); + } + + if (m_settingsManager) { + connect(m_settingsManager, &SettingsManager::timeFormatChanged, this, + [this]() { updateTime(); }); + } + + updateTime(); + m_timeTimer.setInterval(1000); + m_timeTimer.setSingleShot(false); + connect(&m_timeTimer, &QTimer::timeout, this, &SystemStatusStore::updateTime); + m_timeTimer.start(); +} + +void SystemStatusStore::updateTime() { + setCurrentTime(QDateTime::currentDateTime()); + QString timeFormat = "24h"; + if (m_settingsManager) { + timeFormat = m_settingsManager->timeFormat(); + } + const QString timePattern = (timeFormat == "24h") ? "HH:mm" : "h:mm AP"; + setTimeString(QLocale::system().toString(m_currentTime.time(), timePattern)); + setDateString(QLocale::system().toString(m_currentTime.date(), "dddd, MMMM d")); +} + +void SystemStatusStore::addNotification(const QString &title, const QString &message, + const QString &app) { + if (m_notificationService) { + QVariantMap payload; + payload["category"] = "message"; + payload["priority"] = "normal"; + m_notificationService->sendNotification(app, title, message, payload); + } +} + +void SystemStatusStore::removeNotification(int notificationId) { + if (m_notificationService) { + m_notificationService->dismissNotification(notificationId); + } +} + +void SystemStatusStore::clearAllNotifications() { + if (m_notificationService) { + m_notificationService->dismissAllNotifications(); + } +} + +void SystemStatusStore::updateSystemInfo() { + setCpuUsage(QRandomGenerator::global()->bounded(10, 41)); + setMemoryUsage(QRandomGenerator::global()->bounded(35, 56)); +} + +void SystemStatusStore::updateChargingType() { + setChargingType(m_isCharging ? "usb" : "none"); +} + +void SystemStatusStore::updateBluetoothDevices() { + QVariantList devices; + int connectedCount = 0; + if (m_bluetoothManager) { + const QList paired = m_bluetoothManager->pairedDevices(); + for (QObject *device : paired) { + devices.append(QVariant::fromValue(device)); + if (device && device->property("connected").toBool()) { + connectedCount++; + } + } + } + setBluetoothDevices(devices); + setBluetoothConnectedDevicesCount(connectedCount); + setIsBluetoothConnected(connectedCount > 0); +} + +void SystemStatusStore::updateNotifications() { + if (!m_notificationService) { + return; + } + setNotifications(m_notificationService->notifications()); + setNotificationCount(m_notificationService->unreadCount()); +} + +void SystemStatusStore::setBatteryLevel(int level) { + if (m_batteryLevel == level) { + return; + } + m_batteryLevel = level; + emit batteryLevelChanged(); +} + +void SystemStatusStore::setIsCharging(bool charging) { + if (m_isCharging == charging) { + return; + } + m_isCharging = charging; + emit isChargingChanged(); +} + +void SystemStatusStore::setIsPluggedIn(bool pluggedIn) { + if (m_isPluggedIn == pluggedIn) { + return; + } + m_isPluggedIn = pluggedIn; + emit isPluggedInChanged(); +} + +void SystemStatusStore::setChargingType(const QString &type) { + if (m_chargingType == type) { + return; + } + m_chargingType = type; + emit chargingTypeChanged(); +} + +void SystemStatusStore::setIsWifiOn(bool enabled) { + if (m_isWifiOn == enabled) { + return; + } + m_isWifiOn = enabled; + emit isWifiOnChanged(); +} + +void SystemStatusStore::setWifiStrength(int strength) { + if (m_wifiStrength == strength) { + return; + } + m_wifiStrength = strength; + emit wifiStrengthChanged(); +} + +void SystemStatusStore::setWifiNetwork(const QString &network) { + if (m_wifiNetwork == network) { + return; + } + m_wifiNetwork = network; + emit wifiNetworkChanged(); +} + +void SystemStatusStore::setWifiConnected(bool connected) { + if (m_wifiConnected == connected) { + return; + } + m_wifiConnected = connected; + emit wifiConnectedChanged(); +} + +void SystemStatusStore::setEthernetConnected(bool connected) { + if (m_ethernetConnected == connected) { + return; + } + m_ethernetConnected = connected; + emit ethernetConnectedChanged(); +} + +void SystemStatusStore::setIsBluetoothOn(bool enabled) { + if (m_isBluetoothOn == enabled) { + return; + } + m_isBluetoothOn = enabled; + emit isBluetoothOnChanged(); +} + +void SystemStatusStore::setBluetoothDevices(const QVariantList &devices) { + if (m_bluetoothDevices == devices) { + return; + } + m_bluetoothDevices = devices; + emit bluetoothDevicesChanged(); +} + +void SystemStatusStore::setBluetoothConnectedDevicesCount(int count) { + if (m_bluetoothConnectedDevicesCount == count) { + return; + } + m_bluetoothConnectedDevicesCount = count; + emit bluetoothConnectedDevicesCountChanged(); +} + +void SystemStatusStore::setIsBluetoothConnected(bool connected) { + if (m_isBluetoothConnected == connected) { + return; + } + m_isBluetoothConnected = connected; + emit isBluetoothConnectedChanged(); +} + +void SystemStatusStore::setIsAirplaneMode(bool enabled) { + if (m_isAirplaneMode == enabled) { + return; + } + m_isAirplaneMode = enabled; + emit isAirplaneModeChanged(); +} + +void SystemStatusStore::setIsDndMode(bool enabled) { + if (m_isDndMode == enabled) { + return; + } + m_isDndMode = enabled; + emit isDndModeChanged(); +} + +void SystemStatusStore::setCellularStrength(int strength) { + if (m_cellularStrength == strength) { + return; + } + m_cellularStrength = strength; + emit cellularStrengthChanged(); +} + +void SystemStatusStore::setCarrier(const QString &carrier) { + if (m_carrier == carrier) { + return; + } + m_carrier = carrier; + emit carrierChanged(); +} + +void SystemStatusStore::setDataType(const QString &dataType) { + if (m_dataType == dataType) { + return; + } + m_dataType = dataType; + emit dataTypeChanged(); +} + +void SystemStatusStore::setCpuUsage(int usage) { + if (m_cpuUsage == usage) { + return; + } + m_cpuUsage = usage; + emit cpuUsageChanged(); +} + +void SystemStatusStore::setMemoryUsage(int usage) { + if (m_memoryUsage == usage) { + return; + } + m_memoryUsage = usage; + emit memoryUsageChanged(); +} + +void SystemStatusStore::setStorageUsed(double used) { + if (qFuzzyCompare(m_storageUsed, used)) { + return; + } + m_storageUsed = used; + emit storageUsedChanged(); +} + +void SystemStatusStore::setStorageTotal(double total) { + if (qFuzzyCompare(m_storageTotal, total)) { + return; + } + m_storageTotal = total; + emit storageTotalChanged(); +} + +void SystemStatusStore::setCurrentTime(const QDateTime &time) { + if (m_currentTime == time) { + return; + } + m_currentTime = time; + emit currentTimeChanged(); +} + +void SystemStatusStore::setTimeString(const QString &timeString) { + if (m_timeString == timeString) { + return; + } + m_timeString = timeString; + emit timeStringChanged(); +} + +void SystemStatusStore::setDateString(const QString &dateString) { + if (m_dateString == dateString) { + return; + } + m_dateString = dateString; + emit dateStringChanged(); +} + +void SystemStatusStore::setNotifications(const QVariantList ¬ifications) { + if (m_notifications == notifications) { + return; + } + m_notifications = notifications; + emit notificationsChanged(); +} + +void SystemStatusStore::setNotificationCount(int count) { + if (m_notificationCount == count) { + return; + } + m_notificationCount = count; + emit notificationCountChanged(); +} diff --git a/shell/src/services/systemstatusstore.h b/shell/src/services/systemstatusstore.h new file mode 100644 index 000000000..5a7abeddb --- /dev/null +++ b/shell/src/services/systemstatusstore.h @@ -0,0 +1,273 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class PowerManagerCpp; +class NetworkManagerCpp; +class BluetoothManager; +class ModemManagerCpp; +class NotificationServiceCpp; +class SettingsManager; + +class SystemStatusStore : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(int batteryLevel READ batteryLevel NOTIFY batteryLevelChanged) + Q_PROPERTY(bool isCharging READ isCharging NOTIFY isChargingChanged) + Q_PROPERTY(bool isPluggedIn READ isPluggedIn NOTIFY isPluggedInChanged) + Q_PROPERTY(QString chargingType READ chargingType NOTIFY chargingTypeChanged) + + Q_PROPERTY(bool isWifiOn READ isWifiOn NOTIFY isWifiOnChanged) + Q_PROPERTY(int wifiStrength READ wifiStrength NOTIFY wifiStrengthChanged) + Q_PROPERTY(QString wifiNetwork READ wifiNetwork NOTIFY wifiNetworkChanged) + Q_PROPERTY(bool wifiConnected READ wifiConnected NOTIFY wifiConnectedChanged) + Q_PROPERTY(bool ethernetConnected READ ethernetConnected NOTIFY ethernetConnectedChanged) + + Q_PROPERTY(bool isBluetoothOn READ isBluetoothOn NOTIFY isBluetoothOnChanged) + Q_PROPERTY(QVariantList bluetoothDevices READ bluetoothDevices NOTIFY bluetoothDevicesChanged) + Q_PROPERTY(int bluetoothConnectedDevicesCount READ bluetoothConnectedDevicesCount NOTIFY + bluetoothConnectedDevicesCountChanged) + Q_PROPERTY( + bool isBluetoothConnected READ isBluetoothConnected NOTIFY isBluetoothConnectedChanged) + + Q_PROPERTY(bool isAirplaneMode READ isAirplaneMode NOTIFY isAirplaneModeChanged) + Q_PROPERTY(bool isDndMode READ isDndMode NOTIFY isDndModeChanged) + + Q_PROPERTY(int cellularStrength READ cellularStrength NOTIFY cellularStrengthChanged) + Q_PROPERTY(QString carrier READ carrier NOTIFY carrierChanged) + Q_PROPERTY(QString dataType READ dataType NOTIFY dataTypeChanged) + + Q_PROPERTY(int cpuUsage READ cpuUsage NOTIFY cpuUsageChanged) + Q_PROPERTY(int memoryUsage READ memoryUsage NOTIFY memoryUsageChanged) + Q_PROPERTY(double storageUsed READ storageUsed NOTIFY storageUsedChanged) + Q_PROPERTY(double storageTotal READ storageTotal NOTIFY storageTotalChanged) + + Q_PROPERTY(QDateTime currentTime READ currentTime NOTIFY currentTimeChanged) + Q_PROPERTY(QString timeString READ timeString NOTIFY timeStringChanged) + Q_PROPERTY(QString dateString READ dateString NOTIFY dateStringChanged) + + Q_PROPERTY(QVariantList notifications READ notifications NOTIFY notificationsChanged) + Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged) + + public: + explicit SystemStatusStore(PowerManagerCpp *powerManager, NetworkManagerCpp *networkManager, + BluetoothManager *bluetoothManager, ModemManagerCpp *modemManager, + NotificationServiceCpp *notificationService, + SettingsManager *settingsManager, QObject *parent = nullptr); + + int batteryLevel() const { + return m_batteryLevel; + } + bool isCharging() const { + return m_isCharging; + } + bool isPluggedIn() const { + return m_isPluggedIn; + } + QString chargingType() const { + return m_chargingType; + } + + bool isWifiOn() const { + return m_isWifiOn; + } + int wifiStrength() const { + return m_wifiStrength; + } + QString wifiNetwork() const { + return m_wifiNetwork; + } + bool wifiConnected() const { + return m_wifiConnected; + } + bool ethernetConnected() const { + return m_ethernetConnected; + } + + bool isBluetoothOn() const { + return m_isBluetoothOn; + } + QVariantList bluetoothDevices() const { + return m_bluetoothDevices; + } + int bluetoothConnectedDevicesCount() const { + return m_bluetoothConnectedDevicesCount; + } + bool isBluetoothConnected() const { + return m_isBluetoothConnected; + } + + bool isAirplaneMode() const { + return m_isAirplaneMode; + } + bool isDndMode() const { + return m_isDndMode; + } + + int cellularStrength() const { + return m_cellularStrength; + } + QString carrier() const { + return m_carrier; + } + QString dataType() const { + return m_dataType; + } + + int cpuUsage() const { + return m_cpuUsage; + } + int memoryUsage() const { + return m_memoryUsage; + } + double storageUsed() const { + return m_storageUsed; + } + double storageTotal() const { + return m_storageTotal; + } + + QDateTime currentTime() const { + return m_currentTime; + } + QString timeString() const { + return m_timeString; + } + QString dateString() const { + return m_dateString; + } + + QVariantList notifications() const { + return m_notifications; + } + int notificationCount() const { + return m_notificationCount; + } + + Q_INVOKABLE void updateTime(); + Q_INVOKABLE void addNotification(const QString &title, const QString &message, + const QString &app); + Q_INVOKABLE void removeNotification(int notificationId); + Q_INVOKABLE void clearAllNotifications(); + Q_INVOKABLE void updateSystemInfo(); + + signals: + void batteryLevelChanged(); + void isChargingChanged(); + void isPluggedInChanged(); + void chargingTypeChanged(); + + void isWifiOnChanged(); + void wifiStrengthChanged(); + void wifiNetworkChanged(); + void wifiConnectedChanged(); + void ethernetConnectedChanged(); + + void isBluetoothOnChanged(); + void bluetoothDevicesChanged(); + void bluetoothConnectedDevicesCountChanged(); + void isBluetoothConnectedChanged(); + + void isAirplaneModeChanged(); + void isDndModeChanged(); + + void cellularStrengthChanged(); + void carrierChanged(); + void dataTypeChanged(); + + void cpuUsageChanged(); + void memoryUsageChanged(); + void storageUsedChanged(); + void storageTotalChanged(); + + void currentTimeChanged(); + void timeStringChanged(); + void dateStringChanged(); + + void notificationsChanged(); + void notificationCountChanged(); + + private: + void updateChargingType(); + void updateBluetoothDevices(); + void updateNotifications(); + + void setBatteryLevel(int level); + void setIsCharging(bool charging); + void setIsPluggedIn(bool pluggedIn); + void setChargingType(const QString &type); + void setIsWifiOn(bool enabled); + void setWifiStrength(int strength); + void setWifiNetwork(const QString &network); + void setWifiConnected(bool connected); + void setEthernetConnected(bool connected); + void setIsBluetoothOn(bool enabled); + void setBluetoothDevices(const QVariantList &devices); + void setBluetoothConnectedDevicesCount(int count); + void setIsBluetoothConnected(bool connected); + void setIsAirplaneMode(bool enabled); + void setIsDndMode(bool enabled); + void setCellularStrength(int strength); + void setCarrier(const QString &carrier); + void setDataType(const QString &dataType); + void setCpuUsage(int usage); + void setMemoryUsage(int usage); + void setStorageUsed(double used); + void setStorageTotal(double total); + void setCurrentTime(const QDateTime &time); + void setTimeString(const QString &timeString); + void setDateString(const QString &dateString); + void setNotifications(const QVariantList ¬ifications); + void setNotificationCount(int count); + + PowerManagerCpp *m_powerManager; + NetworkManagerCpp *m_networkManager; + BluetoothManager *m_bluetoothManager; + ModemManagerCpp *m_modemManager; + NotificationServiceCpp *m_notificationService; + SettingsManager *m_settingsManager; + + int m_batteryLevel = 0; + bool m_isCharging = false; + bool m_isPluggedIn = false; + QString m_chargingType = "none"; + + bool m_isWifiOn = true; + int m_wifiStrength = 0; + QString m_wifiNetwork; + bool m_wifiConnected = false; + bool m_ethernetConnected = false; + + bool m_isBluetoothOn = false; + QVariantList m_bluetoothDevices; + int m_bluetoothConnectedDevicesCount = 0; + bool m_isBluetoothConnected = false; + + bool m_isAirplaneMode = false; + bool m_isDndMode = false; + + int m_cellularStrength = 0; + QString m_carrier; + QString m_dataType; + + int m_cpuUsage = 23; + int m_memoryUsage = 45; + double m_storageUsed = 45.2; + double m_storageTotal = 128; + + QDateTime m_currentTime = QDateTime::currentDateTime(); + QString m_timeString; + QString m_dateString; + + QVariantList m_notifications; + int m_notificationCount = 0; + + QTimer m_timeTimer; +}; diff --git a/shell/src/services/uistore.cpp b/shell/src/services/uistore.cpp new file mode 100644 index 000000000..d32457c63 --- /dev/null +++ b/shell/src/services/uistore.cpp @@ -0,0 +1,222 @@ +#include "uistore.h" +#include + +UIStore::UIStore(QObject *parent) + : QObject(parent) {} + +void UIStore::setShellRef(QObject *shellRef) { + if (m_shellRef == shellRef) { + return; + } + m_shellRef = shellRef; + emit shellRefChanged(); +} + +void UIStore::openSearch() { + setSearchOpen(true); +} + +void UIStore::closeSearch() { + setSearchOpen(false); +} + +void UIStore::toggleSearch() { + if (m_searchOpen) { + closeSearch(); + } else { + openSearch(); + } +} + +void UIStore::openShareSheet(const QVariant &content, const QString &contentType) { + emit showShareSheet(content, contentType.isEmpty() ? QStringLiteral("text") : contentType); + setShareSheetOpen(true); +} + +void UIStore::closeShareSheet() { + setShareSheetOpen(false); +} + +void UIStore::openClipboardManager() { + setClipboardManagerOpen(true); +} + +void UIStore::closeClipboardManager() { + setClipboardManagerOpen(false); +} + +void UIStore::openQuickSettings() { + setQuickSettingsOpen(true); + if (m_shellRef) { + QVariant heightValue = m_shellRef->property("maxQuickSettingsHeight"); + if (heightValue.isValid()) { + setQuickSettingsHeight(heightValue.toDouble()); + return; + } + } + setQuickSettingsHeight(1000.0); +} + +void UIStore::closeQuickSettings() { + setQuickSettingsOpen(false); + setQuickSettingsHeight(0.0); +} + +void UIStore::toggleQuickSettings() { + if (m_quickSettingsOpen) { + closeQuickSettings(); + } else { + openQuickSettings(); + } +} + +void UIStore::openApp(const QString &appId, const QString &appName, const QString &appIcon) { + setCurrentAppId(appId); + setCurrentAppName(appName); + setCurrentAppIcon(appIcon); + setAppWindowOpen(true); + if (appId == QStringLiteral("settings")) { + setSettingsOpen(true); + } +} + +void UIStore::openApp(const QVariant &appId, const QVariant &appName, const QVariant &appIcon) { + openApp(appId.toString(), appName.toString(), appIcon.toString()); +} + +void UIStore::closeApp() { + setAppWindowOpen(false); + if (m_currentAppId == QStringLiteral("settings")) { + setSettingsOpen(false); + } + setCurrentAppId(QString()); + setCurrentAppName(QString()); + setCurrentAppIcon(QString()); +} + +void UIStore::minimizeApp() { + closeApp(); +} + +void UIStore::restoreApp(const QString &appId, const QString &appName, const QString &appIcon) { + setAppWindowOpen(true); + setCurrentAppName(appName); + setCurrentAppIcon(appIcon); + setCurrentAppId(appId); + if (appId == QStringLiteral("settings")) { + setSettingsOpen(true); + } +} + +void UIStore::restoreApp(const QVariant &appId, const QVariant &appName, const QVariant &appIcon) { + restoreApp(appId.toString(), appName.toString(), appIcon.toString()); +} + +void UIStore::openSettings() { + setSettingsOpen(true); +} + +void UIStore::closeSettings() { + setSettingsOpen(false); +} + +void UIStore::minimizeSettings() { + minimizeApp(); +} + +void UIStore::closeAll() { + closeQuickSettings(); + closeApp(); + closeSettings(); + closeSearch(); + closeShareSheet(); + closeClipboardManager(); +} + +void UIStore::setQuickSettingsOpen(bool open) { + if (m_quickSettingsOpen == open) { + return; + } + m_quickSettingsOpen = open; + emit quickSettingsOpenChanged(); +} + +void UIStore::setQuickSettingsHeight(double height) { + if (qFuzzyCompare(m_quickSettingsHeight, height)) { + return; + } + m_quickSettingsHeight = height; + emit quickSettingsHeightChanged(); +} + +void UIStore::setQuickSettingsDragging(bool dragging) { + if (m_quickSettingsDragging == dragging) { + return; + } + m_quickSettingsDragging = dragging; + emit quickSettingsDraggingChanged(); +} + +void UIStore::setAppWindowOpen(bool open) { + if (m_appWindowOpen == open) { + return; + } + m_appWindowOpen = open; + emit appWindowOpenChanged(); +} + +void UIStore::setCurrentAppId(const QString &appId) { + if (m_currentAppId == appId) { + return; + } + m_currentAppId = appId; + emit currentAppIdChanged(); +} + +void UIStore::setCurrentAppName(const QString &appName) { + if (m_currentAppName == appName) { + return; + } + m_currentAppName = appName; + emit currentAppNameChanged(); +} + +void UIStore::setCurrentAppIcon(const QString &appIcon) { + if (m_currentAppIcon == appIcon) { + return; + } + m_currentAppIcon = appIcon; + emit currentAppIconChanged(); +} + +void UIStore::setSettingsOpen(bool open) { + if (m_settingsOpen == open) { + return; + } + m_settingsOpen = open; + emit settingsOpenChanged(); +} + +void UIStore::setSearchOpen(bool open) { + if (m_searchOpen == open) { + return; + } + m_searchOpen = open; + emit searchOpenChanged(); +} + +void UIStore::setShareSheetOpen(bool open) { + if (m_shareSheetOpen == open) { + return; + } + m_shareSheetOpen = open; + emit shareSheetOpenChanged(); +} + +void UIStore::setClipboardManagerOpen(bool open) { + if (m_clipboardManagerOpen == open) { + return; + } + m_clipboardManagerOpen = open; + emit clipboardManagerOpenChanged(); +} diff --git a/shell/src/services/uistore.h b/shell/src/services/uistore.h new file mode 100644 index 000000000..e50927115 --- /dev/null +++ b/shell/src/services/uistore.h @@ -0,0 +1,138 @@ +#pragma once + +#include +#include +#include +#include + +class UIStore : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool quickSettingsOpen READ quickSettingsOpen NOTIFY quickSettingsOpenChanged) + Q_PROPERTY(double quickSettingsHeight READ quickSettingsHeight WRITE setQuickSettingsHeight + NOTIFY quickSettingsHeightChanged) + Q_PROPERTY(bool quickSettingsDragging READ quickSettingsDragging WRITE setQuickSettingsDragging + NOTIFY quickSettingsDraggingChanged) + Q_PROPERTY(bool appWindowOpen READ appWindowOpen NOTIFY appWindowOpenChanged) + Q_PROPERTY(QString currentAppId READ currentAppId NOTIFY currentAppIdChanged) + Q_PROPERTY(QString currentAppName READ currentAppName NOTIFY currentAppNameChanged) + Q_PROPERTY(QString currentAppIcon READ currentAppIcon NOTIFY currentAppIconChanged) + Q_PROPERTY(bool settingsOpen READ settingsOpen NOTIFY settingsOpenChanged) + Q_PROPERTY(bool searchOpen READ searchOpen NOTIFY searchOpenChanged) + Q_PROPERTY(bool shareSheetOpen READ shareSheetOpen NOTIFY shareSheetOpenChanged) + Q_PROPERTY( + bool clipboardManagerOpen READ clipboardManagerOpen NOTIFY clipboardManagerOpenChanged) + Q_PROPERTY(QObject *shellRef READ shellRef WRITE setShellRef NOTIFY shellRefChanged) + + public: + explicit UIStore(QObject *parent = nullptr); + + bool quickSettingsOpen() const { + return m_quickSettingsOpen; + } + double quickSettingsHeight() const { + return m_quickSettingsHeight; + } + bool quickSettingsDragging() const { + return m_quickSettingsDragging; + } + bool appWindowOpen() const { + return m_appWindowOpen; + } + QString currentAppId() const { + return m_currentAppId; + } + QString currentAppName() const { + return m_currentAppName; + } + QString currentAppIcon() const { + return m_currentAppIcon; + } + bool settingsOpen() const { + return m_settingsOpen; + } + bool searchOpen() const { + return m_searchOpen; + } + bool shareSheetOpen() const { + return m_shareSheetOpen; + } + bool clipboardManagerOpen() const { + return m_clipboardManagerOpen; + } + QObject *shellRef() const { + return m_shellRef; + } + void setShellRef(QObject *shellRef); + + Q_INVOKABLE void openSearch(); + Q_INVOKABLE void closeSearch(); + Q_INVOKABLE void toggleSearch(); + Q_INVOKABLE void openShareSheet(const QVariant &content, const QString &contentType); + Q_INVOKABLE void closeShareSheet(); + Q_INVOKABLE void openClipboardManager(); + Q_INVOKABLE void closeClipboardManager(); + Q_INVOKABLE void openQuickSettings(); + Q_INVOKABLE void closeQuickSettings(); + Q_INVOKABLE void toggleQuickSettings(); + Q_INVOKABLE void openApp(const QString &appId, const QString &appName, const QString &appIcon); + Q_INVOKABLE void openApp(const QVariant &appId, const QVariant &appName, + const QVariant &appIcon); + Q_INVOKABLE void closeApp(); + Q_INVOKABLE void minimizeApp(); + Q_INVOKABLE void restoreApp(const QString &appId, const QString &appName, + const QString &appIcon); + Q_INVOKABLE void restoreApp(const QVariant &appId, const QVariant &appName, + const QVariant &appIcon); + Q_INVOKABLE void openSettings(); + Q_INVOKABLE void closeSettings(); + Q_INVOKABLE void minimizeSettings(); + Q_INVOKABLE void closeAll(); + + signals: + void quickSettingsOpenChanged(); + void quickSettingsHeightChanged(); + void quickSettingsDraggingChanged(); + void appWindowOpenChanged(); + void currentAppIdChanged(); + void currentAppNameChanged(); + void currentAppIconChanged(); + void settingsOpenChanged(); + void searchOpenChanged(); + void shareSheetOpenChanged(); + void clipboardManagerOpenChanged(); + void shellRefChanged(); + + void showNotificationToast(const QVariant ¬ification); + void showSystemHUD(const QString &type, double value); + void showConfirmDialog(const QString &title, const QString &message, const QVariant &onConfirm); + void showShareSheet(const QVariant &content, const QString &contentType); + + private: + void setQuickSettingsOpen(bool open); + void setQuickSettingsHeight(double height); + void setQuickSettingsDragging(bool dragging); + void setAppWindowOpen(bool open); + void setCurrentAppId(const QString &appId); + void setCurrentAppName(const QString &appName); + void setCurrentAppIcon(const QString &appIcon); + void setSettingsOpen(bool open); + void setSearchOpen(bool open); + void setShareSheetOpen(bool open); + void setClipboardManagerOpen(bool open); + + bool m_quickSettingsOpen = false; + double m_quickSettingsHeight = 0.0; + bool m_quickSettingsDragging = false; + bool m_appWindowOpen = false; + QString m_currentAppId; + QString m_currentAppName; + QString m_currentAppIcon; + bool m_settingsOpen = false; + bool m_searchOpen = false; + bool m_shareSheetOpen = false; + bool m_clipboardManagerOpen = false; + QObject *m_shellRef = nullptr; +}; diff --git a/shell/src/services/wallpaperstore.cpp b/shell/src/services/wallpaperstore.cpp new file mode 100644 index 000000000..0a6ca4cf1 --- /dev/null +++ b/shell/src/services/wallpaperstore.cpp @@ -0,0 +1,129 @@ +#include "wallpaperstore.h" +#include "src/managers/settingsmanager.h" +#include +#include + +WallpaperStore::WallpaperStore(SettingsManager *settingsManager, QObject *parent) + : QObject(parent) + , m_settingsManager(settingsManager) { + m_wallpapers = { + QVariantMap{ + {"name", "Gradient 1"}, + {"path", resolveAssetPath("wallpapers/wallpaper.jpg", "qrc:/wallpapers/wallpaper.jpg")}, + {"isDark", true}}, + QVariantMap{ + {"name", "Gradient 2"}, + {"path", + resolveAssetPath("wallpapers/wallpaper2.jpg", "qrc:/wallpapers/wallpaper2.jpg")}, + {"isDark", true}}, + QVariantMap{ + {"name", "Gradient 3"}, + {"path", + resolveAssetPath("wallpapers/wallpaper3.jpg", "qrc:/wallpapers/wallpaper3.jpg")}, + {"isDark", true}}, + QVariantMap{ + {"name", "Gradient 4"}, + {"path", + resolveAssetPath("wallpapers/wallpaper4.jpg", "qrc:/wallpapers/wallpaper4.jpg")}, + {"isDark", false}}, + QVariantMap{ + {"name", "Gradient 5"}, + {"path", + resolveAssetPath("wallpapers/wallpaper5.jpg", "qrc:/wallpapers/wallpaper5.jpg")}, + {"isDark", true}}, + QVariantMap{ + {"name", "Gradient 6"}, + {"path", + resolveAssetPath("wallpapers/wallpaper6.jpg", "qrc:/wallpapers/wallpaper6.jpg")}, + {"isDark", false}}, + QVariantMap{ + {"name", "Gradient 7"}, + {"path", + resolveAssetPath("wallpapers/wallpaper7.jpg", "qrc:/wallpapers/wallpaper7.jpg")}, + {"isDark", true}}, + QVariantMap{ + {"name", "Gradient 8"}, + {"path", + resolveAssetPath("wallpapers/wallpaper8.jpg", "qrc:/wallpapers/wallpaper8.jpg")}, + {"isDark", true}}}; + emit wallpapersChanged(); + + if (m_settingsManager && !m_settingsManager->wallpaperPath().isEmpty()) { + setCurrentWallpaper(m_settingsManager->wallpaperPath()); + } else { + refreshIsDark(); + } + + if (m_settingsManager) { + connect(m_settingsManager, &SettingsManager::wallpaperPathChanged, this, [this]() { + if (!m_settingsManager) { + return; + } + const QString newPath = m_settingsManager->wallpaperPath(); + if (!newPath.isEmpty() && newPath != m_currentWallpaper) { + setCurrentWallpaper(newPath); + } + }); + } +} + +void WallpaperStore::setWallpaper(const QString &newPath, bool newIsDark) { + if (newPath.isEmpty()) { + if (newIsDark != m_isDark) { + setIsDark(newIsDark); + } + return; + } + if (newPath == m_currentWallpaper) { + if (newIsDark != m_isDark) { + setIsDark(newIsDark); + } + return; + } + setCurrentWallpaper(newPath); + if (newIsDark != m_isDark) { + setIsDark(newIsDark); + } + if (m_settingsManager && m_settingsManager->wallpaperPath() != newPath) { + m_settingsManager->setWallpaperPath(newPath); + } +} + +QString WallpaperStore::resolveAssetPath(const QString &relativePath, + const QString &fallback) const { + if (m_settingsManager) { + const QString assetUrl = m_settingsManager->assetUrl(relativePath); + if (!assetUrl.isEmpty()) { + return assetUrl; + } + } + return fallback; +} + +void WallpaperStore::setCurrentWallpaper(const QString &path) { + if (m_currentWallpaper == path) { + return; + } + m_currentWallpaper = path; + emit currentWallpaperChanged(); + emit pathChanged(); + refreshIsDark(); +} + +void WallpaperStore::setIsDark(bool isDark) { + if (m_isDark == isDark) { + return; + } + m_isDark = isDark; + emit isDarkChanged(); +} + +void WallpaperStore::refreshIsDark() { + for (const QVariant &entry : m_wallpapers) { + const QVariantMap map = entry.toMap(); + if (map.value("path").toString() == m_currentWallpaper) { + setIsDark(map.value("isDark").toBool()); + return; + } + } +} diff --git a/shell/src/services/wallpaperstore.h b/shell/src/services/wallpaperstore.h new file mode 100644 index 000000000..4576961b2 --- /dev/null +++ b/shell/src/services/wallpaperstore.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +class SettingsManager; + +class WallpaperStore : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString currentWallpaper READ currentWallpaper NOTIFY currentWallpaperChanged) + Q_PROPERTY(QString path READ path NOTIFY pathChanged) + Q_PROPERTY(bool isDark READ isDark NOTIFY isDarkChanged) + Q_PROPERTY(QVariantList wallpapers READ wallpapers NOTIFY wallpapersChanged) + + public: + explicit WallpaperStore(SettingsManager *settingsManager, QObject *parent = nullptr); + + QString currentWallpaper() const { + return m_currentWallpaper; + } + QString path() const { + return m_currentWallpaper; + } + bool isDark() const { + return m_isDark; + } + QVariantList wallpapers() const { + return m_wallpapers; + } + + Q_INVOKABLE void setWallpaper(const QString &newPath, bool newIsDark); + + signals: + void currentWallpaperChanged(); + void pathChanged(); + void isDarkChanged(); + void wallpapersChanged(); + + private: + QString resolveAssetPath(const QString &relativePath, const QString &fallback) const; + void setCurrentWallpaper(const QString &path); + void setIsDark(bool isDark); + void refreshIsDark(); + + SettingsManager *m_settingsManager; + QString m_currentWallpaper = QStringLiteral("qrc:/wallpapers/wallpaper.jpg"); + bool m_isDark = true; + QVariantList m_wallpapers; +}; From 516e7465ccd8a08521a2bceccf3c19288fed5934 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 22:30:22 +0000 Subject: [PATCH 16/28] fix(shell): wire QML to C++ stores Import the shell module where needed and route app restore through UIStore to keep app window state consistent. --- shell/qml/components/AppContextMenu.qml | 1 + shell/qml/components/ClipboardManager.qml | 1 + shell/qml/components/ConnectionToast.qml | 1 + shell/qml/components/MarathonAppGrid.qml | 1 + shell/qml/components/MarathonAppWindow.qml | 9 +++------ shell/qml/components/MarathonBottomBar.qml | 1 + shell/qml/components/MarathonNavBar.qml | 1 + shell/qml/components/MarathonOOBE.qml | 1 + shell/qml/components/MarathonTaskSwitcher.qml | 1 + shell/qml/components/PermissionDialog.qml | 1 + shell/qml/components/ShareSheet.qml | 1 + shell/qml/components/ShellInitialization.qml | 1 + shell/qml/components/TaskCard.qml | 1 + 13 files changed, 15 insertions(+), 6 deletions(-) diff --git a/shell/qml/components/AppContextMenu.qml b/shell/qml/components/AppContextMenu.qml index 01714515c..31346b195 100644 --- a/shell/qml/components/AppContextMenu.qml +++ b/shell/qml/components/AppContextMenu.qml @@ -2,6 +2,7 @@ import MarathonUI.Containers import MarathonUI.Controls import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Item { diff --git a/shell/qml/components/ClipboardManager.qml b/shell/qml/components/ClipboardManager.qml index 8c2f44aab..0d062949f 100644 --- a/shell/qml/components/ClipboardManager.qml +++ b/shell/qml/components/ClipboardManager.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Rectangle { diff --git a/shell/qml/components/ConnectionToast.qml b/shell/qml/components/ConnectionToast.qml index be90995d4..d3ca4b98b 100644 --- a/shell/qml/components/ConnectionToast.qml +++ b/shell/qml/components/ConnectionToast.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Item { diff --git a/shell/qml/components/MarathonAppGrid.qml b/shell/qml/components/MarathonAppGrid.qml index baf865c79..db8cff461 100644 --- a/shell/qml/components/MarathonAppGrid.qml +++ b/shell/qml/components/MarathonAppGrid.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick import QtQuick.Effects diff --git a/shell/qml/components/MarathonAppWindow.qml b/shell/qml/components/MarathonAppWindow.qml index 37b47bb86..78151faf5 100644 --- a/shell/qml/components/MarathonAppWindow.qml +++ b/shell/qml/components/MarathonAppWindow.qml @@ -1,6 +1,7 @@ import MarathonUI.Core import MarathonUI.Modals import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Rectangle { @@ -215,12 +216,8 @@ Rectangle { appWindow.appName = name; appWindow.appIcon = icon; appWindow.appType = type; - if (typeof UIStore !== 'undefined') { - UIStore.appWindowOpen = true; - UIStore.currentAppId = id; - UIStore.currentAppName = name; - UIStore.currentAppIcon = icon; - } + if (typeof UIStore !== 'undefined') + UIStore.restoreApp(id, name, icon); if (!appContentLoader.item) { appWindow.pendingAppInstance = instance; appContentLoader.source = "appInstanceContainer"; diff --git a/shell/qml/components/MarathonBottomBar.qml b/shell/qml/components/MarathonBottomBar.qml index d1db6c777..5ca2e1565 100644 --- a/shell/qml/components/MarathonBottomBar.qml +++ b/shell/qml/components/MarathonBottomBar.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Item { diff --git a/shell/qml/components/MarathonNavBar.qml b/shell/qml/components/MarathonNavBar.qml index d15c67fb1..5853d0b66 100644 --- a/shell/qml/components/MarathonNavBar.qml +++ b/shell/qml/components/MarathonNavBar.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Rectangle { diff --git a/shell/qml/components/MarathonOOBE.qml b/shell/qml/components/MarathonOOBE.qml index 04677135b..c5bb71661 100644 --- a/shell/qml/components/MarathonOOBE.qml +++ b/shell/qml/components/MarathonOOBE.qml @@ -2,6 +2,7 @@ import MarathonUI.Containers import MarathonUI.Controls import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick import QtQuick.Controls import QtQuick.Layouts diff --git a/shell/qml/components/MarathonTaskSwitcher.qml b/shell/qml/components/MarathonTaskSwitcher.qml index ecce2ec59..9593e9cb9 100644 --- a/shell/qml/components/MarathonTaskSwitcher.qml +++ b/shell/qml/components/MarathonTaskSwitcher.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Item { diff --git a/shell/qml/components/PermissionDialog.qml b/shell/qml/components/PermissionDialog.qml index e8bd4c737..469292507 100644 --- a/shell/qml/components/PermissionDialog.qml +++ b/shell/qml/components/PermissionDialog.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick import QtQuick.Controls diff --git a/shell/qml/components/ShareSheet.qml b/shell/qml/components/ShareSheet.qml index 45f10c622..7e31b0cba 100644 --- a/shell/qml/components/ShareSheet.qml +++ b/shell/qml/components/ShareSheet.qml @@ -2,6 +2,7 @@ import MarathonUI.Containers import MarathonUI.Controls import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Item { diff --git a/shell/qml/components/ShellInitialization.qml b/shell/qml/components/ShellInitialization.qml index f100d9668..0c088b537 100644 --- a/shell/qml/components/ShellInitialization.qml +++ b/shell/qml/components/ShellInitialization.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Window +import MarathonOS.Shell 1.0 QtObject { id: root diff --git a/shell/qml/components/TaskCard.qml b/shell/qml/components/TaskCard.qml index cf2a84f82..8dfdbe995 100644 --- a/shell/qml/components/TaskCard.qml +++ b/shell/qml/components/TaskCard.qml @@ -1,5 +1,6 @@ import MarathonUI.Core import MarathonUI.Theme +import MarathonOS.Shell 1.0 import QtQuick Item { From 0e4b8d4d6d1a0362190fdb878dc21448d57caee9 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Fri, 16 Jan 2026 22:30:26 +0000 Subject: [PATCH 17/28] fix(app-runner): expose shell store proxies Register system store clients as module singletons and align navigation signals so apps can access shell state without QML errors. --- .../ipc/shellipcclients.cpp | 253 ++++++++++++++++++ .../marathon-app-runner/ipc/shellipcclients.h | 126 +++++++++ tools/marathon-app-runner/main.cpp | 22 +- 3 files changed, 400 insertions(+), 1 deletion(-) diff --git a/tools/marathon-app-runner/ipc/shellipcclients.cpp b/tools/marathon-app-runner/ipc/shellipcclients.cpp index 5b65acbb7..5e4427e42 100644 --- a/tools/marathon-app-runner/ipc/shellipcclients.cpp +++ b/tools/marathon-app-runner/ipc/shellipcclients.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include static bool isAccessDenied(const QDBusError &e) { return e.type() == QDBusError::AccessDenied; @@ -2234,6 +2236,257 @@ NavigationClient::NavigationClient(QObject *parent) this, SIGNAL(navigationFailed(QString, QString))); } +SystemStatusStoreClient::SystemStatusStoreClient(PowerClient *powerClient, + NetworkClient *networkClient, QObject *parent) + : QObject(parent) + , m_powerClient(powerClient) + , m_networkClient(networkClient) { + refreshPower(); + refreshNetwork(); + if (m_powerClient) { + connect(m_powerClient, &PowerClient::stateChanged, this, [this]() { refreshPower(); }); + } + if (m_networkClient) { + connect(m_networkClient, &NetworkClient::wifiConnectedChanged, this, + [this]() { refreshNetwork(); }); + connect(m_networkClient, &NetworkClient::wifiSsidChanged, this, + [this]() { refreshNetwork(); }); + } +} + +void SystemStatusStoreClient::refreshPower() { + if (!m_powerClient) { + setBatteryLevel(0); + return; + } + setBatteryLevel(m_powerClient->batteryLevel()); +} + +void SystemStatusStoreClient::refreshNetwork() { + if (!m_networkClient) { + setWifiConnected(false); + setWifiNetwork(QString()); + return; + } + setWifiConnected(m_networkClient->wifiConnected()); + setWifiNetwork(m_networkClient->wifiSsid()); +} + +void SystemStatusStoreClient::setBatteryLevel(int level) { + if (m_batteryLevel == level) { + return; + } + m_batteryLevel = level; + emit batteryLevelChanged(); +} + +void SystemStatusStoreClient::setWifiConnected(bool connected) { + if (m_wifiConnected == connected) { + return; + } + m_wifiConnected = connected; + emit wifiConnectedChanged(); +} + +void SystemStatusStoreClient::setWifiNetwork(const QString &network) { + if (m_wifiNetwork == network) { + return; + } + m_wifiNetwork = network; + emit wifiNetworkChanged(); +} + +SystemControlStoreClient::SystemControlStoreClient(NetworkClient *networkClient, + BluetoothClient *bluetoothClient, + DisplayClient *displayClient, + SettingsClient *settingsClient, + AudioClient *audioClient, QObject *parent) + : QObject(parent) + , m_networkClient(networkClient) + , m_bluetoothClient(bluetoothClient) + , m_displayClient(displayClient) + , m_settingsClient(settingsClient) + , m_audioClient(audioClient) { + refreshNetwork(); + refreshBluetooth(); + refreshDisplay(); + refreshSettings(); + refreshAudio(); + + if (m_networkClient) { + connect(m_networkClient, &NetworkClient::wifiEnabledChanged, this, + [this]() { refreshNetwork(); }); + connect(m_networkClient, &NetworkClient::airplaneModeEnabledChanged, this, + [this]() { refreshNetwork(); }); + } + if (m_bluetoothClient) { + connect(m_bluetoothClient, &BluetoothClient::stateChanged, this, + [this]() { refreshBluetooth(); }); + } + if (m_displayClient) { + connect(m_displayClient, &DisplayClient::stateChanged, this, + [this]() { refreshDisplay(); }); + } + if (m_settingsClient) { + connect(m_settingsClient, &SettingsClient::dndEnabledChanged, this, + [this]() { refreshSettings(); }); + } + if (m_audioClient) { + connect(m_audioClient, &AudioClient::stateChanged, this, [this]() { refreshAudio(); }); + } +} + +void SystemControlStoreClient::toggleWifi() { + if (m_networkClient) { + m_networkClient->toggleWifi(); + } +} + +void SystemControlStoreClient::toggleBluetooth() { + if (m_bluetoothClient) { + m_bluetoothClient->setEnabled(!m_bluetoothClient->enabled()); + } +} + +void SystemControlStoreClient::toggleAirplaneMode() { + if (m_networkClient) { + m_networkClient->setAirplaneMode(!m_isAirplaneModeOn); + } +} + +void SystemControlStoreClient::toggleRotationLock() { + if (m_displayClient) { + m_displayClient->setRotationLocked(!m_isRotationLocked); + } +} + +void SystemControlStoreClient::toggleDndMode() { + if (m_settingsClient) { + m_settingsClient->setDndEnabled(!m_isDndMode); + } + if (m_audioClient) { + m_audioClient->setDoNotDisturb(!m_isDndMode); + } +} + +void SystemControlStoreClient::setBrightness(int value) { + const int clamped = std::max(0, std::min(100, value)); + if (m_displayClient) { + m_displayClient->setBrightness(static_cast(clamped) / 100.0); + } + setBrightnessValue(clamped); +} + +void SystemControlStoreClient::setVolume(int value) { + const int clamped = std::max(0, std::min(100, value)); + if (m_audioClient) { + m_audioClient->setVolume(static_cast(clamped) / 100.0); + } + setVolumeValue(clamped); +} + +void SystemControlStoreClient::refreshNetwork() { + if (!m_networkClient) { + setIsWifiOn(true); + setIsAirplaneModeOn(false); + return; + } + setIsWifiOn(m_networkClient->wifiEnabled()); + setIsAirplaneModeOn(m_networkClient->airplaneModeEnabled()); +} + +void SystemControlStoreClient::refreshBluetooth() { + if (!m_bluetoothClient) { + setIsBluetoothOn(false); + return; + } + setIsBluetoothOn(m_bluetoothClient->enabled()); +} + +void SystemControlStoreClient::refreshDisplay() { + if (!m_displayClient) { + setIsRotationLocked(false); + setBrightnessValue(50); + return; + } + setIsRotationLocked(m_displayClient->rotationLocked()); + setBrightnessValue( + static_cast(std::round(std::clamp(m_displayClient->brightness(), 0.0, 1.0) * 100.0))); +} + +void SystemControlStoreClient::refreshSettings() { + if (!m_settingsClient) { + setIsDndMode(false); + return; + } + setIsDndMode(m_settingsClient->dndEnabled()); +} + +void SystemControlStoreClient::refreshAudio() { + if (!m_audioClient) { + setVolumeValue(50); + return; + } + setVolumeValue( + static_cast(std::round(std::clamp(m_audioClient->volume(), 0.0, 1.0) * 100.0))); +} + +void SystemControlStoreClient::setIsWifiOn(bool enabled) { + if (m_isWifiOn == enabled) { + return; + } + m_isWifiOn = enabled; + emit isWifiOnChanged(); +} + +void SystemControlStoreClient::setIsBluetoothOn(bool enabled) { + if (m_isBluetoothOn == enabled) { + return; + } + m_isBluetoothOn = enabled; + emit isBluetoothOnChanged(); +} + +void SystemControlStoreClient::setIsAirplaneModeOn(bool enabled) { + if (m_isAirplaneModeOn == enabled) { + return; + } + m_isAirplaneModeOn = enabled; + emit isAirplaneModeOnChanged(); +} + +void SystemControlStoreClient::setIsRotationLocked(bool locked) { + if (m_isRotationLocked == locked) { + return; + } + m_isRotationLocked = locked; + emit isRotationLockedChanged(); +} + +void SystemControlStoreClient::setIsDndMode(bool enabled) { + if (m_isDndMode == enabled) { + return; + } + m_isDndMode = enabled; + emit isDndModeChanged(); +} + +void SystemControlStoreClient::setBrightnessValue(int value) { + if (m_brightness == value) { + return; + } + m_brightness = value; + emit brightnessChanged(); +} + +void SystemControlStoreClient::setVolumeValue(int value) { + if (m_volume == value) { + return; + } + m_volume = value; + emit volumeChanged(); +} + bool NavigationClient::launchApp(const QString &appId) { if (appId.isEmpty()) { qWarning() << "[NavigationClient] launchApp: appId cannot be empty"; diff --git a/tools/marathon-app-runner/ipc/shellipcclients.h b/tools/marathon-app-runner/ipc/shellipcclients.h index 04e5d654a..5d4fc1a28 100644 --- a/tools/marathon-app-runner/ipc/shellipcclients.h +++ b/tools/marathon-app-runner/ipc/shellipcclients.h @@ -852,11 +852,137 @@ class NavigationClient : public QObject { signals: void appLaunched(const QString &appId); void navigationFailed(const QString &uri, const QString &error); + void deepLinkRequested(const QString &appId, const QString &route, const QVariantMap ¶ms); + void settingsNavigationRequested(const QString &page, const QString &subpage, + const QVariantMap ¶ms); private: QDBusInterface m_iface; }; +class SystemStatusStoreClient : public QObject { + Q_OBJECT + Q_PROPERTY(int batteryLevel READ batteryLevel NOTIFY batteryLevelChanged) + Q_PROPERTY(bool wifiConnected READ wifiConnected NOTIFY wifiConnectedChanged) + Q_PROPERTY(QString wifiNetwork READ wifiNetwork NOTIFY wifiNetworkChanged) + + public: + explicit SystemStatusStoreClient(PowerClient *powerClient, NetworkClient *networkClient, + QObject *parent = nullptr); + + int batteryLevel() const { + return m_batteryLevel; + } + bool wifiConnected() const { + return m_wifiConnected; + } + QString wifiNetwork() const { + return m_wifiNetwork; + } + + signals: + void batteryLevelChanged(); + void wifiConnectedChanged(); + void wifiNetworkChanged(); + + private: + void refreshPower(); + void refreshNetwork(); + void setBatteryLevel(int level); + void setWifiConnected(bool connected); + void setWifiNetwork(const QString &network); + + PowerClient *m_powerClient; + NetworkClient *m_networkClient; + int m_batteryLevel = 0; + bool m_wifiConnected = false; + QString m_wifiNetwork; +}; + +class SystemControlStoreClient : public QObject { + Q_OBJECT + Q_PROPERTY(bool isWifiOn READ isWifiOn NOTIFY isWifiOnChanged) + Q_PROPERTY(bool isBluetoothOn READ isBluetoothOn NOTIFY isBluetoothOnChanged) + Q_PROPERTY(bool isAirplaneModeOn READ isAirplaneModeOn NOTIFY isAirplaneModeOnChanged) + Q_PROPERTY(bool isRotationLocked READ isRotationLocked NOTIFY isRotationLockedChanged) + Q_PROPERTY(bool isDndMode READ isDndMode NOTIFY isDndModeChanged) + Q_PROPERTY(int brightness READ brightness NOTIFY brightnessChanged) + Q_PROPERTY(int volume READ volume NOTIFY volumeChanged) + + public: + explicit SystemControlStoreClient(NetworkClient *networkClient, + BluetoothClient *bluetoothClient, + DisplayClient *displayClient, SettingsClient *settingsClient, + AudioClient *audioClient, QObject *parent = nullptr); + + bool isWifiOn() const { + return m_isWifiOn; + } + bool isBluetoothOn() const { + return m_isBluetoothOn; + } + bool isAirplaneModeOn() const { + return m_isAirplaneModeOn; + } + bool isRotationLocked() const { + return m_isRotationLocked; + } + bool isDndMode() const { + return m_isDndMode; + } + int brightness() const { + return m_brightness; + } + int volume() const { + return m_volume; + } + + Q_INVOKABLE void toggleWifi(); + Q_INVOKABLE void toggleBluetooth(); + Q_INVOKABLE void toggleAirplaneMode(); + Q_INVOKABLE void toggleRotationLock(); + Q_INVOKABLE void toggleDndMode(); + Q_INVOKABLE void setBrightness(int value); + Q_INVOKABLE void setVolume(int value); + + signals: + void isWifiOnChanged(); + void isBluetoothOnChanged(); + void isAirplaneModeOnChanged(); + void isRotationLockedChanged(); + void isDndModeChanged(); + void brightnessChanged(); + void volumeChanged(); + + private: + void refreshNetwork(); + void refreshBluetooth(); + void refreshDisplay(); + void refreshSettings(); + void refreshAudio(); + + void setIsWifiOn(bool enabled); + void setIsBluetoothOn(bool enabled); + void setIsAirplaneModeOn(bool enabled); + void setIsRotationLocked(bool locked); + void setIsDndMode(bool enabled); + void setBrightnessValue(int value); + void setVolumeValue(int value); + + NetworkClient *m_networkClient; + BluetoothClient *m_bluetoothClient; + DisplayClient *m_displayClient; + SettingsClient *m_settingsClient; + AudioClient *m_audioClient; + bool m_isWifiOn = true; + bool m_isBluetoothOn = false; + bool m_isAirplaneModeOn = false; + bool m_isRotationLocked = false; + bool m_isDndMode = false; + int m_brightness = 50; + int m_volume = 50; +}; + class HapticClient : public QObject { Q_OBJECT Q_PROPERTY(bool available READ available NOTIFY availableChanged) diff --git a/tools/marathon-app-runner/main.cpp b/tools/marathon-app-runner/main.cpp index c153df541..8321e2b78 100644 --- a/tools/marathon-app-runner/main.cpp +++ b/tools/marathon-app-runner/main.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -432,7 +433,9 @@ int main(int argc, char *argv[]) { if (hasPerm("network")) ctx->setContextProperty("NetworkManagerCpp", new NetworkClient(appId, &app)); - ctx->setContextProperty("NavigationService", new NavigationClient(&app)); + auto *navigationClient = new NavigationClient(&app); + ctx->setContextProperty("NavigationService", navigationClient); + ctx->setContextProperty("NavigationRouter", navigationClient); ctx->setContextProperty("HapticService", new HapticClient(&app)); auto *notificationClient = new NotificationClient(appId, &app); ctx->setContextProperty("NativeNotificationService", notificationClient); @@ -440,6 +443,23 @@ int main(int argc, char *argv[]) { ctx->setContextProperty("LocationService", new LocationClient(&app)); ctx->setContextProperty("AlarmService", new AlarmClient(&app)); + auto *systemStatusStore = new SystemStatusStoreClient( + qobject_cast(ctx->contextProperty("PowerManagerService").value()), + qobject_cast(ctx->contextProperty("NetworkManagerCpp").value()), + &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, + "SystemStatusStore", systemStatusStore); + auto *systemControlStore = new SystemControlStoreClient( + qobject_cast(ctx->contextProperty("NetworkManagerCpp").value()), + qobject_cast( + ctx->contextProperty("BluetoothManagerCpp").value()), + qobject_cast(ctx->contextProperty("DisplayManagerCpp").value()), + settingsClient, + qobject_cast(ctx->contextProperty("AudioManagerCpp").value()), + &app); + qmlRegisterSingletonInstance( + "MarathonOS.Shell", 1, 0, "SystemControlStore", systemControlStore); + if (appId == "calendar") { ctx->setContextProperty("CalendarManager", new CalendarManagerCpp(settingsClient, notificationClient, &app)); From ee93d362bf89b4694c53835af43eaf1a1845183a Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 14:20:31 +0000 Subject: [PATCH 18/28] feat(shell): move keyboard and core singletons to C++ Replace QML router, surface registry, language, predictors, dictionary, and autocorrect with C++ singletons and update the keyboard integration. --- shell/CMakeLists.txt | 43 +++- shell/main.cpp | 25 ++ shell/qml/core/Router.qml | 73 ------ shell/qml/core/SurfaceRegistry.qml | 27 --- shell/qml/keyboard/Core/LanguageManager.qml | 228 ------------------ shell/qml/keyboard/Core/MarathonKeyboard.qml | 13 +- shell/qml/keyboard/Data/AutoCorrect.qml | 109 --------- shell/qml/keyboard/Data/Dictionary.qml | 180 -------------- shell/qml/keyboard/Data/DomainSuggestions.qml | 60 ----- shell/qml/keyboard/Data/EmojiPredictor.qml | 92 ------- shell/qml/keyboard/Data/PhrasePredictor.qml | 83 ------- shell/qml/keyboard/Layouts/QwertyLayout.qml | 1 - shell/qml/keyboard/Layouts/TerminalLayout.qml | 1 - shell/src/services/autocorrect.cpp | 134 ++++++++++ shell/src/services/autocorrect.h | 38 +++ shell/src/services/dictionary.cpp | 93 +++++++ shell/src/services/dictionary.h | 51 ++++ shell/src/services/domainsuggestions.cpp | 79 ++++++ shell/src/services/domainsuggestions.h | 22 ++ shell/src/services/emojipredictor.cpp | 88 +++++++ shell/src/services/emojipredictor.h | 21 ++ shell/src/services/languagemanager.cpp | 182 ++++++++++++++ shell/src/services/languagemanager.h | 69 ++++++ shell/src/services/phrasepredictor.cpp | 81 +++++++ shell/src/services/phrasepredictor.h | 21 ++ shell/src/services/router.cpp | 91 +++++++ shell/src/services/router.h | 66 +++++ shell/src/services/surfaceregistry.cpp | 37 +++ shell/src/services/surfaceregistry.h | 24 ++ 29 files changed, 1157 insertions(+), 875 deletions(-) delete mode 100644 shell/qml/core/Router.qml delete mode 100644 shell/qml/core/SurfaceRegistry.qml delete mode 100644 shell/qml/keyboard/Core/LanguageManager.qml delete mode 100644 shell/qml/keyboard/Data/AutoCorrect.qml delete mode 100644 shell/qml/keyboard/Data/Dictionary.qml delete mode 100644 shell/qml/keyboard/Data/DomainSuggestions.qml delete mode 100644 shell/qml/keyboard/Data/EmojiPredictor.qml delete mode 100644 shell/qml/keyboard/Data/PhrasePredictor.qml create mode 100644 shell/src/services/autocorrect.cpp create mode 100644 shell/src/services/autocorrect.h create mode 100644 shell/src/services/dictionary.cpp create mode 100644 shell/src/services/dictionary.h create mode 100644 shell/src/services/domainsuggestions.cpp create mode 100644 shell/src/services/domainsuggestions.h create mode 100644 shell/src/services/emojipredictor.cpp create mode 100644 shell/src/services/emojipredictor.h create mode 100644 shell/src/services/languagemanager.cpp create mode 100644 shell/src/services/languagemanager.h create mode 100644 shell/src/services/phrasepredictor.cpp create mode 100644 shell/src/services/phrasepredictor.h create mode 100644 shell/src/services/router.cpp create mode 100644 shell/src/services/router.h create mode 100644 shell/src/services/surfaceregistry.cpp create mode 100644 shell/src/services/surfaceregistry.h diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 58f5fe752..102b39c91 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -104,6 +104,22 @@ set(SOURCES src/services/appstore.cpp src/services/wallpaperstore.h src/services/wallpaperstore.cpp + src/services/router.h + src/services/router.cpp + src/services/surfaceregistry.h + src/services/surfaceregistry.cpp + src/services/domainsuggestions.h + src/services/domainsuggestions.cpp + src/services/emojipredictor.h + src/services/emojipredictor.cpp + src/services/phrasepredictor.h + src/services/phrasepredictor.cpp + src/services/languagemanager.h + src/services/languagemanager.cpp + src/services/dictionary.h + src/services/dictionary.cpp + src/services/autocorrect.h + src/services/autocorrect.cpp src/managers/flashlightmanagercpp.h src/managers/flashlightmanagercpp.cpp src/controllers/audioroutingmanager.h @@ -203,7 +219,6 @@ set(QML_FILES qml/components/PermissionDialog.qml qml/keyboard/Core/MarathonKeyboard.qml qml/keyboard/Core/KeyboardPerformance.qml - qml/keyboard/Core/LanguageManager.qml qml/keyboard/UI/Key.qml qml/keyboard/UI/PredictionBar.qml qml/keyboard/UI/LongPressPopup.qml @@ -218,9 +233,6 @@ set(QML_FILES qml/keyboard/Layouts/PhoneLayout.qml qml/keyboard/Layouts/TerminalLayout.qml qml/keyboard/Input/InputContext.qml - qml/keyboard/Data/Dictionary.qml - qml/keyboard/Data/AutoCorrect.qml - qml/keyboard/Data/DomainSuggestions.qml qml/components/PowerMenu.qml qml/components/QuickSettingsTile.qml qml/components/MediaPlaybackManager.qml @@ -228,8 +240,6 @@ set(QML_FILES qml/apps/native/NativeAppWindow.qml qml/core/Constants.qml qml/core/Logger.qml - qml/core/SurfaceRegistry.qml - qml/core/Router.qml qml/models/FilteredAppModel.qml qml/services/CalendarWorker.js ) @@ -260,11 +270,6 @@ qt6_add_executable(marathon-shell ${SOURCES}) set(SINGLETON_FILES qml/core/Constants.qml qml/core/Logger.qml - qml/core/SurfaceRegistry.qml - qml/core/Router.qml - qml/keyboard/Core/LanguageManager.qml - qml/keyboard/Data/Dictionary.qml - qml/keyboard/Data/AutoCorrect.qml ) foreach(singleton_file ${SINGLETON_FILES}) @@ -314,6 +319,22 @@ qt6_add_qml_module(marathon-shell src/services/appstore.h src/services/wallpaperstore.cpp src/services/wallpaperstore.h + src/services/router.cpp + src/services/router.h + src/services/surfaceregistry.cpp + src/services/surfaceregistry.h + src/services/domainsuggestions.cpp + src/services/domainsuggestions.h + src/services/emojipredictor.cpp + src/services/emojipredictor.h + src/services/phrasepredictor.cpp + src/services/phrasepredictor.h + src/services/languagemanager.cpp + src/services/languagemanager.h + src/services/dictionary.cpp + src/services/dictionary.h + src/services/autocorrect.cpp + src/services/autocorrect.h RESOURCES ${JSON_FILES} OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/shell/qml/MarathonOS/Shell ) diff --git a/shell/main.cpp b/shell/main.cpp index 3cbe97ac9..719684bb7 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -79,6 +79,13 @@ #include "src/services/uistore.h" #include "src/services/appstore.h" #include "src/services/wallpaperstore.h" +#include "src/services/router.h" +#include "src/services/surfaceregistry.h" +#include "src/services/languagemanager.h" +#include "src/services/dictionary.h" +#include "src/services/autocorrect.h" +#include "src/services/emojipredictor.h" +#include "src/services/phrasepredictor.h" #include "qml/keyboard/Data/WordEngine.h" #include "src/dbus/freedesktopnotifications.h" #include "src/dbus/notificationdatabase.h" @@ -443,6 +450,11 @@ int main(int argc, char *argv[]) { systemControlStore); auto *uiStore = new UIStore(&app); qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "UIStore", uiStore); + auto *router = new Router(&app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "Router", router); + auto *surfaceRegistry = new SurfaceRegistry(&app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "SurfaceRegistry", + surfaceRegistry); createObject(ctx, "CursorManager", &app); if (debugEnabled || profileMode) { @@ -472,6 +484,19 @@ int main(int argc, char *argv[]) { wordEngine->setLanguage("en_US"); wordEngine->setEnabled(true); qInfo() << "[MarathonShell] ✓ Word Engine initialized"; + auto *emojiPredictor = new EmojiPredictor(&app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "EmojiPredictor", + emojiPredictor); + auto *phrasePredictor = new PhrasePredictor(&app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "PhrasePredictor", + phrasePredictor); + auto *dictionary = new Dictionary(wordEngine, emojiPredictor, phrasePredictor, &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "Dictionary", dictionary); + auto *autoCorrect = new AutoCorrect(dictionary, &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "AutoCorrect", autoCorrect); + auto *languageManager = new LanguageManager(settingsManager, wordEngine, &app); + qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "LanguageManager", + languageManager); auto *rtScheduler = createObject(ctx, "RTScheduler", &app); if (rtScheduler->isRealtimeKernel()) { diff --git a/shell/qml/core/Router.qml b/shell/qml/core/Router.qml deleted file mode 100644 index e21f68bc2..000000000 --- a/shell/qml/core/Router.qml +++ /dev/null @@ -1,73 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: router - - property int currentPageIndex: 2 - property int previousPageIndex: 2 - readonly property int pageHub: 0 - readonly property int pageFrames: 1 - readonly property int pageApps: 2 - - signal navigateToHub - signal navigateToFrames - signal navigateToApps(int appPage) - signal navigateBack - signal navigateNext - signal navigateToSettingPage(string settingId) - - function goToHub() { - Logger.nav("current", "Hub", "direct"); - previousPageIndex = currentPageIndex; - currentPageIndex = pageHub; - navigateToHub(); - } - - function goToFrames() { - Logger.nav("current", "ActiveFrames", "direct"); - previousPageIndex = currentPageIndex; - currentPageIndex = pageFrames; - navigateToFrames(); - } - - function goToAppPage(page) { - Logger.nav("current", "AppPage" + page, "direct"); - previousPageIndex = currentPageIndex; - currentPageIndex = pageApps + page; - navigateToApps(page); - } - - function goBack() { - Logger.nav("current", "previous", "back"); - var temp = currentPageIndex; - currentPageIndex = previousPageIndex; - previousPageIndex = temp; - navigateBack(); - } - - function navigateLeft() { - if (currentPageIndex < 10) { - previousPageIndex = currentPageIndex; - currentPageIndex++; - Logger.nav("page" + previousPageIndex, "page" + currentPageIndex, "swipeLeft"); - } - } - - function navigateRight() { - if (currentPageIndex > 0) { - previousPageIndex = currentPageIndex; - currentPageIndex--; - Logger.nav("page" + previousPageIndex, "page" + currentPageIndex, "swipeRight"); - } - } - - function getCurrentPage() { - return currentPageIndex - 2; - } - - function navigateToSetting(settingId) { - Logger.nav("current", "Setting:" + settingId, "search"); - navigateToSettingPage(settingId); - } -} diff --git a/shell/qml/core/SurfaceRegistry.qml b/shell/qml/core/SurfaceRegistry.qml deleted file mode 100644 index efce77cd4..000000000 --- a/shell/qml/core/SurfaceRegistry.qml +++ /dev/null @@ -1,27 +0,0 @@ -pragma Singleton -import MarathonUI.Core -import QtQuick - -QtObject { - id: surfaceRegistry - - property var surfaceMap: ({}) - - function registerSurface(surfaceId, item) { - if (surfaceId !== -1 && item) { - surfaceMap[surfaceId] = item; - Logger.info("SurfaceRegistry", "Registered surface: " + surfaceId); - } - } - - function unregisterSurface(surfaceId) { - if (surfaceMap[surfaceId]) { - delete surfaceMap[surfaceId]; - Logger.info("SurfaceRegistry", "Unregistered surface: " + surfaceId); - } - } - - function getSurfaceItem(surfaceId) { - return surfaceMap[surfaceId] || null; - } -} diff --git a/shell/qml/keyboard/Core/LanguageManager.qml b/shell/qml/keyboard/Core/LanguageManager.qml deleted file mode 100644 index 9fbc6a06e..000000000 --- a/shell/qml/keyboard/Core/LanguageManager.qml +++ /dev/null @@ -1,228 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: languageManager - - property string currentLanguage: SettingsManagerCpp.keyboardLanguage || "en_US" - property var currentLayout: null - property var availableLanguages: [] - readonly property var languageFlags: { - "en_US": "🇺🇸", - "fr_FR": "🇫🇷", - "de_DE": "🇩🇪", - "es_ES": "🇪🇸", - "pt_BR": "🇧🇷", - "it_IT": "🇮🇹", - "ru_RU": "🇷🇺", - "ar_SA": "🇸🇦", - "zh_CN": "🇨🇳", - "ja_JP": "🇯🇵" - } - property var activeRequest: null - - signal languageChanged(string languageId) - signal layoutLoaded(var layout) - - function initialize() { - availableLanguages = ["en_US", "fr_FR", "de_DE", "es_ES", "pt_BR", "it_IT", "ru_RU", "ar_SA", "zh_CN", "ja_JP"]; - loadLanguage(currentLanguage); - } - - function loadLanguage(languageId) { - if (activeRequest) { - activeRequest.onreadystatechange = null; - activeRequest = null; - } - var xhr = new XMLHttpRequest(); - activeRequest = xhr; - var url = Qt.resolvedUrl("../Layouts/languages/" + languageId + ".json"); - xhr.onreadystatechange = function () { - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200 || xhr.status === 0) { - try { - var response = xhr.responseText; - if (!response) - throw "Empty response text"; - - currentLayout = JSON.parse(response); - currentLanguage = languageId; - SettingsManagerCpp.keyboardLanguage = languageId; - if (typeof WordEngine !== 'undefined' && WordEngine !== null) - WordEngine.setLanguage(currentLayout.dictionary || languageId); - - languageChanged(languageId); - layoutLoaded(currentLayout); - } catch (e) { - console.error("[LanguageManager] Error parsing layout JSON: " + e); - loadFallback(); - } - } else { - console.error("[LanguageManager] Failed to load layout file. Status: " + xhr.status); - loadFallback(); - } - activeRequest = null; - } - }; - xhr.onerror = function () { - console.error("[LanguageManager] XHR Network Error"); - loadFallback(); - }; - try { - xhr.open("GET", url); - xhr.send(); - } catch (e) { - console.error("[LanguageManager] XHR Exception: " + e); - loadFallback(); - } - } - - function loadFallback() { - console.warn("[LanguageManager] Loading fallback layout (English US)"); - currentLayout = { - "id": "en_US", - "name": "English (US)", - "dictionary": "en_US", - "rtl": false, - "rows": [ - { - "keys": [ - { - "char": "q", - "alts": [] - }, - { - "char": "w", - "alts": [] - }, - { - "char": "e", - "alts": ["è", "é", "ê", "ë", "ē"] - }, - { - "char": "r", - "alts": [] - }, - { - "char": "t", - "alts": [] - }, - { - "char": "y", - "alts": [] - }, - { - "char": "u", - "alts": [] - }, - { - "char": "i", - "alts": [] - }, - { - "char": "o", - "alts": [] - }, - { - "char": "p", - "alts": [] - } - ] - }, - { - "keys": [ - { - "char": "a", - "alts": [] - }, - { - "char": "s", - "alts": [] - }, - { - "char": "d", - "alts": [] - }, - { - "char": "f", - "alts": [] - }, - { - "char": "g", - "alts": [] - }, - { - "char": "h", - "alts": [] - }, - { - "char": "j", - "alts": [] - }, - { - "char": "k", - "alts": [] - }, - { - "char": "l", - "alts": [] - } - ] - }, - { - "keys": [ - { - "char": "z", - "alts": [] - }, - { - "char": "x", - "alts": [] - }, - { - "char": "c", - "alts": [] - }, - { - "char": "v", - "alts": [] - }, - { - "char": "b", - "alts": [] - }, - { - "char": "n", - "alts": [] - }, - { - "char": "m", - "alts": [] - } - ] - } - ] - }; - currentLanguage = "en_US"; - layoutLoaded(currentLayout); - } - - function nextLanguage() { - var idx = availableLanguages.indexOf(currentLanguage); - var nextIdx = (idx + 1) % availableLanguages.length; - loadLanguage(availableLanguages[nextIdx]); - } - - function getFlag(languageId) { - return languageFlags[languageId] || "🌐"; - } - - function getName(languageId) { - if (!currentLayout || currentLayout.id !== languageId) - return languageId; - - return currentLayout.name || languageId; - } - - Component.onCompleted: initialize() -} diff --git a/shell/qml/keyboard/Core/MarathonKeyboard.qml b/shell/qml/keyboard/Core/MarathonKeyboard.qml index b79164c65..3570c3fbf 100644 --- a/shell/qml/keyboard/Core/MarathonKeyboard.qml +++ b/shell/qml/keyboard/Core/MarathonKeyboard.qml @@ -1,3 +1,4 @@ +import MarathonOS.Shell 1.0 import QtQuick Rectangle { @@ -182,8 +183,8 @@ Rectangle { var isEmail = inputContextInstance.inputMode === "email"; var isUrl = inputContextInstance.inputMode === "url"; if (isEmail || isUrl) { - if (domainSuggestions.shouldShowDomainSuggestions(keyboard.currentWord, isEmail, isUrl)) { - var domainSugs = domainSuggestions.getSuggestions(keyboard.currentWord, isEmail); + if (DomainSuggestions.shouldShowDomainSuggestions(keyboard.currentWord, isEmail, isUrl)) { + var domainSugs = DomainSuggestions.getSuggestions(keyboard.currentWord, isEmail); if (domainSugs.length > 0) { keyboard.currentPredictions = domainSugs; return; @@ -260,10 +261,6 @@ Rectangle { currentPredictions = []; } - DomainSuggestions { - id: domainSuggestions - } - Connections { function onCachedPredictionsChanged() { if (inputContextInstance.shouldShowPredictions && keyboard.currentWord === Dictionary.lastPredictionPrefix) @@ -582,10 +579,6 @@ Rectangle { width: parent.width visible: keyboard.showLanguageSelector - onLanguageSelected: function (languageId) { - if (typeof WordEngine !== 'undefined' && WordEngine !== null) - WordEngine.setLanguage(LanguageManager.currentLayout.dictionary); - } onDismissed: { keyboard.showLanguageSelector = false; } diff --git a/shell/qml/keyboard/Data/AutoCorrect.qml b/shell/qml/keyboard/Data/AutoCorrect.qml deleted file mode 100644 index d09ec6735..000000000 --- a/shell/qml/keyboard/Data/AutoCorrect.qml +++ /dev/null @@ -1,109 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: autoCorrect - - property var commonTypos: ({ - "teh": "the", - "adn": "and", - "youre": "you're", - "dont": "don't", - "cant": "can't", - "wont": "won't", - "didnt": "didn't", - "doesnt": "doesn't", - "hasnt": "hasn't", - "havent": "haven't", - "isnt": "isn't", - "wasnt": "wasn't", - "werent": "weren't", - "shouldnt": "shouldn't", - "wouldnt": "wouldn't", - "couldnt": "couldn't", - "thats": "that's", - "whats": "what's", - "hes": "he's", - "shes": "she's", - "its": "it's", - "im": "I'm", - "youve": "you've", - "theyve": "they've", - "weve": "we've", - "ive": "I've", - "recieve": "receive", - "beleive": "believe", - "neccessary": "necessary", - "seperate": "separate", - "definately": "definitely", - "occured": "occurred", - "begining": "beginning", - "tommorrow": "tomorrow", - "untill": "until", - "alot": "a lot" - }) - property bool enabled: true - - function correct(word) { - if (!enabled || !word || word.length === 0) - return word; - - var lowerWord = word.toLowerCase(); - if (commonTypos.hasOwnProperty(lowerWord)) { - var correction = commonTypos[lowerWord]; - Logger.info("AutoCorrect", "Correcting '" + word + "' to '" + correction + "'"); - return correction; - } - if (!Dictionary.hasWord(lowerWord)) { - var suggestions = Dictionary.predict(lowerWord); - if (suggestions.length > 0) { - var bestMatch = suggestions[0]; - var distance = levenshteinDistance(lowerWord, bestMatch.toLowerCase()); - if (distance <= 2) { - Logger.info("AutoCorrect", "Correcting '" + word + "' to '" + bestMatch + "' (distance: " + distance + ")"); - return bestMatch; - } - } - } - return word; - } - - function levenshteinDistance(a, b) { - if (a.length === 0) - return b.length; - - if (b.length === 0) - return a.length; - - var matrix = []; - for (var i = 0; i <= b.length; i++) - matrix[i] = [i]; - for (var j = 0; j <= a.length; j++) - matrix[0][j] = j; - for (var i = 1; i <= b.length; i++) { - for (var j = 1; j <= a.length; j++) { - if (b.charAt(i - 1) === a.charAt(j - 1)) - matrix[i][j] = matrix[i - 1][j - 1]; - else - matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); - } - } - return matrix[b.length][a.length]; - } - - function shouldCorrect(word) { - if (!enabled || !word || word.length < 3) - return null; - - var corrected = correct(word); - if (corrected !== word) - return corrected; - - return null; - } - - function learnCorrection(typo, correction) { - commonTypos[typo.toLowerCase()] = correction; - Logger.info("AutoCorrect", "Learned: '" + typo + "' -> '" + correction + "'"); - } -} diff --git a/shell/qml/keyboard/Data/Dictionary.qml b/shell/qml/keyboard/Data/Dictionary.qml deleted file mode 100644 index d957c410d..000000000 --- a/shell/qml/keyboard/Data/Dictionary.qml +++ /dev/null @@ -1,180 +0,0 @@ -pragma Singleton -import QtQuick - -Item { - id: dictionary - - property var cachedPredictions: [] - property string lastPredictionPrefix: "" - property string lastContext: "" - readonly property var emojiMap: ({ - "happy": "😀", - "sad": "😢", - "love": "❤️", - "heart": "❤️", - "laugh": "😂", - "lol": "😂", - "cry": "😭", - "angry": "😠", - "cool": "😎", - "great": "👍", - "thanks": "🙏", - "thank": "🙏", - "yes": "👍", - "no": "👎", - "ok": "👌", - "good": "👍", - "bad": "👎", - "fire": "🔥", - "party": "🎉", - "birthday": "🎂", - "food": "🍕", - "pizza": "🍕", - "coffee": "☕", - "beer": "🍺", - "sun": "☀️", - "rain": "🌧️", - "snow": "❄️", - "hot": "🔥", - "cold": "🥶", - "music": "🎵", - "phone": "📱", - "home": "🏠", - "work": "💼", - "sleep": "😴", - "tired": "😴", - "think": "🤔", - "idea": "💡", - "money": "💰", - "time": "⏰", - "star": "⭐", - "check": "✅", - "question": "❓", - "warning": "⚠️", - "congrats": "🎉", - "sorry": "😔", - "please": "🙏", - "hello": "👋", - "hi": "👋", - "bye": "👋", - "wow": "😮", - "omg": "😱", - "haha": "😂", - "nice": "👍", - "cute": "🥰", - "beautiful": "😍", - "amazing": "🤩", - "awesome": "🔥", - "perfect": "💯", - "dog": "🐕", - "cat": "🐱" - }) - readonly property var phraseMap: ({ - "how are": ["you", "you doing", "things"], - "how is": ["it going", "everything", "your day"], - "what are": ["you doing", "you up to"], - "what is": ["up", "going on", "your name"], - "i am": ["here", "coming", "on my way", "good"], - "i will": ["be there", "call you", "let you know"], - "i have": ["to go", "a question", "an idea"], - "i think": ["so", "that", "it's good"], - "i need": ["to", "help", "more time"], - "i want": ["to", "that", "it"], - "i love": ["you", "it", "this"], - "i like": ["it", "that", "this"], - "can you": ["help", "do it", "send me"], - "can i": ["help", "ask you", "call you"], - "do you": ["know", "have", "want"], - "are you": ["there", "coming", "ok", "free"], - "is it": ["ok", "possible", "working"], - "let me": ["know", "think", "check"], - "thank you": ["so much", "very much"], - "thanks for": ["your help", "letting me know"], - "looking forward": ["to it", "to hearing"], - "nice to": ["meet you", "see you"], - "have a": ["good day", "nice day", "great day"], - "see you": ["later", "soon", "tomorrow"], - "talk to": ["you later", "you soon"], - "on my": ["way", "phone", "mind"], - "be right": ["back", "there"], - "going to": ["be", "do", "the"], - "want to": ["go", "do", "see"], - "need to": ["go", "do", "talk"], - "sounds good": ["to me"], - "that sounds": ["good", "great", "perfect"], - "no problem": ["at all"] - }) - - function predict(prefix) { - if (!prefix || prefix.length === 0) - return []; - - if (typeof WordEngine === 'undefined' || WordEngine === null || !WordEngine.enabled) - return []; - - if (dictionary.lastPredictionPrefix !== prefix) { - dictionary.lastPredictionPrefix = prefix; - dictionary.cachedPredictions = []; - WordEngine.requestPredictions(prefix, 5); - } - return cachedPredictions; - } - - function getEmojiForWord(word) { - if (!word) - return ""; - - var lowerWord = word.toLowerCase(); - return emojiMap.hasOwnProperty(lowerWord) ? emojiMap[lowerWord] : ""; - } - - function getPhraseCompletions(context) { - if (!context) - return []; - - var lowerContext = context.toLowerCase().trim(); - if (phraseMap.hasOwnProperty(lowerContext)) - return phraseMap[lowerContext]; - - var words = lowerContext.split(" "); - if (words.length >= 2) { - var lastTwo = words.slice(-2).join(" "); - if (phraseMap.hasOwnProperty(lastTwo)) - return phraseMap[lastTwo]; - } - return []; - } - - function learnWord(word) { - if (!word || word.length < 2) - return; - - if (typeof WordEngine !== 'undefined' && WordEngine !== null && WordEngine.enabled) - WordEngine.learnWord(word); - } - - function hasWord(word) { - if (!word) - return false; - - if (typeof WordEngine !== 'undefined' && WordEngine !== null && WordEngine.enabled) - return WordEngine.hasWord(word); - - return false; - } - - Connections { - function onPredictionsReady(prefix, predictions) { - if (prefix === dictionary.lastPredictionPrefix) { - var enhanced = predictions.slice(); - var emoji = dictionary.getEmojiForWord(prefix); - if (emoji && enhanced.length < 5) - enhanced.push(emoji); - - dictionary.cachedPredictions = enhanced; - } - } - - target: typeof WordEngine !== 'undefined' ? WordEngine : null - } -} diff --git a/shell/qml/keyboard/Data/DomainSuggestions.qml b/shell/qml/keyboard/Data/DomainSuggestions.qml deleted file mode 100644 index a7931dfa5..000000000 --- a/shell/qml/keyboard/Data/DomainSuggestions.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick - -QtObject { - id: domainSuggestions - - readonly property var commonTLDs: ["com", "org", "net", "edu", "gov", "co.uk", "co", "io", "app", "dev", "uk", "ca", "au", "de", "fr", "jp", "cn", "in", "br", "ru"] - readonly property var emailDomains: ["gmail.com", "outlook.com", "yahoo.com", "hotmail.com", "icloud.com", "protonmail.com", "me.com", "live.com", "msn.com"] - readonly property var urlPrefixes: ["www.", "mail.", "blog.", "shop.", "m.", "api.", "dev.", "staging."] - - function getSuggestions(text, isEmail) { - if (!text || text.length === 0) - return []; - - var suggestions = []; - var lowerText = text.toLowerCase(); - if (isEmail) { - if (lowerText.indexOf("@") !== -1) { - var parts = lowerText.split("@"); - if (parts.length === 2) { - var domain = parts[1]; - for (var i = 0; i < emailDomains.length && suggestions.length < 3; i++) { - if (emailDomains[i].startsWith(domain) && emailDomains[i] !== domain) - suggestions.push(parts[0] + "@" + emailDomains[i]); - } - } - } else if (lowerText.length >= 2) { - for (var j = 0; j < emailDomains.length && suggestions.length < 3; j++) - suggestions.push(lowerText + "@" + emailDomains[j]); - } - } else { - var lastDot = lowerText.lastIndexOf("."); - if (lastDot !== -1) { - var afterDot = lowerText.substring(lastDot + 1); - for (var k = 0; k < commonTLDs.length && suggestions.length < 3; k++) { - if (commonTLDs[k].startsWith(afterDot) && commonTLDs[k] !== afterDot) { - var beforeDot = lowerText.substring(0, lastDot + 1); - suggestions.push(beforeDot + commonTLDs[k]); - } - } - } else if (lowerText.length >= 2 && lowerText.indexOf("://") === -1) { - for (var l = 0; l < Math.min(3, commonTLDs.length); l++) - suggestions.push(lowerText + "." + commonTLDs[l]); - } - } - return suggestions; - } - - function shouldShowDomainSuggestions(text, isEmail, isUrl) { - if (!text || text.length === 0) - return false; - - if (isEmail) - return text.indexOf("@") !== -1 || text.length >= 2; - - if (isUrl) - return text.indexOf(".") !== -1 || (text.length >= 2 && text.indexOf("://") === -1); - - return false; - } -} diff --git a/shell/qml/keyboard/Data/EmojiPredictor.qml b/shell/qml/keyboard/Data/EmojiPredictor.qml deleted file mode 100644 index e882c5257..000000000 --- a/shell/qml/keyboard/Data/EmojiPredictor.qml +++ /dev/null @@ -1,92 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: emojiPredictor - - readonly property var emojiMap: ({ - "happy": ["😀", "😊", "😄"], - "sad": ["😢", "😞", "😔"], - "love": ["❤️", "😍", "💕"], - "heart": ["❤️", "💖", "💗"], - "laugh": ["😂", "🤣", "😆"], - "lol": ["😂", "🤣", "😄"], - "cry": ["😢", "😭", "😿"], - "angry": ["😠", "😡", "🤬"], - "cool": ["😎", "🆒", "👍"], - "great": ["👍", "🙌", "✨"], - "thanks": ["🙏", "👍", "❤️"], - "thank": ["🙏", "👍", "❤️"], - "yes": ["👍", "✅", "👌"], - "no": ["👎", "❌", "🙅"], - "ok": ["👌", "👍", "✅"], - "good": ["👍", "👌", "🙌"], - "bad": ["👎", "😕", "❌"], - "fire": ["🔥", "💥", "⚡"], - "party": ["🎉", "🥳", "🎊"], - "birthday": ["🎂", "🎉", "🎁"], - "food": ["🍕", "🍔", "🍟"], - "pizza": ["🍕", "🧀", "🍴"], - "coffee": ["☕", "🫖", "🍵"], - "beer": ["🍺", "🍻", "🥂"], - "sun": ["☀️", "🌞", "🌅"], - "rain": ["🌧️", "☔", "💧"], - "snow": ["❄️", "☃️", "🌨️"], - "hot": ["🔥", "🥵", "☀️"], - "cold": ["🥶", "❄️", "🧊"], - "music": ["🎵", "🎶", "🎸"], - "phone": ["📱", "☎️", "📞"], - "home": ["🏠", "🏡", "🏘️"], - "work": ["💼", "🏢", "👔"], - "sleep": ["😴", "💤", "🛏️"], - "tired": ["😴", "😩", "🥱"], - "think": ["🤔", "💭", "🧠"], - "idea": ["💡", "🧠", "✨"], - "money": ["💰", "💵", "🤑"], - "time": ["⏰", "🕐", "⌚"], - "star": ["⭐", "🌟", "✨"], - "check": ["✅", "☑️", "✔️"], - "question": ["❓", "🤔", "❔"], - "warning": ["⚠️", "🚨", "⛔"], - "congrats": ["🎉", "👏", "🥳"], - "sorry": ["😔", "🙏", "😢"], - "please": ["🙏", "🥺", "😊"], - "hello": ["👋", "😊", "🙋"], - "hi": ["👋", "😊", "🙋"], - "bye": ["👋", "😢", "🙋"], - "goodbye": ["👋", "😢", "🙋"], - "wow": ["😮", "🤩", "😲"], - "omg": ["😱", "😮", "🙀"], - "haha": ["😂", "🤣", "😆"], - "nice": ["👍", "👌", "🙌"], - "cute": ["🥰", "😊", "💕"], - "beautiful": ["😍", "✨", "💖"], - "amazing": ["🤩", "😍", "✨"], - "awesome": ["🔥", "🙌", "✨"], - "perfect": ["💯", "✨", "👌"], - "run": ["🏃", "🏃‍♂️", "🏃‍♀️"], - "walk": ["🚶", "🚶‍♂️", "🚶‍♀️"], - "car": ["🚗", "🚙", "🏎️"], - "plane": ["✈️", "🛫", "🛬"], - "dog": ["🐕", "🐶", "🦮"], - "cat": ["🐱", "🐈", "😺"], - "book": ["📖", "📚", "📕"], - "movie": ["🎬", "🎥", "📽️"], - "game": ["🎮", "🕹️", "🎯"], - "win": ["🏆", "🥇", "🎉"], - "lose": ["😞", "😔", "👎"] - }) - - function getEmojis(word) { - var lowerWord = word.toLowerCase(); - if (emojiMap.hasOwnProperty(lowerWord)) - return emojiMap[lowerWord]; - - return []; - } - - function getTopEmoji(word) { - var emojis = getEmojis(word); - return emojis.length > 0 ? emojis[0] : ""; - } -} diff --git a/shell/qml/keyboard/Data/PhrasePredictor.qml b/shell/qml/keyboard/Data/PhrasePredictor.qml deleted file mode 100644 index 2ed7c9a51..000000000 --- a/shell/qml/keyboard/Data/PhrasePredictor.qml +++ /dev/null @@ -1,83 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: phrasePredictor - - readonly property var phraseMap: ({ - "how are": ["you", "you doing", "things"], - "how is": ["it going", "everything", "your day"], - "what are": ["you doing", "you up to", "your plans"], - "what is": ["up", "going on", "your name"], - "where are": ["you", "we going", "we meeting"], - "where is": ["it", "that", "everyone"], - "when are": ["you coming", "we meeting", "you free"], - "when is": ["it", "the meeting", "your birthday"], - "i am": ["here", "coming", "on my way", "good", "fine"], - "i will": ["be there", "call you", "let you know", "do it"], - "i have": ["to go", "a question", "an idea", "time"], - "i think": ["so", "that", "it's good", "we should"], - "i need": ["to", "help", "more time", "your help"], - "i want": ["to", "that", "it", "one"], - "i hope": ["so", "you're well", "it works", "to see you"], - "i love": ["you", "it", "this", "that"], - "i like": ["it", "that", "this", "your idea"], - "can you": ["help", "do it", "send me", "call me"], - "can i": ["help", "ask you", "call you", "come"], - "do you": ["know", "have", "want", "think"], - "did you": ["get it", "see that", "hear", "know"], - "are you": ["there", "coming", "ok", "sure", "free"], - "is it": ["ok", "possible", "working", "done"], - "let me": ["know", "think", "check", "see"], - "thank you": ["so much", "very much", "for everything"], - "thanks for": ["your help", "letting me know", "everything"], - "looking forward": ["to it", "to hearing", "to seeing you"], - "nice to": ["meet you", "see you", "hear from you"], - "have a": ["good day", "nice day", "great day", "good one"], - "see you": ["later", "soon", "tomorrow", "there"], - "talk to": ["you later", "you soon", "me"], - "on my": ["way", "phone", "computer", "mind"], - "be right": ["back", "there", "over"], - "going to": ["be", "do", "the", "try"], - "want to": ["go", "do", "see", "try", "know"], - "need to": ["go", "do", "talk", "know", "finish"], - "have to": ["go", "do", "leave", "work"], - "got to": ["go", "run", "do"], - "good to": ["know", "see you", "hear"], - "great to": ["see you", "hear", "meet you"], - "happy to": ["help", "hear", "see you", "know"], - "sorry for": ["the delay", "the wait", "bothering you"], - "sorry i": ["missed", "forgot", "can't", "didn't"], - "no problem": ["at all", "happy to help"], - "sounds good": ["to me", "let's do it"], - "that sounds": ["good", "great", "perfect", "fun"], - "that's a": ["good idea", "great idea", "good point"], - "that's great": ["news", "to hear"], - "the best": ["way", "thing", "option", "part"], - "a lot": ["of", "more", "better"], - "of course": ["not", "I will", "you can"], - "by the": ["way", "time", "end"], - "at the": ["moment", "end", "same time"], - "in the": ["morning", "afternoon", "evening", "end"], - "on the": ["way", "phone", "other hand"] - }) - - function getPhrases(words) { - var key = words.toLowerCase().trim(); - if (phraseMap.hasOwnProperty(key)) - return phraseMap[key]; - - var parts = key.split(" "); - if (parts.length >= 2) { - var lastTwo = parts.slice(-2).join(" "); - if (phraseMap.hasOwnProperty(lastTwo)) - return phraseMap[lastTwo]; - } - return []; - } - - function getTopPhrase(words) { - var phrases = getPhrases(words); - return phrases.length > 0 ? phrases[0] : ""; - } -} diff --git a/shell/qml/keyboard/Layouts/QwertyLayout.qml b/shell/qml/keyboard/Layouts/QwertyLayout.qml index ad9c29e9c..78bbf4a58 100644 --- a/shell/qml/keyboard/Layouts/QwertyLayout.qml +++ b/shell/qml/keyboard/Layouts/QwertyLayout.qml @@ -1,4 +1,3 @@ -import "../Data" import "../UI" import QtQuick diff --git a/shell/qml/keyboard/Layouts/TerminalLayout.qml b/shell/qml/keyboard/Layouts/TerminalLayout.qml index 4d0690d23..cabc335ab 100644 --- a/shell/qml/keyboard/Layouts/TerminalLayout.qml +++ b/shell/qml/keyboard/Layouts/TerminalLayout.qml @@ -1,4 +1,3 @@ -import "../Data" import "../UI" import QtQuick diff --git a/shell/src/services/autocorrect.cpp b/shell/src/services/autocorrect.cpp new file mode 100644 index 000000000..821e57af9 --- /dev/null +++ b/shell/src/services/autocorrect.cpp @@ -0,0 +1,134 @@ +#include "autocorrect.h" + +#include +#include + +#include "dictionary.h" + +AutoCorrect::AutoCorrect(Dictionary *dictionary, QObject *parent) + : QObject(parent) + , m_dictionary(dictionary) + , m_commonTypos({ + {"teh", "the"}, + {"adn", "and"}, + {"youre", "you're"}, + {"dont", "don't"}, + {"cant", "can't"}, + {"wont", "won't"}, + {"didnt", "didn't"}, + {"doesnt", "doesn't"}, + {"hasnt", "hasn't"}, + {"havent", "haven't"}, + {"isnt", "isn't"}, + {"wasnt", "wasn't"}, + {"werent", "weren't"}, + {"shouldnt", "shouldn't"}, + {"wouldnt", "wouldn't"}, + {"couldnt", "couldn't"}, + {"thats", "that's"}, + {"whats", "what's"}, + {"hes", "he's"}, + {"shes", "she's"}, + {"its", "it's"}, + {"im", "I'm"}, + {"youve", "you've"}, + {"theyve", "they've"}, + {"weve", "we've"}, + {"ive", "I've"}, + {"recieve", "receive"}, + {"beleive", "believe"}, + {"neccessary", "necessary"}, + {"seperate", "separate"}, + {"definately", "definitely"}, + {"occured", "occurred"}, + {"begining", "beginning"}, + {"tommorrow", "tomorrow"}, + {"untill", "until"}, + {"alot", "a lot"}, + }) {} + +void AutoCorrect::setEnabled(bool enabled) { + if (m_enabled == enabled) { + return; + } + m_enabled = enabled; + emit enabledChanged(); +} + +QString AutoCorrect::correct(const QString &word) { + if (!m_enabled || word.isEmpty()) { + return word; + } + + const QString lowerWord = word.toLower(); + const auto typoIt = m_commonTypos.constFind(lowerWord); + if (typoIt != m_commonTypos.constEnd()) { + return typoIt.value(); + } + + if (!m_dictionary || m_dictionary->hasWord(lowerWord)) { + return word; + } + + const QStringList suggestions = m_dictionary->predict(lowerWord); + if (suggestions.isEmpty()) { + return word; + } + + const QString bestMatch = suggestions.first(); + const int distance = levenshteinDistance(lowerWord, bestMatch.toLower()); + if (distance <= 2) { + return bestMatch; + } + return word; +} + +QString AutoCorrect::shouldCorrect(const QString &word) { + if (!m_enabled || word.size() < 3) { + return {}; + } + const QString corrected = correct(word); + if (corrected != word) { + return corrected; + } + return {}; +} + +void AutoCorrect::learnCorrection(const QString &typo, const QString &correction) { + if (typo.isEmpty() || correction.isEmpty()) { + return; + } + m_commonTypos.insert(typo.toLower(), correction); +} + +int AutoCorrect::levenshteinDistance(const QString &first, const QString &second) const { + if (first.isEmpty()) { + return second.size(); + } + if (second.isEmpty()) { + return first.size(); + } + + const int rows = second.size() + 1; + const int cols = first.size() + 1; + QVector previous(cols); + QVector current(cols); + + for (int col = 0; col < cols; ++col) { + previous[col] = col; + } + + for (int row = 1; row < rows; ++row) { + current[0] = row; + for (int col = 1; col < cols; ++col) { + const bool same = second.at(row - 1) == first.at(col - 1); + const int cost = same ? 0 : 1; + const int insertion = current[col - 1] + 1; + const int deletion = previous[col] + 1; + const int replace = previous[col - 1] + cost; + current[col] = std::min({insertion, deletion, replace}); + } + std::swap(current, previous); + } + return previous[cols - 1]; +} diff --git a/shell/src/services/autocorrect.h b/shell/src/services/autocorrect.h new file mode 100644 index 000000000..d4797cc2b --- /dev/null +++ b/shell/src/services/autocorrect.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +class Dictionary; + +class AutoCorrect : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + + public: + explicit AutoCorrect(Dictionary *dictionary, QObject *parent = nullptr); + + bool enabled() const { + return m_enabled; + } + void setEnabled(bool enabled); + + Q_INVOKABLE QString correct(const QString &word); + Q_INVOKABLE QString shouldCorrect(const QString &word); + Q_INVOKABLE void learnCorrection(const QString &typo, const QString &correction); + + signals: + void enabledChanged(); + + private: + int levenshteinDistance(const QString &first, const QString &second) const; + + Dictionary *m_dictionary; + QHash m_commonTypos; + bool m_enabled = true; +}; diff --git a/shell/src/services/dictionary.cpp b/shell/src/services/dictionary.cpp new file mode 100644 index 000000000..b5ef61c0f --- /dev/null +++ b/shell/src/services/dictionary.cpp @@ -0,0 +1,93 @@ +#include "dictionary.h" + +#include "emojipredictor.h" +#include "phrasepredictor.h" +#include "qml/keyboard/Data/WordEngine.h" + +Dictionary::Dictionary(WordEngine *wordEngine, EmojiPredictor *emojiPredictor, + PhrasePredictor *phrasePredictor, QObject *parent) + : QObject(parent) + , m_wordEngine(wordEngine) + , m_emojiPredictor(emojiPredictor) + , m_phrasePredictor(phrasePredictor) { + if (m_wordEngine) { + connect(m_wordEngine, &WordEngine::predictionsReady, this, &Dictionary::onPredictionsReady); + } +} + +QStringList Dictionary::predict(const QString &prefix) { + if (prefix.isEmpty()) { + return {}; + } + if (!m_wordEngine || !m_wordEngine->enabled()) { + return {}; + } + if (m_lastPredictionPrefix != prefix) { + setLastPredictionPrefix(prefix); + setCachedPredictions({}); + m_wordEngine->requestPredictions(prefix, 5); + } + return m_cachedPredictions; +} + +QString Dictionary::getEmojiForWord(const QString &word) const { + if (!m_emojiPredictor || word.isEmpty()) { + return {}; + } + return m_emojiPredictor->getTopEmoji(word); +} + +QStringList Dictionary::getPhraseCompletions(const QString &context) const { + if (!m_phrasePredictor || context.isEmpty()) { + return {}; + } + return m_phrasePredictor->getPhrases(context); +} + +void Dictionary::learnWord(const QString &word) { + if (!m_wordEngine || !m_wordEngine->enabled()) { + return; + } + if (word.size() < 2) { + return; + } + m_wordEngine->learnWord(word); +} + +bool Dictionary::hasWord(const QString &word) const { + if (!m_wordEngine || !m_wordEngine->enabled()) { + return false; + } + if (word.isEmpty()) { + return false; + } + return m_wordEngine->hasWord(word); +} + +void Dictionary::setCachedPredictions(const QStringList &predictions) { + if (m_cachedPredictions == predictions) { + return; + } + m_cachedPredictions = predictions; + emit cachedPredictionsChanged(); +} + +void Dictionary::setLastPredictionPrefix(const QString &prefix) { + if (m_lastPredictionPrefix == prefix) { + return; + } + m_lastPredictionPrefix = prefix; + emit lastPredictionPrefixChanged(); +} + +void Dictionary::onPredictionsReady(const QString &prefix, const QStringList &predictions) { + if (prefix != m_lastPredictionPrefix) { + return; + } + QStringList enhanced = predictions; + const QString emoji = getEmojiForWord(prefix); + if (!emoji.isEmpty() && enhanced.size() < 5 && !enhanced.contains(emoji)) { + enhanced.append(emoji); + } + setCachedPredictions(enhanced); +} diff --git a/shell/src/services/dictionary.h b/shell/src/services/dictionary.h new file mode 100644 index 000000000..07f3ddb78 --- /dev/null +++ b/shell/src/services/dictionary.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +class WordEngine; +class EmojiPredictor; +class PhrasePredictor; + +class Dictionary : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QStringList cachedPredictions READ cachedPredictions NOTIFY cachedPredictionsChanged) + Q_PROPERTY( + QString lastPredictionPrefix READ lastPredictionPrefix NOTIFY lastPredictionPrefixChanged) + + public: + explicit Dictionary(WordEngine *wordEngine, EmojiPredictor *emojiPredictor, + PhrasePredictor *phrasePredictor, QObject *parent = nullptr); + + QStringList cachedPredictions() const { + return m_cachedPredictions; + } + QString lastPredictionPrefix() const { + return m_lastPredictionPrefix; + } + + Q_INVOKABLE QStringList predict(const QString &prefix); + Q_INVOKABLE QString getEmojiForWord(const QString &word) const; + Q_INVOKABLE QStringList getPhraseCompletions(const QString &context) const; + Q_INVOKABLE void learnWord(const QString &word); + Q_INVOKABLE bool hasWord(const QString &word) const; + + signals: + void cachedPredictionsChanged(); + void lastPredictionPrefixChanged(); + + private: + void setCachedPredictions(const QStringList &predictions); + void setLastPredictionPrefix(const QString &prefix); + void onPredictionsReady(const QString &prefix, const QStringList &predictions); + + WordEngine *m_wordEngine; + EmojiPredictor *m_emojiPredictor; + PhrasePredictor *m_phrasePredictor; + QStringList m_cachedPredictions; + QString m_lastPredictionPrefix; +}; diff --git a/shell/src/services/domainsuggestions.cpp b/shell/src/services/domainsuggestions.cpp new file mode 100644 index 000000000..0d2c8ac3e --- /dev/null +++ b/shell/src/services/domainsuggestions.cpp @@ -0,0 +1,79 @@ +#include "domainsuggestions.h" + +DomainSuggestions::DomainSuggestions(QObject *parent) + : QObject(parent) + , m_commonTlds({"com", "org", "net", "edu", "gov", "co.uk", "co", "io", "app", "dev", + "uk", "ca", "au", "de", "fr", "jp", "cn", "in", "br", "ru"}) + , m_emailDomains({"gmail.com", "outlook.com", "yahoo.com", "hotmail.com", "icloud.com", + "protonmail.com", "me.com", "live.com", "msn.com"}) {} + +QStringList DomainSuggestions::getSuggestions(const QString &text, bool isEmail) const { + if (text.isEmpty()) { + return {}; + } + + QStringList suggestions; + const QString lowerText = text.toLower(); + + if (isEmail) { + if (lowerText.contains("@")) { + const QStringList parts = lowerText.split("@"); + if (parts.size() == 2) { + const QString domain = parts[1]; + for (const QString &emailDomain : m_emailDomains) { + if (suggestions.size() >= 3) { + break; + } + if (emailDomain.startsWith(domain) && emailDomain != domain) { + suggestions.append(parts[0] + "@" + emailDomain); + } + } + } + } else if (lowerText.size() >= 2) { + for (const QString &emailDomain : m_emailDomains) { + if (suggestions.size() >= 3) { + break; + } + suggestions.append(lowerText + "@" + emailDomain); + } + } + return suggestions; + } + + const int lastDot = lowerText.lastIndexOf("."); + if (lastDot != -1) { + const QString afterDot = lowerText.mid(lastDot + 1); + const QString beforeDot = lowerText.left(lastDot + 1); + for (const QString &tld : m_commonTlds) { + if (suggestions.size() >= 3) { + break; + } + if (tld.startsWith(afterDot) && tld != afterDot) { + suggestions.append(beforeDot + tld); + } + } + } else if (lowerText.size() >= 2 && !lowerText.contains("://")) { + for (int i = 0; i < m_commonTlds.size() && suggestions.size() < 3; ++i) { + suggestions.append(lowerText + "." + m_commonTlds[i]); + } + } + + return suggestions; +} + +bool DomainSuggestions::shouldShowDomainSuggestions(const QString &text, bool isEmail, + bool isUrl) const { + if (text.isEmpty()) { + return false; + } + + if (isEmail) { + return text.contains("@") || text.size() >= 2; + } + + if (isUrl) { + return text.contains(".") || (text.size() >= 2 && !text.contains("://")); + } + + return false; +} diff --git a/shell/src/services/domainsuggestions.h b/shell/src/services/domainsuggestions.h new file mode 100644 index 000000000..6924ebf3c --- /dev/null +++ b/shell/src/services/domainsuggestions.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +class DomainSuggestions : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + public: + explicit DomainSuggestions(QObject *parent = nullptr); + + Q_INVOKABLE QStringList getSuggestions(const QString &text, bool isEmail) const; + Q_INVOKABLE bool shouldShowDomainSuggestions(const QString &text, bool isEmail, + bool isUrl) const; + + private: + QStringList m_commonTlds; + QStringList m_emailDomains; +}; diff --git a/shell/src/services/emojipredictor.cpp b/shell/src/services/emojipredictor.cpp new file mode 100644 index 000000000..c85188a3f --- /dev/null +++ b/shell/src/services/emojipredictor.cpp @@ -0,0 +1,88 @@ +#include "emojipredictor.h" + +EmojiPredictor::EmojiPredictor(QObject *parent) + : QObject(parent) { + m_emojiMap.insert("happy", {"😀", "😊", "😄"}); + m_emojiMap.insert("sad", {"😢", "😞", "😔"}); + m_emojiMap.insert("love", {"❤️", "😍", "💕"}); + m_emojiMap.insert("heart", {"❤️", "💖", "💗"}); + m_emojiMap.insert("laugh", {"😂", "🤣", "😆"}); + m_emojiMap.insert("lol", {"😂", "🤣", "😄"}); + m_emojiMap.insert("cry", {"😢", "😭", "😿"}); + m_emojiMap.insert("angry", {"😠", "😡", "🤬"}); + m_emojiMap.insert("cool", {"😎", "🆒", "👍"}); + m_emojiMap.insert("great", {"👍", "🙌", "✨"}); + m_emojiMap.insert("thanks", {"🙏", "👍", "❤️"}); + m_emojiMap.insert("thank", {"🙏", "👍", "❤️"}); + m_emojiMap.insert("yes", {"👍", "✅", "👌"}); + m_emojiMap.insert("no", {"👎", "❌", "🙅"}); + m_emojiMap.insert("ok", {"👌", "👍", "✅"}); + m_emojiMap.insert("good", {"👍", "👌", "🙌"}); + m_emojiMap.insert("bad", {"👎", "😕", "❌"}); + m_emojiMap.insert("fire", {"🔥", "💥", "⚡"}); + m_emojiMap.insert("party", {"🎉", "🥳", "🎊"}); + m_emojiMap.insert("birthday", {"🎂", "🎉", "🎁"}); + m_emojiMap.insert("food", {"🍕", "🍔", "🍟"}); + m_emojiMap.insert("pizza", {"🍕", "🧀", "🍴"}); + m_emojiMap.insert("coffee", {"☕", "🫖", "🍵"}); + m_emojiMap.insert("beer", {"🍺", "🍻", "🥂"}); + m_emojiMap.insert("sun", {"☀️", "🌞", "🌅"}); + m_emojiMap.insert("rain", {"🌧️", "☔", "💧"}); + m_emojiMap.insert("snow", {"❄️", "☃️", "🌨️"}); + m_emojiMap.insert("hot", {"🔥", "🥵", "☀️"}); + m_emojiMap.insert("cold", {"🥶", "❄️", "🧊"}); + m_emojiMap.insert("music", {"🎵", "🎶", "🎸"}); + m_emojiMap.insert("phone", {"📱", "☎️", "📞"}); + m_emojiMap.insert("home", {"🏠", "🏡", "🏘️"}); + m_emojiMap.insert("work", {"💼", "🏢", "👔"}); + m_emojiMap.insert("sleep", {"😴", "💤", "🛏️"}); + m_emojiMap.insert("tired", {"😴", "😩", "🥱"}); + m_emojiMap.insert("think", {"🤔", "💭", "🧠"}); + m_emojiMap.insert("idea", {"💡", "🧠", "✨"}); + m_emojiMap.insert("money", {"💰", "💵", "🤑"}); + m_emojiMap.insert("time", {"⏰", "🕐", "⌚"}); + m_emojiMap.insert("star", {"⭐", "🌟", "✨"}); + m_emojiMap.insert("check", {"✅", "☑️", "✔️"}); + m_emojiMap.insert("question", {"❓", "🤔", "❔"}); + m_emojiMap.insert("warning", {"⚠️", "🚨", "⛔"}); + m_emojiMap.insert("congrats", {"🎉", "👏", "🥳"}); + m_emojiMap.insert("sorry", {"😔", "🙏", "😢"}); + m_emojiMap.insert("please", {"🙏", "🥺", "😊"}); + m_emojiMap.insert("hello", {"👋", "😊", "🙋"}); + m_emojiMap.insert("hi", {"👋", "😊", "🙋"}); + m_emojiMap.insert("bye", {"👋", "😢", "🙋"}); + m_emojiMap.insert("goodbye", {"👋", "😢", "🙋"}); + m_emojiMap.insert("wow", {"😮", "🤩", "😲"}); + m_emojiMap.insert("omg", {"😱", "😮", "🙀"}); + m_emojiMap.insert("haha", {"😂", "🤣", "😆"}); + m_emojiMap.insert("nice", {"👍", "👌", "🙌"}); + m_emojiMap.insert("cute", {"🥰", "😊", "💕"}); + m_emojiMap.insert("beautiful", {"😍", "✨", "💖"}); + m_emojiMap.insert("amazing", {"🤩", "😍", "✨"}); + m_emojiMap.insert("awesome", {"🔥", "🙌", "✨"}); + m_emojiMap.insert("perfect", {"💯", "✨", "👌"}); + m_emojiMap.insert("run", {"🏃", "🏃‍♂️", "🏃‍♀️"}); + m_emojiMap.insert("walk", {"🚶", "🚶‍♂️", "🚶‍♀️"}); + m_emojiMap.insert("car", {"🚗", "🚙", "🏎️"}); + m_emojiMap.insert("plane", {"✈️", "🛫", "🛬"}); + m_emojiMap.insert("dog", {"🐕", "🐶", "🦮"}); + m_emojiMap.insert("cat", {"🐱", "🐈", "😺"}); + m_emojiMap.insert("book", {"📖", "📚", "📕"}); + m_emojiMap.insert("movie", {"🎬", "🎥", "📽️"}); + m_emojiMap.insert("game", {"🎮", "🕹️", "🎯"}); + m_emojiMap.insert("win", {"🏆", "🥇", "🎉"}); + m_emojiMap.insert("lose", {"😞", "😔", "👎"}); +} + +QStringList EmojiPredictor::getEmojis(const QString &word) const { + const QString lowerWord = word.toLower(); + if (m_emojiMap.contains(lowerWord)) { + return m_emojiMap.value(lowerWord); + } + return {}; +} + +QString EmojiPredictor::getTopEmoji(const QString &word) const { + const QStringList emojis = getEmojis(word); + return emojis.isEmpty() ? QString() : emojis.first(); +} diff --git a/shell/src/services/emojipredictor.h b/shell/src/services/emojipredictor.h new file mode 100644 index 000000000..3ea842be8 --- /dev/null +++ b/shell/src/services/emojipredictor.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +class EmojiPredictor : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + public: + explicit EmojiPredictor(QObject *parent = nullptr); + + Q_INVOKABLE QStringList getEmojis(const QString &word) const; + Q_INVOKABLE QString getTopEmoji(const QString &word) const; + + private: + QHash m_emojiMap; +}; diff --git a/shell/src/services/languagemanager.cpp b/shell/src/services/languagemanager.cpp new file mode 100644 index 000000000..23b9e910b --- /dev/null +++ b/shell/src/services/languagemanager.cpp @@ -0,0 +1,182 @@ +#include "languagemanager.h" + +#include +#include +#include + +#include "qml/keyboard/Data/WordEngine.h" +#include "src/managers/settingsmanager.h" + +LanguageManager::LanguageManager(SettingsManager *settingsManager, WordEngine *wordEngine, + QObject *parent) + : QObject(parent) + , m_settingsManager(settingsManager) + , m_wordEngine(wordEngine) { + m_languageFlags = {{"en_US", "🇺🇸"}, {"fr_FR", "🇫🇷"}, {"de_DE", "🇩🇪"}, {"es_ES", "🇪🇸"}, + {"pt_BR", "🇧🇷"}, {"it_IT", "🇮🇹"}, {"ru_RU", "🇷🇺"}, {"ar_SA", "🇸🇦"}, + {"zh_CN", "🇨🇳"}, {"ja_JP", "🇯🇵"}}; + + if (m_settingsManager) { + connect(m_settingsManager, &SettingsManager::keyboardLanguageChanged, this, + &LanguageManager::onSettingsLanguageChanged); + } + + initialize(); +} + +void LanguageManager::initialize() { + m_availableLanguages = {"en_US", "fr_FR", "de_DE", "es_ES", "pt_BR", + "it_IT", "ru_RU", "ar_SA", "zh_CN", "ja_JP"}; + emit availableLanguagesChanged(); + + const QString savedLanguage = + m_settingsManager ? m_settingsManager->keyboardLanguage() : QString(); + const QString languageToLoad = + savedLanguage.isEmpty() ? QStringLiteral("en_US") : savedLanguage; + loadLanguage(languageToLoad); +} + +void LanguageManager::loadLanguage(const QString &languageId) { + if (languageId.isEmpty()) { + loadFallback(); + return; + } + + const QVariantMap layout = parseLayoutFile(languageId); + if (layout.isEmpty()) { + loadFallback(); + return; + } + + applyLayout(layout, languageId); +} + +void LanguageManager::nextLanguage() { + if (m_availableLanguages.isEmpty()) { + return; + } + const int currentIndex = m_availableLanguages.indexOf(m_currentLanguage); + const int nextIndex = (currentIndex + 1) % m_availableLanguages.size(); + loadLanguage(m_availableLanguages.at(nextIndex)); +} + +QString LanguageManager::getFlag(const QString &languageId) const { + const QVariant flag = m_languageFlags.value(languageId); + return flag.isValid() ? flag.toString() : QStringLiteral("🌐"); +} + +QString LanguageManager::getName(const QString &languageId) const { + if (m_currentLayout.value("id").toString() != languageId) { + return languageId; + } + const QString name = m_currentLayout.value("name").toString(); + return name.isEmpty() ? languageId : name; +} + +void LanguageManager::applyLayout(const QVariantMap &layout, const QString &languageId) { + setCurrentLayout(layout); + setCurrentLanguage(languageId); + updateWordEngine(layout, languageId); + + if (m_settingsManager && m_settingsManager->keyboardLanguage() != languageId) { + m_settingsManager->setKeyboardLanguage(languageId); + } + + emit languageChanged(languageId); + emit layoutLoaded(layout); +} + +void LanguageManager::loadFallback() { + applyLayout(fallbackLayout(), QStringLiteral("en_US")); +} + +QVariantMap LanguageManager::parseLayoutFile(const QString &languageId) const { + const QString path = + QStringLiteral(":/qt/qml/MarathonOS/Shell/qml/keyboard/Layouts/languages/%1.json") + .arg(languageId); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + return {}; + } + const QByteArray data = file.readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(data); + if (!doc.isObject()) { + return {}; + } + return doc.object().toVariantMap(); +} + +QVariantMap LanguageManager::fallbackLayout() const { + return { + {"id", "en_US"}, + {"name", "English (US)"}, + {"dictionary", "en_US"}, + {"rtl", false}, + {"rows", + QVariantList{ + QVariantMap{{"keys", + QVariantList{QVariantMap{{"char", "q"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "w"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "e"}, + {"alts", QVariantList{"è", "é", "ê", "ë", "ē"}}}, + QVariantMap{{"char", "r"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "t"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "y"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "u"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "i"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "o"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "p"}, {"alts", QVariantList{}}}}}}, + QVariantMap{{"keys", + QVariantList{QVariantMap{{"char", "a"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "s"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "d"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "f"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "g"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "h"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "j"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "k"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "l"}, {"alts", QVariantList{}}}}}}, + QVariantMap{{"keys", + QVariantList{QVariantMap{{"char", "z"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "x"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "c"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "v"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "b"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "n"}, {"alts", QVariantList{}}}, + QVariantMap{{"char", "m"}, {"alts", QVariantList{}}}}}}}}}; +} + +void LanguageManager::setCurrentLanguage(const QString &languageId) { + if (m_currentLanguage == languageId) { + return; + } + m_currentLanguage = languageId; + emit currentLanguageChanged(); +} + +void LanguageManager::setCurrentLayout(const QVariantMap &layout) { + if (m_currentLayout == layout) { + return; + } + m_currentLayout = layout; + emit currentLayoutChanged(); +} + +void LanguageManager::updateWordEngine(const QVariantMap &layout, const QString &languageId) { + if (!m_wordEngine) { + return; + } + const QString dictionary = layout.value("dictionary").toString(); + m_wordEngine->setLanguage(dictionary.isEmpty() ? languageId : dictionary); +} + +void LanguageManager::onSettingsLanguageChanged() { + if (!m_settingsManager) { + return; + } + const QString languageId = m_settingsManager->keyboardLanguage(); + if (languageId.isEmpty() || languageId == m_currentLanguage) { + return; + } + loadLanguage(languageId); +} diff --git a/shell/src/services/languagemanager.h b/shell/src/services/languagemanager.h new file mode 100644 index 000000000..f89f7a6af --- /dev/null +++ b/shell/src/services/languagemanager.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include + +class SettingsManager; +class WordEngine; + +class LanguageManager : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString currentLanguage READ currentLanguage NOTIFY currentLanguageChanged) + Q_PROPERTY(QVariantMap currentLayout READ currentLayout NOTIFY currentLayoutChanged) + Q_PROPERTY( + QStringList availableLanguages READ availableLanguages NOTIFY availableLanguagesChanged) + Q_PROPERTY(QVariantMap languageFlags READ languageFlags CONSTANT) + + public: + explicit LanguageManager(SettingsManager *settingsManager, WordEngine *wordEngine, + QObject *parent = nullptr); + + QString currentLanguage() const { + return m_currentLanguage; + } + QVariantMap currentLayout() const { + return m_currentLayout; + } + QStringList availableLanguages() const { + return m_availableLanguages; + } + QVariantMap languageFlags() const { + return m_languageFlags; + } + + Q_INVOKABLE void initialize(); + Q_INVOKABLE void loadLanguage(const QString &languageId); + Q_INVOKABLE void nextLanguage(); + Q_INVOKABLE QString getFlag(const QString &languageId) const; + Q_INVOKABLE QString getName(const QString &languageId) const; + + signals: + void currentLanguageChanged(); + void currentLayoutChanged(); + void availableLanguagesChanged(); + void languageChanged(const QString &languageId); + void layoutLoaded(const QVariantMap &layout); + + private: + void applyLayout(const QVariantMap &layout, const QString &languageId); + void loadFallback(); + QVariantMap parseLayoutFile(const QString &languageId) const; + QVariantMap fallbackLayout() const; + void setCurrentLanguage(const QString &languageId); + void setCurrentLayout(const QVariantMap &layout); + void updateWordEngine(const QVariantMap &layout, const QString &languageId); + void onSettingsLanguageChanged(); + + QPointer m_settingsManager; + QPointer m_wordEngine; + QString m_currentLanguage; + QVariantMap m_currentLayout; + QStringList m_availableLanguages; + QVariantMap m_languageFlags; +}; diff --git a/shell/src/services/phrasepredictor.cpp b/shell/src/services/phrasepredictor.cpp new file mode 100644 index 000000000..336ea5094 --- /dev/null +++ b/shell/src/services/phrasepredictor.cpp @@ -0,0 +1,81 @@ +#include "phrasepredictor.h" + +PhrasePredictor::PhrasePredictor(QObject *parent) + : QObject(parent) { + m_phraseMap.insert("how are", {"you", "you doing", "things"}); + m_phraseMap.insert("how is", {"it going", "everything", "your day"}); + m_phraseMap.insert("what are", {"you doing", "you up to", "your plans"}); + m_phraseMap.insert("what is", {"up", "going on", "your name"}); + m_phraseMap.insert("where are", {"you", "we going", "we meeting"}); + m_phraseMap.insert("where is", {"it", "that", "everyone"}); + m_phraseMap.insert("when are", {"you coming", "we meeting", "you free"}); + m_phraseMap.insert("when is", {"it", "the meeting", "your birthday"}); + m_phraseMap.insert("i am", {"here", "coming", "on my way", "good", "fine"}); + m_phraseMap.insert("i will", {"be there", "call you", "let you know", "do it"}); + m_phraseMap.insert("i have", {"to go", "a question", "an idea", "time"}); + m_phraseMap.insert("i think", {"so", "that", "it's good", "we should"}); + m_phraseMap.insert("i need", {"to", "help", "more time", "your help"}); + m_phraseMap.insert("i want", {"to", "that", "it", "one"}); + m_phraseMap.insert("i hope", {"so", "you're well", "it works", "to see you"}); + m_phraseMap.insert("i love", {"you", "it", "this", "that"}); + m_phraseMap.insert("i like", {"it", "that", "this", "your idea"}); + m_phraseMap.insert("can you", {"help", "do it", "send me", "call me"}); + m_phraseMap.insert("can i", {"help", "ask you", "call you", "come"}); + m_phraseMap.insert("do you", {"know", "have", "want", "think"}); + m_phraseMap.insert("did you", {"get it", "see that", "hear", "know"}); + m_phraseMap.insert("are you", {"there", "coming", "ok", "sure", "free"}); + m_phraseMap.insert("is it", {"ok", "possible", "working", "done"}); + m_phraseMap.insert("let me", {"know", "think", "check", "see"}); + m_phraseMap.insert("thank you", {"so much", "very much", "for everything"}); + m_phraseMap.insert("thanks for", {"your help", "letting me know", "everything"}); + m_phraseMap.insert("looking forward", {"to it", "to hearing", "to seeing you"}); + m_phraseMap.insert("nice to", {"meet you", "see you", "hear from you"}); + m_phraseMap.insert("have a", {"good day", "nice day", "great day", "good one"}); + m_phraseMap.insert("see you", {"later", "soon", "tomorrow", "there"}); + m_phraseMap.insert("talk to", {"you later", "you soon", "me"}); + m_phraseMap.insert("on my", {"way", "phone", "computer", "mind"}); + m_phraseMap.insert("be right", {"back", "there", "over"}); + m_phraseMap.insert("going to", {"be", "do", "the", "try"}); + m_phraseMap.insert("want to", {"go", "do", "see", "try", "know"}); + m_phraseMap.insert("need to", {"go", "do", "talk", "know", "finish"}); + m_phraseMap.insert("have to", {"go", "do", "leave", "work"}); + m_phraseMap.insert("got to", {"go", "run", "do"}); + m_phraseMap.insert("good to", {"know", "see you", "hear"}); + m_phraseMap.insert("great to", {"see you", "hear", "meet you"}); + m_phraseMap.insert("happy to", {"help", "hear", "see you", "know"}); + m_phraseMap.insert("sorry for", {"the delay", "the wait", "bothering you"}); + m_phraseMap.insert("sorry i", {"missed", "forgot", "can't", "didn't"}); + m_phraseMap.insert("no problem", {"at all", "happy to help"}); + m_phraseMap.insert("sounds good", {"to me", "let's do it"}); + m_phraseMap.insert("that sounds", {"good", "great", "perfect", "fun"}); + m_phraseMap.insert("that's a", {"good idea", "great idea", "good point"}); + m_phraseMap.insert("that's great", {"news", "to hear"}); + m_phraseMap.insert("the best", {"way", "thing", "option", "part"}); + m_phraseMap.insert("a lot", {"of", "more", "better"}); + m_phraseMap.insert("of course", {"not", "I will", "you can"}); + m_phraseMap.insert("by the", {"way", "time", "end"}); + m_phraseMap.insert("at the", {"moment", "end", "same time"}); + m_phraseMap.insert("in the", {"morning", "afternoon", "evening", "end"}); + m_phraseMap.insert("on the", {"way", "phone", "other hand"}); +} + +QStringList PhrasePredictor::getPhrases(const QString &words) const { + const QString key = words.toLower().trimmed(); + if (m_phraseMap.contains(key)) { + return m_phraseMap.value(key); + } + + const QStringList parts = key.split(" ", Qt::SkipEmptyParts); + if (parts.size() >= 2) { + const QString lastTwo = parts.mid(parts.size() - 2).join(" "); + if (m_phraseMap.contains(lastTwo)) { + return m_phraseMap.value(lastTwo); + } + } + return {}; +} + +QString PhrasePredictor::getTopPhrase(const QString &words) const { + const QStringList phrases = getPhrases(words); + return phrases.isEmpty() ? QString() : phrases.first(); +} diff --git a/shell/src/services/phrasepredictor.h b/shell/src/services/phrasepredictor.h new file mode 100644 index 000000000..b6a4fc055 --- /dev/null +++ b/shell/src/services/phrasepredictor.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +class PhrasePredictor : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + public: + explicit PhrasePredictor(QObject *parent = nullptr); + + Q_INVOKABLE QStringList getPhrases(const QString &words) const; + Q_INVOKABLE QString getTopPhrase(const QString &words) const; + + private: + QHash m_phraseMap; +}; diff --git a/shell/src/services/router.cpp b/shell/src/services/router.cpp new file mode 100644 index 000000000..65b32e6a9 --- /dev/null +++ b/shell/src/services/router.cpp @@ -0,0 +1,91 @@ +#include "router.h" + +#include +#include + +Router::Router(QObject *parent) + : QObject(parent) {} + +void Router::goToHub() { + logNavigation("current", "Hub", "direct"); + setPreviousPageIndex(m_currentPageIndex); + setCurrentPageIndex(m_pageHub); + emit navigateToHub(); +} + +void Router::goToFrames() { + logNavigation("current", "ActiveFrames", "direct"); + setPreviousPageIndex(m_currentPageIndex); + setCurrentPageIndex(m_pageFrames); + emit navigateToFrames(); +} + +void Router::goToAppPage(int page) { + logNavigation("current", QString("AppPage%1").arg(page), "direct"); + setPreviousPageIndex(m_currentPageIndex); + setCurrentPageIndex(m_pageApps + page); + emit navigateToApps(page); +} + +void Router::goBack() { + logNavigation("current", "previous", "back"); + const int previous = m_previousPageIndex; + setPreviousPageIndex(m_currentPageIndex); + setCurrentPageIndex(previous); + emit navigateBack(); +} + +void Router::navigateLeft() { + if (m_currentPageIndex >= 10) { + return; + } + const int previous = m_currentPageIndex; + setPreviousPageIndex(previous); + setCurrentPageIndex(previous + 1); + logNavigation(QString("page%1").arg(previous), QString("page%1").arg(m_currentPageIndex), + "swipeLeft"); +} + +void Router::navigateRight() { + if (m_currentPageIndex <= 0) { + return; + } + const int previous = m_currentPageIndex; + setPreviousPageIndex(previous); + setCurrentPageIndex(previous - 1); + logNavigation(QString("page%1").arg(previous), QString("page%1").arg(m_currentPageIndex), + "swipeRight"); +} + +int Router::getCurrentPage() const { + return m_currentPageIndex - m_pageApps; +} + +void Router::navigateToSetting(const QString &settingId) { + logNavigation("current", QString("Setting:%1").arg(settingId), "search"); + emit navigateToSettingPage(settingId); +} + +void Router::setCurrentPageIndex(int index) { + if (m_currentPageIndex == index) { + return; + } + m_currentPageIndex = index; + emit currentPageIndexChanged(); +} + +void Router::setPreviousPageIndex(int index) { + if (m_previousPageIndex == index) { + return; + } + m_previousPageIndex = index; + emit previousPageIndexChanged(); +} + +void Router::logNavigation(const QString &from, const QString &to, const QString &method) { + const QByteArray debugEnv = qgetenv("MARATHON_DEBUG").trimmed(); + if (!(debugEnv == "1" || debugEnv == "true")) { + return; + } + qDebug().noquote() << "[NAV]" << from << "→" << to << "(" + method + ")"; +} diff --git a/shell/src/services/router.h b/shell/src/services/router.h new file mode 100644 index 000000000..6ca8a2d27 --- /dev/null +++ b/shell/src/services/router.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include + +class Router : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(int currentPageIndex READ currentPageIndex NOTIFY currentPageIndexChanged) + Q_PROPERTY(int previousPageIndex READ previousPageIndex NOTIFY previousPageIndexChanged) + Q_PROPERTY(int pageHub READ pageHub CONSTANT) + Q_PROPERTY(int pageFrames READ pageFrames CONSTANT) + Q_PROPERTY(int pageApps READ pageApps CONSTANT) + + public: + explicit Router(QObject *parent = nullptr); + + int currentPageIndex() const { + return m_currentPageIndex; + } + int previousPageIndex() const { + return m_previousPageIndex; + } + int pageHub() const { + return m_pageHub; + } + int pageFrames() const { + return m_pageFrames; + } + int pageApps() const { + return m_pageApps; + } + + Q_INVOKABLE void goToHub(); + Q_INVOKABLE void goToFrames(); + Q_INVOKABLE void goToAppPage(int page); + Q_INVOKABLE void goBack(); + Q_INVOKABLE void navigateLeft(); + Q_INVOKABLE void navigateRight(); + Q_INVOKABLE int getCurrentPage() const; + Q_INVOKABLE void navigateToSetting(const QString &settingId); + + signals: + void currentPageIndexChanged(); + void previousPageIndexChanged(); + void navigateToHub(); + void navigateToFrames(); + void navigateToApps(int appPage); + void navigateBack(); + void navigateNext(); + void navigateToSettingPage(const QString &settingId); + + private: + void setCurrentPageIndex(int index); + void setPreviousPageIndex(int index); + void logNavigation(const QString &from, const QString &to, const QString &method); + + const int m_pageHub = 0; + const int m_pageFrames = 1; + const int m_pageApps = 2; + int m_currentPageIndex = 2; + int m_previousPageIndex = 2; +}; diff --git a/shell/src/services/surfaceregistry.cpp b/shell/src/services/surfaceregistry.cpp new file mode 100644 index 000000000..e630729c0 --- /dev/null +++ b/shell/src/services/surfaceregistry.cpp @@ -0,0 +1,37 @@ +#include "surfaceregistry.h" + +#include +#include + +SurfaceRegistry::SurfaceRegistry(QObject *parent) + : QObject(parent) {} + +void SurfaceRegistry::registerSurface(int surfaceId, QObject *item) { + if (surfaceId == -1 || !item) { + return; + } + m_surfaceMap.insert(surfaceId, item); + logMessage(QString("Registered surface: %1").arg(surfaceId)); +} + +void SurfaceRegistry::unregisterSurface(int surfaceId) { + if (m_surfaceMap.remove(surfaceId) > 0) { + logMessage(QString("Unregistered surface: %1").arg(surfaceId)); + } +} + +QObject *SurfaceRegistry::getSurfaceItem(int surfaceId) const { + const auto it = m_surfaceMap.constFind(surfaceId); + if (it == m_surfaceMap.constEnd()) { + return nullptr; + } + return it.value(); +} + +void SurfaceRegistry::logMessage(const QString &message) const { + const QByteArray debugEnv = qgetenv("MARATHON_DEBUG").trimmed(); + if (!(debugEnv == "1" || debugEnv == "true")) { + return; + } + qDebug().noquote() << "[INFO]" << "SurfaceRegistry:" << message; +} diff --git a/shell/src/services/surfaceregistry.h b/shell/src/services/surfaceregistry.h new file mode 100644 index 000000000..3f69172b6 --- /dev/null +++ b/shell/src/services/surfaceregistry.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +class SurfaceRegistry : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + public: + explicit SurfaceRegistry(QObject *parent = nullptr); + + Q_INVOKABLE void registerSurface(int surfaceId, QObject *item); + Q_INVOKABLE void unregisterSurface(int surfaceId); + Q_INVOKABLE QObject *getSurfaceItem(int surfaceId) const; + + private: + void logMessage(const QString &message) const; + + QHash> m_surfaceMap; +}; From fe6d69b2f7e4545351c67c644e4ebe070ad983a0 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 14:20:39 +0000 Subject: [PATCH 19/28] feat(settings): move keyboard settings store to C++ Replace the QML keyboard settings singleton with a C++ store and wire the settings page to use it directly. --- apps/settings/CMakeLists.txt | 2 + apps/settings/pages/KeyboardPage.qml | 56 ++---- apps/settings/src/KeyboardSettingsStore.cpp | 184 ++++++++++++++++++++ apps/settings/src/KeyboardSettingsStore.h | 92 ++++++++++ apps/settings/src/KeyboardSettingsStore.qml | 59 ------- 5 files changed, 292 insertions(+), 101 deletions(-) create mode 100644 apps/settings/src/KeyboardSettingsStore.cpp create mode 100644 apps/settings/src/KeyboardSettingsStore.h delete mode 100644 apps/settings/src/KeyboardSettingsStore.qml diff --git a/apps/settings/CMakeLists.txt b/apps/settings/CMakeLists.txt index e921f67fd..2aa4dc71f 100644 --- a/apps/settings/CMakeLists.txt +++ b/apps/settings/CMakeLists.txt @@ -9,6 +9,8 @@ file(GLOB_RECURSE QML_FILES set(SOURCES src/StorageInfo.cpp src/StorageInfo.h + src/KeyboardSettingsStore.cpp + src/KeyboardSettingsStore.h ) add_marathon_app(${APP_NAME} diff --git a/apps/settings/pages/KeyboardPage.qml b/apps/settings/pages/KeyboardPage.qml index 9eee9b832..81a827aa7 100644 --- a/apps/settings/pages/KeyboardPage.qml +++ b/apps/settings/pages/KeyboardPage.qml @@ -10,36 +10,8 @@ Page { id: keyboardPage property string pageName: "keyboard" - property var languageNames: ({ - "en_US": "English (US)", - "fr_FR": "Français", - "de_DE": "Deutsch", - "es_ES": "Español", - "pt_BR": "Português", - "it_IT": "Italiano", - "ru_RU": "Русский", - "ar_SA": "العربية", - "zh_CN": "中文", - "ja_JP": "日本語" - }) - signal navigateBack - function keyboardValue(key, fallbackValue) { - if (KeyboardSettingsStore && KeyboardSettingsStore[key] !== undefined) - return KeyboardSettingsStore[key]; - return fallbackValue; - } - - function setKeyboardValue(key, value) { - if (KeyboardSettingsStore && KeyboardSettingsStore[key] !== undefined) - KeyboardSettingsStore[key] = value; - } - - function languageNameFor(languageId) { - return languageNames[languageId] || languageId; - } - Component.onCompleted: { Logger.info("KeyboardPage", "Initialized"); } @@ -90,15 +62,15 @@ Page { width: parent.width - 48 Repeater { - model: keyboardValue("availableLanguages", []) + model: KeyboardSettingsStore.availableLanguages MSettingsListItem { - title: languageNameFor(modelData) + title: KeyboardSettingsStore.getLanguageName(modelData) subtitle: modelData - iconName: keyboardValue("currentLanguage", "") === modelData ? "check" : "" + iconName: KeyboardSettingsStore.currentLanguage === modelData ? "check" : "" showChevron: false onSettingClicked: { - setKeyboardValue("currentLanguage", modelData); + KeyboardSettingsStore.currentLanguage = modelData; } Rectangle { @@ -108,8 +80,8 @@ Page { width: 32 height: 32 radius: 16 - color: keyboardValue("currentLanguage", "") === modelData ? MColors.accentBright : "transparent" - visible: keyboardValue("currentLanguage", "") === modelData + color: KeyboardSettingsStore.currentLanguage === modelData ? MColors.accentBright : "transparent" + visible: KeyboardSettingsStore.currentLanguage === modelData Text { anchors.centerIn: parent @@ -133,9 +105,9 @@ Page { subtitle: "Automatically correct misspelled words" iconName: "spell-check" showToggle: true - toggleValue: keyboardValue("autoCorrectEnabled", false) + toggleValue: KeyboardSettingsStore.autoCorrectEnabled onToggleChanged: value => { - setKeyboardValue("autoCorrectEnabled", value); + KeyboardSettingsStore.autoCorrectEnabled = value; } } @@ -144,9 +116,9 @@ Page { subtitle: "Capitalize first letter of sentences" iconName: "arrow-up" showToggle: true - toggleValue: keyboardValue("autoCapitalizeEnabled", false) + toggleValue: KeyboardSettingsStore.autoCapitalizeEnabled onToggleChanged: value => { - setKeyboardValue("autoCapitalizeEnabled", value); + KeyboardSettingsStore.autoCapitalizeEnabled = value; } } @@ -155,9 +127,9 @@ Page { subtitle: "Vibrate on key press" iconName: "vibrate" showToggle: true - toggleValue: keyboardValue("hapticFeedbackEnabled", false) + toggleValue: KeyboardSettingsStore.hapticFeedbackEnabled onToggleChanged: value => { - setKeyboardValue("hapticFeedbackEnabled", value); + KeyboardSettingsStore.hapticFeedbackEnabled = value; } } @@ -166,9 +138,9 @@ Page { subtitle: "Show word suggestions while typing" iconName: "message-square" showToggle: true - toggleValue: keyboardValue("predictionsEnabled", false) + toggleValue: KeyboardSettingsStore.predictionsEnabled onToggleChanged: value => { - setKeyboardValue("predictionsEnabled", value); + KeyboardSettingsStore.predictionsEnabled = value; } } } diff --git a/apps/settings/src/KeyboardSettingsStore.cpp b/apps/settings/src/KeyboardSettingsStore.cpp new file mode 100644 index 000000000..74e130439 --- /dev/null +++ b/apps/settings/src/KeyboardSettingsStore.cpp @@ -0,0 +1,184 @@ +#include "KeyboardSettingsStore.h" + +#include +#include +#include +#include +#include + +KeyboardSettingsStore::KeyboardSettingsStore(QObject *parent) + : QObject(parent) + , m_availableLanguages({"en_US", "fr_FR", "de_DE", "es_ES", "pt_BR", "it_IT", "ru_RU", "ar_SA", + "zh_CN", "ja_JP"}) + , m_languageNames({{"en_US", "English (US)"}, + {"fr_FR", "Français"}, + {"de_DE", "Deutsch"}, + {"es_ES", "Español"}, + {"pt_BR", "Português"}, + {"it_IT", "Italiano"}, + {"ru_RU", "Русский"}, + {"ar_SA", "العربية"}, + {"zh_CN", "中文"}, + {"ja_JP", "日本語"}}) + , m_languageFlags({{"en_US", "🇺🇸"}, + {"fr_FR", "🇫🇷"}, + {"de_DE", "🇩🇪"}, + {"es_ES", "🇪🇸"}, + {"pt_BR", "🇧🇷"}, + {"it_IT", "🇮🇹"}, + {"ru_RU", "🇷🇺"}, + {"ar_SA", "🇸🇦"}, + {"zh_CN", "🇨🇳"}, + {"ja_JP", "🇯🇵"}}) { + QTimer::singleShot(0, this, &KeyboardSettingsStore::resolveSettingsManager); +} + +void KeyboardSettingsStore::setCurrentLanguage(const QString &languageId) { + if (m_currentLanguage == languageId) { + return; + } + m_currentLanguage = languageId; + emit currentLanguageChanged(); + writeSettingValue("keyboardLanguage", languageId); +} + +void KeyboardSettingsStore::setAutoCorrectEnabled(bool enabled) { + if (m_autoCorrectEnabled == enabled) { + return; + } + m_autoCorrectEnabled = enabled; + emit autoCorrectEnabledChanged(); + writeSettingValue("keyboardAutoCorrection", enabled); +} + +void KeyboardSettingsStore::setAutoCapitalizeEnabled(bool enabled) { + if (m_autoCapitalizeEnabled == enabled) { + return; + } + m_autoCapitalizeEnabled = enabled; + emit autoCapitalizeEnabledChanged(); +} + +void KeyboardSettingsStore::setHapticFeedbackEnabled(bool enabled) { + if (m_hapticFeedbackEnabled == enabled) { + return; + } + m_hapticFeedbackEnabled = enabled; + emit hapticFeedbackEnabledChanged(); + writeSettingValue("keyboardHapticStrength", + enabled ? QStringLiteral("medium") : QStringLiteral("off")); +} + +void KeyboardSettingsStore::setPredictionsEnabled(bool enabled) { + if (m_predictionsEnabled == enabled) { + return; + } + m_predictionsEnabled = enabled; + emit predictionsEnabledChanged(); + writeSettingValue("keyboardPredictiveText", enabled); +} + +QString KeyboardSettingsStore::getLanguageName(const QString &languageId) const { + const QVariant value = m_languageNames.value(languageId); + return value.isValid() ? value.toString() : languageId; +} + +QString KeyboardSettingsStore::getFlag(const QString &languageId) const { + const QVariant value = m_languageFlags.value(languageId); + return value.isValid() ? value.toString() : QStringLiteral("🌐"); +} + +void KeyboardSettingsStore::resolveSettingsManager() { + if (m_settingsManager) { + return; + } + QQmlEngine *engine = qmlEngine(this); + if (!engine) { + return; + } + QObject *settingsObject = + engine->rootContext()->contextProperty("SettingsManagerCpp").value(); + if (!settingsObject) { + return; + } + m_settingsManager = settingsObject; + connectSettingSignal("keyboardLanguageChanged()", SLOT(syncFromSettings())); + connectSettingSignal("keyboardAutoCorrectionChanged()", SLOT(syncFromSettings())); + connectSettingSignal("keyboardPredictiveTextChanged()", SLOT(syncFromSettings())); + connectSettingSignal("keyboardHapticStrengthChanged()", SLOT(syncFromSettings())); + syncFromSettings(); +} + +void KeyboardSettingsStore::syncFromSettings() { + const QString language = readSettingValue("keyboardLanguage", "en_US").toString(); + if (m_currentLanguage != language) { + m_currentLanguage = language; + emit currentLanguageChanged(); + } + + const bool autoCorrect = readSettingValue("keyboardAutoCorrection", true).toBool(); + if (m_autoCorrectEnabled != autoCorrect) { + m_autoCorrectEnabled = autoCorrect; + emit autoCorrectEnabledChanged(); + } + + const bool predictions = readSettingValue("keyboardPredictiveText", true).toBool(); + if (m_predictionsEnabled != predictions) { + m_predictionsEnabled = predictions; + emit predictionsEnabledChanged(); + } + + const QString hapticStrength = + readSettingValue("keyboardHapticStrength", QStringLiteral("medium")).toString(); + const bool hapticEnabled = hapticStrength != QStringLiteral("off"); + if (m_hapticFeedbackEnabled != hapticEnabled) { + m_hapticFeedbackEnabled = hapticEnabled; + emit hapticFeedbackEnabledChanged(); + } +} + +QVariant KeyboardSettingsStore::readSettingValue(const QString &key, + const QVariant &fallback) const { + if (!m_settingsManager) { + return fallback; + } + if (hasProperty(key.toUtf8().constData())) { + const QVariant value = m_settingsManager->property(key.toUtf8().constData()); + return value.isValid() ? value : fallback; + } + QVariant value = fallback; + QMetaObject::invokeMethod(m_settingsManager, "get", Q_RETURN_ARG(QVariant, value), + Q_ARG(QString, key), Q_ARG(QVariant, fallback)); + return value; +} + +void KeyboardSettingsStore::writeSettingValue(const QString &key, const QVariant &value) { + if (!m_settingsManager) { + return; + } + if (hasProperty(key.toUtf8().constData())) { + m_settingsManager->setProperty(key.toUtf8().constData(), value); + return; + } + QMetaObject::invokeMethod(m_settingsManager, "set", Q_ARG(QString, key), + Q_ARG(QVariant, value)); +} + +bool KeyboardSettingsStore::hasProperty(const char *name) const { + if (!m_settingsManager) { + return false; + } + return m_settingsManager->metaObject()->indexOfProperty(name) != -1; +} + +void KeyboardSettingsStore::connectSettingSignal(const char *signal, const char *slot) { + if (!m_settingsManager) { + return; + } + const QMetaObject *meta = m_settingsManager->metaObject(); + if (meta->indexOfSignal(signal) == -1) { + return; + } + QMetaObject::connect(m_settingsManager, meta->indexOfSignal(signal), this, + this->metaObject()->indexOfSlot(slot)); +} diff --git a/apps/settings/src/KeyboardSettingsStore.h b/apps/settings/src/KeyboardSettingsStore.h new file mode 100644 index 000000000..c4116fff6 --- /dev/null +++ b/apps/settings/src/KeyboardSettingsStore.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include + +class KeyboardSettingsStore : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString currentLanguage READ currentLanguage WRITE setCurrentLanguage NOTIFY + currentLanguageChanged) + Q_PROPERTY(bool autoCorrectEnabled READ autoCorrectEnabled WRITE setAutoCorrectEnabled NOTIFY + autoCorrectEnabledChanged) + Q_PROPERTY(bool autoCapitalizeEnabled READ autoCapitalizeEnabled WRITE setAutoCapitalizeEnabled + NOTIFY autoCapitalizeEnabledChanged) + Q_PROPERTY(bool hapticFeedbackEnabled READ hapticFeedbackEnabled WRITE setHapticFeedbackEnabled + NOTIFY hapticFeedbackEnabledChanged) + Q_PROPERTY(bool predictionsEnabled READ predictionsEnabled WRITE setPredictionsEnabled NOTIFY + predictionsEnabledChanged) + Q_PROPERTY(QStringList availableLanguages READ availableLanguages CONSTANT) + Q_PROPERTY(QVariantMap languageNames READ languageNames CONSTANT) + Q_PROPERTY(QVariantMap languageFlags READ languageFlags CONSTANT) + + public: + explicit KeyboardSettingsStore(QObject *parent = nullptr); + + QString currentLanguage() const { + return m_currentLanguage; + } + bool autoCorrectEnabled() const { + return m_autoCorrectEnabled; + } + bool autoCapitalizeEnabled() const { + return m_autoCapitalizeEnabled; + } + bool hapticFeedbackEnabled() const { + return m_hapticFeedbackEnabled; + } + bool predictionsEnabled() const { + return m_predictionsEnabled; + } + QStringList availableLanguages() const { + return m_availableLanguages; + } + QVariantMap languageNames() const { + return m_languageNames; + } + QVariantMap languageFlags() const { + return m_languageFlags; + } + + void setCurrentLanguage(const QString &languageId); + void setAutoCorrectEnabled(bool enabled); + void setAutoCapitalizeEnabled(bool enabled); + void setHapticFeedbackEnabled(bool enabled); + void setPredictionsEnabled(bool enabled); + + Q_INVOKABLE QString getLanguageName(const QString &languageId) const; + Q_INVOKABLE QString getFlag(const QString &languageId) const; + + signals: + void currentLanguageChanged(); + void autoCorrectEnabledChanged(); + void autoCapitalizeEnabledChanged(); + void hapticFeedbackEnabledChanged(); + void predictionsEnabledChanged(); + + private: + void resolveSettingsManager(); + QVariant readSettingValue(const QString &key, const QVariant &fallback) const; + void writeSettingValue(const QString &key, const QVariant &value); + bool hasProperty(const char *name) const; + void connectSettingSignal(const char *signal, const char *slot); + + QPointer m_settingsManager; + QStringList m_availableLanguages; + QVariantMap m_languageNames; + QVariantMap m_languageFlags; + + QString m_currentLanguage = QStringLiteral("en_US"); + bool m_autoCorrectEnabled = true; + bool m_autoCapitalizeEnabled = true; + bool m_hapticFeedbackEnabled = true; + bool m_predictionsEnabled = true; + + private slots: + void syncFromSettings(); +}; diff --git a/apps/settings/src/KeyboardSettingsStore.qml b/apps/settings/src/KeyboardSettingsStore.qml deleted file mode 100644 index f58995650..000000000 --- a/apps/settings/src/KeyboardSettingsStore.qml +++ /dev/null @@ -1,59 +0,0 @@ -pragma Singleton -import MarathonOS.Shell -import QtQuick - -QtObject { - id: keyboardSettingsStore - - property string currentLanguage: SettingsManagerCpp.keyboardLanguage || "en_US" - property bool autoCorrectEnabled: SettingsManagerCpp.keyboardAutoCorrection - property bool autoCapitalizeEnabled: true - property bool hapticFeedbackEnabled: SettingsManagerCpp.keyboardHapticStrength !== "off" - property bool predictionsEnabled: SettingsManagerCpp.keyboardPredictiveText - readonly property var availableLanguages: ["en_US", "fr_FR", "de_DE", "es_ES", "pt_BR", "it_IT", "ru_RU", "ar_SA", "zh_CN", "ja_JP"] - readonly property var languageNames: { - "en_US": "English (US)", - "fr_FR": "Français", - "de_DE": "Deutsch", - "es_ES": "Español", - "pt_BR": "Português", - "it_IT": "Italiano", - "ru_RU": "Русский", - "ar_SA": "العربية", - "zh_CN": "中文", - "ja_JP": "日本語" - } - readonly property var languageFlags: { - "en_US": "🇺🇸", - "fr_FR": "🇫🇷", - "de_DE": "🇩🇪", - "es_ES": "🇪🇸", - "pt_BR": "🇧🇷", - "it_IT": "🇮🇹", - "ru_RU": "🇷🇺", - "ar_SA": "🇸🇦", - "zh_CN": "🇨🇳", - "ja_JP": "🇯🇵" - } - - function getLanguageName(languageId) { - return languageNames[languageId] || languageId; - } - - function getFlag(languageId) { - return languageFlags[languageId] || "🌐"; - } - - onCurrentLanguageChanged: { - SettingsManagerCpp.keyboardLanguage = currentLanguage; - } - onAutoCorrectEnabledChanged: { - SettingsManagerCpp.keyboardAutoCorrection = autoCorrectEnabled; - } - onHapticFeedbackEnabledChanged: { - SettingsManagerCpp.keyboardHapticStrength = hapticFeedbackEnabled ? "medium" : "off"; - } - onPredictionsEnabledChanged: { - SettingsManagerCpp.keyboardPredictiveText = predictionsEnabled; - } -} From 969e897c698fb7e171f13813c424adb1c891502f Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 14:20:47 +0000 Subject: [PATCH 20/28] fix(shell): focus search input reliably Centralize search focus and keyboard visibility to ensure the quick search overlay brings up input immediately. --- shell/qml/components/MarathonSearch.qml | 26 ++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/shell/qml/components/MarathonSearch.qml b/shell/qml/components/MarathonSearch.qml index 326e6880e..31d2c650d 100644 --- a/shell/qml/components/MarathonSearch.qml +++ b/shell/qml/components/MarathonSearch.qml @@ -25,6 +25,22 @@ Item { Logger.info("Search", "Search overlay closed"); } + function setKeyboardVisible(show) { + if (typeof InputMethodEngine !== "undefined" && (!Platform || !Platform.hasHardwareKeyboard)) { + InputMethodEngine.showKeyboard(show); + } + } + + function ensureInputFocus() { + if (searchInput.activeFocus) + return; + + Qt.callLater(function () { + searchInput.forceActiveFocus(); + setKeyboardVisible(true); + }); + } + function appendToSearch(text) { searchInput.text += text; searchInput.forceActiveFocus(); @@ -58,17 +74,21 @@ Item { onActiveChanged: { if (active) { Logger.info("Search", "Search became active - focusing input"); - Qt.callLater(function () { - searchInput.forceActiveFocus(); - }); + ensureInputFocus(); } else { searchInput.text = ""; searchResults = []; + setKeyboardVisible(false); Logger.info("Search", "Search became inactive - emitting closed signal"); closed(); } } + onPullProgressChanged: { + if (pullProgress > 0 && !active) + ensureInputFocus(); + } + MouseArea { anchors.fill: parent anchors.bottomMargin: Constants.bottomBarHeight From d2ee1b032d1b627df25d67d2131d494b9e6bc24f Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 17:34:41 +0000 Subject: [PATCH 21/28] feat(shell): move InputContext to C++ Replace the QML input context with a C++ QML element and drop unused keyboard performance singleton QML. --- shell/CMakeLists.txt | 6 +- .../qml/keyboard/Core/KeyboardPerformance.qml | 42 ------ shell/qml/keyboard/Input/InputContext.qml | 138 ------------------ shell/src/services/inputcontext.cpp | 136 +++++++++++++++++ shell/src/services/inputcontext.h | 63 ++++++++ 5 files changed, 203 insertions(+), 182 deletions(-) delete mode 100644 shell/qml/keyboard/Core/KeyboardPerformance.qml delete mode 100644 shell/qml/keyboard/Input/InputContext.qml create mode 100644 shell/src/services/inputcontext.cpp create mode 100644 shell/src/services/inputcontext.h diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 102b39c91..cdfc0702e 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -120,6 +120,8 @@ set(SOURCES src/services/dictionary.cpp src/services/autocorrect.h src/services/autocorrect.cpp + src/services/inputcontext.h + src/services/inputcontext.cpp src/managers/flashlightmanagercpp.h src/managers/flashlightmanagercpp.cpp src/controllers/audioroutingmanager.h @@ -218,7 +220,6 @@ set(QML_FILES qml/components/VirtualKeyboard.qml qml/components/PermissionDialog.qml qml/keyboard/Core/MarathonKeyboard.qml - qml/keyboard/Core/KeyboardPerformance.qml qml/keyboard/UI/Key.qml qml/keyboard/UI/PredictionBar.qml qml/keyboard/UI/LongPressPopup.qml @@ -232,7 +233,6 @@ set(QML_FILES qml/keyboard/Layouts/NumberLayout.qml qml/keyboard/Layouts/PhoneLayout.qml qml/keyboard/Layouts/TerminalLayout.qml - qml/keyboard/Input/InputContext.qml qml/components/PowerMenu.qml qml/components/QuickSettingsTile.qml qml/components/MediaPlaybackManager.qml @@ -335,6 +335,8 @@ qt6_add_qml_module(marathon-shell src/services/dictionary.h src/services/autocorrect.cpp src/services/autocorrect.h + src/services/inputcontext.cpp + src/services/inputcontext.h RESOURCES ${JSON_FILES} OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/shell/qml/MarathonOS/Shell ) diff --git a/shell/qml/keyboard/Core/KeyboardPerformance.qml b/shell/qml/keyboard/Core/KeyboardPerformance.qml deleted file mode 100644 index f95b72570..000000000 --- a/shell/qml/keyboard/Core/KeyboardPerformance.qml +++ /dev/null @@ -1,42 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: keyboardPerf - - readonly property bool useTextMetricsCache: true - readonly property int keyPressAnimationDuration: 50 - readonly property bool lazyLoadPredictions: true - readonly property int touchEventPriority: 1 - readonly property bool optimizeBindings: true - property var frameTimings: [] - property real averageFrameTime: 16.67 - property var touchLatencies: [] - property real averageTouchLatency: 0 - - function recordFrameTime(time) { - frameTimings.push(time); - if (frameTimings.length > 100) - frameTimings.shift(); - - let sum = 0; - for (let i = 0; i < frameTimings.length; i++) - sum += frameTimings[i]; - averageFrameTime = sum / frameTimings.length; - if (averageFrameTime > 16.67) - console.warn("[KeyboardPerf] Frame time exceeded 16.67ms:", averageFrameTime); - } - - function recordTouchLatency(latency) { - touchLatencies.push(latency); - if (touchLatencies.length > 50) - touchLatencies.shift(); - - let sum = 0; - for (let i = 0; i < touchLatencies.length; i++) - sum += touchLatencies[i]; - averageTouchLatency = sum / touchLatencies.length; - if (averageTouchLatency > 5) - console.warn("[KeyboardPerf] Touch latency exceeded 5ms:", averageTouchLatency); - } -} diff --git a/shell/qml/keyboard/Input/InputContext.qml b/shell/qml/keyboard/Input/InputContext.qml deleted file mode 100644 index 367c63be0..000000000 --- a/shell/qml/keyboard/Input/InputContext.qml +++ /dev/null @@ -1,138 +0,0 @@ -import QtQuick - -Item { - id: inputContext - - property var inputMethod: Qt.inputMethod - property var keyboard: null - readonly property bool hasActiveFocus: inputMethod && inputMethod["visible"] - property string inputMode: "text" - readonly property string recommendedLayout: { - switch (inputMode) { - case "email": - return "email"; - case "url": - return "url"; - case "number": - return "number"; - case "phone": - return "phone"; - case "terminal": - return "terminal"; - default: - return "dynamic"; - } - } - readonly property bool shouldAutoCapitalize: { - switch (inputMode) { - case "email": - case "url": - case "number": - case "phone": - case "terminal": - return false; - default: - return true; - } - } - readonly property bool shouldAutoCorrect: { - switch (inputMode) { - case "email": - case "url": - case "number": - case "phone": - case "terminal": - return false; - default: - return true; - } - } - readonly property bool shouldShowPredictions: { - switch (inputMode) { - case "url": - case "email": - return true; - case "number": - case "phone": - case "terminal": - return false; - default: - return true; - } - } - - signal textInserted(string text) - signal backspacePressed - signal enterPressed - signal cursorKeyPressed(int key) - - function insertText(text) { - textInserted(text); - } - - function handleBackspace() { - backspacePressed(); - } - - function handleEnter() { - enterPressed(); - } - - function sendCursorKey(key) { - cursorKeyPressed(key); - } - - function replaceCurrentWord(newWord) { - var currentWord = getCurrentWord(); - if (currentWord.length === 0) { - insertText(newWord); - return; - } - for (var i = 0; i < currentWord.length; i++) { - handleBackspace(); - } - insertText(newWord); - } - - function getCurrentWord() { - if (keyboard && keyboard["currentWord"]) - return keyboard["currentWord"]; - - return ""; - } - - function detectInputMode() { - if (!inputMethod) { - inputMode = "text"; - return; - } - var hints = inputMethod["inputItemHints"]; - if (typeof hints === 'undefined' || hints === null) { - inputMode = "text"; - return; - } - if (hints & Qt.ImhEmailCharactersOnly) - inputMode = "email"; - else if (hints & Qt.ImhUrlCharactersOnly || ((hints & Qt.ImhNoAutoUppercase) && (hints & Qt.ImhNoPredictiveText))) - inputMode = "url"; - else if (hints & Qt.ImhDigitsOnly) - inputMode = "number"; - else if (hints & Qt.ImhDialableCharactersOnly) - inputMode = "phone"; - else - inputMode = "text"; - } - - visible: false - width: 0 - height: 0 - - Connections { - function onVisibleChanged() { - if (inputMethod["visible"]) - detectInputMode(); - } - - target: Qt.inputMethod - } -} diff --git a/shell/src/services/inputcontext.cpp b/shell/src/services/inputcontext.cpp new file mode 100644 index 000000000..05db1ee40 --- /dev/null +++ b/shell/src/services/inputcontext.cpp @@ -0,0 +1,136 @@ +#include "inputcontext.h" + +#include +#include + +InputContext::InputContext(QObject *parent) + : QObject(parent) { + m_inputMethod = QGuiApplication::inputMethod(); + if (m_inputMethod) { + connect(m_inputMethod, &QInputMethod::visibleChanged, this, [this]() { + emit hasActiveFocusChanged(); + if (m_inputMethod->isVisible()) { + detectInputMode(); + } + }); + connect(m_inputMethod, &QInputMethod::inputItemHintsChanged, this, + &InputContext::detectInputMode); + } +} + +void InputContext::setKeyboard(QObject *keyboard) { + if (m_keyboard == keyboard) { + return; + } + m_keyboard = keyboard; + emit keyboardChanged(); +} + +void InputContext::setInputMode(const QString &mode) { + setInputModeInternal(mode); +} + +QString InputContext::recommendedLayout() const { + if (m_inputMode == "email") { + return "email"; + } + if (m_inputMode == "url") { + return "url"; + } + if (m_inputMode == "number") { + return "number"; + } + if (m_inputMode == "phone") { + return "phone"; + } + if (m_inputMode == "terminal") { + return "terminal"; + } + return "dynamic"; +} + +bool InputContext::shouldAutoCapitalize() const { + return !(m_inputMode == "email" || m_inputMode == "url" || m_inputMode == "number" || + m_inputMode == "phone" || m_inputMode == "terminal"); +} + +bool InputContext::shouldAutoCorrect() const { + return !(m_inputMode == "email" || m_inputMode == "url" || m_inputMode == "number" || + m_inputMode == "phone" || m_inputMode == "terminal"); +} + +bool InputContext::shouldShowPredictions() const { + if (m_inputMode == "number" || m_inputMode == "phone" || m_inputMode == "terminal") { + return false; + } + return true; +} + +bool InputContext::hasActiveFocus() const { + return m_inputMethod && m_inputMethod->isVisible(); +} + +void InputContext::insertText(const QString &text) { + emit textInserted(text); +} + +void InputContext::handleBackspace() { + emit backspacePressed(); +} + +void InputContext::handleEnter() { + emit enterPressed(); +} + +void InputContext::sendCursorKey(int key) { + emit cursorKeyPressed(key); +} + +void InputContext::replaceCurrentWord(const QString &newWord) { + const QString currentWord = getCurrentWord(); + if (currentWord.isEmpty()) { + insertText(newWord); + return; + } + for (int i = 0; i < currentWord.length(); ++i) { + handleBackspace(); + } + insertText(newWord); +} + +QString InputContext::getCurrentWord() const { + if (!m_keyboard) { + return {}; + } + const QVariant value = m_keyboard->property("currentWord"); + return value.toString(); +} + +void InputContext::detectInputMode() { + if (!m_inputMethod) { + setInputModeInternal("text"); + return; + } + + const Qt::InputMethodHints hints = m_inputMethod->inputItemHints(); + if (hints & Qt::ImhEmailCharactersOnly) { + setInputModeInternal("email"); + } else if ((hints & Qt::ImhUrlCharactersOnly) || + ((hints & Qt::ImhNoAutoUppercase) && (hints & Qt::ImhNoPredictiveText))) { + setInputModeInternal("url"); + } else if (hints & Qt::ImhDigitsOnly) { + setInputModeInternal("number"); + } else if (hints & Qt::ImhDialableCharactersOnly) { + setInputModeInternal("phone"); + } else { + setInputModeInternal("text"); + } +} + +void InputContext::setInputModeInternal(const QString &mode) { + if (m_inputMode == mode) { + return; + } + m_inputMode = mode; + emit inputModeChanged(); +} diff --git a/shell/src/services/inputcontext.h b/shell/src/services/inputcontext.h new file mode 100644 index 000000000..ea01e26a5 --- /dev/null +++ b/shell/src/services/inputcontext.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +class QInputMethod; + +class InputContext : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QObject *keyboard READ keyboard WRITE setKeyboard NOTIFY keyboardChanged) + Q_PROPERTY(QString inputMode READ inputMode WRITE setInputMode NOTIFY inputModeChanged) + Q_PROPERTY(QString recommendedLayout READ recommendedLayout NOTIFY inputModeChanged) + Q_PROPERTY(bool shouldAutoCapitalize READ shouldAutoCapitalize NOTIFY inputModeChanged) + Q_PROPERTY(bool shouldAutoCorrect READ shouldAutoCorrect NOTIFY inputModeChanged) + Q_PROPERTY(bool shouldShowPredictions READ shouldShowPredictions NOTIFY inputModeChanged) + Q_PROPERTY(bool hasActiveFocus READ hasActiveFocus NOTIFY hasActiveFocusChanged) + + public: + explicit InputContext(QObject *parent = nullptr); + + QObject *keyboard() const { + return m_keyboard; + } + void setKeyboard(QObject *keyboard); + + QString inputMode() const { + return m_inputMode; + } + void setInputMode(const QString &mode); + + QString recommendedLayout() const; + bool shouldAutoCapitalize() const; + bool shouldAutoCorrect() const; + bool shouldShowPredictions() const; + bool hasActiveFocus() const; + + Q_INVOKABLE void insertText(const QString &text); + Q_INVOKABLE void handleBackspace(); + Q_INVOKABLE void handleEnter(); + Q_INVOKABLE void sendCursorKey(int key); + Q_INVOKABLE void replaceCurrentWord(const QString &newWord); + Q_INVOKABLE QString getCurrentWord() const; + Q_INVOKABLE void detectInputMode(); + + signals: + void keyboardChanged(); + void inputModeChanged(); + void hasActiveFocusChanged(); + void textInserted(const QString &text); + void backspacePressed(); + void enterPressed(); + void cursorKeyPressed(int key); + + private: + void setInputModeInternal(const QString &mode); + + QObject *m_keyboard = nullptr; + QInputMethod *m_inputMethod = nullptr; + QString m_inputMode = QStringLiteral("text"); +}; From d12c2d882fc6565382d27d66e15ddd216c9952b4 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 17:34:50 +0000 Subject: [PATCH 22/28] feat(calendar): move calendar storage to C++ Replace the QML calendar store with a C++ backing store that proxies CalendarManager and keeps the app state in C++. --- apps/calendar/CMakeLists.txt | 4 +- apps/calendar/CalendarApp.qml | 1 - apps/calendar/src/CalendarStorage.cpp | 133 +++++++++++++++++++++++ apps/calendar/src/CalendarStorage.h | 49 +++++++++ apps/calendar/stores/CalendarStorage.qml | 92 ---------------- 5 files changed, 184 insertions(+), 95 deletions(-) create mode 100644 apps/calendar/src/CalendarStorage.cpp create mode 100644 apps/calendar/src/CalendarStorage.h delete mode 100644 apps/calendar/stores/CalendarStorage.qml diff --git a/apps/calendar/CMakeLists.txt b/apps/calendar/CMakeLists.txt index 5de7f4ea8..32fe25830 100644 --- a/apps/calendar/CMakeLists.txt +++ b/apps/calendar/CMakeLists.txt @@ -7,8 +7,8 @@ file(GLOB_RECURSE QML_FILES ) set(SOURCES - # Future: src/calendarmanager.cpp - # Future: src/eventmanager.cpp + src/CalendarStorage.cpp + src/CalendarStorage.h ) add_marathon_app(${APP_NAME} diff --git a/apps/calendar/CalendarApp.qml b/apps/calendar/CalendarApp.qml index aacfedbc1..c12ae6337 100644 --- a/apps/calendar/CalendarApp.qml +++ b/apps/calendar/CalendarApp.qml @@ -6,7 +6,6 @@ import MarathonUI.Theme import QtQuick import QtQuick.Controls import QtQuick.Layouts -import "stores" MApp { id: calendarApp diff --git a/apps/calendar/src/CalendarStorage.cpp b/apps/calendar/src/CalendarStorage.cpp new file mode 100644 index 000000000..14b56d5ae --- /dev/null +++ b/apps/calendar/src/CalendarStorage.cpp @@ -0,0 +1,133 @@ +#include "CalendarStorage.h" + +#include +#include +#include +#include + +CalendarStorage::CalendarStorage(QObject *parent) + : QObject(parent) { + QTimer::singleShot(0, this, &CalendarStorage::resolveCalendarManager); +} + +void CalendarStorage::init() { + if (!hasManager()) { + return; + } + refreshFromManager(); + emit dataChanged(); +} + +void CalendarStorage::save() { + emit dataChanged(); +} + +QVariantMap CalendarStorage::addEvent(const QVariantMap &event) { + if (!hasManager()) { + return {}; + } + QVariantMap result; + QMetaObject::invokeMethod(m_calendarManager, "createEvent", Q_RETURN_ARG(QVariantMap, result), + Q_ARG(QVariantMap, event)); + refreshFromManager(); + emit dataChanged(); + return result; +} + +bool CalendarStorage::updateEvent(const QVariantMap &event) { + if (!hasManager()) { + return false; + } + bool result = false; + QMetaObject::invokeMethod(m_calendarManager, "updateEvent", Q_RETURN_ARG(bool, result), + Q_ARG(QVariantMap, event)); + refreshFromManager(); + emit dataChanged(); + return result; +} + +bool CalendarStorage::deleteEvent(int id) { + if (!hasManager()) { + return false; + } + bool result = false; + QMetaObject::invokeMethod(m_calendarManager, "deleteEvent", Q_RETURN_ARG(bool, result), + Q_ARG(int, id)); + refreshFromManager(); + emit dataChanged(); + return result; +} + +QVariantList CalendarStorage::getEventsForDate(const QDateTime &date) { + if (!hasManager()) { + return {}; + } + QVariantList result; + QMetaObject::invokeMethod(m_calendarManager, "getEventsForDate", + Q_RETURN_ARG(QVariantList, result), Q_ARG(QDateTime, date)); + return result; +} + +QVariantList CalendarStorage::getAllEvents() const { + return m_events; +} + +void CalendarStorage::refreshFromManager() { + if (!hasManager()) { + return; + } + const QVariant eventsValue = m_calendarManager->property("events"); + if (eventsValue.isValid()) { + const QVariantList updated = eventsValue.toList(); + if (m_events != updated) { + m_events = updated; + emit eventsChanged(); + } + } + const QVariant nextIdValue = m_calendarManager->property("nextEventId"); + if (nextIdValue.isValid()) { + const int nextId = nextIdValue.toInt(); + if (m_nextEventId != nextId) { + m_nextEventId = nextId; + emit nextEventIdChanged(); + } + } +} + +void CalendarStorage::resolveCalendarManager() { + if (m_calendarManager) { + return; + } + QQmlEngine *engine = qmlEngine(this); + if (!engine) { + return; + } + QObject *manager = engine->rootContext()->contextProperty("CalendarManager").value(); + if (!manager) { + return; + } + m_calendarManager = manager; + const QMetaObject *meta = m_calendarManager->metaObject(); + const int slotIndex = metaObject()->indexOfSlot("refreshFromManager()"); + const int loaded = meta->indexOfSignal("eventsLoaded()"); + if (loaded != -1) { + QMetaObject::connect(m_calendarManager, loaded, this, slotIndex); + } + const int created = meta->indexOfSignal("eventCreated(QVariantMap)"); + if (created != -1) { + QMetaObject::connect(m_calendarManager, created, this, slotIndex); + } + const int updated = meta->indexOfSignal("eventUpdated(QVariantMap)"); + if (updated != -1) { + QMetaObject::connect(m_calendarManager, updated, this, slotIndex); + } + const int deleted = meta->indexOfSignal("eventDeleted(int)"); + if (deleted != -1) { + QMetaObject::connect(m_calendarManager, deleted, this, slotIndex); + } + refreshFromManager(); +} + +bool CalendarStorage::hasManager() const { + return m_calendarManager; +} diff --git a/apps/calendar/src/CalendarStorage.h b/apps/calendar/src/CalendarStorage.h new file mode 100644 index 000000000..9e774e01e --- /dev/null +++ b/apps/calendar/src/CalendarStorage.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include + +class CalendarStorage : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QVariantList events READ events NOTIFY eventsChanged) + Q_PROPERTY(int nextEventId READ nextEventId NOTIFY nextEventIdChanged) + + public: + explicit CalendarStorage(QObject *parent = nullptr); + + QVariantList events() const { + return m_events; + } + int nextEventId() const { + return m_nextEventId; + } + + Q_INVOKABLE void init(); + Q_INVOKABLE void save(); + Q_INVOKABLE QVariantMap addEvent(const QVariantMap &event); + Q_INVOKABLE bool updateEvent(const QVariantMap &event); + Q_INVOKABLE bool deleteEvent(int id); + Q_INVOKABLE QVariantList getEventsForDate(const QDateTime &date); + Q_INVOKABLE QVariantList getAllEvents() const; + + signals: + void dataChanged(); + void eventsChanged(); + void nextEventIdChanged(); + + private slots: + void refreshFromManager(); + + private: + void resolveCalendarManager(); + bool hasManager() const; + + QPointer m_calendarManager; + QVariantList m_events; + int m_nextEventId = 1; +}; diff --git a/apps/calendar/stores/CalendarStorage.qml b/apps/calendar/stores/CalendarStorage.qml deleted file mode 100644 index d709d1b9a..000000000 --- a/apps/calendar/stores/CalendarStorage.qml +++ /dev/null @@ -1,92 +0,0 @@ -pragma Singleton -import MarathonOS.Shell -import QtQuick - -QtObject { - id: root - - property var events: [] - property int nextEventId: 1 - property Connections calendarConnection - - signal dataChanged - - function init() { - if (typeof CalendarManager === "undefined") - return; - - events = CalendarManager.events; - nextEventId = CalendarManager.nextEventId; - dataChanged(); - } - - function save() { - dataChanged(); - } - - function addEvent(event) { - if (typeof CalendarManager === "undefined") - return null; - - var newEvent = CalendarManager.createEvent(event); - events = CalendarManager.events; - dataChanged(); - return newEvent; - } - - function updateEvent(event) { - if (typeof CalendarManager === "undefined") - return false; - - var result = CalendarManager.updateEvent(event); - events = CalendarManager.events; - dataChanged(); - return result; - } - - function deleteEvent(id) { - if (typeof CalendarManager === "undefined") - return false; - - var result = CalendarManager.deleteEvent(id); - events = CalendarManager.events; - dataChanged(); - return result; - } - - function getEventsForDate(date) { - if (typeof CalendarManager === "undefined") - return []; - - return CalendarManager.getEventsForDate(date); - } - - function getAllEvents() { - return events; - } - - calendarConnection: Connections { - function onEventsLoaded() { - root.events = CalendarManager.events; - root.dataChanged(); - } - - function onEventCreated(event) { - root.events = CalendarManager.events; - root.dataChanged(); - } - - function onEventUpdated(event) { - root.events = CalendarManager.events; - root.dataChanged(); - } - - function onEventDeleted(eventId) { - root.events = CalendarManager.events; - root.dataChanged(); - } - - target: typeof CalendarManager !== "undefined" ? CalendarManager : null - enabled: typeof CalendarManager !== "undefined" - } -} From ea17db42fcd6bf557ceb678632702cb78ea98e71 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 17:34:56 +0000 Subject: [PATCH 23/28] chore(marathon-keyboard): remove unused QML data files Drop unused keyboard performance and data singleton QML files to keep the module UI-only. --- .../qml/Core/KeyboardPerformance.qml | 56 -- marathon-keyboard/qml/Data/AutoCorrect.qml | 123 ---- marathon-keyboard/qml/Data/Dictionary.qml | 579 ------------------ 3 files changed, 758 deletions(-) delete mode 100644 marathon-keyboard/qml/Core/KeyboardPerformance.qml delete mode 100644 marathon-keyboard/qml/Data/AutoCorrect.qml delete mode 100644 marathon-keyboard/qml/Data/Dictionary.qml diff --git a/marathon-keyboard/qml/Core/KeyboardPerformance.qml b/marathon-keyboard/qml/Core/KeyboardPerformance.qml deleted file mode 100644 index c6464fabb..000000000 --- a/marathon-keyboard/qml/Core/KeyboardPerformance.qml +++ /dev/null @@ -1,56 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: keyboardPerf - - readonly property bool useTextMetricsCache: true - - readonly property int keyPressAnimationDuration: 50 - - readonly property bool lazyLoadPredictions: true - - readonly property int touchEventPriority: Qt.HighEventPriority - - readonly property bool optimizeBindings: true - - property var frameTimings: [] - property real averageFrameTime: 16.67 - - function recordFrameTime(time) { - frameTimings.push(time); - if (frameTimings.length > 100) { - frameTimings.shift(); - } - - let sum = 0; - for (let i = 0; i < frameTimings.length; i++) { - sum += frameTimings[i]; - } - averageFrameTime = sum / frameTimings.length; - - if (averageFrameTime > 16.67) { - console.warn("[KeyboardPerf] Frame time exceeded 16.67ms:", averageFrameTime); - } - } - - property var touchLatencies: [] - property real averageTouchLatency: 0 - - function recordTouchLatency(latency) { - touchLatencies.push(latency); - if (touchLatencies.length > 50) { - touchLatencies.shift(); - } - - let sum = 0; - for (let i = 0; i < touchLatencies.length; i++) { - sum += touchLatencies[i]; - } - averageTouchLatency = sum / touchLatencies.length; - - if (averageTouchLatency > 5) { - console.warn("[KeyboardPerf] Touch latency exceeded 5ms:", averageTouchLatency); - } - } -} diff --git a/marathon-keyboard/qml/Data/AutoCorrect.qml b/marathon-keyboard/qml/Data/AutoCorrect.qml deleted file mode 100644 index e36354e4f..000000000 --- a/marathon-keyboard/qml/Data/AutoCorrect.qml +++ /dev/null @@ -1,123 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: autoCorrect - - property var commonTypos: ({ - "teh": "the", - "adn": "and", - "youre": "you're", - "dont": "don't", - "cant": "can't", - "wont": "won't", - "didnt": "didn't", - "doesnt": "doesn't", - "hasnt": "hasn't", - "havent": "haven't", - "isnt": "isn't", - "wasnt": "wasn't", - "werent": "weren't", - "shouldnt": "shouldn't", - "wouldnt": "wouldn't", - "couldnt": "couldn't", - "thats": "that's", - "whats": "what's", - "hes": "he's", - "shes": "she's", - "its": "it's", - "im": "I'm", - "youve": "you've", - "theyve": "they've", - "weve": "we've", - "ive": "I've", - "recieve": "receive", - "beleive": "believe", - "neccessary": "necessary", - "seperate": "separate", - "definately": "definitely", - "occured": "occurred", - "begining": "beginning", - "tommorrow": "tomorrow", - "untill": "until", - "alot": "a lot" - }) - - property bool enabled: true - - function correct(word) { - if (!enabled || !word || word.length === 0) { - return word; - } - - var lowerWord = word.toLowerCase(); - - if (commonTypos.hasOwnProperty(lowerWord)) { - var correction = commonTypos[lowerWord]; - keyboard.logMessage("AutoCorrect", "Correcting '" + word + "' to '" + correction + "'"); - return correction; - } - - if (!Dictionary.hasWord(lowerWord)) { - var suggestions = Dictionary.predict(lowerWord); - if (suggestions.length > 0) { - var bestMatch = suggestions[0]; - - var distance = levenshteinDistance(lowerWord, bestMatch.toLowerCase()); - if (distance <= 2) { - keyboard.logMessage("AutoCorrect", "Correcting '" + word + "' to '" + bestMatch + "' (distance: " + distance + ")"); - return bestMatch; - } - } - } - - return word; - } - - function levenshteinDistance(a, b) { - if (a.length === 0) - return b.length; - if (b.length === 0) - return a.length; - - var matrix = []; - - for (var i = 0; i <= b.length; i++) { - matrix[i] = [i]; - } - - for (var j = 0; j <= a.length; j++) { - matrix[0][j] = j; - } - - for (var i = 1; i <= b.length; i++) { - for (var j = 1; j <= a.length; j++) { - if (b.charAt(i - 1) === a.charAt(j - 1)) { - matrix[i][j] = matrix[i - 1][j - 1]; - } else { - matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); - } - } - } - - return matrix[b.length][a.length]; - } - - function shouldCorrect(word) { - if (!enabled || !word || word.length < 3) { - return null; - } - - var corrected = correct(word); - if (corrected !== word) { - return corrected; - } - - return null; - } - - function learnCorrection(typo, correction) { - commonTypos[typo.toLowerCase()] = correction; - keyboard.logMessage("AutoCorrect", "Learned: '" + typo + "' -> '" + correction + "'"); - } -} diff --git a/marathon-keyboard/qml/Data/Dictionary.qml b/marathon-keyboard/qml/Data/Dictionary.qml deleted file mode 100644 index f4ec5140c..000000000 --- a/marathon-keyboard/qml/Data/Dictionary.qml +++ /dev/null @@ -1,579 +0,0 @@ -pragma Singleton -import QtQuick - -QtObject { - id: dictionary - - property var words: [ - { - word: "the", - freq: 10000 - }, - { - word: "be", - freq: 8500 - }, - { - word: "to", - freq: 8000 - }, - { - word: "of", - freq: 7500 - }, - { - word: "and", - freq: 7000 - }, - { - word: "a", - freq: 6500 - }, - { - word: "in", - freq: 6000 - }, - { - word: "that", - freq: 5500 - }, - { - word: "have", - freq: 5000 - }, - { - word: "I", - freq: 4800 - }, - { - word: "it", - freq: 4600 - }, - { - word: "for", - freq: 4400 - }, - { - word: "not", - freq: 4200 - }, - { - word: "on", - freq: 4000 - }, - { - word: "with", - freq: 3800 - }, - { - word: "he", - freq: 3600 - }, - { - word: "as", - freq: 3400 - }, - { - word: "you", - freq: 3200 - }, - { - word: "do", - freq: 3000 - }, - { - word: "at", - freq: 2900 - }, - { - word: "this", - freq: 2800 - }, - { - word: "but", - freq: 2700 - }, - { - word: "his", - freq: 2600 - }, - { - word: "by", - freq: 2500 - }, - { - word: "from", - freq: 2400 - }, - { - word: "they", - freq: 2300 - }, - { - word: "we", - freq: 2200 - }, - { - word: "say", - freq: 2100 - }, - { - word: "her", - freq: 2000 - }, - { - word: "she", - freq: 1900 - }, - { - word: "or", - freq: 1800 - }, - { - word: "an", - freq: 1700 - }, - { - word: "will", - freq: 1600 - }, - { - word: "my", - freq: 1500 - }, - { - word: "one", - freq: 1400 - }, - { - word: "all", - freq: 1300 - }, - { - word: "would", - freq: 1200 - }, - { - word: "there", - freq: 1100 - }, - { - word: "their", - freq: 1000 - }, - { - word: "what", - freq: 950 - }, - { - word: "so", - freq: 900 - }, - { - word: "up", - freq: 850 - }, - { - word: "out", - freq: 800 - }, - { - word: "if", - freq: 750 - }, - { - word: "about", - freq: 700 - }, - { - word: "who", - freq: 650 - }, - { - word: "get", - freq: 600 - }, - { - word: "which", - freq: 550 - }, - { - word: "go", - freq: 500 - }, - { - word: "me", - freq: 480 - }, - { - word: "when", - freq: 460 - }, - { - word: "make", - freq: 440 - }, - { - word: "can", - freq: 420 - }, - { - word: "like", - freq: 400 - }, - { - word: "time", - freq: 380 - }, - { - word: "no", - freq: 360 - }, - { - word: "just", - freq: 340 - }, - { - word: "him", - freq: 320 - }, - { - word: "know", - freq: 300 - }, - { - word: "take", - freq: 290 - }, - { - word: "people", - freq: 280 - }, - { - word: "into", - freq: 270 - }, - { - word: "year", - freq: 260 - }, - { - word: "your", - freq: 250 - }, - { - word: "good", - freq: 240 - }, - { - word: "some", - freq: 230 - }, - { - word: "could", - freq: 220 - }, - { - word: "them", - freq: 210 - }, - { - word: "see", - freq: 200 - }, - { - word: "other", - freq: 195 - }, - { - word: "than", - freq: 190 - }, - { - word: "then", - freq: 185 - }, - { - word: "now", - freq: 180 - }, - { - word: "look", - freq: 175 - }, - { - word: "only", - freq: 170 - }, - { - word: "come", - freq: 165 - }, - { - word: "its", - freq: 160 - }, - { - word: "over", - freq: 155 - }, - { - word: "think", - freq: 150 - }, - { - word: "also", - freq: 148 - }, - { - word: "back", - freq: 146 - }, - { - word: "after", - freq: 144 - }, - { - word: "use", - freq: 142 - }, - { - word: "two", - freq: 140 - }, - { - word: "how", - freq: 138 - }, - { - word: "our", - freq: 136 - }, - { - word: "work", - freq: 134 - }, - { - word: "first", - freq: 132 - }, - { - word: "well", - freq: 130 - }, - { - word: "way", - freq: 128 - }, - { - word: "even", - freq: 126 - }, - { - word: "new", - freq: 124 - }, - { - word: "want", - freq: 122 - }, - { - word: "because", - freq: 120 - }, - { - word: "any", - freq: 118 - }, - { - word: "these", - freq: 116 - }, - { - word: "give", - freq: 114 - }, - { - word: "day", - freq: 112 - }, - { - word: "most", - freq: 110 - }, - { - word: "us", - freq: 108 - }, - { - word: "is", - freq: 9000 - }, - { - word: "was", - freq: 4500 - }, - { - word: "are", - freq: 3500 - }, - { - word: "been", - freq: 2800 - }, - { - word: "has", - freq: 2400 - }, - { - word: "had", - freq: 2200 - }, - { - word: "were", - freq: 1800 - }, - { - word: "said", - freq: 1600 - }, - { - word: "did", - freq: 900 - }, - { - word: "here", - freq: 600 - }, - { - word: "where", - freq: 550 - }, - { - word: "why", - freq: 400 - }, - { - word: "how", - freq: 380 - }, - { - word: "hello", - freq: 200 - }, - { - word: "yes", - freq: 180 - }, - { - word: "please", - freq: 160 - }, - { - word: "thank", - freq: 150 - }, - { - word: "thanks", - freq: 140 - }, - { - word: "sorry", - freq: 130 - }, - { - word: "okay", - freq: 120 - }, - { - word: "ok", - freq: 110 - }, - { - word: "great", - freq: 100 - }, - { - word: "awesome", - freq: 90 - } - ] - - property var userWords: [] - - function predict(prefix) { - if (!prefix || prefix.length === 0) { - return []; - } - - const lowerPrefix = prefix.toLowerCase(); - - const allWords = words.concat(userWords); - - const matches = allWords.filter(function (entry) { - return entry.word.toLowerCase().startsWith(lowerPrefix); - }); - - matches.sort(function (a, b) { - return b.freq - a.freq; - }); - - return matches.slice(0, 3).map(function (entry) { - return entry.word; - }); - } - - function learnWord(word) { - if (!word || word.length < 2) { - return; - } - - const existing = userWords.findIndex(function (entry) { - return entry.word.toLowerCase() === word.toLowerCase(); - }); - - if (existing >= 0) { - userWords[existing].freq += 10; - } else { - userWords.push({ - word: word, - freq: 100 - }); - } - - keyboard.logMessage("Dictionary", "Learned word: " + word); - } - - function hasWord(word) { - const lowerWord = word.toLowerCase(); - - const inSystem = words.some(function (entry) { - return entry.word.toLowerCase() === lowerWord; - }); - - if (inSystem) - return true; - - return userWords.some(function (entry) { - return entry.word.toLowerCase() === lowerWord; - }); - } - - function getFrequency(word) { - const lowerWord = word.toLowerCase(); - - const systemEntry = words.find(function (entry) { - return entry.word.toLowerCase() === lowerWord; - }); - - if (systemEntry) { - return systemEntry.freq; - } - - const userEntry = userWords.find(function (entry) { - return entry.word.toLowerCase() === lowerWord; - }); - - return userEntry ? userEntry.freq : 0; - } -} From f16e8394a215d04a7ca9bd87ed58a9a9e849796b Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 18:04:13 +0000 Subject: [PATCH 24/28] fix(shell): register InputContext and track hints Register the InputContext QML type explicitly and refresh its input mode when focus changes. --- shell/main.cpp | 5 ++++- shell/src/services/inputcontext.cpp | 12 +++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/shell/main.cpp b/shell/main.cpp index 719684bb7..15af31ba1 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -86,6 +86,7 @@ #include "src/services/autocorrect.h" #include "src/services/emojipredictor.h" #include "src/services/phrasepredictor.h" +#include "src/services/inputcontext.h" #include "qml/keyboard/Data/WordEngine.h" #include "src/dbus/freedesktopnotifications.h" #include "src/dbus/notificationdatabase.h" @@ -311,7 +312,9 @@ int main(int argc, char *argv[]) { QQmlApplicationEngine engine; - auto *sessionStore = new SessionStore(&app); + qmlRegisterType("MarathonOS.Shell", 1, 0, "InputContext"); + + auto *sessionStore = new SessionStore(&app); qmlRegisterSingletonInstance("MarathonOS.Shell", 1, 0, "SessionStore", sessionStore); diff --git a/shell/src/services/inputcontext.cpp b/shell/src/services/inputcontext.cpp index 05db1ee40..cb61d055f 100644 --- a/shell/src/services/inputcontext.cpp +++ b/shell/src/services/inputcontext.cpp @@ -1,5 +1,6 @@ #include "inputcontext.h" +#include #include #include @@ -13,8 +14,12 @@ InputContext::InputContext(QObject *parent) detectInputMode(); } }); - connect(m_inputMethod, &QInputMethod::inputItemHintsChanged, this, - &InputContext::detectInputMode); + } + + auto *app = qobject_cast(QCoreApplication::instance()); + if (app) { + connect(app, &QGuiApplication::focusObjectChanged, this, + [this](QObject *) { detectInputMode(); }); } } @@ -112,7 +117,8 @@ void InputContext::detectInputMode() { return; } - const Qt::InputMethodHints hints = m_inputMethod->inputItemHints(); + const QVariant hintValue = QInputMethod::queryFocusObject(Qt::ImHints, QVariant()); + const Qt::InputMethodHints hints = static_cast(hintValue.toInt()); if (hints & Qt::ImhEmailCharactersOnly) { setInputModeInternal("email"); } else if ((hints & Qt::ImhUrlCharactersOnly) || From 91e4c79d45642b41e1a363a3307acfcbb01cba68 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 22:35:33 +0000 Subject: [PATCH 25/28] refactor(shell): remove CalendarWorker QML service Delete the CalendarWorker.js file and update CMakeLists.txt to exclude it from the build, streamlining the project by removing unused QML services. --- shell/CMakeLists.txt | 5 ----- shell/qml/services/CalendarWorker.js | 29 ---------------------------- 2 files changed, 34 deletions(-) delete mode 100644 shell/qml/services/CalendarWorker.js diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index cdfc0702e..2cb19d43b 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -241,7 +241,6 @@ set(QML_FILES qml/core/Constants.qml qml/core/Logger.qml qml/models/FilteredAppModel.qml - qml/services/CalendarWorker.js ) set(JSON_FILES @@ -278,10 +277,6 @@ foreach(singleton_file ${SINGLETON_FILES}) ) endforeach() -set_source_files_properties(qml/services/CalendarWorker.js PROPERTIES - QT_QML_SKIP_QMLDIR_ENTRY TRUE -) - if(POLICY CMP0071) cmake_policy(SET CMP0071 NEW) endif() diff --git a/shell/qml/services/CalendarWorker.js b/shell/qml/services/CalendarWorker.js deleted file mode 100644 index 3ccbe075f..000000000 --- a/shell/qml/services/CalendarWorker.js +++ /dev/null @@ -1,29 +0,0 @@ - -WorkerScript.onMessage = function (message) { - if (message.action === 'processEvents') { - try { - var events = message.events; - - for (var i = 0; i < events.length; i++) { - events[i].id = Number(events[i].id); - } - - events.sort(function (a, b) { - var dateA = new Date(a.date + "T" + a.time); - var dateB = new Date(b.date + "T" + b.time); - return dateA - dateB; - }); - - WorkerScript.sendMessage({ - 'action': 'eventsParsed', - 'events': events - }); - } catch (e) { - console.error("CalendarWorker: Failed to process events: " + e); - WorkerScript.sendMessage({ - 'action': 'error', - 'error': e.toString() - }); - } - } -} From a25deceaa14c3e3045226db871dad273601080a7 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sat, 17 Jan 2026 22:48:30 +0000 Subject: [PATCH 26/28] feat(shell): enhance MarathonOOBE with mouse interaction and icon updates Add a MouseArea to the MarathonOOBE component to improve user interaction by accepting all mouse events. Update icon names from "check-circle" to "circle-check" for consistency and adjust sizes accordingly. --- shell/qml/components/MarathonOOBE.qml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/shell/qml/components/MarathonOOBE.qml b/shell/qml/components/MarathonOOBE.qml index c5bb71661..7334591b3 100644 --- a/shell/qml/components/MarathonOOBE.qml +++ b/shell/qml/components/MarathonOOBE.qml @@ -42,6 +42,18 @@ Item { visible: !SettingsManagerCpp.firstRunComplete z: Constants.zIndexModalOverlay + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + preventStealing: true + onPressed: function (mouse) { + mouse.accepted = true; + } + onWheel: function (wheel) { + wheel.accepted = true; + } + } + Rectangle { anchors.fill: parent color: MColors.background @@ -350,7 +362,7 @@ Item { } Icon { - name: "check-circle" + name: "circle-check" size: Math.round(24 * Constants.scaleFactor) color: MColors.accent visible: modelData.connected @@ -658,9 +670,9 @@ Item { anchors.horizontalCenter: parent.horizontalCenter Icon { - name: "check-circle" - size: MTypography.sizeXLarge - color: MColors.text + name: "circle-check" + size: Math.round(48 * Constants.scaleFactor) + color: MColors.accent anchors.centerIn: parent } } From 8d5e7236306a86afa5e6c034d275d99101283485 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sun, 18 Jan 2026 08:32:26 +0000 Subject: [PATCH 27/28] feat(shell): add display scaling options to MarathonOOBE Introduce a new scale page in the MarathonOOBE component, allowing users to select their preferred display size with adjustable text and UI element scaling. This enhancement improves accessibility and user experience by providing multiple scaling options. --- shell/qml/components/MarathonOOBE.qml | 139 ++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/shell/qml/components/MarathonOOBE.qml b/shell/qml/components/MarathonOOBE.qml index 7334591b3..4684a3904 100644 --- a/shell/qml/components/MarathonOOBE.qml +++ b/shell/qml/components/MarathonOOBE.qml @@ -18,6 +18,10 @@ Item { "id": "welcome", "title": "Welcome" }, + { + "id": "scale", + "title": "Display" + }, { "id": "wifi", "title": "WiFi" @@ -161,6 +165,141 @@ Item { } } + Item { + id: scalePage + + property var scaleOptions: [ + { + "factor": 0.75, + "title": "75% - Compact", + "description": "More content, smaller text" + }, + { + "factor": 1, + "title": "100% - Default", + "description": "Recommended for most users" + }, + { + "factor": 1.25, + "title": "125% - Comfortable", + "description": "Larger text, easier to read" + }, + { + "factor": 1.5, + "title": "150% - Large", + "description": "Maximum readability" + } + ] + + Column { + anchors.centerIn: parent + width: parent.width + spacing: MSpacing.xl + + Text { + text: "Choose Display Size" + font.pixelSize: MTypography.sizeXXLarge + font.weight: Font.Bold + font.family: MTypography.fontFamily + color: MColors.text + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + Text { + text: "Adjust the size of text and UI elements. You can change this later in Settings." + font.pixelSize: MTypography.sizeBody + font.family: MTypography.fontFamily + color: MColors.textSecondary + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + Column { + width: parent.width + spacing: MSpacing.sm + + Repeater { + model: scalePage.scaleOptions + + Rectangle { + width: parent.width + height: Constants.touchTargetMedium + radius: Constants.borderRadiusSmall + color: Constants.userScaleFactor === modelData.factor ? Qt.rgba(20, 184, 166, 0.08) : "transparent" + border.width: Constants.userScaleFactor === modelData.factor ? 1 : 0 + border.color: Qt.rgba(20, 184, 166, 0.3) + + Row { + anchors.fill: parent + anchors.margins: MSpacing.md + spacing: MSpacing.md + + Rectangle { + width: Math.round(28 * Constants.userScaleFactor) + height: Math.round(28 * Constants.userScaleFactor) + radius: Math.round(14 * Constants.userScaleFactor) + color: Constants.userScaleFactor === modelData.factor ? MColors.marathonTeal : "transparent" + border.width: Math.round(2 * Constants.userScaleFactor) + border.color: Constants.userScaleFactor === modelData.factor ? MColors.marathonTeal : MColors.textSecondary + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + visible: Constants.userScaleFactor === modelData.factor + width: Math.round(12 * Constants.userScaleFactor) + height: Math.round(12 * Constants.userScaleFactor) + radius: Math.round(6 * Constants.userScaleFactor) + color: MColors.background + anchors.centerIn: parent + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + Text { + text: modelData.title + color: MColors.textPrimary + font.pixelSize: MTypography.sizeBody + font.weight: Font.DemiBold + font.family: MTypography.fontFamily + } + + Text { + text: modelData.description + color: MColors.textSecondary + font.pixelSize: MTypography.sizeSmall + font.family: MTypography.fontFamily + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + Constants.userScaleFactor = modelData.factor; + SettingsManagerCpp.userScaleFactor = modelData.factor; + } + } + } + } + + Text { + text: "Current: " + Math.round(Constants.scaleFactor * 100) + "% (Base: " + Math.round((Constants.screenHeight / Constants.baseHeight) * 100) + "% × User: " + Math.round(Constants.userScaleFactor * 100) + "%)" + font.pixelSize: MTypography.sizeSmall + font.family: MTypography.fontFamily + color: MColors.textSecondary + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + } + Item { Row { id: wifiHeader From 556dae8e886c06d6442c96418006a4b5b01acee3 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Sun, 18 Jan 2026 09:14:34 +0000 Subject: [PATCH 28/28] refactor(terminal): clean up imports and improve focus handling Remove redundant import statements in TerminalSession, TerminalTabButton, and VirtualKeyRow components. Update focus handling logic in TerminalApp to enhance clarity and maintainability. --- apps/store/components/InfoRow.qml | 6 ++---- apps/terminal/TerminalApp.qml | 3 ++- apps/terminal/components/TerminalSession.qml | 1 - apps/terminal/components/TerminalTabButton.qml | 1 - apps/terminal/components/VirtualKeyRow.qml | 1 - marathon-ui/Controls/MRadioGroup.qml | 4 ++-- marathon-ui/Effects/MRipple.qml | 5 +++-- shell/CMakeLists.txt | 1 - 8 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/store/components/InfoRow.qml b/apps/store/components/InfoRow.qml index 8d920e966..a2ebbb5e9 100644 --- a/apps/store/components/InfoRow.qml +++ b/apps/store/components/InfoRow.qml @@ -12,14 +12,12 @@ Row { MLabel { text: label + ":" width: 100 - variant: "small" - color: MColors.textSecondary + variant: "secondary" } MLabel { text: value width: parent.width - 112 - variant: "small" - color: MColors.textPrimary + variant: "primary" } } diff --git a/apps/terminal/TerminalApp.qml b/apps/terminal/TerminalApp.qml index 52406fd86..235782025 100644 --- a/apps/terminal/TerminalApp.qml +++ b/apps/terminal/TerminalApp.qml @@ -48,7 +48,8 @@ MApp { onCurrentTabIndexChanged: { Qt.callLater(() => { var tab = tabs[currentTabIndex]; - if (tab && tab.session && terminalApp.Window.window && terminalApp.Window.window.active) + var appWindow = terminalApp.Window.window; + if (tab && tab.session && appWindow && appWindow.active) tab.session.forceFocus(); }); } diff --git a/apps/terminal/components/TerminalSession.qml b/apps/terminal/components/TerminalSession.qml index c04f9a9ed..b9067e6f7 100644 --- a/apps/terminal/components/TerminalSession.qml +++ b/apps/terminal/components/TerminalSession.qml @@ -1,5 +1,4 @@ import MarathonApp.Terminal -import MarathonApp.Terminal import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme diff --git a/apps/terminal/components/TerminalTabButton.qml b/apps/terminal/components/TerminalTabButton.qml index a976a9135..47e7bb649 100644 --- a/apps/terminal/components/TerminalTabButton.qml +++ b/apps/terminal/components/TerminalTabButton.qml @@ -1,5 +1,4 @@ import MarathonApp.Terminal -import MarathonApp.Terminal import MarathonOS.Shell import MarathonUI.Core import MarathonUI.Theme diff --git a/apps/terminal/components/VirtualKeyRow.qml b/apps/terminal/components/VirtualKeyRow.qml index 22581b5ad..ce7040ea1 100644 --- a/apps/terminal/components/VirtualKeyRow.qml +++ b/apps/terminal/components/VirtualKeyRow.qml @@ -1,5 +1,4 @@ import MarathonApp.Terminal -import MarathonApp.Terminal import MarathonUI.Core import MarathonUI.Theme import QtQuick diff --git a/marathon-ui/Controls/MRadioGroup.qml b/marathon-ui/Controls/MRadioGroup.qml index 494ee4449..6021cae13 100644 --- a/marathon-ui/Controls/MRadioGroup.qml +++ b/marathon-ui/Controls/MRadioGroup.qml @@ -8,11 +8,11 @@ Column { property variant selectedValue: undefined property int selectedIndex: -1 property alias options: optionsRepeater.model - property int spacingValue: MSpacing.sm + property alias spacing: root.spacing signal selectionChanged(variant value, int index) - spacing: spacingValue + spacing: MSpacing.sm Accessible.role: Accessible.RadioButton Accessible.name: "Radio button group" diff --git a/marathon-ui/Effects/MRipple.qml b/marathon-ui/Effects/MRipple.qml index 6af6f6945..36504df1f 100644 --- a/marathon-ui/Effects/MRipple.qml +++ b/marathon-ui/Effects/MRipple.qml @@ -7,6 +7,7 @@ Item { property point origin: Qt.point(width / 2, height / 2) property bool active: false property color rippleColor: MColors.ripple + readonly property real rippleDiameter: Math.max(width, height) * MMotion.rippleMaxRadius anchors.fill: parent clip: true @@ -25,8 +26,8 @@ Item { name: "active" when: root.active PropertyChanges { - rippleCircle.width: Math.max(root.width, root.height) * MMotion.rippleMaxRadius - rippleCircle.height: Math.max(root.width, root.height) * MMotion.rippleMaxRadius + rippleCircle.width: root.rippleDiameter + rippleCircle.height: root.rippleDiameter rippleCircle.opacity: 0 } } diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 2cb19d43b..7e16228bd 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -56,7 +56,6 @@ set(SOURCES src/managers/sensormanagercpp.cpp src/managers/settingsmanager.h src/managers/settingsmanager.cpp - src/managers/platform.h src/managers/platformcpp.h src/services/navigationroutercpp.h src/services/navigationroutercpp.cpp