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/
|
||||
scss/scheme/_index.scss
|
||||
scss/scheme/_dynamic.scss
|
||||
assets/weather-api-key.txt
|
||||
|
|
|
|||
|
|
@ -77,3 +77,10 @@ export const math = {
|
|||
export const updates = {
|
||||
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 "networks";
|
||||
@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 { App, Astal, astalify, Gdk, Gtk, type ConstructProps } from "astal/gtk3";
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
|
|
@ -13,7 +13,7 @@ import Players from "../services/players";
|
|||
import Updates from "../services/updates";
|
||||
import { getAppCategoryIcon } from "../utils/icons";
|
||||
import { ellipsize } from "../utils/strings";
|
||||
import { osIcon } from "../utils/system";
|
||||
import { bindCurrentTime, osIcon } from "../utils/system";
|
||||
import { setupCustomTooltip } from "../utils/widgets";
|
||||
import type PopupWindow from "../widgets/popupwindow";
|
||||
|
||||
|
|
@ -438,19 +438,14 @@ const NotifCount = () => (
|
|||
);
|
||||
|
||||
const DateTime = () => (
|
||||
<box className="module date-time">
|
||||
<label className="icon" label="calendar_month" />
|
||||
<label
|
||||
setup={self => {
|
||||
const pollVar = Variable(null).poll(5000, () => {
|
||||
self.label =
|
||||
GLib.DateTime.new_now_local().format(config.dateTimeFormat) ?? new Date().toLocaleString();
|
||||
return null;
|
||||
});
|
||||
self.connect("destroy", () => pollVar.drop());
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
<button
|
||||
onClick={(self, event) => event.button === Astal.MouseButton.PRIMARY && togglePopup(self, event, "sideright")}
|
||||
>
|
||||
<box className="module date-time">
|
||||
<label className="icon" label="calendar_month" />
|
||||
<label label={bindCurrentTime(config.dateTimeFormat)} />
|
||||
</box>
|
||||
</button>
|
||||
);
|
||||
|
||||
const Power = () => (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import BluetoothDevices from "./bluetoothdevices";
|
|||
import Media from "./media";
|
||||
import Networks from "./networks";
|
||||
import Notifications from "./notifications";
|
||||
import SideRight from "./sideright";
|
||||
import Updates from "./updates";
|
||||
|
||||
export default () => {
|
||||
|
|
@ -10,6 +11,7 @@ export default () => {
|
|||
<BluetoothDevices />;
|
||||
<Networks />;
|
||||
<Media />;
|
||||
<SideRight />;
|
||||
|
||||
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 { osIcons } from "./icons";
|
||||
|
||||
|
|
@ -63,3 +63,7 @@ export const osIcon = String.fromCodePoint(
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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