mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
refactor: mega refactoring of a bunch of things
This commit is contained in:
@@ -369,11 +369,10 @@ PanelWindow {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (launcher.isVisible) {
|
||||
if (launcher.isVisible)
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
}
|
||||
// Handle keyboard shortcuts
|
||||
Keys.onPressed: function(event) {
|
||||
@@ -458,17 +457,6 @@ PanelWindow {
|
||||
onTextEdited: {
|
||||
searchDebounceTimer.restart();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: launcher
|
||||
function onVisibleChanged() {
|
||||
if (launcher.visible) {
|
||||
searchField.forceActiveFocus();
|
||||
} else {
|
||||
searchField.clearFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
Keys.onPressed: function(event) {
|
||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count && text.length > 0) {
|
||||
// Launch first app when typing in search field
|
||||
@@ -481,14 +469,23 @@ PanelWindow {
|
||||
}
|
||||
launcher.hide();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
|
||||
(event.key === Qt.Key_Left && viewMode === "grid") ||
|
||||
(event.key === Qt.Key_Right && viewMode === "grid") ||
|
||||
((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || (event.key === Qt.Key_Left && viewMode === "grid") || (event.key === Qt.Key_Right && viewMode === "grid") || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
// Pass navigation keys and enter (when not searching) to main handler
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onVisibleChanged() {
|
||||
if (launcher.visible)
|
||||
searchField.forceActiveFocus();
|
||||
else
|
||||
searchField.clearFocus();
|
||||
}
|
||||
|
||||
target: launcher
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Category filter and view mode controls
|
||||
|
||||
@@ -16,9 +16,9 @@ PanelWindow {
|
||||
|
||||
visible: calendarVisible
|
||||
onVisibleChanged: {
|
||||
if (visible && CalendarService) {
|
||||
if (visible && CalendarService)
|
||||
CalendarService.loadCurrentMonth();
|
||||
}
|
||||
|
||||
}
|
||||
implicitWidth: 480
|
||||
implicitHeight: 600
|
||||
|
||||
@@ -42,9 +42,7 @@ Rectangle {
|
||||
interval: 2000
|
||||
running: {
|
||||
// Run when no active player (for cache clearing) OR when playing (for position updates)
|
||||
return (!activePlayer) ||
|
||||
(activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing &&
|
||||
activePlayer.length > 0 && !progressMouseArea.isSeeking);
|
||||
return (!activePlayer) || (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking);
|
||||
}
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
|
||||
@@ -3,17 +3,19 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: clipboardHistory
|
||||
// Don't hide the interface, just show toast
|
||||
|
||||
id: clipboardHistoryModal
|
||||
|
||||
property bool isVisible: false
|
||||
property int totalCount: 0
|
||||
property var activeTheme: Theme
|
||||
property bool showClearConfirmation: false
|
||||
property var clipboardEntries: []
|
||||
|
||||
property string searchText: ""
|
||||
|
||||
function updateFilteredModel() {
|
||||
@@ -33,7 +35,7 @@ DankModal {
|
||||
|
||||
}
|
||||
}
|
||||
clipboardHistory.totalCount = filteredClipboardModel.count;
|
||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -44,14 +46,14 @@ DankModal {
|
||||
}
|
||||
|
||||
function show() {
|
||||
clipboardHistory.isVisible = true;
|
||||
clipboardHistoryModal.isVisible = true;
|
||||
refreshClipboard();
|
||||
console.log("ClipboardHistory: Opening and refreshing");
|
||||
console.log("ClipboardHistoryModal: Opening and refreshing");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clipboardHistory.isVisible = false;
|
||||
clipboardHistory.searchText = "";
|
||||
clipboardHistoryModal.isVisible = false;
|
||||
clipboardHistoryModal.searchText = "";
|
||||
cleanupTempFiles();
|
||||
}
|
||||
|
||||
@@ -68,8 +70,8 @@ DankModal {
|
||||
const entryId = entry.split('\t')[0];
|
||||
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`];
|
||||
copyProcess.running = true;
|
||||
console.log("ClipboardHistory: Entry copied, hiding interface");
|
||||
hide();
|
||||
console.log("ClipboardHistoryModal: Entry copied, showing toast");
|
||||
ToastService.showInfo("Copied to clipboard");
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
@@ -116,11 +118,217 @@ DankModal {
|
||||
width: 650
|
||||
height: 550
|
||||
keyboardFocus: "ondemand"
|
||||
|
||||
onBackgroundClicked: {
|
||||
hide();
|
||||
}
|
||||
|
||||
// Clear confirmation dialog
|
||||
DankModal {
|
||||
id: clearConfirmDialog
|
||||
|
||||
visible: showClearConfirmation
|
||||
width: 350
|
||||
height: 150
|
||||
keyboardFocus: "ondemand"
|
||||
onBackgroundClicked: {
|
||||
showClearConfirmation = false;
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Clear All History?"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "This will permanently delete all clipboard history."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelClearButton.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Text {
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelClearButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: showClearConfirmation = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: confirmClearButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.9) : Theme.error
|
||||
|
||||
Text {
|
||||
text: "Clear All"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmClearButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
clearAll();
|
||||
showClearConfirmation = false;
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Data models
|
||||
ListModel {
|
||||
id: clipboardModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredClipboardModel
|
||||
}
|
||||
|
||||
// Processes
|
||||
Process {
|
||||
id: clipboardProcess
|
||||
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
clipboardModel.clear();
|
||||
const lines = text.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim().length > 0)
|
||||
clipboardModel.append({
|
||||
"entry": line
|
||||
});
|
||||
|
||||
}
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: copyProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.error("Copy failed with exit code:", exitCode);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deleteProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
refreshClipboard();
|
||||
else
|
||||
console.error("Delete failed with exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: clearProcess
|
||||
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
clipboardModel.clear();
|
||||
filteredClipboardModel.clear();
|
||||
totalCount = 0;
|
||||
} else {
|
||||
console.error("Clear failed with exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cleanupProcess
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
console.log("ClipboardHistoryModal: IPC open() called");
|
||||
clipboardHistoryModal.show();
|
||||
return "CLIPBOARD_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close() {
|
||||
console.log("ClipboardHistoryModal: IPC close() called");
|
||||
clipboardHistoryModal.hide();
|
||||
return "CLIPBOARD_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
console.log("ClipboardHistoryModal: IPC toggle() called");
|
||||
clipboardHistoryModal.toggle();
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "clipboard"
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
@@ -152,6 +360,7 @@ DankModal {
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -160,8 +369,8 @@ DankModal {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "delete"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconName: "delete_sweep"
|
||||
iconSize: Theme.iconSize
|
||||
iconColor: Theme.error
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: {
|
||||
@@ -176,33 +385,39 @@ DankModal {
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: hide()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Search field
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width
|
||||
placeholderText: "Search clipboard history..."
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
onTextChanged: {
|
||||
clipboardHistory.searchText = text;
|
||||
clipboardHistoryModal.searchText = text;
|
||||
updateFilteredModel();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: clipboardHistory
|
||||
function onOpened() {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
searchField.clearFocus();
|
||||
}
|
||||
|
||||
target: clipboardHistoryModal
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clipboard entries list
|
||||
// Clipboard entries list
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - 110
|
||||
@@ -219,270 +434,158 @@ DankModal {
|
||||
|
||||
ListView {
|
||||
id: clipboardListView
|
||||
|
||||
width: parent.availableWidth
|
||||
model: filteredClipboardModel
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
delegate: Rectangle {
|
||||
width: clipboardListView.width
|
||||
height: Math.max(60, contentText.contentHeight + Theme.spacingL)
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
|
||||
Text {
|
||||
text: "No clipboard entries found"
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: filteredClipboardModel.count === 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
color: "transparent"
|
||||
delegate: Rectangle {
|
||||
property string entryType: getEntryType(model.entry)
|
||||
property string entryPreview: getEntryPreview(model.entry)
|
||||
property int entryIndex: index + 1
|
||||
|
||||
width: clipboardListView.width
|
||||
height: Math.max(60, contentText.contentHeight + Theme.spacingL)
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.04)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const type = getEntryType(model.entry);
|
||||
if (type === "image") return "image";
|
||||
if (type === "long_text") return "subject";
|
||||
return "content_copy";
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS // Reduced right margin
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Index number
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: entryIndex.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
id: contentText
|
||||
text: getEntryPreview(model.entry)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: clipboardListView.width - Theme.iconSize - 80 - Theme.spacingM * 4
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 3
|
||||
// Content icon and text
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 68 // Account for index (24) + spacing (16) + delete button (32) - small margin
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (entryType === "image")
|
||||
return "image";
|
||||
|
||||
if (entryType === "long_text")
|
||||
return "subject";
|
||||
|
||||
return "content_copy";
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (entryType) {
|
||||
case "image":
|
||||
return "Image • " + entryPreview;
|
||||
case "long_text":
|
||||
return "Long Text";
|
||||
default:
|
||||
return "Text";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
id: contentText
|
||||
|
||||
text: entryPreview
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: entryType === "long_text" ? 3 : 1
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Delete button
|
||||
DankActionButton {
|
||||
iconName: "delete"
|
||||
iconSize: 16
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "dangerous"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.error
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
console.log("Delete clicked for entry:", model.entry);
|
||||
deleteEntry(model.entry);
|
||||
refreshClipboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyEntry(model.entry)
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: "No clipboard entries found"
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: filteredClipboardModel.count === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Clear confirmation dialog
|
||||
DankModal {
|
||||
id: clearConfirmDialog
|
||||
visible: showClearConfirmation
|
||||
width: 350
|
||||
height: 150
|
||||
keyboardFocus: "ondemand"
|
||||
|
||||
onBackgroundClicked: {
|
||||
showClearConfirmation = false;
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Clear All History?"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "This will permanently delete all clipboard history."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelClearButton.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Text {
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelClearButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: showClearConfirmation = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: confirmClearButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.9) : Theme.error
|
||||
|
||||
Text {
|
||||
text: "Clear All"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmClearButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
clearAll();
|
||||
showClearConfirmation = false;
|
||||
hide();
|
||||
// Main click area - explicitly excludes delete button area
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40 // Enough space to avoid delete button (32 + 8 margin)
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyEntry(model.entry)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Data models
|
||||
ListModel {
|
||||
id: clipboardModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredClipboardModel
|
||||
}
|
||||
|
||||
// Processes
|
||||
Process {
|
||||
id: clipboardProcess
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
clipboardModel.clear();
|
||||
const lines = text.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim().length > 0) {
|
||||
clipboardModel.append({"entry": line});
|
||||
}
|
||||
}
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: copyProcess
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.error("Copy failed with exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deleteProcess
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
refreshClipboard();
|
||||
} else {
|
||||
console.error("Delete failed with exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: clearProcess
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
clipboardModel.clear();
|
||||
filteredClipboardModel.clear();
|
||||
totalCount = 0;
|
||||
} else {
|
||||
console.error("Clear failed with exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cleanupProcess
|
||||
running: false
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
console.log("ClipboardHistory: IPC open() called");
|
||||
clipboardHistory.show();
|
||||
return "CLIPBOARD_OPEN_SUCCESS";
|
||||
}
|
||||
function close() {
|
||||
console.log("ClipboardHistory: IPC close() called");
|
||||
clipboardHistory.hide();
|
||||
return "CLIPBOARD_CLOSE_SUCCESS";
|
||||
}
|
||||
function toggle() {
|
||||
console.log("ClipboardHistory: IPC toggle() called");
|
||||
clipboardHistory.toggle();
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS";
|
||||
}
|
||||
target: "clipboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
138
Modules/ControlCenter/Audio/AudioDevicesList.qml
Normal file
138
Modules/ControlCenter/Audio/AudioDevicesList.qml
Normal file
@@ -0,0 +1,138 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(AudioService.sink) : ""
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Output Device"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 35
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
visible: AudioService.sink !== null
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (root.currentSinkDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
|
||||
let sinks = []
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node || node.isStream) continue
|
||||
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink) {
|
||||
sinks.push(node)
|
||||
}
|
||||
}
|
||||
return sinks
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset";
|
||||
else if (modelData.name.includes("hdmi"))
|
||||
return "tv";
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset";
|
||||
else
|
||||
return "speaker";
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
|
||||
return AudioService.subtitle(modelData.name) + (modelData === AudioService.sink ? " • Selected" : "");
|
||||
else
|
||||
return modelData === AudioService.sink ? "Selected" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData)
|
||||
Pipewire.preferredDefaultAudioSink = modelData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
Modules/ControlCenter/Audio/AudioInputDevicesList.qml
Normal file
136
Modules/ControlCenter/Audio/AudioInputDevicesList.qml
Normal file
@@ -0,0 +1,136 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(AudioService.source) : ""
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Input Device"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 35
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
visible: AudioService.source !== null
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (root.currentSourceDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
|
||||
let sources = []
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node || node.isStream) continue
|
||||
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.name.includes(".monitor")) {
|
||||
sources.push(node)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset_mic";
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset_mic";
|
||||
else
|
||||
return "mic";
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
|
||||
return AudioService.subtitle(modelData.name) + (modelData === AudioService.source ? " • Selected" : "");
|
||||
else
|
||||
return modelData === AudioService.source ? "Selected" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sourceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData)
|
||||
Pipewire.preferredDefaultAudioSource = modelData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
174
Modules/ControlCenter/Audio/MicrophoneControl.qml
Normal file
174
Modules/ControlCenter/Audio/MicrophoneControl.qml
Normal file
@@ -0,0 +1,174 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property real micLevel: (AudioService.source && AudioService.source.audio && AudioService.source.audio.volume * 100) || 0
|
||||
property bool micMuted: (AudioService.source && AudioService.source.audio && AudioService.source.audio.muted) || false
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Microphone Level"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: root.micMuted ? "mic_off" : "mic"
|
||||
size: Theme.iconSize
|
||||
color: root.micMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.source && AudioService.source.audio)
|
||||
AudioService.source.audio.muted = !AudioService.source.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: micSliderContainer
|
||||
|
||||
width: parent.width - 80
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: micSliderTrack
|
||||
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: micSliderFill
|
||||
|
||||
width: parent.width * (root.micLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: micHandle
|
||||
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: micMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true;
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isDragging = false;
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: micGlobalMouseArea
|
||||
|
||||
x: 0
|
||||
y: 0
|
||||
width: root.parent ? root.parent.width : 0
|
||||
height: root.parent ? root.parent.height : 0
|
||||
enabled: micMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: (mouse) => {
|
||||
if (micMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
micMouseArea.isDragging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "mic"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
174
Modules/ControlCenter/Audio/VolumeControl.qml
Normal file
174
Modules/ControlCenter/Audio/VolumeControl.qml
Normal file
@@ -0,0 +1,174 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property real volumeLevel: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
|
||||
property bool volumeMuted: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) || false
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Volume"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: root.volumeMuted ? "volume_off" : "volume_down"
|
||||
size: Theme.iconSize
|
||||
color: root.volumeMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.sink && AudioService.sink.audio)
|
||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: volumeSliderContainer
|
||||
|
||||
width: parent.width - 80
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderTrack
|
||||
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderFill
|
||||
|
||||
width: parent.width * (root.volumeLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: volumeHandle
|
||||
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: volumeMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true;
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isDragging = false;
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: volumeGlobalMouseArea
|
||||
|
||||
x: 0
|
||||
y: 0
|
||||
width: root.parent ? root.parent.width : 0
|
||||
height: root.parent ? root.parent.height : 0
|
||||
enabled: volumeMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: (mouse) => {
|
||||
if (volumeMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y);
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
volumeMouseArea.isDragging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "volume_up"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,45 +5,34 @@ import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter.Audio
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../Widgets"
|
||||
|
||||
Item {
|
||||
id: audioTab
|
||||
|
||||
property int audioSubTab: 0 // 0: Output, 1: Input
|
||||
readonly property real volumeLevel: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
|
||||
readonly property real micLevel: (AudioService.source && AudioService.source.audio && AudioService.source.audio.volume * 100) || 0
|
||||
readonly property bool volumeMuted: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) || false
|
||||
readonly property bool micMuted: (AudioService.source && AudioService.source.audio && AudioService.source.audio.muted) || false
|
||||
readonly property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(AudioService.sink) : ""
|
||||
readonly property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(AudioService.source) : ""
|
||||
property int audioSubTab: 0
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Audio Sub-tabs
|
||||
DankTabBar {
|
||||
width: parent.width
|
||||
tabHeight: 40
|
||||
currentIndex: audioTab.audioSubTab
|
||||
showIcons: false
|
||||
model: [
|
||||
{
|
||||
"text": "Output"
|
||||
},
|
||||
{
|
||||
"text": "Input"
|
||||
}
|
||||
]
|
||||
model: [{
|
||||
"text": "Output"
|
||||
}, {
|
||||
"text": "Input"
|
||||
}]
|
||||
onTabClicked: function(index) {
|
||||
audioTab.audioSubTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
// Output Tab Content
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
@@ -54,321 +43,16 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Volume Control
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Volume"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: audioTab.volumeMuted ? "volume_off" : "volume_down"
|
||||
size: Theme.iconSize
|
||||
color: audioTab.volumeMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.sink && AudioService.sink.audio)
|
||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: volumeSliderContainer
|
||||
|
||||
width: parent.width - 80
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderTrack
|
||||
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderFill
|
||||
|
||||
width: parent.width * (audioTab.volumeLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: volumeHandle
|
||||
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: volumeMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true;
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isDragging = false;
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: volumeGlobalMouseArea
|
||||
|
||||
x: 0
|
||||
y: 0
|
||||
width: audioTab.width
|
||||
height: audioTab.height
|
||||
enabled: volumeMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: (mouse) => {
|
||||
if (volumeMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y);
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
volumeMouseArea.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "volume_up"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
VolumeControl {
|
||||
}
|
||||
|
||||
// Output Devices
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Output Device"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// Current device indicator
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 35
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
visible: AudioService.sink !== null
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (audioTab.currentSinkDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Real audio devices
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
|
||||
let sinks = []
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node || node.isStream) continue
|
||||
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink) {
|
||||
sinks.push(node)
|
||||
}
|
||||
}
|
||||
return sinks
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset";
|
||||
else if (modelData.name.includes("hdmi"))
|
||||
return "tv";
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset";
|
||||
else
|
||||
return "speaker";
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
|
||||
return AudioService.subtitle(modelData.name) + (modelData === AudioService.sink ? " • Selected" : "");
|
||||
else
|
||||
return modelData === AudioService.sink ? "Selected" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData)
|
||||
Pipewire.preferredDefaultAudioSink = modelData;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AudioDevicesList {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Input Tab Content
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
@@ -379,312 +63,10 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Microphone Level Control
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Microphone Level"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: audioTab.micMuted ? "mic_off" : "mic"
|
||||
size: Theme.iconSize
|
||||
color: audioTab.micMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.source && AudioService.source.audio)
|
||||
AudioService.source.audio.muted = !AudioService.source.audio.muted;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: micSliderContainer
|
||||
|
||||
width: parent.width - 80
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: micSliderTrack
|
||||
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: micSliderFill
|
||||
|
||||
width: parent.width * (audioTab.micLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: micHandle
|
||||
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: micMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true;
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isDragging = false;
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: micGlobalMouseArea
|
||||
|
||||
x: 0
|
||||
y: 0
|
||||
width: audioTab.width
|
||||
height: audioTab.height
|
||||
enabled: micMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: (mouse) => {
|
||||
if (micMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
micMouseArea.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "mic"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MicrophoneControl {
|
||||
}
|
||||
|
||||
// Input Devices
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Input Device"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// Current device indicator
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 35
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
visible: AudioService.source !== null
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (audioTab.currentSourceDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Real audio input devices
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
|
||||
let sources = []
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node || node.isStream) continue
|
||||
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.name.includes(".monitor")) {
|
||||
sources.push(node)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset_mic";
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset_mic";
|
||||
else
|
||||
return "mic";
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
|
||||
return AudioService.subtitle(modelData.name) + (modelData === AudioService.source ? " • Selected" : "");
|
||||
else
|
||||
return modelData === AudioService.source ? "Selected" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sourceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData)
|
||||
Pipewire.preferredDefaultAudioSource = modelData;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AudioInputDevicesList {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
386
Modules/ControlCenter/Bluetooth/AvailableDevicesList.qml
Normal file
386
Modules/ControlCenter/Bluetooth/AvailableDevicesList.qml
Normal file
@@ -0,0 +1,386 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Available Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: scanText
|
||||
|
||||
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Start Scanning"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: scanArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: noteColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: noteColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Pairing Limitation"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.warning
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Quickshell does not support pairing devices that require pin or confirmation."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
return [];
|
||||
|
||||
var filtered = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
});
|
||||
return BluetoothService.sortDevices(filtered);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||
|
||||
width: parent.width
|
||||
height: 70
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse && !isBusy)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
|
||||
if (modelData.blocked)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
||||
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
border.color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.getDeviceIcon(modelData)
|
||||
size: Theme.iconSize
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
font.weight: modelData.pairing ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing...";
|
||||
if (modelData.blocked)
|
||||
return "Blocked";
|
||||
return BluetoothService.getSignalStrength(modelData);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.getSignalIcon(modelData)
|
||||
size: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: modelData.state !== BluetoothDeviceState.Connecting
|
||||
color: {
|
||||
if (!canConnect && !isBusy)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
|
||||
|
||||
if (actionButtonArea.containsMouse && !isBusy)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
|
||||
return "transparent";
|
||||
}
|
||||
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
opacity: canConnect || isBusy ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing...";
|
||||
|
||||
if (modelData.blocked)
|
||||
return "Blocked";
|
||||
|
||||
return "Connect";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
enabled: canConnect && !isBusy
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
BluetoothService.connectDeviceWithTrust(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: availableDeviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 90
|
||||
hoverEnabled: true
|
||||
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
enabled: canConnect && !isBusy
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
BluetoothService.connectDeviceWithTrust(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
return false;
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
|
||||
return availableCount === 0;
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "sync"
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 2000
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Scanning for devices..."
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Make sure your device is in pairing mode"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return true;
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
|
||||
return availableCount === 0 && !BluetoothService.adapter.discovering;
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
201
Modules/ControlCenter/Bluetooth/BluetoothContextMenu.qml
Normal file
201
Modules/ControlCenter/Bluetooth/BluetoothContextMenu.qml
Normal file
@@ -0,0 +1,201 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var deviceData: null
|
||||
property bool menuVisible: false
|
||||
property var parentItem
|
||||
|
||||
function show(x, y) {
|
||||
const menuWidth = 160;
|
||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
|
||||
let finalX = x - menuWidth / 2;
|
||||
let finalY = y;
|
||||
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth));
|
||||
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight));
|
||||
root.x = finalX;
|
||||
root.y = finalY;
|
||||
root.visible = true;
|
||||
root.menuVisible = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
root.menuVisible = false;
|
||||
Qt.callLater(() => {
|
||||
root.visible = false;
|
||||
});
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 160
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
z: 1000
|
||||
opacity: menuVisible ? 1 : 0
|
||||
scale: menuVisible ? 1 : 0.85
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.deviceData && root.deviceData.connected ? "link_off" : "link"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.deviceData) {
|
||||
if (root.deviceData.connected) {
|
||||
root.deviceData.disconnect();
|
||||
} else {
|
||||
BluetoothService.connectDeviceWithTrust(root.deviceData);
|
||||
}
|
||||
}
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "delete"
|
||||
size: Theme.iconSize - 2
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Forget Device"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.deviceData) {
|
||||
root.deviceData.forget();
|
||||
}
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Modules/ControlCenter/Bluetooth/BluetoothToggle.qml
Normal file
65
Modules/ControlCenter/Bluetooth/BluetoothToggle.qml
Normal file
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
|
||||
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : "transparent"
|
||||
border.width: 2
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "bluetooth"
|
||||
size: Theme.iconSizeLarge
|
||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "Bluetooth"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BluetoothService.adapter && BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: bluetoothToggle
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
150
Modules/ControlCenter/Bluetooth/PairedDevicesList.qml
Normal file
150
Modules/ControlCenter/Bluetooth/PairedDevicesList.qml
Normal file
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property var bluetoothContextMenuWindow
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Text {
|
||||
text: "Paired Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter((dev) => {
|
||||
return dev && (dev.paired || dev.trusted);
|
||||
}) : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.getDeviceIcon(modelData)
|
||||
size: Theme.iconSize
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: BluetoothDeviceState.toString(modelData.state)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.batteryAvailable && modelData.battery > 0)
|
||||
return "• " + Math.round(modelData.battery * 100) + "%";
|
||||
|
||||
var btBattery = BatteryService.bluetoothDevices.find((dev) => {
|
||||
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase());
|
||||
});
|
||||
return btBattery ? "• " + btBattery.percentage + "%" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: btMenuButton
|
||||
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "more_vert"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btMenuButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (bluetoothContextMenuWindow) {
|
||||
bluetoothContextMenuWindow.deviceData = modelData;
|
||||
let localPos = btMenuButtonArea.mapToItem(bluetoothContextMenuWindow.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height);
|
||||
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btDeviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40
|
||||
hoverEnabled: true
|
||||
enabled: !BluetoothService.isDeviceBusy(modelData)
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
modelData.disconnect();
|
||||
} else {
|
||||
BluetoothService.connectDeviceWithTrust(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Bluetooth
|
||||
|
||||
Item {
|
||||
id: bluetoothTab
|
||||
@@ -21,800 +22,19 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
|
||||
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : "transparent"
|
||||
border.width: 2
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "bluetooth"
|
||||
size: Theme.iconSizeLarge
|
||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "Bluetooth"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BluetoothService.adapter && BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: bluetoothToggle
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Text {
|
||||
text: "Paired Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter((dev) => {
|
||||
return dev && (dev.paired || dev.trusted);
|
||||
}) : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.getDeviceIcon(modelData)
|
||||
size: Theme.iconSize
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: BluetoothDeviceState.toString(modelData.state)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.batteryAvailable && modelData.battery > 0)
|
||||
return "• " + Math.round(modelData.battery * 100) + "%";
|
||||
|
||||
var btBattery = BatteryService.bluetoothDevices.find((dev) => {
|
||||
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase());
|
||||
});
|
||||
return btBattery ? "• " + btBattery.percentage + "%" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: btMenuButton
|
||||
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "more_vert"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btMenuButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
bluetoothContextMenuWindow.deviceData = modelData;
|
||||
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height);
|
||||
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btDeviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40
|
||||
hoverEnabled: true
|
||||
enabled: !BluetoothService.isDeviceBusy(modelData)
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
modelData.disconnect();
|
||||
} else {
|
||||
BluetoothService.connectDeviceWithTrust(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Available Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: scanText
|
||||
|
||||
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Start Scanning"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: scanArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: noteColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: noteColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Pairing Limitation"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.warning
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Quickshell does not support pairing devices that require pin or confirmation."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
return [];
|
||||
|
||||
var filtered = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
});
|
||||
return BluetoothService.sortDevices(filtered);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||
|
||||
width: parent.width
|
||||
height: 70
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse && !isBusy)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
|
||||
if (modelData.blocked)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
||||
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
border.color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.getDeviceIcon(modelData)
|
||||
size: Theme.iconSize
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
font.weight: modelData.pairing ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing...";
|
||||
if (modelData.blocked)
|
||||
return "Blocked";
|
||||
return BluetoothService.getSignalStrength(modelData);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: BluetoothService.getSignalIcon(modelData)
|
||||
size: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: modelData.state !== BluetoothDeviceState.Connecting
|
||||
color: {
|
||||
if (!canConnect && !isBusy)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
|
||||
|
||||
if (actionButtonArea.containsMouse && !isBusy)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
|
||||
return "transparent";
|
||||
}
|
||||
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
opacity: canConnect || isBusy ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing...";
|
||||
|
||||
if (modelData.blocked)
|
||||
return "Blocked";
|
||||
|
||||
return "Connect";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
enabled: canConnect && !isBusy
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
BluetoothService.connectDeviceWithTrust(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: availableDeviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 90 // Don't overlap with action button
|
||||
hoverEnabled: true
|
||||
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
enabled: canConnect && !isBusy
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
BluetoothService.connectDeviceWithTrust(modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
return false;
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
|
||||
return availableCount === 0;
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "sync"
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 2000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Scanning for devices..."
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Make sure your device is in pairing mode"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return true;
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
|
||||
return availableCount === 0 && !BluetoothService.adapter.discovering;
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
BluetoothToggle { }
|
||||
|
||||
PairedDevicesList {
|
||||
bluetoothContextMenuWindow: bluetoothContextMenuWindow
|
||||
}
|
||||
|
||||
AvailableDevicesList { }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
BluetoothContextMenu {
|
||||
id: bluetoothContextMenuWindow
|
||||
|
||||
property var deviceData: null
|
||||
property bool menuVisible: false
|
||||
|
||||
function show(x, y) {
|
||||
const menuWidth = 160;
|
||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
|
||||
let finalX = x - menuWidth / 2;
|
||||
let finalY = y;
|
||||
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth));
|
||||
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight));
|
||||
bluetoothContextMenuWindow.x = finalX;
|
||||
bluetoothContextMenuWindow.y = finalY;
|
||||
bluetoothContextMenuWindow.visible = true;
|
||||
bluetoothContextMenuWindow.menuVisible = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
bluetoothContextMenuWindow.menuVisible = false;
|
||||
Qt.callLater(() => {
|
||||
bluetoothContextMenuWindow.visible = false;
|
||||
});
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 160
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
z: 1000
|
||||
opacity: menuVisible ? 1 : 0
|
||||
scale: menuVisible ? 1 : 0.85
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "link_off" : "link"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "Disconnect" : "Connect"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (bluetoothContextMenuWindow.deviceData) {
|
||||
if (bluetoothContextMenuWindow.deviceData.connected) {
|
||||
bluetoothContextMenuWindow.deviceData.disconnect();
|
||||
} else {
|
||||
BluetoothService.connectDeviceWithTrust(bluetoothContextMenuWindow.deviceData);
|
||||
}
|
||||
}
|
||||
bluetoothContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "delete"
|
||||
size: Theme.iconSize - 2
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Forget Device"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (bluetoothContextMenuWindow.deviceData) {
|
||||
bluetoothContextMenuWindow.deviceData.forget();
|
||||
}
|
||||
bluetoothContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
parentItem: bluetoothTab
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -832,7 +52,5 @@ Item {
|
||||
onClicked: {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import "../../Widgets"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
@@ -9,7 +10,6 @@ import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../Widgets"
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
@@ -25,13 +25,13 @@ PanelWindow {
|
||||
// Enable/disable WiFi auto-refresh based on control center visibility
|
||||
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled;
|
||||
// Stop bluetooth scanning when control center is closed
|
||||
if (!visible && BluetoothService.adapter && BluetoothService.adapter.discovering) {
|
||||
if (!visible && BluetoothService.adapter && BluetoothService.adapter.discovering)
|
||||
BluetoothService.adapter.discovering = false;
|
||||
}
|
||||
|
||||
// Refresh uptime when opened
|
||||
if (visible && UserInfoService) {
|
||||
if (visible && UserInfoService)
|
||||
UserInfoService.getUptime();
|
||||
}
|
||||
|
||||
}
|
||||
implicitWidth: 600
|
||||
implicitHeight: 500
|
||||
@@ -296,6 +296,7 @@ PanelWindow {
|
||||
|
||||
DankIcon {
|
||||
id: dankIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
name: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
||||
size: Theme.iconSize - 2
|
||||
@@ -617,47 +618,48 @@ PanelWindow {
|
||||
let tabs = ["network", "audio"];
|
||||
if (BluetoothService.available)
|
||||
tabs.push("bluetooth");
|
||||
|
||||
tabs.push("display");
|
||||
return tabs.indexOf(root.currentTab);
|
||||
}
|
||||
model: {
|
||||
let tabs = [{
|
||||
"text": "Network",
|
||||
"icon": "wifi",
|
||||
"id": "network"
|
||||
}];
|
||||
// Always show audio
|
||||
let tabs = [{
|
||||
"text": "Network",
|
||||
"icon": "wifi",
|
||||
"id": "network"
|
||||
}];
|
||||
// Always show audio
|
||||
tabs.push({
|
||||
"text": "Audio",
|
||||
"icon": "volume_up",
|
||||
"id": "audio"
|
||||
});
|
||||
// Show Bluetooth only if available
|
||||
if (BluetoothService.available)
|
||||
tabs.push({
|
||||
"text": "Audio",
|
||||
"icon": "volume_up",
|
||||
"id": "audio"
|
||||
});
|
||||
// Show Bluetooth only if available
|
||||
if (BluetoothService.available)
|
||||
tabs.push({
|
||||
"text": "Bluetooth",
|
||||
"icon": "bluetooth",
|
||||
"id": "bluetooth"
|
||||
});
|
||||
"text": "Bluetooth",
|
||||
"icon": "bluetooth",
|
||||
"id": "bluetooth"
|
||||
});
|
||||
|
||||
// Always show display
|
||||
tabs.push({
|
||||
"text": "Display",
|
||||
"icon": "brightness_6",
|
||||
"id": "display"
|
||||
});
|
||||
return tabs;
|
||||
// Always show display
|
||||
tabs.push({
|
||||
"text": "Display",
|
||||
"icon": "brightness_6",
|
||||
"id": "display"
|
||||
});
|
||||
return tabs;
|
||||
}
|
||||
onTabClicked: function(index) {
|
||||
let tabs = ["network", "audio"];
|
||||
if (BluetoothService.available)
|
||||
tabs.push("bluetooth");
|
||||
|
||||
tabs.push("display");
|
||||
root.currentTab = tabs[index];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Tab content area
|
||||
|
||||
122
Modules/ControlCenter/Network/EthernetCard.qml
Normal file
122
Modules/ControlCenter/Network/EthernetCard.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: ethernetCard
|
||||
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (ethernetPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet")
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
|
||||
}
|
||||
border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: ethernetToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "lan"
|
||||
size: Theme.iconSize
|
||||
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Ethernet"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP || "Connected") : "Disconnected"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
leftPadding: Theme.iconSize + Theme.spacingM
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner for preference changes
|
||||
DankIcon {
|
||||
id: ethernetLoadingSpinner
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.right: ethernetToggle.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: NetworkService.changingPreference && NetworkService.targetPreference === "ethernet"
|
||||
z: 10
|
||||
|
||||
RotationAnimation {
|
||||
target: ethernetLoadingSpinner
|
||||
property: "rotation"
|
||||
running: ethernetLoadingSpinner.visible
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
// Ethernet toggle switch (matching WiFi style)
|
||||
DankToggle {
|
||||
id: ethernetToggle
|
||||
checked: NetworkService.ethernetConnected
|
||||
enabled: true
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
NetworkService.toggleNetworkConnection("ethernet");
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea for network preference (excluding toggle area)
|
||||
MouseArea {
|
||||
id: ethernetPreferenceArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 60 // Exclude toggle area
|
||||
hoverEnabled: true
|
||||
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet" && !NetworkService.changingNetworkPreference
|
||||
onClicked: {
|
||||
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
|
||||
console.log("Ethernet card clicked for preference");
|
||||
if (NetworkService.networkStatus !== "ethernet")
|
||||
NetworkService.setNetworkPreference("ethernet");
|
||||
else
|
||||
NetworkService.setNetworkPreference("auto");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
174
Modules/ControlCenter/Network/WiFiCard.qml
Normal file
174
Modules/ControlCenter/Network/WiFiCard.qml
Normal file
@@ -0,0 +1,174 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: wifiCard
|
||||
|
||||
property var refreshTimer
|
||||
|
||||
function getWiFiSignalIcon(signalStrength) {
|
||||
switch (signalStrength) {
|
||||
case "excellent": return "wifi";
|
||||
case "good": return "wifi_2_bar";
|
||||
case "fair": return "wifi_1_bar";
|
||||
case "poor": return "signal_wifi_0_bar";
|
||||
default: return "wifi";
|
||||
}
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi")
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
|
||||
}
|
||||
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
|
||||
visible: NetworkService.wifiAvailable
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: wifiToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "wifi_off";
|
||||
} else if (WifiService.currentWifiSSID !== "") {
|
||||
return getWiFiSignalIcon(WifiService.wifiSignalStrength);
|
||||
} else {
|
||||
return "wifi";
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "WiFi is off";
|
||||
} else if (NetworkService.wifiEnabled && WifiService.currentWifiSSID) {
|
||||
return WifiService.currentWifiSSID || "Connected";
|
||||
} else {
|
||||
return "Not Connected";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "Turn on WiFi to see networks";
|
||||
} else if (NetworkService.wifiEnabled && WifiService.currentWifiSSID) {
|
||||
return NetworkService.wifiIP || "Connected";
|
||||
} else {
|
||||
return "Select a network below";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
leftPadding: Theme.iconSize + Theme.spacingM
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner for preference changes
|
||||
DankIcon {
|
||||
id: wifiLoadingSpinner
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.right: wifiToggle.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: NetworkService.changingPreference && NetworkService.targetPreference === "wifi"
|
||||
z: 10
|
||||
|
||||
RotationAnimation {
|
||||
target: wifiLoadingSpinner
|
||||
property: "rotation"
|
||||
running: wifiLoadingSpinner.visible
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi toggle switch
|
||||
DankToggle {
|
||||
id: wifiToggle
|
||||
checked: NetworkService.wifiEnabled
|
||||
enabled: true
|
||||
toggling: NetworkService.wifiToggling
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
if (NetworkService.wifiEnabled) {
|
||||
// When turning WiFi off, clear all cached WiFi data
|
||||
WifiService.currentWifiSSID = "";
|
||||
WifiService.wifiSignalStrength = "excellent";
|
||||
WifiService.wifiNetworks = [];
|
||||
WifiService.savedWifiNetworks = [];
|
||||
WifiService.connectionStatus = "";
|
||||
WifiService.connectingSSID = "";
|
||||
WifiService.isScanning = false;
|
||||
NetworkService.refreshNetworkStatus();
|
||||
}
|
||||
NetworkService.toggleWifiRadio();
|
||||
if (refreshTimer) {
|
||||
refreshTimer.triggered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea for network preference (excluding toggle area)
|
||||
MouseArea {
|
||||
id: wifiPreferenceArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 60 // Exclude toggle area
|
||||
hoverEnabled: true
|
||||
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi" && !NetworkService.changingNetworkPreference
|
||||
onClicked: {
|
||||
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
|
||||
console.log("WiFi card clicked for preference");
|
||||
if (NetworkService.networkStatus !== "wifi")
|
||||
NetworkService.setNetworkPreference("wifi");
|
||||
else
|
||||
NetworkService.setNetworkPreference("auto");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
280
Modules/ControlCenter/Network/WiFiContextMenu.qml
Normal file
280
Modules/ControlCenter/Network/WiFiContextMenu.qml
Normal file
@@ -0,0 +1,280 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: wifiContextMenuWindow
|
||||
|
||||
property var networkData: null
|
||||
property bool menuVisible: false
|
||||
property var parentItem
|
||||
property var wifiPasswordDialogRef
|
||||
property var networkInfoDialogRef
|
||||
|
||||
function show(x, y) {
|
||||
const menuWidth = 160;
|
||||
wifiContextMenuWindow.visible = true;
|
||||
|
||||
Qt.callLater(() => {
|
||||
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2;
|
||||
let finalX = x - menuWidth / 2;
|
||||
let finalY = y + 4;
|
||||
|
||||
finalX = Math.max(Theme.spacingS, Math.min(finalX, parentItem.width - menuWidth - Theme.spacingS));
|
||||
finalY = Math.max(Theme.spacingS, Math.min(finalY, parentItem.height - menuHeight - Theme.spacingS));
|
||||
|
||||
if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
|
||||
finalY = y - menuHeight - 4;
|
||||
finalY = Math.max(Theme.spacingS, finalY);
|
||||
}
|
||||
|
||||
wifiContextMenuWindow.x = finalX;
|
||||
wifiContextMenuWindow.y = finalY;
|
||||
wifiContextMenuWindow.menuVisible = true;
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
wifiContextMenuWindow.menuVisible = false;
|
||||
Qt.callLater(() => {
|
||||
wifiContextMenuWindow.visible = false;
|
||||
});
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 160
|
||||
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
z: 1000
|
||||
opacity: menuVisible ? 1 : 0
|
||||
scale: menuVisible ? 1 : 0.85
|
||||
|
||||
Component.onCompleted: {
|
||||
menuVisible = false;
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Drop shadow
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: wifiMenuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
// Connect/Disconnect option
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectWifiArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (wifiContextMenuWindow.networkData) {
|
||||
if (wifiContextMenuWindow.networkData.connected) {
|
||||
WifiService.disconnectWifi();
|
||||
} else {
|
||||
if (wifiContextMenuWindow.networkData.saved) {
|
||||
WifiService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
|
||||
} else if (wifiContextMenuWindow.networkData.secured) {
|
||||
if (wifiPasswordDialogRef) {
|
||||
wifiPasswordDialogRef.wifiPasswordSSID = wifiContextMenuWindow.networkData.ssid;
|
||||
wifiPasswordDialogRef.wifiPasswordInput = "";
|
||||
wifiPasswordDialogRef.wifiPasswordDialogVisible = true;
|
||||
}
|
||||
} else {
|
||||
WifiService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
wifiContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
// Forget Network option (only for saved networks)
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
visible: wifiContextMenuWindow.networkData && (wifiContextMenuWindow.networkData.saved || wifiContextMenuWindow.networkData.connected)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "delete"
|
||||
size: Theme.iconSize - 2
|
||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Forget Network"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetWifiArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (wifiContextMenuWindow.networkData) {
|
||||
WifiService.forgetWifiNetwork(wifiContextMenuWindow.networkData.ssid);
|
||||
}
|
||||
wifiContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Info option
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Network Info"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: infoWifiArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (wifiContextMenuWindow.networkData && networkInfoDialogRef) {
|
||||
networkInfoDialogRef.showNetworkInfo(wifiContextMenuWindow.networkData.ssid, wifiContextMenuWindow.networkData);
|
||||
}
|
||||
wifiContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
276
Modules/ControlCenter/Network/WiFiNetworksList.qml
Normal file
276
Modules/ControlCenter/Network/WiFiNetworksList.qml
Normal file
@@ -0,0 +1,276 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property var wifiContextMenuWindow
|
||||
property var sortedWifiNetworks
|
||||
property var wifiPasswordDialogRef
|
||||
|
||||
function getWiFiSignalIcon(signalStrength) {
|
||||
switch (signalStrength) {
|
||||
case "excellent": return "wifi";
|
||||
case "good": return "wifi_2_bar";
|
||||
case "fair": return "wifi_1_bar";
|
||||
case "poor": return "signal_wifi_0_bar";
|
||||
default: return "wifi";
|
||||
}
|
||||
}
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 100
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: NetworkService.wifiEnabled
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Available Networks Section with refresh button (spanning version)
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Available Networks"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 170
|
||||
height: 1
|
||||
}
|
||||
|
||||
// WiFi refresh button (spanning version)
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: refreshAreaSpan.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : WifiService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
id: refreshIconSpan
|
||||
anchors.centerIn: parent
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 6
|
||||
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
rotation: WifiService.isScanning ? refreshIconSpan.rotation : 0
|
||||
|
||||
RotationAnimation {
|
||||
target: refreshIconSpan
|
||||
property: "rotation"
|
||||
running: WifiService.isScanning
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
Behavior on rotation {
|
||||
RotationAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: refreshAreaSpan
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!WifiService.isScanning) {
|
||||
// Immediate visual feedback
|
||||
refreshIconSpan.rotation += 30;
|
||||
WifiService.scanWifi();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable networks container
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - 40
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: spanningNetworksColumn.height
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
|
||||
Column {
|
||||
id: spanningNetworksColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: NetworkService.wifiAvailable && NetworkService.wifiEnabled ? sortedWifiNetworks : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 38
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: networkArea2.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: modelData.connected ? 1 : 0
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
|
||||
|
||||
// Signal strength icon
|
||||
DankIcon {
|
||||
id: signalIcon2
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: getWiFiSignalIcon(modelData.signalStrength)
|
||||
size: Theme.iconSize - 2
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
// Network info
|
||||
Column {
|
||||
anchors.left: signalIcon2.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: rightIcons2.left
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: modelData.ssid
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (modelData.connected)
|
||||
return "Connected";
|
||||
if (WifiService.connectionStatus === "connecting" && WifiService.connectingSSID === modelData.ssid)
|
||||
return "Connecting...";
|
||||
if (WifiService.connectionStatus === "invalid_password" && WifiService.connectingSSID === modelData.ssid)
|
||||
return "Invalid password";
|
||||
if (modelData.saved)
|
||||
return "Saved" + (modelData.secured ? " • Secured" : " • Open");
|
||||
return modelData.secured ? "Secured" : "Open";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: {
|
||||
if (WifiService.connectionStatus === "connecting" && WifiService.connectingSSID === modelData.ssid)
|
||||
return Theme.primary;
|
||||
if (WifiService.connectionStatus === "invalid_password" && WifiService.connectingSSID === modelData.ssid)
|
||||
return Theme.error;
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// Right side icons
|
||||
Row {
|
||||
id: rightIcons2
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Lock icon (if secured)
|
||||
DankIcon {
|
||||
name: "lock"
|
||||
size: Theme.iconSize - 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
visible: modelData.secured
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Context menu button
|
||||
Rectangle {
|
||||
id: wifiMenuButton
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
name: "more_vert"
|
||||
size: Theme.iconSize - 8
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wifiMenuButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
wifiContextMenuWindow.networkData = modelData;
|
||||
let buttonCenter = wifiMenuButtonArea.width / 2;
|
||||
let buttonBottom = wifiMenuButtonArea.height;
|
||||
let globalPos = wifiMenuButtonArea.mapToItem(wifiContextMenuWindow.parentItem, buttonCenter, buttonBottom);
|
||||
|
||||
Qt.callLater(() => {
|
||||
wifiContextMenuWindow.show(globalPos.x, globalPos.y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: networkArea2
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 32 // Exclude menu button area
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData.connected)
|
||||
return;
|
||||
|
||||
if (modelData.saved) {
|
||||
WifiService.connectToWifi(modelData.ssid);
|
||||
} else if (modelData.secured) {
|
||||
if (wifiPasswordDialogRef) {
|
||||
wifiPasswordDialogRef.wifiPasswordSSID = modelData.ssid;
|
||||
wifiPasswordDialogRef.wifiPasswordInput = "";
|
||||
wifiPasswordDialogRef.wifiPasswordDialogVisible = true;
|
||||
}
|
||||
} else {
|
||||
WifiService.connectToWifi(modelData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,13 @@ import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../Widgets"
|
||||
import qs.Modules.ControlCenter.Network
|
||||
|
||||
Item {
|
||||
id: networkTab
|
||||
|
||||
// Helper function for consistent WiFi signal icons
|
||||
function getWiFiSignalIcon(signalStrength) {
|
||||
switch (signalStrength) {
|
||||
case "excellent": return "wifi";
|
||||
case "good": return "wifi_2_bar";
|
||||
case "fair": return "wifi_1_bar";
|
||||
case "poor": return "signal_wifi_0_bar";
|
||||
default: return "wifi";
|
||||
}
|
||||
}
|
||||
property var wifiPasswordDialogRef: wifiPasswordDialog
|
||||
property var networkInfoDialogRef: networkInfoDialog
|
||||
|
||||
// Properly sorted WiFi networks with connected networks first
|
||||
property var sortedWifiNetworks: {
|
||||
@@ -92,7 +84,6 @@ Item {
|
||||
height: parent.height
|
||||
spacing: Theme.spacingS
|
||||
|
||||
|
||||
// WiFi Content in Flickable
|
||||
Flickable {
|
||||
width: parent.width
|
||||
@@ -106,165 +97,13 @@ Item {
|
||||
|
||||
Column {
|
||||
id: wifiContent
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Current WiFi connection status card
|
||||
Rectangle {
|
||||
id: wifiCard
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi")
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
|
||||
}
|
||||
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
|
||||
visible: NetworkService.wifiAvailable
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: wifiToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "wifi_off";
|
||||
} else if (WifiService.currentWifiSSID !== "") {
|
||||
return getWiFiSignalIcon(WifiService.wifiSignalStrength);
|
||||
} else {
|
||||
return "wifi";
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "WiFi is off";
|
||||
} else if (NetworkService.wifiEnabled && WifiService.currentWifiSSID) {
|
||||
return WifiService.currentWifiSSID || "Connected";
|
||||
} else {
|
||||
return "Not Connected";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "Turn on WiFi to see networks";
|
||||
} else if (NetworkService.wifiEnabled && WifiService.currentWifiSSID) {
|
||||
return NetworkService.wifiIP || "Connected";
|
||||
} else {
|
||||
return "Select a network below";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
leftPadding: Theme.iconSize + Theme.spacingM
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Loading spinner for preference changes
|
||||
DankIcon {
|
||||
id: wifiLoadingSpinner
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.right: wifiToggle.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: NetworkService.changingPreference && NetworkService.targetPreference === "wifi"
|
||||
z: 10
|
||||
|
||||
RotationAnimation {
|
||||
target: wifiLoadingSpinner
|
||||
property: "rotation"
|
||||
running: wifiLoadingSpinner.visible
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi toggle switch
|
||||
DankToggle {
|
||||
id: wifiToggle
|
||||
checked: NetworkService.wifiEnabled
|
||||
enabled: true
|
||||
toggling: NetworkService.wifiToggling
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
if (NetworkService.wifiEnabled) {
|
||||
// When turning WiFi off, clear all cached WiFi data
|
||||
WifiService.currentWifiSSID = "";
|
||||
WifiService.wifiSignalStrength = "excellent";
|
||||
WifiService.wifiNetworks = [];
|
||||
WifiService.savedWifiNetworks = [];
|
||||
WifiService.connectionStatus = "";
|
||||
WifiService.connectingSSID = "";
|
||||
WifiService.isScanning = false;
|
||||
NetworkService.refreshNetworkStatus();
|
||||
}
|
||||
NetworkService.toggleWifiRadio();
|
||||
refreshTimer.triggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea for network preference (excluding toggle area)
|
||||
MouseArea {
|
||||
id: wifiPreferenceArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 60 // Exclude toggle area
|
||||
hoverEnabled: true
|
||||
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi" && !NetworkService.changingNetworkPreference
|
||||
onClicked: {
|
||||
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
|
||||
console.log("WiFi card clicked for preference");
|
||||
if (NetworkService.networkStatus !== "wifi")
|
||||
NetworkService.setNetworkPreference("wifi");
|
||||
else
|
||||
NetworkService.setNetworkPreference("auto");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
WiFiCard {
|
||||
refreshTimer: refreshTimer
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
@@ -279,8 +118,6 @@ Item {
|
||||
height: parent.height
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Ethernet Header removed
|
||||
|
||||
// Ethernet Content in Flickable
|
||||
Flickable {
|
||||
width: parent.width
|
||||
@@ -294,124 +131,12 @@ Item {
|
||||
|
||||
Column {
|
||||
id: ethernetContent
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Ethernet connection status card (matching WiFi height)
|
||||
Rectangle {
|
||||
id: ethernetCard
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (ethernetPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet")
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
|
||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
|
||||
}
|
||||
border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: ethernetToggle.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "lan"
|
||||
size: Theme.iconSize
|
||||
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Ethernet"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP || "Connected") : "Disconnected"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
leftPadding: Theme.iconSize + Theme.spacingM
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner for preference changes
|
||||
DankIcon {
|
||||
id: ethernetLoadingSpinner
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.right: ethernetToggle.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: NetworkService.changingPreference && NetworkService.targetPreference === "ethernet"
|
||||
z: 10
|
||||
|
||||
RotationAnimation {
|
||||
target: ethernetLoadingSpinner
|
||||
property: "rotation"
|
||||
running: ethernetLoadingSpinner.visible
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
// Ethernet toggle switch (matching WiFi style)
|
||||
DankToggle {
|
||||
id: ethernetToggle
|
||||
checked: NetworkService.ethernetConnected
|
||||
enabled: true
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
NetworkService.toggleNetworkConnection("ethernet");
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea for network preference (excluding toggle area)
|
||||
MouseArea {
|
||||
id: ethernetPreferenceArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 60 // Exclude toggle area
|
||||
hoverEnabled: true
|
||||
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet" && !NetworkService.changingNetworkPreference
|
||||
onClicked: {
|
||||
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
|
||||
console.log("Ethernet card clicked for preference");
|
||||
if (NetworkService.networkStatus !== "ethernet")
|
||||
NetworkService.setNetworkPreference("ethernet");
|
||||
else
|
||||
NetworkService.setNetworkPreference("auto");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
EthernetCard {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
@@ -459,253 +184,11 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi networks spanning across both columns when Ethernet preference button is hidden
|
||||
Column {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 100
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: NetworkService.wifiEnabled
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Available Networks Section with refresh button (spanning version)
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Available Networks"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 170
|
||||
height: 1
|
||||
}
|
||||
|
||||
// WiFi refresh button (spanning version)
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: refreshAreaSpan.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : WifiService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
id: refreshIconSpan
|
||||
anchors.centerIn: parent
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 6
|
||||
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
rotation: WifiService.isScanning ? refreshIconSpan.rotation : 0
|
||||
|
||||
RotationAnimation {
|
||||
target: refreshIconSpan
|
||||
property: "rotation"
|
||||
running: WifiService.isScanning
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
Behavior on rotation {
|
||||
RotationAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: refreshAreaSpan
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!WifiService.isScanning) {
|
||||
// Immediate visual feedback
|
||||
refreshIconSpan.rotation += 30;
|
||||
WifiService.scanWifi();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable networks container
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - 40
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: spanningNetworksColumn.height
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
|
||||
Column {
|
||||
id: spanningNetworksColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: NetworkService.wifiAvailable && NetworkService.wifiEnabled ? sortedWifiNetworks : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 38
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: networkArea2.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: modelData.connected ? 1 : 0
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
|
||||
|
||||
// Signal strength icon
|
||||
DankIcon {
|
||||
id: signalIcon2
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: getWiFiSignalIcon(modelData.signalStrength)
|
||||
size: Theme.iconSize - 2
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
// Network info
|
||||
Column {
|
||||
anchors.left: signalIcon2.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: rightIcons2.left
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: modelData.ssid
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (modelData.connected)
|
||||
return "Connected";
|
||||
if (WifiService.connectionStatus === "connecting" && WifiService.connectingSSID === modelData.ssid)
|
||||
return "Connecting...";
|
||||
if (WifiService.connectionStatus === "invalid_password" && WifiService.connectingSSID === modelData.ssid)
|
||||
return "Invalid password";
|
||||
if (modelData.saved)
|
||||
return "Saved" + (modelData.secured ? " • Secured" : " • Open");
|
||||
return modelData.secured ? "Secured" : "Open";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: {
|
||||
if (WifiService.connectionStatus === "connecting" && WifiService.connectingSSID === modelData.ssid)
|
||||
return Theme.primary;
|
||||
if (WifiService.connectionStatus === "invalid_password" && WifiService.connectingSSID === modelData.ssid)
|
||||
return Theme.error;
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// Right side icons
|
||||
Row {
|
||||
id: rightIcons2
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Lock icon (if secured)
|
||||
DankIcon {
|
||||
name: "lock"
|
||||
size: Theme.iconSize - 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
visible: modelData.secured
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Context menu button
|
||||
Rectangle {
|
||||
id: wifiMenuButton
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
|
||||
DankIcon {
|
||||
name: "more_vert"
|
||||
size: Theme.iconSize - 8
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wifiMenuButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
wifiContextMenuWindow.networkData = modelData;
|
||||
let localPos = wifiMenuButtonArea.mapToItem(networkTab, wifiMenuButtonArea.width / 2, wifiMenuButtonArea.height);
|
||||
wifiContextMenuWindow.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: networkArea2
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 32 // Exclude menu button area
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData.connected)
|
||||
return;
|
||||
|
||||
if (modelData.saved) {
|
||||
// Saved network, connect directly
|
||||
WifiService.connectToWifi(modelData.ssid);
|
||||
} else if (modelData.secured) {
|
||||
// Secured network, need password - use root dialog
|
||||
wifiPasswordDialog.wifiPasswordSSID = modelData.ssid;
|
||||
wifiPasswordDialog.wifiPasswordInput = "";
|
||||
wifiPasswordDialog.wifiPasswordDialogVisible = true;
|
||||
} else {
|
||||
// Open network, connect directly
|
||||
WifiService.connectToWifi(modelData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
}
|
||||
// WiFi networks spanning across both columns when WiFi is enabled
|
||||
WiFiNetworksList {
|
||||
wifiContextMenuWindow: wifiContextMenuWindow
|
||||
sortedWifiNetworks: networkTab.sortedWifiNetworks
|
||||
wifiPasswordDialogRef: networkTab.wifiPasswordDialogRef
|
||||
}
|
||||
|
||||
// Timer for refreshing network status after WiFi toggle
|
||||
@@ -826,259 +309,11 @@ Item {
|
||||
}
|
||||
|
||||
// WiFi Context Menu Window
|
||||
Rectangle {
|
||||
WiFiContextMenu {
|
||||
id: wifiContextMenuWindow
|
||||
|
||||
property var networkData: null
|
||||
property bool menuVisible: false
|
||||
|
||||
function show(x, y) {
|
||||
const menuWidth = 160;
|
||||
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2;
|
||||
let finalX = x - menuWidth / 2;
|
||||
let finalY = y;
|
||||
finalX = Math.max(0, Math.min(finalX, networkTab.width - menuWidth));
|
||||
finalY = Math.max(0, Math.min(finalY, networkTab.height - menuHeight));
|
||||
wifiContextMenuWindow.x = finalX;
|
||||
wifiContextMenuWindow.y = finalY;
|
||||
wifiContextMenuWindow.visible = true;
|
||||
wifiContextMenuWindow.menuVisible = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
wifiContextMenuWindow.menuVisible = false;
|
||||
Qt.callLater(() => {
|
||||
wifiContextMenuWindow.visible = false;
|
||||
});
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 160
|
||||
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
z: 1000
|
||||
opacity: menuVisible ? 1 : 0
|
||||
scale: menuVisible ? 1 : 0.85
|
||||
|
||||
// Drop shadow
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: wifiMenuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
// Connect/Disconnect option
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectWifiArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (wifiContextMenuWindow.networkData) {
|
||||
if (wifiContextMenuWindow.networkData.connected) {
|
||||
// Disconnect from current network
|
||||
WifiService.disconnectWifi();
|
||||
} else {
|
||||
// Connect to selected network
|
||||
if (wifiContextMenuWindow.networkData.saved) {
|
||||
WifiService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
|
||||
} else if (wifiContextMenuWindow.networkData.secured) {
|
||||
// Show password dialog for secured networks
|
||||
wifiPasswordDialog.wifiPasswordSSID = wifiContextMenuWindow.networkData.ssid;
|
||||
wifiPasswordDialog.wifiPasswordInput = "";
|
||||
wifiPasswordDialog.wifiPasswordDialogVisible = true;
|
||||
} else {
|
||||
WifiService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
wifiContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
// Forget Network option (only for saved networks)
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
visible: wifiContextMenuWindow.networkData && (wifiContextMenuWindow.networkData.saved || wifiContextMenuWindow.networkData.connected)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "delete"
|
||||
size: Theme.iconSize - 2
|
||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Forget Network"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetWifiArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (wifiContextMenuWindow.networkData) {
|
||||
WifiService.forgetWifiNetwork(wifiContextMenuWindow.networkData.ssid);
|
||||
}
|
||||
wifiContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Info option
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Network Info"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: infoWifiArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (wifiContextMenuWindow.networkData) {
|
||||
networkInfoDialog.showNetworkInfo(wifiContextMenuWindow.networkData.ssid, wifiContextMenuWindow.networkData);
|
||||
}
|
||||
wifiContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
parentItem: networkTab
|
||||
wifiPasswordDialogRef: networkTab.wifiPasswordDialogRef
|
||||
networkInfoDialogRef: networkTab.networkInfoDialogRef
|
||||
}
|
||||
|
||||
// Background MouseArea to close the context menu
|
||||
|
||||
@@ -408,7 +408,9 @@ PanelWindow {
|
||||
font.pixelSize: 9
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -465,6 +467,8 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Text {
|
||||
// No truncation for notification center - show full text
|
||||
|
||||
property bool hasUrls: {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
return urlRegex.test(modelData.latestNotification.body);
|
||||
@@ -473,8 +477,6 @@ PanelWindow {
|
||||
text: {
|
||||
// Auto-detect and make URLs clickable, with truncation for center notifications
|
||||
let bodyText = modelData.latestNotification.body;
|
||||
// No truncation for notification center - show full text
|
||||
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
return bodyText.replace(urlRegex, '<a href="$1" style="color: ' + Theme.primary + '; text-decoration: underline;">$1</a>');
|
||||
}
|
||||
@@ -490,8 +492,11 @@ PanelWindow {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -507,6 +512,7 @@ PanelWindow {
|
||||
// Expand button - always takes up space but only visible when needed
|
||||
Rectangle {
|
||||
id: collapsedExpandButton
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
width: 20
|
||||
@@ -524,11 +530,13 @@ PanelWindow {
|
||||
|
||||
MouseArea {
|
||||
id: expandArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Close button - always positioned at the right edge
|
||||
@@ -569,8 +577,11 @@ PanelWindow {
|
||||
}
|
||||
onClicked: NotificationService.dismissGroup(modelData.key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expanded view - shows all notifications stacked
|
||||
@@ -836,67 +847,8 @@ PanelWindow {
|
||||
height: contentColumn.height
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
property bool isMessageExpanded: NotificationService.expandedMessages[modelData.notification.id] || false
|
||||
|
||||
width: parent.width
|
||||
spacing: 2 // Reduced from Theme.spacingXS (4px) by 2px
|
||||
|
||||
// Title • timestamp format
|
||||
Text {
|
||||
text: {
|
||||
const summary = modelData.summary || "";
|
||||
const timeStr = modelData.timeStr || "";
|
||||
if (summary && timeStr)
|
||||
return summary + " • " + timeStr;
|
||||
else if (summary)
|
||||
return summary;
|
||||
else
|
||||
return "Message • " + timeStr;
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
// Body text with expandable behavior
|
||||
Text {
|
||||
text: modelData.body
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: parent.isMessageExpanded ? -1 : 3 // Unlimited when expanded, 3 when collapsed (more space in center)
|
||||
elide: parent.isMessageExpanded ? Text.ElideNone : Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
// Clickable area for View action on individual message
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
// Find and invoke the View action
|
||||
if (modelData.actions) {
|
||||
for (const action of modelData.actions) {
|
||||
if (action.text && action.text.toLowerCase() === "view") {
|
||||
if (action.invoke)
|
||||
action.invoke();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Individual inline reply
|
||||
/*
|
||||
// COMMENTED OUT: Individual inline reply
|
||||
/*
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
@@ -960,7 +912,67 @@ PanelWindow {
|
||||
}
|
||||
*/
|
||||
|
||||
id: contentColumn
|
||||
|
||||
property bool isMessageExpanded: NotificationService.expandedMessages[modelData.notification.id] || false
|
||||
|
||||
width: parent.width
|
||||
spacing: 2 // Reduced from Theme.spacingXS (4px) by 2px
|
||||
|
||||
// Title • timestamp format
|
||||
Text {
|
||||
text: {
|
||||
const summary = modelData.summary || "";
|
||||
const timeStr = modelData.timeStr || "";
|
||||
if (summary && timeStr)
|
||||
return summary + " • " + timeStr;
|
||||
else if (summary)
|
||||
return summary;
|
||||
else
|
||||
return "Message • " + timeStr;
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
// Body text with expandable behavior
|
||||
Text {
|
||||
text: modelData.body
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: parent.isMessageExpanded ? -1 : 3 // Unlimited when expanded, 3 when collapsed (more space in center)
|
||||
elide: parent.isMessageExpanded ? Text.ElideNone : Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
// Clickable area for View action on individual message
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
// Find and invoke the View action
|
||||
if (modelData.actions) {
|
||||
for (const action of modelData.actions) {
|
||||
if (action.text && action.text.toLowerCase() === "view") {
|
||||
if (action.invoke)
|
||||
action.invoke();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,4 +14,5 @@ Column {
|
||||
return Prefs.setClockFormat(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
@@ -158,4 +159,5 @@ Column {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -146,7 +146,6 @@ Column {
|
||||
onEditingFinished: {
|
||||
Prefs.setProfileImage(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -164,4 +163,5 @@ Column {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,22 +11,27 @@ DankModal {
|
||||
signal closingModal()
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
if (!visible)
|
||||
closingModal();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// DankModal configuration
|
||||
visible: settingsVisible
|
||||
width: 650
|
||||
height: 750
|
||||
keyboardFocus: "ondemand"
|
||||
enableShadow: true
|
||||
|
||||
onBackgroundClicked: {
|
||||
settingsVisible = false;
|
||||
}
|
||||
|
||||
// Keyboard focus and shortcuts
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: settingsModal.settingsVisible
|
||||
Keys.onEscapePressed: settingsModal.settingsVisible = false
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
@@ -73,6 +78,7 @@ DankModal {
|
||||
// Settings sections
|
||||
ScrollView {
|
||||
id: settingsScrollView
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - 50
|
||||
clip: true
|
||||
@@ -81,6 +87,7 @@ DankModal {
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
|
||||
width: settingsScrollView.width - 20
|
||||
spacing: Theme.spacingL
|
||||
bottomPadding: Theme.spacingL
|
||||
@@ -89,42 +96,60 @@ DankModal {
|
||||
SettingsSection {
|
||||
title: "Profile"
|
||||
iconName: "person"
|
||||
content: ProfileTab {}
|
||||
|
||||
content: ProfileTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clock Settings
|
||||
SettingsSection {
|
||||
title: "Clock & Time"
|
||||
iconName: "schedule"
|
||||
content: ClockTab {}
|
||||
|
||||
content: ClockTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Weather Settings
|
||||
SettingsSection {
|
||||
title: "Weather"
|
||||
iconName: "wb_sunny"
|
||||
content: WeatherTab {}
|
||||
|
||||
content: WeatherTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Widget Visibility Settings
|
||||
SettingsSection {
|
||||
title: "Top Bar Widgets"
|
||||
iconName: "widgets"
|
||||
content: WidgetsTab {}
|
||||
|
||||
content: WidgetsTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Workspace Settings
|
||||
SettingsSection {
|
||||
title: "Workspaces"
|
||||
iconName: "tab"
|
||||
content: WorkspaceTab {}
|
||||
|
||||
content: WorkspaceTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Display Settings
|
||||
SettingsSection {
|
||||
title: "Display & Appearance"
|
||||
iconName: "palette"
|
||||
content: DisplayTab {}
|
||||
|
||||
content: DisplayTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -132,13 +157,7 @@ DankModal {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Keyboard focus and shortcuts
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: settingsModal.settingsVisible
|
||||
Keys.onEscapePressed: settingsModal.settingsVisible = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,4 @@ Column {
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,49 +14,44 @@ Column {
|
||||
return Prefs.setTemperatureUnit(checked);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Weather Location Override
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
|
||||
DankToggle {
|
||||
text: "Override Location"
|
||||
description: "Use a specific location instead of auto-detection"
|
||||
checked: Prefs.weatherLocationOverrideEnabled
|
||||
onToggled: (checked) => Prefs.setWeatherLocationOverrideEnabled(checked)
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setWeatherLocationOverrideEnabled(checked);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Location input - only visible when override is enabled
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: Prefs.weatherLocationOverrideEnabled
|
||||
opacity: visible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Text {
|
||||
text: "Location"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
|
||||
DankLocationSearch {
|
||||
width: parent.width
|
||||
currentLocation: Prefs.weatherLocationOverride
|
||||
placeholderText: "Search for a location..."
|
||||
onLocationSelected: (displayName, coordinates) => {
|
||||
Prefs.setWeatherLocationOverride(coordinates)
|
||||
Prefs.setWeatherLocationOverride(coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Text {
|
||||
text: "Examples: \"New York\", \"Tokyo\", \"44511\""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
@@ -64,6 +59,17 @@ Column {
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -109,13 +109,13 @@ Column {
|
||||
bottomPadding: Theme.spacingS
|
||||
onEditingFinished: {
|
||||
var color = text.trim();
|
||||
if (color === "" || /^#[0-9A-Fa-f]{6}$/.test(color)) {
|
||||
if (color === "" || /^#[0-9A-Fa-f]{6}$/.test(color))
|
||||
Prefs.setOSLogoColorOverride(color);
|
||||
} else {
|
||||
else
|
||||
text = Prefs.osLogoColorOverride;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -139,9 +139,10 @@ Column {
|
||||
unit: ""
|
||||
showValue: false
|
||||
onSliderValueChanged: (newValue) => {
|
||||
Prefs.setOSLogoBrightness(newValue / 100.0);
|
||||
Prefs.setOSLogoBrightness(newValue / 100);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -165,9 +166,12 @@ Column {
|
||||
unit: ""
|
||||
showValue: false
|
||||
onSliderValueChanged: (newValue) => {
|
||||
Prefs.setOSLogoContrast(newValue / 100.0);
|
||||
Prefs.setOSLogoContrast(newValue / 100);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,4 +23,5 @@ Column {
|
||||
return Prefs.setShowWorkspacePadding(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,15 +9,20 @@ Rectangle {
|
||||
property bool isActive: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
|
||||
// Helper function for consistent WiFi signal icons
|
||||
function getWiFiSignalIcon(signalStrength) {
|
||||
switch (signalStrength) {
|
||||
case "excellent": return "wifi";
|
||||
case "good": return "wifi_2_bar";
|
||||
case "fair": return "wifi_1_bar";
|
||||
case "poor": return "signal_wifi_0_bar";
|
||||
default: return "wifi";
|
||||
case "excellent":
|
||||
return "wifi";
|
||||
case "good":
|
||||
return "wifi_2_bar";
|
||||
case "fair":
|
||||
return "wifi_1_bar";
|
||||
case "poor":
|
||||
return "signal_wifi_0_bar";
|
||||
default:
|
||||
return "wifi";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +40,12 @@ Rectangle {
|
||||
// Network Status Icon
|
||||
DankIcon {
|
||||
name: {
|
||||
if (NetworkService.networkStatus === "ethernet") {
|
||||
if (NetworkService.networkStatus === "ethernet")
|
||||
return "lan";
|
||||
} else if (NetworkService.networkStatus === "wifi") {
|
||||
else if (NetworkService.networkStatus === "wifi")
|
||||
return getWiFiSignalIcon(WifiService.wifiSignalStrength);
|
||||
} else {
|
||||
else
|
||||
return "wifi_off";
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize - 8
|
||||
color: NetworkService.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
@@ -68,7 +72,7 @@ Rectangle {
|
||||
DankIcon {
|
||||
id: audioIcon
|
||||
|
||||
name: AudioService.sinkMuted ? "volume_off" : AudioService.volumeLevel < 33 ? "volume_down" : "volume_up"
|
||||
name: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) ? "volume_off" : (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) < 33 ? "volume_down" : "volume_up"
|
||||
size: Theme.iconSize - 8
|
||||
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -6,10 +6,10 @@ import qs.Widgets
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
signal clicked()
|
||||
|
||||
property bool isActive: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: 40
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
@@ -10,9 +10,9 @@ import Quickshell.Services.SystemTray
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modules
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules
|
||||
|
||||
PanelWindow {
|
||||
// Proxy objects for external connections
|
||||
@@ -29,12 +29,11 @@ PanelWindow {
|
||||
screen: modelData
|
||||
implicitHeight: Theme.barHeight - 4
|
||||
color: "transparent"
|
||||
|
||||
Component.onCompleted: {
|
||||
let fonts = Qt.fontFamilies();
|
||||
if (fonts.indexOf("Material Symbols Rounded") === -1) {
|
||||
if (fonts.indexOf("Material Symbols Rounded") === -1)
|
||||
ToastService.showError("Please install Material Symbols Rounded and Restart your Shell. See README.md for instructions");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -226,7 +225,7 @@ PanelWindow {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
clipboardHistoryPopup.toggle();
|
||||
clipboardHistoryModalPopup.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,22 +16,20 @@ Rectangle {
|
||||
|
||||
function padWorkspaces(list) {
|
||||
var padded = list.slice();
|
||||
while (padded.length < 3) {
|
||||
padded.push(-1); // Use -1 as a placeholder
|
||||
}
|
||||
while (padded.length < 3)padded.push(-1) // Use -1 as a placeholder
|
||||
return padded;
|
||||
}
|
||||
|
||||
function getDisplayWorkspaces() {
|
||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
|
||||
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
|
||||
return [1, 2];
|
||||
|
||||
if (!root.screenName)
|
||||
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers();
|
||||
return NiriService.getCurrentOutputWorkspaceNumbers();
|
||||
|
||||
var displayWorkspaces = [];
|
||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
||||
var ws = NiriWorkspaceService.allWorkspaces[i];
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
var ws = NiriService.allWorkspaces[i];
|
||||
if (ws.output === root.screenName)
|
||||
displayWorkspaces.push(ws.idx + 1);
|
||||
|
||||
@@ -40,14 +38,14 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getDisplayActiveWorkspace() {
|
||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
|
||||
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
|
||||
return 1;
|
||||
|
||||
if (!root.screenName)
|
||||
return NiriWorkspaceService.getCurrentWorkspaceNumber();
|
||||
return NiriService.getCurrentWorkspaceNumber();
|
||||
|
||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
||||
var ws = NiriWorkspaceService.allWorkspaces[i];
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
var ws = NiriService.allWorkspaces[i];
|
||||
if (ws.output === root.screenName && ws.is_active)
|
||||
return ws.idx + 1;
|
||||
|
||||
@@ -59,7 +57,7 @@ Rectangle {
|
||||
height: 30
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||
visible: NiriWorkspaceService.niriAvailable
|
||||
visible: NiriService.niriAvailable
|
||||
|
||||
Connections {
|
||||
function onAllWorkspacesChanged() {
|
||||
@@ -72,13 +70,13 @@ Rectangle {
|
||||
}
|
||||
|
||||
function onNiriAvailableChanged() {
|
||||
if (NiriWorkspaceService.niriAvailable) {
|
||||
if (NiriService.niriAvailable) {
|
||||
root.workspaceList = Prefs.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces();
|
||||
root.currentWorkspace = root.getDisplayActiveWorkspace();
|
||||
}
|
||||
}
|
||||
|
||||
target: NiriWorkspaceService
|
||||
target: NiriService
|
||||
}
|
||||
|
||||
// Force update when padding preference changes
|
||||
@@ -92,7 +90,6 @@ Rectangle {
|
||||
target: Prefs
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
id: workspaceRow
|
||||
|
||||
@@ -115,14 +112,15 @@ Rectangle {
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: !isPlaceholder
|
||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !isPlaceholder
|
||||
onClicked: {
|
||||
if (!isPlaceholder) {
|
||||
if (!isPlaceholder)
|
||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,12 +134,12 @@ Rectangle {
|
||||
font.bold: isActive && !isPlaceholder
|
||||
}
|
||||
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
@@ -149,8 +147,11 @@ Rectangle {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ PanelWindow {
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 1
|
||||
model: menuOpener.children
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
@@ -93,8 +94,6 @@ PanelWindow {
|
||||
text: "M"
|
||||
}
|
||||
|
||||
model: menuOpener.children
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: modelData.isSeparator ? 5 : 28
|
||||
|
||||
@@ -15,232 +15,18 @@ DankModal {
|
||||
width: 420
|
||||
height: 230
|
||||
keyboardFocus: "ondemand"
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
if (!visible)
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
onBackgroundClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "Connect to Wi-Fi"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Enter password for \"" + wifiPasswordSSID + "\""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password input
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
border.color: passwordInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: passwordInput.activeFocus ? 2 : 1
|
||||
|
||||
DankTextField {
|
||||
id: passwordInput
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiPasswordInput
|
||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: "Enter password"
|
||||
backgroundColor: "transparent"
|
||||
normalBorderColor: "transparent"
|
||||
focusedBorderColor: "transparent"
|
||||
onTextEdited: {
|
||||
wifiPasswordInput = text;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onOpened() {
|
||||
passwordInput.forceActiveFocus();
|
||||
}
|
||||
function onDialogClosed() {
|
||||
passwordInput.clearFocus();
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
WifiService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
passwordInput.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show password checkbox
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: showPasswordCheckbox
|
||||
property bool checked: false
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: checked ? Theme.primary : "transparent"
|
||||
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
|
||||
border.width: 2
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 12
|
||||
color: Theme.background
|
||||
visible: parent.checked
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Show password"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: cancelText
|
||||
anchors.centerIn: parent
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: passwordInput.text.length > 0
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
id: connectText
|
||||
anchors.centerIn: parent
|
||||
text: "Connect"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
WifiService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
passwordInput.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-reopen dialog on invalid password
|
||||
Connections {
|
||||
target: WifiService
|
||||
function onPasswordDialogShouldReopenChanged() {
|
||||
if (WifiService.passwordDialogShouldReopen && WifiService.connectingSSID !== "") {
|
||||
wifiPasswordSSID = WifiService.connectingSSID;
|
||||
@@ -249,5 +35,241 @@ DankModal {
|
||||
WifiService.passwordDialogShouldReopen = false;
|
||||
}
|
||||
}
|
||||
|
||||
target: WifiService
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "Connect to Wi-Fi"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Enter password for \"" + wifiPasswordSSID + "\""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Password input
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
border.color: passwordInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: passwordInput.activeFocus ? 2 : 1
|
||||
|
||||
DankTextField {
|
||||
id: passwordInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiPasswordInput
|
||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: "Enter password"
|
||||
backgroundColor: "transparent"
|
||||
normalBorderColor: "transparent"
|
||||
focusedBorderColor: "transparent"
|
||||
onTextEdited: {
|
||||
wifiPasswordInput = text;
|
||||
}
|
||||
onAccepted: {
|
||||
WifiService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
passwordInput.text = "";
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onOpened() {
|
||||
passwordInput.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
passwordInput.clearFocus();
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Show password checkbox
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: showPasswordCheckbox
|
||||
|
||||
property bool checked: false
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: checked ? Theme.primary : "transparent"
|
||||
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
|
||||
border.width: 2
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 12
|
||||
color: Theme.background
|
||||
visible: parent.checked
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Show password"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: cancelText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: passwordInput.text.length > 0
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
id: connectText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "Connect"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
WifiService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
passwordInput.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user