mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
266 lines
8.7 KiB
QML
266 lines
8.7 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Bluetooth
|
|
import qs.Commons
|
|
import qs.Modules.MainScreen
|
|
import qs.Services.Networking
|
|
import qs.Services.UI
|
|
import qs.Widgets
|
|
|
|
SmartPanel {
|
|
id: root
|
|
|
|
preferredWidth: Math.round(440 * Style.uiScaleRatio)
|
|
preferredHeight: Math.round(500 * Style.uiScaleRatio)
|
|
|
|
panelContent: Rectangle {
|
|
color: Color.transparent
|
|
|
|
// Calculate content height based on header + devices list (or minimum for empty states)
|
|
property real headerHeight: headerRow.implicitHeight + Style.marginM * 2
|
|
property real devicesHeight: devicesList.implicitHeight
|
|
property real calculatedHeight: (devicesHeight !== 0) ? (headerHeight + devicesHeight + Style.marginL * 2 + Style.marginM) : (280 * Style.uiScaleRatio)
|
|
property real contentPreferredHeight: (BluetoothService.adapter && BluetoothService.adapter.enabled) ? Math.min(root.preferredHeight, calculatedHeight) : Math.min(root.preferredHeight, 280 * Style.uiScaleRatio)
|
|
|
|
ColumnLayout {
|
|
id: mainColumn
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginL
|
|
spacing: Style.marginM
|
|
|
|
// Header
|
|
NBox {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: headerRow.implicitHeight + Style.marginM * 2
|
|
|
|
RowLayout {
|
|
id: headerRow
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginM
|
|
spacing: Style.marginM
|
|
|
|
NIcon {
|
|
icon: "bluetooth"
|
|
pointSize: Style.fontSizeXXL
|
|
color: Color.mPrimary
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("bluetooth.panel.title")
|
|
pointSize: Style.fontSizeL
|
|
font.weight: Style.fontWeightBold
|
|
color: Color.mOnSurface
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NToggle {
|
|
id: bluetoothSwitch
|
|
checked: BluetoothService.enabled
|
|
onToggled: function(checked) { BluetoothService.setBluetoothEnabled(checked); }
|
|
baseSize: Style.baseWidgetSize * 0.65
|
|
}
|
|
|
|
// Discoverability toggle (advertising)
|
|
NIconButton {
|
|
enabled: BluetoothService.enabled
|
|
icon: BluetoothService.discoverable ? "broadcast" : "broadcast-off"
|
|
tooltipText: I18n.tr("bluetooth.panel.discoverable")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: {
|
|
BluetoothService.setDiscoverable(!BluetoothService.discoverable);
|
|
}
|
|
}
|
|
|
|
NIconButton {
|
|
enabled: BluetoothService.enabled
|
|
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
|
|
tooltipText: I18n.tr("tooltips.refresh-devices")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: {
|
|
if (BluetoothService.adapter) {
|
|
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
|
|
}
|
|
}
|
|
}
|
|
|
|
NIconButton {
|
|
icon: "close"
|
|
tooltipText: I18n.tr("tooltips.close")
|
|
baseSize: Style.baseWidgetSize * 0.8
|
|
onClicked: {
|
|
root.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adapter not available of disabled
|
|
NBox {
|
|
id: disabledBox
|
|
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
// Center the content within this rectangle
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: Style.marginM
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
NIcon {
|
|
icon: "bluetooth-off"
|
|
pointSize: 48
|
|
color: Color.mOnSurfaceVariant
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("bluetooth.panel.disabled")
|
|
pointSize: Style.fontSizeL
|
|
color: Color.mOnSurfaceVariant
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("bluetooth.panel.enable-message")
|
|
pointSize: Style.fontSizeS
|
|
color: Color.mOnSurfaceVariant
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
Item {
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
}
|
|
|
|
NScrollView {
|
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
horizontalPolicy: ScrollBar.AlwaysOff
|
|
verticalPolicy: ScrollBar.AsNeeded
|
|
clip: true
|
|
contentWidth: availableWidth
|
|
|
|
ColumnLayout {
|
|
id: devicesList
|
|
width: parent.width
|
|
spacing: Style.marginM
|
|
|
|
// Connected devices
|
|
BluetoothDevicesList {
|
|
label: I18n.tr("bluetooth.panel.connected-devices")
|
|
property var items: {
|
|
if (!BluetoothService.adapter || !Bluetooth.devices)
|
|
return [];
|
|
var filtered = Bluetooth.devices.values.filter(function(dev) { return dev && !dev.blocked && dev.connected; });
|
|
filtered = BluetoothService.dedupeDevices(filtered);
|
|
return BluetoothService.sortDevices(filtered);
|
|
}
|
|
model: items
|
|
visible: items.length > 0
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// Paired devices
|
|
BluetoothDevicesList {
|
|
label: I18n.tr("bluetooth.panel.paired-devices")
|
|
tooltipText: I18n.tr("tooltips.connect-disconnect-devices")
|
|
property var items: {
|
|
if (!BluetoothService.adapter || !Bluetooth.devices)
|
|
return [];
|
|
var filtered = Bluetooth.devices.values.filter(function(dev) { return dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted); });
|
|
filtered = BluetoothService.dedupeDevices(filtered);
|
|
return BluetoothService.sortDevices(filtered);
|
|
}
|
|
model: items
|
|
visible: items.length > 0
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// Available devices (for pairing)
|
|
BluetoothDevicesList {
|
|
label: I18n.tr("bluetooth.panel.available-devices")
|
|
property var items: {
|
|
if (!BluetoothService.adapter || !Bluetooth.devices)
|
|
return [];
|
|
var filtered = Bluetooth.devices.values.filter(function(dev) { return dev && !dev.blocked && !dev.paired && !dev.trusted; });
|
|
filtered = BluetoothService.dedupeDevices(filtered);
|
|
return BluetoothService.sortDevices(filtered);
|
|
}
|
|
model: items
|
|
visible: items.length > 0
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// Fallback - No devices, scanning
|
|
NBox {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: columnScanning.implicitHeight + Style.marginM * 2
|
|
visible: {
|
|
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
|
|
return false;
|
|
}
|
|
|
|
var availableCount = Bluetooth.devices.values.filter(function(dev) {
|
|
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
|
}).length;
|
|
return (availableCount === 0);
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: columnScanning
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginM
|
|
|
|
spacing: Style.marginM
|
|
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: Style.marginXS
|
|
|
|
NIcon {
|
|
icon: "refresh"
|
|
pointSize: Style.fontSizeXXL * 1.5
|
|
color: Color.mPrimary
|
|
|
|
RotationAnimation on rotation {
|
|
running: true
|
|
loops: Animation.Infinite
|
|
from: 0
|
|
to: 360
|
|
duration: Style.animationSlow * 4
|
|
}
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("bluetooth.panel.scanning")
|
|
pointSize: Style.fontSizeL
|
|
color: Color.mOnSurface
|
|
}
|
|
}
|
|
|
|
NText {
|
|
text: I18n.tr("bluetooth.panel.pairing-mode")
|
|
pointSize: Style.fontSizeM
|
|
color: Color.mOnSurfaceVariant
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|