add noctalia and fuzzel
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Location
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Python scripts
|
||||
readonly property string checkCalendarAvailableScript: Quickshell.shellDir + '/Scripts/python/src/calendar/check-calendar.py'
|
||||
readonly property string listCalendarsScript: Quickshell.shellDir + '/Scripts/python/src/calendar/list-calendars.py'
|
||||
readonly property string calendarEventsScript: Quickshell.shellDir + '/Scripts/python/src/calendar/calendar-events.py'
|
||||
|
||||
function init() {
|
||||
availabilityCheckProcess.running = true;
|
||||
}
|
||||
function loadCalendars() {
|
||||
listCalendarsProcess.running = true;
|
||||
}
|
||||
function loadEvents(daysAhead = 31, daysBehind = 14) {
|
||||
CalendarService.loading = true;
|
||||
CalendarService.lastError = "";
|
||||
|
||||
const now = new Date();
|
||||
const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000));
|
||||
const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000));
|
||||
|
||||
loadEventsProcess.startTime = Math.floor(startDate.getTime() / 1000);
|
||||
loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000);
|
||||
loadEventsProcess.running = true;
|
||||
|
||||
Logger.d("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`);
|
||||
}
|
||||
|
||||
// Process to check for evolution-data-server libraries
|
||||
Process {
|
||||
id: availabilityCheckProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'unavailable: python3 not installed'"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const result = text.trim();
|
||||
if (result === "available") {
|
||||
CalendarService.dataProvider = CalendarService.dataProvider || root;
|
||||
CalendarService.available = true;
|
||||
}
|
||||
|
||||
if (CalendarService.available) {
|
||||
Logger.i("Calendar", "EDS libraries available");
|
||||
loadCalendars();
|
||||
} else {
|
||||
Logger.w("Calendar", "EDS libraries not available: " + result);
|
||||
CalendarService.lastError = "Evolution Data Server libraries not installed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "Availability check error: " + text);
|
||||
CalendarService.lastError = "Failed to check library availability";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to list available calendars
|
||||
Process {
|
||||
id: listCalendarsProcess
|
||||
running: false
|
||||
command: ["python3", root.listCalendarsScript]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const result = JSON.parse(text.trim());
|
||||
CalendarService.setCalendars(result);
|
||||
Logger.d("Calendar", `Found ${result.length} calendar(s)`);
|
||||
|
||||
// Auto-load events after discovering calendars
|
||||
if (result.length > 0) {
|
||||
loadEvents();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.d("Calendar", "Failed to parse calendars: " + e);
|
||||
CalendarService.lastError = "Failed to parse calendar list";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "List calendars error: " + text);
|
||||
CalendarService.lastError = text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to load events
|
||||
Process {
|
||||
id: loadEventsProcess
|
||||
running: false
|
||||
property int startTime: 0
|
||||
property int endTime: 0
|
||||
|
||||
command: ["python3", root.calendarEventsScript, startTime.toString(), endTime.toString()]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
CalendarService.loading = false;
|
||||
|
||||
try {
|
||||
const events = JSON.parse(text.trim());
|
||||
CalendarService.setEvents(events);
|
||||
Logger.d("Calendar", `Loaded ${events.length} events(s)`);
|
||||
} catch (e) {
|
||||
Logger.d("Calendar", "Failed to parse events: " + e);
|
||||
CalendarService.lastError = "Failed to parse events";
|
||||
// Fall back to cached events
|
||||
CalendarService.loadCachedEvents();
|
||||
Logger.d("Calendar", "Using cached events");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
CalendarService.loading = false;
|
||||
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "Load events error: " + text);
|
||||
CalendarService.lastError = text.trim();
|
||||
|
||||
// Fall back to cached events
|
||||
CalendarService.loadCachedEvents();
|
||||
Logger.d("Calendar", "Using cached events");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Location
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string khalEventsScript: Quickshell.shellDir + '/Scripts/python/src/calendar/khal-events.py'
|
||||
|
||||
function init() {
|
||||
availabilityCheckProcess.running = true;
|
||||
}
|
||||
function loadCalendars() {
|
||||
listCalendarsProcess.running = true;
|
||||
}
|
||||
function loadEvents(daysAhead = 31, daysBehind = 14) {
|
||||
CalendarService.loading = true;
|
||||
CalendarService.lastError = "";
|
||||
|
||||
const now = new Date();
|
||||
const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000));
|
||||
|
||||
loadEventsProcess.startTime = formatDateYMD(startDate);
|
||||
loadEventsProcess.duration = `${daysAhead + daysBehind}d`;
|
||||
loadEventsProcess.running = true;
|
||||
|
||||
Logger.d("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${loadEventsProcess.startTime} ${loadEventsProcess.duration}`);
|
||||
}
|
||||
|
||||
// Process to check for khal installation
|
||||
Process {
|
||||
id: availabilityCheckProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "command -v khal >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1"]
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
CalendarService.available = true;
|
||||
CalendarService.dataProvider = CalendarService.dataProvider || root;
|
||||
}
|
||||
|
||||
if (CalendarService.available) {
|
||||
Logger.i("Calendar", "Khal available");
|
||||
loadCalendars();
|
||||
} else {
|
||||
Logger.w("Calendar", "Khal not available");
|
||||
CalendarService.lastError = "khal binary not installed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to list available calendars
|
||||
Process {
|
||||
id: listCalendarsProcess
|
||||
running: false
|
||||
command: ["khal", "printcalendars"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const calendars = text.trim().split("\n");
|
||||
CalendarService.setCalendars(calendars);
|
||||
|
||||
Logger.d("Calendar", `Found ${calendars.length} calendar(s)`);
|
||||
|
||||
// Auto-load events after discovering calendars
|
||||
if (calendars.length > 0) {
|
||||
loadEvents();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.d("Calendar", "Failed to parse calendars: " + e);
|
||||
CalendarService.lastError = "Failed to parse calendar list";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "List calendars error: " + text);
|
||||
CalendarService.lastError = text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to load events
|
||||
Process {
|
||||
id: loadEventsProcess
|
||||
running: false
|
||||
property string startTime: ""
|
||||
property string duration: ""
|
||||
|
||||
command: ["python3", root.khalEventsScript, startTime, duration]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
CalendarService.loading = false;
|
||||
|
||||
try {
|
||||
const events = parseEvents(text.trim());
|
||||
CalendarService.setEvents(events);
|
||||
Logger.d("Calendar", `Loaded ${events.length} events(s)`);
|
||||
} catch (e) {
|
||||
Logger.d("Calendar", "Failed to parse events: " + e);
|
||||
CalendarService.lastError = "Failed to parse events";
|
||||
// Fall back to cached events
|
||||
CalendarService.loadCachedEvents();
|
||||
Logger.d("Calendar", "Using cached events");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
CalendarService.loading = false;
|
||||
|
||||
if (text.trim()) {
|
||||
Logger.d("Calendar", "Load events error: " + text);
|
||||
CalendarService.lastError = text.trim();
|
||||
// Fall back to cached events
|
||||
CalendarService.loadCachedEvents();
|
||||
Logger.d("Calendar", "Using cached events");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseEvents(text) {
|
||||
const result = [];
|
||||
const duplicates = new Set();
|
||||
|
||||
for (const line of text.split("\n")) {
|
||||
if (!line.trim())
|
||||
continue;
|
||||
const dayEvents = JSON.parse(line);
|
||||
for (const event of dayEvents) {
|
||||
if (event["repeat-pattern"] !== "") {
|
||||
// if there is a repeat pattern, the event must be included each time
|
||||
result.push({
|
||||
uid: event.uid,
|
||||
calendar: event.calendar,
|
||||
summary: event.title,
|
||||
start: parseTimestamp(event["start-long-full"]),
|
||||
end: parseTimestamp(event["end-long-full"]),
|
||||
location: event.location,
|
||||
description: event.description
|
||||
});
|
||||
} else if (!duplicates.has(event.uid)) {
|
||||
// in any other cases, we must remove duplicates using the uid of the event
|
||||
result.push({
|
||||
uid: event.uid,
|
||||
calendar: event.calendar,
|
||||
summary: event.title,
|
||||
start: parseTimestamp(event["start-long-full"]),
|
||||
end: parseTimestamp(event["end-long-full"]),
|
||||
location: event.location,
|
||||
description: event.description
|
||||
});
|
||||
duplicates.add(event.uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseTimestamp(timeStr) {
|
||||
// expects ISO8601 format
|
||||
return Math.floor((Date.parse(timeStr)).valueOf() / 1000);
|
||||
}
|
||||
|
||||
function formatDateYMD(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
return y + "-" + m + "-" + d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Location.Calendar
|
||||
import qs.Services.System
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Core state
|
||||
property var events: ([])
|
||||
property bool loading: false
|
||||
property bool available: false
|
||||
property string lastError: ""
|
||||
property var calendars: ([])
|
||||
|
||||
property var dataProvider: null
|
||||
|
||||
// Persistent cache
|
||||
property string cacheFile: Settings.cacheDir + "calendar.json"
|
||||
|
||||
// Cache file handling
|
||||
FileView {
|
||||
id: cacheFileView
|
||||
path: root.cacheFile
|
||||
printErrors: false
|
||||
|
||||
JsonAdapter {
|
||||
id: cacheAdapter
|
||||
property var cachedEvents: ([])
|
||||
property var cachedCalendars: ([])
|
||||
property string lastUpdate: ""
|
||||
}
|
||||
|
||||
onLoadFailed: {
|
||||
cacheAdapter.cachedEvents = ([]);
|
||||
cacheAdapter.cachedCalendars = ([]);
|
||||
cacheAdapter.lastUpdate = "";
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
loadFromCache();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("Calendar", "Service started");
|
||||
loadFromCache();
|
||||
checkAvailability();
|
||||
}
|
||||
|
||||
// Save cache with debounce
|
||||
Timer {
|
||||
id: saveDebounce
|
||||
interval: 1000
|
||||
onTriggered: cacheFileView.writeAdapter()
|
||||
}
|
||||
|
||||
function setEvents(newEvents) {
|
||||
root.events = newEvents;
|
||||
cacheAdapter.cachedEvents = newEvents;
|
||||
cacheAdapter.lastUpdate = new Date().toISOString();
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function setCalendars(newCalendars) {
|
||||
root.calendars = newCalendars;
|
||||
cacheAdapter.cachedCalendars = newCalendars;
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function loadCachedEvents() {
|
||||
if (cacheAdapter.cachedEvents.length > 0) {
|
||||
root.events = cacheAdapter.cachedEvents;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCache() {
|
||||
saveDebounce.restart();
|
||||
}
|
||||
|
||||
// Load events and calendars from cache
|
||||
function loadFromCache() {
|
||||
if (cacheAdapter.cachedEvents && cacheAdapter.cachedEvents.length > 0) {
|
||||
root.events = cacheAdapter.cachedEvents;
|
||||
Logger.d("Calendar", `Loaded ${cacheAdapter.cachedEvents.length} cached event(s)`);
|
||||
}
|
||||
|
||||
if (cacheAdapter.cachedCalendars && cacheAdapter.cachedCalendars.length > 0) {
|
||||
root.calendars = cacheAdapter.cachedCalendars;
|
||||
Logger.d("Calendar", `Loaded ${cacheAdapter.cachedCalendars.length} cached calendar(s)`);
|
||||
}
|
||||
|
||||
if (cacheAdapter.lastUpdate) {
|
||||
Logger.d("Calendar", `Cache last updated: ${cacheAdapter.lastUpdate}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh timer (every 5 minutes)
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 300000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: loadEvents()
|
||||
}
|
||||
|
||||
// Core functions
|
||||
function checkAvailability() {
|
||||
if (!Settings.data.location.showCalendarEvents) {
|
||||
root.available = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Khal.init();
|
||||
EvolutionDataServer.init();
|
||||
}
|
||||
|
||||
function loadCalendars() {
|
||||
if (!root.available || !dataProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataProvider.loadCalendars();
|
||||
}
|
||||
function loadEvents(daysAhead = 31, daysBehind = 14) {
|
||||
if (!root.available || !dataProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataProvider.loadEvents(daysAhead, daysBehind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool initComplete: false
|
||||
property bool nextDarkModeState: false
|
||||
|
||||
Connections {
|
||||
target: LocationService.data
|
||||
enabled: Settings.data.colorSchemes.schedulingMode == "location"
|
||||
function onWeatherChanged() {
|
||||
if (LocationService.data.weather !== null) {
|
||||
const changes = root.collectWeatherChanges(LocationService.data.weather);
|
||||
if (!root.initComplete) {
|
||||
root.initComplete = true;
|
||||
root.applyCurrentMode(changes);
|
||||
}
|
||||
root.scheduleNextMode(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.colorSchemes
|
||||
enabled: Settings.data.colorSchemes.schedulingMode == "manual"
|
||||
function onManualSunriseChanged() {
|
||||
const changes = root.collectManualChanges();
|
||||
root.applyCurrentMode(changes);
|
||||
root.scheduleNextMode(changes);
|
||||
}
|
||||
function onManualSunsetChanged() {
|
||||
const changes = root.collectManualChanges();
|
||||
root.applyCurrentMode(changes);
|
||||
root.scheduleNextMode(changes);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.colorSchemes
|
||||
function onSchedulingModeChanged() {
|
||||
root.update();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Time
|
||||
function onResumed() {
|
||||
Logger.i("DarkModeService", "System resumed - re-evaluating dark mode");
|
||||
root.update();
|
||||
resumeRetryTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
onTriggered: {
|
||||
Settings.data.colorSchemes.darkMode = root.nextDarkModeState;
|
||||
root.update();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resumeRetryTimer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
Logger.i("DarkModeService", "Resume retry - re-evaluating dark mode again");
|
||||
root.update();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
Logger.i("DarkModeService", "Service started");
|
||||
root.update();
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (Settings.data.colorSchemes.schedulingMode == "manual") {
|
||||
const changes = collectManualChanges();
|
||||
initComplete = true;
|
||||
applyCurrentMode(changes);
|
||||
scheduleNextMode(changes);
|
||||
} else if (Settings.data.colorSchemes.schedulingMode == "location" && LocationService.data.weather) {
|
||||
const changes = collectWeatherChanges(LocationService.data.weather);
|
||||
initComplete = true;
|
||||
applyCurrentMode(changes);
|
||||
scheduleNextMode(changes);
|
||||
}
|
||||
}
|
||||
|
||||
function parseTime(timeString) {
|
||||
const parts = timeString.split(":").map(Number);
|
||||
return {
|
||||
"hour": parts[0],
|
||||
"minute": parts[1]
|
||||
};
|
||||
}
|
||||
|
||||
function collectManualChanges() {
|
||||
const sunriseTime = parseTime(Settings.data.colorSchemes.manualSunrise);
|
||||
const sunsetTime = parseTime(Settings.data.colorSchemes.manualSunset);
|
||||
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
const day = now.getDate();
|
||||
|
||||
const yesterdaysSunset = new Date(year, month, day - 1, sunsetTime.hour, sunsetTime.minute);
|
||||
const todaysSunrise = new Date(year, month, day, sunriseTime.hour, sunriseTime.minute);
|
||||
const todaysSunset = new Date(year, month, day, sunsetTime.hour, sunsetTime.minute);
|
||||
const tomorrowsSunrise = new Date(year, month, day + 1, sunriseTime.hour, sunriseTime.minute);
|
||||
|
||||
return [
|
||||
{
|
||||
"time": yesterdaysSunset.getTime(),
|
||||
"darkMode": true
|
||||
},
|
||||
{
|
||||
"time": todaysSunrise.getTime(),
|
||||
"darkMode": false
|
||||
},
|
||||
{
|
||||
"time": todaysSunset.getTime(),
|
||||
"darkMode": true
|
||||
},
|
||||
{
|
||||
"time": tomorrowsSunrise.getTime(),
|
||||
"darkMode": false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function collectWeatherChanges(weather) {
|
||||
const changes = [];
|
||||
|
||||
if (Date.now() < Date.parse(weather.daily.sunrise[0])) {
|
||||
// The sun has not risen yet
|
||||
changes.push({
|
||||
"time": Date.now() - 1,
|
||||
"darkMode": true
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < weather.daily.sunrise.length; i++) {
|
||||
changes.push({
|
||||
"time": Date.parse(weather.daily.sunrise[i]),
|
||||
"darkMode": false
|
||||
});
|
||||
changes.push({
|
||||
"time": Date.parse(weather.daily.sunset[i]),
|
||||
"darkMode": true
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function applyCurrentMode(changes) {
|
||||
const now = Date.now();
|
||||
Logger.i("DarkModeService", `Applying mode at ${new Date(now).toLocaleString()} (${now})`);
|
||||
|
||||
// changes.findLast(change => change.time < now) // not available in QML...
|
||||
let lastChange = null;
|
||||
for (var i = 0; i < changes.length; i++) {
|
||||
Logger.d("DarkModeService", `Checking change: time=${changes[i].time} (${new Date(changes[i].time).toLocaleString()}), darkMode=${changes[i].darkMode}`);
|
||||
if (changes[i].time < now) {
|
||||
lastChange = changes[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (lastChange) {
|
||||
Logger.i("DarkModeService", `Selected change: time=${lastChange.time}, darkMode=${lastChange.darkMode}`);
|
||||
Settings.data.colorSchemes.darkMode = lastChange.darkMode;
|
||||
Logger.d("DarkModeService", `Reset: darkmode=${lastChange.darkMode}`);
|
||||
} else {
|
||||
Logger.w("DarkModeService", "No suitable change found for current time!");
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextMode(changes) {
|
||||
const now = Date.now();
|
||||
const nextChange = changes.find(change => change.time > now);
|
||||
if (nextChange) {
|
||||
root.nextDarkModeState = nextChange.darkMode;
|
||||
timer.interval = nextChange.time - now;
|
||||
timer.restart();
|
||||
Logger.d("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
// Location and weather service with decoupled geocoding and weather fetching.
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json")
|
||||
property int weatherUpdateFrequency: 30 * 60
|
||||
property bool isFetchingWeather: false
|
||||
|
||||
// Talia weather
|
||||
readonly property int taliaMascotWeatherMonth: 3
|
||||
readonly property int taliaMascotWeatherDay: 1
|
||||
|
||||
readonly property bool taliaWeatherMascotDayActive: {
|
||||
const d = Time.now;
|
||||
return d.getMonth() === root.taliaMascotWeatherMonth && d.getDate() === root.taliaMascotWeatherDay;
|
||||
}
|
||||
|
||||
readonly property bool taliaWeatherMascotActive: taliaWeatherMascotDayActive || Settings.data.location.weatherTaliaMascotAlways
|
||||
|
||||
readonly property alias data: adapter
|
||||
|
||||
// True when the user has set a location name or enabled auto-locate
|
||||
readonly property bool locationConfigured: Settings.data.location.name !== "" || Settings.data.location.autoLocate
|
||||
|
||||
// Stable UI properties - only updated when location is successfully geocoded
|
||||
property bool coordinatesReady: false
|
||||
property string stableLatitude: ""
|
||||
property string stableLongitude: ""
|
||||
property string stableName: ""
|
||||
|
||||
FileView {
|
||||
id: locationFileView
|
||||
path: locationFile
|
||||
printErrors: false
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
onLoaded: {
|
||||
Logger.d("Location", "Loaded cached data");
|
||||
if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
|
||||
root.stableLatitude = adapter.latitude;
|
||||
root.stableLongitude = adapter.longitude;
|
||||
root.stableName = adapter.name;
|
||||
root.coordinatesReady = true;
|
||||
Logger.i("Location", "Coordinates ready");
|
||||
}
|
||||
update();
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
update();
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
property string latitude: ""
|
||||
property string longitude: ""
|
||||
property string name: ""
|
||||
property int weatherLastFetch: 0
|
||||
property var weather: null
|
||||
}
|
||||
}
|
||||
|
||||
// Formatted coordinates for UI display
|
||||
readonly property string displayCoordinates: {
|
||||
if (!root.coordinatesReady || root.stableLatitude === "" || root.stableLongitude === "") {
|
||||
return "";
|
||||
}
|
||||
const lat = parseFloat(root.stableLatitude).toFixed(4);
|
||||
const lon = parseFloat(root.stableLongitude).toFixed(4);
|
||||
return `${lat}, ${lon}`;
|
||||
}
|
||||
|
||||
// Auto-geolocate timer - periodically updates location via IP geolocation
|
||||
Timer {
|
||||
id: autoLocateTimer
|
||||
interval: 30 * 60 * 1000
|
||||
running: Settings.data.location.autoLocate
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.geolocateAndApply()
|
||||
}
|
||||
|
||||
// Update timer runs when weather is enabled or location-based scheduling is active
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 20 * 1000
|
||||
running: Settings.data.location.weatherEnabled || Settings.data.colorSchemes.schedulingMode == "location"
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
running: false
|
||||
interval: 1000
|
||||
onTriggered: locationFileView.writeAdapter()
|
||||
}
|
||||
|
||||
function init() {
|
||||
Logger.i("Location", "Service started");
|
||||
}
|
||||
|
||||
function resetWeather() {
|
||||
Logger.i("Location", "Resetting location and weather data");
|
||||
|
||||
root.coordinatesReady = false;
|
||||
root.stableLatitude = "";
|
||||
root.stableLongitude = "";
|
||||
root.stableName = "";
|
||||
|
||||
adapter.latitude = "";
|
||||
adapter.longitude = "";
|
||||
adapter.name = "";
|
||||
adapter.weatherLastFetch = 0;
|
||||
adapter.weather = null;
|
||||
isFetchingWeather = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// Main update function - geocodes location if needed, then fetches weather if enabled
|
||||
function update() {
|
||||
updateLocation();
|
||||
|
||||
if (Settings.data.location.weatherEnabled) {
|
||||
updateWeatherData();
|
||||
}
|
||||
}
|
||||
|
||||
// Runs independently of weather toggle
|
||||
function updateLocation() {
|
||||
const locationChanged = adapter.name !== Settings.data.location.name;
|
||||
const needsGeocoding = (adapter.latitude === "") || (adapter.longitude === "") || locationChanged;
|
||||
|
||||
if (!needsGeocoding) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFetchingWeather) {
|
||||
return;
|
||||
}
|
||||
|
||||
isFetchingWeather = true;
|
||||
|
||||
if (locationChanged) {
|
||||
root.coordinatesReady = false;
|
||||
Logger.d("Location", "Location changed from", adapter.name, "to", Settings.data.location.name);
|
||||
}
|
||||
|
||||
geocodeLocation(Settings.data.location.name, function (latitude, longitude, name, country) {
|
||||
adapter.name = Settings.data.location.name;
|
||||
adapter.latitude = latitude.toString();
|
||||
adapter.longitude = longitude.toString();
|
||||
root.stableLatitude = adapter.latitude;
|
||||
root.stableLongitude = adapter.longitude;
|
||||
root.stableName = `${name}, ${country}`;
|
||||
root.coordinatesReady = true;
|
||||
|
||||
isFetchingWeather = false;
|
||||
Logger.i("Location", `Geocoded ${Settings.data.location.name}: ${root.stableLatitude}, ${root.stableLongitude}`);
|
||||
|
||||
if (locationChanged) {
|
||||
adapter.weatherLastFetch = 0;
|
||||
updateWeatherData();
|
||||
}
|
||||
}, errorCallback);
|
||||
}
|
||||
|
||||
// Fetch weather data if enabled and coordinates are available
|
||||
function updateWeatherData() {
|
||||
if (!Settings.data.location.weatherEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFetchingWeather) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (adapter.latitude === "" || adapter.longitude === "") {
|
||||
Logger.w("Location", "Cannot fetch weather without coordinates");
|
||||
return;
|
||||
}
|
||||
const needsWeatherUpdate = (adapter.weatherLastFetch === "") || (adapter.weather === null) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency);
|
||||
|
||||
if (needsWeatherUpdate) {
|
||||
isFetchingWeather = true;
|
||||
fetchWeatherData(adapter.latitude, adapter.longitude, errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
// Query geocoding API to convert location name to coordinates
|
||||
function geocodeLocation(locationName, callback, errorCallback) {
|
||||
if (locationName === "") {
|
||||
isFetchingWeather = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.d("Location", "Geocoding location name");
|
||||
var geoUrl = "https://api.noctalia.dev/geocode?city=" + encodeURIComponent(locationName);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var geoData = JSON.parse(xhr.responseText);
|
||||
if (geoData.lat != null) {
|
||||
callback(geoData.lat, geoData.lng, geoData.name, geoData.country);
|
||||
} else {
|
||||
errorCallback("Location", "could not resolve location name");
|
||||
}
|
||||
} catch (e) {
|
||||
errorCallback("Location", "Failed to parse geocoding data: " + e);
|
||||
}
|
||||
} else {
|
||||
errorCallback("Location", `Geocoding error: ${xhr.status} ${xhr.responseText}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", geoUrl);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// Fetch weather data from Open-Meteo API
|
||||
function fetchWeatherData(latitude, longitude, errorCallback) {
|
||||
Logger.d("Location", "Fetching weather from api.open-meteo.com");
|
||||
var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure,is_day&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var weatherData = JSON.parse(xhr.responseText);
|
||||
//console.log(JSON.stringify(weatherData))
|
||||
|
||||
// Save core data
|
||||
data.weather = weatherData;
|
||||
data.weatherLastFetch = Time.timestamp;
|
||||
|
||||
// Update stable display values only when complete and successful
|
||||
root.stableLatitude = data.latitude = weatherData.latitude.toString();
|
||||
root.stableLongitude = data.longitude = weatherData.longitude.toString();
|
||||
root.coordinatesReady = true;
|
||||
|
||||
isFetchingWeather = false;
|
||||
Logger.d("Location", "Cached weather to disk - stable coordinates updated");
|
||||
} catch (e) {
|
||||
errorCallback("Location", "Failed to parse weather data");
|
||||
}
|
||||
} else {
|
||||
errorCallback("Location", `Weather error: ${xhr.status} ${xhr.responseText}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// Geolocate via IP address using the Noctalia API
|
||||
function geolocate(callback, errorCallback) {
|
||||
Logger.d("Location", "Geolocating via IP");
|
||||
var url = "https://api.noctalia.dev/geolocate";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
if (data.lat != null) {
|
||||
callback(data.lat, data.lng, data.city, data.country);
|
||||
} else {
|
||||
errorCallback("Location", "Geolocate: no coordinates returned");
|
||||
}
|
||||
} catch (e) {
|
||||
errorCallback("Location", "Failed to parse geolocate data: " + e);
|
||||
}
|
||||
} else {
|
||||
errorCallback("Location", `Geolocate error: ${xhr.status} ${xhr.responseText}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// Geolocate via IP and apply the result as the current location
|
||||
function geolocateAndApply() {
|
||||
if (isFetchingWeather) {
|
||||
Logger.w("Location", "Geolocate skipped, fetch already in progress");
|
||||
return;
|
||||
}
|
||||
geolocate(function (lat, lng, city, country) {
|
||||
Logger.i("Location", "Geolocated to", city + ",", country + ":", lat + "," + lng);
|
||||
|
||||
const locationChanged = adapter.name !== city;
|
||||
Settings.data.location.name = city;
|
||||
adapter.name = city;
|
||||
adapter.latitude = lat.toString();
|
||||
adapter.longitude = lng.toString();
|
||||
root.stableLatitude = adapter.latitude;
|
||||
root.stableLongitude = adapter.longitude;
|
||||
root.stableName = `${city}, ${country}`;
|
||||
root.coordinatesReady = true;
|
||||
|
||||
if (locationChanged) {
|
||||
adapter.weatherLastFetch = 0;
|
||||
adapter.weather = null;
|
||||
}
|
||||
|
||||
if (Settings.data.location.weatherEnabled) {
|
||||
updateWeatherData();
|
||||
}
|
||||
}, errorCallback);
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function errorCallback(module, message) {
|
||||
Logger.w(module, message);
|
||||
isFetchingWeather = false;
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function weatherSymbolFromCode(code) {
|
||||
var isDay = data.weather ? data.weather.current_weather.is_day : true;
|
||||
if (code === 0)
|
||||
return isDay ? "weather-sun" : "weather-moon";
|
||||
if (code === 1 || code === 2)
|
||||
return isDay ? "weather-cloud-sun" : "weather-moon-stars";
|
||||
if (code === 3)
|
||||
return "weather-cloud";
|
||||
if (code >= 45 && code <= 48)
|
||||
return "weather-cloud-haze";
|
||||
if (code >= 51 && code <= 67)
|
||||
return "weather-cloud-rain";
|
||||
if (code >= 80 && code <= 82)
|
||||
return "weather-cloud-rain";
|
||||
if (code >= 71 && code <= 77)
|
||||
return "weather-cloud-snow";
|
||||
if (code >= 71 && code <= 77)
|
||||
return "weather-cloud-snow";
|
||||
if (code >= 85 && code <= 86)
|
||||
return "weather-cloud-snow";
|
||||
if (code >= 95 && code <= 99)
|
||||
return "weather-cloud-lightning";
|
||||
return "weather-cloud";
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function taliaWeatherImageFromCode(code) {
|
||||
var isDay = data.weather ? data.weather.current_weather.is_day : true;
|
||||
if (code >= 40 && code <= 49)
|
||||
return Quickshell.shellDir + "/Assets/Talia/TaliaDazed.png";
|
||||
if (code >= 95 && code <= 99)
|
||||
return Quickshell.shellDir + "/Assets/Talia/TaliaFear.png";
|
||||
var wet = (code >= 51 && code <= 67) || (code >= 80 && code <= 82) || (code >= 71 && code <= 77) || (code >= 85 && code <= 86);
|
||||
if (wet)
|
||||
return Quickshell.shellDir + "/Assets/Talia/TaliaSob.png";
|
||||
if ((code === 0 || code === 1 || code === 2) && isDay === false)
|
||||
return Quickshell.shellDir + "/Assets/Talia/TaliaVampire.png";
|
||||
if ((code === 0 && isDay === true) || code === 1 || code === 2)
|
||||
return Quickshell.shellDir + "/Assets/Talia/TaliaParty.png";
|
||||
return Quickshell.shellDir + "/Assets/Talia/TaliaBlank.png";
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function weatherDescriptionFromCode(code) {
|
||||
if (code === 0)
|
||||
return "Clear sky";
|
||||
if (code === 1)
|
||||
return "Mainly clear";
|
||||
if (code === 2)
|
||||
return "Partly cloudy";
|
||||
if (code === 3)
|
||||
return "Overcast";
|
||||
if (code === 45 || code === 48)
|
||||
return "Fog";
|
||||
if (code >= 51 && code <= 67)
|
||||
return "Drizzle";
|
||||
if (code >= 71 && code <= 77)
|
||||
return "Snow";
|
||||
if (code >= 80 && code <= 82)
|
||||
return "Rain showers";
|
||||
if (code >= 95 && code <= 99)
|
||||
return "Thunderstorm";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function celsiusToFahrenheit(celsius) {
|
||||
return 32 + celsius * 1.8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Night Light properties - directly bound to settings
|
||||
readonly property var params: Settings.data.nightLight
|
||||
property var lastCommand: []
|
||||
|
||||
// Crash tracking for auto-restart
|
||||
property int _crashCount: 0
|
||||
property int _maxCrashes: 5
|
||||
|
||||
// Manual schedule tracking
|
||||
property bool _manualNightPhase: false
|
||||
|
||||
// Kill any stale wlsunset processes on startup to prevent issues after shell restart
|
||||
Component.onCompleted: {
|
||||
killStaleProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: killStaleProcess
|
||||
running: false
|
||||
command: ["pkill", "-x", "wlsunset"]
|
||||
onExited: function (code, status) {
|
||||
if (code === 0) {
|
||||
Logger.i("NightLight", "Killed stale wlsunset process from previous session");
|
||||
}
|
||||
// Now apply the settings after cleanup
|
||||
root.apply();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: restartTimer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (root.params.enabled && !runner.running) {
|
||||
Logger.w("NightLight", "Restarting after crash...");
|
||||
if (root.isManualMode()) {
|
||||
root.applyManualSchedule();
|
||||
} else {
|
||||
runner.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: manualScheduleTimer
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
Logger.i("NightLight", "Manual schedule: phase boundary reached");
|
||||
root.applyManualSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function timeToMinutes(timeStr) {
|
||||
var parts = timeStr.split(":").map(Number);
|
||||
return parts[0] * 60 + parts[1];
|
||||
}
|
||||
|
||||
function isManualMode() {
|
||||
return !params.forced && !params.autoSchedule;
|
||||
}
|
||||
|
||||
function isCurrentlyNight() {
|
||||
var now = new Date();
|
||||
var nowMin = now.getHours() * 60 + now.getMinutes();
|
||||
var sunsetMin = timeToMinutes(params.manualSunset);
|
||||
var sunriseMin = timeToMinutes(params.manualSunrise);
|
||||
|
||||
if (sunsetMin < sunriseMin) {
|
||||
// Inverted: e.g. sunset=03:00, sunrise=07:00 → night is [03:00, 07:00)
|
||||
return nowMin >= sunsetMin && nowMin < sunriseMin;
|
||||
} else {
|
||||
// Normal: e.g. sunset=18:00, sunrise=06:00 → night is [18:00, 06:00)
|
||||
return nowMin >= sunsetMin || nowMin < sunriseMin;
|
||||
}
|
||||
}
|
||||
|
||||
function msUntilNextBoundary() {
|
||||
var now = new Date();
|
||||
var nowMin = now.getHours() * 60 + now.getMinutes();
|
||||
var sunsetMin = timeToMinutes(params.manualSunset);
|
||||
var sunriseMin = timeToMinutes(params.manualSunrise);
|
||||
|
||||
var targetMin = isCurrentlyNight() ? sunriseMin : sunsetMin;
|
||||
var diffMin = targetMin - nowMin;
|
||||
if (diffMin <= 0)
|
||||
diffMin += 1440;
|
||||
|
||||
return diffMin * 60 * 1000 - now.getSeconds() * 1000 - now.getMilliseconds();
|
||||
}
|
||||
|
||||
function applyManualSchedule() {
|
||||
if (!params.enabled) {
|
||||
manualScheduleTimer.stop();
|
||||
runner.running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var night = isCurrentlyNight();
|
||||
_manualNightPhase = night;
|
||||
|
||||
if (night) {
|
||||
var cmd = ["wlsunset"];
|
||||
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`);
|
||||
cmd.push("-S", "23:59");
|
||||
cmd.push("-s", "00:00");
|
||||
cmd.push("-d", 1);
|
||||
|
||||
if (JSON.stringify(cmd) !== JSON.stringify(lastCommand) || !runner.running) {
|
||||
lastCommand = cmd;
|
||||
runner.command = cmd;
|
||||
runner.running = false;
|
||||
runner.running = true;
|
||||
}
|
||||
Logger.i("NightLight", "Manual schedule: night phase - wlsunset forced on");
|
||||
} else {
|
||||
lastCommand = [];
|
||||
runner.running = false;
|
||||
Logger.i("NightLight", "Manual schedule: day phase - wlsunset stopped");
|
||||
}
|
||||
|
||||
var ms = msUntilNextBoundary();
|
||||
manualScheduleTimer.interval = Math.max(ms, 1000);
|
||||
manualScheduleTimer.restart();
|
||||
Logger.i("NightLight", "Manual schedule: next boundary in " + Math.round(ms / 1000) + "s");
|
||||
}
|
||||
|
||||
function apply(force = false) {
|
||||
// If using LocationService, wait for it to be ready
|
||||
if (!params.forced && params.autoSchedule && !LocationService.coordinatesReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Manual mode: handle scheduling ourselves
|
||||
if (isManualMode() && params.enabled) {
|
||||
_crashCount = 0;
|
||||
restartTimer.stop();
|
||||
applyManualSchedule();
|
||||
return;
|
||||
}
|
||||
|
||||
// Not in manual mode - clean up manual timer
|
||||
manualScheduleTimer.stop();
|
||||
|
||||
var command = buildCommand();
|
||||
|
||||
// Compare with previous command to avoid unnecessary restart
|
||||
if (force || JSON.stringify(command) !== JSON.stringify(lastCommand)) {
|
||||
lastCommand = command;
|
||||
runner.command = command;
|
||||
|
||||
// Set running to false so it may restart below if still enabled
|
||||
runner.running = false;
|
||||
}
|
||||
runner.running = params.enabled;
|
||||
}
|
||||
|
||||
function buildCommand() {
|
||||
var cmd = ["wlsunset"];
|
||||
if (params.forced) {
|
||||
// Force immediate full night temperature regardless of time
|
||||
// Keep distinct day/night temps but set times so we're effectively always in "night"
|
||||
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`);
|
||||
// Night spans from sunset (00:00) to sunrise (23:59) covering almost the full day
|
||||
cmd.push("-S", "23:59"); // sunrise very late
|
||||
cmd.push("-s", "00:00"); // sunset at midnight
|
||||
// Near-instant transition
|
||||
cmd.push("-d", 1);
|
||||
} else if (params.autoSchedule) {
|
||||
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`);
|
||||
cmd.push("-l", `${LocationService.stableLatitude}`, "-L", `${LocationService.stableLongitude}`);
|
||||
cmd.push("-d", 60 * 15); // 15min progressive fade at sunset/sunrise
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// Observe setting changes and location readiness
|
||||
Connections {
|
||||
target: Settings.data.nightLight
|
||||
function onEnabledChanged() {
|
||||
apply();
|
||||
// Toast: night light toggled
|
||||
const enabled = !!Settings.data.nightLight.enabled;
|
||||
ToastService.showNotice(I18n.tr("common.night-light"), enabled ? I18n.tr("common.enabled") : I18n.tr("common.disabled"), enabled ? "nightlight-on" : "nightlight-off");
|
||||
}
|
||||
function onForcedChanged() {
|
||||
apply();
|
||||
if (Settings.data.nightLight.enabled) {
|
||||
ToastService.showNotice(I18n.tr("common.night-light"), Settings.data.nightLight.forced ? I18n.tr("toast.night-light.forced") : I18n.tr("toast.night-light.normal"), Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on");
|
||||
}
|
||||
}
|
||||
function onNightTempChanged() {
|
||||
apply();
|
||||
}
|
||||
function onDayTempChanged() {
|
||||
apply();
|
||||
}
|
||||
function onManualSunriseChanged() {
|
||||
apply();
|
||||
}
|
||||
function onManualSunsetChanged() {
|
||||
apply();
|
||||
}
|
||||
function onAutoScheduleChanged() {
|
||||
apply();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: LocationService
|
||||
function onCoordinatesReadyChanged() {
|
||||
if (LocationService.coordinatesReady) {
|
||||
root.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resumeRetryTimer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
Logger.i("NightLight", "Resume retry - re-applying night light again");
|
||||
root.apply(true);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Time
|
||||
function onResumed() {
|
||||
Logger.i("NightLight", "System resumed - re-applying night light");
|
||||
root.apply(true);
|
||||
resumeRetryTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// Foreground process runner
|
||||
Process {
|
||||
id: runner
|
||||
running: false
|
||||
onStarted: {
|
||||
Logger.i("NightLight", "Wlsunset started:", runner.command);
|
||||
// Reset crash count on successful start
|
||||
if (root._crashCount > 0) {
|
||||
root._crashCount = 0;
|
||||
}
|
||||
}
|
||||
onExited: function (code, status) {
|
||||
if (root.params.enabled && root.isManualMode()) {
|
||||
// Manual mode: only treat as crash if we're in the night phase
|
||||
if (root._manualNightPhase) {
|
||||
root._crashCount++;
|
||||
if (root._crashCount <= root._maxCrashes) {
|
||||
Logger.w("NightLight", "Wlsunset exited unexpectedly during manual night phase (code: " + code + "), restarting in 2s... (attempt " + root._crashCount + "/" + root._maxCrashes + ")");
|
||||
restartTimer.start();
|
||||
} else {
|
||||
Logger.e("NightLight", "Wlsunset crashed too many times (" + root._maxCrashes + "), giving up");
|
||||
}
|
||||
} else {
|
||||
Logger.i("NightLight", "Wlsunset exited (manual day phase):", code, status);
|
||||
root._crashCount = 0;
|
||||
}
|
||||
} else if (root.params.enabled) {
|
||||
// Non-manual mode: any exit while enabled is a crash
|
||||
root._crashCount++;
|
||||
if (root._crashCount <= root._maxCrashes) {
|
||||
Logger.w("NightLight", "Wlsunset exited unexpectedly (code: " + code + "), restarting in 2s... (attempt " + root._crashCount + "/" + root._maxCrashes + ")");
|
||||
restartTimer.start();
|
||||
} else {
|
||||
Logger.e("NightLight", "Wlsunset crashed too many times (" + root._maxCrashes + "), giving up");
|
||||
}
|
||||
} else {
|
||||
Logger.i("NightLight", "Wlsunset exited (disabled):", code, status);
|
||||
root._crashCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user