forked from Shinonome/dots-hyprland
feat(modules/bar): add weather bar (#1520)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Bar, wallpaper
|
# Bar, wallpaper
|
||||||
exec-once = swww-daemon --format xrgb --no-cache
|
exec-once = swww-daemon --format xrgb --no-cache
|
||||||
exec-once = sleep 0.5; swww img "$(cat ~/.local/state/quickshell/user/generated/wallpaper/path.txt)" --transition-step 100 --transition-fps 120 --transition-type grow --transition-angle 30 --transition-duration 1
|
exec-once = sleep 0.5; swww img "$(cat ~/.local/state/quickshell/user/generated/wallpaper/path.txt)" --transition-step 100 --transition-fps 120 --transition-type grow --transition-angle 30 --transition-duration 1
|
||||||
exec-once = /usr/lib/geoclue-2.0/demos/agent & gammastep
|
exec-once = ~/.config/hypr/hyprland/scripts/start_geoclue_agent.sh & gammastep
|
||||||
exec-once = qs &
|
exec-once = qs &
|
||||||
|
|
||||||
# Input method
|
# Input method
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Check if GeoClue agent is already running
|
||||||
|
if pgrep -f 'geoclue-2.0/demos/agent' > /dev/null; then
|
||||||
|
echo "GeoClue agent is already running."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List of known possible GeoClue agent paths
|
||||||
|
AGENT_PATHS="
|
||||||
|
/usr/libexec/geoclue-2.0/demos/agent
|
||||||
|
/usr/lib/geoclue-2.0/demos/agent
|
||||||
|
"
|
||||||
|
|
||||||
|
# Find the first valid agent path
|
||||||
|
for path in $AGENT_PATHS; do
|
||||||
|
if [ -x "$path" ]; then
|
||||||
|
echo "Starting GeoClue agent from: $path"
|
||||||
|
"$path" & # starts in the background
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If we got here, none of the paths worked
|
||||||
|
echo "GeoClue agent not found in known paths."
|
||||||
|
echo "Please install GeoClue or update the script with the correct path."
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
// credits: calestia
|
||||||
|
// this snippet is taken from
|
||||||
|
// https://github.com/caelestia-dots/shell
|
||||||
|
readonly property var codeToName: ({
|
||||||
|
"113": "clear_day",
|
||||||
|
"116": "partly_cloudy_day",
|
||||||
|
"119": "cloud",
|
||||||
|
"122": "cloud",
|
||||||
|
"143": "foggy",
|
||||||
|
"176": "rainy",
|
||||||
|
"179": "rainy",
|
||||||
|
"182": "rainy",
|
||||||
|
"185": "rainy",
|
||||||
|
"200": "thunderstorm",
|
||||||
|
"227": "cloudy_snowing",
|
||||||
|
"230": "snowing_heavy",
|
||||||
|
"248": "foggy",
|
||||||
|
"260": "foggy",
|
||||||
|
"263": "rainy",
|
||||||
|
"266": "rainy",
|
||||||
|
"281": "rainy",
|
||||||
|
"284": "rainy",
|
||||||
|
"293": "rainy",
|
||||||
|
"296": "rainy",
|
||||||
|
"299": "rainy",
|
||||||
|
"302": "weather_hail",
|
||||||
|
"305": "rainy",
|
||||||
|
"308": "weather_hail",
|
||||||
|
"311": "rainy",
|
||||||
|
"314": "rainy",
|
||||||
|
"317": "rainy",
|
||||||
|
"320": "cloudy_snowing",
|
||||||
|
"323": "cloudy_snowing",
|
||||||
|
"326": "cloudy_snowing",
|
||||||
|
"329": "snowing_heavy",
|
||||||
|
"332": "snowing_heavy",
|
||||||
|
"335": "snowing",
|
||||||
|
"338": "snowing_heavy",
|
||||||
|
"350": "rainy",
|
||||||
|
"353": "rainy",
|
||||||
|
"356": "rainy",
|
||||||
|
"359": "weather_hail",
|
||||||
|
"362": "rainy",
|
||||||
|
"365": "rainy",
|
||||||
|
"368": "cloudy_snowing",
|
||||||
|
"371": "snowing",
|
||||||
|
"374": "rainy",
|
||||||
|
"377": "rainy",
|
||||||
|
"386": "thunderstorm",
|
||||||
|
"389": "thunderstorm",
|
||||||
|
"392": "thunderstorm",
|
||||||
|
"395": "snowing"
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import "root:/services"
|
|||||||
import "root:/modules/common/"
|
import "root:/modules/common/"
|
||||||
import "root:/modules/common/widgets"
|
import "root:/modules/common/widgets"
|
||||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||||
|
import "root:/modules/bar/weather"
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
@@ -26,7 +27,8 @@ Scope {
|
|||||||
color: Appearance.colors.colOutlineVariant
|
color: Appearance.colors.colOutlineVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants { // For each monitor
|
Variants {
|
||||||
|
// For each monitor
|
||||||
model: {
|
model: {
|
||||||
const screens = Quickshell.screens;
|
const screens = Quickshell.screens;
|
||||||
const list = Config.options.bar.screenList;
|
const list = Config.options.bar.screenList;
|
||||||
@@ -41,12 +43,8 @@ Scope {
|
|||||||
screen: modelData
|
screen: modelData
|
||||||
|
|
||||||
property var brightnessMonitor: Brightness.getMonitorForScreen(modelData)
|
property var brightnessMonitor: Brightness.getMonitorForScreen(modelData)
|
||||||
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 :
|
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0
|
||||||
(Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0
|
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
|
||||||
readonly property int centerSideModuleWidth:
|
|
||||||
(useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened :
|
|
||||||
(useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened :
|
|
||||||
Appearance.sizes.barCenterSideModuleWidth
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "quickshell:bar"
|
WlrLayershell.namespace: "quickshell:bar"
|
||||||
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
|
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
|
||||||
@@ -121,21 +119,21 @@ Scope {
|
|||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
propagateComposedEvents: true
|
propagateComposedEvents: true
|
||||||
onEntered: (event) => {
|
onEntered: event => {
|
||||||
barLeftSideMouseArea.hovered = true
|
barLeftSideMouseArea.hovered = true;
|
||||||
}
|
}
|
||||||
onExited: (event) => {
|
onExited: event => {
|
||||||
barLeftSideMouseArea.hovered = false
|
barLeftSideMouseArea.hovered = false;
|
||||||
barLeftSideMouseArea.trackingScroll = false
|
barLeftSideMouseArea.trackingScroll = false;
|
||||||
}
|
}
|
||||||
onPressed: (event) => {
|
onPressed: event => {
|
||||||
if (event.button === Qt.LeftButton) {
|
if (event.button === Qt.LeftButton) {
|
||||||
Hyprland.dispatch('global quickshell:sidebarLeftOpen')
|
Hyprland.dispatch('global quickshell:sidebarLeftOpen');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scroll to change brightness
|
// Scroll to change brightness
|
||||||
WheelHandler {
|
WheelHandler {
|
||||||
onWheel: (event) => {
|
onWheel: event => {
|
||||||
if (event.angleDelta.y < 0)
|
if (event.angleDelta.y < 0)
|
||||||
barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness - 0.05);
|
barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness - 0.05);
|
||||||
else if (event.angleDelta.y > 0)
|
else if (event.angleDelta.y > 0)
|
||||||
@@ -147,17 +145,18 @@ Scope {
|
|||||||
}
|
}
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||||
}
|
}
|
||||||
onPositionChanged: (mouse) => {
|
onPositionChanged: mouse => {
|
||||||
if (barLeftSideMouseArea.trackingScroll) {
|
if (barLeftSideMouseArea.trackingScroll) {
|
||||||
const dx = mouse.x - barLeftSideMouseArea.lastScrollX;
|
const dx = mouse.x - barLeftSideMouseArea.lastScrollX;
|
||||||
const dy = mouse.y - barLeftSideMouseArea.lastScrollY;
|
const dy = mouse.y - barLeftSideMouseArea.lastScrollY;
|
||||||
if (Math.sqrt(dx*dx + dy*dy) > osdHideMouseMoveThreshold) {
|
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
|
||||||
Hyprland.dispatch('global quickshell:osdBrightnessHide')
|
Hyprland.dispatch('global quickshell:osdBrightnessHide');
|
||||||
barLeftSideMouseArea.trackingScroll = false;
|
barLeftSideMouseArea.trackingScroll = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item { // Left section
|
Item {
|
||||||
|
// Left section
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
implicitHeight: leftSectionRowLayout.implicitHeight
|
implicitHeight: leftSectionRowLayout.implicitHeight
|
||||||
implicitWidth: leftSectionRowLayout.implicitWidth
|
implicitWidth: leftSectionRowLayout.implicitWidth
|
||||||
@@ -169,7 +168,6 @@ Scope {
|
|||||||
side: "left"
|
side: "left"
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout { // Content
|
RowLayout { // Content
|
||||||
@@ -177,7 +175,8 @@ Scope {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
RippleButton { // Left sidebar button
|
RippleButton {
|
||||||
|
// Left sidebar button
|
||||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||||
Layout.leftMargin: Appearance.rounding.screenRounding
|
Layout.leftMargin: Appearance.rounding.screenRounding
|
||||||
Layout.fillWidth: false
|
Layout.fillWidth: false
|
||||||
@@ -193,10 +192,10 @@ Scope {
|
|||||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||||
toggled: GlobalStates.sidebarLeftOpen
|
toggled: GlobalStates.sidebarLeftOpen
|
||||||
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
|
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
Hyprland.dispatch('global quickshell:sidebarLeftToggle')
|
Hyprland.dispatch('global quickshell:sidebarLeftToggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomIcon {
|
CustomIcon {
|
||||||
@@ -204,8 +203,7 @@ Scope {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 19.5
|
width: 19.5
|
||||||
height: 19.5
|
height: 19.5
|
||||||
source: Config.options.bar.topLeftIcon == 'distro' ?
|
source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : "spark-symbolic"
|
||||||
SystemInfo.distroIcon : "spark-symbolic"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColorOverlay {
|
ColorOverlay {
|
||||||
@@ -245,10 +243,11 @@ Scope {
|
|||||||
visible: barRoot.useShortenedForm < 2
|
visible: barRoot.useShortenedForm < 2
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalBarSeparator {visible: Config.options?.bar.borderless}
|
VerticalBarSeparator {
|
||||||
|
visible: Config.options?.bar.borderless
|
||||||
|
}
|
||||||
|
|
||||||
BarGroup {
|
BarGroup {
|
||||||
id: middleCenterGroup
|
id: middleCenterGroup
|
||||||
@@ -259,20 +258,23 @@ Scope {
|
|||||||
id: workspacesWidget
|
id: workspacesWidget
|
||||||
bar: barRoot
|
bar: barRoot
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
MouseArea { // Right-click to toggle overview
|
MouseArea {
|
||||||
|
// Right-click to toggle overview
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
|
|
||||||
onPressed: (event) => {
|
onPressed: event => {
|
||||||
if (event.button === Qt.RightButton) {
|
if (event.button === Qt.RightButton) {
|
||||||
Hyprland.dispatch('global quickshell:overviewToggle')
|
Hyprland.dispatch('global quickshell:overviewToggle');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalBarSeparator {visible: Config.options?.bar.borderless}
|
VerticalBarSeparator {
|
||||||
|
visible: Config.options?.bar.borderless
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: rightCenterGroup
|
id: rightCenterGroup
|
||||||
@@ -282,7 +284,7 @@ Scope {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
Hyprland.dispatch('global quickshell:sidebarRightToggle')
|
Hyprland.dispatch('global quickshell:sidebarRightToggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
BarGroup {
|
BarGroup {
|
||||||
@@ -307,6 +309,9 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VerticalBarSeparator {
|
||||||
|
visible: Config.options.bar.borderless && Config.options.bar.weather.enable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea { // Right side | scroll to change volume
|
MouseArea { // Right side | scroll to change volume
|
||||||
@@ -325,24 +330,23 @@ Scope {
|
|||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
propagateComposedEvents: true
|
propagateComposedEvents: true
|
||||||
onEntered: (event) => {
|
onEntered: event => {
|
||||||
barRightSideMouseArea.hovered = true
|
barRightSideMouseArea.hovered = true;
|
||||||
}
|
}
|
||||||
onExited: (event) => {
|
onExited: event => {
|
||||||
barRightSideMouseArea.hovered = false
|
barRightSideMouseArea.hovered = false;
|
||||||
barRightSideMouseArea.trackingScroll = false
|
barRightSideMouseArea.trackingScroll = false;
|
||||||
}
|
}
|
||||||
onPressed: (event) => {
|
onPressed: event => {
|
||||||
if (event.button === Qt.LeftButton) {
|
if (event.button === Qt.LeftButton) {
|
||||||
Hyprland.dispatch('global quickshell:sidebarRightOpen')
|
Hyprland.dispatch('global quickshell:sidebarRightOpen');
|
||||||
}
|
} else if (event.button === Qt.RightButton) {
|
||||||
else if (event.button === Qt.RightButton) {
|
MprisController.activePlayer.next();
|
||||||
MprisController.activePlayer.next()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scroll to change volume
|
// Scroll to change volume
|
||||||
WheelHandler {
|
WheelHandler {
|
||||||
onWheel: (event) => {
|
onWheel: event => {
|
||||||
const currentVolume = Audio.value;
|
const currentVolume = Audio.value;
|
||||||
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
|
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
|
||||||
if (event.angleDelta.y < 0)
|
if (event.angleDelta.y < 0)
|
||||||
@@ -356,12 +360,12 @@ Scope {
|
|||||||
}
|
}
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||||
}
|
}
|
||||||
onPositionChanged: (mouse) => {
|
onPositionChanged: mouse => {
|
||||||
if (barRightSideMouseArea.trackingScroll) {
|
if (barRightSideMouseArea.trackingScroll) {
|
||||||
const dx = mouse.x - barRightSideMouseArea.lastScrollX;
|
const dx = mouse.x - barRightSideMouseArea.lastScrollX;
|
||||||
const dy = mouse.y - barRightSideMouseArea.lastScrollY;
|
const dy = mouse.y - barRightSideMouseArea.lastScrollY;
|
||||||
if (Math.sqrt(dx*dx + dy*dy) > osdHideMouseMoveThreshold) {
|
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
|
||||||
Hyprland.dispatch('global quickshell:osdVolumeHide')
|
Hyprland.dispatch('global quickshell:osdVolumeHide');
|
||||||
barRightSideMouseArea.trackingScroll = false;
|
barRightSideMouseArea.trackingScroll = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,7 +416,7 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
Hyprland.dispatch('global quickshell:sidebarRightToggle')
|
Hyprland.dispatch('global quickshell:sidebarRightToggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -480,6 +484,17 @@ Scope {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Weather
|
||||||
|
Loader {
|
||||||
|
Layout.leftMargin: 8
|
||||||
|
Layout.fillHeight: true
|
||||||
|
active: Config.options.bar.weather.enable
|
||||||
|
sourceComponent: BarGroup {
|
||||||
|
implicitHeight: Appearance.sizes.baseBarHeight
|
||||||
|
WeatherBar {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,9 +565,6 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import "root:/modules/common"
|
||||||
|
import "root:/modules/common/widgets"
|
||||||
|
import "root:/constants"
|
||||||
|
import "root:/services"
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
property real margin: 10
|
||||||
|
property bool hovered: false
|
||||||
|
implicitWidth: rowLayout.implicitWidth + margin * 2
|
||||||
|
implicitHeight: rowLayout.implicitHeight
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: rowLayout
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
MaterialSymbol {
|
||||||
|
fill: 0
|
||||||
|
text: WeatherIcons.codeToName[Weather.data.wCode]
|
||||||
|
iconSize: Appearance.font.pixelSize.large
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
visible: true
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
text: Weather.data.temp
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: popupLoader
|
||||||
|
active: root.containsMouse
|
||||||
|
|
||||||
|
component: PopupWindow {
|
||||||
|
id: popupWindow
|
||||||
|
visible: true
|
||||||
|
implicitWidth: weatherPopup.implicitWidth
|
||||||
|
implicitHeight: weatherPopup.implicitHeight
|
||||||
|
anchor.item: root
|
||||||
|
anchor.edges: Edges.Bottom
|
||||||
|
anchor.rect.x: (root.implicitWidth - popupWindow.implicitWidth) / 2
|
||||||
|
anchor.rect.y: root.implicitHeight + 10
|
||||||
|
color: "transparent"
|
||||||
|
WeatherPopup {
|
||||||
|
id: weatherPopup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import "root:/modules/common"
|
||||||
|
import "root:/modules/common/widgets"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
implicitWidth: columnLayout.implicitWidth * 2
|
||||||
|
implicitHeight: columnLayout.implicitHeight * 2
|
||||||
|
Layout.fillWidth: parent
|
||||||
|
|
||||||
|
property alias title: title.text
|
||||||
|
property alias value: value.text
|
||||||
|
property alias symbol: symbol.text
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: columnLayout
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: -10
|
||||||
|
RowLayout {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
MaterialSymbol {
|
||||||
|
id: symbol
|
||||||
|
fill: 0
|
||||||
|
iconSize: Appearance.font.pixelSize.normal
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
id: title
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
id: value
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import "root:/services"
|
||||||
|
import "root:/modules/common"
|
||||||
|
import "root:/modules/common/widgets"
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
readonly property real margin: 10
|
||||||
|
implicitWidth: columnLayout.implicitWidth + margin * 2
|
||||||
|
implicitHeight: columnLayout.implicitHeight + margin * 2
|
||||||
|
color: Appearance.colors.colLayer0
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
border.width: 1
|
||||||
|
border.color: Appearance.m3colors.m3outlineVariant
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: columnLayout
|
||||||
|
spacing: 5
|
||||||
|
anchors.centerIn: root
|
||||||
|
implicitWidth: Math.max(header.implicitWidth, gridLayout.implicitWidth)
|
||||||
|
implicitHeight: gridLayout.implicitHeight
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RowLayout {
|
||||||
|
id: header
|
||||||
|
spacing: 5
|
||||||
|
Layout.fillWidth: parent
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
MaterialSymbol {
|
||||||
|
fill: 0
|
||||||
|
text: "location_on"
|
||||||
|
iconSize: Appearance.font.pixelSize.huge
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: Weather.data.city
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.title
|
||||||
|
font.family: Appearance.font.family.title
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics grid
|
||||||
|
GridLayout {
|
||||||
|
id: gridLayout
|
||||||
|
columns: 2
|
||||||
|
rowSpacing: 5
|
||||||
|
columnSpacing: 5
|
||||||
|
uniformCellWidths: true
|
||||||
|
|
||||||
|
WeatherCard {
|
||||||
|
title: "UV Index"
|
||||||
|
symbol: "wb_sunny"
|
||||||
|
value: Weather.data.uv
|
||||||
|
}
|
||||||
|
WeatherCard {
|
||||||
|
title: "Wind"
|
||||||
|
symbol: "air"
|
||||||
|
value: `(${Weather.data.windDir}) ${Weather.data.wind}`
|
||||||
|
}
|
||||||
|
WeatherCard {
|
||||||
|
title: "Precipitation"
|
||||||
|
symbol: "rainy_light"
|
||||||
|
value: Weather.data.precip
|
||||||
|
}
|
||||||
|
WeatherCard {
|
||||||
|
title: "Humidity"
|
||||||
|
symbol: "humidity_low"
|
||||||
|
value: Weather.data.humidity
|
||||||
|
}
|
||||||
|
WeatherCard {
|
||||||
|
title: "Visibility"
|
||||||
|
symbol: "visibility"
|
||||||
|
value: Weather.data.visib
|
||||||
|
}
|
||||||
|
WeatherCard {
|
||||||
|
title: "Pressure"
|
||||||
|
symbol: "readiness_score"
|
||||||
|
value: Weather.data.press
|
||||||
|
}
|
||||||
|
WeatherCard {
|
||||||
|
title: "Sunrise"
|
||||||
|
symbol: "wb_twilight"
|
||||||
|
value: Weather.data.sunrise
|
||||||
|
}
|
||||||
|
WeatherCard {
|
||||||
|
title: "Sunset"
|
||||||
|
symbol: "bedtime"
|
||||||
|
value: Weather.data.sunset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,6 +124,13 @@ Singleton {
|
|||||||
property bool alwaysShowNumbers: false
|
property bool alwaysShowNumbers: false
|
||||||
property int showNumberDelay: 300 // milliseconds
|
property int showNumberDelay: 300 // milliseconds
|
||||||
}
|
}
|
||||||
|
property JsonObject weather: JsonObject {
|
||||||
|
property bool enable: false
|
||||||
|
property bool enableGPS: true // gps based location
|
||||||
|
property string city: "" // When 'enableGPS' is false
|
||||||
|
property bool useUSCS: false // Instead of metric (SI) units
|
||||||
|
property int fetchInterval: 10 // minutes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject battery: JsonObject {
|
property JsonObject battery: JsonObject {
|
||||||
|
|||||||
@@ -138,4 +138,14 @@ ContentPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContentSection {
|
||||||
|
title: "Weather"
|
||||||
|
ConfigSwitch {
|
||||||
|
text: "enable"
|
||||||
|
checked: Config.options.bar.weather.enable
|
||||||
|
onCheckedChanged: {
|
||||||
|
Config.options.bar.weather.enable = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
import QtPositioning
|
||||||
|
|
||||||
|
import "root:/modules/common"
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
// 10 minute
|
||||||
|
readonly property int fetchInterval: Config.options.bar.weather.fetchInterval * 60 * 1000
|
||||||
|
readonly property string city: Config.options.bar.weather.city
|
||||||
|
readonly property bool useUSCS: Config.options.bar.weather.useUSCS
|
||||||
|
property bool gpsActive: Config.options.bar.weather.enableGPS
|
||||||
|
|
||||||
|
property var location: ({
|
||||||
|
valid: false,
|
||||||
|
lat: 0,
|
||||||
|
lon: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
property var data: ({
|
||||||
|
uv: 0,
|
||||||
|
humidity: 0,
|
||||||
|
sunrise: 0,
|
||||||
|
sunset: 0,
|
||||||
|
windDir: 0,
|
||||||
|
wCode: 0,
|
||||||
|
city: 0,
|
||||||
|
wind: 0,
|
||||||
|
precip: 0,
|
||||||
|
visib: 0,
|
||||||
|
press: 0,
|
||||||
|
temp: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
function refineData(data) {
|
||||||
|
let temp = {};
|
||||||
|
temp.uv = data?.current?.uvIndex || 0;
|
||||||
|
temp.humidity = (data?.current?.humidity || 0) + "%";
|
||||||
|
temp.sunrise = data?.astronomy?.sunrise || "0.0";
|
||||||
|
temp.sunset = data?.astronomy?.sunset || "0.0";
|
||||||
|
temp.windDir = data?.current?.winddir16Point || "N";
|
||||||
|
temp.wCode = data?.current?.weatherCode || "113";
|
||||||
|
temp.city = data?.location?.areaName[0]?.value || "City";
|
||||||
|
temp.temp = "";
|
||||||
|
if (root.useUSCS) {
|
||||||
|
temp.wind = (data?.current?.windspeedMiles || 0) + " mph";
|
||||||
|
temp.precip = (data?.current?.precipInches || 0) + " in";
|
||||||
|
temp.visib = (data?.current?.visibilityMiles || 0) + " m";
|
||||||
|
temp.press = (data?.current?.pressureInches || 0) + " psi";
|
||||||
|
temp.temp += (data?.current?.temp_F || 0);
|
||||||
|
temp.temp += " (" + (data?.current?.FeelsLikeF || 0) + ") ";
|
||||||
|
temp.temp += "\u{02109}";
|
||||||
|
} else {
|
||||||
|
temp.wind = (data?.current?.windspeedKmph || 0) + " km/h";
|
||||||
|
temp.precip = (data?.current?.precipMM || 0) + " mm";
|
||||||
|
temp.visib = (data?.current?.visibility || 0) + " km";
|
||||||
|
temp.press = (data?.current?.pressure || 0) + " hPa";
|
||||||
|
temp.temp += (data?.current?.temp_C || 0);
|
||||||
|
temp.temp += " (" + (data?.current?.FeelsLikeC || 0) + ") ";
|
||||||
|
temp.temp += "\u{02103}";
|
||||||
|
}
|
||||||
|
root.data = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getData() {
|
||||||
|
let command = "curl -s wttr.in";
|
||||||
|
|
||||||
|
if (root.gpsActive && root.location.valid) {
|
||||||
|
command += `/${root.location.lat},${root.location.long}`;
|
||||||
|
} else {
|
||||||
|
command += `/${formatCityName(root.city)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// format as json
|
||||||
|
command += "?format=j1";
|
||||||
|
command += " | ";
|
||||||
|
// only take the current weather, location, asytronmy data
|
||||||
|
command += "jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'";
|
||||||
|
fetcher.command[2] = command;
|
||||||
|
fetcher.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCityName(cityName) {
|
||||||
|
return cityName.trim().split(/\s+/).join('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!root.gpsActive)
|
||||||
|
return;
|
||||||
|
console.info("[WeatherService] Starting the GPS service.");
|
||||||
|
positionSource.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: fetcher
|
||||||
|
command: ["bash", "-c", ""]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.length === 0)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(text);
|
||||||
|
root.refineData(parsedData);
|
||||||
|
// console.info(`[ data: ${JSON.stringify(parsedData)}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[WeatherService] ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PositionSource {
|
||||||
|
id: positionSource
|
||||||
|
updateInterval: root.fetchInterval
|
||||||
|
|
||||||
|
onPositionChanged: {
|
||||||
|
// update the location if the given location is valid
|
||||||
|
// if it fails getting the location, use the last valid location
|
||||||
|
if (position.latitudeValid && position.longitudeValid) {
|
||||||
|
root.location.lat = position.coordinate.latitude;
|
||||||
|
root.location.long = position.coordinate.longitude;
|
||||||
|
root.location.valid = true;
|
||||||
|
// console.info(`📍 Location: ${position.coordinate.latitude}, ${position.coordinate.longitude}`);
|
||||||
|
root.getData();
|
||||||
|
// if can't get initialized with valid location deactivate the GPS
|
||||||
|
} else {
|
||||||
|
root.gpsActive = root.location.valid ? true : false;
|
||||||
|
console.error("[WeatherService] Failed to get the GPS location.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onValidityChanged: {
|
||||||
|
if (!positionSource.valid) {
|
||||||
|
positionSource.stop();
|
||||||
|
root.location.valid = false;
|
||||||
|
root.gpsActive = false;
|
||||||
|
Quickshell.execDetached(["bash", "-c", `notify-send WeatherService 'Can not find a GPS service. Using the fallback method instead.'`]);
|
||||||
|
console.error("[WeatherService] Could not aquire a valid backend plugin.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
running: !root.gpsActive
|
||||||
|
repeat: true
|
||||||
|
interval: root.fetchInterval
|
||||||
|
triggeredOnStart: !root.gpsActive
|
||||||
|
onTriggered: root.getData()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user