quickshell: material symbols icons, network cards, session menu morphs from right edge
- Material Symbols Rounded (ligature names) replaces nerd-font glyphs for all shell icons; text stays on the stylix mono font - network dropdown gets the card treatment like the others - power menu is now an icon-only session panel melting out of the right frame column at screen centre (still Super+L); launcher is apps-only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4540e38321
commit
a14ccd8c09
1 changed files with 415 additions and 232 deletions
|
|
@ -12,6 +12,10 @@ in
|
||||||
qt6.qt5compat # Qt5Compat.GraphicalEffects in Bar.qml
|
qt6.qt5compat # Qt5Compat.GraphicalEffects in Bar.qml
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Icon font for the shell (ligature-based: text "volume_up" renders the
|
||||||
|
# icon) — same font caelestia uses; nerd-font glyphs stay for terminals.
|
||||||
|
fonts.packages = [ pkgs.material-symbols ];
|
||||||
|
|
||||||
home-manager.users.fred = { config, lib, pkgs, osConfig, ... }:
|
home-manager.users.fred = { config, lib, pkgs, osConfig, ... }:
|
||||||
let
|
let
|
||||||
c = config.lib.stylix.colors;
|
c = config.lib.stylix.colors;
|
||||||
|
|
@ -64,6 +68,7 @@ in
|
||||||
vec4 cutout; // cx, cy, hw, hh — rounded inner screen cutout
|
vec4 cutout; // cx, cy, hw, hh — rounded inner screen cutout
|
||||||
vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none)
|
vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none)
|
||||||
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
||||||
|
vec4 session; // cx, cy, hw, hh — session menu (hw <= 0: none)
|
||||||
vec4 fillColor; // straight (non-premultiplied) rgba
|
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||||
vec4 borderColor;
|
vec4 borderColor;
|
||||||
vec2 res;
|
vec2 res;
|
||||||
|
|
@ -94,6 +99,8 @@ in
|
||||||
d = smin(d, sdRoundedBox(p, panel.xy, panel.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, panel.xy, panel.zw, panelR), meltK);
|
||||||
if (toast.z > 0.5)
|
if (toast.z > 0.5)
|
||||||
d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK);
|
||||||
|
if (session.z > 0.5)
|
||||||
|
d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK);
|
||||||
|
|
||||||
float fw = fwidth(d);
|
float fw = fwidth(d);
|
||||||
// 1 inside the union, 0 outside (antialiased)
|
// 1 inside the union, 0 outside (antialiased)
|
||||||
|
|
@ -152,6 +159,8 @@ in
|
||||||
readonly property color barBg: "#B3${c.base00}"
|
readonly property color barBg: "#B3${c.base00}"
|
||||||
readonly property color toastBg: "#E6${c.base00}"
|
readonly property color toastBg: "#E6${c.base00}"
|
||||||
readonly property string fontFamily: "${monoFont}"
|
readonly property string fontFamily: "${monoFont}"
|
||||||
|
// Ligature-based icon font: text "volume_up" renders the icon
|
||||||
|
readonly property string iconFont: "Material Symbols Rounded"
|
||||||
// Matches hyprland general.border_size (col.inactive_border = base03)
|
// Matches hyprland general.border_size (col.inactive_border = base03)
|
||||||
readonly property int borderWidth: 2
|
readonly property int borderWidth: 2
|
||||||
// Screen frame band; sits inside hyprland's gaps_out (12)
|
// Screen frame band; sits inside hyprland's gaps_out (12)
|
||||||
|
|
@ -189,17 +198,19 @@ in
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
property var latestNotification: null
|
property var latestNotification: null
|
||||||
|
property var mainBar: null
|
||||||
signal notificationReceived()
|
signal notificationReceived()
|
||||||
|
|
||||||
Launcher {
|
Launcher {
|
||||||
id: launcher
|
id: launcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bound in hyprland.nix: Super+R → toggle, Super+L → powermenu
|
// Bound in hyprland.nix: Super+R → app launcher,
|
||||||
|
// Super+L → session menu (in the bar window).
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "launcher"
|
target: "launcher"
|
||||||
function toggle(): void { launcher.toggleMode("apps"); }
|
function toggle(): void { launcher.toggle(); }
|
||||||
function powermenu(): void { launcher.toggleMode("power"); }
|
function powermenu(): void { if (root.mainBar) root.mainBar.toggleSession(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft reload, used by the nix onChange hook — keeps the
|
// Soft reload, used by the nix onChange hook — keeps the
|
||||||
|
|
@ -249,9 +260,6 @@ in
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// "apps" (Super+R) or "power" (Super+L)
|
|
||||||
property string mode: "apps"
|
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
screen: Quickshell.screens[0]
|
screen: Quickshell.screens[0]
|
||||||
WlrLayershell.namespace: "quickshell-launcher"
|
WlrLayershell.namespace: "quickshell-launcher"
|
||||||
|
|
@ -267,9 +275,8 @@ in
|
||||||
right: true
|
right: true
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMode(m) {
|
function toggle() {
|
||||||
if (visible && mode === m) { close(); return; }
|
if (visible) { close(); return; }
|
||||||
mode = m;
|
|
||||||
search.text = "";
|
search.text = "";
|
||||||
list.currentIndex = 0;
|
list.currentIndex = 0;
|
||||||
visible = true;
|
visible = true;
|
||||||
|
|
@ -279,18 +286,6 @@ in
|
||||||
visible = false;
|
visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock/reboot/shutdown spawn via Quickshell.execDetached — fully
|
|
||||||
// detached, so a quickshell restart can never kill a running
|
|
||||||
// hyprlock. Logout (empty cmd) goes through Hyprland IPC; with a
|
|
||||||
// Lua config the dispatch body is evaluated as a Lua dispatcher
|
|
||||||
// expression, so it must use hl.dsp.* syntax, not hyprlang's.
|
|
||||||
readonly property var powerActions: [
|
|
||||||
{ name: "Lock", glyph: "", cmd: [Commands.hyprlock] },
|
|
||||||
{ name: "Logout", glyph: "", cmd: [] },
|
|
||||||
{ name: "Reboot", glyph: "", cmd: [Commands.systemctl, "reboot"] },
|
|
||||||
{ name: "Shutdown", glyph: "", cmd: [Commands.systemctl, "poweroff"] }
|
|
||||||
]
|
|
||||||
|
|
||||||
function score(name, extra, q) {
|
function score(name, extra, q) {
|
||||||
let n = name.toLowerCase();
|
let n = name.toLowerCase();
|
||||||
if (n.startsWith(q)) return 5;
|
if (n.startsWith(q)) return 5;
|
||||||
|
|
@ -313,9 +308,6 @@ in
|
||||||
|
|
||||||
property var entries: {
|
property var entries: {
|
||||||
let q = search.text.toLowerCase().trim();
|
let q = search.text.toLowerCase().trim();
|
||||||
if (mode === "power") {
|
|
||||||
return powerActions.filter(a => q === "" || score(a.name, "", q) > 0);
|
|
||||||
}
|
|
||||||
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
||||||
if (q === "") {
|
if (q === "") {
|
||||||
apps.sort((a, b) => a.name.localeCompare(b.name));
|
apps.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
@ -332,12 +324,7 @@ in
|
||||||
|
|
||||||
function activate(item) {
|
function activate(item) {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (mode === "power") {
|
|
||||||
if (item.cmd.length === 0) Hyprland.dispatch("hl.dsp.exit()");
|
|
||||||
else Quickshell.execDetached(item.cmd);
|
|
||||||
} else {
|
|
||||||
item.execute();
|
item.execute();
|
||||||
}
|
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,7 +386,7 @@ in
|
||||||
anchors.fill: search
|
anchors.fill: search
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
visible: search.text === ""
|
visible: search.text === ""
|
||||||
text: root.mode === "power" ? "Power" : "Search"
|
text: "Search"
|
||||||
color: Theme.base03
|
color: Theme.base03
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
|
|
@ -429,22 +416,13 @@ in
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
visible: root.mode === "apps" && source != ""
|
visible: source != ""
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 18
|
width: 18
|
||||||
height: 18
|
height: 18
|
||||||
sourceSize.width: 18
|
sourceSize.width: 18
|
||||||
sourceSize.height: 18
|
sourceSize.height: 18
|
||||||
source: root.mode === "apps" ? Quickshell.iconPath(modelData.icon, true) : ""
|
source: Quickshell.iconPath(modelData.icon, true)
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: root.mode === "power"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: root.mode === "power" ? modelData.glyph : ""
|
|
||||||
color: Theme.base0D
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: 14
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|
@ -522,6 +500,12 @@ in
|
||||||
width: toastItem.visible ? toastItem.width : 0
|
width: toastItem.visible ? toastItem.width : 0
|
||||||
height: toastItem.visible ? toastItem.height : 0
|
height: toastItem.visible ? toastItem.height : 0
|
||||||
}
|
}
|
||||||
|
Region {
|
||||||
|
x: sessionMenu.visible ? sessionMenu.x : 0
|
||||||
|
y: sessionMenu.visible ? sessionMenu.y : 0
|
||||||
|
width: sessionMenu.visible ? sessionMenu.width : 0
|
||||||
|
height: sessionMenu.visible ? sessionMenu.height : 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -532,6 +516,113 @@ in
|
||||||
height: 30
|
height: 30
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register the primary bar so shell.qml's IPC handler can
|
||||||
|
// reach the session menu.
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (bar.screen === Quickshell.screens[0])
|
||||||
|
bar.shellRoot.mainBar = bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSession() {
|
||||||
|
sessionMenu.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Session menu: icon-only power controls morphing out of
|
||||||
|
// the right frame column at screen centre (Super+L).
|
||||||
|
Item {
|
||||||
|
id: sessionMenu
|
||||||
|
property bool open: false
|
||||||
|
property real openW: open ? 56 : 0
|
||||||
|
Behavior on openW {
|
||||||
|
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
|
||||||
|
x: bar.width - Theme.frameWidth - openW
|
||||||
|
y: Math.round((bar.height - height) / 2)
|
||||||
|
width: openW
|
||||||
|
height: sessionCol.height + 24
|
||||||
|
visible: openW > 0.5
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
open = !open;
|
||||||
|
if (open) _sessionAutoClose.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _sessionAutoClose
|
||||||
|
interval: 2500
|
||||||
|
onTriggered: sessionMenu.open = false
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
onHoveredChanged: {
|
||||||
|
if (hovered) _sessionAutoClose.stop();
|
||||||
|
else if (sessionMenu.open) _sessionAutoClose.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content pinned to the column edge, revealed by the grow
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
opacity: sessionMenu.open ? 1 : 0
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: sessionCol
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: [
|
||||||
|
{ icon: "lock", danger: false, act: "lock" },
|
||||||
|
{ icon: "logout", danger: false, act: "logout" },
|
||||||
|
{ icon: "restart_alt", danger: true, act: "reboot" },
|
||||||
|
{ icon: "power_settings_new", danger: true, act: "poweroff" }
|
||||||
|
]
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sessBtn
|
||||||
|
required property var modelData
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: 8
|
||||||
|
color: sessBtnMa.containsMouse ? Theme.base02 : "transparent"
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: sessBtn.modelData.icon
|
||||||
|
color: sessBtn.modelData.danger && sessBtnMa.containsMouse
|
||||||
|
? Theme.base08 : Theme.base05
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sessBtnMa
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
sessionMenu.open = false;
|
||||||
|
if (sessBtn.modelData.act === "lock") Quickshell.execDetached([Commands.hyprlock]);
|
||||||
|
else if (sessBtn.modelData.act === "logout") Hyprland.dispatch("hl.dsp.exit()");
|
||||||
|
else if (sessBtn.modelData.act === "reboot") Quickshell.execDetached([Commands.systemctl, "reboot"]);
|
||||||
|
else Quickshell.execDetached([Commands.systemctl, "poweroff"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Shell chrome: bar, frame, panel and toast rendered as
|
// ── Shell chrome: bar, frame, panel and toast rendered as
|
||||||
// ONE signed-distance field (caelestia-style). Surfaces merge
|
// ONE signed-distance field (caelestia-style). Surfaces merge
|
||||||
// via circular smooth-min, and the 2px border is the distance
|
// via circular smooth-min, and the 2px border is the distance
|
||||||
|
|
@ -555,6 +646,11 @@ in
|
||||||
? Qt.vector4d(toastItem.x + 8 + _toastRect.width / 2, 26 + _toastRect.height / 2,
|
? Qt.vector4d(toastItem.x + 8 + _toastRect.width / 2, 26 + _toastRect.height / 2,
|
||||||
_toastRect.width / 2, 4 + _toastRect.height / 2)
|
_toastRect.width / 2, 4 + _toastRect.height / 2)
|
||||||
: Qt.vector4d(0, 0, 0, 0)
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
|
readonly property real sessRight: bar.width - Theme.frameWidth + 4
|
||||||
|
property vector4d session: sessionMenu.visible
|
||||||
|
? Qt.vector4d((sessionMenu.x + sessRight) / 2, sessionMenu.y + sessionMenu.height / 2,
|
||||||
|
(sessRight - sessionMenu.x) / 2, sessionMenu.height / 2)
|
||||||
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
||||||
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
||||||
property vector2d res: Qt.vector2d(width, height)
|
property vector2d res: Qt.vector2d(width, height)
|
||||||
|
|
@ -691,7 +787,7 @@ in
|
||||||
// Volume
|
// Volume
|
||||||
Item {
|
Item {
|
||||||
id: volWidget
|
id: volWidget
|
||||||
width: volText.width
|
width: volRow.width
|
||||||
height: 30
|
height: 30
|
||||||
|
|
||||||
property PwNode sink: Pipewire.defaultAudioSink
|
property PwNode sink: Pipewire.defaultAudioSink
|
||||||
|
|
@ -702,11 +798,11 @@ in
|
||||||
|
|
||||||
property int vol: sink && sink.audio ? Math.round(sink.audio.volume * 100) : 0
|
property int vol: sink && sink.audio ? Math.round(sink.audio.volume * 100) : 0
|
||||||
property bool muted: sink && sink.audio ? sink.audio.muted : false
|
property bool muted: sink && sink.audio ? sink.audio.muted : false
|
||||||
property string volIcon: muted ? "\u{f0581}"
|
property string volIcon: muted ? "volume_off"
|
||||||
: vol > 66 ? "\u{f057e}"
|
: vol > 66 ? "volume_up"
|
||||||
: vol > 33 ? "\u{f0580}"
|
: vol > 33 ? "volume_down"
|
||||||
: vol > 0 ? "\u{f057f}"
|
: vol > 0 ? "volume_mute"
|
||||||
: "\u{f0581}"
|
: "volume_off"
|
||||||
|
|
||||||
function openVolDropdown() {
|
function openVolDropdown() {
|
||||||
bar.toggleDropdown(volDropdown, function() {
|
bar.toggleDropdown(volDropdown, function() {
|
||||||
|
|
@ -715,14 +811,27 @@ in
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Row {
|
||||||
id: volText
|
id: volRow
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: volWidget.volIcon + " " + volWidget.vol + "%"
|
spacing: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: volWidget.volIcon
|
||||||
|
color: volWidget.muted ? Theme.base03 : Theme.base05
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: volWidget.vol + "%"
|
||||||
color: volWidget.muted ? Theme.base03 : Theme.base05
|
color: volWidget.muted ? Theme.base03 : Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -754,7 +863,7 @@ in
|
||||||
property string netState: "disconnected"
|
property string netState: "disconnected"
|
||||||
property string netConn: ""
|
property string netConn: ""
|
||||||
property string netType: ""
|
property string netType: ""
|
||||||
property string netIcon: "\u{f0b0}"
|
property string netIcon: "wifi_off"
|
||||||
property var wifiNetworks: []
|
property var wifiNetworks: []
|
||||||
property string netDevice: ""
|
property string netDevice: ""
|
||||||
|
|
||||||
|
|
@ -832,9 +941,9 @@ in
|
||||||
netWidget.netType = netWidget._pendingType.length > 0 ? netWidget._pendingType : netWidget.netType;
|
netWidget.netType = netWidget._pendingType.length > 0 ? netWidget._pendingType : netWidget.netType;
|
||||||
netWidget.netDevice = netWidget._pendingDevice.length > 0 ? netWidget._pendingDevice : netWidget.netDevice;
|
netWidget.netDevice = netWidget._pendingDevice.length > 0 ? netWidget._pendingDevice : netWidget.netDevice;
|
||||||
if (netWidget.netState === "connected") {
|
if (netWidget.netState === "connected") {
|
||||||
netWidget.netIcon = netWidget.netType === "wifi" ? "\u{f05a9}" : "\u{f0200}";
|
netWidget.netIcon = netWidget.netType === "wifi" ? "wifi" : "lan";
|
||||||
} else {
|
} else {
|
||||||
netWidget.netIcon = netWidget.netType === "wifi" ? "\u{f05aa}" : "\u{f0201}";
|
netWidget.netIcon = netWidget.netType === "wifi" ? "wifi_off" : "settings_ethernet";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -844,8 +953,8 @@ in
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: netWidget.netIcon
|
text: netWidget.netIcon
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 14
|
font.pixelSize: 16
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
|
|
@ -938,13 +1047,13 @@ in
|
||||||
PowerProfiles.profile === PowerProfile.PowerSaver ? "power-saver"
|
PowerProfiles.profile === PowerProfile.PowerSaver ? "power-saver"
|
||||||
: PowerProfiles.profile === PowerProfile.Performance ? "performance"
|
: PowerProfiles.profile === PowerProfile.Performance ? "performance"
|
||||||
: "balanced"
|
: "balanced"
|
||||||
property string batteryIcon: charging ? "\u{f0084}"
|
property string batteryIcon: charging ? "battery_charging_full"
|
||||||
: batteryLevel >= 90 ? "\u{f0079}"
|
: batteryLevel >= 90 ? "battery_full"
|
||||||
: batteryLevel >= 70 ? "\u{f0082}"
|
: batteryLevel >= 70 ? "battery_6_bar"
|
||||||
: batteryLevel >= 50 ? "\u{f007f}"
|
: batteryLevel >= 50 ? "battery_5_bar"
|
||||||
: batteryLevel >= 30 ? "\u{f007c}"
|
: batteryLevel >= 30 ? "battery_3_bar"
|
||||||
: batteryLevel >= 15 ? "\u{f007a}"
|
: batteryLevel >= 15 ? "battery_2_bar"
|
||||||
: "\u{f008e}"
|
: "battery_alert"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -966,8 +1075,8 @@ in
|
||||||
color: batteryWidget.batteryLevel <= 15 ? Theme.base08
|
color: batteryWidget.batteryLevel <= 15 ? Theme.base08
|
||||||
: batteryWidget.batteryLevel <= 30 ? Theme.base0A
|
: batteryWidget.batteryLevel <= 30 ? Theme.base0A
|
||||||
: Theme.base05
|
: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 14
|
font.pixelSize: 16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1383,13 +1492,24 @@ in
|
||||||
width: parent.width - 16
|
width: parent.width - 16
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 6
|
||||||
Text {
|
Text {
|
||||||
text: "\u{f057e} Master"
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "volume_up"
|
||||||
|
color: Theme.base05
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Master"
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
@ -1456,13 +1576,24 @@ in
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
radius: 4
|
radius: 4
|
||||||
|
|
||||||
Text {
|
Row {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: volWidget.muted ? "\u{f0581} Unmute" : "\u{f057e} Mute"
|
spacing: 6
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: volWidget.muted ? "volume_off" : "volume_up"
|
||||||
|
color: Theme.base05
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 15
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: volWidget.muted ? "Unmute" : "Mute"
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
}
|
}
|
||||||
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: masterMuteMa
|
id: masterMuteMa
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -1493,13 +1624,24 @@ in
|
||||||
width: parent.width - 16
|
width: parent.width - 16
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 6
|
||||||
Text {
|
Text {
|
||||||
text: "\u{f0641} Applications"
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "graphic_eq"
|
||||||
|
color: Theme.base05
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Applications"
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Per-app streams
|
// Per-app streams
|
||||||
Column {
|
Column {
|
||||||
|
|
@ -1602,20 +1744,48 @@ in
|
||||||
Column {
|
Column {
|
||||||
id: netDropdownCol
|
id: netDropdownCol
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 220
|
width: 228
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
// Connection card
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: connCardCol.height + 16
|
||||||
|
radius: 8
|
||||||
|
color: Theme.base01
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: connCardCol
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 8
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: parent.width - 16
|
||||||
spacing: 4
|
spacing: 4
|
||||||
|
|
||||||
Text {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: netWidget.netState === "connected" ? "wifi" : "wifi_off"
|
||||||
|
color: Theme.base05
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - 22
|
||||||
text: netWidget.netState === "connected"
|
text: netWidget.netState === "connected"
|
||||||
? "\u{f05a9} " + netWidget.netConn
|
? netWidget.netConn : "Not connected"
|
||||||
: "\u{f05aa} Not connected"
|
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: netWidget.netState === "connected"
|
visible: netWidget.netState === "connected"
|
||||||
|
|
@ -1637,31 +1807,41 @@ in
|
||||||
id: disconnectMouse
|
id: disconnectMouse
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
netDisconnectProc.targetDevice = netWidget.netDevice;
|
netDisconnectProc.targetDevice = netWidget.netDevice;
|
||||||
netDisconnectProc.running = true;
|
netDisconnectProc.running = true;
|
||||||
netWidget.netState = "disconnected";
|
netWidget.netState = "disconnected";
|
||||||
netWidget.netConn = "";
|
netWidget.netConn = "";
|
||||||
netWidget.netIcon = "\u{f05aa}";
|
netWidget.netIcon = "wifi_off";
|
||||||
bar.closeAllDropdowns();
|
bar.closeAllDropdowns();
|
||||||
netRefreshDelay.start();
|
netRefreshDelay.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - 20
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
height: 1
|
|
||||||
color: Theme.base03
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available networks card
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: netsCardCol.height + 16
|
||||||
|
radius: 8
|
||||||
|
color: Theme.base01
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: netsCardCol
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 8
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: parent.width - 16
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Available networks"
|
text: "Available networks"
|
||||||
color: Theme.base03
|
color: Theme.base04
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
topPadding: 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|
@ -1669,7 +1849,7 @@ in
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
width: 220
|
width: netsCardCol.width
|
||||||
height: 32
|
height: 32
|
||||||
color: netItemMouse.containsMouse ? Theme.base02 : "transparent"
|
color: netItemMouse.containsMouse ? Theme.base02 : "transparent"
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
|
@ -1686,14 +1866,14 @@ in
|
||||||
Text {
|
Text {
|
||||||
text: {
|
text: {
|
||||||
let s = modelData.signal;
|
let s = modelData.signal;
|
||||||
if (s >= 75) return "\u{f0928}"; // strength 4
|
if (s >= 75) return "signal_wifi_4_bar";
|
||||||
if (s >= 50) return "\u{f0925}"; // strength 3
|
if (s >= 50) return "network_wifi_3_bar";
|
||||||
if (s >= 25) return "\u{f0922}"; // strength 2
|
if (s >= 25) return "network_wifi_2_bar";
|
||||||
return "\u{f091f}"; // strength 1
|
return "network_wifi_1_bar";
|
||||||
}
|
}
|
||||||
color: modelData.active ? Theme.base0B : Theme.base04
|
color: modelData.active ? Theme.base0B : Theme.base04
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 13
|
font.pixelSize: 16
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1709,10 +1889,10 @@ in
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: modelData.security !== "" && modelData.security !== "--"
|
visible: modelData.security !== "" && modelData.security !== "--"
|
||||||
text: "\u{f0341}"
|
text: "lock"
|
||||||
color: Theme.base03
|
color: Theme.base03
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 10
|
font.pixelSize: 13
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1721,6 +1901,7 @@ in
|
||||||
id: netItemMouse
|
id: netItemMouse
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!modelData.active) {
|
if (!modelData.active) {
|
||||||
wifiConnectProc.targetSsid = modelData.ssid;
|
wifiConnectProc.targetSsid = modelData.ssid;
|
||||||
|
|
@ -1734,6 +1915,8 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
${lib.optionalString isMacbook ''
|
${lib.optionalString isMacbook ''
|
||||||
// Battery dropdown
|
// Battery dropdown
|
||||||
|
|
@ -1770,8 +1953,8 @@ in
|
||||||
Text {
|
Text {
|
||||||
text: batteryWidget.batteryIcon
|
text: batteryWidget.batteryIcon
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 18
|
font.pixelSize: 22
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1848,9 +2031,9 @@ in
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: [
|
model: [
|
||||||
{ name: "power-saver", profile: PowerProfile.PowerSaver, label: "\u{f0425}", tip: "Saver" },
|
{ name: "power-saver", profile: PowerProfile.PowerSaver, label: "energy_savings_leaf", tip: "Saver" },
|
||||||
{ name: "balanced", profile: PowerProfile.Balanced, label: "\u{f0376}", tip: "Balanced" },
|
{ name: "balanced", profile: PowerProfile.Balanced, label: "balance", tip: "Balanced" },
|
||||||
{ name: "performance", profile: PowerProfile.Performance, label: "\u{f0e0e}", tip: "Performance" }
|
{ name: "performance", profile: PowerProfile.Performance, label: "speed", tip: "Performance" }
|
||||||
]
|
]
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -1871,8 +2054,8 @@ in
|
||||||
color: batteryWidget.powerProfile === modelData.name
|
color: batteryWidget.powerProfile === modelData.name
|
||||||
? Theme.base0D : Theme.base05
|
? Theme.base0D : Theme.base05
|
||||||
Behavior on color { ColorAnimation { duration: 200 } }
|
Behavior on color { ColorAnimation { duration: 200 } }
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 14
|
font.pixelSize: 17
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
@ -1963,16 +2146,16 @@ in
|
||||||
onTriggered: weatherProc.running = true
|
onTriggered: weatherProc.running = true
|
||||||
}
|
}
|
||||||
function weatherGlyph(code) {
|
function weatherGlyph(code) {
|
||||||
if (code === 0) return "\u{f0599}"; // sunny
|
if (code === 0) return "clear_day";
|
||||||
if (code <= 2) return "\u{f0595}"; // partly cloudy
|
if (code <= 2) return "partly_cloudy_day";
|
||||||
if (code === 3) return "\u{f0590}"; // overcast
|
if (code === 3) return "cloud";
|
||||||
if (code <= 48) return "\u{f0591}"; // fog
|
if (code <= 48) return "foggy";
|
||||||
if (code <= 57) return "\u{f0597}"; // drizzle
|
if (code <= 57) return "rainy";
|
||||||
if (code <= 67) return "\u{f0596}"; // rain
|
if (code <= 67) return "rainy";
|
||||||
if (code <= 77) return "\u{f0598}"; // snow
|
if (code <= 77) return "cloudy_snowing";
|
||||||
if (code <= 82) return "\u{f0597}"; // showers
|
if (code <= 82) return "rainy";
|
||||||
if (code <= 86) return "\u{f0598}"; // snow showers
|
if (code <= 86) return "cloudy_snowing";
|
||||||
return "\u{f0593}"; // thunder
|
return "thunderstorm";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Media: prefer the actively playing MPRIS player ---
|
// --- Media: prefer the actively playing MPRIS player ---
|
||||||
|
|
@ -2029,10 +2212,10 @@ in
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "\u{f0141}"
|
text: "chevron_left"
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 16
|
font.pixelSize: 18
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: calPrevMa
|
id: calPrevMa
|
||||||
|
|
@ -2064,10 +2247,10 @@ in
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "\u{f0142}"
|
text: "chevron_right"
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 16
|
font.pixelSize: 18
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: calNextMa
|
id: calNextMa
|
||||||
|
|
@ -2159,8 +2342,8 @@ in
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: calPopup.weatherGlyph(modelData.code)
|
text: calPopup.weatherGlyph(modelData.code)
|
||||||
color: Theme.base0C
|
color: Theme.base0C
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 14
|
font.pixelSize: 16
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
@ -2210,10 +2393,10 @@ in
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: albumArt.status !== Image.Ready
|
visible: albumArt.status !== Image.Ready
|
||||||
text: "\u{f0387}"
|
text: "music_note"
|
||||||
color: Theme.base04
|
color: Theme.base04
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 20
|
font.pixelSize: 22
|
||||||
}
|
}
|
||||||
Image {
|
Image {
|
||||||
id: albumArt
|
id: albumArt
|
||||||
|
|
@ -2251,9 +2434,9 @@ in
|
||||||
spacing: 2
|
spacing: 2
|
||||||
Repeater {
|
Repeater {
|
||||||
model: [
|
model: [
|
||||||
{ glyph: "\u{f04ae}", act: "prev" },
|
{ glyph: "skip_previous", act: "prev" },
|
||||||
{ glyph: calPopup.player && calPopup.player.playbackState === MprisPlaybackState.Playing ? "\u{f03e4}" : "\u{f040a}", act: "toggle" },
|
{ glyph: calPopup.player && calPopup.player.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow", act: "toggle" },
|
||||||
{ glyph: "\u{f04ad}", act: "next" }
|
{ glyph: "skip_next", act: "next" }
|
||||||
]
|
]
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: mediaBtn
|
id: mediaBtn
|
||||||
|
|
@ -2265,8 +2448,8 @@ in
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: mediaBtn.modelData.glyph
|
text: mediaBtn.modelData.glyph
|
||||||
color: Theme.base05
|
color: Theme.base05
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 16
|
font.pixelSize: 18
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mediaBtnMa
|
id: mediaBtnMa
|
||||||
|
|
@ -2425,10 +2608,10 @@ in
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.margins: 6
|
anchors.margins: 6
|
||||||
text: "\u{f0156}"
|
text: "close"
|
||||||
color: dismissMa.containsMouse ? Theme.base05 : Theme.base03
|
color: dismissMa.containsMouse ? Theme.base05 : Theme.base03
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 12
|
font.pixelSize: 14
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: dismissMa
|
id: dismissMa
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -2604,10 +2787,10 @@ in
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.margins: 8
|
anchors.margins: 8
|
||||||
text: "\u{f0156}"
|
text: "close"
|
||||||
color: toastDismissMa.containsMouse ? Theme.base05 : Theme.base03
|
color: toastDismissMa.containsMouse ? Theme.base05 : Theme.base03
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 13
|
font.pixelSize: 15
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: toastDismissMa
|
id: toastDismissMa
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue