diff --git a/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml b/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml index 3093916b..12536527 100644 --- a/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/AudioInputDetail.qml @@ -122,6 +122,21 @@ Rectangle { contentHeight: audioColumn.height clip: true + property int maxPinnedInputs: 3 + + function normalizePinList(value) { + if (Array.isArray(value)) + return value.filter(v => v) + if (typeof value === "string" && value.length > 0) + return [value] + return [] + } + + function getPinnedInputs() { + const pins = SettingsData.audioInputDevicePins || {} + return normalizePinList(pins["preferredInput"]) + } + Column { id: audioColumn width: parent.width @@ -133,16 +148,20 @@ Rectangle { const nodes = Pipewire.nodes.values.filter(node => { return node.audio && !node.isSink && !node.isStream; }); - const pins = SettingsData.audioInputDevicePins || {}; - const pinnedName = pins["preferredInput"]; + const pinnedList = audioContent.getPinnedInputs(); let sorted = [...nodes]; sorted.sort((a, b) => { // Pinned device first - if (a.name === pinnedName && b.name !== pinnedName) - return -1; - if (b.name === pinnedName && a.name !== pinnedName) - return 1; + const aPinnedIndex = pinnedList.indexOf(a.name) + const bPinnedIndex = pinnedList.indexOf(b.name) + if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { + if (aPinnedIndex === -1) + return 1 + if (bPinnedIndex === -1) + return -1 + return aPinnedIndex - bPinnedIndex + } // Then active device if (a === AudioService.source && b !== AudioService.source) return -1; @@ -224,7 +243,7 @@ Rectangle { height: 28 radius: height / 2 color: { - const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name); return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05); } @@ -237,7 +256,7 @@ Rectangle { name: "push_pin" size: 16 color: { - const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name); return isThisDevicePinned ? Theme.primary : Theme.surfaceText; } anchors.verticalCenter: parent.verticalCenter @@ -245,12 +264,12 @@ Rectangle { StyledText { text: { - const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name); return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin"); } font.pixelSize: Theme.fontSizeSmall color: { - const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name); return isThisDevicePinned ? Theme.primary : Theme.surfaceText; } anchors.verticalCenter: parent.verticalCenter @@ -261,16 +280,24 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {})); - const isCurrentlyPinned = pins["preferredInput"] === modelData.name; + const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {})) + let pinnedList = audioContent.normalizePinList(pins["preferredInput"]) + const pinIndex = pinnedList.indexOf(modelData.name) - if (isCurrentlyPinned) { - delete pins["preferredInput"]; + if (pinIndex !== -1) { + pinnedList.splice(pinIndex, 1) } else { - pins["preferredInput"] = modelData.name; + pinnedList.unshift(modelData.name) + if (pinnedList.length > audioContent.maxPinnedInputs) + pinnedList = pinnedList.slice(0, audioContent.maxPinnedInputs) } - SettingsData.set("audioInputDevicePins", pins); + if (pinnedList.length > 0) + pins["preferredInput"] = pinnedList + else + delete pins["preferredInput"] + + SettingsData.set("audioInputDevicePins", pins) } } } diff --git a/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml b/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml index dac15bd6..0e033310 100644 --- a/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml @@ -132,6 +132,21 @@ Rectangle { contentHeight: audioColumn.height clip: true + property int maxPinnedOutputs: 3 + + function normalizePinList(value) { + if (Array.isArray(value)) + return value.filter(v => v) + if (typeof value === "string" && value.length > 0) + return [value] + return [] + } + + function getPinnedOutputs() { + const pins = SettingsData.audioOutputDevicePins || {} + return normalizePinList(pins["preferredOutput"]) + } + Column { id: audioColumn width: parent.width @@ -143,16 +158,20 @@ Rectangle { const nodes = Pipewire.nodes.values.filter(node => { return node.audio && node.isSink && !node.isStream; }); - const pins = SettingsData.audioOutputDevicePins || {}; - const pinnedName = pins["preferredOutput"]; + const pinnedList = audioContent.getPinnedOutputs(); let sorted = [...nodes]; sorted.sort((a, b) => { // Pinned device first - if (a.name === pinnedName && b.name !== pinnedName) - return -1; - if (b.name === pinnedName && a.name !== pinnedName) - return 1; + const aPinnedIndex = pinnedList.indexOf(a.name) + const bPinnedIndex = pinnedList.indexOf(b.name) + if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { + if (aPinnedIndex === -1) + return 1 + if (bPinnedIndex === -1) + return -1 + return aPinnedIndex - bPinnedIndex + } // Then active device if (a === AudioService.sink && b !== AudioService.sink) return -1; @@ -236,7 +255,7 @@ Rectangle { height: 28 radius: height / 2 color: { - const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name); return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05); } @@ -249,7 +268,7 @@ Rectangle { name: "push_pin" size: 16 color: { - const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name); return isThisDevicePinned ? Theme.primary : Theme.surfaceText; } anchors.verticalCenter: parent.verticalCenter @@ -257,12 +276,12 @@ Rectangle { StyledText { text: { - const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name); return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin"); } font.pixelSize: Theme.fontSizeSmall color: { - const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; + const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name); return isThisDevicePinned ? Theme.primary : Theme.surfaceText; } anchors.verticalCenter: parent.verticalCenter @@ -273,16 +292,24 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {})); - const isCurrentlyPinned = pins["preferredOutput"] === modelData.name; + const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {})) + let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]) + const pinIndex = pinnedList.indexOf(modelData.name) - if (isCurrentlyPinned) { - delete pins["preferredOutput"]; + if (pinIndex !== -1) { + pinnedList.splice(pinIndex, 1) } else { - pins["preferredOutput"] = modelData.name; + pinnedList.unshift(modelData.name) + if (pinnedList.length > audioContent.maxPinnedOutputs) + pinnedList = pinnedList.slice(0, audioContent.maxPinnedOutputs) } - SettingsData.set("audioOutputDevicePins", pins); + if (pinnedList.length > 0) + pins["preferredOutput"] = pinnedList + else + delete pins["preferredOutput"] + + SettingsData.set("audioOutputDevicePins", pins) } } } diff --git a/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml b/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml index 012fed50..4d198192 100644 --- a/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/BluetoothDetail.qml @@ -150,6 +150,21 @@ Rectangle { contentHeight: bluetoothColumn.height clip: true + property int maxPinnedDevices: 3 + + function normalizePinList(value) { + if (Array.isArray(value)) + return value.filter(v => v) + if (typeof value === "string" && value.length > 0) + return [value] + return [] + } + + function getPinnedDevices() { + const pins = SettingsData.bluetoothDevicePins || {} + return normalizePinList(pins["preferredDevice"]) + } + Column { id: bluetoothColumn width: parent.width @@ -162,14 +177,18 @@ Rectangle { if (!BluetoothService.adapter || !BluetoothService.adapter.devices) return [] - const pins = SettingsData.bluetoothDevicePins || {} - const pinnedAddr = pins["preferredDevice"] + const pinnedList = bluetoothContent.getPinnedDevices() let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))] devices.sort((a, b) => { // Pinned device first - if (a.address === pinnedAddr && b.address !== pinnedAddr) return -1 - if (b.address === pinnedAddr && a.address !== pinnedAddr) return 1 + const aPinnedIndex = pinnedList.indexOf(a.address) + const bPinnedIndex = pinnedList.indexOf(b.address) + if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { + if (aPinnedIndex === -1) return 1 + if (bPinnedIndex === -1) return -1 + return aPinnedIndex - bPinnedIndex + } // Then connected devices if (a.connected && !b.connected) return -1 if (!a.connected && b.connected) return 1 @@ -302,7 +321,7 @@ Rectangle { height: 28 radius: height / 2 color: { - const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address + const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address) return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05) } @@ -315,7 +334,7 @@ Rectangle { name: "push_pin" size: 16 color: { - const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address + const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address) return isThisDevicePinned ? Theme.primary : Theme.surfaceText } anchors.verticalCenter: parent.verticalCenter @@ -323,12 +342,12 @@ Rectangle { StyledText { text: { - const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address + const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address) return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin") } font.pixelSize: Theme.fontSizeSmall color: { - const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address + const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address) return isThisDevicePinned ? Theme.primary : Theme.surfaceText } anchors.verticalCenter: parent.verticalCenter @@ -340,14 +359,22 @@ Rectangle { cursorShape: Qt.PointingHandCursor onClicked: { const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {})) - const isCurrentlyPinned = pins["preferredDevice"] === modelData.address + let pinnedList = bluetoothContent.normalizePinList(pins["preferredDevice"]) + const pinIndex = pinnedList.indexOf(modelData.address) - if (isCurrentlyPinned) { - delete pins["preferredDevice"] + if (pinIndex !== -1) { + pinnedList.splice(pinIndex, 1) } else { - pins["preferredDevice"] = modelData.address + pinnedList.unshift(modelData.address) + if (pinnedList.length > bluetoothContent.maxPinnedDevices) + pinnedList = pinnedList.slice(0, bluetoothContent.maxPinnedDevices) } + if (pinnedList.length > 0) + pins["preferredDevice"] = pinnedList + else + delete pins["preferredDevice"] + SettingsData.set("bluetoothDevicePins", pins) } } diff --git a/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml b/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml index 67ea3975..f85c190b 100644 --- a/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/NetworkDetail.qml @@ -463,20 +463,39 @@ Rectangle { contentHeight: wifiColumn.height clip: true + property int maxPinnedNetworks: 3 + + function normalizePinList(value) { + if (Array.isArray(value)) + return value.filter(v => v) + if (typeof value === "string" && value.length > 0) + return [value] + return [] + } + + function getPinnedNetworks() { + const pins = SettingsData.wifiNetworkPins || {} + return normalizePinList(pins["preferredWifi"]) + } + property var frozenNetworks: [] property bool menuOpen: false property var sortedNetworks: { const ssid = NetworkService.currentWifiSSID; const networks = NetworkService.wifiNetworks; - const pins = SettingsData.wifiNetworkPins || {}; - const pinnedSSID = pins["preferredWifi"]; + const pinnedList = getPinnedNetworks() let sorted = [...networks]; sorted.sort((a, b) => { - if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID) - return -1; - if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID) - return 1; + const aPinnedIndex = pinnedList.indexOf(a.ssid) + const bPinnedIndex = pinnedList.indexOf(b.ssid) + if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { + if (aPinnedIndex === -1) + return 1 + if (bPinnedIndex === -1) + return -1 + return aPinnedIndex - bPinnedIndex + } if (a.ssid === ssid) return -1; if (b.ssid === ssid) @@ -625,7 +644,7 @@ Rectangle { height: 28 radius: height / 2 color: { - const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; + const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid); return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05); } @@ -638,7 +657,7 @@ Rectangle { name: "push_pin" size: 16 color: { - const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; + const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid); return isThisNetworkPinned ? Theme.primary : Theme.surfaceText; } anchors.verticalCenter: parent.verticalCenter @@ -646,12 +665,12 @@ Rectangle { StyledText { text: { - const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; + const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid); return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin"); } font.pixelSize: Theme.fontSizeSmall color: { - const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; + const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid); return isThisNetworkPinned ? Theme.primary : Theme.surfaceText; } anchors.verticalCenter: parent.verticalCenter @@ -662,16 +681,24 @@ Rectangle { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); - const isCurrentlyPinned = pins["preferredWifi"] === modelData.ssid; + const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})) + let pinnedList = wifiContent.normalizePinList(pins["preferredWifi"]) + const pinIndex = pinnedList.indexOf(modelData.ssid) - if (isCurrentlyPinned) { - delete pins["preferredWifi"]; + if (pinIndex !== -1) { + pinnedList.splice(pinIndex, 1) } else { - pins["preferredWifi"] = modelData.ssid; + pinnedList.unshift(modelData.ssid) + if (pinnedList.length > wifiContent.maxPinnedNetworks) + pinnedList = pinnedList.slice(0, wifiContent.maxPinnedNetworks) } - SettingsData.set("wifiNetworkPins", pins); + if (pinnedList.length > 0) + pins["preferredWifi"] = pinnedList + else + delete pins["preferredWifi"] + + SettingsData.set("wifiNetworkPins", pins) } } } diff --git a/quickshell/Modules/Settings/NetworkTab.qml b/quickshell/Modules/Settings/NetworkTab.qml index aa81b710..e45874f6 100644 --- a/quickshell/Modules/Settings/NetworkTab.qml +++ b/quickshell/Modules/Settings/NetworkTab.qml @@ -15,6 +15,7 @@ Item { property string expandedVpnUuid: "" property string expandedWifiSsid: "" property string expandedEthDevice: "" + property int maxPinnedWifiNetworks: 3 Component.onCompleted: { NetworkService.addRef(); @@ -30,6 +31,40 @@ Item { vpnFileBrowserLoader.item.open(); } + function normalizePinList(value) { + if (Array.isArray(value)) + return value.filter(v => v) + if (typeof value === "string" && value.length > 0) + return [value] + return [] + } + + function getPinnedWifiNetworks() { + const pins = SettingsData.wifiNetworkPins || {} + return normalizePinList(pins["preferredWifi"]) + } + + function toggleWifiPin(ssid) { + const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})) + let pinnedList = normalizePinList(pins["preferredWifi"]) + const pinIndex = pinnedList.indexOf(ssid) + + if (pinIndex !== -1) { + pinnedList.splice(pinIndex, 1) + } else { + pinnedList.unshift(ssid) + if (pinnedList.length > maxPinnedWifiNetworks) + pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks) + } + + if (pinnedList.length > 0) + pins["preferredWifi"] = pinnedList + else + delete pins["preferredWifi"] + + SettingsData.set("wifiNetworkPins", pins) + } + LazyLoader { id: vpnFileBrowserLoader active: false @@ -1025,15 +1060,19 @@ Item { model: { const ssid = NetworkService.currentWifiSSID; const networks = NetworkService.wifiNetworks || []; - const pins = SettingsData.wifiNetworkPins || {}; - const pinnedSSID = pins["preferredWifi"]; + const pinnedList = networkTab.getPinnedWifiNetworks(); let sorted = [...networks]; sorted.sort((a, b) => { - if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID) - return -1; - if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID) - return 1; + const aPinnedIndex = pinnedList.indexOf(a.ssid) + const bPinnedIndex = pinnedList.indexOf(b.ssid) + if (aPinnedIndex !== -1 || bPinnedIndex !== -1) { + if (aPinnedIndex === -1) + return 1 + if (bPinnedIndex === -1) + return -1 + return aPinnedIndex - bPinnedIndex + } if (a.ssid === ssid) return -1; if (b.ssid === ssid) @@ -1049,7 +1088,7 @@ Item { required property int index readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID - readonly property bool isPinned: (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid + readonly property bool isPinned: networkTab.getPinnedWifiNetworks().includes(modelData.ssid) readonly property bool isExpanded: networkTab.expandedWifiSsid === modelData.ssid width: parent.width @@ -1224,13 +1263,7 @@ Item { buttonSize: 28 iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText onClicked: { - const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); - if (isPinned) { - delete pins["preferredWifi"]; - } else { - pins["preferredWifi"] = modelData.ssid; - } - SettingsData.set("wifiNetworkPins", pins); + networkTab.toggleWifiPin(modelData.ssid) } }