sideright: make popdown window
Weather requires a weather api key from https://weatherapi.com
This commit is contained in:
parent
751c3bc789
commit
df3b51399c
10 changed files with 705 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
scss/scheme/_index.scss
|
scss/scheme/_index.scss
|
||||||
scss/scheme/_dynamic.scss
|
scss/scheme/_dynamic.scss
|
||||||
|
assets/weather-api-key.txt
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,10 @@ export const math = {
|
||||||
export const updates = {
|
export const updates = {
|
||||||
interval: 900000,
|
interval: 900000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const weather = {
|
||||||
|
interval: 600000,
|
||||||
|
key: "assets/weather-api-key.txt", // Path to file containing api key relative to the base directory. To get a key, visit https://weatherapi.com/
|
||||||
|
location: "", // Location as a string or empty to autodetect
|
||||||
|
// TODO: imperial
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@
|
||||||
@forward "bluetoothdevices";
|
@forward "bluetoothdevices";
|
||||||
@forward "networks";
|
@forward "networks";
|
||||||
@forward "media";
|
@forward "media";
|
||||||
|
@forward "sideright";
|
||||||
|
|
|
||||||
147
scss/popdowns/sideright.scss
Normal file
147
scss/popdowns/sideright.scss
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
@use "sass:color";
|
||||||
|
@use "../scheme";
|
||||||
|
@use "../lib";
|
||||||
|
@use "../font";
|
||||||
|
|
||||||
|
$-accent: scheme.$peach;
|
||||||
|
$-accent2: scheme.$blue;
|
||||||
|
|
||||||
|
.sideright {
|
||||||
|
@include lib.rounded(8);
|
||||||
|
@include lib.border($-accent, 0.4, 2);
|
||||||
|
@include lib.shadow;
|
||||||
|
@include font.mono;
|
||||||
|
|
||||||
|
background-color: scheme.$crust;
|
||||||
|
color: $-accent;
|
||||||
|
padding: lib.s(12);
|
||||||
|
font-size: lib.s(14);
|
||||||
|
|
||||||
|
@include lib.spacing(10, true);
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
@include lib.rounded(5);
|
||||||
|
|
||||||
|
background-color: scheme.$base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
padding: lib.s(12) lib.s(8);
|
||||||
|
font-size: lib.s(48);
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
@include lib.spacing(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ampm {
|
||||||
|
font-size: lib.s(24);
|
||||||
|
font-weight: normal;
|
||||||
|
margin-top: lib.s(18);
|
||||||
|
color: $-accent2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar {
|
||||||
|
padding: lib.s(5) lib.s(8) 0 lib.s(8);
|
||||||
|
min-width: lib.s(350);
|
||||||
|
color: scheme.$text;
|
||||||
|
|
||||||
|
calendar {
|
||||||
|
padding: lib.s(8); // Padding for each day
|
||||||
|
|
||||||
|
// Month and year
|
||||||
|
&.header {
|
||||||
|
color: $-accent2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change month/year buttons
|
||||||
|
&.button {
|
||||||
|
&:hover {
|
||||||
|
color: scheme.$subtext0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: color.mix(scheme.$text, scheme.$base, 65%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weekday labels
|
||||||
|
&.highlight {
|
||||||
|
color: $-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days in other month
|
||||||
|
&:indeterminate {
|
||||||
|
color: color.mix(scheme.$text, scheme.$base, 65%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current day
|
||||||
|
&:selected {
|
||||||
|
box-shadow: inset 0 lib.s(-12) 0 0 $-accent, inset 0 lib.s(-9) 0 0 scheme.$base,
|
||||||
|
inset lib.s(-10) lib.s(-3) 0 0 scheme.$base, inset lib.s(10) 0 0 lib.s(0.1) scheme.$base;
|
||||||
|
color: $-accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather {
|
||||||
|
padding: lib.s(5) lib.s(15);
|
||||||
|
|
||||||
|
@include lib.spacing(10, true);
|
||||||
|
|
||||||
|
.current {
|
||||||
|
@include lib.spacing(20);
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: lib.s(64);
|
||||||
|
padding: lib.s(10) lib.s(20) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: $-accent2;
|
||||||
|
font-size: lib.s(16);
|
||||||
|
|
||||||
|
@include lib.spacing($vertical: true);
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
font-size: lib.s(28);
|
||||||
|
font-weight: bold;
|
||||||
|
color: scheme.$text;
|
||||||
|
|
||||||
|
@include lib.spacing;
|
||||||
|
|
||||||
|
.temp-icon {
|
||||||
|
font-size: lib.s(30);
|
||||||
|
|
||||||
|
$-temps: freezing, cold, normal, hot, burning;
|
||||||
|
$-colours: scheme.$sky, scheme.$blue, scheme.$green, scheme.$yellow, scheme.$red;
|
||||||
|
@for $i from 1 through length($-temps) {
|
||||||
|
&.#{nth($-temps, $i)} {
|
||||||
|
color: nth($-colours, $i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
background-color: scheme.$surface0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast {
|
||||||
|
padding: lib.s(10) 0 lib.s(15) 0;
|
||||||
|
|
||||||
|
@include lib.spacing(10);
|
||||||
|
|
||||||
|
.hour {
|
||||||
|
@include lib.spacing($vertical: true);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: lib.s(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { execAsync, GLib, register, Variable } from "astal";
|
import { execAsync, register, Variable } from "astal";
|
||||||
import { bind, kebabify } from "astal/binding";
|
import { bind, kebabify } from "astal/binding";
|
||||||
import { App, Astal, astalify, Gdk, Gtk, type ConstructProps } from "astal/gtk3";
|
import { App, Astal, astalify, Gdk, Gtk, type ConstructProps } from "astal/gtk3";
|
||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
|
@ -13,7 +13,7 @@ import Players from "../services/players";
|
||||||
import Updates from "../services/updates";
|
import Updates from "../services/updates";
|
||||||
import { getAppCategoryIcon } from "../utils/icons";
|
import { getAppCategoryIcon } from "../utils/icons";
|
||||||
import { ellipsize } from "../utils/strings";
|
import { ellipsize } from "../utils/strings";
|
||||||
import { osIcon } from "../utils/system";
|
import { bindCurrentTime, osIcon } from "../utils/system";
|
||||||
import { setupCustomTooltip } from "../utils/widgets";
|
import { setupCustomTooltip } from "../utils/widgets";
|
||||||
import type PopupWindow from "../widgets/popupwindow";
|
import type PopupWindow from "../widgets/popupwindow";
|
||||||
|
|
||||||
|
|
@ -438,19 +438,14 @@ const NotifCount = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const DateTime = () => (
|
const DateTime = () => (
|
||||||
<box className="module date-time">
|
<button
|
||||||
<label className="icon" label="calendar_month" />
|
onClick={(self, event) => event.button === Astal.MouseButton.PRIMARY && togglePopup(self, event, "sideright")}
|
||||||
<label
|
>
|
||||||
setup={self => {
|
<box className="module date-time">
|
||||||
const pollVar = Variable(null).poll(5000, () => {
|
<label className="icon" label="calendar_month" />
|
||||||
self.label =
|
<label label={bindCurrentTime(config.dateTimeFormat)} />
|
||||||
GLib.DateTime.new_now_local().format(config.dateTimeFormat) ?? new Date().toLocaleString();
|
</box>
|
||||||
return null;
|
</button>
|
||||||
});
|
|
||||||
self.connect("destroy", () => pollVar.drop());
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const Power = () => (
|
const Power = () => (
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import BluetoothDevices from "./bluetoothdevices";
|
||||||
import Media from "./media";
|
import Media from "./media";
|
||||||
import Networks from "./networks";
|
import Networks from "./networks";
|
||||||
import Notifications from "./notifications";
|
import Notifications from "./notifications";
|
||||||
|
import SideRight from "./sideright";
|
||||||
import Updates from "./updates";
|
import Updates from "./updates";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
|
@ -10,6 +11,7 @@ export default () => {
|
||||||
<BluetoothDevices />;
|
<BluetoothDevices />;
|
||||||
<Networks />;
|
<Networks />;
|
||||||
<Media />;
|
<Media />;
|
||||||
|
<SideRight />;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
127
src/modules/popdowns/sideright.tsx
Normal file
127
src/modules/popdowns/sideright.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { bind } from "astal";
|
||||||
|
import { Astal, Gtk, type Gdk } from "astal/gtk3";
|
||||||
|
import SWeather, { type WeatherData } from "../../services/weather";
|
||||||
|
import { ellipsize } from "../../utils/strings";
|
||||||
|
import { bindCurrentTime } from "../../utils/system";
|
||||||
|
import { Calendar as WCal } from "../../utils/widgets";
|
||||||
|
import PopupWindow from "../../widgets/popupwindow";
|
||||||
|
|
||||||
|
const getHoursFromUpdate = (data: WeatherData, hours: number) => {
|
||||||
|
const updateTime = data.location.localtime_epoch;
|
||||||
|
const updateHourStart = updateTime - (updateTime % 3600);
|
||||||
|
|
||||||
|
let nextHour = new Date((updateHourStart + hours * 3600) * 1000).getHours();
|
||||||
|
if (nextHour >= 24) nextHour -= 24;
|
||||||
|
|
||||||
|
return nextHour;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Time = () => (
|
||||||
|
<box className="time">
|
||||||
|
<box hexpand halign={Gtk.Align.CENTER}>
|
||||||
|
<label label={bindCurrentTime("%I:%M:%S")} />
|
||||||
|
<label className="ampm" label={bindCurrentTime("%p", c => (c.get_hour() < 12 ? "AM" : "PM"))} />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Calendar = () => (
|
||||||
|
<box className="calendar">
|
||||||
|
<eventbox
|
||||||
|
setup={self => {
|
||||||
|
self.connect("button-press-event", (_, event: Gdk.Event) => {
|
||||||
|
if (event.get_button()[1] === Astal.MouseButton.MIDDLE) {
|
||||||
|
const now = new Date();
|
||||||
|
const child = self.get_child() as WCal | null;
|
||||||
|
if (!child) return;
|
||||||
|
child.select_month(now.getMonth(), now.getFullYear());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WCal
|
||||||
|
hexpand
|
||||||
|
showDetails={false}
|
||||||
|
day={0}
|
||||||
|
setup={self => {
|
||||||
|
const update = () => {
|
||||||
|
const now = new Date();
|
||||||
|
if (self.month === now.getMonth() && self.year === now.getFullYear()) self.day = now.getDate();
|
||||||
|
else self.day = 0;
|
||||||
|
};
|
||||||
|
self.connect("month-changed", update);
|
||||||
|
self.connect("next-month", update);
|
||||||
|
self.connect("prev-month", update);
|
||||||
|
self.connect("next-year", update);
|
||||||
|
self.connect("prev-year", update);
|
||||||
|
update();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</eventbox>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Weather = () => {
|
||||||
|
const weather = SWeather.get_default();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box vertical className="weather">
|
||||||
|
<centerbox className="current">
|
||||||
|
<label
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
valign={Gtk.Align.CENTER}
|
||||||
|
className="status-icon"
|
||||||
|
label={bind(weather, "icon")}
|
||||||
|
/>
|
||||||
|
<box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="status">
|
||||||
|
<box halign={Gtk.Align.CENTER} className="temperature">
|
||||||
|
<label label={bind(weather, "temperature").as(t => `${Math.round(t)}°C`)} />
|
||||||
|
<label
|
||||||
|
className={bind(weather, "tempColour").as(c => `temp-icon ${c}`)}
|
||||||
|
label={bind(weather, "tempIcon")}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
<label label={bind(weather, "condition").as(c => ellipsize(c, 16))} />
|
||||||
|
</box>
|
||||||
|
<box vertical halign={Gtk.Align.END} valign={Gtk.Align.CENTER} className="other-data">
|
||||||
|
<label xalign={0} label={bind(weather, "wind").as(w => ` ${Math.round(w)} kph`)} />
|
||||||
|
<label xalign={0} label={bind(weather, "rainChance").as(r => ` ${r}%`)} />
|
||||||
|
</box>
|
||||||
|
</centerbox>
|
||||||
|
<box className="separator" />
|
||||||
|
<box className="forecast">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<box vertical hexpand className="hour">
|
||||||
|
<label
|
||||||
|
label={bind(weather, "raw").as(r => {
|
||||||
|
const hour = getHoursFromUpdate(r, i + 1);
|
||||||
|
return `${hour % 12 || 12}${hour < 12 ? "AM" : "PM"}`;
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="icon"
|
||||||
|
label={bind(weather, "raw").as(r =>
|
||||||
|
weather.getIcon(weather.forecast[getHoursFromUpdate(r, i + 1)]?.condition.text ?? "")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
label={bind(weather, "raw").as(
|
||||||
|
r => `${Math.round(weather.forecast[getHoursFromUpdate(r, i + 1)]?.temp_c) ?? "-"}°C`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
))}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<PopupWindow name="sideright">
|
||||||
|
<box vertical className="sideright">
|
||||||
|
<Time />
|
||||||
|
<Calendar />
|
||||||
|
<Weather />
|
||||||
|
</box>
|
||||||
|
</PopupWindow>
|
||||||
|
);
|
||||||
398
src/services/weather.ts
Normal file
398
src/services/weather.ts
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
import { execAsync, GLib, GObject, interval, property, readFileAsync, register, writeFileAsync } from "astal";
|
||||||
|
import { weather as config } from "../../config";
|
||||||
|
|
||||||
|
export interface WeatherCondition {
|
||||||
|
text: string;
|
||||||
|
icon: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _WeatherState {
|
||||||
|
temp_c: number;
|
||||||
|
temp_f: number;
|
||||||
|
is_day: number;
|
||||||
|
condition: WeatherCondition;
|
||||||
|
wind_mph: number;
|
||||||
|
wind_kph: number;
|
||||||
|
wind_degree: number;
|
||||||
|
wind_dir: "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW";
|
||||||
|
pressure_mb: number;
|
||||||
|
pressure_in: number;
|
||||||
|
precip_mm: number;
|
||||||
|
precip_in: number;
|
||||||
|
humidity: number;
|
||||||
|
cloud: number;
|
||||||
|
feelslike_c: number;
|
||||||
|
feelslike_f: number;
|
||||||
|
windchill_c: number;
|
||||||
|
windchill_f: number;
|
||||||
|
heatindex_c: number;
|
||||||
|
heatindex_f: number;
|
||||||
|
dewpoint_c: number;
|
||||||
|
dewpoint_f: number;
|
||||||
|
vis_km: number;
|
||||||
|
vis_miles: number;
|
||||||
|
uv: number;
|
||||||
|
gust_mph: number;
|
||||||
|
gust_kph: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherCurrent extends _WeatherState {
|
||||||
|
last_updated_epoch: number;
|
||||||
|
last_updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherHour extends _WeatherState {
|
||||||
|
time_epoch: number;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherDay {
|
||||||
|
date: string;
|
||||||
|
date_epoch: number;
|
||||||
|
day: {
|
||||||
|
maxtemp_c: number;
|
||||||
|
maxtemp_f: number;
|
||||||
|
mintemp_c: number;
|
||||||
|
mintemp_f: number;
|
||||||
|
avgtemp_c: number;
|
||||||
|
avgtemp_f: number;
|
||||||
|
maxwind_mph: number;
|
||||||
|
maxwind_kph: number;
|
||||||
|
totalprecip_mm: number;
|
||||||
|
totalprecip_in: number;
|
||||||
|
totalsnow_cm: number;
|
||||||
|
avgvis_km: number;
|
||||||
|
avgvis_miles: number;
|
||||||
|
avghumidity: number;
|
||||||
|
daily_will_it_rain: number;
|
||||||
|
daily_chance_of_rain: number;
|
||||||
|
daily_will_it_snow: number;
|
||||||
|
daily_chance_of_snow: number;
|
||||||
|
condition: WeatherCondition;
|
||||||
|
uv: number;
|
||||||
|
};
|
||||||
|
astro: {
|
||||||
|
sunrise: string;
|
||||||
|
sunset: string;
|
||||||
|
moonrise: string;
|
||||||
|
moonset: string;
|
||||||
|
moon_phase: string;
|
||||||
|
moon_illumination: string;
|
||||||
|
is_moon_up: number;
|
||||||
|
is_sun_up: number;
|
||||||
|
};
|
||||||
|
hour: WeatherHour[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherLocation {
|
||||||
|
name: string;
|
||||||
|
region: string;
|
||||||
|
country: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
tz_id: string;
|
||||||
|
localtime_epoch: number;
|
||||||
|
localtime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherData {
|
||||||
|
current: WeatherCurrent;
|
||||||
|
forecast: { forecastday: WeatherDay[] };
|
||||||
|
location: WeatherLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT: WeatherData = {
|
||||||
|
current: {
|
||||||
|
last_updated_epoch: 0,
|
||||||
|
last_updated: "",
|
||||||
|
temp_c: 0,
|
||||||
|
temp_f: 0,
|
||||||
|
is_day: 0,
|
||||||
|
condition: { text: "", icon: "", code: 0 },
|
||||||
|
wind_mph: 0,
|
||||||
|
wind_kph: 0,
|
||||||
|
wind_degree: 0,
|
||||||
|
wind_dir: "N",
|
||||||
|
pressure_mb: 0,
|
||||||
|
pressure_in: 0,
|
||||||
|
precip_mm: 0,
|
||||||
|
precip_in: 0,
|
||||||
|
humidity: 0,
|
||||||
|
cloud: 0,
|
||||||
|
feelslike_c: 0,
|
||||||
|
feelslike_f: 0,
|
||||||
|
windchill_c: 0,
|
||||||
|
windchill_f: 0,
|
||||||
|
heatindex_c: 0,
|
||||||
|
heatindex_f: 0,
|
||||||
|
dewpoint_c: 0,
|
||||||
|
dewpoint_f: 0,
|
||||||
|
vis_km: 0,
|
||||||
|
vis_miles: 0,
|
||||||
|
uv: 0,
|
||||||
|
gust_mph: 0,
|
||||||
|
gust_kph: 0,
|
||||||
|
},
|
||||||
|
forecast: {
|
||||||
|
forecastday: [
|
||||||
|
{
|
||||||
|
date: "",
|
||||||
|
date_epoch: 0,
|
||||||
|
day: {
|
||||||
|
maxtemp_c: 0,
|
||||||
|
maxtemp_f: 0,
|
||||||
|
mintemp_c: 0,
|
||||||
|
mintemp_f: 0,
|
||||||
|
avgtemp_c: 0,
|
||||||
|
avgtemp_f: 0,
|
||||||
|
maxwind_mph: 0,
|
||||||
|
maxwind_kph: 0,
|
||||||
|
totalprecip_mm: 0,
|
||||||
|
totalprecip_in: 0,
|
||||||
|
totalsnow_cm: 0,
|
||||||
|
avgvis_km: 0,
|
||||||
|
avgvis_miles: 0,
|
||||||
|
avghumidity: 0,
|
||||||
|
daily_will_it_rain: 0,
|
||||||
|
daily_chance_of_rain: 0,
|
||||||
|
daily_will_it_snow: 0,
|
||||||
|
daily_chance_of_snow: 0,
|
||||||
|
condition: { text: "", icon: "", code: 0 },
|
||||||
|
uv: 0,
|
||||||
|
},
|
||||||
|
astro: {
|
||||||
|
sunrise: "",
|
||||||
|
sunset: "",
|
||||||
|
moonrise: "",
|
||||||
|
moonset: "",
|
||||||
|
moon_phase: "",
|
||||||
|
moon_illumination: "",
|
||||||
|
is_moon_up: 0,
|
||||||
|
is_sun_up: 0,
|
||||||
|
},
|
||||||
|
hour: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
name: "",
|
||||||
|
region: "",
|
||||||
|
country: "",
|
||||||
|
lat: 0,
|
||||||
|
lon: 0,
|
||||||
|
tz_id: "",
|
||||||
|
localtime_epoch: 0,
|
||||||
|
localtime: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_ICONS: Record<string, string> = {
|
||||||
|
warning: "",
|
||||||
|
sunny: "",
|
||||||
|
clear: "",
|
||||||
|
partly_cloudy: "",
|
||||||
|
partly_cloudy_night: "",
|
||||||
|
cloudy: "",
|
||||||
|
overcast: "",
|
||||||
|
mist: "",
|
||||||
|
patchy_rain_nearby: "",
|
||||||
|
patchy_rain_possible: "",
|
||||||
|
patchy_snow_possible: "",
|
||||||
|
patchy_sleet_possible: "",
|
||||||
|
patchy_freezing_drizzle_possible: "",
|
||||||
|
thundery_outbreaks_possible: "",
|
||||||
|
blowing_snow: "",
|
||||||
|
blizzard: "",
|
||||||
|
fog: "",
|
||||||
|
freezing_fog: "",
|
||||||
|
patchy_light_drizzle: "",
|
||||||
|
light_drizzle: "",
|
||||||
|
freezing_drizzle: "",
|
||||||
|
heavy_freezing_drizzle: "",
|
||||||
|
patchy_light_rain: "",
|
||||||
|
light_rain: "",
|
||||||
|
moderate_rain_at_times: "",
|
||||||
|
moderate_rain: "",
|
||||||
|
heavy_rain_at_times: "",
|
||||||
|
heavy_rain: "",
|
||||||
|
light_freezing_rain: "",
|
||||||
|
moderate_or_heavy_freezing_rain: "",
|
||||||
|
light_sleet: "",
|
||||||
|
moderate_or_heavy_sleet: "",
|
||||||
|
patchy_light_snow: "",
|
||||||
|
light_snow: "",
|
||||||
|
patchy_moderate_snow: "",
|
||||||
|
moderate_snow: "",
|
||||||
|
patchy_heavy_snow: "",
|
||||||
|
heavy_snow: "",
|
||||||
|
ice_pellets: "",
|
||||||
|
light_rain_shower: "",
|
||||||
|
moderate_or_heavy_rain_shower: "",
|
||||||
|
torrential_rain_shower: "",
|
||||||
|
light_sleet_showers: "",
|
||||||
|
moderate_or_heavy_sleet_showers: "",
|
||||||
|
light_snow_showers: "",
|
||||||
|
moderate_or_heavy_snow_showers: "",
|
||||||
|
light_showers_of_ice_pellets: "",
|
||||||
|
moderate_or_heavy_showers_of_ice_pellets: "",
|
||||||
|
patchy_light_rain_with_thunder: "",
|
||||||
|
moderate_or_heavy_rain_with_thunder: "",
|
||||||
|
moderate_or_heavy_rain_in_area_with_thunder: "",
|
||||||
|
patchy_light_snow_with_thunder: "",
|
||||||
|
moderate_or_heavy_snow_with_thunder: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEMP_ICONS: Record<string, string> = {};
|
||||||
|
|
||||||
|
@register({ GTypeName: "Weather" })
|
||||||
|
export default class Weather extends GObject.Object {
|
||||||
|
static instance: Weather;
|
||||||
|
static get_default() {
|
||||||
|
if (!this.instance) this.instance = new Weather();
|
||||||
|
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly #cache: string = `${CACHE}/weather.json`;
|
||||||
|
|
||||||
|
#key: string = "";
|
||||||
|
#data: WeatherData = DEFAULT;
|
||||||
|
|
||||||
|
@property(Object)
|
||||||
|
get raw() {
|
||||||
|
return this.#data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(Object)
|
||||||
|
get current() {
|
||||||
|
return this.#data.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(Object)
|
||||||
|
get forecast() {
|
||||||
|
return this.#data.forecast.forecastday[0].hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(Object)
|
||||||
|
get location() {
|
||||||
|
return this.#data.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(String)
|
||||||
|
get condition() {
|
||||||
|
return this.#data.current.condition.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
|
get temperature() {
|
||||||
|
return this.#data.current.temp_c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
|
get wind() {
|
||||||
|
return this.#data.current.wind_kph;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
|
get rainChance() {
|
||||||
|
return this.#data.forecast.forecastday[0].day.daily_chance_of_rain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(String)
|
||||||
|
get icon() {
|
||||||
|
return this.getIcon(this.#data.current.condition.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(String)
|
||||||
|
get tempIcon() {
|
||||||
|
return this.getTempIcon(this.#data.current.temp_c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(String)
|
||||||
|
get tempColour() {
|
||||||
|
return this.getTempDesc(this.#data.current.temp_c);
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon(status: string) {
|
||||||
|
let query = status.trim().toLowerCase().replaceAll(" ", "_");
|
||||||
|
if (!this.#data.current.is_day && query + "_night" in STATUS_ICONS) query += "_night";
|
||||||
|
return STATUS_ICONS[query] ?? STATUS_ICONS.warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTempIcon(temp: number) {
|
||||||
|
if (temp >= 40) return "";
|
||||||
|
if (temp >= 30) return "";
|
||||||
|
if (temp >= 20) return "";
|
||||||
|
if (temp >= 10) return "";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getTempDesc(temp: number) {
|
||||||
|
if (temp >= 40) return "burning";
|
||||||
|
if (temp >= 30) return "hot";
|
||||||
|
if (temp >= 20) return "normal";
|
||||||
|
if (temp >= 10) return "cold";
|
||||||
|
return "freezing";
|
||||||
|
}
|
||||||
|
|
||||||
|
#notify() {
|
||||||
|
this.notify("raw");
|
||||||
|
this.notify("current");
|
||||||
|
this.notify("forecast");
|
||||||
|
this.notify("location");
|
||||||
|
this.notify("condition");
|
||||||
|
this.notify("temperature");
|
||||||
|
this.notify("wind");
|
||||||
|
this.notify("rain-chance");
|
||||||
|
this.notify("icon");
|
||||||
|
this.notify("temp-icon");
|
||||||
|
this.notify("temp-colour");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWeather() {
|
||||||
|
const location = config.location || JSON.parse(await execAsync("curl ipinfo.io")).city;
|
||||||
|
return JSON.parse(
|
||||||
|
await execAsync([
|
||||||
|
"curl",
|
||||||
|
`https://api.weatherapi.com/v1/forecast.json?key=${this.#key}&q=${location}&days=1&aqi=no&alerts=no`,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWeather() {
|
||||||
|
if (GLib.file_test(this.#cache, GLib.FileTest.EXISTS)) {
|
||||||
|
const cache = await readFileAsync(this.#cache);
|
||||||
|
const cache_data: WeatherData = JSON.parse(cache);
|
||||||
|
if (cache_data.location.localtime_epoch * 1000 + config.interval > Date.now()) {
|
||||||
|
if (JSON.stringify(this.#data) !== cache) {
|
||||||
|
this.#data = cache_data;
|
||||||
|
this.#notify();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.getWeather();
|
||||||
|
this.#data = data;
|
||||||
|
writeFileAsync(this.#cache, JSON.stringify(data)).catch(console.error); // Catch here so it doesn't propagate
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error getting weather:", e);
|
||||||
|
this.#data = DEFAULT;
|
||||||
|
}
|
||||||
|
this.#notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
readFileAsync(config.key)
|
||||||
|
.then(k => {
|
||||||
|
this.#key = k;
|
||||||
|
this.updateWeather().catch(console.error);
|
||||||
|
interval(config.interval, () => this.updateWeather().catch(console.error));
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { execAsync, GLib, type Gio } from "astal";
|
import { bind, execAsync, GLib, Variable, type Gio } from "astal";
|
||||||
import type AstalApps from "gi://AstalApps";
|
import type AstalApps from "gi://AstalApps";
|
||||||
import { osIcons } from "./icons";
|
import { osIcons } from "./icons";
|
||||||
|
|
||||||
|
|
@ -63,3 +63,7 @@ export const osIcon = String.fromCodePoint(
|
||||||
return 0xf31a;
|
return 0xf31a;
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const currentTime = Variable(GLib.DateTime.new_now_local()).poll(1000, () => GLib.DateTime.new_now_local());
|
||||||
|
export const bindCurrentTime = (format: string, fallback?: (time: GLib.DateTime) => string) =>
|
||||||
|
bind(currentTime).as(c => c.format(format) ?? fallback?.(c) ?? new Date().toLocaleString());
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,10 @@ export class MenuItem extends astalify(Gtk.MenuItem) {
|
||||||
super(props as any);
|
super(props as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@register()
|
||||||
|
export class Calendar extends astalify(Gtk.Calendar) {
|
||||||
|
constructor(props: ConstructProps<Calendar, Gtk.Calendar.ConstructorProps>) {
|
||||||
|
super(props as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue