Converted a significant amount of files from js to ts.
This commit is contained in:
130
.eslintrc.yml
Normal file
130
.eslintrc.yml
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
env:
|
||||||
|
es2022: true
|
||||||
|
extends:
|
||||||
|
- "eslint:recommended"
|
||||||
|
- "plugin:@typescript-eslint/recommended"
|
||||||
|
parser: "@typescript-eslint/parser"
|
||||||
|
parserOptions:
|
||||||
|
ecmaVersion: 2022
|
||||||
|
sourceType: "module"
|
||||||
|
project: "./tsconfig.json"
|
||||||
|
warnOnUnsupportedTypeScriptVersion: false
|
||||||
|
root: true
|
||||||
|
ignorePatterns:
|
||||||
|
- types/
|
||||||
|
plugins:
|
||||||
|
- "@typescript-eslint"
|
||||||
|
rules:
|
||||||
|
"@typescript-eslint/ban-ts-comment":
|
||||||
|
- "off"
|
||||||
|
"@typescript-eslint/no-non-null-assertion":
|
||||||
|
- "off"
|
||||||
|
# "@typescript-eslint/no-explicit-any":
|
||||||
|
# - "off"
|
||||||
|
"@typescript-eslint/no-unused-vars":
|
||||||
|
- error
|
||||||
|
- varsIgnorePattern: (^unused|_$)
|
||||||
|
argsIgnorePattern: ^(unused|_)
|
||||||
|
"@typescript-eslint/no-empty-interface":
|
||||||
|
- "off"
|
||||||
|
|
||||||
|
arrow-parens:
|
||||||
|
- error
|
||||||
|
- as-needed
|
||||||
|
comma-dangle:
|
||||||
|
- error
|
||||||
|
- always-multiline
|
||||||
|
comma-spacing:
|
||||||
|
- error
|
||||||
|
- before: false
|
||||||
|
after: true
|
||||||
|
comma-style:
|
||||||
|
- error
|
||||||
|
- last
|
||||||
|
curly:
|
||||||
|
- error
|
||||||
|
- multi-or-nest
|
||||||
|
- consistent
|
||||||
|
dot-location:
|
||||||
|
- error
|
||||||
|
- property
|
||||||
|
eol-last:
|
||||||
|
- error
|
||||||
|
eqeqeq:
|
||||||
|
- error
|
||||||
|
- always
|
||||||
|
indent:
|
||||||
|
- error
|
||||||
|
- 4
|
||||||
|
- SwitchCase: 1
|
||||||
|
keyword-spacing:
|
||||||
|
- error
|
||||||
|
- before: true
|
||||||
|
lines-between-class-members:
|
||||||
|
- error
|
||||||
|
- always
|
||||||
|
- exceptAfterSingleLine: true
|
||||||
|
padded-blocks:
|
||||||
|
- error
|
||||||
|
- never
|
||||||
|
- allowSingleLineBlocks: false
|
||||||
|
prefer-const:
|
||||||
|
- error
|
||||||
|
quotes:
|
||||||
|
- error
|
||||||
|
- double
|
||||||
|
- avoidEscape: true
|
||||||
|
semi:
|
||||||
|
- error
|
||||||
|
- never
|
||||||
|
nonblock-statement-body-position:
|
||||||
|
- error
|
||||||
|
- below
|
||||||
|
no-trailing-spaces:
|
||||||
|
- error
|
||||||
|
no-useless-escape:
|
||||||
|
- off
|
||||||
|
max-len:
|
||||||
|
- error
|
||||||
|
- code: 100
|
||||||
|
func-call-spacing:
|
||||||
|
- error
|
||||||
|
array-bracket-spacing:
|
||||||
|
- error
|
||||||
|
space-before-function-paren:
|
||||||
|
- error
|
||||||
|
- anonymous: never
|
||||||
|
named: never
|
||||||
|
asyncArrow: ignore
|
||||||
|
space-before-blocks:
|
||||||
|
- error
|
||||||
|
key-spacing:
|
||||||
|
- error
|
||||||
|
object-curly-spacing:
|
||||||
|
- error
|
||||||
|
- always
|
||||||
|
globals:
|
||||||
|
Widget: readonly
|
||||||
|
Utils: readonly
|
||||||
|
App: readonly
|
||||||
|
Variable: readonly
|
||||||
|
Service: readonly
|
||||||
|
pkg: readonly
|
||||||
|
ARGV: readonly
|
||||||
|
Debugger: readonly
|
||||||
|
GIRepositoryGType: readonly
|
||||||
|
globalThis: readonly
|
||||||
|
imports: readonly
|
||||||
|
Intl: readonly
|
||||||
|
log: readonly
|
||||||
|
logError: readonly
|
||||||
|
print: readonly
|
||||||
|
printerr: readonly
|
||||||
|
window: readonly
|
||||||
|
TextEncoder: readonly
|
||||||
|
TextDecoder: readonly
|
||||||
|
console: readonly
|
||||||
|
setTimeout: readonly
|
||||||
|
setInterval: readonly
|
||||||
|
clearTimeout: readonly
|
||||||
|
clearInterval: readonly
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const globalMousePos = Variable([]);
|
const globalMousePos = Variable([0, 0]);
|
||||||
|
|
||||||
globalThis["globalMousePos"] = globalMousePos;
|
globalThis["globalMousePos"] = globalMousePos;
|
||||||
|
|
||||||
1053
lib/types/defaults/weather.ts
Normal file
1053
lib/types/defaults/weather.ts
Normal file
File diff suppressed because it is too large
Load Diff
1
lib/types/options.d.ts
vendored
Normal file
1
lib/types/options.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type Unit = "imperial" | "metric";
|
||||||
107
lib/types/weather.d.ts
vendored
Normal file
107
lib/types/weather.d.ts
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
export type Weather = {
|
||||||
|
location: Location;
|
||||||
|
current: Current;
|
||||||
|
forecast: Forecast;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Current = {
|
||||||
|
last_updated_epoch?: number;
|
||||||
|
last_updated?: string;
|
||||||
|
temp_c: number;
|
||||||
|
temp_f: number;
|
||||||
|
is_day: number;
|
||||||
|
condition: Condition;
|
||||||
|
wind_mph: number;
|
||||||
|
wind_kph: number;
|
||||||
|
wind_degree: number;
|
||||||
|
wind_dir: string;
|
||||||
|
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;
|
||||||
|
time_epoch?: number;
|
||||||
|
time?: string;
|
||||||
|
snow_cm?: number;
|
||||||
|
will_it_rain?: number;
|
||||||
|
chance_of_rain?: number;
|
||||||
|
will_it_snow?: number;
|
||||||
|
chance_of_snow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Condition = {
|
||||||
|
text: string;
|
||||||
|
icon: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Forecast = {
|
||||||
|
forecastday: Forecastday[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Forecastday = {
|
||||||
|
date: string;
|
||||||
|
date_epoch: number;
|
||||||
|
day: Day;
|
||||||
|
astro: Astro;
|
||||||
|
hour: Current[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Astro = {
|
||||||
|
sunrise: string;
|
||||||
|
sunset: string;
|
||||||
|
moonrise: string;
|
||||||
|
moonset: string;
|
||||||
|
moon_phase: string;
|
||||||
|
moon_illumination: number;
|
||||||
|
is_moon_up: number;
|
||||||
|
is_sun_up: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type 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: Condition;
|
||||||
|
uv: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Location = {
|
||||||
|
name: string;
|
||||||
|
region: string;
|
||||||
|
country: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
tz_id: string;
|
||||||
|
localtime_epoch: number;
|
||||||
|
localtime: string;
|
||||||
|
}
|
||||||
8
lib/types/workspace.d.ts
vendored
Normal file
8
lib/types/workspace.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type WorkspaceRule = {
|
||||||
|
workspaceString: string,
|
||||||
|
monitor: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkspaceMap = {
|
||||||
|
[key: string]: number[],
|
||||||
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
const battery = await Service.import("battery");
|
|
||||||
import { openMenu } from "../utils.js";
|
|
||||||
import options from "options";
|
|
||||||
|
|
||||||
const { label: show_label } = options.bar.battery;
|
|
||||||
|
|
||||||
const BatteryLabel = () => {
|
|
||||||
const isVis = Variable(battery.available);
|
|
||||||
|
|
||||||
const icon = () =>
|
|
||||||
battery
|
|
||||||
.bind("percent")
|
|
||||||
.as((p) => `battery-level-${Math.floor(p / 10) * 10}-symbolic`);
|
|
||||||
|
|
||||||
battery.connect("changed", ({ available }) => {
|
|
||||||
isVis.value = available;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatTime = (seconds) => {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
return { hours, minutes };
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateTooltip = (timeSeconds, isCharging, isCharged) => {
|
|
||||||
if (isCharged) {
|
|
||||||
return "Fully Charged!!!";
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hours, minutes } = formatTime(timeSeconds);
|
|
||||||
if (isCharging) {
|
|
||||||
return `${hours} hours ${minutes} minutes until full`;
|
|
||||||
} else {
|
|
||||||
return `${hours} hours ${minutes} minutes left`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: Widget.Box({
|
|
||||||
class_name: "battery",
|
|
||||||
visible: battery.bind("available"),
|
|
||||||
tooltip_text: battery.bind("time_remaining").as((t) => t.toString()),
|
|
||||||
children: Utils.merge(
|
|
||||||
[battery.bind("available"), show_label.bind("value")],
|
|
||||||
(batAvail, showLabel) => {
|
|
||||||
if (batAvail && showLabel) {
|
|
||||||
return [
|
|
||||||
Widget.Icon({ icon: icon() }),
|
|
||||||
Widget.Label({
|
|
||||||
label: battery.bind("percent").as((p) => ` ${p}%`),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
} else if (batAvail && !showLabel) {
|
|
||||||
return [Widget.Icon({ icon: icon() })];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
setup: (self) => {
|
|
||||||
self.hook(battery, () => {
|
|
||||||
if (battery.available) {
|
|
||||||
self.tooltip_text = generateTooltip(
|
|
||||||
battery.time_remaining,
|
|
||||||
battery.charging,
|
|
||||||
battery.charged,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
isVis,
|
|
||||||
boxClass: "battery",
|
|
||||||
props: {
|
|
||||||
on_primary_click: (clicked, event) => {
|
|
||||||
openMenu(clicked, event, "energymenu");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { BatteryLabel };
|
|
||||||
84
modules/bar/battery/index.ts
Normal file
84
modules/bar/battery/index.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const battery = await Service.import("battery");
|
||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
import EventHandler from 'types/widgets/button.ts'
|
||||||
|
import { openMenu } from "../utils.js";
|
||||||
|
import options from "options";
|
||||||
|
|
||||||
|
const { label: show_label } = options.bar.battery;
|
||||||
|
|
||||||
|
const BatteryLabel = () => {
|
||||||
|
const isVis = Variable(battery.available);
|
||||||
|
|
||||||
|
const icon = () =>
|
||||||
|
battery
|
||||||
|
.bind("percent")
|
||||||
|
.as((p) => `battery-level-${Math.floor(p / 10) * 10}-symbolic`);
|
||||||
|
|
||||||
|
battery.connect("changed", ({ available }) => {
|
||||||
|
isVis.value = available;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
return { hours, minutes };
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateTooltip = (timeSeconds: number, isCharging: boolean, isCharged: boolean) => {
|
||||||
|
if (isCharged) {
|
||||||
|
return "Fully Charged!!!";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hours, minutes } = formatTime(timeSeconds);
|
||||||
|
if (isCharging) {
|
||||||
|
return `${hours} hours ${minutes} minutes until full`;
|
||||||
|
} else {
|
||||||
|
return `${hours} hours ${minutes} minutes left`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: Widget.Box({
|
||||||
|
class_name: "battery",
|
||||||
|
visible: battery.bind("available"),
|
||||||
|
tooltip_text: battery.bind("time_remaining").as((t) => t.toString()),
|
||||||
|
children: Utils.merge(
|
||||||
|
[battery.bind("available"), show_label.bind("value")],
|
||||||
|
(batAvail, showLabel) => {
|
||||||
|
if (batAvail && showLabel) {
|
||||||
|
return [
|
||||||
|
Widget.Icon({ icon: icon() }),
|
||||||
|
Widget.Label({
|
||||||
|
label: battery.bind("percent").as((p) => ` ${p}%`),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
} else if (batAvail && !showLabel) {
|
||||||
|
return [Widget.Icon({ icon: icon() })];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
setup: (self) => {
|
||||||
|
self.hook(battery, () => {
|
||||||
|
if (battery.available) {
|
||||||
|
self.tooltip_text = generateTooltip(
|
||||||
|
battery.time_remaining,
|
||||||
|
battery.charging,
|
||||||
|
battery.charged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
isVis,
|
||||||
|
boxClass: "battery",
|
||||||
|
props: {
|
||||||
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
|
openMenu(clicked, event, "energymenu");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BatteryLabel };
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
const bluetooth = await Service.import('bluetooth')
|
|
||||||
import options from "options";
|
|
||||||
import { openMenu } from "../utils.js";
|
|
||||||
|
|
||||||
const Bluetooth = () => {
|
|
||||||
const btIcon = Widget.Label({
|
|
||||||
label: bluetooth.bind("enabled").as((v) => v ? "" : ""),
|
|
||||||
class_name: "bar-bt_icon",
|
|
||||||
});
|
|
||||||
|
|
||||||
const btText = Widget.Label({
|
|
||||||
label: Utils.merge([bluetooth.bind("enabled"), options.bar.bluetooth.label.bind("value")], (btEnabled, showLabel) => {
|
|
||||||
if (showLabel) {
|
|
||||||
return btEnabled ? " On" : " Off"
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
|
|
||||||
}),
|
|
||||||
class_name: "bar-bt_label",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: Widget.Box({
|
|
||||||
class_name: "volume",
|
|
||||||
children: [btIcon, btText],
|
|
||||||
}),
|
|
||||||
isVisible: true,
|
|
||||||
boxClass: "bluetooth",
|
|
||||||
props: {
|
|
||||||
on_primary_click: (clicked, event) => {
|
|
||||||
openMenu(clicked, event, "bluetoothmenu");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Bluetooth }
|
|
||||||
39
modules/bar/bluetooth/index.ts
Normal file
39
modules/bar/bluetooth/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const bluetooth = await Service.import('bluetooth')
|
||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
import options from "options";
|
||||||
|
import { openMenu } from "../utils.js";
|
||||||
|
|
||||||
|
const Bluetooth = () => {
|
||||||
|
const btIcon = Widget.Label({
|
||||||
|
label: bluetooth.bind("enabled").as((v) => v ? "" : ""),
|
||||||
|
class_name: "bar-bt_icon",
|
||||||
|
});
|
||||||
|
|
||||||
|
const btText = Widget.Label({
|
||||||
|
label: Utils.merge([bluetooth.bind("enabled"), options.bar.bluetooth.label.bind("value")], (btEnabled, showLabel) => {
|
||||||
|
if (showLabel) {
|
||||||
|
return btEnabled ? " On" : " Off"
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
|
||||||
|
}),
|
||||||
|
class_name: "bar-bt_label",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: Widget.Box({
|
||||||
|
class_name: "volume",
|
||||||
|
children: [btIcon, btText],
|
||||||
|
}),
|
||||||
|
isVisible: true,
|
||||||
|
boxClass: "bluetooth",
|
||||||
|
props: {
|
||||||
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
|
openMenu(clicked, event, "bluetoothmenu");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Bluetooth }
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import GLib from "gi://GLib";
|
|
||||||
import { openMenu } from "../utils.js";
|
|
||||||
import options from "options";
|
|
||||||
const { format } = options.bar.clock;
|
|
||||||
|
|
||||||
const date = Variable(GLib.DateTime.new_now_local(), {
|
|
||||||
poll: [1000, () => GLib.DateTime.new_now_local()],
|
|
||||||
});
|
|
||||||
const time = Utils.derive([date, format], (c, f) => c.format(f) || "");
|
|
||||||
|
|
||||||
const Clock = () => {
|
|
||||||
return {
|
|
||||||
component: Widget.Label({
|
|
||||||
class_name: "clock",
|
|
||||||
label: time.bind(),
|
|
||||||
}),
|
|
||||||
isVisible: true,
|
|
||||||
boxClass: "clock",
|
|
||||||
props: {
|
|
||||||
on_primary_click: (clicked, event) => {
|
|
||||||
openMenu(clicked, event, "calendarmenu");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Clock };
|
|
||||||
28
modules/bar/clock/index.ts
Normal file
28
modules/bar/clock/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
import GLib from "gi://GLib";
|
||||||
|
import { openMenu } from "../utils.js";
|
||||||
|
import options from "options";
|
||||||
|
const { format } = options.bar.clock;
|
||||||
|
|
||||||
|
const date = Variable(GLib.DateTime.new_now_local(), {
|
||||||
|
poll: [1000, () => GLib.DateTime.new_now_local()],
|
||||||
|
});
|
||||||
|
const time = Utils.derive([date, format], (c, f) => c.format(f) || "");
|
||||||
|
|
||||||
|
const Clock = () => {
|
||||||
|
return {
|
||||||
|
component: Widget.Label({
|
||||||
|
class_name: "clock",
|
||||||
|
label: time.bind(),
|
||||||
|
}),
|
||||||
|
isVisible: true,
|
||||||
|
boxClass: "clock",
|
||||||
|
props: {
|
||||||
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
|
openMenu(clicked, event, "calendarmenu");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Clock };
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
const mpris = await Service.import("mpris");
|
|
||||||
import { openMenu } from "../utils.js";
|
|
||||||
|
|
||||||
const Media = () => {
|
|
||||||
const activePlayer = Variable(mpris.players[0]);
|
|
||||||
|
|
||||||
mpris.connect("changed", (value) => {
|
|
||||||
const statusOrder = {
|
|
||||||
Playing: 1,
|
|
||||||
Paused: 2,
|
|
||||||
Stopped: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (value.players.length === 0) {
|
|
||||||
activePlayer.value = mpris.players[0];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPlaying = value.players.find(
|
|
||||||
(p) => p["play-back-status"] === "Playing",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isPlaying) {
|
|
||||||
activePlayer.value = value.players.sort(
|
|
||||||
(a, b) =>
|
|
||||||
statusOrder[a["play-back-status"]] -
|
|
||||||
statusOrder[b["play-back-status"]],
|
|
||||||
)[0];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getIconForPlayer = (playerName) => {
|
|
||||||
const windowTitleMap = [
|
|
||||||
["Mozilla Firefox", " "],
|
|
||||||
["Microsoft Edge", " "],
|
|
||||||
["(.*)Discord(.*)", " "],
|
|
||||||
["Plex", " "],
|
|
||||||
["(.*) Spotify Free", " "],
|
|
||||||
["(.*)Spotify Premium", " "],
|
|
||||||
["Spotify", " "],
|
|
||||||
["(.*)", " "],
|
|
||||||
];
|
|
||||||
|
|
||||||
const foundMatch = windowTitleMap.find((wt) =>
|
|
||||||
RegExp(wt[0]).test(playerName),
|
|
||||||
);
|
|
||||||
|
|
||||||
return foundMatch ? foundMatch[1] : "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const songIcon = Variable("");
|
|
||||||
|
|
||||||
const label = Utils.watch(" Media ", mpris, "changed", () => {
|
|
||||||
if (activePlayer.value) {
|
|
||||||
const { track_title, identity } = activePlayer.value;
|
|
||||||
songIcon.value = getIconForPlayer(identity);
|
|
||||||
return track_title.length === 0
|
|
||||||
? ` No media playing...`
|
|
||||||
: ` ${track_title}`;
|
|
||||||
} else {
|
|
||||||
songIcon.value = "";
|
|
||||||
return " Media ";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: Widget.Box({
|
|
||||||
visible: false,
|
|
||||||
child: Widget.Box({
|
|
||||||
class_name: "media",
|
|
||||||
child: Widget.Box({
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "bar-media_icon",
|
|
||||||
label: songIcon.bind("value"),
|
|
||||||
maxWidthChars: 30,
|
|
||||||
}),
|
|
||||||
Widget.Label({
|
|
||||||
label,
|
|
||||||
truncate: "end",
|
|
||||||
wrap: true,
|
|
||||||
maxWidthChars: 30,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
isVisible: false,
|
|
||||||
boxClass: "media",
|
|
||||||
name: "media",
|
|
||||||
props: {
|
|
||||||
on_scroll_up: () => mpris.getPlayer("")?.next(),
|
|
||||||
on_scroll_down: () => mpris.getPlayer("")?.previous(),
|
|
||||||
on_primary_click: (clicked, event) => {
|
|
||||||
openMenu(clicked, event, "mediamenu");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Media };
|
|
||||||
102
modules/bar/media/index.ts
Normal file
102
modules/bar/media/index.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
const mpris = await Service.import("mpris");
|
||||||
|
import { openMenu } from "../utils.js";
|
||||||
|
|
||||||
|
const Media = () => {
|
||||||
|
const activePlayer = Variable(mpris.players[0]);
|
||||||
|
|
||||||
|
mpris.connect("changed", (value) => {
|
||||||
|
const statusOrder = {
|
||||||
|
Playing: 1,
|
||||||
|
Paused: 2,
|
||||||
|
Stopped: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value.players.length === 0) {
|
||||||
|
activePlayer.value = mpris.players[0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPlaying = value.players.find(
|
||||||
|
(p) => p["play-back-status"] === "Playing",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
activePlayer.value = value.players.sort(
|
||||||
|
(a, b) =>
|
||||||
|
statusOrder[a["play-back-status"]] -
|
||||||
|
statusOrder[b["play-back-status"]],
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getIconForPlayer = (playerName: string) => {
|
||||||
|
const windowTitleMap = [
|
||||||
|
["Mozilla Firefox", " "],
|
||||||
|
["Microsoft Edge", " "],
|
||||||
|
["(.*)Discord(.*)", " "],
|
||||||
|
["Plex", " "],
|
||||||
|
["(.*) Spotify Free", " "],
|
||||||
|
["(.*)Spotify Premium", " "],
|
||||||
|
["Spotify", " "],
|
||||||
|
["(.*)", " "],
|
||||||
|
];
|
||||||
|
|
||||||
|
const foundMatch = windowTitleMap.find((wt) =>
|
||||||
|
RegExp(wt[0]).test(playerName),
|
||||||
|
);
|
||||||
|
|
||||||
|
return foundMatch ? foundMatch[1] : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const songIcon = Variable("");
|
||||||
|
|
||||||
|
const label = Utils.watch(" Media ", mpris, "changed", () => {
|
||||||
|
if (activePlayer.value) {
|
||||||
|
const { track_title, identity } = activePlayer.value;
|
||||||
|
songIcon.value = getIconForPlayer(identity);
|
||||||
|
return track_title.length === 0
|
||||||
|
? ` No media playing...`
|
||||||
|
: ` ${track_title}`;
|
||||||
|
} else {
|
||||||
|
songIcon.value = "";
|
||||||
|
return " Media ";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: Widget.Box({
|
||||||
|
visible: false,
|
||||||
|
child: Widget.Box({
|
||||||
|
class_name: "media",
|
||||||
|
child: Widget.Box({
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "bar-media_icon",
|
||||||
|
label: songIcon.bind("value"),
|
||||||
|
maxWidthChars: 30,
|
||||||
|
}),
|
||||||
|
Widget.Label({
|
||||||
|
label,
|
||||||
|
truncate: "end",
|
||||||
|
wrap: true,
|
||||||
|
maxWidthChars: 30,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
isVisible: false,
|
||||||
|
boxClass: "media",
|
||||||
|
name: "media",
|
||||||
|
props: {
|
||||||
|
on_scroll_up: () => mpris.getPlayer("")?.next(),
|
||||||
|
on_scroll_down: () => mpris.getPlayer("")?.previous(),
|
||||||
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
|
openMenu(clicked, event, "mediamenu");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Media };
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { openMenu } from "../utils.js";
|
|
||||||
import options from "options";
|
|
||||||
|
|
||||||
const Menu = () => {
|
|
||||||
return {
|
|
||||||
component: Widget.Box({
|
|
||||||
child: Widget.Label({
|
|
||||||
class_name: "bar-menu_label",
|
|
||||||
label: options.bar.launcher.icon.bind("value"),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
isVisible: true,
|
|
||||||
boxClass: "dashboard",
|
|
||||||
props: {
|
|
||||||
on_primary_click: (clicked, event) => {
|
|
||||||
openMenu(clicked, event, "dashboardmenu");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Menu };
|
|
||||||
23
modules/bar/menu/index.ts
Normal file
23
modules/bar/menu/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
import { openMenu } from "../utils.js";
|
||||||
|
import options from "options";
|
||||||
|
|
||||||
|
const Menu = () => {
|
||||||
|
return {
|
||||||
|
component: Widget.Box({
|
||||||
|
child: Widget.Label({
|
||||||
|
class_name: "bar-menu_label",
|
||||||
|
label: options.bar.launcher.icon.bind("value"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
isVisible: true,
|
||||||
|
boxClass: "dashboard",
|
||||||
|
props: {
|
||||||
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
|
openMenu(clicked, event, "dashboardmenu");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Menu };
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
const network = await Service.import("network");
|
const network = await Service.import("network");
|
||||||
import options from "options";
|
import options from "options";
|
||||||
import { openMenu } from "../utils.js";
|
import { openMenu } from "../utils.js";
|
||||||
@@ -52,7 +53,7 @@ const Network = () => {
|
|||||||
isVisible: true,
|
isVisible: true,
|
||||||
boxClass: "network",
|
boxClass: "network",
|
||||||
props: {
|
props: {
|
||||||
on_primary_click: (clicked, event) => {
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
openMenu(clicked, event, "networkmenu");
|
openMenu(clicked, event, "networkmenu");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { openMenu } from "../utils.js";
|
|
||||||
import options from "options";
|
|
||||||
|
|
||||||
const { show_total } = options.bar.notifications;
|
|
||||||
|
|
||||||
const notifs = await Service.import("notifications");
|
|
||||||
|
|
||||||
export const Notifications = () => {
|
|
||||||
return {
|
|
||||||
component: Widget.Box({
|
|
||||||
hpack: "start",
|
|
||||||
child: Widget.Box({
|
|
||||||
hpack: "start",
|
|
||||||
class_name: "bar-notifications",
|
|
||||||
children: Utils.merge(
|
|
||||||
[notifs.bind("notifications"), notifs.bind("dnd"), show_total.bind("value")],
|
|
||||||
(notif, dnd, showTotal) => {
|
|
||||||
const notifIcon = Widget.Label({
|
|
||||||
hpack: "center",
|
|
||||||
class_name: "bar-notifications-label",
|
|
||||||
label: dnd ? "" : notif.length > 0 ? "" : "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const notifLabel = Widget.Label({
|
|
||||||
hpack: "center",
|
|
||||||
class_name: "bar-notifications-total",
|
|
||||||
label: notif.length.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (showTotal) {
|
|
||||||
return [notifIcon, notifLabel];
|
|
||||||
}
|
|
||||||
return [notifIcon];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
isVisible: true,
|
|
||||||
boxClass: "notifications",
|
|
||||||
props: {
|
|
||||||
on_primary_click: (clicked, event) => {
|
|
||||||
openMenu(clicked, event, "notificationsmenu");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
47
modules/bar/notifications/index.ts
Normal file
47
modules/bar/notifications/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
import { openMenu } from "../utils.js";
|
||||||
|
import options from "options";
|
||||||
|
|
||||||
|
const { show_total } = options.bar.notifications;
|
||||||
|
|
||||||
|
const notifs = await Service.import("notifications");
|
||||||
|
|
||||||
|
export const Notifications = () => {
|
||||||
|
return {
|
||||||
|
component: Widget.Box({
|
||||||
|
hpack: "start",
|
||||||
|
child: Widget.Box({
|
||||||
|
hpack: "start",
|
||||||
|
class_name: "bar-notifications",
|
||||||
|
children: Utils.merge(
|
||||||
|
[notifs.bind("notifications"), notifs.bind("dnd"), show_total.bind("value")],
|
||||||
|
(notif, dnd, showTotal) => {
|
||||||
|
const notifIcon = Widget.Label({
|
||||||
|
hpack: "center",
|
||||||
|
class_name: "bar-notifications-label",
|
||||||
|
label: dnd ? "" : notif.length > 0 ? "" : "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifLabel = Widget.Label({
|
||||||
|
hpack: "center",
|
||||||
|
class_name: "bar-notifications-total",
|
||||||
|
label: notif.length.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (showTotal) {
|
||||||
|
return [notifIcon, notifLabel];
|
||||||
|
}
|
||||||
|
return [notifIcon];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
isVisible: true,
|
||||||
|
boxClass: "notifications",
|
||||||
|
props: {
|
||||||
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
|
openMenu(clicked, event, "notificationsmenu");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
const systemtray = await Service.import("systemtray");
|
|
||||||
import { bash } from "lib/utils";
|
|
||||||
import options from "options";
|
|
||||||
|
|
||||||
const { ignore } = options.bar.systray;
|
|
||||||
|
|
||||||
const SysTray = () => {
|
|
||||||
const isVis = Variable(false);
|
|
||||||
|
|
||||||
const items = Utils.merge(
|
|
||||||
[systemtray.bind("items"), ignore.bind("value")],
|
|
||||||
(items, ignored) => {
|
|
||||||
const filteredTray = items.filter(({ id }) => !ignored.includes(id));
|
|
||||||
|
|
||||||
isVis.value = filteredTray.length > 0;
|
|
||||||
|
|
||||||
return filteredTray.map((item) => {
|
|
||||||
if (item.menu !== undefined) {
|
|
||||||
item.menu["class_name"] = "systray-menu";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Widget.Button({
|
|
||||||
cursor: "pointer",
|
|
||||||
child: Widget.Icon({
|
|
||||||
class_name: "systray-icon",
|
|
||||||
icon: item.bind("icon"),
|
|
||||||
}),
|
|
||||||
on_primary_click: (_, event) => item.activate(event),
|
|
||||||
on_secondary_click: (_, event) => item.openMenu(event),
|
|
||||||
tooltip_markup: item.bind("tooltip_markup"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: Widget.Box({
|
|
||||||
class_name: "systray",
|
|
||||||
children: items,
|
|
||||||
}),
|
|
||||||
isVisible: true,
|
|
||||||
boxClass: "systray",
|
|
||||||
isVis,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { SysTray };
|
|
||||||
47
modules/bar/systray/index.ts
Normal file
47
modules/bar/systray/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
const systemtray = await Service.import("systemtray");
|
||||||
|
import options from "options";
|
||||||
|
|
||||||
|
const { ignore } = options.bar.systray;
|
||||||
|
|
||||||
|
const SysTray = () => {
|
||||||
|
const isVis = Variable(false);
|
||||||
|
|
||||||
|
const items = Utils.merge(
|
||||||
|
[systemtray.bind("items"), ignore.bind("value")],
|
||||||
|
(items, ignored) => {
|
||||||
|
const filteredTray = items.filter(({ id }) => !ignored.includes(id));
|
||||||
|
|
||||||
|
isVis.value = filteredTray.length > 0;
|
||||||
|
|
||||||
|
return filteredTray.map((item) => {
|
||||||
|
if (item.menu !== undefined) {
|
||||||
|
item.menu["class_name"] = "systray-menu";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Widget.Button({
|
||||||
|
cursor: "pointer",
|
||||||
|
child: Widget.Icon({
|
||||||
|
class_name: "systray-icon",
|
||||||
|
icon: item.bind("icon"),
|
||||||
|
}),
|
||||||
|
on_primary_click: (_: any, event: Gdk.Event) => item.activate(event),
|
||||||
|
on_secondary_click: (_, event) => item.openMenu(event),
|
||||||
|
tooltip_markup: item.bind("tooltip_markup"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: Widget.Box({
|
||||||
|
class_name: "systray",
|
||||||
|
children: items,
|
||||||
|
}),
|
||||||
|
isVisible: true,
|
||||||
|
boxClass: "systray",
|
||||||
|
isVis,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SysTray };
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
export const closeAllMenus = () => {
|
|
||||||
const menuWindows = App.windows
|
|
||||||
.filter((w) => {
|
|
||||||
if (w.name) {
|
|
||||||
return /.*menu/.test(w.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((w) => w.name);
|
|
||||||
|
|
||||||
menuWindows.forEach((w) => {
|
|
||||||
if (w) {
|
|
||||||
App.closeWindow(w);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const openMenu = (clicked, event, window) => {
|
|
||||||
/*
|
|
||||||
* NOTE: We have to make some adjustments so the menu pops up relatively
|
|
||||||
* to the center of the button clicked. We don't want the menu to spawn
|
|
||||||
* offcenter dependending on which edge of the button you click on.
|
|
||||||
* -------------
|
|
||||||
* To fix this, we take the x coordinate of the click within the button's bounds.
|
|
||||||
* If you click the left edge of a 100 width button, then the x axis will be 0
|
|
||||||
* and if you click the right edge then the x axis will be 100.
|
|
||||||
* -------------
|
|
||||||
* Then we divide the width of the button by 2 to get the center of the button and then get
|
|
||||||
* the offset by subtracting the clicked x coordinate. Then we can apply that offset
|
|
||||||
* to the x coordinate of the click relative to the screen to get the center of the
|
|
||||||
* icon click.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const middleOfButton = Math.floor(clicked.get_allocated_width() / 2);
|
|
||||||
const xAxisOfButtonClick = clicked.get_pointer()[0];
|
|
||||||
const middleOffset = middleOfButton - xAxisOfButtonClick;
|
|
||||||
|
|
||||||
const clickPos = event.get_root_coords();
|
|
||||||
const adjustedXCoord = clickPos[1] + middleOffset;
|
|
||||||
const coords = [adjustedXCoord, clickPos[2]];
|
|
||||||
|
|
||||||
globalMousePos.value = coords;
|
|
||||||
|
|
||||||
closeAllMenus();
|
|
||||||
App.toggleWindow(window);
|
|
||||||
};
|
|
||||||
51
modules/bar/utils.ts
Normal file
51
modules/bar/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
|
||||||
|
import { globalMousePos } from 'globals';
|
||||||
|
|
||||||
|
export const closeAllMenus = () => {
|
||||||
|
const menuWindows = App.windows
|
||||||
|
.filter((w) => {
|
||||||
|
if (w.name) {
|
||||||
|
return /.*menu/.test(w.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((w) => w.name);
|
||||||
|
|
||||||
|
menuWindows.forEach((w) => {
|
||||||
|
if (w) {
|
||||||
|
App.closeWindow(w);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openMenu = (clicked: any, event: Gdk.Event, window: string) => {
|
||||||
|
/*
|
||||||
|
* NOTE: We have to make some adjustments so the menu pops up relatively
|
||||||
|
* to the center of the button clicked. We don't want the menu to spawn
|
||||||
|
* offcenter dependending on which edge of the button you click on.
|
||||||
|
* -------------
|
||||||
|
* To fix this, we take the x coordinate of the click within the button's bounds.
|
||||||
|
* If you click the left edge of a 100 width button, then the x axis will be 0
|
||||||
|
* and if you click the right edge then the x axis will be 100.
|
||||||
|
* -------------
|
||||||
|
* Then we divide the width of the button by 2 to get the center of the button and then get
|
||||||
|
* the offset by subtracting the clicked x coordinate. Then we can apply that offset
|
||||||
|
* to the x coordinate of the click relative to the screen to get the center of the
|
||||||
|
* icon click.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const middleOfButton = Math.floor(clicked.get_allocated_width() / 2);
|
||||||
|
const xAxisOfButtonClick = clicked.get_pointer()[0];
|
||||||
|
const middleOffset = middleOfButton - xAxisOfButtonClick;
|
||||||
|
|
||||||
|
const clickPos = event.get_root_coords();
|
||||||
|
const adjustedXCoord = clickPos[1] + middleOffset;
|
||||||
|
const coords = [adjustedXCoord, clickPos[2]];
|
||||||
|
|
||||||
|
globalMousePos.value = coords;
|
||||||
|
|
||||||
|
closeAllMenus();
|
||||||
|
App.toggleWindow(window);
|
||||||
|
};
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
const audio = await Service.import("audio");
|
|
||||||
import { openMenu } from "../utils.js";
|
|
||||||
import options from "options";
|
|
||||||
|
|
||||||
import { globalMousePos } from "globals.js";
|
|
||||||
|
|
||||||
const Volume = () => {
|
|
||||||
const icons = {
|
|
||||||
101: "",
|
|
||||||
66: "",
|
|
||||||
34: "",
|
|
||||||
1: "",
|
|
||||||
0: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIcon = () => {
|
|
||||||
const icon = Utils.merge(
|
|
||||||
[audio.speaker.bind("is_muted"), audio.speaker.bind("volume")],
|
|
||||||
(isMuted, vol) => {
|
|
||||||
return isMuted
|
|
||||||
? 0
|
|
||||||
: [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return icon.as((i) => icons[i]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const volIcn = Widget.Label({
|
|
||||||
vpack: "center",
|
|
||||||
label: getIcon(),
|
|
||||||
class_name: "bar-volume_icon",
|
|
||||||
});
|
|
||||||
|
|
||||||
const volPct = Widget.Label({
|
|
||||||
vpack: "center",
|
|
||||||
label: audio.speaker.bind("volume").as((v) => ` ${Math.floor(v * 100)}%`),
|
|
||||||
class_name: "bar-volume_percentage",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: Widget.Box({
|
|
||||||
vpack: "center",
|
|
||||||
class_name: "volume",
|
|
||||||
children: options.bar.volume.label.bind("value").as((showLabel) => {
|
|
||||||
if (showLabel) {
|
|
||||||
return [volIcn, volPct];
|
|
||||||
}
|
|
||||||
return [volIcn];
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
isVisible: true,
|
|
||||||
boxClass: "volume",
|
|
||||||
props: {
|
|
||||||
on_primary_click: (clicked, event) => {
|
|
||||||
openMenu(clicked, event, "audiomenu");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Volume };
|
|
||||||
63
modules/bar/volume/index.ts
Normal file
63
modules/bar/volume/index.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import Gdk from 'gi://Gdk?version=3.0';
|
||||||
|
const audio = await Service.import("audio");
|
||||||
|
import { openMenu } from "../utils.js";
|
||||||
|
import options from "options";
|
||||||
|
|
||||||
|
import { globalMousePos } from "globals.js";
|
||||||
|
|
||||||
|
const Volume = () => {
|
||||||
|
const icons = {
|
||||||
|
101: "",
|
||||||
|
66: "",
|
||||||
|
34: "",
|
||||||
|
1: "",
|
||||||
|
0: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
const icon = Utils.merge(
|
||||||
|
[audio.speaker.bind("is_muted"), audio.speaker.bind("volume")],
|
||||||
|
(isMuted, vol) => {
|
||||||
|
return isMuted
|
||||||
|
? 0
|
||||||
|
: [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return icon.as((i) => i !== undefined ? icons[i] : 101);
|
||||||
|
};
|
||||||
|
|
||||||
|
const volIcn = Widget.Label({
|
||||||
|
vpack: "center",
|
||||||
|
label: getIcon(),
|
||||||
|
class_name: "bar-volume_icon",
|
||||||
|
});
|
||||||
|
|
||||||
|
const volPct = Widget.Label({
|
||||||
|
vpack: "center",
|
||||||
|
label: audio.speaker.bind("volume").as((v) => ` ${Math.floor(v * 100)}%`),
|
||||||
|
class_name: "bar-volume_percentage",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: Widget.Box({
|
||||||
|
vpack: "center",
|
||||||
|
class_name: "volume",
|
||||||
|
children: options.bar.volume.label.bind("value").as((showLabel) => {
|
||||||
|
if (showLabel) {
|
||||||
|
return [volIcn, volPct];
|
||||||
|
}
|
||||||
|
return [volIcn];
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
isVisible: true,
|
||||||
|
boxClass: "volume",
|
||||||
|
props: {
|
||||||
|
on_primary_click: (clicked: any, event: Gdk.Event) => {
|
||||||
|
openMenu(clicked, event, "audiomenu");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Volume };
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
const hyprland = await Service.import("hyprland");
|
|
||||||
|
|
||||||
const filterTitle = (windowtitle) => {
|
|
||||||
const windowTitleMap = [
|
|
||||||
["kitty", " Kitty Terminal"],
|
|
||||||
["firefox", " Firefox"],
|
|
||||||
["microsoft-edge", " Edge"],
|
|
||||||
["discord", " Discord"],
|
|
||||||
["org.kde.dolphin", " Dolphin"],
|
|
||||||
["plex", " Plex"],
|
|
||||||
["steam", " Steam"],
|
|
||||||
["spotify", " Spotify"],
|
|
||||||
["obsidian", " Obsidian"],
|
|
||||||
["^$", " Desktop"],
|
|
||||||
["(.+)", ` ${windowtitle.class.charAt(0).toUpperCase() + windowtitle.class.slice(1)}`],
|
|
||||||
];
|
|
||||||
|
|
||||||
const foundMatch = windowTitleMap.find((wt) =>
|
|
||||||
RegExp(wt[0]).test(windowtitle.class.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
return foundMatch ? foundMatch[1] : windowtitle.class;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClientTitle = () => {
|
|
||||||
return {
|
|
||||||
component: Widget.Label({
|
|
||||||
class_name: "window_title",
|
|
||||||
label: hyprland.active.bind("client").as((v) => filterTitle(v)),
|
|
||||||
}),
|
|
||||||
isVisible: true,
|
|
||||||
boxClass: "windowtitle",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ClientTitle };
|
|
||||||
37
modules/bar/window_title/index.ts
Normal file
37
modules/bar/window_title/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const hyprland = await Service.import("hyprland");
|
||||||
|
import { ActiveClient } from 'types/service/hyprland'
|
||||||
|
|
||||||
|
const filterTitle = (windowtitle: ActiveClient) => {
|
||||||
|
const windowTitleMap = [
|
||||||
|
["kitty", " Kitty Terminal"],
|
||||||
|
["firefox", " Firefox"],
|
||||||
|
["microsoft-edge", " Edge"],
|
||||||
|
["discord", " Discord"],
|
||||||
|
["org.kde.dolphin", " Dolphin"],
|
||||||
|
["plex", " Plex"],
|
||||||
|
["steam", " Steam"],
|
||||||
|
["spotify", " Spotify"],
|
||||||
|
["obsidian", " Obsidian"],
|
||||||
|
["^$", " Desktop"],
|
||||||
|
["(.+)", ` ${windowtitle.class.charAt(0).toUpperCase() + windowtitle.class.slice(1)}`],
|
||||||
|
];
|
||||||
|
|
||||||
|
const foundMatch = windowTitleMap.find((wt) =>
|
||||||
|
RegExp(wt[0]).test(windowtitle.class.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return foundMatch ? foundMatch[1] : windowtitle.class;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientTitle = () => {
|
||||||
|
return {
|
||||||
|
component: Widget.Label({
|
||||||
|
class_name: "window_title",
|
||||||
|
label: hyprland.active.bind("client").as((v) => filterTitle(v)),
|
||||||
|
}),
|
||||||
|
isVisible: true,
|
||||||
|
boxClass: "windowtitle",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ClientTitle };
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
const hyprland = await Service.import("hyprland");
|
const hyprland = await Service.import("hyprland");
|
||||||
|
import { WorkspaceRule, WorkspaceMap } from "lib/types/workspace";
|
||||||
import options from "options";
|
import options from "options";
|
||||||
|
|
||||||
const { workspaces, monitorSpecific } = options.bar.workspaces;
|
const { workspaces, monitorSpecific } = options.bar.workspaces;
|
||||||
|
|
||||||
function range(length, start = 1) {
|
function range(length: number, start = 1) {
|
||||||
return Array.from({ length }, (_, i) => i + start);
|
return Array.from({ length }, (_, i) => i + start);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Workspaces = (monitor = -1, ws = 8) => {
|
const Workspaces = (monitor = -1, ws = 8) => {
|
||||||
const getWorkspacesForMonitor = (curWs, wsRules) => {
|
const getWorkspacesForMonitor = (curWs: number, wsRules: WorkspaceMap) => {
|
||||||
if (!wsRules || !Object.keys(wsRules).length) {
|
if (!wsRules || !Object.keys(wsRules).length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -20,13 +21,13 @@ const Workspaces = (monitor = -1, ws = 8) => {
|
|||||||
return wsRules[currentMonitorName].includes(curWs);
|
return wsRules[currentMonitorName].includes(curWs);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWorkspaceRules = () => {
|
const getWorkspaceRules = (): WorkspaceMap => {
|
||||||
try {
|
try {
|
||||||
const rules = Utils.exec("hyprctl workspacerules -j");
|
const rules = Utils.exec("hyprctl workspacerules -j");
|
||||||
|
|
||||||
const workspaceRules = {};
|
const workspaceRules = {};
|
||||||
|
|
||||||
JSON.parse(rules).forEach((rule, index) => {
|
JSON.parse(rules).forEach((rule: WorkspaceRule, index: number) => {
|
||||||
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
|
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
|
||||||
workspaceRules[rule.monitor].push(index + 1);
|
workspaceRules[rule.monitor].push(index + 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -37,6 +38,7 @@ const Workspaces = (monitor = -1, ws = 8) => {
|
|||||||
return workspaceRules;
|
return workspaceRules;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,6 +143,7 @@ export default {
|
|||||||
light: "light-mode-symbolic",
|
light: "light-mode-symbolic",
|
||||||
},
|
},
|
||||||
weather: {
|
weather: {
|
||||||
|
warning: "dialog-warning-symbolic",
|
||||||
sunny: "weather-clear-symbolic",
|
sunny: "weather-clear-symbolic",
|
||||||
clear: "weather-clear-night-symbolic",
|
clear: "weather-clear-night-symbolic",
|
||||||
partly_cloudy: "weather-few-clouds-symbolic",
|
partly_cloudy: "weather-few-clouds-symbolic",
|
||||||
@@ -1,64 +1,65 @@
|
|||||||
const hyprland = await Service.import("hyprland");
|
const hyprland = await Service.import("hyprland");
|
||||||
|
import { globalMousePos } from "globals";
|
||||||
|
|
||||||
export const Padding = (name) =>
|
export const Padding = (name) =>
|
||||||
Widget.EventBox({
|
Widget.EventBox({
|
||||||
hexpand: true,
|
hexpand: true,
|
||||||
vexpand: true,
|
vexpand: true,
|
||||||
can_focus: true,
|
can_focus: true,
|
||||||
child: Widget.Box(),
|
child: Widget.Box(),
|
||||||
setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)),
|
setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const moveBoxToCursor = (self, fixed) => {
|
const moveBoxToCursor = (self, fixed) => {
|
||||||
if (fixed) {
|
if (fixed) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
globalMousePos.connect("changed", ({ value }) => {
|
|
||||||
const currentWidth = self.child.get_allocation().width;
|
|
||||||
|
|
||||||
let monWidth = hyprland.monitors[hyprland.active.monitor.id].width;
|
|
||||||
let monHeight = hyprland.monitors[hyprland.active.monitor.id].height;
|
|
||||||
|
|
||||||
// If GDK Scaling is applied, then get divide width by scaling
|
|
||||||
// to get the proper coordinates.
|
|
||||||
// Ex: On a 2860px wide monitor... if scaling is set to 2, then the right
|
|
||||||
// end of the monitor is the 1430th pixel.
|
|
||||||
const gdkScale = Utils.exec('bash -c "echo $GDK_SCALE"');
|
|
||||||
|
|
||||||
if (/^\d+(.\d+)?$/.test(gdkScale)) {
|
|
||||||
const scale = parseFloat(gdkScale);
|
|
||||||
monWidth = monWidth / scale;
|
|
||||||
monHeight = monHeight / scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If monitor is vertical (transform = 1 || 3) swap height and width
|
globalMousePos.connect("changed", ({ value }) => {
|
||||||
if (hyprland.monitors[hyprland.active.monitor.id].transform % 2 !== 0) {
|
const currentWidth = self.child.get_allocation().width;
|
||||||
[monWidth, monHeight] = [monHeight, monWidth];
|
|
||||||
}
|
|
||||||
|
|
||||||
let marginRight = monWidth - currentWidth / 2;
|
let monWidth = hyprland.monitors[hyprland.active.monitor.id].width;
|
||||||
marginRight = fixed ? marginRight - monWidth / 2 : marginRight - value[0];
|
let monHeight = hyprland.monitors[hyprland.active.monitor.id].height;
|
||||||
let marginLeft = monWidth - currentWidth - marginRight;
|
|
||||||
|
|
||||||
const minimumMargin = 0;
|
// If GDK Scaling is applied, then get divide width by scaling
|
||||||
|
// to get the proper coordinates.
|
||||||
|
// Ex: On a 2860px wide monitor... if scaling is set to 2, then the right
|
||||||
|
// end of the monitor is the 1430th pixel.
|
||||||
|
const gdkScale = Utils.exec('bash -c "echo $GDK_SCALE"');
|
||||||
|
|
||||||
if (marginRight < minimumMargin) {
|
if (/^\d+(.\d+)?$/.test(gdkScale)) {
|
||||||
marginRight = minimumMargin;
|
const scale = parseFloat(gdkScale);
|
||||||
marginLeft = monWidth - currentWidth - minimumMargin;
|
monWidth = monWidth / scale;
|
||||||
}
|
monHeight = monHeight / scale;
|
||||||
|
}
|
||||||
|
|
||||||
if (marginLeft < minimumMargin) {
|
// If monitor is vertical (transform = 1 || 3) swap height and width
|
||||||
marginLeft = minimumMargin;
|
if (hyprland.monitors[hyprland.active.monitor.id].transform % 2 !== 0) {
|
||||||
marginRight = monWidth - currentWidth - minimumMargin;
|
[monWidth, monHeight] = [monHeight, monWidth];
|
||||||
}
|
}
|
||||||
|
|
||||||
const marginTop = 45;
|
let marginRight = monWidth - currentWidth / 2;
|
||||||
const marginBottom = monHeight - marginTop;
|
marginRight = fixed ? marginRight - monWidth / 2 : marginRight - value[0];
|
||||||
self.set_margin_left(marginLeft);
|
let marginLeft = monWidth - currentWidth - marginRight;
|
||||||
self.set_margin_right(marginRight);
|
|
||||||
self.set_margin_bottom(marginBottom);
|
const minimumMargin = 0;
|
||||||
});
|
|
||||||
|
if (marginRight < minimumMargin) {
|
||||||
|
marginRight = minimumMargin;
|
||||||
|
marginLeft = monWidth - currentWidth - minimumMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marginLeft < minimumMargin) {
|
||||||
|
marginLeft = minimumMargin;
|
||||||
|
marginRight = monWidth - currentWidth - minimumMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marginTop = 45;
|
||||||
|
const marginBottom = monHeight - marginTop;
|
||||||
|
self.set_margin_left(marginLeft);
|
||||||
|
self.set_margin_right(marginRight);
|
||||||
|
self.set_margin_bottom(marginBottom);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: We make the window visible for 2 seconds (on startup) so the child
|
// NOTE: We make the window visible for 2 seconds (on startup) so the child
|
||||||
@@ -68,78 +69,78 @@ const moveBoxToCursor = (self, fixed) => {
|
|||||||
const initRender = Variable(true);
|
const initRender = Variable(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initRender.value = false;
|
initRender.value = false;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
name,
|
|
||||||
child,
|
|
||||||
layout = "center",
|
|
||||||
transition,
|
|
||||||
exclusivity = "ignore",
|
|
||||||
fixed = false,
|
|
||||||
...props
|
|
||||||
}) =>
|
|
||||||
Widget.Window({
|
|
||||||
name,
|
name,
|
||||||
class_names: [name, "dropdown-menu"],
|
child,
|
||||||
setup: (w) => w.keybind("Escape", () => App.closeWindow(name)),
|
layout = "center",
|
||||||
visible: initRender.bind("value"),
|
transition,
|
||||||
keymode: "on-demand",
|
exclusivity = "ignore",
|
||||||
exclusivity,
|
fixed = false,
|
||||||
layer: "top",
|
...props
|
||||||
anchor: ["top", "left"],
|
}) =>
|
||||||
child: Widget.EventBox({
|
Widget.Window({
|
||||||
class_name: "parent-event",
|
name,
|
||||||
on_primary_click: () => App.closeWindow(name),
|
class_names: [name, "dropdown-menu"],
|
||||||
on_secondary_click: () => App.closeWindow(name),
|
setup: (w) => w.keybind("Escape", () => App.closeWindow(name)),
|
||||||
child: Widget.Box({
|
visible: initRender.bind("value"),
|
||||||
class_name: "top-eb",
|
keymode: "on-demand",
|
||||||
vertical: true,
|
exclusivity,
|
||||||
children: [
|
layer: "top",
|
||||||
Widget.EventBox({
|
anchor: ["top", "left"],
|
||||||
class_name: "mid-eb event-top-padding",
|
child: Widget.EventBox({
|
||||||
hexpand: true,
|
class_name: "parent-event",
|
||||||
vexpand: false,
|
on_primary_click: () => App.closeWindow(name),
|
||||||
can_focus: false,
|
on_secondary_click: () => App.closeWindow(name),
|
||||||
child: Widget.Box(),
|
|
||||||
setup: (w) => {
|
|
||||||
w.on("button-press-event", () => App.toggleWindow(name));
|
|
||||||
w.set_margin_top(1);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Widget.EventBox({
|
|
||||||
class_name: "in-eb menu-event-box",
|
|
||||||
on_primary_click: () => {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
on_secondary_click: () => {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
setup: (self) => {
|
|
||||||
moveBoxToCursor(self, fixed);
|
|
||||||
},
|
|
||||||
child: Widget.Box({
|
child: Widget.Box({
|
||||||
class_name: "dropdown-menu-container",
|
class_name: "top-eb",
|
||||||
css: "padding: 1px; margin: -1px;",
|
vertical: true,
|
||||||
child: Widget.Revealer({
|
children: [
|
||||||
revealChild: false,
|
Widget.EventBox({
|
||||||
setup: (self) =>
|
class_name: "mid-eb event-top-padding",
|
||||||
self.hook(App, (_, wname, visible) => {
|
hexpand: true,
|
||||||
if (wname === name) self.reveal_child = visible;
|
vexpand: false,
|
||||||
}),
|
can_focus: false,
|
||||||
transition: "crossfade",
|
child: Widget.Box(),
|
||||||
transitionDuration: 350,
|
setup: (w) => {
|
||||||
child: Widget.Box({
|
w.on("button-press-event", () => App.toggleWindow(name));
|
||||||
class_name: "dropdown-menu-container",
|
w.set_margin_top(1);
|
||||||
can_focus: true,
|
},
|
||||||
children: [child],
|
}),
|
||||||
}),
|
Widget.EventBox({
|
||||||
}),
|
class_name: "in-eb menu-event-box",
|
||||||
|
on_primary_click: () => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
on_secondary_click: () => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
setup: (self) => {
|
||||||
|
moveBoxToCursor(self, fixed);
|
||||||
|
},
|
||||||
|
child: Widget.Box({
|
||||||
|
class_name: "dropdown-menu-container",
|
||||||
|
css: "padding: 1px; margin: -1px;",
|
||||||
|
child: Widget.Revealer({
|
||||||
|
revealChild: false,
|
||||||
|
setup: (self) =>
|
||||||
|
self.hook(App, (_, wname, visible) => {
|
||||||
|
if (wname === name) self.reveal_child = visible;
|
||||||
|
}),
|
||||||
|
transition: "crossfade",
|
||||||
|
transitionDuration: 350,
|
||||||
|
child: Widget.Box({
|
||||||
|
class_name: "dropdown-menu-container",
|
||||||
|
can_focus: true,
|
||||||
|
children: [child],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
...props,
|
||||||
}),
|
});
|
||||||
}),
|
|
||||||
...props,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
const audio = await Service.import("audio");
|
|
||||||
|
|
||||||
const renderInputDevices = (inputDevices) => {
|
|
||||||
if (!inputDevices.length) {
|
|
||||||
return [
|
|
||||||
Widget.Box({
|
|
||||||
class_name: `menu-unfound-button input`,
|
|
||||||
child: Widget.Box({
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "menu-button-name input",
|
|
||||||
label: "No input devices found...",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return inputDevices.map((device) => {
|
|
||||||
return Widget.Button({
|
|
||||||
on_primary_click: () => (audio.microphone = device),
|
|
||||||
class_name: `menu-button audio input ${device}`,
|
|
||||||
child: Widget.Box({
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
hpack: "start",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: audio.microphone
|
|
||||||
.bind("description")
|
|
||||||
.as((v) =>
|
|
||||||
device.description === v
|
|
||||||
? "menu-button-icon active input"
|
|
||||||
: "menu-button-icon input",
|
|
||||||
),
|
|
||||||
label: "",
|
|
||||||
}),
|
|
||||||
Widget.Label({
|
|
||||||
truncate: "end",
|
|
||||||
wrap: true,
|
|
||||||
class_name: audio.microphone
|
|
||||||
.bind("description")
|
|
||||||
.as((v) =>
|
|
||||||
device.description === v
|
|
||||||
? "menu-button-name active input"
|
|
||||||
: "menu-button-name input",
|
|
||||||
),
|
|
||||||
label: device.description,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { renderInputDevices };
|
|
||||||
65
modules/menus/audio/available/InputDevices.ts
Normal file
65
modules/menus/audio/available/InputDevices.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const audio = await Service.import("audio");
|
||||||
|
import { Stream } from "types/service/audio";
|
||||||
|
|
||||||
|
const renderInputDevices = (inputDevices: Stream[]) => {
|
||||||
|
if (inputDevices.length === 0) {
|
||||||
|
return [
|
||||||
|
Widget.Button({
|
||||||
|
class_name: `menu-unfound-button input`,
|
||||||
|
child: Widget.Box({
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
hpack: "start",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "menu-button-name input",
|
||||||
|
label: "No input devices found...",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return inputDevices.map((device) => {
|
||||||
|
return Widget.Button({
|
||||||
|
on_primary_click: () => (audio.microphone = device),
|
||||||
|
class_name: `menu-button audio input ${device}`,
|
||||||
|
child: Widget.Box({
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
hpack: "start",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
wrap: true,
|
||||||
|
class_name: audio.microphone
|
||||||
|
.bind("description")
|
||||||
|
.as((v) =>
|
||||||
|
device.description === v
|
||||||
|
? "menu-button-icon active input"
|
||||||
|
: "menu-button-icon input",
|
||||||
|
),
|
||||||
|
label: "",
|
||||||
|
}),
|
||||||
|
Widget.Label({
|
||||||
|
truncate: "end",
|
||||||
|
wrap: true,
|
||||||
|
class_name: audio.microphone
|
||||||
|
.bind("description")
|
||||||
|
.as((v) =>
|
||||||
|
device.description === v
|
||||||
|
? "menu-button-name active input"
|
||||||
|
: "menu-button-name input",
|
||||||
|
),
|
||||||
|
label: device.description,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { renderInputDevices };
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
const audio = await Service.import("audio");
|
|
||||||
|
|
||||||
const renderPlaybacks = (playbackDevices) => {
|
|
||||||
return playbackDevices.map((device) => {
|
|
||||||
if (device.description === "Dummy Output") {
|
|
||||||
return Widget.Box({
|
|
||||||
class_name: "menu-unfound-button playback",
|
|
||||||
child: Widget.Box({
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "menu-button-name playback",
|
|
||||||
label: "No playback devices found...",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Widget.Button({
|
|
||||||
class_name: `menu-button audio playback ${device}`,
|
|
||||||
on_primary_click: () => (audio.speaker = device),
|
|
||||||
child: Widget.Box({
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
hpack: "start",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
truncate: "end",
|
|
||||||
wrap: true,
|
|
||||||
class_name: audio.speaker
|
|
||||||
.bind("description")
|
|
||||||
.as((v) =>
|
|
||||||
device.description === v
|
|
||||||
? "menu-button-icon active playback"
|
|
||||||
: "menu-button-icon playback",
|
|
||||||
),
|
|
||||||
label: "",
|
|
||||||
}),
|
|
||||||
Widget.Label({
|
|
||||||
class_name: audio.speaker
|
|
||||||
.bind("description")
|
|
||||||
.as((v) =>
|
|
||||||
device.description === v
|
|
||||||
? "menu-button-name active playback"
|
|
||||||
: "menu-button-name playback",
|
|
||||||
),
|
|
||||||
truncate: "end",
|
|
||||||
wrap: true,
|
|
||||||
label: device.description,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { renderPlaybacks };
|
|
||||||
59
modules/menus/audio/available/PlaybackDevices.ts
Normal file
59
modules/menus/audio/available/PlaybackDevices.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const audio = await Service.import("audio");
|
||||||
|
import { Stream } from "types/service/audio";
|
||||||
|
|
||||||
|
const renderPlaybacks = (playbackDevices: Stream[]) => {
|
||||||
|
return playbackDevices.map((device) => {
|
||||||
|
if (device.description === "Dummy Output") {
|
||||||
|
return Widget.Box({
|
||||||
|
class_name: "menu-unfound-button playback",
|
||||||
|
child: Widget.Box({
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "menu-button-name playback",
|
||||||
|
label: "No playback devices found...",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Widget.Button({
|
||||||
|
class_name: `menu-button audio playback ${device}`,
|
||||||
|
on_primary_click: () => (audio.speaker = device),
|
||||||
|
child: Widget.Box({
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
hpack: "start",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
truncate: "end",
|
||||||
|
wrap: true,
|
||||||
|
class_name: audio.speaker
|
||||||
|
.bind("description")
|
||||||
|
.as((v) =>
|
||||||
|
device.description === v
|
||||||
|
? "menu-button-icon active playback"
|
||||||
|
: "menu-button-icon playback",
|
||||||
|
),
|
||||||
|
label: "",
|
||||||
|
}),
|
||||||
|
Widget.Label({
|
||||||
|
truncate: "end",
|
||||||
|
wrap: true,
|
||||||
|
class_name: audio.speaker
|
||||||
|
.bind("description")
|
||||||
|
.as((v) =>
|
||||||
|
device.description === v
|
||||||
|
? "menu-button-name active playback"
|
||||||
|
: "menu-button-name playback",
|
||||||
|
),
|
||||||
|
label: device.description,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { renderPlaybacks };
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
const audio = await Service.import("audio");
|
|
||||||
import { renderInputDevices } from "./InputDevices.js";
|
|
||||||
import { renderPlaybacks } from "./PlaybackDevices.js";
|
|
||||||
|
|
||||||
const availableDevices = () => {
|
|
||||||
return Widget.Box({
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-section-container playback",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-label-container playback",
|
|
||||||
hpack: "fill",
|
|
||||||
child: Widget.Label({
|
|
||||||
class_name: "menu-label audio playback",
|
|
||||||
hexpand: true,
|
|
||||||
hpack: "start",
|
|
||||||
label: "Playback Devices",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-items-section playback",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-container playback",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
vertical: true,
|
|
||||||
children: audio
|
|
||||||
.bind("speakers")
|
|
||||||
.as((v) => renderPlaybacks(v)),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-label-container input",
|
|
||||||
hpack: "fill",
|
|
||||||
child: Widget.Label({
|
|
||||||
class_name: "menu-label audio input",
|
|
||||||
hexpand: true,
|
|
||||||
hpack: "start",
|
|
||||||
label: "Input Devices",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-items-section input",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-container input",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
vertical: true,
|
|
||||||
children: audio
|
|
||||||
.bind("microphones")
|
|
||||||
.as((v) => renderInputDevices(v)),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { availableDevices };
|
|
||||||
75
modules/menus/audio/available/index.ts
Normal file
75
modules/menus/audio/available/index.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const audio = await Service.import("audio");
|
||||||
|
import { renderInputDevices } from "./InputDevices.js";
|
||||||
|
import { renderPlaybacks } from "./PlaybackDevices.js";
|
||||||
|
|
||||||
|
const availableDevices = () => {
|
||||||
|
return Widget.Box({
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-section-container playback",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-label-container playback",
|
||||||
|
hpack: "fill",
|
||||||
|
child: Widget.Label({
|
||||||
|
class_name: "menu-label audio playback",
|
||||||
|
hexpand: true,
|
||||||
|
hpack: "start",
|
||||||
|
label: "Playback Devices",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-items-section playback",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-container playback",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
vertical: true,
|
||||||
|
children: audio
|
||||||
|
.bind("speakers")
|
||||||
|
.as((v) => renderPlaybacks(v)),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-label-container input",
|
||||||
|
hpack: "fill",
|
||||||
|
child: Widget.Label({
|
||||||
|
class_name: "menu-label audio input",
|
||||||
|
hexpand: true,
|
||||||
|
hpack: "start",
|
||||||
|
label: "Input Devices",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-items-section input",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-container input",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
vertical: true,
|
||||||
|
children: audio
|
||||||
|
.bind("microphones")
|
||||||
|
.as((v) => renderInputDevices(v)),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { availableDevices };
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
const connectedControls = (dev, connectedDevices) => {
|
|
||||||
if (!connectedDevices.includes(dev.address)) {
|
|
||||||
return Widget.Box({});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Widget.Box({
|
|
||||||
vpack: "start",
|
|
||||||
class_name: "bluetooth-controls",
|
|
||||||
children: [
|
|
||||||
Widget.Button({
|
|
||||||
class_name: "menu-icon-button unpair bluetooth",
|
|
||||||
child: Widget.Label({
|
|
||||||
tooltip_text: dev.paired ? "Unpair" : "Pair",
|
|
||||||
class_name: "menu-icon-button-label unpair bluetooth",
|
|
||||||
label: dev.paired ? "" : "",
|
|
||||||
}),
|
|
||||||
on_primary_click: () =>
|
|
||||||
Utils.execAsync([
|
|
||||||
"bash",
|
|
||||||
"-c",
|
|
||||||
`bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`,
|
|
||||||
]).catch((err) =>
|
|
||||||
console.error(
|
|
||||||
`bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`,
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
Widget.Button({
|
|
||||||
class_name: "menu-icon-button disconnect bluetooth",
|
|
||||||
child: Widget.Label({
|
|
||||||
tooltip_text: dev.connected ? "Disconnect" : "Connect",
|
|
||||||
class_name: "menu-icon-button-label disconnect bluetooth",
|
|
||||||
label: dev.connected ? "" : "",
|
|
||||||
}),
|
|
||||||
on_primary_click: () => dev.setConnection(!dev.connected),
|
|
||||||
}),
|
|
||||||
Widget.Button({
|
|
||||||
class_name: "menu-icon-button untrust bluetooth",
|
|
||||||
child: Widget.Label({
|
|
||||||
tooltip_text: dev.trusted ? "Untrust" : "Trust",
|
|
||||||
class_name: "menu-icon-button-label untrust bluetooth",
|
|
||||||
label: dev.trusted ? "" : "",
|
|
||||||
}),
|
|
||||||
on_primary_click: () =>
|
|
||||||
Utils.execAsync([
|
|
||||||
"bash",
|
|
||||||
"-c",
|
|
||||||
`bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`,
|
|
||||||
]).catch((err) =>
|
|
||||||
console.error(
|
|
||||||
`bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`,
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
Widget.Button({
|
|
||||||
class_name: "menu-icon-button delete bluetooth",
|
|
||||||
child: Widget.Label({
|
|
||||||
tooltip_text: "Forget",
|
|
||||||
class_name: "menu-icon-button-label delete bluetooth",
|
|
||||||
label: "",
|
|
||||||
}),
|
|
||||||
on_primary_click: () => {
|
|
||||||
Utils.execAsync([
|
|
||||||
"bash",
|
|
||||||
"-c",
|
|
||||||
`bluetoothctl remove ${dev.address}`,
|
|
||||||
]).catch((err) => console.error("Bluetooth Remove", err));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { connectedControls };
|
|
||||||
78
modules/menus/bluetooth/devices/connectedControls.ts
Normal file
78
modules/menus/bluetooth/devices/connectedControls.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { BluetoothDevice } from "types/service/bluetooth";
|
||||||
|
|
||||||
|
const connectedControls = (dev: BluetoothDevice, connectedDevices: BluetoothDevice[]) => {
|
||||||
|
if (!connectedDevices.includes(dev.address)) {
|
||||||
|
return Widget.Box({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Widget.Box({
|
||||||
|
vpack: "start",
|
||||||
|
class_name: "bluetooth-controls",
|
||||||
|
children: [
|
||||||
|
Widget.Button({
|
||||||
|
class_name: "menu-icon-button unpair bluetooth",
|
||||||
|
child: Widget.Label({
|
||||||
|
tooltip_text: dev.paired ? "Unpair" : "Pair",
|
||||||
|
class_name: "menu-icon-button-label unpair bluetooth",
|
||||||
|
label: dev.paired ? "" : "",
|
||||||
|
}),
|
||||||
|
on_primary_click: () =>
|
||||||
|
Utils.execAsync([
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
`bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`,
|
||||||
|
]).catch((err) =>
|
||||||
|
console.error(
|
||||||
|
`bluetoothctl ${dev.paired ? "unpair" : "pair"} ${dev.address}`,
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
Widget.Button({
|
||||||
|
class_name: "menu-icon-button disconnect bluetooth",
|
||||||
|
child: Widget.Label({
|
||||||
|
tooltip_text: dev.connected ? "Disconnect" : "Connect",
|
||||||
|
class_name: "menu-icon-button-label disconnect bluetooth",
|
||||||
|
label: dev.connected ? "" : "",
|
||||||
|
}),
|
||||||
|
on_primary_click: () => dev.setConnection(!dev.connected),
|
||||||
|
}),
|
||||||
|
Widget.Button({
|
||||||
|
class_name: "menu-icon-button untrust bluetooth",
|
||||||
|
child: Widget.Label({
|
||||||
|
tooltip_text: dev.trusted ? "Untrust" : "Trust",
|
||||||
|
class_name: "menu-icon-button-label untrust bluetooth",
|
||||||
|
label: dev.trusted ? "" : "",
|
||||||
|
}),
|
||||||
|
on_primary_click: () =>
|
||||||
|
Utils.execAsync([
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
`bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`,
|
||||||
|
]).catch((err) =>
|
||||||
|
console.error(
|
||||||
|
`bluetoothctl ${dev.trusted ? "untrust" : "trust"} ${dev.address}`,
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
Widget.Button({
|
||||||
|
class_name: "menu-icon-button delete bluetooth",
|
||||||
|
child: Widget.Label({
|
||||||
|
tooltip_text: "Forget",
|
||||||
|
class_name: "menu-icon-button-label delete bluetooth",
|
||||||
|
label: "",
|
||||||
|
}),
|
||||||
|
on_primary_click: () => {
|
||||||
|
Utils.execAsync([
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
`bluetoothctl remove ${dev.address}`,
|
||||||
|
]).catch((err) => console.error("Bluetooth Remove", err));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { connectedControls };
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { connectedControls } from "./connectedControls.js";
|
|
||||||
import { getBluetoothIcon } from "../utils.js";
|
|
||||||
|
|
||||||
const devices = (bluetooth, self) => {
|
|
||||||
return self.hook(bluetooth, () => {
|
|
||||||
if (!bluetooth.enabled) {
|
|
||||||
return (self.child = Widget.Box({
|
|
||||||
class_name: "bluetooth-items",
|
|
||||||
vertical: true,
|
|
||||||
expand: true,
|
|
||||||
vpack: "center",
|
|
||||||
hpack: "center",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "dim",
|
|
||||||
hexpand: true,
|
|
||||||
label: "Bluetooth is disabled",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableDevices = bluetooth.devices
|
|
||||||
.filter(
|
|
||||||
(btDev) => btDev.name !== null,
|
|
||||||
)
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.connected || a.paired) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b.connected || b.paired) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.name - a.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
const conDevNames = availableDevices
|
|
||||||
.filter((d) => d.connected || d.paired)
|
|
||||||
.map((d) => d.address);
|
|
||||||
|
|
||||||
if (!availableDevices.length) {
|
|
||||||
return (self.child = Widget.Box({
|
|
||||||
class_name: "bluetooth-items",
|
|
||||||
vertical: true,
|
|
||||||
expand: true,
|
|
||||||
vpack: "center",
|
|
||||||
hpack: "center",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "no-bluetooth-devices dim",
|
|
||||||
hexpand: true,
|
|
||||||
label: "No devices currently found",
|
|
||||||
}),
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "search-bluetooth-label dim",
|
|
||||||
hexpand: true,
|
|
||||||
label: "Press '' to search",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (self.child = Widget.Box({
|
|
||||||
vertical: true,
|
|
||||||
children: availableDevices.map((device) => {
|
|
||||||
return Widget.Box({
|
|
||||||
children: [
|
|
||||||
Widget.Button({
|
|
||||||
hexpand: true,
|
|
||||||
class_name: `bluetooth-element-item ${device}`,
|
|
||||||
on_primary_click: () => {
|
|
||||||
if (!conDevNames.includes(device.address))
|
|
||||||
device.setConnection(true);
|
|
||||||
},
|
|
||||||
child: Widget.Box({
|
|
||||||
hexpand: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
hexpand: true,
|
|
||||||
hpack: "start",
|
|
||||||
class_name: "menu-button-container",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
vpack: "start",
|
|
||||||
class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? "active" : ""}`,
|
|
||||||
label: getBluetoothIcon(`${device["icon-name"]}-symbolic`),
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
vertical: true,
|
|
||||||
vpack: "center",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
vpack: "center",
|
|
||||||
hpack: "start",
|
|
||||||
class_name: "menu-button-name bluetooth",
|
|
||||||
truncate: "end",
|
|
||||||
wrap: true,
|
|
||||||
label: device.alias,
|
|
||||||
}),
|
|
||||||
Widget.Revealer({
|
|
||||||
hpack: "start",
|
|
||||||
reveal_child: device.connected || device.paired,
|
|
||||||
child: Widget.Label({
|
|
||||||
hpack: "start",
|
|
||||||
class_name: "connection-status dim",
|
|
||||||
label: device.connected ? "Connected" : "Paired",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
hpack: "end",
|
|
||||||
children: device.connecting
|
|
||||||
? [
|
|
||||||
Widget.Spinner({
|
|
||||||
vpack: "start",
|
|
||||||
class_name: "spinner bluetooth",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
connectedControls(device, conDevNames),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { devices };
|
|
||||||
139
modules/menus/bluetooth/devices/devicelist.ts
Normal file
139
modules/menus/bluetooth/devices/devicelist.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { Bluetooth } from "types/service/bluetooth.js";
|
||||||
|
import Box from "types/widgets/box.js";
|
||||||
|
import { connectedControls } from "./connectedControls.js";
|
||||||
|
import { getBluetoothIcon } from "../utils.js";
|
||||||
|
|
||||||
|
const devices = (bluetooth: Bluetooth, self: Box<any, any>) => {
|
||||||
|
return self.hook(bluetooth, () => {
|
||||||
|
if (!bluetooth.enabled) {
|
||||||
|
return (self.child = Widget.Box({
|
||||||
|
class_name: "bluetooth-items",
|
||||||
|
vertical: true,
|
||||||
|
expand: true,
|
||||||
|
vpack: "center",
|
||||||
|
hpack: "center",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "dim",
|
||||||
|
hexpand: true,
|
||||||
|
label: "Bluetooth is disabled",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableDevices = bluetooth.devices
|
||||||
|
.filter(
|
||||||
|
(btDev) => btDev.name !== null,
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.connected || a.paired) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.connected || b.paired) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.name - a.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const conDevNames = availableDevices
|
||||||
|
.filter((d) => d.connected || d.paired)
|
||||||
|
.map((d) => d.address);
|
||||||
|
|
||||||
|
if (!availableDevices.length) {
|
||||||
|
return (self.child = Widget.Box({
|
||||||
|
class_name: "bluetooth-items",
|
||||||
|
vertical: true,
|
||||||
|
expand: true,
|
||||||
|
vpack: "center",
|
||||||
|
hpack: "center",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "no-bluetooth-devices dim",
|
||||||
|
hexpand: true,
|
||||||
|
label: "No devices currently found",
|
||||||
|
}),
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "search-bluetooth-label dim",
|
||||||
|
hexpand: true,
|
||||||
|
label: "Press '' to search",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (self.child = Widget.Box({
|
||||||
|
vertical: true,
|
||||||
|
children: availableDevices.map((device) => {
|
||||||
|
return Widget.Box({
|
||||||
|
children: [
|
||||||
|
Widget.Button({
|
||||||
|
hexpand: true,
|
||||||
|
class_name: `bluetooth-element-item ${device}`,
|
||||||
|
on_primary_click: () => {
|
||||||
|
if (!conDevNames.includes(device.address))
|
||||||
|
device.setConnection(true);
|
||||||
|
},
|
||||||
|
child: Widget.Box({
|
||||||
|
hexpand: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
hexpand: true,
|
||||||
|
hpack: "start",
|
||||||
|
class_name: "menu-button-container",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
vpack: "start",
|
||||||
|
class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? "active" : ""}`,
|
||||||
|
label: getBluetoothIcon(`${device["icon-name"]}-symbolic`),
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
vertical: true,
|
||||||
|
vpack: "center",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
vpack: "center",
|
||||||
|
hpack: "start",
|
||||||
|
class_name: "menu-button-name bluetooth",
|
||||||
|
truncate: "end",
|
||||||
|
wrap: true,
|
||||||
|
label: device.alias,
|
||||||
|
}),
|
||||||
|
Widget.Revealer({
|
||||||
|
hpack: "start",
|
||||||
|
reveal_child: device.connected || device.paired,
|
||||||
|
child: Widget.Label({
|
||||||
|
hpack: "start",
|
||||||
|
class_name: "connection-status dim",
|
||||||
|
label: device.connected ? "Connected" : "Paired",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
hpack: "end",
|
||||||
|
children: device.connecting
|
||||||
|
? [
|
||||||
|
Widget.Spinner({
|
||||||
|
vpack: "start",
|
||||||
|
class_name: "spinner bluetooth",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
connectedControls(device, conDevNames),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { devices };
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const bluetooth = await Service.import("bluetooth");
|
|
||||||
import { label } from "./label.js";
|
|
||||||
import { devices } from "./devicelist.js";
|
|
||||||
|
|
||||||
const Devices = () => {
|
|
||||||
return Widget.Box({
|
|
||||||
class_name: "menu-section-container",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
label(bluetooth),
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "menu-items-section",
|
|
||||||
// hscroll: 'never',
|
|
||||||
// vscroll: 'always',
|
|
||||||
child: Widget.Box({
|
|
||||||
class_name: "menu-content",
|
|
||||||
vertical: true,
|
|
||||||
setup: (self) => {
|
|
||||||
devices(bluetooth, self);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Devices };
|
|
||||||
25
modules/menus/bluetooth/devices/index.ts
Normal file
25
modules/menus/bluetooth/devices/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const bluetooth = await Service.import("bluetooth");
|
||||||
|
import { label } from "./label.js";
|
||||||
|
import { devices } from "./devicelist.js";
|
||||||
|
|
||||||
|
const Devices = () => {
|
||||||
|
return Widget.Box({
|
||||||
|
class_name: "menu-section-container",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
label(bluetooth),
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "menu-items-section",
|
||||||
|
child: Widget.Box({
|
||||||
|
class_name: "menu-content",
|
||||||
|
vertical: true,
|
||||||
|
setup: (self) => {
|
||||||
|
devices(bluetooth, self);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Devices };
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
const label = (bluetooth) => {
|
|
||||||
const searchInProgress = Variable(false);
|
|
||||||
const startRotation = () => {
|
|
||||||
searchInProgress.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
searchInProgress.value = false;
|
|
||||||
}, 10 * 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return Widget.Box({
|
|
||||||
class_name: "menu-label-container",
|
|
||||||
hpack: "fill",
|
|
||||||
vpack: "start",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "menu-label",
|
|
||||||
vpack: "center",
|
|
||||||
hpack: "start",
|
|
||||||
label: "Bluetooth",
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "controls-container",
|
|
||||||
vpack: "start",
|
|
||||||
children: [
|
|
||||||
Widget.Switch({
|
|
||||||
class_name: "menu-switch bluetooth",
|
|
||||||
hexpand: true,
|
|
||||||
hpack: "end",
|
|
||||||
active: bluetooth.bind("enabled"),
|
|
||||||
on_activate: ({ active }) => {
|
|
||||||
searchInProgress.value = false;
|
|
||||||
Utils.execAsync([
|
|
||||||
"bash",
|
|
||||||
"-c",
|
|
||||||
`bluetoothctl power ${active ? "on" : "off"}`,
|
|
||||||
]).catch((err) =>
|
|
||||||
console.error(
|
|
||||||
`bluetoothctl power ${active ? "on" : "off"}`,
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Widget.Separator({
|
|
||||||
class_name: "menu-separator bluetooth",
|
|
||||||
}),
|
|
||||||
Widget.Button({
|
|
||||||
vpack: "center",
|
|
||||||
class_name: "menu-icon-button search",
|
|
||||||
on_primary_click: () => {
|
|
||||||
startRotation();
|
|
||||||
Utils.execAsync([
|
|
||||||
"bash",
|
|
||||||
"-c",
|
|
||||||
"bluetoothctl --timeout 120 scan on",
|
|
||||||
]).catch((err) => {
|
|
||||||
searchInProgress.value = false;
|
|
||||||
console.error("bluetoothctl --timeout 120 scan on", err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Widget.Icon({
|
|
||||||
class_name: searchInProgress
|
|
||||||
.bind("value")
|
|
||||||
.as((v) => (v ? "spinning" : "")),
|
|
||||||
icon: "view-refresh-symbolic",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { label };
|
|
||||||
75
modules/menus/bluetooth/devices/label.ts
Normal file
75
modules/menus/bluetooth/devices/label.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Bluetooth } from "types/service/bluetooth";
|
||||||
|
const label = (bluetooth: Bluetooth) => {
|
||||||
|
const searchInProgress = Variable(false);
|
||||||
|
const startRotation = () => {
|
||||||
|
searchInProgress.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
searchInProgress.value = false;
|
||||||
|
}, 10 * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Widget.Box({
|
||||||
|
class_name: "menu-label-container",
|
||||||
|
hpack: "fill",
|
||||||
|
vpack: "start",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "menu-label",
|
||||||
|
vpack: "center",
|
||||||
|
hpack: "start",
|
||||||
|
label: "Bluetooth",
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "controls-container",
|
||||||
|
vpack: "start",
|
||||||
|
children: [
|
||||||
|
Widget.Switch({
|
||||||
|
class_name: "menu-switch bluetooth",
|
||||||
|
hexpand: true,
|
||||||
|
hpack: "end",
|
||||||
|
active: bluetooth.bind("enabled"),
|
||||||
|
on_activate: ({ active }) => {
|
||||||
|
searchInProgress.value = false;
|
||||||
|
Utils.execAsync([
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
`bluetoothctl power ${active ? "on" : "off"}`,
|
||||||
|
]).catch((err) =>
|
||||||
|
console.error(
|
||||||
|
`bluetoothctl power ${active ? "on" : "off"}`,
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Widget.Separator({
|
||||||
|
class_name: "menu-separator bluetooth",
|
||||||
|
}),
|
||||||
|
Widget.Button({
|
||||||
|
vpack: "center",
|
||||||
|
class_name: "menu-icon-button search",
|
||||||
|
on_primary_click: () => {
|
||||||
|
startRotation();
|
||||||
|
Utils.execAsync([
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
"bluetoothctl --timeout 120 scan on",
|
||||||
|
]).catch((err) => {
|
||||||
|
searchInProgress.value = false;
|
||||||
|
console.error("bluetoothctl --timeout 120 scan on", err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Widget.Icon({
|
||||||
|
class_name: searchInProgress
|
||||||
|
.bind("value")
|
||||||
|
.as((v) => (v ? "spinning" : "")),
|
||||||
|
icon: "view-refresh-symbolic",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { label };
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const getBluetoothIcon = (iconName) => {
|
|
||||||
const deviceIconMap = [
|
|
||||||
["^audio-card*", ""],
|
|
||||||
["^audio-headphones*", ""],
|
|
||||||
["^audio-headset*", ""],
|
|
||||||
["^audio-input*", ""],
|
|
||||||
["^audio-speakers*", ""],
|
|
||||||
["^bluetooth*", ""],
|
|
||||||
["^camera*", ""],
|
|
||||||
["^computer*", ""],
|
|
||||||
["^input-gaming*", ""],
|
|
||||||
["^input-keyboard*", ""],
|
|
||||||
["^input-mouse*", ""],
|
|
||||||
["^input-tablet*", ""],
|
|
||||||
["^media*", ""],
|
|
||||||
["^modem*", ""],
|
|
||||||
["^network*", ""],
|
|
||||||
["^phone*", ""],
|
|
||||||
["^printer*", ""],
|
|
||||||
["^scanner*", ""],
|
|
||||||
["^video-camera*", ""],
|
|
||||||
];
|
|
||||||
|
|
||||||
const foundMatch = deviceIconMap.find((icon) =>
|
|
||||||
RegExp(icon[0]).test(iconName.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
return foundMatch ? foundMatch[1] : "";
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getBluetoothIcon };
|
|
||||||
31
modules/menus/bluetooth/utils.ts
Normal file
31
modules/menus/bluetooth/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const getBluetoothIcon = (iconName: string) => {
|
||||||
|
const deviceIconMap = [
|
||||||
|
["^audio-card*", ""],
|
||||||
|
["^audio-headphones*", ""],
|
||||||
|
["^audio-headset*", ""],
|
||||||
|
["^audio-input*", ""],
|
||||||
|
["^audio-speakers*", ""],
|
||||||
|
["^bluetooth*", ""],
|
||||||
|
["^camera*", ""],
|
||||||
|
["^computer*", ""],
|
||||||
|
["^input-gaming*", ""],
|
||||||
|
["^input-keyboard*", ""],
|
||||||
|
["^input-mouse*", ""],
|
||||||
|
["^input-tablet*", ""],
|
||||||
|
["^media*", ""],
|
||||||
|
["^modem*", ""],
|
||||||
|
["^network*", ""],
|
||||||
|
["^phone*", ""],
|
||||||
|
["^printer*", ""],
|
||||||
|
["^scanner*", ""],
|
||||||
|
["^video-camera*", ""],
|
||||||
|
];
|
||||||
|
|
||||||
|
const foundMatch = deviceIconMap.find((icon) =>
|
||||||
|
RegExp(icon[0]).test(iconName.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return foundMatch ? foundMatch[1] : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getBluetoothIcon };
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { Weather } from "lib/types/weather.js";
|
||||||
|
import { Variable } from "types/variable.js";
|
||||||
import icons from "../../../../../icons/index.js";
|
import icons from "../../../../../icons/index.js";
|
||||||
|
|
||||||
export const HourlyIcon = (theWeather, getNextEpoch) => {
|
export const HourlyIcon = (theWeather: Variable<Weather>, getNextEpoch: any) => {
|
||||||
return Widget.Icon({
|
return Widget.Icon({
|
||||||
class_name: "hourly-weather-icon",
|
class_name: "hourly-weather-icon",
|
||||||
icon: theWeather.bind("value").as((w) => {
|
icon: theWeather.bind("value").as((w) => {
|
||||||
@@ -16,9 +18,11 @@ export const HourlyIcon = (theWeather, getNextEpoch) => {
|
|||||||
let iconQuery = weatherAtEpoch?.condition.text
|
let iconQuery = weatherAtEpoch?.condition.text
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(" ", "_");
|
.replaceAll(" ", "_")
|
||||||
|
|| "warning"
|
||||||
|
;
|
||||||
|
|
||||||
if (!weatherAtEpoch?.isDay && iconQuery === "partly_cloudy") {
|
if (!weatherAtEpoch?.is_day && iconQuery === "partly_cloudy") {
|
||||||
iconQuery = "partly_cloudy_night";
|
iconQuery = "partly_cloudy_night";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { HourlyIcon } from "./icon/index.js";
|
|
||||||
import { HourlyTemp } from "./temperature/index.js";
|
|
||||||
import { HourlyTime } from "./time/index.js";
|
|
||||||
|
|
||||||
export const Hourly = (theWeather) => {
|
|
||||||
return Widget.Box({
|
|
||||||
vertical: false,
|
|
||||||
hexpand: true,
|
|
||||||
hpack: "fill",
|
|
||||||
class_name: "hourly-weather-container",
|
|
||||||
children: [1, 2, 3, 4].map((hoursFromNow) => {
|
|
||||||
const getNextEpoch = (wthr) => {
|
|
||||||
const currentEpoch = wthr.location.localtime_epoch;
|
|
||||||
const epochAtHourStart = currentEpoch - (currentEpoch % 3600);
|
|
||||||
let nextEpoch = 3600 * hoursFromNow + epochAtHourStart;
|
|
||||||
|
|
||||||
const curHour = new Date(currentEpoch * 1000).getHours();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* NOTE: Since the API is only capable of showing the current day; if
|
|
||||||
* the hours left in the day are less than 4 (aka spilling into the next day),
|
|
||||||
* then rewind to contain the prediction within the current day.
|
|
||||||
*/
|
|
||||||
if (curHour > 19) {
|
|
||||||
const hoursToRewind = curHour - 19;
|
|
||||||
nextEpoch =
|
|
||||||
3600 * hoursFromNow + epochAtHourStart - hoursToRewind * 3600;
|
|
||||||
}
|
|
||||||
return nextEpoch;
|
|
||||||
};
|
|
||||||
|
|
||||||
return Widget.Box({
|
|
||||||
class_name: "hourly-weather-item",
|
|
||||||
hexpand: true,
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
HourlyTime(theWeather, getNextEpoch),
|
|
||||||
HourlyIcon(theWeather, getNextEpoch),
|
|
||||||
HourlyTemp(theWeather, getNextEpoch),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
46
modules/menus/calendar/weather/hourly/index.ts
Normal file
46
modules/menus/calendar/weather/hourly/index.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Weather } from "lib/types/weather";
|
||||||
|
import { Variable } from "types/variable";
|
||||||
|
import { HourlyIcon } from "./icon/index.js";
|
||||||
|
import { HourlyTemp } from "./temperature/index.js";
|
||||||
|
import { HourlyTime } from "./time/index.js";
|
||||||
|
|
||||||
|
export const Hourly = (theWeather: Variable<Weather>) => {
|
||||||
|
return Widget.Box({
|
||||||
|
vertical: false,
|
||||||
|
hexpand: true,
|
||||||
|
hpack: "fill",
|
||||||
|
class_name: "hourly-weather-container",
|
||||||
|
children: [1, 2, 3, 4].map((hoursFromNow) => {
|
||||||
|
const getNextEpoch = (wthr: Weather) => {
|
||||||
|
const currentEpoch = wthr.location.localtime_epoch;
|
||||||
|
const epochAtHourStart = currentEpoch - (currentEpoch % 3600);
|
||||||
|
let nextEpoch = 3600 * hoursFromNow + epochAtHourStart;
|
||||||
|
|
||||||
|
const curHour = new Date(currentEpoch * 1000).getHours();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE: Since the API is only capable of showing the current day; if
|
||||||
|
* the hours left in the day are less than 4 (aka spilling into the next day),
|
||||||
|
* then rewind to contain the prediction within the current day.
|
||||||
|
*/
|
||||||
|
if (curHour > 19) {
|
||||||
|
const hoursToRewind = curHour - 19;
|
||||||
|
nextEpoch =
|
||||||
|
3600 * hoursFromNow + epochAtHourStart - hoursToRewind * 3600;
|
||||||
|
}
|
||||||
|
return nextEpoch;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Widget.Box({
|
||||||
|
class_name: "hourly-weather-item",
|
||||||
|
hexpand: true,
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
HourlyTime(theWeather, getNextEpoch),
|
||||||
|
HourlyIcon(theWeather, getNextEpoch),
|
||||||
|
HourlyTemp(theWeather, getNextEpoch),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import options from "options";
|
|
||||||
|
|
||||||
const { unit } = options.menus.clock.weather;
|
|
||||||
|
|
||||||
export const HourlyTemp = (theWeather, getNextEpoch) => {
|
|
||||||
return Widget.Label({
|
|
||||||
class_name: "hourly-weather-temp",
|
|
||||||
label: Utils.merge(
|
|
||||||
[theWeather.bind("value"), unit.bind("value")],
|
|
||||||
(wthr, unt) => {
|
|
||||||
if (!Object.keys(wthr).length) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextEpoch = getNextEpoch(wthr);
|
|
||||||
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find(
|
|
||||||
(h) => h.time_epoch === nextEpoch,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unt === "imperial") {
|
|
||||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : "-"}° F`;
|
|
||||||
}
|
|
||||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : "-"}° C`;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
29
modules/menus/calendar/weather/hourly/temperature/index.ts
Normal file
29
modules/menus/calendar/weather/hourly/temperature/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Weather } from "lib/types/weather";
|
||||||
|
import { Variable } from "types/variable";
|
||||||
|
import options from "options";
|
||||||
|
|
||||||
|
const { unit } = options.menus.clock.weather;
|
||||||
|
|
||||||
|
export const HourlyTemp = (theWeather: Variable<Weather>, getNextEpoch: any) => {
|
||||||
|
return Widget.Label({
|
||||||
|
class_name: "hourly-weather-temp",
|
||||||
|
label: Utils.merge(
|
||||||
|
[theWeather.bind("value"), unit.bind("value")],
|
||||||
|
(wthr, unt) => {
|
||||||
|
if (!Object.keys(wthr).length) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEpoch = getNextEpoch(wthr);
|
||||||
|
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find(
|
||||||
|
(h) => h.time_epoch === nextEpoch,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unt === "imperial") {
|
||||||
|
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : "-"}° F`;
|
||||||
|
}
|
||||||
|
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : "-"}° C`;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
export const HourlyTime = (theWeather, getNextEpoch) => {
|
|
||||||
return Widget.Label({
|
|
||||||
class_name: "hourly-weather-time",
|
|
||||||
label: theWeather.bind("value").as((w) => {
|
|
||||||
if (!Object.keys(w).length) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextEpoch = getNextEpoch(w);
|
|
||||||
const dateAtEpoch = new Date(nextEpoch * 1000);
|
|
||||||
let hours = dateAtEpoch.getHours();
|
|
||||||
const ampm = hours >= 12 ? "PM" : "AM";
|
|
||||||
hours = hours % 12 || 12;
|
|
||||||
|
|
||||||
return `${hours}${ampm}`;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
21
modules/menus/calendar/weather/hourly/time/index.ts
Normal file
21
modules/menus/calendar/weather/hourly/time/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Weather } from "lib/types/weather";
|
||||||
|
import { Variable } from "types/variable";
|
||||||
|
|
||||||
|
export const HourlyTime = (theWeather: Variable<Weather>, getNextEpoch: any) => {
|
||||||
|
return Widget.Label({
|
||||||
|
class_name: "hourly-weather-time",
|
||||||
|
label: theWeather.bind("value").as((w) => {
|
||||||
|
if (!Object.keys(w).length) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEpoch = getNextEpoch(w);
|
||||||
|
const dateAtEpoch = new Date(nextEpoch * 1000);
|
||||||
|
let hours = dateAtEpoch.getHours();
|
||||||
|
const ampm = hours >= 12 ? "PM" : "AM";
|
||||||
|
hours = hours % 12 || 12;
|
||||||
|
|
||||||
|
return `${hours}${ampm}`;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import icons from "../../../../icons/index.js";
|
|
||||||
|
|
||||||
export const TodayIcon = (theWeather) => {
|
|
||||||
return Widget.Box({
|
|
||||||
vpack: "center",
|
|
||||||
hpack: "start",
|
|
||||||
class_name: "calendar-menu-weather today icon container",
|
|
||||||
children: [
|
|
||||||
Widget.Icon({
|
|
||||||
class_name: "calendar-menu-weather today icon",
|
|
||||||
icon: theWeather.bind("value").as((v) => {
|
|
||||||
let iconQuery = v.current.condition.text
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replaceAll(" ", "_");
|
|
||||||
|
|
||||||
if (!v.current.isDay && iconQuery === "partly_cloudy") {
|
|
||||||
iconQuery = "partly_cloudy_night";
|
|
||||||
}
|
|
||||||
return icons.weather[iconQuery];
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
27
modules/menus/calendar/weather/icon/index.ts
Normal file
27
modules/menus/calendar/weather/icon/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Weather } from "lib/types/weather";
|
||||||
|
import { Variable } from "types/variable";
|
||||||
|
import icons from "../../../../icons/index.js";
|
||||||
|
|
||||||
|
export const TodayIcon = (theWeather: Variable<Weather>) => {
|
||||||
|
return Widget.Box({
|
||||||
|
vpack: "center",
|
||||||
|
hpack: "start",
|
||||||
|
class_name: "calendar-menu-weather today icon container",
|
||||||
|
children: [
|
||||||
|
Widget.Icon({
|
||||||
|
class_name: "calendar-menu-weather today icon",
|
||||||
|
icon: theWeather.bind("value").as((v) => {
|
||||||
|
let iconQuery = v.current.condition.text
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(" ", "_");
|
||||||
|
|
||||||
|
if (!v.current.is_day && iconQuery === "partly_cloudy") {
|
||||||
|
iconQuery = "partly_cloudy_night";
|
||||||
|
}
|
||||||
|
return icons.weather[iconQuery];
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import options from "options";
|
|
||||||
import { TodayIcon } from "./icon/index.js";
|
|
||||||
import { TodayStats } from "./stats/index.js";
|
|
||||||
import { TodayTemperature } from "./temperature/index.js";
|
|
||||||
import { Hourly } from "./hourly/index.js";
|
|
||||||
|
|
||||||
const { key, interval } = options.menus.clock.weather;
|
|
||||||
|
|
||||||
const defaultWeather = {
|
|
||||||
location: {
|
|
||||||
localtime_epoch: 1719471600,
|
|
||||||
},
|
|
||||||
current: {
|
|
||||||
temp_f: 0,
|
|
||||||
wind_mph: 0,
|
|
||||||
condition: {
|
|
||||||
text: "Clear",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
forecast: {
|
|
||||||
forecastday: [
|
|
||||||
{
|
|
||||||
day: {
|
|
||||||
daily_chance_of_rain: 0,
|
|
||||||
},
|
|
||||||
hour: [
|
|
||||||
{
|
|
||||||
time_epoch: 1719471600,
|
|
||||||
temp_f: 0,
|
|
||||||
condition: {
|
|
||||||
text: "Clear",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const theWeather = Variable(defaultWeather);
|
|
||||||
|
|
||||||
const WeatherWidget = () => {
|
|
||||||
return Widget.Box({
|
|
||||||
class_name: "calendar-menu-item-container weather",
|
|
||||||
child: Widget.Box({
|
|
||||||
class_name: "weather-container-box",
|
|
||||||
setup: (self) => {
|
|
||||||
Utils.merge(
|
|
||||||
[key.bind("value"), interval.bind("value")],
|
|
||||||
(weatherKey, weatherInterval) => {
|
|
||||||
Utils.interval(weatherInterval, () => {
|
|
||||||
Utils.execAsync(
|
|
||||||
`curl "https://api.weatherapi.com/v1/forecast.json?key=${weatherKey}&q=93722&days=1&aqi=no&alerts=no"`,
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
if (typeof res === "string") {
|
|
||||||
theWeather.value = JSON.parse(res);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(`Failed to fetch weather: ${err}`);
|
|
||||||
theWeather.value = defaultWeather;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (self.child = Widget.Box({
|
|
||||||
vertical: true,
|
|
||||||
hexpand: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "calendar-menu-weather today",
|
|
||||||
hexpand: true,
|
|
||||||
children: [
|
|
||||||
TodayIcon(theWeather),
|
|
||||||
TodayTemperature(theWeather),
|
|
||||||
TodayStats(theWeather),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Widget.Separator({
|
|
||||||
class_name: "menu-separator weather",
|
|
||||||
}),
|
|
||||||
Hourly(theWeather),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { WeatherWidget };
|
|
||||||
63
modules/menus/calendar/weather/index.ts
Normal file
63
modules/menus/calendar/weather/index.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import options from "options";
|
||||||
|
import { TodayIcon } from "./icon/index.js";
|
||||||
|
import { TodayStats } from "./stats/index.js";
|
||||||
|
import { TodayTemperature } from "./temperature/index.js";
|
||||||
|
import { Hourly } from "./hourly/index.js";
|
||||||
|
import { Weather } from "lib/types/weather.js";
|
||||||
|
import { DEFAULT_WEATHER } from "lib/types/defaults/weather.js";
|
||||||
|
|
||||||
|
const { key, interval } = options.menus.clock.weather;
|
||||||
|
|
||||||
|
const theWeather = Variable<Weather>(DEFAULT_WEATHER);
|
||||||
|
|
||||||
|
const WeatherWidget = () => {
|
||||||
|
return Widget.Box({
|
||||||
|
class_name: "calendar-menu-item-container weather",
|
||||||
|
child: Widget.Box({
|
||||||
|
class_name: "weather-container-box",
|
||||||
|
setup: (self) => {
|
||||||
|
Utils.merge(
|
||||||
|
[key.bind("value"), interval.bind("value")],
|
||||||
|
(weatherKey, weatherInterval) => {
|
||||||
|
Utils.interval(weatherInterval, () => {
|
||||||
|
Utils.execAsync(
|
||||||
|
`curl "https://api.weatherapi.com/v1/forecast.json?key=${weatherKey}&q=93722&days=1&aqi=no&alerts=no"`,
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (typeof res === "string") {
|
||||||
|
theWeather.value = JSON.parse(res);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`Failed to fetch weather: ${err}`);
|
||||||
|
theWeather.value = DEFAULT_WEATHER;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (self.child = Widget.Box({
|
||||||
|
vertical: true,
|
||||||
|
hexpand: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "calendar-menu-weather today",
|
||||||
|
hexpand: true,
|
||||||
|
children: [
|
||||||
|
TodayIcon(theWeather),
|
||||||
|
TodayTemperature(theWeather),
|
||||||
|
TodayStats(theWeather),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Widget.Separator({
|
||||||
|
class_name: "menu-separator weather",
|
||||||
|
}),
|
||||||
|
Hourly(theWeather),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WeatherWidget };
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import options from "options";
|
|
||||||
|
|
||||||
const { unit } = options.menus.clock.weather;
|
|
||||||
|
|
||||||
export const TodayStats = (theWeather) => {
|
|
||||||
return Widget.Box({
|
|
||||||
class_name: "calendar-menu-weather today stats container",
|
|
||||||
hpack: "end",
|
|
||||||
vpack: "center",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "weather wind",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "weather wind icon",
|
|
||||||
label: "",
|
|
||||||
}),
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "weather wind label",
|
|
||||||
label: Utils.merge(
|
|
||||||
[theWeather.bind("value"), unit.bind("value")],
|
|
||||||
(wthr, unt) => {
|
|
||||||
if (unt === "imperial") {
|
|
||||||
return `${Math.floor(wthr.current.wind_mph)} mph`;
|
|
||||||
}
|
|
||||||
return `${Math.floor(wthr.current.wind_kph)} kph`;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
class_name: "weather precip",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "weather precip icon",
|
|
||||||
label: "",
|
|
||||||
}),
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "weather precip label",
|
|
||||||
label: theWeather
|
|
||||||
.bind("value")
|
|
||||||
.as(
|
|
||||||
(v) => `${v.forecast.forecastday[0].day.daily_chance_of_rain}%`,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
55
modules/menus/calendar/weather/stats/index.ts
Normal file
55
modules/menus/calendar/weather/stats/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Weather } from "lib/types/weather";
|
||||||
|
import { Variable } from "types/variable";
|
||||||
|
import options from "options";
|
||||||
|
import { Unit } from "lib/types/options";
|
||||||
|
|
||||||
|
const { unit } = options.menus.clock.weather;
|
||||||
|
|
||||||
|
export const TodayStats = (theWeather: Variable<Weather>) => {
|
||||||
|
return Widget.Box({
|
||||||
|
class_name: "calendar-menu-weather today stats container",
|
||||||
|
hpack: "end",
|
||||||
|
vpack: "center",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "weather wind",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "weather wind icon",
|
||||||
|
label: "",
|
||||||
|
}),
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "weather wind label",
|
||||||
|
label: Utils.merge(
|
||||||
|
[theWeather.bind("value"), unit.bind("value")],
|
||||||
|
(wthr: Weather, unt: Unit) => {
|
||||||
|
if (unt === "imperial") {
|
||||||
|
return `${Math.floor(wthr.current.wind_mph)} mph`;
|
||||||
|
}
|
||||||
|
return `${Math.floor(wthr.current.wind_kph)} kph`;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
class_name: "weather precip",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "weather precip icon",
|
||||||
|
label: "",
|
||||||
|
}),
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "weather precip label",
|
||||||
|
label: theWeather
|
||||||
|
.bind("value")
|
||||||
|
.as(
|
||||||
|
(v) => `${v.forecast.forecastday[0].day.daily_chance_of_rain}%`,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import options from "options";
|
|
||||||
const { unit } = options.menus.clock.weather;
|
|
||||||
|
|
||||||
export const TodayTemperature = (theWeather) => {
|
|
||||||
const getIcon = (fahren) => {
|
|
||||||
const icons = {
|
|
||||||
100: "",
|
|
||||||
75: "",
|
|
||||||
50: "",
|
|
||||||
25: "",
|
|
||||||
0: "",
|
|
||||||
};
|
|
||||||
const colors = {
|
|
||||||
100: "weather-color red",
|
|
||||||
75: "weather-color orange",
|
|
||||||
50: "weather-color lavender",
|
|
||||||
25: "weather-color blue",
|
|
||||||
0: "weather-color sky",
|
|
||||||
};
|
|
||||||
|
|
||||||
const threshold =
|
|
||||||
fahren < 0
|
|
||||||
? 0
|
|
||||||
: [100, 75, 50, 25, 0].find((threshold) => threshold <= fahren);
|
|
||||||
|
|
||||||
return {
|
|
||||||
icon: icons[threshold],
|
|
||||||
color: colors[threshold],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return Widget.Box({
|
|
||||||
hpack: "center",
|
|
||||||
vpack: "center",
|
|
||||||
vertical: true,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
hexpand: true,
|
|
||||||
vpack: "center",
|
|
||||||
class_name: "calendar-menu-weather today temp container",
|
|
||||||
vertical: false,
|
|
||||||
children: [
|
|
||||||
Widget.Box({
|
|
||||||
hexpand: true,
|
|
||||||
hpack: "center",
|
|
||||||
children: [
|
|
||||||
Widget.Label({
|
|
||||||
class_name: "calendar-menu-weather today temp label",
|
|
||||||
label: Utils.merge(
|
|
||||||
[theWeather.bind("value"), unit.bind("value")],
|
|
||||||
(wthr, unt) => {
|
|
||||||
if (unt === "imperial") {
|
|
||||||
return `${Math.ceil(wthr.current.temp_f)}° F`;
|
|
||||||
} else {
|
|
||||||
return `${Math.ceil(wthr.current.temp_c)}° C`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
Widget.Label({
|
|
||||||
class_name: theWeather
|
|
||||||
.bind("value")
|
|
||||||
.as(
|
|
||||||
(v) =>
|
|
||||||
`calendar-menu-weather today temp label icon ${getIcon(Math.ceil(v.current.temp_f)).color}`,
|
|
||||||
),
|
|
||||||
label: theWeather
|
|
||||||
.bind("value")
|
|
||||||
.as((v) => getIcon(Math.ceil(v.current.temp_f)).icon),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Widget.Box({
|
|
||||||
hpack: "center",
|
|
||||||
child: Widget.Label({
|
|
||||||
max_width_chars: 17,
|
|
||||||
truncate: "end",
|
|
||||||
lines: 2,
|
|
||||||
class_name: theWeather
|
|
||||||
.bind("value")
|
|
||||||
.as(
|
|
||||||
(v) =>
|
|
||||||
`calendar-menu-weather today condition label ${getIcon(Math.ceil(v.current.temp_f)).color}`,
|
|
||||||
),
|
|
||||||
label: theWeather.bind("value").as((v) => v.current.condition.text),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
94
modules/menus/calendar/weather/temperature/index.ts
Normal file
94
modules/menus/calendar/weather/temperature/index.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Weather } from "lib/types/weather";
|
||||||
|
import { Variable } from "types/variable";
|
||||||
|
import options from "options";
|
||||||
|
const { unit } = options.menus.clock.weather;
|
||||||
|
|
||||||
|
export const TodayTemperature = (theWeather: Variable<Weather>) => {
|
||||||
|
const getIcon = (fahren: number) => {
|
||||||
|
const icons = {
|
||||||
|
100: "",
|
||||||
|
75: "",
|
||||||
|
50: "",
|
||||||
|
25: "",
|
||||||
|
0: "",
|
||||||
|
};
|
||||||
|
const colors = {
|
||||||
|
100: "weather-color red",
|
||||||
|
75: "weather-color orange",
|
||||||
|
50: "weather-color lavender",
|
||||||
|
25: "weather-color blue",
|
||||||
|
0: "weather-color sky",
|
||||||
|
};
|
||||||
|
|
||||||
|
const threshold =
|
||||||
|
fahren < 0
|
||||||
|
? 0
|
||||||
|
: [100, 75, 50, 25, 0].find((threshold) => threshold <= fahren);
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: icons[threshold || 50],
|
||||||
|
color: colors[threshold || 50],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return Widget.Box({
|
||||||
|
hpack: "center",
|
||||||
|
vpack: "center",
|
||||||
|
vertical: true,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
hexpand: true,
|
||||||
|
vpack: "center",
|
||||||
|
class_name: "calendar-menu-weather today temp container",
|
||||||
|
vertical: false,
|
||||||
|
children: [
|
||||||
|
Widget.Box({
|
||||||
|
hexpand: true,
|
||||||
|
hpack: "center",
|
||||||
|
children: [
|
||||||
|
Widget.Label({
|
||||||
|
class_name: "calendar-menu-weather today temp label",
|
||||||
|
label: Utils.merge(
|
||||||
|
[theWeather.bind("value"), unit.bind("value")],
|
||||||
|
(wthr, unt) => {
|
||||||
|
if (unt === "imperial") {
|
||||||
|
return `${Math.ceil(wthr.current.temp_f)}° F`;
|
||||||
|
} else {
|
||||||
|
return `${Math.ceil(wthr.current.temp_c)}° C`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
Widget.Label({
|
||||||
|
class_name: theWeather
|
||||||
|
.bind("value")
|
||||||
|
.as(
|
||||||
|
(v) =>
|
||||||
|
`calendar-menu-weather today temp label icon ${getIcon(Math.ceil(v.current.temp_f)).color}`,
|
||||||
|
),
|
||||||
|
label: theWeather
|
||||||
|
.bind("value")
|
||||||
|
.as((v) => getIcon(Math.ceil(v.current.temp_f)).icon),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Widget.Box({
|
||||||
|
hpack: "center",
|
||||||
|
child: Widget.Label({
|
||||||
|
max_width_chars: 17,
|
||||||
|
truncate: "end",
|
||||||
|
lines: 2,
|
||||||
|
class_name: theWeather
|
||||||
|
.bind("value")
|
||||||
|
.as(
|
||||||
|
(v) =>
|
||||||
|
`calendar-menu-weather today condition label ${getIcon(Math.ceil(v.current.temp_f)).color}`,
|
||||||
|
),
|
||||||
|
label: theWeather.bind("value").as((v) => v.current.condition.text),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user