quickshell: should be everything I want now
This commit is contained in:
@@ -4,6 +4,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Utils
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -60,6 +61,7 @@ Singleton {
|
||||
|
||||
function onMutedChanged() {
|
||||
root._muted = (sink?.audio.muted ?? true)
|
||||
Logger.log("AudioService", "OnMuteChanged:", root._muted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ Singleton {
|
||||
|
||||
function onMutedChanged() {
|
||||
root._inputMuted = (source?.audio.muted ?? true)
|
||||
Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ Singleton {
|
||||
sink.audio.muted = false
|
||||
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
Logger.warn("AudioService", "No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +104,7 @@ Singleton {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
Logger.warn("AudioService", "No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +114,7 @@ Singleton {
|
||||
source.audio.muted = false
|
||||
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
Logger.warn("AudioService", "No source available")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +122,7 @@ Singleton {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
Logger.warn("AudioService", "No source available")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -44,6 +45,10 @@ Singleton {
|
||||
|
||||
reloadableId: "brightness"
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Brightness", "Service started")
|
||||
}
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = []
|
||||
ddcProc.running = true
|
||||
@@ -80,7 +85,7 @@ Singleton {
|
||||
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
|
||||
var model = modelMatch ? modelMatch[1] : "Unknown"
|
||||
var bus = busMatch ? busMatch[1] : "Unknown"
|
||||
console.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
|
||||
Logger.log("Brigthness", "Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
|
||||
return {
|
||||
"model": model,
|
||||
"busNum": bus,
|
||||
@@ -188,7 +193,7 @@ Singleton {
|
||||
var val = parseInt(dataText)
|
||||
if (!isNaN(val)) {
|
||||
monitor.brightness = val / 101
|
||||
console.log("Apple display brightness:", monitor.brightness)
|
||||
Logger.log("Brightness", "Apple display brightness:", monitor.brightness)
|
||||
}
|
||||
} else if (monitor.isDdc) {
|
||||
var parts = dataText.split(" ")
|
||||
@@ -197,7 +202,7 @@ Singleton {
|
||||
var max = parseInt(parts[4])
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.brightness = current / max
|
||||
console.log("DDC brightness:", monitor.brightness)
|
||||
Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -213,8 +218,8 @@ Singleton {
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max
|
||||
monitor.brightness = current / max
|
||||
console.log("Internal brightness:", monitor.brightness)
|
||||
console.log("Using backlight device:", monitor.backlightDevice)
|
||||
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
||||
Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -136,10 +137,10 @@ Singleton {
|
||||
if (isInhibited)
|
||||
isInhibited = false;
|
||||
|
||||
console.log("Inhibitor process exited with code:", exitCode, "status:", exitStatus);
|
||||
Logger.log("Caffeine", "Inhibitor process exited with code:", exitCode, "status:", exitStatus);
|
||||
}
|
||||
onStarted: function() {
|
||||
console.log("Inhibitor process started with strategy:", root.strategy);
|
||||
Logger.log("Caffeine", "Inhibitor process started with PID:", inhibitorProcess.processId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
quickshell/Services/IPCService.qml
Normal file
35
quickshell/Services/IPCService.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
|
||||
Item {
|
||||
IpcHandler {
|
||||
function setPrimary(color: color) {
|
||||
SettingsService.primaryColor = color;
|
||||
}
|
||||
|
||||
target: "colors"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggleCalendar() {
|
||||
calendarPanel.toggle();
|
||||
}
|
||||
|
||||
function toggleControlCenter() {
|
||||
controlCenterPanel.toggle();
|
||||
}
|
||||
|
||||
target: "panels"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggleBarLyrics() {
|
||||
SettingsService.showLyricsBar = !SettingsService.showLyricsBar;
|
||||
}
|
||||
|
||||
target: "lyrics"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -9,28 +10,93 @@ Singleton {
|
||||
property real fetchInterval: 30 // in s
|
||||
property real fetchTimeout: 10 // in s
|
||||
property string ipURL: "https://api.uyanide.com/ip"
|
||||
property string geoURL: "curl https://api.ipinfo.io/lite/"
|
||||
property string geoURL: "https://api.ipinfo.io/lite/"
|
||||
property string geoURLToken: ""
|
||||
|
||||
function fetchIP() {
|
||||
if (fetchIPProcess.running) {
|
||||
console.warn("Fetch IP process is still running, skipping fetchIP");
|
||||
return ;
|
||||
}
|
||||
fetchIPProcess.running = true;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.timeout = fetchTimeout * 1000;
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response && response.ip) {
|
||||
let newIP = response.ip;
|
||||
Logger.log("IpService", "Fetched IP: " + newIP);
|
||||
if (newIP !== ip) {
|
||||
ip = newIP;
|
||||
fetchGeoInfo(); // Fetch geo info only if IP has changed
|
||||
}
|
||||
} else {
|
||||
ip = "N/A";
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "IP response does not contain 'ip' field");
|
||||
}
|
||||
} catch (e) {
|
||||
ip = "N/A";
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "Failed to parse IP response: " + e);
|
||||
}
|
||||
} else {
|
||||
ip = "N/A";
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "Failed to fetch IP, status: " + xhr.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.ontimeout = function() {
|
||||
ip = "N/A";
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "Fetch IP request timed out");
|
||||
};
|
||||
xhr.open("GET", ipURL);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function fetchGeoInfo() {
|
||||
if (fetchGeoProcess.running) {
|
||||
console.warn("Fetch geo process is still running, skipping fetchGeoInfo");
|
||||
return ;
|
||||
}
|
||||
if (!ip || ip === "N/A") {
|
||||
countryCode = "N/A";
|
||||
return ;
|
||||
}
|
||||
fetchGeoProcess.command = ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${geoURL}${ip}${geoURLToken ? "?token=" + geoURLToken : ""}`];
|
||||
fetchGeoProcess.running = true;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.timeout = fetchTimeout * 1000;
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response && response.country) {
|
||||
let newCountryCode = response.country_code;
|
||||
Logger.log("IpService", "Fetched country code: " + newCountryCode);
|
||||
if (newCountryCode !== countryCode) {
|
||||
countryCode = newCountryCode;
|
||||
SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
|
||||
}
|
||||
} else {
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "Geo response does not contain 'country' field");
|
||||
}
|
||||
} catch (e) {
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "Failed to parse geo response: " + e);
|
||||
}
|
||||
} else {
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "Failed to fetch geo info, status: " + xhr.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.ontimeout = function() {
|
||||
countryCode = "N/A";
|
||||
Logger.error("IpService", "Fetch geo info request timed out");
|
||||
};
|
||||
let url = geoURL + ip;
|
||||
if (geoURLToken)
|
||||
url += "?token=" + geoURLToken;
|
||||
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
@@ -46,11 +112,11 @@ Singleton {
|
||||
FileView {
|
||||
id: tokenFile
|
||||
|
||||
path: Qt.resolvedUrl("../Assets/Ip/token.txt")
|
||||
path: Qt.resolvedUrl("../Assets/Config/GeoInfoToken.txt")
|
||||
onLoaded: {
|
||||
geoURLToken = tokenFile.text();
|
||||
if (!geoURLToken)
|
||||
console.warn("No token found for geoIP service, assuming none is required");
|
||||
Logger.warn("IpService", "No token found for geoIP service, assuming none is required");
|
||||
|
||||
fetchIP();
|
||||
fetchTimer.start();
|
||||
@@ -68,64 +134,4 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchIPProcess
|
||||
|
||||
command: ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${ipURL}`]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
let newIP = "";
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response && response.ip) {
|
||||
newIP = response.ip;
|
||||
console.log("Fetched IP: " + newIP);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse IP response: " + e);
|
||||
}
|
||||
if (newIP && newIP !== ip) {
|
||||
ip = newIP;
|
||||
fetchGeoInfo();
|
||||
} else if (!newIP) {
|
||||
ip = "N/A";
|
||||
countryCode = "N/A";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchGeoProcess
|
||||
|
||||
command: []
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
let newCountryCode = "";
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response && response.country) {
|
||||
newCountryCode = response.country_code;
|
||||
console.log("Fetched country code: " + newCountryCode);
|
||||
SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse geo response: " + e);
|
||||
}
|
||||
if (newCountryCode && newCountryCode !== countryCode)
|
||||
countryCode = newCountryCode;
|
||||
else if (!newCountryCode)
|
||||
countryCode = "N/A";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
323
quickshell/Services/LocationService.qml
Normal file
323
quickshell/Services/LocationService.qml
Normal file
@@ -0,0 +1,323 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
// Weather logic and caching with stable UI properties
|
||||
Singleton {
|
||||
//console.log(JSON.stringify(weatherData))
|
||||
|
||||
id: root
|
||||
|
||||
property string locationName: "Munich"
|
||||
property string locationFile: Qt.resolvedUrl("../Assets/Config/Location.json")
|
||||
property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds
|
||||
property bool isFetchingWeather: false
|
||||
readonly property alias data: adapter // Used to access via LocationService.data.xxx from outside, best to use "adapter" inside the service.
|
||||
// Stable UI properties - only updated when location is fully resolved
|
||||
property bool coordinatesReady: false
|
||||
property string stableLatitude: ""
|
||||
property string stableLongitude: ""
|
||||
property string stableName: ""
|
||||
// Helper property for UI components (outside JsonAdapter to avoid binding loops)
|
||||
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}`;
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function init() {
|
||||
// does nothing but ensure the singleton is created
|
||||
// do not remove
|
||||
Logger.log("Location", "Service started");
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function resetWeather() {
|
||||
Logger.log("Location", "Resetting weather data");
|
||||
// Mark as changing to prevent UI updates
|
||||
root.coordinatesReady = false;
|
||||
// Reset stable properties
|
||||
root.stableLatitude = "";
|
||||
root.stableLongitude = "";
|
||||
root.stableName = "";
|
||||
// Reset core data
|
||||
adapter.latitude = "";
|
||||
adapter.longitude = "";
|
||||
adapter.name = "";
|
||||
adapter.weatherLastFetch = 0;
|
||||
adapter.weather = null;
|
||||
// Try to fetch immediately
|
||||
updateWeather();
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function updateWeather() {
|
||||
if (isFetchingWeather) {
|
||||
Logger.warn("Location", "Weather is still fetching");
|
||||
return ;
|
||||
}
|
||||
if ((adapter.weatherLastFetch === "") || (adapter.weather === null) || (adapter.latitude === "") || (adapter.longitude === "") || (adapter.name !== root.locationName) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency))
|
||||
getFreshWeather();
|
||||
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function getFreshWeather() {
|
||||
isFetchingWeather = true;
|
||||
// Check if location name has changed
|
||||
const locationChanged = data.name !== root.locationName;
|
||||
if (locationChanged) {
|
||||
root.coordinatesReady = false;
|
||||
Logger.log("Location", "Location changed from", adapter.name, "to", root.locationName);
|
||||
}
|
||||
if ((adapter.latitude === "") || (adapter.longitude === "") || locationChanged)
|
||||
_geocodeLocation(root.locationName, function(latitude, longitude, name, country) {
|
||||
Logger.log("Location", "Geocoded", root.locationName, "to:", latitude, "/", longitude);
|
||||
// Save location name
|
||||
adapter.name = root.locationName;
|
||||
// Save GPS coordinates
|
||||
adapter.latitude = latitude.toString();
|
||||
adapter.longitude = longitude.toString();
|
||||
root.stableName = `${name}, ${country}`;
|
||||
_fetchWeather(latitude, longitude, errorCallback);
|
||||
}, errorCallback);
|
||||
else
|
||||
_fetchWeather(adapter.latitude, adapter.longitude, errorCallback);
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function _geocodeLocation(locationName, callback, errorCallback) {
|
||||
Logger.log("Location", "Geocoding location name");
|
||||
var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(locationName) + "&language=en&format=json";
|
||||
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.open("GET", geoUrl);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function _fetchWeather(latitude, longitude, errorCallback) {
|
||||
Logger.log("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&daily=temperature_2m_max,temperature_2m_min,weathercode&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);
|
||||
// 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.log("Location", "Cached weather to disk - stable coordinates updated");
|
||||
} catch (e) {
|
||||
errorCallback("Location", "Failed to parse weather data");
|
||||
}
|
||||
} else {
|
||||
errorCallback("Location", "Weather fetch error: " + xhr.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function errorCallback(module, message) {
|
||||
Logger.error(module, message);
|
||||
isFetchingWeather = false;
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
function weatherSymbolFromCode(code) {
|
||||
if (code === 0)
|
||||
return "weather-sun";
|
||||
|
||||
if (code === 1 || code === 2)
|
||||
return "weather-cloud-sun";
|
||||
|
||||
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 >= 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 weatherColorFromCode(code) {
|
||||
// Clear sky - bright yellow
|
||||
if (code === 0)
|
||||
return Colors.yellow;
|
||||
|
||||
// Mainly clear/Partly cloudy - soft peach/rosewater tones
|
||||
if (code === 1 || code === 2)
|
||||
return Colors.peach;
|
||||
|
||||
// Overcast - neutral sky blue
|
||||
if (code === 3)
|
||||
return Colors.sky;
|
||||
|
||||
// Fog - soft lavender/muted tone
|
||||
if (code >= 45 && code <= 48)
|
||||
return Colors.lavender;
|
||||
|
||||
// Drizzle - light blue/sapphire
|
||||
if (code >= 51 && code <= 67)
|
||||
return Colors.sapphire;
|
||||
|
||||
// Snow - cool teal
|
||||
if (code >= 71 && code <= 77)
|
||||
return Colors.teal;
|
||||
|
||||
// Rain showers - deeper blue
|
||||
if (code >= 80 && code <= 82)
|
||||
return Colors.blue;
|
||||
|
||||
// Snow showers - teal
|
||||
if (code >= 85 && code <= 86)
|
||||
return Colors.teal;
|
||||
|
||||
// Thunderstorm - dramatic mauve/pink
|
||||
if (code >= 95 && code <= 99)
|
||||
return Colors.mauve;
|
||||
|
||||
// Default - sky blue
|
||||
return Colors.sky;
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
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;
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: locationFileView
|
||||
|
||||
path: locationFile
|
||||
printErrors: false
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
onLoaded: {
|
||||
Logger.log("Location", "Loaded cached data");
|
||||
// Initialize stable properties on load
|
||||
if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) {
|
||||
root.stableLatitude = adapter.latitude;
|
||||
root.stableLongitude = adapter.longitude;
|
||||
root.stableName = adapter.name;
|
||||
root.coordinatesReady = true;
|
||||
Logger.log("Location", "Coordinates ready");
|
||||
}
|
||||
updateWeather();
|
||||
}
|
||||
onLoadFailed: function(error) {
|
||||
updateWeather();
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
// Core data properties
|
||||
property string latitude: ""
|
||||
property string longitude: ""
|
||||
property string name: ""
|
||||
property int weatherLastFetch: 0
|
||||
property var weather: null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Every 20s check if we need to fetch new weather
|
||||
Timer {
|
||||
id: updateTimer
|
||||
|
||||
interval: 20 * 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
updateWeather();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
running: false
|
||||
interval: 1000
|
||||
onTriggered: locationFileView.writeAdapter()
|
||||
}
|
||||
|
||||
}
|
||||
144
quickshell/Services/LyricsService.qml
Normal file
144
quickshell/Services/LyricsService.qml
Normal file
@@ -0,0 +1,144 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
property int linesCount: 3
|
||||
property int linesAhead: linesCount / 2
|
||||
property int currentIndex: linesCount - linesAhead - 1
|
||||
property string offsetFile: Qt.resolvedUrl("../Assets/Config/LyricsOffset.txt")
|
||||
property int offset: 0 // in ms
|
||||
property int offsetStep: 500 // in ms
|
||||
property int referenceCount: 0
|
||||
// with linesCount=3 and linesAhead=1, lyrics will be like:
|
||||
// line 1
|
||||
// line 2 <- current line
|
||||
// line 3
|
||||
property var lyrics: Array(linesCount).fill(" ")
|
||||
|
||||
function startSyncing() {
|
||||
referenceCount++;
|
||||
Logger.log("LyricsService", "Reference count:", referenceCount);
|
||||
if (referenceCount === 1) {
|
||||
Logger.log("LyricsService", "Starting lyrics syncing");
|
||||
// fill lyrics with empty lines
|
||||
lyrics = Array(linesCount).fill(" ");
|
||||
listenProcess.exec(["sh", "-c", `sl-wrap listen -l ${linesCount} -a ${linesAhead} -f ${offsetFile.slice(7)}`]);
|
||||
}
|
||||
}
|
||||
|
||||
function stopSyncing() {
|
||||
referenceCount--;
|
||||
Logger.log("LyricsService", "Reference count:", referenceCount);
|
||||
if (referenceCount <= 0) {
|
||||
Logger.log("LyricsService", "Stopping lyrics syncing");
|
||||
// Execute again to stop
|
||||
// kinda ugly but works, but meanwhile:
|
||||
// listenProcess.signal(9)
|
||||
// listenProcess.signal(15)
|
||||
// listenProcess.running = false
|
||||
// counts on exec() to terminate previous exec()
|
||||
// all don't work
|
||||
listenProcess.exec(["sh", "-c", `sl-wrap trackid`]);
|
||||
}
|
||||
}
|
||||
|
||||
function writeOffset() {
|
||||
offsetFileView.setText(String(offset));
|
||||
}
|
||||
|
||||
function increaseOffset() {
|
||||
offset += offsetStep;
|
||||
}
|
||||
|
||||
function decreaseOffset() {
|
||||
offset -= offsetStep;
|
||||
}
|
||||
|
||||
function resetOffset() {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
action.command = ["sh", "-c", "spotify-lyrics clear $(spotify-lyrics trackid)"];
|
||||
action.startDetached();
|
||||
}
|
||||
|
||||
function showLyricsText() {
|
||||
action.command = ["sh", "-c", "ghostty -e sh -c 'spotify-lyrics fetch | less'"];
|
||||
action.startDetached();
|
||||
}
|
||||
|
||||
onOffsetChanged: {
|
||||
if (SettingsService.showLyricsBar)
|
||||
SendNotification.show("Lyrics Offset Changed", `Current offset: ${offset} ms`, 1000);
|
||||
|
||||
writeOffset();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: listenProcess
|
||||
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
lyrics = data.split("\n").slice(0, linesCount);
|
||||
if (lyrics.length < linesCount) {
|
||||
// fill with empty lines if not enough
|
||||
for (let i = lyrics.length; i < linesCount; i++) {
|
||||
lyrics[i] = " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: offsetFileView
|
||||
|
||||
path: offsetFile
|
||||
watchChanges: false
|
||||
onLoaded: {
|
||||
try {
|
||||
const fileContents = text();
|
||||
if (fileContents.length > 0) {
|
||||
const val = parseInt(fileContents);
|
||||
if (!isNaN(val)) {
|
||||
offset = val;
|
||||
Logger.log("LyricsService", "Loaded offset:", offset);
|
||||
} else {
|
||||
offset = 0;
|
||||
writeOffset();
|
||||
}
|
||||
} else {
|
||||
offset = 0;
|
||||
writeOffset();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.log("LyricsService", "Error reading offset file:", e);
|
||||
}
|
||||
}
|
||||
onLoadFailed: {
|
||||
Logger.log("LyricsService", "Error loading offset file:", errorString);
|
||||
}
|
||||
onSaveFailed: {
|
||||
Logger.log("LyricsService", "Error saving offset file:", errorString);
|
||||
}
|
||||
onSaved: {
|
||||
Logger.log("LyricsService", "Offset file saved.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Modules.Misc
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -11,6 +12,7 @@ Singleton {
|
||||
property var currentPlayer: null
|
||||
property real currentPosition: 0
|
||||
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
|
||||
property int selectedPlayerIndex: -1
|
||||
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
|
||||
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
|
||||
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
|
||||
@@ -59,11 +61,25 @@ Singleton {
|
||||
|
||||
// Updates currentPlayer and currentPosition
|
||||
function updateCurrentPlayer() {
|
||||
// Use selected player if index is valid
|
||||
if (selectedPlayerIndex >= 0) {
|
||||
let availablePlayers = getAvailablePlayers();
|
||||
if (selectedPlayerIndex < availablePlayers.length) {
|
||||
currentPlayer = availablePlayers[selectedPlayerIndex];
|
||||
currentPosition = currentPlayer.position;
|
||||
Logger.log("MusicManager", "Current player set by index:", currentPlayer ? currentPlayer.identity : "None");
|
||||
return ;
|
||||
} else {
|
||||
selectedPlayerIndex = -1; // Reset if index is out of range
|
||||
}
|
||||
}
|
||||
// Otherwise, find active player
|
||||
let newPlayer = findActivePlayer();
|
||||
if (newPlayer !== currentPlayer) {
|
||||
currentPlayer = newPlayer;
|
||||
currentPosition = currentPlayer ? currentPlayer.position : 0;
|
||||
}
|
||||
Logger.log("MusicManager", "Current player updated:", currentPlayer ? currentPlayer.identity : "None");
|
||||
}
|
||||
|
||||
// Player control functions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
@@ -66,7 +67,7 @@ Singleton {
|
||||
});
|
||||
root.workspaces = workspacesList;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse workspaces:", e, line);
|
||||
Logger.error("Niri", "Failed to parse workspaces:", e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +103,7 @@ Singleton {
|
||||
}
|
||||
root.windows = windowsMap;
|
||||
} catch (e) {
|
||||
console.error("Error parsing windows event:", e);
|
||||
Logger.error("Niri", "Error parsing windows event:", e);
|
||||
}
|
||||
} else if (event.WorkspaceActivated) {
|
||||
workspaceProcess.running = true;
|
||||
@@ -120,13 +121,13 @@ Singleton {
|
||||
root.focusedWindowId = -1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window focus event:", e);
|
||||
Logger.error("Niri", "Error parsing window focus event:", e);
|
||||
}
|
||||
} else if (event.OverviewOpenedOrClosed) {
|
||||
try {
|
||||
root.inOverview = event.OverviewOpenedOrClosed.is_open === true;
|
||||
} catch (e) {
|
||||
console.error("Error parsing overview state:", e);
|
||||
Logger.error("Niri", "Error parsing overview state:", e);
|
||||
}
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
try {
|
||||
@@ -161,7 +162,7 @@ Singleton {
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window opened/changed event:", e);
|
||||
Logger.error("Niri", "Error parsing window opened/changed event:", e);
|
||||
}
|
||||
} else if (event.windowClosed) {
|
||||
try {
|
||||
@@ -170,11 +171,11 @@ Singleton {
|
||||
delete root.windows[closedId];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window closed event:", e);
|
||||
Logger.error("Niri", "Error parsing window closed event:", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing event stream:", e, data);
|
||||
Logger.error("Niri", "Error parsing event stream:", e, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
quickshell/Services/PowerProfileService.qml
Normal file
88
quickshell/Services/PowerProfileService.qml
Normal file
@@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var powerProfiles: PowerProfiles
|
||||
readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile
|
||||
property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced
|
||||
|
||||
function getName(p) {
|
||||
if (!available)
|
||||
return "Unknown";
|
||||
|
||||
const prof = (p !== undefined) ? p : profile;
|
||||
switch (prof) {
|
||||
case PowerProfile.Performance:
|
||||
return "Performance";
|
||||
case PowerProfile.Balanced:
|
||||
return "Balanced";
|
||||
case PowerProfile.PowerSaver:
|
||||
return "Power saver";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function getIcon(p) {
|
||||
if (!available)
|
||||
return "balanced";
|
||||
|
||||
const prof = (p !== undefined) ? p : profile;
|
||||
switch (prof) {
|
||||
case PowerProfile.Performance:
|
||||
return "performance";
|
||||
case PowerProfile.Balanced:
|
||||
return "balanced";
|
||||
case PowerProfile.PowerSaver:
|
||||
return "powersaver";
|
||||
default:
|
||||
return "balanced";
|
||||
}
|
||||
}
|
||||
|
||||
function setProfile(p) {
|
||||
if (!available)
|
||||
return ;
|
||||
|
||||
try {
|
||||
powerProfiles.profile = p;
|
||||
} catch (e) {
|
||||
Logger.error("PowerProfileService", "Failed to set profile:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function cycleProfile() {
|
||||
if (!available)
|
||||
return ;
|
||||
|
||||
const current = powerProfiles.profile;
|
||||
if (current === PowerProfile.Performance)
|
||||
setProfile(PowerProfile.PowerSaver);
|
||||
else if (current === PowerProfile.Balanced)
|
||||
setProfile(PowerProfile.Performance);
|
||||
else if (current === PowerProfile.PowerSaver)
|
||||
setProfile(PowerProfile.Balanced);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onProfileChanged() {
|
||||
root.profile = powerProfiles.profile;
|
||||
// Only show toast if we have a valid profile name (not "Unknown")
|
||||
const profileName = root.getName();
|
||||
if (profileName !== "Unknown")
|
||||
ToastService.showNotice(I18n.tr("toast.power-profile.changed"), I18n.tr("toast.power-profile.profile-name", {
|
||||
"profile": profileName
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
target: powerProfiles
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ pragma Singleton
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function show(title, message, icon = "", urgency = "normal", timeout = 5000) {
|
||||
function show(title, message, timeout = 5000, icon = "", urgency = "normal") {
|
||||
if (icon)
|
||||
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
|
||||
else
|
||||
|
||||
35
quickshell/Services/SettingsService.qml
Normal file
35
quickshell/Services/SettingsService.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
property alias primaryColor: adapter.primaryColor
|
||||
property alias showLyricsBar: adapter.showLyricsBar
|
||||
property string settingsFilePath: Qt.resolvedUrl("../Assets/Config/Settings.json")
|
||||
|
||||
FileView {
|
||||
id: settingsFile
|
||||
|
||||
path: settingsFilePath
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property string primaryColor: "#89b4fa"
|
||||
property bool showLyricsBar: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: adapter
|
||||
onPrimaryColorChanged: settingsFile.writeAdapter()
|
||||
onShowLyricsBarChanged: settingsFile.writeAdapter()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Qt.labs.folderlistmodel
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Utils
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
@@ -222,7 +223,7 @@ Singleton {
|
||||
}
|
||||
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
|
||||
} else {
|
||||
console.warn("No temperature sensors found for coretemp");
|
||||
Logger.warn("SystemStatService", "No temperature sensors found for coretemp");
|
||||
root.cpuTemp = 0;
|
||||
}
|
||||
return ;
|
||||
@@ -328,7 +329,7 @@ Singleton {
|
||||
function checkNext() {
|
||||
if (currentIndex >= 16) {
|
||||
// Check up to hwmon10
|
||||
console.warn("No supported temperature sensor found");
|
||||
Logger.warn("SystemStatService", "No supported temperature sensor found");
|
||||
return ;
|
||||
}
|
||||
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
|
||||
@@ -341,7 +342,7 @@ Singleton {
|
||||
if (root.supportedTempCpuSensorNames.includes(name)) {
|
||||
root.cpuTempSensorName = name;
|
||||
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
|
||||
console.log(`Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
|
||||
Logger.log("SystemStatService", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
|
||||
} else {
|
||||
currentIndex++;
|
||||
Qt.callLater(() => {
|
||||
@@ -370,7 +371,6 @@ Singleton {
|
||||
if (root.cpuTempSensorName === "coretemp") {
|
||||
// For Intel, collect all temperature values
|
||||
const temp = parseInt(data) / 1000;
|
||||
//console.log(temp, cpuTempReader.path)
|
||||
root.intelTempValues.push(temp);
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
|
||||
@@ -5,6 +5,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -40,7 +41,7 @@ Singleton {
|
||||
try {
|
||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
|
||||
} catch (e) {
|
||||
console.error("Error switching Niri workspace:", e);
|
||||
Logger.error("WorkspaceManager", "Error switching Niri workspace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user