mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-30 00:12:50 -05:00
Add bluetooth codec switching, via @Vantesh
This commit is contained in:
363
Modules/ControlCenter/Bluetooth/BluetoothCodecSelector.qml
Normal file
363
Modules/ControlCenter/Bluetooth/BluetoothCodecSelector.qml
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var device: null
|
||||||
|
property bool modalVisible: false
|
||||||
|
property var parentItem
|
||||||
|
property var availableCodecs: []
|
||||||
|
property string currentCodec: ""
|
||||||
|
property bool isLoading: false
|
||||||
|
property bool parsingTargetCard: false
|
||||||
|
|
||||||
|
function show(bluetoothDevice) {
|
||||||
|
device = bluetoothDevice;
|
||||||
|
isLoading = true;
|
||||||
|
availableCodecs = [];
|
||||||
|
currentCodec = "";
|
||||||
|
visible = true;
|
||||||
|
modalVisible = true;
|
||||||
|
queryCodecs();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
focusScope.forceActiveFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modalVisible = false;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
visible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryCodecs() {
|
||||||
|
if (!device)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
codecQueryProcess.cardName = BluetoothService.getCardName(device);
|
||||||
|
codecQueryProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCodec(profileName) {
|
||||||
|
if (!device || isLoading)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
codecSwitchProcess.cardName = BluetoothService.getCardName(device);
|
||||||
|
codecSwitchProcess.profile = profileName;
|
||||||
|
codecSwitchProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCodecLine(line) {
|
||||||
|
if (!codecQueryProcess.cardName)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
if (line.includes(`Name: ${codecQueryProcess.cardName}`)) {
|
||||||
|
parsingTargetCard = true;
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
if (parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) {
|
||||||
|
parsingTargetCard = false;
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
if (parsingTargetCard) {
|
||||||
|
if (line.startsWith("Active Profile:")) {
|
||||||
|
let profile = line.split(": ")[1] || "";
|
||||||
|
let activeCodec = availableCodecs.find((c) => {
|
||||||
|
return c.profile === profile;
|
||||||
|
});
|
||||||
|
if (activeCodec)
|
||||||
|
currentCodec = activeCodec.name;
|
||||||
|
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
if (line.includes("codec") && line.includes("available: yes")) {
|
||||||
|
let parts = line.split(": ");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
let profile = parts[0].trim();
|
||||||
|
let description = parts[1];
|
||||||
|
let codecMatch = description.match(/codec ([^\)\s]+)/i);
|
||||||
|
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN";
|
||||||
|
let codecInfo = BluetoothService.getCodecInfo(codecName);
|
||||||
|
if (codecInfo && !availableCodecs.some((c) => {
|
||||||
|
return c.profile === profile;
|
||||||
|
})) {
|
||||||
|
let newCodecs = availableCodecs.slice();
|
||||||
|
newCodecs.push({
|
||||||
|
"name": codecInfo.name,
|
||||||
|
"profile": profile,
|
||||||
|
"description": codecInfo.description,
|
||||||
|
"qualityColor": codecInfo.qualityColor
|
||||||
|
});
|
||||||
|
availableCodecs = newCodecs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
z: 2000
|
||||||
|
opacity: modalVisible ? 1 : 0
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: focusScope
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: root.visible
|
||||||
|
enabled: root.visible
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.hide()
|
||||||
|
onWheel: (wheel) => {
|
||||||
|
return wheel.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 320
|
||||||
|
height: Math.min(contentColumn.implicitHeight + Theme.spacingL * 2, 400)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: modalVisible ? 1 : 0
|
||||||
|
scale: modalVisible ? 1 : 0.9
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: device ? BluetoothService.getDeviceIcon(device) : "headset"
|
||||||
|
size: Theme.iconSize + 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: device ? (device.name || device.deviceName) : ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Audio Codec Selection"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: isLoading ? "Loading codecs..." : `Current: ${currentCodec}`
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: isLoading ? Theme.primary : Theme.surfaceTextMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: !isLoading
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: availableCodecs
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (modelData.name === currentCodec)
|
||||||
|
return Theme.surfaceContainerHigh;
|
||||||
|
else if (codecMouseArea.containsMouse)
|
||||||
|
return Theme.surfaceHover;
|
||||||
|
else
|
||||||
|
return "transparent";
|
||||||
|
}
|
||||||
|
border.color: "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 6
|
||||||
|
height: 6
|
||||||
|
radius: 3
|
||||||
|
color: modelData.qualityColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: modelData.name === currentCodec ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: modelData.name === currentCodec ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.description
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceTextMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "check"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: modelData.name === currentCodec
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: codecMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: modelData.name !== currentCodec && !isLoading
|
||||||
|
onClicked: {
|
||||||
|
selectCodec(modelData.profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: codecQueryProcess
|
||||||
|
|
||||||
|
property string cardName: ""
|
||||||
|
|
||||||
|
command: ["pactl", "list", "cards"]
|
||||||
|
onExited: function(exitCode, exitStatus) {
|
||||||
|
isLoading = false;
|
||||||
|
if (exitCode !== 0)
|
||||||
|
console.warn("Failed to query codecs:", exitCode);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
return parseCodecLine(data.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: codecSwitchProcess
|
||||||
|
|
||||||
|
property string cardName: ""
|
||||||
|
property string profile: ""
|
||||||
|
|
||||||
|
command: ["pactl", "set-card-profile", cardName, profile]
|
||||||
|
onExited: function(exitCode, exitStatus) {
|
||||||
|
isLoading = false;
|
||||||
|
if (exitCode === 0) {
|
||||||
|
queryCodecs();
|
||||||
|
ToastService.showToast("Codec switched successfully", ToastService.levelInfo);
|
||||||
|
Qt.callLater(root.hide);
|
||||||
|
} else {
|
||||||
|
ToastService.showToast("Failed to switch codec", ToastService.levelError);
|
||||||
|
console.warn("Failed to switch codec:", exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,25 +14,26 @@ Rectangle {
|
|||||||
property var deviceData: null
|
property var deviceData: null
|
||||||
property bool menuVisible: false
|
property bool menuVisible: false
|
||||||
property var parentItem
|
property var parentItem
|
||||||
|
property var codecSelector
|
||||||
|
|
||||||
function show(x, y) {
|
function show(x, y) {
|
||||||
const menuWidth = 160
|
const menuWidth = 160;
|
||||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
|
||||||
let finalX = x - menuWidth / 2
|
let finalX = x - menuWidth / 2;
|
||||||
let finalY = y
|
let finalY = y;
|
||||||
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth))
|
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth));
|
||||||
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight))
|
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight));
|
||||||
root.x = finalX
|
root.x = finalX;
|
||||||
root.y = finalY
|
root.y = finalY;
|
||||||
root.visible = true
|
root.visible = true;
|
||||||
root.menuVisible = true
|
root.menuVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
root.menuVisible = false
|
root.menuVisible = false;
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
root.visible = false
|
root.visible = false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
@@ -40,8 +41,7 @@ Rectangle {
|
|||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.popupBackground()
|
color: Theme.popupBackground()
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
border.width: 1
|
||||||
z: 1000
|
z: 1000
|
||||||
opacity: menuVisible ? 1 : 0
|
opacity: menuVisible ? 1 : 0
|
||||||
@@ -69,10 +69,7 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 32
|
height: 32
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -81,8 +78,7 @@ Rectangle {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: root.deviceData
|
name: root.deviceData && root.deviceData.connected ? "link_off" : "link"
|
||||||
&& root.deviceData.connected ? "link_off" : "link"
|
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
@@ -90,13 +86,13 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: root.deviceData
|
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
|
||||||
&& root.deviceData.connected ? "Disconnect" : "Connect"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Normal
|
font.weight: Font.Normal
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -108,12 +104,11 @@ Rectangle {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
if (root.deviceData) {
|
if (root.deviceData) {
|
||||||
if (root.deviceData.connected)
|
if (root.deviceData.connected)
|
||||||
root.deviceData.disconnect()
|
root.deviceData.disconnect();
|
||||||
else
|
else
|
||||||
BluetoothService.connectDeviceWithTrust(
|
BluetoothService.connectDeviceWithTrust(root.deviceData);
|
||||||
root.deviceData)
|
|
||||||
}
|
}
|
||||||
root.hide()
|
root.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +117,62 @@ Rectangle {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: codecArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
visible: root.deviceData && BluetoothService.isAudioDevice(root.deviceData) && root.deviceData.connected
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "high_quality"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
opacity: 0.7
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Audio Codec"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: codecArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
codecSelector.show(root.deviceData);
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -135,19 +185,16 @@ Rectangle {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 1
|
height: 1
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 32
|
height: 32
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r,
|
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -170,6 +217,7 @@ Rectangle {
|
|||||||
font.weight: Font.Normal
|
font.weight: Font.Normal
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -180,9 +228,9 @@ Rectangle {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (root.deviceData)
|
if (root.deviceData)
|
||||||
root.deviceData.forget()
|
root.deviceData.forget();
|
||||||
|
|
||||||
root.hide()
|
root.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,8 +239,11 @@ Rectangle {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -200,6 +251,7 @@ Rectangle {
|
|||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.emphasizedEasing
|
easing.type: Theme.emphasizedEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
@@ -207,5 +259,7 @@ Rectangle {
|
|||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.emphasizedEasing
|
easing.type: Theme.emphasizedEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ Item {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: mainColumn
|
id: mainColumn
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
@@ -39,19 +40,30 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
sourceComponent: availableComponent
|
sourceComponent: availableComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BluetoothContextMenu {
|
BluetoothContextMenu {
|
||||||
id: bluetoothContextMenuWindow
|
id: bluetoothContextMenuWindow
|
||||||
|
|
||||||
|
parentItem: bluetoothTab
|
||||||
|
codecSelector: codecSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothCodecSelector {
|
||||||
|
id: codecSelector
|
||||||
|
|
||||||
parentItem: bluetoothTab
|
parentItem: bluetoothTab
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: bluetoothContextMenuWindow.visible
|
visible: bluetoothContextMenuWindow.visible || codecSelector.visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
bluetoothContextMenuWindow.hide()
|
bluetoothContextMenuWindow.hide();
|
||||||
|
codecSelector.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -60,29 +72,36 @@ Item {
|
|||||||
width: bluetoothContextMenuWindow.width
|
width: bluetoothContextMenuWindow.width
|
||||||
height: bluetoothContextMenuWindow.height
|
height: bluetoothContextMenuWindow.height
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: toggleComponent
|
id: toggleComponent
|
||||||
|
|
||||||
BluetoothToggle {
|
BluetoothToggle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: pairedComponent
|
id: pairedComponent
|
||||||
|
|
||||||
PairedDevicesList {
|
PairedDevicesList {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: availableComponent
|
id: availableComponent
|
||||||
|
|
||||||
AvailableDevicesList {
|
AvailableDevicesList {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,4 +156,70 @@ Singleton {
|
|||||||
device.trusted = true
|
device.trusted = true
|
||||||
device.connect()
|
device.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCardName(device) {
|
||||||
|
if (!device)
|
||||||
|
return ""
|
||||||
|
return "bluez_card." + device.address.replace(/:/g, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAudioDevice(device) {
|
||||||
|
if (!device)
|
||||||
|
return false
|
||||||
|
let icon = getDeviceIcon(device)
|
||||||
|
return icon === "headset" || icon === "speaker"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodecInfo(codecName) {
|
||||||
|
let codec = codecName.replace(/-/g, "_").toUpperCase()
|
||||||
|
|
||||||
|
let codecMap = {
|
||||||
|
"LDAC": {
|
||||||
|
name: "LDAC",
|
||||||
|
description: "Highest quality • Higher battery usage",
|
||||||
|
qualityColor: "#4CAF50"
|
||||||
|
},
|
||||||
|
"APTX_HD": {
|
||||||
|
name: "aptX HD",
|
||||||
|
description: "High quality • Balanced battery",
|
||||||
|
qualityColor: "#FF9800"
|
||||||
|
},
|
||||||
|
"APTX": {
|
||||||
|
name: "aptX",
|
||||||
|
description: "Good quality • Low latency",
|
||||||
|
qualityColor: "#FF9800"
|
||||||
|
},
|
||||||
|
"AAC": {
|
||||||
|
name: "AAC",
|
||||||
|
description: "Balanced quality and battery",
|
||||||
|
qualityColor: "#2196F3"
|
||||||
|
},
|
||||||
|
"SBC_XQ": {
|
||||||
|
name: "SBC-XQ",
|
||||||
|
description: "Enhanced SBC • Better compatibility",
|
||||||
|
qualityColor: "#2196F3"
|
||||||
|
},
|
||||||
|
"SBC": {
|
||||||
|
name: "SBC",
|
||||||
|
description: "Basic quality • Universal compatibility",
|
||||||
|
qualityColor: "#9E9E9E"
|
||||||
|
},
|
||||||
|
"MSBC": {
|
||||||
|
name: "mSBC",
|
||||||
|
description: "Modified SBC • Optimized for speech",
|
||||||
|
qualityColor: "#9E9E9E"
|
||||||
|
},
|
||||||
|
"CVSD": {
|
||||||
|
name: "CVSD",
|
||||||
|
description: "Basic speech codec • Legacy compatibility",
|
||||||
|
qualityColor: "#9E9E9E"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return codecMap[codec] || {
|
||||||
|
name: codecName,
|
||||||
|
description: "Unknown codec",
|
||||||
|
qualityColor: "#9E9E9E"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ Singleton {
|
|||||||
// WiFi details
|
// WiFi details
|
||||||
property string currentWifiSSID: ""
|
property string currentWifiSSID: ""
|
||||||
property int wifiSignalStrength: 0
|
property int wifiSignalStrength: 0
|
||||||
property int wifiRSSI: -100 // dBm value, -100 = no signal
|
|
||||||
property var wifiNetworks: []
|
property var wifiNetworks: []
|
||||||
property var savedConnections: []
|
property var savedConnections: []
|
||||||
property var wifiSignalIcon: {
|
property var wifiSignalIcon: {
|
||||||
@@ -39,21 +38,7 @@ Singleton {
|
|||||||
return "wifi_off"
|
return "wifi_off"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use RSSI if available
|
// Use nmcli signal strength percentage
|
||||||
if (wifiRSSI > -100) {
|
|
||||||
// Use RSSI-based thresholds (dBm values)
|
|
||||||
if (wifiRSSI >= -50) {
|
|
||||||
return "wifi" // Excellent: -50 dBm and better
|
|
||||||
}
|
|
||||||
if (wifiRSSI >= -65) {
|
|
||||||
return "wifi_2_bar" // Good: -65 to -50 dBm
|
|
||||||
}
|
|
||||||
if (wifiRSSI >= -80) {
|
|
||||||
return "wifi_1_bar" // Fair: -80 to -65 dBm
|
|
||||||
}
|
|
||||||
return "signal_wifi_0_bar" // Poor: worse than -80 dBm
|
|
||||||
}
|
|
||||||
// Fall back to nmcli signal strength percentage
|
|
||||||
if (wifiSignalStrength >= 75) {
|
if (wifiSignalStrength >= 75) {
|
||||||
return "wifi"
|
return "wifi"
|
||||||
}
|
}
|
||||||
@@ -333,8 +318,6 @@ Singleton {
|
|||||||
|
|
||||||
if (wifiInterface) {
|
if (wifiInterface) {
|
||||||
root.wifiInterface = wifiInterface
|
root.wifiInterface = wifiInterface
|
||||||
// Try to parse RSSI now that we have the interface
|
|
||||||
wirelessFileView.parseWifiRSSI()
|
|
||||||
getWifiDevicePath.command = ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.NetworkManager.GetDeviceByIpIface", wifiInterface]
|
getWifiDevicePath.command = ["gdbus", "call", "--system", "--dest", "org.freedesktop.NetworkManager", "--object-path", "/org/freedesktop/NetworkManager", "--method", "org.freedesktop.NetworkManager.GetDeviceByIpIface", wifiInterface]
|
||||||
getWifiDevicePath.running = true
|
getWifiDevicePath.running = true
|
||||||
} else {
|
} else {
|
||||||
@@ -380,8 +363,6 @@ Singleton {
|
|||||||
if (root.wifiConnected) {
|
if (root.wifiConnected) {
|
||||||
getWifiIP.running = true
|
getWifiIP.running = true
|
||||||
getCurrentWifiInfo.running = true
|
getCurrentWifiInfo.running = true
|
||||||
// Parse RSSI now that we're connected
|
|
||||||
wirelessFileView.parseWifiRSSI()
|
|
||||||
// Ensure SSID is resolved even if scan output lacks ACTIVE marker
|
// Ensure SSID is resolved even if scan output lacks ACTIVE marker
|
||||||
if (root.currentWifiSSID === "") {
|
if (root.currentWifiSSID === "") {
|
||||||
if (root.wifiConnectionUuid) {
|
if (root.wifiConnectionUuid) {
|
||||||
@@ -395,7 +376,6 @@ Singleton {
|
|||||||
root.wifiIP = ""
|
root.wifiIP = ""
|
||||||
root.currentWifiSSID = ""
|
root.currentWifiSSID = ""
|
||||||
root.wifiSignalStrength = 0
|
root.wifiSignalStrength = 0
|
||||||
root.wifiRSSI = -100
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,59 +414,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: wirelessFileView
|
|
||||||
path: "/proc/net/wireless"
|
|
||||||
watchChanges: wifiConnected && wifiInterface !== ""
|
|
||||||
|
|
||||||
function parseWifiRSSI() {
|
|
||||||
if (!root.wifiInterface || !wifiConnected) {
|
|
||||||
root.wifiRSSI = -100
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = wirelessFileView.text()
|
|
||||||
if (!content) {
|
|
||||||
root.wifiRSSI = -100
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = content.trim().split('\n')
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes(root.wifiInterface + ":")) {
|
|
||||||
const parts = line.trim().split(/\s+/)
|
|
||||||
if (parts.length >= 4) {
|
|
||||||
// Level is the 4th column (signal strength in dBm)
|
|
||||||
const level = parseFloat(parts[3])
|
|
||||||
if (!isNaN(level)) {
|
|
||||||
root.wifiRSSI = Math.round(level)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root.wifiRSSI = -100 // Interface not found
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to parse /proc/net/wireless:", e)
|
|
||||||
root.wifiRSSI = -100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
parseWifiRSSI()
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileChanged: {
|
|
||||||
console.log("FILE CHANGE")
|
|
||||||
wirelessFileView.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadFailed: function(error) {
|
|
||||||
console.warn("Failed to read /proc/net/wireless:", error)
|
|
||||||
root.wifiRSSI = -100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateActiveConnections() {
|
function updateActiveConnections() {
|
||||||
getActiveConnections.running = true
|
getActiveConnections.running = true
|
||||||
|
|||||||
Reference in New Issue
Block a user