feat: init quickshell

This commit is contained in:
2025-07-13 22:10:17 +08:00
parent a63be876f7
commit 237a62ea8a
103 changed files with 14997 additions and 498 deletions

View File

@@ -0,0 +1,394 @@
import QtQuick
import Quickshell
import Quickshell.Io
// App launcher service - discovers and manages applications
Item {
id: appService
property var applications: []
property bool isLoading: false
// Categories for apps
property var categories: {
"AudioVideo": "🎵",
"Audio": "🎵",
"Video": "🎬",
"Development": "💻",
"Education": "📚",
"Game": "🎮",
"Graphics": "🎨",
"Network": "🌐",
"Office": "📄",
"Science": "🔬",
"Settings": "⚙️",
"System": "🔧",
"Utility": "🛠️",
"Other": "📦"
}
property string userName: ""
property string homeDirectory: ""
property bool userInfoLoaded: false
property var currentApp: ({})
property var pendingSearchPaths: []
Component.onCompleted: {
// First get user info, then load applications
loadUserInfo();
}
function loadUserInfo() {
userNameProcess.running = true;
}
Process {
id: userNameProcess
command: ["whoami"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.trim()) {
userName = line.trim();
}
}
}
onExited: {
// Now get home directory
homeDirProcess.running = true;
}
}
Process {
id: homeDirProcess
command: ["sh", "-c", "echo $HOME"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.trim()) {
homeDirectory = line.trim();
}
}
}
onExited: {
// Now we have user info, start loading applications
userInfoLoaded = true;
loadApplications();
}
}
function loadApplications() {
if (!userInfoLoaded) {
console.log("User info not loaded yet, skipping application scan");
return;
}
isLoading = true;
applications = [];
console.log("DEBUG: Starting application scan with user:", userName, "home:", homeDirectory);
// Comprehensive search paths for maximum Linux compatibility
appService.pendingSearchPaths = [
// User-specific locations (highest priority)
homeDirectory + "/.local/share/applications/",
// Standard FreeDesktop.org locations
"/usr/share/applications/", "/usr/local/share/applications/",
// Flatpak locations
"/var/lib/flatpak/exports/share/applications/", homeDirectory + "/.local/share/flatpak/exports/share/applications/",
// Snap locations
"/var/lib/snapd/desktop/applications/", "/snap/bin/",
// AppImage locations (common user directories)
homeDirectory + "/Applications/", homeDirectory + "/AppImages/",
// Distribution-specific paths
"/opt/*/share/applications/" // For manually installed software
, "/usr/share/applications/kde4/" // KDE4 legacy
,
// NixOS-specific (will be ignored on non-NixOS systems)
"/run/current-system/sw/share/applications/", "/etc/profiles/per-user/" + userName + "/share/applications/"];
console.log("DEBUG: Starting with essential paths:", JSON.stringify(appService.pendingSearchPaths));
// Add XDG and home-manager paths
getXdgDataDirs.running = true;
}
Process {
id: getXdgDataDirs
command: ["sh", "-c", "echo $XDG_DATA_DIRS"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.trim()) {
var xdgDirs = line.trim().split(":");
for (var i = 0; i < xdgDirs.length; i++) {
if (xdgDirs[i].trim()) {
var xdgPath = xdgDirs[i].trim() + "/applications/";
if (appService.pendingSearchPaths.indexOf(xdgPath) === -1) {
appService.pendingSearchPaths.push(xdgPath);
console.log("DEBUG: Added XDG path:", xdgPath);
}
}
}
}
}
}
onExited: {
// Now add home-manager path
getHomeManagerPaths.running = true;
}
}
Process {
id: getHomeManagerPaths
command: ["sh", "-c", "find /nix/store -maxdepth 1 -name '*home-manager-path*' -type d 2>/dev/null | head -1"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.trim()) {
var homeManagerPath = line.trim() + "/share/applications/";
appService.pendingSearchPaths.push(homeManagerPath);
console.log("DEBUG: Added home-manager path:", homeManagerPath);
}
}
}
onExited: {
// CRITICAL: Always ensure these essential directories are included
var essentialPaths = ["/run/current-system/sw/share/applications/", "/usr/share/applications/", "/usr/local/share/applications/"];
for (var i = 0; i < essentialPaths.length; i++) {
var path = essentialPaths[i];
if (appService.pendingSearchPaths.indexOf(path) === -1) {
appService.pendingSearchPaths.push(path);
console.log("DEBUG: Added missing essential path:", path);
}
}
// Start bulk parsing with all paths including XDG and home-manager
startBulkParsing(appService.pendingSearchPaths);
}
}
function startBulkParsing(searchPaths) {
// BULLETPROOF: Ensure critical system directories are always included
var criticalPaths = ["/run/current-system/sw/share/applications/", "/usr/share/applications/", "/usr/local/share/applications/"];
for (var i = 0; i < criticalPaths.length; i++) {
var path = criticalPaths[i];
if (searchPaths.indexOf(path) === -1) {
searchPaths.push(path);
console.log("DEBUG: BULLETPROOF: Added missing critical path:", path);
}
}
console.log("DEBUG: Final directories to scan:", searchPaths.join(", "));
// Single command to parse all .desktop files at once
// Only parse fields from the main [Desktop Entry] section, ignore [Desktop Action] sections
var cmd = 'for dir in ' + searchPaths.map(p => "'" + p + "'").join(" ") + '; do ' + 'if [ -d "$dir" ]; then ' + 'find "$dir" -name "*.desktop" 2>/dev/null | while read file; do ' + 'echo "===FILE:$file"; ' + 'sed -n \'/^\\[Desktop Entry\\]/,/^\\[.*\\]/{/^\\[Desktop Entry\\]/d; /^\\[.*\\]/q; /^Name=/p; /^Exec=/p; /^Icon=/p; /^Comment=/p; /^Categories=/p; /^Hidden=/p; /^NoDisplay=/p}\' "$file" 2>/dev/null || true; ' + 'done; ' + 'fi; ' + 'done';
bulkParseProcess.command = ["sh", "-c", cmd];
bulkParseProcess.running = true;
}
Process {
id: bulkParseProcess
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.startsWith("===FILE:")) {
// Start of new file
if (appService.currentApp.name && appService.currentApp.exec && !appService.currentApp.hidden && !appService.currentApp.noDisplay) {
applications.push(appService.currentApp);
}
appService.currentApp = {
name: "",
exec: "",
icon: "",
comment: "",
categories: [],
hidden: false,
noDisplay: false,
filePath: line.substring(8) // Remove "===FILE:" prefix
};
} else if (line.startsWith("Name=")) {
appService.currentApp.name = line.substring(5);
} else if (line.startsWith("Exec=")) {
appService.currentApp.exec = line.substring(5);
} else if (line.startsWith("Icon=")) {
appService.currentApp.icon = line.substring(5);
} else if (line.startsWith("Comment=")) {
appService.currentApp.comment = line.substring(8);
} else if (line.startsWith("Categories=")) {
appService.currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0);
} else if (line === "Hidden=true") {
appService.currentApp.hidden = true;
} else if (line === "NoDisplay=true") {
appService.currentApp.noDisplay = true;
}
}
}
onStarted: {
appService.currentApp = {};
}
onExited: {
// Process the last app
if (appService.currentApp.name && appService.currentApp.exec && !appService.currentApp.hidden && !appService.currentApp.noDisplay) {
applications.push(appService.currentApp);
}
console.log("DEBUG: Before deduplication: Found", applications.length, "applications");
// Deduplicate applications - prefer user installations over system ones
var uniqueApps = {};
var finalApps = [];
for (var i = 0; i < applications.length; i++) {
var app = applications[i];
var key = app.name + "|" + app.exec.split(" ")[0]; // Use name + base command as key
if (!uniqueApps[key]) {
// First occurrence of this app
uniqueApps[key] = app;
finalApps.push(app);
} else {
// Duplicate found - check if this version should replace the existing one
var existing = uniqueApps[key];
var shouldReplace = false;
// Priority order (higher priority replaces lower):
// 1. User local applications (highest priority)
// 2. Home-manager applications
// 3. User profile applications
// 4. System applications (lowest priority)
if (app.filePath.includes("/.local/share/applications/")) {
shouldReplace = true; // User local always wins
} else if (app.filePath.includes("home-manager-path") && !existing.filePath.includes("/.local/share/applications/")) {
shouldReplace = true; // Home-manager beats system
} else if (app.filePath.includes("/home/") && !existing.filePath.includes("/.local/share/applications/") && !existing.filePath.includes("home-manager-path")) {
shouldReplace = true; // User profile beats system
}
if (shouldReplace) {
// Replace the existing app in finalApps array
for (var j = 0; j < finalApps.length; j++) {
if (finalApps[j] === existing) {
finalApps[j] = app;
uniqueApps[key] = app;
break;
}
}
}
// If not replacing, just ignore the duplicate
}
}
applications = finalApps;
console.log("DEBUG: After deduplication: Found", applications.length, "unique applications");
isLoading = false;
applicationsChanged();
}
}
function launchApplication(app) {
if (!app || !app.exec)
return;
// Clean up the exec command (remove field codes like %f, %F, %u, %U)
var cleanExec = app.exec.replace(/%[fFuU]/g, "").trim();
launchProcess.command = ["sh", "-c", cleanExec];
launchProcess.running = true;
console.log("Launching:", app.name, "with command:", cleanExec);
}
Process {
id: launchProcess
running: false
onExited: {
if (exitCode !== 0) {
console.log("Failed to launch application, exit code:", exitCode);
}
}
}
// Fuzzy search function
function fuzzySearch(query, apps) {
if (!query || query.length === 0) {
return apps;
}
query = query.toLowerCase();
return apps.filter(app => {
var searchText = (app.name + " " + app.comment).toLowerCase();
// Simple fuzzy matching - check if all characters of query appear in order
var queryIndex = 0;
for (var i = 0; i < searchText.length && queryIndex < query.length; i++) {
if (searchText[i] === query[queryIndex]) {
queryIndex++;
}
}
return queryIndex === query.length;
}).sort((a, b) => {
// Sort by relevance - exact matches first, then by name
var aName = a.name.toLowerCase();
var bName = b.name.toLowerCase();
var aExact = aName.includes(query);
var bExact = bName.includes(query);
if (aExact && !bExact)
return -1;
if (!aExact && bExact)
return 1;
return aName.localeCompare(bName);
});
}
function getCategoryIcon(app) {
if (!app.categories || app.categories.length === 0) {
return categories["Other"];
}
// Find the first matching category
for (var i = 0; i < app.categories.length; i++) {
var category = app.categories[i];
if (categories[category]) {
return categories[category];
}
}
return categories["Other"];
}
}

View File

@@ -0,0 +1,155 @@
import QtQuick
import Quickshell.Io
import "root:/Data" as Data
// Matugen color integration service
Item {
id: service
property var shell
property var colors: ({})
property bool isLoaded: false
property int colorVersion: 0 // Increments every time colors update to force QML re-evaluation
// Signals to notify when colors change
signal matugenColorsUpdated
signal matugenColorsLoaded
// File watcher for the matugen quickshell-colors.qml
FileView {
id: matugenFile
path: "/home/imxyy/.config/quickshell/Data/quickshell-colors.qml"
blockWrites: true
onLoaded: {
parseColors(text());
}
onTextChanged: {
parseColors(text());
}
}
// Parse QML color definitions and map them to base16 colors
function parseColors(qmlText) {
if (!qmlText) {
console.warn("MatugenService: No QML content to parse");
return;
}
const lines = qmlText.split('\n');
const parsedColors = {};
// Extract readonly property color definitions
for (const line of lines) {
const match = line.match(/readonly\s+property\s+color\s+(\w+):\s*"(#[0-9a-fA-F]{6})"/);
if (match) {
const colorName = match[1];
const colorValue = match[2];
parsedColors[colorName] = colorValue;
}
}
// Detect if this is a light or dark theme based on surface luminance
const surfaceColor = parsedColors.surface || "#000000";
const isLightTheme = getLuminance(surfaceColor) > 0.5;
console.log(`MatugenService: Detected ${isLightTheme ? 'light' : 'dark'} theme from surface color: ${surfaceColor}`);
// Use Material Design 3 colors directly with better contrast
const baseMapping = {
base00: parsedColors.surface || (isLightTheme ? "#ffffff" : "#000000") // Background
,
base01: parsedColors.surface_container_low || (isLightTheme ? "#f8f9fa" : "#1a1a1a") // Panel bg
,
base02: parsedColors.surface_container || (isLightTheme ? "#e9ecef" : "#2a2a2a") // Selection
,
base03: parsedColors.surface_container_high || (isLightTheme ? "#dee2e6" : "#3a3a3a") // Border/separator
,
base04: parsedColors.on_surface_variant || (isLightTheme ? "#6c757d" : "#adb5bd") // Secondary text (better contrast)
,
base05: parsedColors.on_surface || (isLightTheme ? "#212529" : "#f8f9fa") // Primary text (high contrast)
,
base06: parsedColors.on_background || (isLightTheme ? "#000000" : "#ffffff") // Bright text
,
base07: isLightTheme ? parsedColors.surface_container_lowest || "#ffffff" : parsedColors.surface_bright || "#ffffff" // Brightest
,
base08: isLightTheme ? parsedColors.on_error || "#dc3545" : parsedColors.error || "#ff6b6b" // Error (theme appropriate)
,
base09: parsedColors.tertiary || (isLightTheme ? "#6f42c1" : "#a855f7") // Purple
,
base0A: parsedColors.primary_fixed || (isLightTheme ? "#fd7e14" : "#fbbf24") // Orange/Yellow
,
base0B: parsedColors.secondary || (isLightTheme ? "#198754" : "#10b981") // Green
,
base0C: parsedColors.surface_tint || (isLightTheme ? "#0dcaf0" : "#06b6d4") // Cyan
,
base0D: parsedColors.primary_container || (isLightTheme ? "#0d6efd" : "#3b82f6") // Blue
,
base0E: parsedColors.primary || (isLightTheme ? "#6610f2" : parsedColors.secondary || "#8b5cf6") // Accent - use primary for light, secondary for dark
,
base0F: parsedColors.scrim || "#000000" // Special/black
};
// Create the theme object
const theme = Object.assign({
name: isLightTheme ? "Matugen Light" : "Matugen Dark",
type: isLightTheme ? "light" : "dark"
}, baseMapping);
// Store colors in the appropriate theme slot
colors = {
raw: parsedColors,
[isLightTheme ? 'light' : 'dark']: theme,
// Keep the other theme as null or use fallback
[isLightTheme ? 'dark' : 'light']: null
};
isLoaded = true;
colorVersion++; // Increment version to force QML property updates
console.log("MatugenService: Colors loaded successfully from QML (version " + colorVersion + ")");
console.log("Available colors:", Object.keys(parsedColors).join(", "));
// Emit signals to notify theme system
matugenColorsUpdated();
matugenColorsLoaded();
}
// Calculate luminance of a hex color
function getLuminance(hexColor) {
// Remove # if present
const hex = hexColor.replace('#', '');
// Convert to RGB
const r = parseInt(hex.substr(0, 2), 16) / 255;
const g = parseInt(hex.substr(2, 2), 16) / 255;
const b = parseInt(hex.substr(4, 2), 16) / 255;
// Calculate relative luminance
const rs = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
const gs = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
const bs = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
// Reload colors from file
function reloadColors() {
matugenFile.reload();
}
// Get specific color by name
function getColor(colorName) {
return colors.raw ? colors.raw[colorName] : null;
}
// Check if matugen colors are available
function isAvailable() {
return isLoaded && colors.raw && Object.keys(colors.raw).length > 0;
}
Component.onCompleted: {
console.log("MatugenService: Initialized, watching quickshell-colors.qml");
}
}

View File

@@ -0,0 +1,97 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "root:/Data/" as Data
// Notification service with app filtering
Item {
id: service
property var shell
property alias notificationServer: notificationServer
property int maxHistorySize: Data.Settings.historyLimit
property int cleanupThreshold: maxHistorySize + 10
// Periodic cleanup every 30 minutes
Timer {
interval: 1800000
running: true
repeat: true
onTriggered: cleanupNotifications()
}
function cleanupNotifications() {
if (shell.notificationHistory && shell.notificationHistory.count > cleanupThreshold) {
const removeCount = shell.notificationHistory.count - maxHistorySize;
shell.notificationHistory.remove(maxHistorySize, removeCount);
}
// Remove invalid entries
if (shell.notificationHistory) {
for (let i = shell.notificationHistory.count - 1; i >= 0; i--) {
const item = shell.notificationHistory.get(i);
if (!item || !item.appName) {
shell.notificationHistory.remove(i);
}
}
}
}
NotificationServer {
id: notificationServer
actionsSupported: true
bodyMarkupSupported: true
imageSupported: true
keepOnReload: false
persistenceSupported: true
onNotification: notification => {
// Filter empty notifications
if (!notification.appName && !notification.summary && !notification.body) {
if (typeof notification.dismiss === 'function') {
notification.dismiss();
}
return;
}
// Filter ignored applications (case-insensitive)
var shouldIgnore = false;
if (notification.appName && Data.Settings.ignoredApps && Data.Settings.ignoredApps.length > 0) {
for (var i = 0; i < Data.Settings.ignoredApps.length; i++) {
if (Data.Settings.ignoredApps[i].toLowerCase() === notification.appName.toLowerCase()) {
shouldIgnore = true;
break;
}
}
}
if (shouldIgnore) {
if (typeof notification.dismiss === 'function') {
notification.dismiss();
}
return;
}
// Add to history and cleanup if needed
if (shell.notificationHistory) {
shell.addToNotificationHistory(notification, maxHistorySize);
if (shell.notificationHistory.count > cleanupThreshold) {
cleanupNotifications();
}
}
// Show notification window
if (shell.notificationWindow && shell.notificationWindow.screen === Quickshell.primaryScreen) {
shell.notificationWindow.visible = true;
}
}
}
Component.onDestruction: {
if (shell.notificationHistory) {
shell.notificationHistory.clear();
}
}
}

View File

@@ -0,0 +1,267 @@
import QtQuick
import "root:/Data" as Data
// Weather service using Open-Meteo API
Item {
id: service
property var shell
property string city: Data.Settings.weatherLocation
property bool isAmerican: Data.Settings.useFahrenheit
property int updateInterval: 3600 // 1 hour to reduce API calls
property string weatherDescription: ""
property var weather: null
property Timer retryTimer: Timer {
interval: 30000
repeat: false
running: false
onTriggered: getGeocoding()
}
Timer {
interval: service.updateInterval * 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: getGeocoding()
}
// Watch for settings changes and refresh weather data
Connections {
target: Data.Settings
function onWeatherLocationChanged() {
console.log("Weather location changed to:", Data.Settings.weatherLocation);
retryTimer.stop();
getGeocoding();
}
function onUseFahrenheitChanged() {
console.log("Temperature unit changed to:", Data.Settings.useFahrenheit ? "Fahrenheit" : "Celsius");
retryTimer.stop();
getGeocoding();
}
}
// WMO weather code descriptions (Open-Meteo standard)
property var weatherConsts: {
"omapiCodeDesc": {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Fog",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
56: "Light freezing drizzle",
57: "Dense freezing drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
66: "Light freezing rain",
67: "Heavy freezing rain",
71: "Slight snow fall",
73: "Moderate snow fall",
75: "Heavy snow fall",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail"
}
}
function getTemp(temp, tempUnit) {
return temp + tempUnit;
}
function updateWeather() {
if (!weather || !weather.current || !weather.current_units) {
console.warn("Weather data incomplete, skipping update");
return;
}
const weatherCode = weather.current.weather_code;
const temp = getTemp(Math.round(weather.current.temperature_2m || 0), weather.current_units.temperature_2m || "°C");
// Build 3-day forecast
const forecast = [];
const today = new Date();
if (weather.daily && weather.daily.time && weather.daily.weather_code && weather.daily.temperature_2m_min && weather.daily.temperature_2m_max) {
for (let i = 0; i < Math.min(3, weather.daily.time.length); i++) {
let dayName;
if (i === 0) {
dayName = "Today";
} else if (i === 1) {
dayName = "Tomorrow";
} else {
const futureDate = new Date(today);
futureDate.setDate(today.getDate() + i);
dayName = Qt.formatDate(futureDate, "ddd MMM d");
}
const dailyWeatherCode = weather.daily.weather_code[i];
const condition = weatherConsts.omapiCodeDesc[dailyWeatherCode] || "Unknown";
forecast.push({
dayName: dayName,
condition: condition,
minTemp: Math.round(weather.daily.temperature_2m_min[i]),
maxTemp: Math.round(weather.daily.temperature_2m_max[i])
});
}
}
// Update shell weather data in expected format
shell.weatherData = {
location: city,
currentTemp: temp,
currentCondition: weatherConsts.omapiCodeDesc[weatherCode] || "Unknown",
details: ["Wind: " + Math.round(weather.current.wind_speed_10m || 0) + " km/h"],
forecast: forecast
};
weatherDescription = weatherConsts.omapiCodeDesc[weatherCode] || "Unknown";
shell.weatherLoading = false;
}
// XHR pool to prevent memory leaks
property var activeXHRs: []
function cleanupXHR(xhr) {
if (xhr) {
xhr.abort();
xhr.onreadystatechange = null;
xhr.onerror = null;
const index = activeXHRs.indexOf(xhr);
if (index > -1) {
activeXHRs.splice(index, 1);
}
}
}
function getGeocoding() {
if (!city || city.trim() === "") {
console.warn("Weather location is empty, skipping weather request");
shell.weatherLoading = false;
return;
}
shell.weatherLoading = true;
const xhr = new XMLHttpRequest();
activeXHRs.push(xhr);
xhr.open("GET", `https://geocoding-api.open-meteo.com/v1/search?name=${city}&count=1&language=en&format=json`);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
const geocoding = JSON.parse(xhr.responseText);
if (geocoding.results && geocoding.results.length > 0) {
const lat = geocoding.results[0].latitude;
const lng = geocoding.results[0].longitude;
getWeather(lat, lng);
} else {
console.warn("No geocoding results found for location:", city);
retryTimer.running = true;
shell.weatherLoading = false;
}
} catch (e) {
console.error("Failed to parse geocoding response:", e);
retryTimer.running = true;
shell.weatherLoading = false;
}
} else if (xhr.status === 0) {
// Silent handling of network issues
if (!retryTimer.running) {
console.warn("Weather service: Network unavailable, will retry automatically");
}
retryTimer.running = true;
shell.weatherLoading = false;
} else {
console.error("Geocoding request failed with status:", xhr.status);
retryTimer.running = true;
shell.weatherLoading = false;
}
cleanupXHR(xhr);
}
};
xhr.onerror = function () {
console.error("Geocoding request failed with network error");
retryTimer.running = true;
shell.weatherLoading = false;
cleanupXHR(xhr);
};
xhr.send();
}
function getWeather(lat, lng) {
const xhr = new XMLHttpRequest();
activeXHRs.push(xhr);
xhr.open("GET", `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current=temperature_2m,is_day,weather_code,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min,weather_code&forecast_days=3&temperature_unit=` + (isAmerican ? "fahrenheit" : "celsius"));
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
weather = JSON.parse(xhr.responseText);
updateWeather();
} catch (e) {
console.error("Failed to parse weather response:", e);
retryTimer.running = true;
shell.weatherLoading = false;
}
} else if (xhr.status === 0) {
// Silent handling of network issues
if (!retryTimer.running) {
console.warn("Weather service: Network unavailable for weather data");
}
retryTimer.running = true;
shell.weatherLoading = false;
} else {
console.error("Weather request failed with status:", xhr.status);
retryTimer.running = true;
shell.weatherLoading = false;
}
cleanupXHR(xhr);
}
};
xhr.onerror = function () {
console.error("Weather request failed with network error");
retryTimer.running = true;
shell.weatherLoading = false;
cleanupXHR(xhr);
};
xhr.send();
}
function loadWeather() {
getGeocoding();
}
Component.onCompleted: getGeocoding()
Component.onDestruction: {
// Cleanup all active XHR requests
for (let i = 0; i < activeXHRs.length; i++) {
if (activeXHRs[i]) {
activeXHRs[i].abort();
activeXHRs[i].onreadystatechange = null;
activeXHRs[i].onerror = null;
}
}
activeXHRs = [];
weather = null;
shell.weatherData = null;
weatherDescription = "";
}
}