Move panel plugins under panels
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
import "Model.js" as Model
|
||||
|
||||
Panel {
|
||||
id: root
|
||||
moduleName: "omarchy.weather"
|
||||
ipcTarget: "omarchy.weather"
|
||||
|
||||
property var anchorItem: null
|
||||
|
||||
function open() {
|
||||
root.controller.show()
|
||||
root.refresh()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (root.opened) root.close()
|
||||
else root.open()
|
||||
}
|
||||
|
||||
// Parsed wttr.in j1 response. Kept on failure so stale data stays visible.
|
||||
property var report: null
|
||||
property var dailyForecastReport: null
|
||||
property string wttrLocation: ""
|
||||
|
||||
// Bar pill state. Polled locally; populated by weatherProc below.
|
||||
property string label: ""
|
||||
property string klass: ""
|
||||
|
||||
function updateWeather(raw) {
|
||||
var data = Model.parseWeatherStatus(raw)
|
||||
label = data.label
|
||||
klass = data.klass
|
||||
}
|
||||
|
||||
readonly property var current: report && report.current_condition && report.current_condition[0] ? report.current_condition[0] : null
|
||||
readonly property var areaInfo: report && report.nearest_area && report.nearest_area[0] ? report.nearest_area[0] : null
|
||||
readonly property var forecastDays: buildForecastDays()
|
||||
|
||||
readonly property bool useImperial: {
|
||||
var override = setting("unit", "")
|
||||
if (override === "imperial") return true
|
||||
if (override === "metric") return false
|
||||
var name = String(Qt.locale().name || "")
|
||||
return /^en_US/.test(name) || /^en_LR/.test(name) || /^my/.test(name)
|
||||
}
|
||||
|
||||
// Auto-refresh interval in minutes; clamped to a sane minimum.
|
||||
readonly property int refreshMinutes: Math.max(1, parseInt(setting("refreshMinutes", 15), 10) || 15)
|
||||
|
||||
readonly property string reportLocation: wttrLocation || (areaInfo && areaInfo.areaName && areaInfo.areaName[0] ? areaInfo.areaName[0].value : "")
|
||||
readonly property string reportTempNum: current ? String(useImperial ? current.temp_F : current.temp_C) : ""
|
||||
readonly property string tempUnit: "°" + (useImperial ? "F" : "C")
|
||||
readonly property string reportFeels: current ? formatTemp(useImperial ? current.FeelsLikeF : current.FeelsLikeC) : ""
|
||||
readonly property string reportWind: current ? (useImperial ? (current.windspeedMiles + " mph") : (current.windspeedKmph + " km/h")) : ""
|
||||
readonly property string reportHumidity: current ? (current.humidity + "%") : ""
|
||||
|
||||
function refresh() {
|
||||
if (!forecastProc.running) forecastProc.running = true
|
||||
if (!locationProc.running) locationProc.running = true
|
||||
}
|
||||
|
||||
function refreshDailyForecast(sourceReport) {
|
||||
var area = sourceReport && sourceReport.nearest_area && sourceReport.nearest_area[0] ? sourceReport.nearest_area[0] : root.areaInfo
|
||||
if (!area || dailyForecastProc.running) return
|
||||
|
||||
var lat = parseFloat(String(area.latitude || ""))
|
||||
var lon = parseFloat(String(area.longitude || ""))
|
||||
if (isNaN(lat) || isNaN(lon)) return
|
||||
|
||||
var url = "https://api.open-meteo.com/v1/forecast"
|
||||
+ "?latitude=" + encodeURIComponent(String(lat))
|
||||
+ "&longitude=" + encodeURIComponent(String(lon))
|
||||
+ "&daily=weather_code,temperature_2m_max,temperature_2m_min"
|
||||
+ "&forecast_days=4"
|
||||
+ "&timezone=auto"
|
||||
dailyForecastProc.command = ["curl", "-fsS", "--max-time", "5", url]
|
||||
dailyForecastProc.running = true
|
||||
}
|
||||
|
||||
function buildForecastDays() {
|
||||
return Model.buildForecastDays(report, dailyForecastReport, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
||||
}
|
||||
|
||||
function openMeteoForecastDays() {
|
||||
return Model.openMeteoForecastDays(dailyForecastReport, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
||||
}
|
||||
|
||||
function wttrNextForecastDays() {
|
||||
return Model.wttrNextForecastDays(report, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
||||
}
|
||||
|
||||
function isFutureForecastDate(dateString) {
|
||||
return Model.isFutureForecastDate(dateString, Qt.formatDate(new Date(), "yyyy-MM-dd"))
|
||||
}
|
||||
|
||||
function roundedTemp(value) {
|
||||
return Model.roundedTemp(value)
|
||||
}
|
||||
|
||||
function celsiusToFahrenheit(value) {
|
||||
return Model.celsiusToFahrenheit(value)
|
||||
}
|
||||
|
||||
function formatTemp(value) {
|
||||
return Model.formatTemp(value, useImperial)
|
||||
}
|
||||
|
||||
function dayName(dateString) {
|
||||
return Model.dayName(dateString, function(date) { return Qt.formatDate(date, "dddd") })
|
||||
}
|
||||
|
||||
// Bare degree value (no unit letter), used in the forecast row.
|
||||
function bareTempForDay(day, kind) {
|
||||
return Model.bareTempForDay(day, kind, useImperial)
|
||||
}
|
||||
|
||||
// Representative icon for a forecast day: the hourly entry nearest noon.
|
||||
function dayIcon(day) {
|
||||
return Model.dayIcon(day)
|
||||
}
|
||||
|
||||
function iconForOpenMeteoCode(code) {
|
||||
return Model.iconForOpenMeteoCode(code)
|
||||
}
|
||||
|
||||
// Mirrors omarchy-weather-icon's wttr.in code → nerd-font glyph mapping.
|
||||
function iconForCode(code, night) {
|
||||
return Model.iconForCode(code, night)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: forecastProc
|
||||
command: ["bash", "-lc", "curl -fsS --max-time 5 'https://wttr.in/?format=j1' 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var raw = String(text || "").trim()
|
||||
if (!raw) return
|
||||
try {
|
||||
var parsed = JSON.parse(raw)
|
||||
root.report = parsed
|
||||
root.refreshDailyForecast(parsed)
|
||||
} catch (e) {
|
||||
// Keep last-good report on parse failure so the popup isn't blanked.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dailyForecastProc
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var raw = String(text || "").trim()
|
||||
if (!raw) return
|
||||
try {
|
||||
root.dailyForecastReport = JSON.parse(raw)
|
||||
} catch (e) {
|
||||
// Keep last-good daily forecast on parse failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: locationProc
|
||||
command: ["bash", "-lc", "curl -fsS --max-time 4 'https://wttr.in?format=%l' 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var raw = String(text || "").trim()
|
||||
if (!raw) return
|
||||
root.wttrLocation = raw.split(",")[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: root.refreshMinutes * 60 * 1000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
id: popup
|
||||
anchorItem: root.anchorItem
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.opened
|
||||
centerOnBar: true
|
||||
triggerMode: "click"
|
||||
contentWidth: popup.fittedContentWidth(Style.space(480))
|
||||
contentHeight: popup.fittedContentHeight(weatherColumn.implicitHeight)
|
||||
|
||||
Flickable {
|
||||
id: weatherScroll
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: weatherColumn.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: weatherColumn
|
||||
width: weatherScroll.width
|
||||
spacing: Style.space(14)
|
||||
|
||||
// ---- Hero row: big icon + temp on the left; location and stats stacked on the right.
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(heroLeft.height, heroRight.height)
|
||||
|
||||
Row {
|
||||
id: heroLeft
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(16)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
id: heroIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: 5
|
||||
text: root.label || "—"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
// Decorative condition emoji; intentionally larger than the
|
||||
// Style.font.* scale's displayLarge (28).
|
||||
font.pixelSize: 64
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(2)
|
||||
|
||||
Text {
|
||||
id: tempBig
|
||||
text: root.reportTempNum || "—"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
// Hero temperature read-out; deliberately oversized, outside
|
||||
// the Style.font.* scale.
|
||||
font.pixelSize: 56
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: root.current ? root.tempUnit : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
anchors.top: tempBig.top
|
||||
anchors.topMargin: Style.space(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: heroRight
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.space(20)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(12)
|
||||
|
||||
Row {
|
||||
visible: root.reportLocation !== ""
|
||||
spacing: Style.space(6)
|
||||
|
||||
Text {
|
||||
text: "" // nf-fa-map_marker
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: (root.reportLocation || "").toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
font.letterSpacing: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: !!root.current
|
||||
spacing: Style.space(36)
|
||||
|
||||
Column {
|
||||
spacing: Style.space(5)
|
||||
Text {
|
||||
text: "FEELS"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
text: root.reportFeels
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Style.space(5)
|
||||
Text {
|
||||
text: "WIND"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
text: root.reportWind
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Style.space(5)
|
||||
Text {
|
||||
text: "HUMID"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
text: root.reportHumidity
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.current
|
||||
text: "Fetching forecast…"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
font.italic: true
|
||||
}
|
||||
|
||||
// ---- Divider between current conditions and forecast.
|
||||
Rectangle {
|
||||
visible: root.forecastDays.length > 0
|
||||
width: parent.width
|
||||
height: Style.spacing.hairline
|
||||
color: root.bar.foreground
|
||||
opacity: 0.12
|
||||
}
|
||||
|
||||
// ---- Forecast row: each cell has the day icon left of a day-name + hi/lo column.
|
||||
// Wrapped in an Item so the block of cells can be centered within the popup.
|
||||
Item {
|
||||
visible: root.forecastDays.length > 0
|
||||
width: parent.width
|
||||
height: forecastRow.height
|
||||
|
||||
Row {
|
||||
id: forecastRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.space(44)
|
||||
|
||||
Repeater {
|
||||
model: root.forecastDays
|
||||
|
||||
Row {
|
||||
required property var modelData
|
||||
required property int index
|
||||
spacing: Style.space(10)
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.dayIcon(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(2)
|
||||
|
||||
Text {
|
||||
text: root.dayName(modelData.date).toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Style.space(6)
|
||||
|
||||
Text {
|
||||
text: root.bareTempForDay(modelData, "max")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
Text {
|
||||
text: root.bareTempForDay(modelData, "min")
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll the weather pill text/class every minute. Local to this widget.
|
||||
Process {
|
||||
id: weatherProc
|
||||
command: ["bash", "-lc", root.bar ? Util.shellQuote(root.bar.omarchyPath + "/shell/scripts/weather.sh") : ""]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.updateWeather(text)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 60000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: if (!weatherProc.running) weatherProc.running = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user