Merge branch 'main' into sdfs
This commit is contained in:
commit
b57d596230
35 changed files with 1538 additions and 206 deletions
|
|
@ -55,6 +55,7 @@ Elevation {
|
|||
function onClicked(): void {
|
||||
root.itemSelected(item.modelData);
|
||||
root.active = item.modelData;
|
||||
item.modelData.clicked();
|
||||
root.expanded = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -267,11 +267,13 @@ Singleton {
|
|||
function serializeNotifs(): var {
|
||||
return {
|
||||
expire: notifs.expire,
|
||||
fullscreen: notifs.fullscreen,
|
||||
defaultExpireTimeout: notifs.defaultExpireTimeout,
|
||||
clearThreshold: notifs.clearThreshold,
|
||||
expandThreshold: notifs.expandThreshold,
|
||||
actionOnClick: notifs.actionOnClick,
|
||||
groupPreviewNum: notifs.groupPreviewNum
|
||||
groupPreviewNum: notifs.groupPreviewNum,
|
||||
openExpanded: notifs.openExpanded
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -318,11 +320,30 @@ Singleton {
|
|||
}
|
||||
|
||||
function serializeUtilities(): var {
|
||||
const vpnProviders = [];
|
||||
for (let i = 0; i < utilities.vpn.provider.length; i++) {
|
||||
const p = utilities.vpn.provider[i];
|
||||
const provider = {
|
||||
displayName: p.displayName,
|
||||
enabled: p.enabled,
|
||||
iface: p.iface,
|
||||
name: p.name
|
||||
};
|
||||
if (p.connectCmd && p.connectCmd.length > 0) {
|
||||
provider.connectCmd = p.connectCmd;
|
||||
}
|
||||
if (p.disconnectCmd && p.disconnectCmd.length > 0) {
|
||||
provider.disconnectCmd = p.disconnectCmd;
|
||||
}
|
||||
vpnProviders.push(provider);
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: utilities.enabled,
|
||||
maxToasts: utilities.maxToasts,
|
||||
toasts: {
|
||||
configLoaded: utilities.toasts.configLoaded,
|
||||
fullscreen: utilities.toasts.fullscreen,
|
||||
chargingChanged: utilities.toasts.chargingChanged,
|
||||
gameModeChanged: utilities.toasts.gameModeChanged,
|
||||
dndChanged: utilities.toasts.dndChanged,
|
||||
|
|
@ -336,7 +357,7 @@ Singleton {
|
|||
},
|
||||
vpn: {
|
||||
enabled: utilities.vpn.enabled,
|
||||
provider: utilities.vpn.provider
|
||||
provider: vpnProviders
|
||||
},
|
||||
quickToggles: utilities.quickToggles
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Quickshell.Io
|
|||
|
||||
JsonObject {
|
||||
property bool expire: true
|
||||
property string fullscreen: "on"
|
||||
property int defaultExpireTimeout: 5000
|
||||
property real clearThreshold: 0.3
|
||||
property int expandThreshold: 20
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ JsonObject {
|
|||
|
||||
component Toasts: JsonObject {
|
||||
property bool configLoaded: true
|
||||
property string fullscreen: "off"
|
||||
property bool chargingChanged: true
|
||||
property bool gameModeChanged: true
|
||||
property bool dndChanged: true
|
||||
|
|
@ -61,6 +62,6 @@ JsonObject {
|
|||
|
||||
component Vpn: JsonObject {
|
||||
property bool enabled: false
|
||||
property list<var> provider: ["netbird"]
|
||||
property list<var> provider: []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Scope {
|
|||
id: root
|
||||
|
||||
property bool launcherInterrupted
|
||||
readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false
|
||||
readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Caelestia.Internal
|
||||
import Caelestia.Services
|
||||
import qs.components
|
||||
import qs.services
|
||||
|
|
@ -55,25 +55,29 @@ Item {
|
|||
service: Audio.cava
|
||||
}
|
||||
|
||||
Item {
|
||||
id: content
|
||||
VisualiserBars {
|
||||
id: bars
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Config.border.thickness
|
||||
anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing
|
||||
|
||||
Side {
|
||||
content: content
|
||||
}
|
||||
Side {
|
||||
content: content
|
||||
isRight: true
|
||||
}
|
||||
values: Audio.cava.values
|
||||
primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7)
|
||||
secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7)
|
||||
rounding: Appearance.rounding.small * Config.background.visualiser.rounding
|
||||
spacing: Appearance.spacing.small * Config.background.visualiser.spacing
|
||||
animationDuration: Appearance.anim.durations.normal
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
running: root.opacity > 0 && !bars.settled
|
||||
onTriggered: bars.advance(frameTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -85,69 +89,4 @@ Item {
|
|||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
component Side: Repeater {
|
||||
id: side
|
||||
|
||||
required property Item content
|
||||
property bool isRight
|
||||
|
||||
model: Config.services.visualiserBars
|
||||
|
||||
ClippingRectangle {
|
||||
id: bar
|
||||
|
||||
required property int modelData
|
||||
property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1]))
|
||||
|
||||
clip: true
|
||||
|
||||
x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0)
|
||||
implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing
|
||||
|
||||
y: side.content.height - height
|
||||
implicitHeight: bar.value * side.content.height * 0.4
|
||||
|
||||
color: "transparent"
|
||||
topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding
|
||||
topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding
|
||||
|
||||
Rectangle {
|
||||
topLeftRadius: parent.topLeftRadius
|
||||
topRightRadius: parent.topRightRadius
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Qt.alpha(Colours.palette.m3primary, 0.7)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
y: parent.height - height
|
||||
implicitHeight: side.content.height * 0.4
|
||||
}
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ ColumnLayout {
|
|||
required property ShellScreen screen
|
||||
required property DrawerVisibilities visibilities
|
||||
required property BarPopouts.Wrapper popouts
|
||||
required property bool fullscreen
|
||||
readonly property int vPadding: Appearance.padding.large
|
||||
|
||||
function closeTray(): void {
|
||||
|
|
@ -128,6 +129,7 @@ ColumnLayout {
|
|||
delegate: WrappedLoader {
|
||||
sourceComponent: Workspaces {
|
||||
screen: root.screen
|
||||
fullscreen: root.fullscreen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +137,7 @@ ColumnLayout {
|
|||
roleValue: "activeWindow"
|
||||
delegate: WrappedLoader {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.fullscreen
|
||||
sourceComponent: ActiveWindow {
|
||||
bar: root
|
||||
monitor: Brightness.getMonitorForScreen(root.screen)
|
||||
|
|
@ -144,18 +147,21 @@ ColumnLayout {
|
|||
DelegateChoice {
|
||||
roleValue: "tray"
|
||||
delegate: WrappedLoader {
|
||||
visible: !root.fullscreen
|
||||
sourceComponent: Tray {}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "clock"
|
||||
delegate: WrappedLoader {
|
||||
visible: !root.fullscreen
|
||||
sourceComponent: Clock {}
|
||||
}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "statusIcons"
|
||||
delegate: WrappedLoader {
|
||||
visible: !root.fullscreen
|
||||
sourceComponent: StatusIcons {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ Item {
|
|||
required property DrawerVisibilities visibilities
|
||||
required property BarPopouts.Wrapper popouts
|
||||
required property bool disabled
|
||||
required property bool fullscreen
|
||||
|
||||
readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth)
|
||||
readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
|
||||
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2
|
||||
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness
|
||||
readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered)
|
||||
readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered)
|
||||
property bool isHovered
|
||||
|
||||
function closeTray(): void {
|
||||
|
|
@ -33,8 +34,9 @@ Item {
|
|||
(content.item as Bar)?.handleWheel(y, angleDelta);
|
||||
}
|
||||
|
||||
visible: width > Config.border.thickness
|
||||
implicitWidth: Config.border.thickness
|
||||
clip: true
|
||||
visible: width > 0
|
||||
implicitWidth: fullscreen ? 0 : Config.border.thickness
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
|
|
@ -83,6 +85,7 @@ Item {
|
|||
screen: root.screen
|
||||
visibilities: root.visibilities
|
||||
popouts: root.popouts // qmllint disable incompatible-type
|
||||
fullscreen: root.fullscreen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ StyledRect {
|
|||
required property int activeWsId
|
||||
required property Repeater workspaces
|
||||
required property Item mask
|
||||
required property bool fullscreen
|
||||
|
||||
readonly property int currentWsIdx: {
|
||||
let i = activeWsId - 1;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ StyledClippingRect {
|
|||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property bool fullscreen
|
||||
|
||||
readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== ""
|
||||
readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId
|
||||
|
|
@ -36,6 +37,7 @@ StyledClippingRect {
|
|||
anchors.fill: parent
|
||||
scale: root.onSpecial ? 0.8 : 1
|
||||
opacity: root.onSpecial ? 0.5 : 1
|
||||
visible: !root.fullscreen
|
||||
|
||||
layer.enabled: root.blur > 0
|
||||
layer.effect: MultiEffect {
|
||||
|
|
@ -86,6 +88,7 @@ StyledClippingRect {
|
|||
activeWsId: root.activeWsId
|
||||
workspaces: workspaces
|
||||
mask: layout
|
||||
fullscreen: root.fullscreen
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ QtObject {
|
|||
readonly property string icon: "task_alt"
|
||||
readonly property string component: "taskbar/TaskbarPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "notifications"
|
||||
readonly property string label: "notifications"
|
||||
readonly property string icon: "notifications"
|
||||
readonly property string component: "notifications/NotificationsPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "launcher"
|
||||
readonly property string label: "launcher"
|
||||
|
|
@ -60,7 +66,7 @@ QtObject {
|
|||
return result;
|
||||
}
|
||||
|
||||
function getByIndex(index: int): QtObject {
|
||||
function getByIndex(index: int): var {
|
||||
if (index >= 0 && index < panes.length) {
|
||||
return panes[index];
|
||||
}
|
||||
|
|
@ -76,12 +82,12 @@ QtObject {
|
|||
return -1;
|
||||
}
|
||||
|
||||
function getByLabel(label: string): QtObject {
|
||||
function getByLabel(label: string): var {
|
||||
const index = getIndexByLabel(label);
|
||||
return getByIndex(index);
|
||||
}
|
||||
|
||||
function getById(id: string): QtObject {
|
||||
function getById(id: string): var {
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
if (panes[i].id === id) {
|
||||
return panes[i];
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import "network"
|
|||
import "audio"
|
||||
import "appearance"
|
||||
import "taskbar"
|
||||
import "notifications"
|
||||
import "launcher"
|
||||
import "dashboard"
|
||||
import QtQuick
|
||||
|
|
|
|||
|
|
@ -109,10 +109,13 @@ DeviceDetails {
|
|||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
onClicked: {
|
||||
const provider = Config.utilities.vpn.provider[root.vpnProvider.index];
|
||||
editVpnDialog.editIndex = root.vpnProvider.index;
|
||||
editVpnDialog.providerName = root.vpnProvider.name;
|
||||
editVpnDialog.displayName = root.vpnProvider.displayName;
|
||||
editVpnDialog.interfaceName = root.vpnProvider.interface;
|
||||
editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : "";
|
||||
editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : "";
|
||||
editVpnDialog.open();
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +139,30 @@ DeviceDetails {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== ""
|
||||
text: qsTr("Open Login Page")
|
||||
inactiveColour: Colours.palette.m3tertiaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onTertiaryContainer
|
||||
|
||||
onClicked: {
|
||||
Qt.openUrlExternally(VPN.status.authUrl);
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === ""
|
||||
text: qsTr("Click 'Connect' to generate authentication URL")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -176,11 +203,30 @@ DeviceDetails {
|
|||
return qsTr("Disabled");
|
||||
if (VPN.connecting)
|
||||
return qsTr("Connecting...");
|
||||
if (VPN.connected)
|
||||
|
||||
switch (VPN.status.state) {
|
||||
case "connected":
|
||||
return qsTr("Connected");
|
||||
return qsTr("Enabled (Not connected)");
|
||||
case "disconnected":
|
||||
return qsTr("Disconnected");
|
||||
case "connecting":
|
||||
return qsTr("Connecting...");
|
||||
case "needs-auth":
|
||||
return qsTr("Authentication required");
|
||||
case "error":
|
||||
return qsTr("Error");
|
||||
default:
|
||||
return qsTr("Unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
visible: VPN.status.reason !== ""
|
||||
showTopMargin: true
|
||||
label: qsTr("Details")
|
||||
value: VPN.status.reason
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
|
|
@ -200,6 +246,8 @@ DeviceDetails {
|
|||
property string providerName: ""
|
||||
property string displayName: ""
|
||||
property string interfaceName: ""
|
||||
property string connectCmd: ""
|
||||
property string disconnectCmd: ""
|
||||
|
||||
function closeWithAnimation(): void {
|
||||
close();
|
||||
|
|
@ -349,6 +397,82 @@ DeviceDetails {
|
|||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
visible: editVpnDialog.connectCmd.length > 0
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Connect Command")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: connectCmdFieldEdit
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: editVpnDialog.connectCmd
|
||||
onTextChanged: editVpnDialog.connectCmd = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
visible: editVpnDialog.disconnectCmd.length > 0
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Disconnect Command")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: disconnectCmdFieldEdit
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: editVpnDialog.disconnectCmd
|
||||
onTextChanged: editVpnDialog.disconnectCmd = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -376,12 +500,23 @@ DeviceDetails {
|
|||
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i === editVpnDialog.editIndex) {
|
||||
providers.push({
|
||||
name: editVpnDialog.providerName,
|
||||
const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0;
|
||||
const newProvider = {
|
||||
displayName: editVpnDialog.displayName || editVpnDialog.interfaceName,
|
||||
interface: editVpnDialog.interfaceName,
|
||||
enabled: wasEnabled
|
||||
});
|
||||
enabled: wasEnabled,
|
||||
iface: editVpnDialog.interfaceName,
|
||||
name: editVpnDialog.providerName,
|
||||
connectCmd: hasCommands ? editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0) : undefined,
|
||||
disconnectCmd: hasCommands ? editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0) : undefined
|
||||
};
|
||||
|
||||
// Remove undefined properties
|
||||
if (!hasCommands) {
|
||||
delete newProvider.connectCmd;
|
||||
delete newProvider.disconnectCmd;
|
||||
}
|
||||
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,15 +159,38 @@ ColumnLayout {
|
|||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (modelData.enabled && VPN.connected)
|
||||
return qsTr("Connected");
|
||||
if (modelData.enabled && VPN.connecting)
|
||||
return qsTr("Connecting...");
|
||||
if (modelData.enabled)
|
||||
return qsTr("Enabled");
|
||||
if (!modelData.enabled)
|
||||
return qsTr("Disabled");
|
||||
|
||||
if (VPN.connecting)
|
||||
return qsTr("Connecting...");
|
||||
|
||||
switch (VPN.status.state) {
|
||||
case "connected":
|
||||
return qsTr("Connected");
|
||||
case "disconnected":
|
||||
return qsTr("Enabled");
|
||||
case "connecting":
|
||||
return qsTr("Connecting...");
|
||||
case "needs-auth":
|
||||
return qsTr("Auth required");
|
||||
case "error":
|
||||
return qsTr("Error");
|
||||
default:
|
||||
return qsTr("Enabled");
|
||||
}
|
||||
}
|
||||
color: {
|
||||
if (!modelData.enabled)
|
||||
return Colours.palette.m3outline;
|
||||
if (VPN.status.state === "connected")
|
||||
return Colours.palette.m3primary;
|
||||
if (VPN.status.state === "error")
|
||||
return Colours.palette.m3error;
|
||||
if (VPN.status.state === "needs-auth")
|
||||
return Colours.palette.m3tertiary;
|
||||
return Colours.palette.m3onSurface;
|
||||
}
|
||||
color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.enabled && VPN.connected ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
|
|
@ -271,6 +294,8 @@ ColumnLayout {
|
|||
property string providerName: ""
|
||||
property string displayName: ""
|
||||
property string interfaceName: ""
|
||||
property string connectCmd: ""
|
||||
property string disconnectCmd: ""
|
||||
|
||||
function showProviderSelection(): void {
|
||||
currentState = "selection";
|
||||
|
|
@ -286,6 +311,8 @@ ColumnLayout {
|
|||
providerName = providerType;
|
||||
displayName = defaultDisplayName;
|
||||
interfaceName = "";
|
||||
connectCmd = "";
|
||||
disconnectCmd = "";
|
||||
|
||||
if (currentState === "selection") {
|
||||
transitionToForm.start();
|
||||
|
|
@ -304,6 +331,8 @@ ColumnLayout {
|
|||
providerName = isObject ? (provider.name || "custom") : String(provider);
|
||||
displayName = isObject ? (provider.displayName || providerName) : providerName;
|
||||
interfaceName = isObject ? (provider.interface || "") : "";
|
||||
connectCmd = isObject && provider.connectCmd ? provider.connectCmd.join(" ") : "";
|
||||
disconnectCmd = isObject && provider.disconnectCmd ? provider.disconnectCmd.join(" ") : "";
|
||||
|
||||
currentState = "form";
|
||||
open();
|
||||
|
|
@ -491,7 +520,7 @@ ColumnLayout {
|
|||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("WireGuard (Custom)")
|
||||
text: qsTr("WireGuard")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
|
|
@ -499,6 +528,16 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Custom")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
vpnDialog.showAddForm("custom", "Custom VPN");
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -604,6 +643,82 @@ ColumnLayout {
|
|||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom")
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Connect Command (e.g., wg-quick up wg0)")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: connectCmdField
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: vpnDialog.connectCmd
|
||||
onTextChanged: vpnDialog.connectCmd = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom")
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Disconnect Command (e.g., wg-quick down wg0)")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: disconnectCmdField
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: vpnDialog.disconnectCmd
|
||||
onTextChanged: vpnDialog.disconnectCmd = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -620,19 +735,37 @@ ColumnLayout {
|
|||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Save")
|
||||
enabled: vpnDialog.interfaceName.length > 0
|
||||
enabled: {
|
||||
const hasCommands = vpnDialog.connectCmd.length > 0 || vpnDialog.disconnectCmd.length > 0;
|
||||
if (hasCommands) {
|
||||
return vpnDialog.interfaceName.length > 0 && vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0;
|
||||
}
|
||||
return vpnDialog.interfaceName.length > 0;
|
||||
}
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
const hasCommands = vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0;
|
||||
const newProvider = {
|
||||
name: vpnDialog.providerName,
|
||||
displayName: vpnDialog.displayName || vpnDialog.interfaceName,
|
||||
interface: vpnDialog.interfaceName
|
||||
enabled: false,
|
||||
interface: vpnDialog.interfaceName,
|
||||
name: vpnDialog.providerName
|
||||
};
|
||||
|
||||
if (hasCommands) {
|
||||
newProvider.connectCmd = vpnDialog.connectCmd.split(" ").filter(s => s.length > 0);
|
||||
newProvider.disconnectCmd = vpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0);
|
||||
}
|
||||
|
||||
if (vpnDialog.editIndex >= 0) {
|
||||
const oldProvider = Config.utilities.vpn.provider[vpnDialog.editIndex];
|
||||
if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) {
|
||||
newProvider.enabled = oldProvider.enabled;
|
||||
}
|
||||
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i === vpnDialog.editIndex) {
|
||||
providers.push(newProvider);
|
||||
|
|
|
|||
427
modules/controlcenter/notifications/NotificationsPane.qml
Normal file
427
modules/controlcenter/notifications/NotificationsPane.qml
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property bool notificationsExpire: Config.notifs.expire ?? true
|
||||
property string notificationsFullscreen: Config.notifs.fullscreen ?? "on"
|
||||
property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false
|
||||
property int notificationsDefaultExpireTimeout: Config.notifs.defaultExpireTimeout ?? 5000
|
||||
property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3
|
||||
|
||||
property int maxToasts: Config.utilities.maxToasts ?? 4
|
||||
property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off"
|
||||
property bool chargingChanged: Config.utilities.toasts.chargingChanged ?? true
|
||||
property bool gameModeChanged: Config.utilities.toasts.gameModeChanged ?? true
|
||||
property bool dndChanged: Config.utilities.toasts.dndChanged ?? true
|
||||
property bool audioOutputChanged: Config.utilities.toasts.audioOutputChanged ?? true
|
||||
property bool audioInputChanged: Config.utilities.toasts.audioInputChanged ?? true
|
||||
property bool capsLockChanged: Config.utilities.toasts.capsLockChanged ?? true
|
||||
property bool numLockChanged: Config.utilities.toasts.numLockChanged ?? true
|
||||
property bool kbLayoutChanged: Config.utilities.toasts.kbLayoutChanged ?? true
|
||||
property bool vpnChanged: Config.utilities.toasts.vpnChanged ?? true
|
||||
property bool nowPlaying: Config.utilities.toasts.nowPlaying ?? false
|
||||
|
||||
function saveConfig(): void {
|
||||
Config.notifs.expire = root.notificationsExpire;
|
||||
Config.notifs.fullscreen = root.notificationsFullscreen;
|
||||
Config.notifs.openExpanded = root.notificationsOpenExpanded;
|
||||
Config.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout;
|
||||
Config.notifs.groupPreviewNum = root.notificationsGroupPreviewNum;
|
||||
|
||||
Config.utilities.maxToasts = root.maxToasts;
|
||||
Config.utilities.toasts.fullscreen = root.toastsFullscreen;
|
||||
Config.utilities.toasts.chargingChanged = root.chargingChanged;
|
||||
Config.utilities.toasts.gameModeChanged = root.gameModeChanged;
|
||||
Config.utilities.toasts.dndChanged = root.dndChanged;
|
||||
Config.utilities.toasts.audioOutputChanged = root.audioOutputChanged;
|
||||
Config.utilities.toasts.audioInputChanged = root.audioInputChanged;
|
||||
Config.utilities.toasts.capsLockChanged = root.capsLockChanged;
|
||||
Config.utilities.toasts.numLockChanged = root.numLockChanged;
|
||||
Config.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged;
|
||||
Config.utilities.toasts.vpnChanged = root.vpnChanged;
|
||||
Config.utilities.toasts.nowPlaying = root.nowPlaying;
|
||||
|
||||
Config.save();
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
ClippingRectangle {
|
||||
id: notificationsClippingRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
color: "transparent"
|
||||
radius: notificationsBorder.innerRadius
|
||||
|
||||
Loader {
|
||||
id: notificationsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
|
||||
sourceComponent: notificationsContentComponent
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: notificationsBorder
|
||||
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notificationsContentComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: notificationsFlickable
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: notificationsLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: notificationsFlickable
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: notificationsLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 500
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Notifications")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SplitButtonRow {
|
||||
id: notificationsFullscreenSelector
|
||||
|
||||
function syncActiveItem(): void {
|
||||
active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem;
|
||||
}
|
||||
|
||||
label: qsTr("Show in fullscreen")
|
||||
menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem]
|
||||
|
||||
Component.onCompleted: syncActiveItem()
|
||||
|
||||
Connections {
|
||||
function onNotificationsFullscreenChanged(): void {
|
||||
notificationsFullscreenSelector.syncActiveItem();
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: notificationsFullscreenOffItem
|
||||
|
||||
text: qsTr("Off")
|
||||
icon: "notifications_off"
|
||||
activeText: qsTr("Off")
|
||||
onClicked: {
|
||||
root.notificationsFullscreen = "off";
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: notificationsFullscreenOnItem
|
||||
|
||||
text: qsTr("On")
|
||||
icon: "notifications"
|
||||
activeText: qsTr("On")
|
||||
onClicked: {
|
||||
root.notificationsFullscreen = "on";
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Expire automatically")
|
||||
checked: root.notificationsExpire
|
||||
onToggled: checked => {
|
||||
root.notificationsExpire = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Open expanded")
|
||||
checked: root.notificationsOpenExpanded
|
||||
onToggled: checked => {
|
||||
root.notificationsOpenExpanded = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SpinBoxRow {
|
||||
label: qsTr("Default timeout")
|
||||
value: root.notificationsDefaultExpireTimeout
|
||||
min: 1000
|
||||
max: 60000
|
||||
step: 500
|
||||
onValueModified: value => {
|
||||
root.notificationsDefaultExpireTimeout = value;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SpinBoxRow {
|
||||
label: qsTr("Group preview count")
|
||||
value: root.notificationsGroupPreviewNum
|
||||
min: 1
|
||||
max: 10
|
||||
step: 1
|
||||
onValueModified: value => {
|
||||
root.notificationsGroupPreviewNum = value;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Toast settings")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SplitButtonRow {
|
||||
id: toastFullscreenSelector
|
||||
|
||||
function syncActiveItem(): void {
|
||||
if (root.toastsFullscreen === "all") {
|
||||
active = toastFullscreenAllItem;
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.toastsFullscreen === "important") {
|
||||
active = toastFullscreenImportantItem;
|
||||
return;
|
||||
}
|
||||
|
||||
active = toastFullscreenOffItem;
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
z: expanded ? 100 : 0
|
||||
label: qsTr("Show in fullscreen")
|
||||
menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem]
|
||||
|
||||
Component.onCompleted: syncActiveItem()
|
||||
|
||||
Connections {
|
||||
function onToastsFullscreenChanged(): void {
|
||||
toastFullscreenSelector.syncActiveItem();
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: toastFullscreenOffItem
|
||||
|
||||
text: qsTr("Off")
|
||||
icon: "notifications_off"
|
||||
activeText: qsTr("Off")
|
||||
onClicked: {
|
||||
root.toastsFullscreen = "off";
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: toastFullscreenImportantItem
|
||||
|
||||
text: qsTr("Important")
|
||||
icon: "priority_high"
|
||||
activeText: qsTr("Important")
|
||||
onClicked: {
|
||||
root.toastsFullscreen = "important";
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: toastFullscreenAllItem
|
||||
|
||||
text: qsTr("On")
|
||||
icon: "notifications"
|
||||
activeText: qsTr("On")
|
||||
onClicked: {
|
||||
root.toastsFullscreen = "all";
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpinBoxRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Visible toasts")
|
||||
value: root.maxToasts
|
||||
min: 1
|
||||
max: 10
|
||||
step: 1
|
||||
onValueModified: value => {
|
||||
root.maxToasts = value;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 2
|
||||
columnSpacing: Appearance.spacing.normal
|
||||
rowSpacing: Appearance.spacing.normal
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Charging changes")
|
||||
checked: root.chargingChanged
|
||||
onToggled: checked => {
|
||||
root.chargingChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Game mode changes")
|
||||
checked: root.gameModeChanged
|
||||
onToggled: checked => {
|
||||
root.gameModeChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Do not disturb")
|
||||
checked: root.dndChanged
|
||||
onToggled: checked => {
|
||||
root.dndChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Audio output changes")
|
||||
checked: root.audioOutputChanged
|
||||
onToggled: checked => {
|
||||
root.audioOutputChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Audio input changes")
|
||||
checked: root.audioInputChanged
|
||||
onToggled: checked => {
|
||||
root.audioInputChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Caps lock changes")
|
||||
checked: root.capsLockChanged
|
||||
onToggled: checked => {
|
||||
root.capsLockChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Num lock changes")
|
||||
checked: root.numLockChanged
|
||||
onToggled: checked => {
|
||||
root.numLockChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Keyboard layout changes")
|
||||
checked: root.kbLayoutChanged
|
||||
onToggled: checked => {
|
||||
root.kbLayoutChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("VPN changes")
|
||||
checked: root.vpnChanged
|
||||
onToggled: checked => {
|
||||
root.vpnChanged = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Now playing")
|
||||
checked: root.nowPlaying
|
||||
onToggled: checked => {
|
||||
root.nowPlaying = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -358,32 +358,29 @@ Item {
|
|||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * heroCard.animatedUsage
|
||||
implicitWidth: parent.width * heroCard.animatedUsage
|
||||
color: Qt.alpha(heroCard.accentColor, 0.15)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
anchors.topMargin: Appearance.padding.normal
|
||||
anchors.bottomMargin: Appearance.padding.normal
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CardHeader {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.topMargin: Math.round(Appearance.padding.large * 1.2)
|
||||
|
||||
width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Appearance.spacing.normal
|
||||
icon: heroCard.icon
|
||||
title: heroCard.title
|
||||
accentColor: heroCard.accentColor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.fillWidth: true
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Math.round(Appearance.padding.large * 1.2)
|
||||
anchors.bottomMargin: Math.round(Appearance.padding.large * 1.3)
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Row {
|
||||
|
|
@ -404,21 +401,17 @@ Item {
|
|||
}
|
||||
|
||||
ProgressBar {
|
||||
width: parent.width * 0.5
|
||||
height: 6
|
||||
implicitWidth: parent.width * 0.5
|
||||
implicitHeight: 6
|
||||
value: heroCard.tempProgress
|
||||
fgColor: heroCard.accentColor
|
||||
bgColor: Qt.alpha(heroCard.accentColor, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: usageColumn
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
|
@ -426,6 +419,8 @@ Item {
|
|||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
id: usageLabel
|
||||
|
||||
anchors.right: parent.right
|
||||
text: heroCard.mainLabel
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
|
|
|
|||
|
|
@ -15,14 +15,17 @@ Shape {
|
|||
|
||||
required property Panels panels
|
||||
required property Item bar
|
||||
required property real borderThickness
|
||||
required property real borderRounding
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Config.border.thickness
|
||||
anchors.margins: root.borderThickness
|
||||
anchors.leftMargin: bar.implicitWidth
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
Osd.Background {
|
||||
wrapper: root.panels.osd // qmllint disable incompatible-type
|
||||
rounding: Config.border.rounding
|
||||
|
||||
startX: root.width - root.panels.session.width - root.panels.sidebar.width
|
||||
startY: (root.height - wrapper.height) / 2 - rounding
|
||||
|
|
@ -31,6 +34,7 @@ Shape {
|
|||
Notifications.Background {
|
||||
wrapper: root.panels.notifications // qmllint disable incompatible-type
|
||||
sidebar: sidebar
|
||||
rounding: Config.border.rounding
|
||||
|
||||
startX: root.width
|
||||
startY: 0
|
||||
|
|
@ -68,6 +72,7 @@ Shape {
|
|||
Utilities.Background {
|
||||
wrapper: root.panels.utilities // qmllint disable incompatible-type
|
||||
sidebar: sidebar
|
||||
rounding: root.borderRounding
|
||||
|
||||
startX: root.width
|
||||
startY: root.height
|
||||
|
|
@ -78,6 +83,7 @@ Shape {
|
|||
|
||||
wrapper: root.panels.sidebar // qmllint disable incompatible-type
|
||||
panels: root.panels
|
||||
rounding: root.borderRounding
|
||||
|
||||
startX: root.width
|
||||
startY: root.panels.notifications.height
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import QtQuick
|
|||
import QtQuick.Effects
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item bar
|
||||
required property real borderThickness
|
||||
required property real borderRounding
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
|
|
@ -36,9 +37,9 @@ Item {
|
|||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Config.border.thickness
|
||||
anchors.margins: root.borderThickness
|
||||
anchors.leftMargin: root.bar.implicitWidth
|
||||
radius: Config.border.rounding
|
||||
radius: root.borderRounding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,18 +26,34 @@ Variants {
|
|||
Exclusions {
|
||||
screen: scope.modelData
|
||||
bar: bar
|
||||
borderThickness: Config.border.thickness
|
||||
}
|
||||
|
||||
StyledWindow {
|
||||
id: win
|
||||
|
||||
readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false
|
||||
readonly property var monitor: Hypr.monitorFor(screen)
|
||||
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject?.specialWorkspace?.name.length ?? 0) > 0
|
||||
readonly property bool hasFullscreen: {
|
||||
if (hasSpecialWorkspace) {
|
||||
const specialName = monitor?.lastIpcObject?.specialWorkspace?.name;
|
||||
if (!specialName)
|
||||
return false;
|
||||
const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName);
|
||||
return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
|
||||
}
|
||||
return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
|
||||
}
|
||||
property real borderThickness: hasFullscreen ? 0 : Config.border.thickness
|
||||
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness
|
||||
property real borderRounding: hasFullscreen ? 0 : Config.border.rounding
|
||||
property real shadowOpacity: hasFullscreen ? 0 : 0.7
|
||||
readonly property int dragMaskPadding: {
|
||||
if (focusGrab.active || panels.popouts.isDetached)
|
||||
return 0;
|
||||
|
||||
const mon = Hypr.monitorFor(screen);
|
||||
if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0)
|
||||
if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace.lastIpcObject.windows > 0)
|
||||
return 0;
|
||||
|
||||
const thresholds = [];
|
||||
|
|
@ -56,6 +72,7 @@ Variants {
|
|||
screen: scope.modelData
|
||||
name: "drawers"
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
mask: Regions {
|
||||
|
|
@ -69,6 +86,30 @@ Variants {
|
|||
anchors.left: true
|
||||
anchors.right: true
|
||||
|
||||
Behavior on borderThickness {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on borderRounding {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on shadowOpacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
|
||||
|
|
@ -101,7 +142,7 @@ Variants {
|
|||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
blurMax: 15
|
||||
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
|
||||
shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity))
|
||||
}
|
||||
|
||||
// Border {
|
||||
|
|
@ -127,11 +168,11 @@ Variants {
|
|||
anchors.fill: parent
|
||||
anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers
|
||||
group: blobGroup
|
||||
radius: Config.border.rounding
|
||||
radius: win.borderRounding
|
||||
borderLeft: bar.implicitWidth - anchors.margins
|
||||
borderRight: Config.border.thickness - anchors.margins
|
||||
borderTop: Config.border.thickness - anchors.margins
|
||||
borderBottom: Config.border.thickness - anchors.margins
|
||||
borderRight: win.borderThickness - anchors.margins
|
||||
borderTop: win.borderThickness - anchors.margins
|
||||
borderBottom: win.borderThickness - anchors.margins
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
|
|
@ -140,6 +181,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.dashboard
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
deformAmount: 0.1
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +191,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.launcher
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
deformAmount: 0.1
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +201,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.sessionWrapper
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
deformAmount: 0.25
|
||||
x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth
|
||||
implicitWidth: panels.session.width
|
||||
|
|
@ -169,6 +213,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.sidebar
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
deformAmount: 0.05
|
||||
implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2
|
||||
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg]
|
||||
|
|
@ -181,6 +226,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.osdWrapper
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
deformAmount: 0.3
|
||||
x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth
|
||||
implicitWidth: panels.osd.width
|
||||
|
|
@ -192,6 +238,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.notifications
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
|
|
@ -200,6 +247,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.utilities
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
deformAmount: panels.sidebar.visible ? 0.1 : 0.15
|
||||
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
|
||||
topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius
|
||||
|
|
@ -211,6 +259,7 @@ Variants {
|
|||
blobGroup: blobGroup
|
||||
panel: panels.popouts
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
|
||||
x: bar.implicitWidth - (panels.popouts.isDetached ? -(win.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5)
|
||||
implicitWidth: panels.popouts.shownWidth
|
||||
|
|
@ -243,6 +292,8 @@ Variants {
|
|||
visibilities: visibilities
|
||||
panels: panels
|
||||
bar: bar
|
||||
borderThickness: win.borderLayoutThickness
|
||||
fullscreen: win.hasFullscreen
|
||||
|
||||
Panels {
|
||||
id: panels
|
||||
|
|
@ -250,6 +301,7 @@ Variants {
|
|||
screen: scope.modelData
|
||||
visibilities: visibilities
|
||||
bar: bar
|
||||
borderThickness: win.borderThickness
|
||||
|
||||
utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1
|
||||
|
||||
|
|
@ -290,6 +342,7 @@ Variants {
|
|||
popouts: panels.popouts
|
||||
|
||||
disabled: scope.barDisabled
|
||||
fullscreen: win.hasFullscreen
|
||||
|
||||
Component.onCompleted: Visibilities.bars.set(scope.modelData, this)
|
||||
}
|
||||
|
|
@ -301,11 +354,12 @@ Variants {
|
|||
required property BlobGroup blobGroup
|
||||
required property Item panel
|
||||
required property Item bar
|
||||
required property real borderThickness
|
||||
property real deformAmount: 0.15
|
||||
|
||||
group: panel.width > 0 && panel.height > 0 ? blobGroup : null
|
||||
x: panel.x + bar.implicitWidth
|
||||
y: panel.y + Config.border.thickness
|
||||
y: panel.y + borderThickness
|
||||
implicitWidth: panel.width
|
||||
implicitHeight: panel.height
|
||||
radius: Config.border.rounding
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import qs.modules.bar as Bar
|
||||
|
||||
Scope {
|
||||
|
|
@ -11,6 +10,7 @@ Scope {
|
|||
|
||||
required property ShellScreen screen
|
||||
required property Bar.BarWrapper bar
|
||||
required property real borderThickness
|
||||
|
||||
ExclusionZone {
|
||||
anchors.left: true
|
||||
|
|
@ -32,7 +32,7 @@ Scope {
|
|||
component ExclusionZone: StyledWindow {
|
||||
screen: root.screen
|
||||
name: "border-exclusion"
|
||||
exclusiveZone: Config.border.thickness
|
||||
exclusiveZone: root.borderThickness
|
||||
mask: Region {}
|
||||
implicitWidth: 1
|
||||
implicitHeight: 1
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ CustomMouseArea {
|
|||
required property DrawerVisibilities visibilities
|
||||
required property Panels panels
|
||||
required property Bar.BarWrapper bar
|
||||
required property real borderThickness
|
||||
required property bool fullscreen
|
||||
|
||||
property point dragStart
|
||||
property bool dashboardShortcutActive
|
||||
|
|
@ -22,7 +24,7 @@ CustomMouseArea {
|
|||
property bool utilitiesShortcutActive
|
||||
|
||||
function withinPanelHeight(panel: Item, x: real, y: real): bool {
|
||||
const panelY = Config.border.thickness + panel.y;
|
||||
const panelY = root.borderThickness + panel.y;
|
||||
return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding;
|
||||
}
|
||||
|
||||
|
|
@ -49,13 +51,16 @@ CustomMouseArea {
|
|||
}
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
if (fullscreen)
|
||||
return;
|
||||
if (event.x < bar.implicitWidth) {
|
||||
bar.handleWheel(event.y, event.angleDelta);
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons
|
||||
hoverEnabled: !fullscreen
|
||||
|
||||
onPressed: event => dragStart = Qt.point(event.x, event.y)
|
||||
onContainsMouseChanged: {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ Item {
|
|||
required property ShellScreen screen
|
||||
required property DrawerVisibilities visibilities
|
||||
required property Bar.BarWrapper bar
|
||||
required property real borderThickness
|
||||
|
||||
readonly property alias osd: osd
|
||||
readonly property alias osdWrapper: osdWrapper
|
||||
|
|
@ -33,7 +34,7 @@ Item {
|
|||
readonly property alias sidebar: sidebar
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Config.border.thickness
|
||||
anchors.margins: root.borderThickness
|
||||
anchors.leftMargin: bar.implicitWidth
|
||||
|
||||
Item {
|
||||
|
|
@ -123,7 +124,7 @@ Item {
|
|||
if (isDetached)
|
||||
return (root.height - nonAnimHeight) / 2;
|
||||
|
||||
const off = currentCenter - Config.border.thickness - nonAnimHeight / 2;
|
||||
const off = currentCenter - root.borderThickness - nonAnimHeight / 2;
|
||||
const diff = root.height - Math.floor(off + nonAnimHeight);
|
||||
if (diff < 0)
|
||||
return off + diff;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ import QtQuick
|
|||
import QtQuick.Shapes
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
required property var sidebar
|
||||
readonly property real rounding: Config.border.rounding
|
||||
required property real rounding
|
||||
readonly property bool flatten: wrapper.height < rounding * 2
|
||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
||||
|
||||
|
|
@ -31,14 +30,14 @@ ShapePath {
|
|||
relativeY: root.wrapper.height - root.roundingY * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.sidebar.notifsRoundingX
|
||||
relativeX: root.rounding
|
||||
relativeY: root.roundingY
|
||||
radiusX: root.sidebar.notifsRoundingX
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width
|
||||
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import QtQuick
|
|||
import QtQuick.Shapes
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
readonly property real rounding: Config.border.rounding
|
||||
required property real rounding
|
||||
readonly property bool flatten: wrapper.width < rounding * 2
|
||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ import QtQuick
|
|||
import QtQuick.Shapes
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
required property var panels
|
||||
|
||||
readonly property real rounding: Config.border.rounding
|
||||
required property real rounding
|
||||
|
||||
readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width
|
||||
readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ import QtQuick
|
|||
import QtQuick.Shapes
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
required property var sidebar
|
||||
readonly property real rounding: Config.border.rounding
|
||||
required property real rounding
|
||||
readonly property bool flatten: wrapper.height < rounding * 2
|
||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
||||
|
||||
|
|
|
|||
|
|
@ -141,8 +141,10 @@ StyledRect {
|
|||
roleValue: "vpn"
|
||||
delegate: Toggle {
|
||||
icon: "vpn_key"
|
||||
checked: VPN.connected
|
||||
checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error"
|
||||
enabled: !VPN.connecting
|
||||
toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error"
|
||||
inactiveOnColour: Colours.palette.m3onSurfaceVariant
|
||||
onClicked: VPN.toggle()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import QtQuick
|
|||
import Quickshell
|
||||
import Caelestia
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
||||
Item {
|
||||
|
|
@ -12,6 +13,16 @@ Item {
|
|||
readonly property int spacing: Appearance.spacing.small
|
||||
property bool flag
|
||||
|
||||
function shouldShowToast(toast: Toast): bool {
|
||||
if (!Notifs.hasFullscreen())
|
||||
return true;
|
||||
if (Config.utilities.toasts.fullscreen === "all")
|
||||
return true;
|
||||
if (Config.utilities.toasts.fullscreen === "important")
|
||||
return toast.type === Toast.Warning || toast.type === Toast.Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2
|
||||
implicitHeight: {
|
||||
let h = -spacing;
|
||||
|
|
@ -31,6 +42,8 @@ Item {
|
|||
const toasts = [];
|
||||
let count = 0;
|
||||
for (const toast of Toaster.toasts) {
|
||||
if (!root.shouldShowToast(toast))
|
||||
continue;
|
||||
toasts.push(toast);
|
||||
if (!toast.closed) {
|
||||
count++;
|
||||
|
|
|
|||
|
|
@ -126,8 +126,6 @@ in
|
|||
prePatch = ''
|
||||
substituteInPlace assets/pam.d/fprint \
|
||||
--replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so
|
||||
substituteInPlace shell.qml \
|
||||
--replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false'
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ qml_module(caelestia-internal
|
|||
hyprextras.hpp hyprextras.cpp
|
||||
logindmanager.hpp logindmanager.cpp
|
||||
sparklineitem.hpp sparklineitem.cpp
|
||||
visualiserbars.hpp visualiserbars.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
|
|
|
|||
198
plugin/src/Caelestia/Internal/visualiserbars.cpp
Normal file
198
plugin/src/Caelestia/Internal/visualiserbars.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
#include "visualiserbars.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <qbrush.h>
|
||||
#include <qpainter.h>
|
||||
#include <qpainterpath.h>
|
||||
#include <qpen.h>
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
VisualiserBars::VisualiserBars(QQuickItem* parent)
|
||||
: QQuickPaintedItem(parent) {
|
||||
setAntialiasing(true);
|
||||
}
|
||||
|
||||
void VisualiserBars::advance(qreal dt) {
|
||||
if (m_displayValues.isEmpty() || m_settled)
|
||||
return;
|
||||
|
||||
// dt is in seconds (from FrameAnimation.frameTime), convert to ms
|
||||
const qreal dtMs = dt * 1000.0;
|
||||
const qreal tau = m_animationDuration / 3.0;
|
||||
const qreal alpha = 1.0 - std::exp(-dtMs / tau);
|
||||
|
||||
bool allSettled = true;
|
||||
|
||||
for (qsizetype i = 0; i < m_displayValues.size(); ++i) {
|
||||
const double diff = m_targetValues[i] - m_displayValues[i];
|
||||
|
||||
if (std::abs(diff) > 0.001) {
|
||||
m_displayValues[i] += diff * alpha;
|
||||
allSettled = false;
|
||||
} else {
|
||||
m_displayValues[i] = m_targetValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
if (allSettled && !m_settled) {
|
||||
m_settled = true;
|
||||
emit settledChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void VisualiserBars::paint(QPainter* painter) {
|
||||
if (m_displayValues.isEmpty())
|
||||
return;
|
||||
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setPen(Qt::NoPen);
|
||||
|
||||
const qreal h = height();
|
||||
const qreal maxBarHeight = h * 0.4;
|
||||
|
||||
QLinearGradient gradient(0, h - maxBarHeight, 0, h);
|
||||
gradient.setColorAt(0, m_primaryColor);
|
||||
gradient.setColorAt(1, m_secondaryColor);
|
||||
painter->setBrush(gradient);
|
||||
|
||||
drawSide(painter, false);
|
||||
drawSide(painter, true);
|
||||
}
|
||||
|
||||
void VisualiserBars::drawSide(QPainter* painter, bool rightSide) {
|
||||
const qreal w = width();
|
||||
const qreal h = height();
|
||||
const auto count = m_displayValues.size();
|
||||
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
const qreal sideWidth = w * 0.4;
|
||||
const qreal slotWidth = sideWidth / static_cast<qreal>(count);
|
||||
const qreal barWidth = slotWidth - m_spacing;
|
||||
|
||||
if (barWidth <= 0)
|
||||
return;
|
||||
|
||||
const qreal sideOffset = rightSide ? w * 0.6 : 0;
|
||||
const qreal maxBarHeight = h * 0.4;
|
||||
|
||||
for (qsizetype i = 0; i < count; ++i) {
|
||||
const qsizetype valueIndex = rightSide ? i : (count - i - 1);
|
||||
const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0);
|
||||
const qreal barHeight = value * maxBarHeight;
|
||||
|
||||
if (barHeight <= 0)
|
||||
continue;
|
||||
|
||||
const qreal x = static_cast<qreal>(i) * slotWidth + sideOffset;
|
||||
const qreal y = h - barHeight;
|
||||
const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight });
|
||||
|
||||
QPainterPath path;
|
||||
path.moveTo(x, h);
|
||||
path.lineTo(x, y + r);
|
||||
|
||||
if (r > 0) {
|
||||
path.arcTo(x, y, r * 2, r * 2, 180, -90);
|
||||
path.lineTo(x + barWidth - r, y);
|
||||
path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
path.lineTo(x + barWidth, y);
|
||||
}
|
||||
|
||||
path.lineTo(x + barWidth, h);
|
||||
path.closeSubpath();
|
||||
|
||||
painter->drawPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
QVector<double> VisualiserBars::values() const {
|
||||
return m_targetValues;
|
||||
}
|
||||
|
||||
void VisualiserBars::setValues(const QVector<double>& values) {
|
||||
m_targetValues = values;
|
||||
|
||||
if (m_displayValues.size() != values.size()) {
|
||||
m_displayValues.resize(values.size(), 0.0);
|
||||
}
|
||||
|
||||
if (m_settled) {
|
||||
m_settled = false;
|
||||
emit settledChanged();
|
||||
}
|
||||
|
||||
emit valuesChanged();
|
||||
}
|
||||
|
||||
bool VisualiserBars::settled() const {
|
||||
return m_settled;
|
||||
}
|
||||
|
||||
QColor VisualiserBars::primaryColor() const {
|
||||
return m_primaryColor;
|
||||
}
|
||||
|
||||
void VisualiserBars::setPrimaryColor(const QColor& color) {
|
||||
if (m_primaryColor == color)
|
||||
return;
|
||||
m_primaryColor = color;
|
||||
emit primaryColorChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
QColor VisualiserBars::secondaryColor() const {
|
||||
return m_secondaryColor;
|
||||
}
|
||||
|
||||
void VisualiserBars::setSecondaryColor(const QColor& color) {
|
||||
if (m_secondaryColor == color)
|
||||
return;
|
||||
m_secondaryColor = color;
|
||||
emit secondaryColorChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
qreal VisualiserBars::rounding() const {
|
||||
return m_rounding;
|
||||
}
|
||||
|
||||
void VisualiserBars::setRounding(qreal rounding) {
|
||||
if (qFuzzyCompare(m_rounding, rounding))
|
||||
return;
|
||||
m_rounding = rounding;
|
||||
emit roundingChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
qreal VisualiserBars::spacing() const {
|
||||
return m_spacing;
|
||||
}
|
||||
|
||||
void VisualiserBars::setSpacing(qreal spacing) {
|
||||
if (qFuzzyCompare(m_spacing, spacing))
|
||||
return;
|
||||
m_spacing = spacing;
|
||||
emit spacingChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
int VisualiserBars::animationDuration() const {
|
||||
return m_animationDuration;
|
||||
}
|
||||
|
||||
void VisualiserBars::setAnimationDuration(int duration) {
|
||||
if (m_animationDuration == duration)
|
||||
return;
|
||||
m_animationDuration = duration;
|
||||
emit animationDurationChanged();
|
||||
}
|
||||
|
||||
} // namespace caelestia::internal
|
||||
72
plugin/src/Caelestia/Internal/visualiserbars.hpp
Normal file
72
plugin/src/Caelestia/Internal/visualiserbars.hpp
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qquickpainteditem.h>
|
||||
#include <qvector.h>
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
class VisualiserBars : public QQuickPaintedItem {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QVector<double> values READ values WRITE setValues NOTIFY valuesChanged)
|
||||
Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged)
|
||||
Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged)
|
||||
Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged)
|
||||
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
|
||||
Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged)
|
||||
Q_PROPERTY(bool settled READ settled NOTIFY settledChanged)
|
||||
|
||||
public:
|
||||
explicit VisualiserBars(QQuickItem* parent = nullptr);
|
||||
|
||||
void paint(QPainter* painter) override;
|
||||
|
||||
Q_INVOKABLE void advance(qreal dt);
|
||||
|
||||
[[nodiscard]] QVector<double> values() const;
|
||||
void setValues(const QVector<double>& values);
|
||||
|
||||
[[nodiscard]] QColor primaryColor() const;
|
||||
void setPrimaryColor(const QColor& color);
|
||||
|
||||
[[nodiscard]] QColor secondaryColor() const;
|
||||
void setSecondaryColor(const QColor& color);
|
||||
|
||||
[[nodiscard]] qreal rounding() const;
|
||||
void setRounding(qreal rounding);
|
||||
|
||||
[[nodiscard]] qreal spacing() const;
|
||||
void setSpacing(qreal spacing);
|
||||
|
||||
[[nodiscard]] int animationDuration() const;
|
||||
void setAnimationDuration(int duration);
|
||||
|
||||
[[nodiscard]] bool settled() const;
|
||||
|
||||
signals:
|
||||
void valuesChanged();
|
||||
void primaryColorChanged();
|
||||
void secondaryColorChanged();
|
||||
void roundingChanged();
|
||||
void spacingChanged();
|
||||
void animationDurationChanged();
|
||||
void settledChanged();
|
||||
|
||||
private:
|
||||
void drawSide(QPainter* painter, bool rightSide);
|
||||
|
||||
QVector<double> m_targetValues;
|
||||
QVector<double> m_displayValues;
|
||||
QColor m_primaryColor;
|
||||
QColor m_secondaryColor;
|
||||
qreal m_rounding = 0.0;
|
||||
qreal m_spacing = 0.0;
|
||||
int m_animationDuration = 200;
|
||||
bool m_settled = true;
|
||||
};
|
||||
|
||||
} // namespace caelestia::internal
|
||||
|
|
@ -21,6 +21,22 @@ Singleton {
|
|||
|
||||
property bool loaded
|
||||
|
||||
function hasFullscreen(): bool {
|
||||
for (const monitor of Hypr.monitors.values) {
|
||||
if (monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldShowPopup(): bool {
|
||||
if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar))
|
||||
return false;
|
||||
if (Config.notifs.fullscreen === "off" && hasFullscreen())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
onDndChanged: {
|
||||
if (!Config.utilities.toasts.dndChanged)
|
||||
return;
|
||||
|
|
@ -79,7 +95,7 @@ Singleton {
|
|||
notif.tracked = true;
|
||||
|
||||
const comp = notifComp.createObject(root, {
|
||||
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar),
|
||||
popup: root.shouldShowPopup(),
|
||||
notification: notif
|
||||
});
|
||||
root.list = [comp, ...root.list];
|
||||
|
|
|
|||
332
services/VPN.qml
332
services/VPN.qml
|
|
@ -10,6 +10,12 @@ Singleton {
|
|||
id: root
|
||||
|
||||
property bool connected: false
|
||||
property var status: ({
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "",
|
||||
authUrl: ""
|
||||
})
|
||||
|
||||
readonly property bool connecting: connectProc.running || disconnectProc.running
|
||||
readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false)
|
||||
|
|
@ -19,7 +25,7 @@ Singleton {
|
|||
}
|
||||
readonly property bool isCustomProvider: typeof providerInput === "object"
|
||||
readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
|
||||
readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : ""
|
||||
readonly property string interfaceName: isCustomProvider ? (providerInput.iface || "") : ""
|
||||
readonly property var currentConfig: {
|
||||
const name = providerName;
|
||||
const iface = interfaceName;
|
||||
|
|
@ -30,7 +36,7 @@ Singleton {
|
|||
return {
|
||||
connectCmd: custom.connectCmd || defaults.connectCmd,
|
||||
disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd,
|
||||
interface: custom.interface || defaults.interface,
|
||||
interface: custom.iface || defaults.interface,
|
||||
displayName: custom.displayName || defaults.displayName
|
||||
};
|
||||
}
|
||||
|
|
@ -53,7 +59,7 @@ Singleton {
|
|||
displayName: "Warp"
|
||||
},
|
||||
"netbird": {
|
||||
connectCmd: ["netbird", "up"],
|
||||
connectCmd: ["netbird", "up", "--no-browser"],
|
||||
disconnectCmd: ["netbird", "down"],
|
||||
interface: "wt0",
|
||||
displayName: "NetBird"
|
||||
|
|
@ -75,6 +81,10 @@ Singleton {
|
|||
}
|
||||
|
||||
function connect(): void {
|
||||
if (status.state === "needs-auth" && status.authUrl) {
|
||||
emitStatusToast(status);
|
||||
return;
|
||||
}
|
||||
if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) {
|
||||
connectProc.exec(root.currentConfig.connectCmd);
|
||||
}
|
||||
|
|
@ -87,11 +97,7 @@ Singleton {
|
|||
}
|
||||
|
||||
function toggle(): void {
|
||||
if (connected) {
|
||||
disconnect();
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
connected ? disconnect() : connect();
|
||||
}
|
||||
|
||||
function checkStatus(): void {
|
||||
|
|
@ -100,16 +106,221 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
onConnectedChanged: {
|
||||
function getStatusCommand(): var {
|
||||
switch (providerName) {
|
||||
case "tailscale":
|
||||
return ["tailscale", "status", "--json"];
|
||||
case "netbird":
|
||||
return ["netbird", "status", "--json"];
|
||||
case "warp":
|
||||
return ["warp-cli", "status"];
|
||||
case "wireguard":
|
||||
return ["ip", "link", "show"];
|
||||
default:
|
||||
return ["ip", "link", "show"];
|
||||
}
|
||||
}
|
||||
|
||||
function parseTailscaleStatus(output: string): var {
|
||||
const status = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "",
|
||||
authUrl: ""
|
||||
};
|
||||
|
||||
// Handle empty or whitespace-only output
|
||||
if (!output || output.trim().length === 0) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Check for common non-JSON states first
|
||||
if (output.includes("Logged out") || output.includes("Stopped") || output.includes("not running") || output.includes("Tailscale is not running")) {
|
||||
status.state = "disconnected";
|
||||
return status;
|
||||
}
|
||||
|
||||
// Try to parse as JSON
|
||||
try {
|
||||
const data = JSON.parse(output);
|
||||
const backendState = data.BackendState || "";
|
||||
|
||||
if (backendState === "Running") {
|
||||
status.connected = true;
|
||||
status.state = "connected";
|
||||
} else if (backendState === "Starting") {
|
||||
status.state = "connecting";
|
||||
} else if (backendState === "NeedsLogin" || backendState === "NeedsMachineAuth") {
|
||||
status.state = "needs-auth";
|
||||
status.reason = backendState === "NeedsLogin" ? "Login required" : "Machine authorization required";
|
||||
status.authUrl = data.AuthURL || "";
|
||||
}
|
||||
} catch (e) {
|
||||
// JSON parsing failed - treat as disconnected unless it looks like an error
|
||||
if (output.includes("error") || output.includes("Error") || output.includes("failed")) {
|
||||
status.state = "disconnected";
|
||||
status.reason = "Tailscale may not be running";
|
||||
} else {
|
||||
status.state = "disconnected";
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function parseNetBirdStatus(output: string): var {
|
||||
const status = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "",
|
||||
authUrl: ""
|
||||
};
|
||||
try {
|
||||
const data = JSON.parse(output);
|
||||
const mgmtConnected = data.management?.connected;
|
||||
const signalConnected = data.signal?.connected;
|
||||
|
||||
if (mgmtConnected && signalConnected) {
|
||||
status.connected = true;
|
||||
status.state = "connected";
|
||||
} else if (data.management?.error) {
|
||||
const error = data.management.error;
|
||||
if (error.includes("auth") || error.includes("login")) {
|
||||
status.state = "needs-auth";
|
||||
status.reason = "Authentication required";
|
||||
} else {
|
||||
status.reason = error;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
status.state = "error";
|
||||
status.reason = "Failed to parse status";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function parseWarpStatus(output: string): var {
|
||||
const status = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "",
|
||||
authUrl: ""
|
||||
};
|
||||
|
||||
if (output.includes("Connected")) {
|
||||
status.connected = true;
|
||||
status.state = "connected";
|
||||
} else if (output.includes("Connecting")) {
|
||||
status.state = "connecting";
|
||||
} else if (output.includes("Unable") || output.includes("Registration Missing") || output.includes("registration") || output.includes("register")) {
|
||||
status.state = "needs-auth";
|
||||
status.reason = "WARP registration required";
|
||||
} else if (!output.includes("Disconnected")) {
|
||||
status.state = "error";
|
||||
status.reason = "Unknown WARP status";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function parseWireGuardStatus(output: string): var {
|
||||
const status = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "",
|
||||
authUrl: ""
|
||||
};
|
||||
const iface = root.currentConfig?.interface || "";
|
||||
|
||||
if (iface && output.includes(iface + ":")) {
|
||||
status.connected = true;
|
||||
status.state = "connected";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function parseStatusOutput(output: string): var {
|
||||
switch (providerName) {
|
||||
case "tailscale":
|
||||
return parseTailscaleStatus(output);
|
||||
case "netbird":
|
||||
return parseNetBirdStatus(output);
|
||||
case "warp":
|
||||
return parseWarpStatus(output);
|
||||
case "wireguard":
|
||||
default:
|
||||
return parseWireGuardStatus(output);
|
||||
}
|
||||
}
|
||||
|
||||
function extractAuthUrl(text: string): string {
|
||||
const urlMatch = text.match(/(https?:\/\/[^\s]+)/);
|
||||
return urlMatch ? urlMatch[1] : "";
|
||||
}
|
||||
|
||||
function createAuthStatus(authUrl: string): var {
|
||||
return {
|
||||
connected: false,
|
||||
state: "needs-auth",
|
||||
reason: "Authentication required",
|
||||
authUrl: authUrl
|
||||
};
|
||||
}
|
||||
|
||||
function updateStatus(newStatus: var): void {
|
||||
const oldState = status.state;
|
||||
if (newStatus.state === "needs-auth" && !newStatus.authUrl && status.authUrl) {
|
||||
newStatus.authUrl = status.authUrl;
|
||||
}
|
||||
status = newStatus;
|
||||
root.connected = newStatus.connected;
|
||||
|
||||
if (oldState !== newStatus.state) {
|
||||
emitStatusToast(newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
function emitStatusToast(statusObj: var): void {
|
||||
if (!Config.utilities.toasts.vpnChanged)
|
||||
return;
|
||||
|
||||
const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN";
|
||||
if (connected) {
|
||||
|
||||
switch (statusObj.state) {
|
||||
case "connected":
|
||||
Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key");
|
||||
} else {
|
||||
break;
|
||||
case "disconnected":
|
||||
if (status.connected) {
|
||||
Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off");
|
||||
}
|
||||
break;
|
||||
case "needs-auth":
|
||||
const authMsg = statusObj.reason || "Authentication required";
|
||||
Toaster.toast(qsTr("VPN authentication required"), qsTr("%1: %2").arg(displayName).arg(authMsg), "vpn_lock");
|
||||
break;
|
||||
case "error":
|
||||
if (status.state === "connected" || status.state === "connecting" || status.state === "needs-auth") {
|
||||
const errMsg = statusObj.reason || "Unknown error";
|
||||
Toaster.toast(qsTr("VPN error"), qsTr("%1: %2").arg(displayName).arg(errMsg), "error");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (providerName === "warp" && status.state === "needs-auth" && status.reason.includes("registration")) {
|
||||
warpRegisterProc.exec(["warp-cli", "registration", "new"]);
|
||||
}
|
||||
}
|
||||
|
||||
onProviderNameChanged: {
|
||||
status = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "",
|
||||
authUrl: ""
|
||||
};
|
||||
root.connected = false;
|
||||
statusCheckTimer.start();
|
||||
}
|
||||
|
||||
Component.onCompleted: root.enabled && statusCheckTimer.start()
|
||||
|
|
@ -127,7 +338,7 @@ Singleton {
|
|||
Process {
|
||||
id: statusProc
|
||||
|
||||
command: ["ip", "link", "show"]
|
||||
command: root.getStatusCommand()
|
||||
// qmllint disable incompatible-type
|
||||
environment: ({
|
||||
// qmllint enable incompatible-type
|
||||
|
|
@ -136,8 +347,38 @@ Singleton {
|
|||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const iface = root.currentConfig ? root.currentConfig.interface : "";
|
||||
root.connected = iface && text.includes(iface + ":");
|
||||
const newStatus = root.parseStatusOutput(text);
|
||||
root.updateStatus(newStatus);
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim().length > 0) {
|
||||
if (text.includes("doesn't appear to be running") || text.includes("failed to connect to local tailscaled") || text.includes("daemon is not running") || text.includes("not running") && (text.includes("netbird") || text.includes("warp"))) {
|
||||
let cmd = "sudo systemctl start ";
|
||||
switch (root.providerName) {
|
||||
case "tailscale":
|
||||
cmd += "tailscaled";
|
||||
break;
|
||||
case "netbird":
|
||||
cmd += "netbird";
|
||||
break;
|
||||
case "warp":
|
||||
cmd += "warp-svc";
|
||||
break;
|
||||
default:
|
||||
cmd += root.providerName + "d";
|
||||
break;
|
||||
}
|
||||
const errorStatus = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: `Service not running (run: ${cmd})`,
|
||||
authUrl: ""
|
||||
};
|
||||
root.updateStatus(errorStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -145,12 +386,59 @@ Singleton {
|
|||
Process {
|
||||
id: connectProc
|
||||
|
||||
onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters
|
||||
onExited: exitCode => { // qmllint disable signal-handler-parameters
|
||||
if (exitCode !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.providerName === "tailscale") {
|
||||
Qt.callLater(() => {
|
||||
if (root.status.state !== "needs-auth") {
|
||||
statusCheckTimer.start();
|
||||
}
|
||||
});
|
||||
} else if (root.status.state !== "needs-auth") {
|
||||
statusCheckTimer.start();
|
||||
}
|
||||
}
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const authUrl = root.extractAuthUrl(data);
|
||||
if (authUrl) {
|
||||
root.updateStatus(root.createAuthStatus(authUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const error = text.trim();
|
||||
if (error && !error.includes("[#]") && !error.includes("already exists")) {
|
||||
console.warn("VPN connection error:", error);
|
||||
|
||||
if (error.includes("Access denied") || error.includes("checkprefs access denied")) {
|
||||
const errorStatus = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "Permission denied. Run in terminal: sudo tailscale set --operator=$USER",
|
||||
authUrl: ""
|
||||
};
|
||||
root.updateStatus(errorStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error.includes("Unknown device type") || error.includes("Protocol not supported")) {
|
||||
const errorStatus = {
|
||||
connected: false,
|
||||
state: "disconnected",
|
||||
reason: "WireGuard module not loaded. Run: sudo modprobe wireguard",
|
||||
authUrl: ""
|
||||
};
|
||||
root.updateStatus(errorStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
const authUrl = root.extractAuthUrl(error);
|
||||
|
||||
if (authUrl) {
|
||||
root.updateStatus(root.createAuthStatus(authUrl));
|
||||
} else if (error.includes("already exists")) {
|
||||
root.connected = true;
|
||||
}
|
||||
|
|
@ -172,6 +460,16 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: warpRegisterProc
|
||||
|
||||
onExited: exitCode => { // qmllint disable signal-handler-parameters
|
||||
if (exitCode === 0) {
|
||||
statusCheckTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: statusCheckTimer
|
||||
|
||||
|
|
|
|||
|
|
@ -121,8 +121,8 @@ Singleton {
|
|||
humidity: json.current.relative_humidity_2m,
|
||||
windSpeed: json.current.wind_speed_10m,
|
||||
isDay: json.current.is_day,
|
||||
sunrise: json.daily.sunrise[0],
|
||||
sunset: json.daily.sunset[0]
|
||||
sunrise: json.daily.sunrise[0].replace("T", " "),
|
||||
sunset: json.daily.sunset[0].replace("T", " ")
|
||||
};
|
||||
|
||||
const forecastList = [];
|
||||
|
|
@ -218,7 +218,6 @@ Singleton {
|
|||
target: Config.services
|
||||
}
|
||||
|
||||
// Refresh current location hourly
|
||||
Timer {
|
||||
interval: 3600000 // 1 hour
|
||||
running: true
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue