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:
@@ -115,7 +115,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
- Services handle system commands, state management, and hardware integration
|
- Services handle system commands, state management, and hardware integration
|
||||||
|
|
||||||
4. **Modules/** - UI components
|
4. **Modules/** - UI components
|
||||||
- **Full-screen components**: AppLauncher, ClipboardHistory, ControlCenter
|
- **Full-screen components**: AppLauncher, ClipboardHistoryModal, ControlCenter
|
||||||
- **Panel components**: TopBar, SystemTrayWidget, NotificationPopup
|
- **Panel components**: TopBar, SystemTrayWidget, NotificationPopup
|
||||||
- **Layout components**: WorkspaceSwitcher
|
- **Layout components**: WorkspaceSwitcher
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
|
|
||||||
- **ControlCenter**: System controls (WiFi, Bluetooth, brightness, volume, night mode)
|
- **ControlCenter**: System controls (WiFi, Bluetooth, brightness, volume, night mode)
|
||||||
- **AppLauncher**: Full-featured app grid/list with 93+ applications, search, categories
|
- **AppLauncher**: Full-featured app grid/list with 93+ applications, search, categories
|
||||||
- **ClipboardHistory**: Complete clipboard management with cliphist integration
|
- **ClipboardHistoryModal**: Complete clipboard management with cliphist integration
|
||||||
- **TopBar**: Per-monitor panels with workspace switching, clock, system tray
|
- **TopBar**: Per-monitor panels with workspace switching, clock, system tray
|
||||||
|
|
||||||
#### Key Widgets
|
#### Key Widgets
|
||||||
|
|||||||
@@ -369,11 +369,10 @@ PanelWindow {
|
|||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (launcher.isVisible) {
|
if (launcher.isVisible)
|
||||||
forceActiveFocus();
|
forceActiveFocus();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Handle keyboard shortcuts
|
// Handle keyboard shortcuts
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
@@ -458,17 +457,6 @@ PanelWindow {
|
|||||||
onTextEdited: {
|
onTextEdited: {
|
||||||
searchDebounceTimer.restart();
|
searchDebounceTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: launcher
|
|
||||||
function onVisibleChanged() {
|
|
||||||
if (launcher.visible) {
|
|
||||||
searchField.forceActiveFocus();
|
|
||||||
} else {
|
|
||||||
searchField.clearFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count && text.length > 0) {
|
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count && text.length > 0) {
|
||||||
// Launch first app when typing in search field
|
// Launch first app when typing in search field
|
||||||
@@ -481,14 +469,23 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
launcher.hide();
|
launcher.hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
|
} 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)) {
|
||||||
(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
|
// Pass navigation keys and enter (when not searching) to main handler
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onVisibleChanged() {
|
||||||
|
if (launcher.visible)
|
||||||
|
searchField.forceActiveFocus();
|
||||||
|
else
|
||||||
|
searchField.clearFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: launcher
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category filter and view mode controls
|
// Category filter and view mode controls
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ PanelWindow {
|
|||||||
|
|
||||||
visible: calendarVisible
|
visible: calendarVisible
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible && CalendarService) {
|
if (visible && CalendarService)
|
||||||
CalendarService.loadCurrentMonth();
|
CalendarService.loadCurrentMonth();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
implicitWidth: 480
|
implicitWidth: 480
|
||||||
implicitHeight: 600
|
implicitHeight: 600
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ Rectangle {
|
|||||||
interval: 2000
|
interval: 2000
|
||||||
running: {
|
running: {
|
||||||
// Run when no active player (for cache clearing) OR when playing (for position updates)
|
// Run when no active player (for cache clearing) OR when playing (for position updates)
|
||||||
return (!activePlayer) ||
|
return (!activePlayer) || (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking);
|
||||||
(activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing &&
|
|
||||||
activePlayer.length > 0 && !progressMouseArea.isSeeking);
|
|
||||||
}
|
}
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ import QtQuick.Controls
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
DankModal {
|
DankModal {
|
||||||
id: clipboardHistory
|
// Don't hide the interface, just show toast
|
||||||
|
|
||||||
|
id: clipboardHistoryModal
|
||||||
|
|
||||||
property bool isVisible: false
|
property bool isVisible: false
|
||||||
property int totalCount: 0
|
property int totalCount: 0
|
||||||
property var activeTheme: Theme
|
property var activeTheme: Theme
|
||||||
property bool showClearConfirmation: false
|
property bool showClearConfirmation: false
|
||||||
property var clipboardEntries: []
|
property var clipboardEntries: []
|
||||||
|
|
||||||
property string searchText: ""
|
property string searchText: ""
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
@@ -33,7 +35,7 @@ DankModal {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clipboardHistory.totalCount = filteredClipboardModel.count;
|
clipboardHistoryModal.totalCount = filteredClipboardModel.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
@@ -44,14 +46,14 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
clipboardHistory.isVisible = true;
|
clipboardHistoryModal.isVisible = true;
|
||||||
refreshClipboard();
|
refreshClipboard();
|
||||||
console.log("ClipboardHistory: Opening and refreshing");
|
console.log("ClipboardHistoryModal: Opening and refreshing");
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
clipboardHistory.isVisible = false;
|
clipboardHistoryModal.isVisible = false;
|
||||||
clipboardHistory.searchText = "";
|
clipboardHistoryModal.searchText = "";
|
||||||
cleanupTempFiles();
|
cleanupTempFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +70,8 @@ DankModal {
|
|||||||
const entryId = entry.split('\t')[0];
|
const entryId = entry.split('\t')[0];
|
||||||
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`];
|
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`];
|
||||||
copyProcess.running = true;
|
copyProcess.running = true;
|
||||||
console.log("ClipboardHistory: Entry copied, hiding interface");
|
console.log("ClipboardHistoryModal: Entry copied, showing toast");
|
||||||
hide();
|
ToastService.showInfo("Copied to clipboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEntry(entry) {
|
function deleteEntry(entry) {
|
||||||
@@ -116,11 +118,217 @@ DankModal {
|
|||||||
width: 650
|
width: 650
|
||||||
height: 550
|
height: 550
|
||||||
keyboardFocus: "ondemand"
|
keyboardFocus: "ondemand"
|
||||||
|
|
||||||
onBackgroundClicked: {
|
onBackgroundClicked: {
|
||||||
hide();
|
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 {
|
content: Component {
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -152,6 +360,7 @@ DankModal {
|
|||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -160,8 +369,8 @@ DankModal {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
iconName: "delete"
|
iconName: "delete_sweep"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize
|
||||||
iconColor: Theme.error
|
iconColor: Theme.error
|
||||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -176,33 +385,39 @@ DankModal {
|
|||||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||||
onClicked: hide()
|
onClicked: hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search field
|
// Search field
|
||||||
DankTextField {
|
DankTextField {
|
||||||
id: searchField
|
id: searchField
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
placeholderText: "Search clipboard history..."
|
placeholderText: "Search clipboard history..."
|
||||||
leftIconName: "search"
|
leftIconName: "search"
|
||||||
showClearButton: true
|
showClearButton: true
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
clipboardHistory.searchText = text;
|
clipboardHistoryModal.searchText = text;
|
||||||
updateFilteredModel();
|
updateFilteredModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: clipboardHistory
|
|
||||||
function onOpened() {
|
function onOpened() {
|
||||||
searchField.forceActiveFocus();
|
searchField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDialogClosed() {
|
function onDialogClosed() {
|
||||||
searchField.clearFocus();
|
searchField.clearFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: clipboardHistoryModal
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clipboard entries list
|
// Clipboard entries list
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 110
|
height: parent.height - 110
|
||||||
@@ -219,270 +434,158 @@ DankModal {
|
|||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: clipboardListView
|
id: clipboardListView
|
||||||
|
|
||||||
width: parent.availableWidth
|
width: parent.availableWidth
|
||||||
model: filteredClipboardModel
|
model: filteredClipboardModel
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
delegate: Rectangle {
|
Text {
|
||||||
width: clipboardListView.width
|
text: "No clipboard entries found"
|
||||||
height: Math.max(60, contentText.contentHeight + Theme.spacingL)
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
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)
|
color: Theme.surfaceVariantText
|
||||||
|
visible: filteredClipboardModel.count === 0
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
delegate: Rectangle {
|
||||||
anchors.fill: parent
|
property string entryType: getEntryType(model.entry)
|
||||||
anchors.margins: Theme.spacingM
|
property string entryPreview: getEntryPreview(model.entry)
|
||||||
color: "transparent"
|
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 {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.fill: parent
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.margins: Theme.spacingM
|
||||||
spacing: Theme.spacingM
|
anchors.rightMargin: Theme.spacingS // Reduced right margin
|
||||||
|
spacing: Theme.spacingL
|
||||||
DankIcon {
|
|
||||||
name: {
|
// Index number
|
||||||
const type = getEntryType(model.entry);
|
Rectangle {
|
||||||
if (type === "image") return "image";
|
width: 24
|
||||||
if (type === "long_text") return "subject";
|
height: 24
|
||||||
return "content_copy";
|
radius: 12
|
||||||
}
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: entryIndex.toString()
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
// Content icon and text
|
||||||
id: contentText
|
Row {
|
||||||
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
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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 {
|
DankActionButton {
|
||||||
iconName: "delete"
|
anchors.right: parent.right
|
||||||
iconSize: 16
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "dangerous"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: Theme.error
|
iconColor: Theme.error
|
||||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
console.log("Delete clicked for entry:", model.entry);
|
||||||
deleteEntry(model.entry);
|
deleteEntry(model.entry);
|
||||||
refreshClipboard();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
// Main click area - explicitly excludes delete button area
|
||||||
id: mouseArea
|
MouseArea {
|
||||||
anchors.fill: parent
|
id: mouseArea
|
||||||
hoverEnabled: true
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
anchors.rightMargin: 40 // Enough space to avoid delete button (32 + 8 margin)
|
||||||
onClicked: copyEntry(model.entry)
|
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Services.Pipewire
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modules.ControlCenter.Audio
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import "../../Widgets"
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: audioTab
|
id: audioTab
|
||||||
|
|
||||||
property int audioSubTab: 0 // 0: Output, 1: Input
|
property int audioSubTab: 0
|
||||||
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) : ""
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
// Audio Sub-tabs
|
|
||||||
DankTabBar {
|
DankTabBar {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
tabHeight: 40
|
tabHeight: 40
|
||||||
currentIndex: audioTab.audioSubTab
|
currentIndex: audioTab.audioSubTab
|
||||||
showIcons: false
|
showIcons: false
|
||||||
model: [
|
model: [{
|
||||||
{
|
"text": "Output"
|
||||||
"text": "Output"
|
}, {
|
||||||
},
|
"text": "Input"
|
||||||
{
|
}]
|
||||||
"text": "Input"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
onTabClicked: function(index) {
|
onTabClicked: function(index) {
|
||||||
audioTab.audioSubTab = index;
|
audioTab.audioSubTab = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output Tab Content
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 48
|
height: parent.height - 48
|
||||||
@@ -54,321 +43,16 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
// Volume Control
|
VolumeControl {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output Devices
|
AudioDevicesList {
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Tab Content
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 48
|
height: parent.height - 48
|
||||||
@@ -379,312 +63,10 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
// Microphone Level Control
|
MicrophoneControl {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Devices
|
AudioInputDevicesList {
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
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.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Bluetooth
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: bluetoothTab
|
id: bluetoothTab
|
||||||
@@ -21,800 +22,19 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
Rectangle {
|
BluetoothToggle { }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
PairedDevicesList {
|
||||||
|
bluetoothContextMenuWindow: bluetoothContextMenuWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvailableDevicesList { }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
BluetoothContextMenu {
|
||||||
id: bluetoothContextMenuWindow
|
id: bluetoothContextMenuWindow
|
||||||
|
parentItem: bluetoothTab
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -832,7 +52,5 @@ Item {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "../../Widgets"
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
@@ -9,7 +10,6 @@ import Quickshell.Widgets
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import "../../Widgets"
|
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
@@ -25,13 +25,13 @@ PanelWindow {
|
|||||||
// Enable/disable WiFi auto-refresh based on control center visibility
|
// Enable/disable WiFi auto-refresh based on control center visibility
|
||||||
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled;
|
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled;
|
||||||
// Stop bluetooth scanning when control center is closed
|
// 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;
|
BluetoothService.adapter.discovering = false;
|
||||||
}
|
|
||||||
// Refresh uptime when opened
|
// Refresh uptime when opened
|
||||||
if (visible && UserInfoService) {
|
if (visible && UserInfoService)
|
||||||
UserInfoService.getUptime();
|
UserInfoService.getUptime();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
implicitWidth: 600
|
implicitWidth: 600
|
||||||
implicitHeight: 500
|
implicitHeight: 500
|
||||||
@@ -296,6 +296,7 @@ PanelWindow {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: dankIcon
|
id: dankIcon
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
name: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
@@ -617,47 +618,48 @@ PanelWindow {
|
|||||||
let tabs = ["network", "audio"];
|
let tabs = ["network", "audio"];
|
||||||
if (BluetoothService.available)
|
if (BluetoothService.available)
|
||||||
tabs.push("bluetooth");
|
tabs.push("bluetooth");
|
||||||
|
|
||||||
tabs.push("display");
|
tabs.push("display");
|
||||||
return tabs.indexOf(root.currentTab);
|
return tabs.indexOf(root.currentTab);
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
let tabs = [{
|
let tabs = [{
|
||||||
"text": "Network",
|
"text": "Network",
|
||||||
"icon": "wifi",
|
"icon": "wifi",
|
||||||
"id": "network"
|
"id": "network"
|
||||||
}];
|
}];
|
||||||
// Always show audio
|
// Always show audio
|
||||||
|
tabs.push({
|
||||||
|
"text": "Audio",
|
||||||
|
"icon": "volume_up",
|
||||||
|
"id": "audio"
|
||||||
|
});
|
||||||
|
// Show Bluetooth only if available
|
||||||
|
if (BluetoothService.available)
|
||||||
tabs.push({
|
tabs.push({
|
||||||
"text": "Audio",
|
"text": "Bluetooth",
|
||||||
"icon": "volume_up",
|
"icon": "bluetooth",
|
||||||
"id": "audio"
|
"id": "bluetooth"
|
||||||
});
|
});
|
||||||
// Show Bluetooth only if available
|
|
||||||
if (BluetoothService.available)
|
|
||||||
tabs.push({
|
|
||||||
"text": "Bluetooth",
|
|
||||||
"icon": "bluetooth",
|
|
||||||
"id": "bluetooth"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always show display
|
// Always show display
|
||||||
tabs.push({
|
tabs.push({
|
||||||
"text": "Display",
|
"text": "Display",
|
||||||
"icon": "brightness_6",
|
"icon": "brightness_6",
|
||||||
"id": "display"
|
"id": "display"
|
||||||
});
|
});
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
onTabClicked: function(index) {
|
onTabClicked: function(index) {
|
||||||
let tabs = ["network", "audio"];
|
let tabs = ["network", "audio"];
|
||||||
if (BluetoothService.available)
|
if (BluetoothService.available)
|
||||||
tabs.push("bluetooth");
|
tabs.push("bluetooth");
|
||||||
|
|
||||||
tabs.push("display");
|
tabs.push("display");
|
||||||
root.currentTab = tabs[index];
|
root.currentTab = tabs[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab content area
|
// 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.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import "../../Widgets"
|
import qs.Modules.ControlCenter.Network
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: networkTab
|
id: networkTab
|
||||||
|
|
||||||
// Helper function for consistent WiFi signal icons
|
property var wifiPasswordDialogRef: wifiPasswordDialog
|
||||||
function getWiFiSignalIcon(signalStrength) {
|
property var networkInfoDialogRef: networkInfoDialog
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Properly sorted WiFi networks with connected networks first
|
// Properly sorted WiFi networks with connected networks first
|
||||||
property var sortedWifiNetworks: {
|
property var sortedWifiNetworks: {
|
||||||
@@ -92,7 +84,6 @@ Item {
|
|||||||
height: parent.height
|
height: parent.height
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
|
||||||
// WiFi Content in Flickable
|
// WiFi Content in Flickable
|
||||||
Flickable {
|
Flickable {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -106,165 +97,13 @@ Item {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: wifiContent
|
id: wifiContent
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
// Current WiFi connection status card
|
// Current WiFi connection status card
|
||||||
Rectangle {
|
WiFiCard {
|
||||||
id: wifiCard
|
refreshTimer: refreshTimer
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
@@ -279,8 +118,6 @@ Item {
|
|||||||
height: parent.height
|
height: parent.height
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
// Ethernet Header removed
|
|
||||||
|
|
||||||
// Ethernet Content in Flickable
|
// Ethernet Content in Flickable
|
||||||
Flickable {
|
Flickable {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -294,124 +131,12 @@ Item {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: ethernetContent
|
id: ethernetContent
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
// Ethernet connection status card (matching WiFi height)
|
// Ethernet connection status card (matching WiFi height)
|
||||||
Rectangle {
|
EthernetCard {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
@@ -459,253 +184,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WiFi networks spanning across both columns when Ethernet preference button is hidden
|
// WiFi networks spanning across both columns when WiFi is enabled
|
||||||
Column {
|
WiFiNetworksList {
|
||||||
anchors.top: parent.top
|
wifiContextMenuWindow: wifiContextMenuWindow
|
||||||
anchors.topMargin: 100
|
sortedWifiNetworks: networkTab.sortedWifiNetworks
|
||||||
anchors.left: parent.left
|
wifiPasswordDialogRef: networkTab.wifiPasswordDialogRef
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer for refreshing network status after WiFi toggle
|
// Timer for refreshing network status after WiFi toggle
|
||||||
@@ -826,259 +309,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WiFi Context Menu Window
|
// WiFi Context Menu Window
|
||||||
Rectangle {
|
WiFiContextMenu {
|
||||||
id: wifiContextMenuWindow
|
id: wifiContextMenuWindow
|
||||||
|
parentItem: networkTab
|
||||||
property var networkData: null
|
wifiPasswordDialogRef: networkTab.wifiPasswordDialogRef
|
||||||
property bool menuVisible: false
|
networkInfoDialogRef: networkTab.networkInfoDialogRef
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background MouseArea to close the context menu
|
// Background MouseArea to close the context menu
|
||||||
|
|||||||
@@ -408,7 +408,9 @@ PanelWindow {
|
|||||||
font.pixelSize: 9
|
font.pixelSize: 9
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -465,6 +467,8 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
// No truncation for notification center - show full text
|
||||||
|
|
||||||
property bool hasUrls: {
|
property bool hasUrls: {
|
||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
return urlRegex.test(modelData.latestNotification.body);
|
return urlRegex.test(modelData.latestNotification.body);
|
||||||
@@ -473,8 +477,6 @@ PanelWindow {
|
|||||||
text: {
|
text: {
|
||||||
// Auto-detect and make URLs clickable, with truncation for center notifications
|
// Auto-detect and make URLs clickable, with truncation for center notifications
|
||||||
let bodyText = modelData.latestNotification.body;
|
let bodyText = modelData.latestNotification.body;
|
||||||
// No truncation for notification center - show full text
|
|
||||||
|
|
||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
return bodyText.replace(urlRegex, '<a href="$1" style="color: ' + Theme.primary + '; text-decoration: underline;">$1</a>');
|
return bodyText.replace(urlRegex, '<a href="$1" style="color: ' + Theme.primary + '; text-decoration: underline;">$1</a>');
|
||||||
}
|
}
|
||||||
@@ -490,8 +492,11 @@ PanelWindow {
|
|||||||
Qt.openUrlExternally(link);
|
Qt.openUrlExternally(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -507,6 +512,7 @@ PanelWindow {
|
|||||||
// Expand button - always takes up space but only visible when needed
|
// Expand button - always takes up space but only visible when needed
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: collapsedExpandButton
|
id: collapsedExpandButton
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
width: 20
|
width: 20
|
||||||
@@ -524,11 +530,13 @@ PanelWindow {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: expandArea
|
id: expandArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
|
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close button - always positioned at the right edge
|
// Close button - always positioned at the right edge
|
||||||
@@ -569,8 +577,11 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
onClicked: NotificationService.dismissGroup(modelData.key)
|
onClicked: NotificationService.dismissGroup(modelData.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expanded view - shows all notifications stacked
|
// Expanded view - shows all notifications stacked
|
||||||
@@ -836,67 +847,8 @@ PanelWindow {
|
|||||||
height: contentColumn.height
|
height: contentColumn.height
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: contentColumn
|
// COMMENTED OUT: Individual inline reply
|
||||||
|
/*
|
||||||
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
|
|
||||||
/*
|
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
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);
|
return Prefs.setClockFormat(checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import qs.Widgets
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
@@ -158,4 +159,5 @@ Column {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ Column {
|
|||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
Prefs.setProfileImage(text);
|
Prefs.setProfileImage(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -164,4 +163,5 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,22 +11,27 @@ DankModal {
|
|||||||
signal closingModal()
|
signal closingModal()
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (!visible) {
|
if (!visible)
|
||||||
closingModal();
|
closingModal();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
// DankModal configuration
|
// DankModal configuration
|
||||||
visible: settingsVisible
|
visible: settingsVisible
|
||||||
width: 650
|
width: 650
|
||||||
height: 750
|
height: 750
|
||||||
keyboardFocus: "ondemand"
|
keyboardFocus: "ondemand"
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
|
|
||||||
onBackgroundClicked: {
|
onBackgroundClicked: {
|
||||||
settingsVisible = false;
|
settingsVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keyboard focus and shortcuts
|
||||||
|
FocusScope {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: settingsModal.settingsVisible
|
||||||
|
Keys.onEscapePressed: settingsModal.settingsVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -73,6 +78,7 @@ DankModal {
|
|||||||
// Settings sections
|
// Settings sections
|
||||||
ScrollView {
|
ScrollView {
|
||||||
id: settingsScrollView
|
id: settingsScrollView
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 50
|
height: parent.height - 50
|
||||||
clip: true
|
clip: true
|
||||||
@@ -81,6 +87,7 @@ DankModal {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: settingsColumn
|
id: settingsColumn
|
||||||
|
|
||||||
width: settingsScrollView.width - 20
|
width: settingsScrollView.width - 20
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
bottomPadding: Theme.spacingL
|
bottomPadding: Theme.spacingL
|
||||||
@@ -89,42 +96,60 @@ DankModal {
|
|||||||
SettingsSection {
|
SettingsSection {
|
||||||
title: "Profile"
|
title: "Profile"
|
||||||
iconName: "person"
|
iconName: "person"
|
||||||
content: ProfileTab {}
|
|
||||||
|
content: ProfileTab {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clock Settings
|
// Clock Settings
|
||||||
SettingsSection {
|
SettingsSection {
|
||||||
title: "Clock & Time"
|
title: "Clock & Time"
|
||||||
iconName: "schedule"
|
iconName: "schedule"
|
||||||
content: ClockTab {}
|
|
||||||
|
content: ClockTab {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weather Settings
|
// Weather Settings
|
||||||
SettingsSection {
|
SettingsSection {
|
||||||
title: "Weather"
|
title: "Weather"
|
||||||
iconName: "wb_sunny"
|
iconName: "wb_sunny"
|
||||||
content: WeatherTab {}
|
|
||||||
|
content: WeatherTab {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget Visibility Settings
|
// Widget Visibility Settings
|
||||||
SettingsSection {
|
SettingsSection {
|
||||||
title: "Top Bar Widgets"
|
title: "Top Bar Widgets"
|
||||||
iconName: "widgets"
|
iconName: "widgets"
|
||||||
content: WidgetsTab {}
|
|
||||||
|
content: WidgetsTab {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workspace Settings
|
// Workspace Settings
|
||||||
SettingsSection {
|
SettingsSection {
|
||||||
title: "Workspaces"
|
title: "Workspaces"
|
||||||
iconName: "tab"
|
iconName: "tab"
|
||||||
content: WorkspaceTab {}
|
|
||||||
|
content: WorkspaceTab {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display Settings
|
// Display Settings
|
||||||
SettingsSection {
|
SettingsSection {
|
||||||
title: "Display & Appearance"
|
title: "Display & Appearance"
|
||||||
iconName: "palette"
|
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
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,49 +14,44 @@ Column {
|
|||||||
return Prefs.setTemperatureUnit(checked);
|
return Prefs.setTemperatureUnit(checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weather Location Override
|
// Weather Location Override
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
DankToggle {
|
DankToggle {
|
||||||
text: "Override Location"
|
text: "Override Location"
|
||||||
description: "Use a specific location instead of auto-detection"
|
description: "Use a specific location instead of auto-detection"
|
||||||
checked: Prefs.weatherLocationOverrideEnabled
|
checked: Prefs.weatherLocationOverrideEnabled
|
||||||
onToggled: (checked) => Prefs.setWeatherLocationOverrideEnabled(checked)
|
onToggled: (checked) => {
|
||||||
|
return Prefs.setWeatherLocationOverrideEnabled(checked);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Location input - only visible when override is enabled
|
// Location input - only visible when override is enabled
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
visible: Prefs.weatherLocationOverrideEnabled
|
visible: Prefs.weatherLocationOverrideEnabled
|
||||||
opacity: visible ? 1.0 : 0.0
|
opacity: visible ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Location"
|
text: "Location"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
|
|
||||||
DankLocationSearch {
|
DankLocationSearch {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
currentLocation: Prefs.weatherLocationOverride
|
currentLocation: Prefs.weatherLocationOverride
|
||||||
placeholderText: "Search for a location..."
|
placeholderText: "Search for a location..."
|
||||||
onLocationSelected: (displayName, coordinates) => {
|
onLocationSelected: (displayName, coordinates) => {
|
||||||
Prefs.setWeatherLocationOverride(coordinates)
|
Prefs.setWeatherLocationOverride(coordinates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Examples: \"New York\", \"Tokyo\", \"44511\""
|
text: "Examples: \"New York\", \"Tokyo\", \"44511\""
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -64,6 +59,17 @@ Column {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,13 +109,13 @@ Column {
|
|||||||
bottomPadding: Theme.spacingS
|
bottomPadding: Theme.spacingS
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
var color = text.trim();
|
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);
|
Prefs.setOSLogoColorOverride(color);
|
||||||
} else {
|
else
|
||||||
text = Prefs.osLogoColorOverride;
|
text = Prefs.osLogoColorOverride;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -139,9 +139,10 @@ Column {
|
|||||||
unit: ""
|
unit: ""
|
||||||
showValue: false
|
showValue: false
|
||||||
onSliderValueChanged: (newValue) => {
|
onSliderValueChanged: (newValue) => {
|
||||||
Prefs.setOSLogoBrightness(newValue / 100.0);
|
Prefs.setOSLogoBrightness(newValue / 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -165,9 +166,12 @@ Column {
|
|||||||
unit: ""
|
unit: ""
|
||||||
showValue: false
|
showValue: false
|
||||||
onSliderValueChanged: (newValue) => {
|
onSliderValueChanged: (newValue) => {
|
||||||
Prefs.setOSLogoContrast(newValue / 100.0);
|
Prefs.setOSLogoContrast(newValue / 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ Column {
|
|||||||
return Prefs.setShowWorkspacePadding(checked);
|
return Prefs.setShowWorkspacePadding(checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,15 +9,20 @@ Rectangle {
|
|||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
|
|
||||||
signal clicked()
|
signal clicked()
|
||||||
|
|
||||||
// Helper function for consistent WiFi signal icons
|
// Helper function for consistent WiFi signal icons
|
||||||
function getWiFiSignalIcon(signalStrength) {
|
function getWiFiSignalIcon(signalStrength) {
|
||||||
switch (signalStrength) {
|
switch (signalStrength) {
|
||||||
case "excellent": return "wifi";
|
case "excellent":
|
||||||
case "good": return "wifi_2_bar";
|
return "wifi";
|
||||||
case "fair": return "wifi_1_bar";
|
case "good":
|
||||||
case "poor": return "signal_wifi_0_bar";
|
return "wifi_2_bar";
|
||||||
default: return "wifi";
|
case "fair":
|
||||||
|
return "wifi_1_bar";
|
||||||
|
case "poor":
|
||||||
|
return "signal_wifi_0_bar";
|
||||||
|
default:
|
||||||
|
return "wifi";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,13 +40,12 @@ Rectangle {
|
|||||||
// Network Status Icon
|
// Network Status Icon
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: {
|
name: {
|
||||||
if (NetworkService.networkStatus === "ethernet") {
|
if (NetworkService.networkStatus === "ethernet")
|
||||||
return "lan";
|
return "lan";
|
||||||
} else if (NetworkService.networkStatus === "wifi") {
|
else if (NetworkService.networkStatus === "wifi")
|
||||||
return getWiFiSignalIcon(WifiService.wifiSignalStrength);
|
return getWiFiSignalIcon(WifiService.wifiSignalStrength);
|
||||||
} else {
|
else
|
||||||
return "wifi_off";
|
return "wifi_off";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
size: Theme.iconSize - 8
|
size: Theme.iconSize - 8
|
||||||
color: NetworkService.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
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 {
|
DankIcon {
|
||||||
id: audioIcon
|
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
|
size: Theme.iconSize - 8
|
||||||
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import qs.Widgets
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
signal clicked()
|
|
||||||
|
|
||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
width: 40
|
width: 40
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import Quickshell.Services.SystemTray
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modules
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Modules
|
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
// Proxy objects for external connections
|
// Proxy objects for external connections
|
||||||
@@ -29,12 +29,11 @@ PanelWindow {
|
|||||||
screen: modelData
|
screen: modelData
|
||||||
implicitHeight: Theme.barHeight - 4
|
implicitHeight: Theme.barHeight - 4
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
let fonts = Qt.fontFamilies();
|
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");
|
ToastService.showError("Please install Material Symbols Rounded and Restart your Shell. See README.md for instructions");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -226,7 +225,7 @@ PanelWindow {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
clipboardHistoryPopup.toggle();
|
clipboardHistoryModalPopup.toggle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,22 +16,20 @@ Rectangle {
|
|||||||
|
|
||||||
function padWorkspaces(list) {
|
function padWorkspaces(list) {
|
||||||
var padded = list.slice();
|
var padded = list.slice();
|
||||||
while (padded.length < 3) {
|
while (padded.length < 3)padded.push(-1) // Use -1 as a placeholder
|
||||||
padded.push(-1); // Use -1 as a placeholder
|
|
||||||
}
|
|
||||||
return padded;
|
return padded;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayWorkspaces() {
|
function getDisplayWorkspaces() {
|
||||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
|
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
|
||||||
return [1, 2];
|
return [1, 2];
|
||||||
|
|
||||||
if (!root.screenName)
|
if (!root.screenName)
|
||||||
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers();
|
return NiriService.getCurrentOutputWorkspaceNumbers();
|
||||||
|
|
||||||
var displayWorkspaces = [];
|
var displayWorkspaces = [];
|
||||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||||
var ws = NiriWorkspaceService.allWorkspaces[i];
|
var ws = NiriService.allWorkspaces[i];
|
||||||
if (ws.output === root.screenName)
|
if (ws.output === root.screenName)
|
||||||
displayWorkspaces.push(ws.idx + 1);
|
displayWorkspaces.push(ws.idx + 1);
|
||||||
|
|
||||||
@@ -40,14 +38,14 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayActiveWorkspace() {
|
function getDisplayActiveWorkspace() {
|
||||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
|
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
if (!root.screenName)
|
if (!root.screenName)
|
||||||
return NiriWorkspaceService.getCurrentWorkspaceNumber();
|
return NiriService.getCurrentWorkspaceNumber();
|
||||||
|
|
||||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||||
var ws = NiriWorkspaceService.allWorkspaces[i];
|
var ws = NiriService.allWorkspaces[i];
|
||||||
if (ws.output === root.screenName && ws.is_active)
|
if (ws.output === root.screenName && ws.is_active)
|
||||||
return ws.idx + 1;
|
return ws.idx + 1;
|
||||||
|
|
||||||
@@ -59,7 +57,7 @@ Rectangle {
|
|||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadiusLarge
|
radius: Theme.cornerRadiusLarge
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||||
visible: NiriWorkspaceService.niriAvailable
|
visible: NiriService.niriAvailable
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onAllWorkspacesChanged() {
|
function onAllWorkspacesChanged() {
|
||||||
@@ -72,13 +70,13 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onNiriAvailableChanged() {
|
function onNiriAvailableChanged() {
|
||||||
if (NiriWorkspaceService.niriAvailable) {
|
if (NiriService.niriAvailable) {
|
||||||
root.workspaceList = Prefs.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces();
|
root.workspaceList = Prefs.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces();
|
||||||
root.currentWorkspace = root.getDisplayActiveWorkspace();
|
root.currentWorkspace = root.getDisplayActiveWorkspace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target: NiriWorkspaceService
|
target: NiriService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force update when padding preference changes
|
// Force update when padding preference changes
|
||||||
@@ -92,7 +90,6 @@ Rectangle {
|
|||||||
target: Prefs
|
target: Prefs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: workspaceRow
|
id: workspaceRow
|
||||||
|
|
||||||
@@ -115,14 +112,15 @@ Rectangle {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: !isPlaceholder
|
hoverEnabled: !isPlaceholder
|
||||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||||
enabled: !isPlaceholder
|
enabled: !isPlaceholder
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!isPlaceholder) {
|
if (!isPlaceholder)
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]);
|
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,12 +134,12 @@ Rectangle {
|
|||||||
font.bold: isActive && !isPlaceholder
|
font.bold: isActive && !isPlaceholder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.emphasizedEasing
|
easing.type: Theme.emphasizedEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
@@ -149,8 +147,11 @@ Rectangle {
|
|||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.emphasizedEasing
|
easing.type: Theme.emphasizedEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 1
|
spacing: 1
|
||||||
|
model: menuOpener.children
|
||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: textMetrics
|
id: textMetrics
|
||||||
@@ -93,8 +94,6 @@ PanelWindow {
|
|||||||
text: "M"
|
text: "M"
|
||||||
}
|
}
|
||||||
|
|
||||||
model: menuOpener.children
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: ListView.view.width
|
width: ListView.view.width
|
||||||
height: modelData.isSeparator ? 5 : 28
|
height: modelData.isSeparator ? 5 : 28
|
||||||
|
|||||||
@@ -15,232 +15,18 @@ DankModal {
|
|||||||
width: 420
|
width: 420
|
||||||
height: 230
|
height: 230
|
||||||
keyboardFocus: "ondemand"
|
keyboardFocus: "ondemand"
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (!visible) {
|
if (!visible)
|
||||||
wifiPasswordInput = "";
|
wifiPasswordInput = "";
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
onBackgroundClicked: {
|
onBackgroundClicked: {
|
||||||
wifiPasswordDialogVisible = false;
|
wifiPasswordDialogVisible = false;
|
||||||
wifiPasswordInput = "";
|
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
|
// Auto-reopen dialog on invalid password
|
||||||
Connections {
|
Connections {
|
||||||
target: WifiService
|
|
||||||
function onPasswordDialogShouldReopenChanged() {
|
function onPasswordDialogShouldReopenChanged() {
|
||||||
if (WifiService.passwordDialogShouldReopen && WifiService.connectingSSID !== "") {
|
if (WifiService.passwordDialogShouldReopen && WifiService.connectingSSID !== "") {
|
||||||
wifiPasswordSSID = WifiService.connectingSSID;
|
wifiPasswordSSID = WifiService.connectingSSID;
|
||||||
@@ -249,5 +35,241 @@ DankModal {
|
|||||||
WifiService.passwordDialogShouldReopen = false;
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let focusedWindow = NiriWorkspaceService.windows.find(w => w.is_focused);
|
let focusedWindow = NiriService.windows.find(w => w.is_focused);
|
||||||
|
|
||||||
if (focusedWindow) {
|
if (focusedWindow) {
|
||||||
root.focusedAppId = focusedWindow.app_id || "";
|
root.focusedAppId = focusedWindow.app_id || "";
|
||||||
@@ -76,9 +76,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
root.niriAvailable = NiriWorkspaceService.niriAvailable;
|
root.niriAvailable = NiriService.niriAvailable;
|
||||||
NiriWorkspaceService.onNiriAvailableChanged.connect(() => {
|
NiriService.onNiriAvailableChanged.connect(() => {
|
||||||
root.niriAvailable = NiriWorkspaceService.niriAvailable;
|
root.niriAvailable = NiriService.niriAvailable;
|
||||||
if (root.niriAvailable)
|
if (root.niriAvailable)
|
||||||
updateFromNiriData();
|
updateFromNiriData();
|
||||||
|
|
||||||
@@ -90,13 +90,13 @@ Singleton {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onFocusedWindowIdChanged() {
|
function onFocusedWindowIdChanged() {
|
||||||
const focusedWindowId = NiriWorkspaceService.focusedWindowId;
|
const focusedWindowId = NiriService.focusedWindowId;
|
||||||
if (!focusedWindowId) {
|
if (!focusedWindowId) {
|
||||||
clearFocusedWindow();
|
clearFocusedWindow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusedWindow = NiriWorkspaceService.windows.find(w => w.id == focusedWindowId);
|
const focusedWindow = NiriService.windows.find(w => w.id == focusedWindowId);
|
||||||
if (focusedWindow) {
|
if (focusedWindow) {
|
||||||
root.focusedAppId = focusedWindow.app_id || "";
|
root.focusedAppId = focusedWindow.app_id || "";
|
||||||
root.focusedWindowTitle = focusedWindow.title || "";
|
root.focusedWindowTitle = focusedWindow.title || "";
|
||||||
@@ -120,7 +120,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target: NiriWorkspaceService
|
target: NiriService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Singleton {
|
|||||||
property bool niriAvailable: false
|
property bool niriAvailable: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
console.log("NiriWorkspaceService: Component.onCompleted - initializing service")
|
console.log("NiriService: Component.onCompleted - initializing service")
|
||||||
checkNiriAvailability()
|
checkNiriAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,11 +42,11 @@ Singleton {
|
|||||||
onExited: (exitCode) => {
|
onExited: (exitCode) => {
|
||||||
root.niriAvailable = exitCode === 0
|
root.niriAvailable = exitCode === 0
|
||||||
if (root.niriAvailable) {
|
if (root.niriAvailable) {
|
||||||
console.log("NiriWorkspaceService: niri found, starting event stream and loading initial data")
|
console.log("NiriService: niri found, starting event stream and loading initial data")
|
||||||
eventStreamProcess.running = true
|
eventStreamProcess.running = true
|
||||||
loadInitialWorkspaceData()
|
loadInitialWorkspaceData()
|
||||||
} else {
|
} else {
|
||||||
console.log("NiriWorkspaceService: niri not found, workspace features disabled")
|
console.log("NiriService: niri not found, workspace features disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,12 +65,12 @@ Singleton {
|
|||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
try {
|
try {
|
||||||
console.log("NiriWorkspaceService: Loaded initial workspace data")
|
console.log("NiriService: Loaded initial workspace data")
|
||||||
const workspaces = JSON.parse(text.trim())
|
const workspaces = JSON.parse(text.trim())
|
||||||
// Initial query returns array directly, event stream wraps it in WorkspacesChanged
|
// Initial query returns array directly, event stream wraps it in WorkspacesChanged
|
||||||
handleWorkspacesChanged({ workspaces: workspaces })
|
handleWorkspacesChanged({ workspaces: workspaces })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("NiriWorkspaceService: Failed to parse initial workspace data:", e)
|
console.warn("NiriService: Failed to parse initial workspace data:", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,10 +90,10 @@ Singleton {
|
|||||||
const windowsData = JSON.parse(text.trim())
|
const windowsData = JSON.parse(text.trim())
|
||||||
if (windowsData && windowsData.windows) {
|
if (windowsData && windowsData.windows) {
|
||||||
handleWindowsChanged(windowsData)
|
handleWindowsChanged(windowsData)
|
||||||
console.log("NiriWorkspaceService: Loaded", windowsData.windows.length, "initial windows")
|
console.log("NiriService: Loaded", windowsData.windows.length, "initial windows")
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("NiriWorkspaceService: Failed to parse initial windows data:", e)
|
console.warn("NiriService: Failed to parse initial windows data:", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,10 +113,10 @@ Singleton {
|
|||||||
const focusedData = JSON.parse(text.trim())
|
const focusedData = JSON.parse(text.trim())
|
||||||
if (focusedData && focusedData.id) {
|
if (focusedData && focusedData.id) {
|
||||||
handleWindowFocusChanged({ id: focusedData.id })
|
handleWindowFocusChanged({ id: focusedData.id })
|
||||||
console.log("NiriWorkspaceService: Loaded initial focused window:", focusedData.id)
|
console.log("NiriService: Loaded initial focused window:", focusedData.id)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("NiriWorkspaceService: Failed to parse initial focused window data:", e)
|
console.warn("NiriService: Failed to parse initial focused window data:", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadInitialWorkspaceData() {
|
function loadInitialWorkspaceData() {
|
||||||
console.log("NiriWorkspaceService: Loading initial workspace data...")
|
console.log("NiriService: Loading initial workspace data...")
|
||||||
initialDataQuery.running = true
|
initialDataQuery.running = true
|
||||||
initialWindowsQuery.running = true
|
initialWindowsQuery.running = true
|
||||||
initialFocusedWindowQuery.running = true
|
initialFocusedWindowQuery.running = true
|
||||||
@@ -142,14 +142,14 @@ Singleton {
|
|||||||
const event = JSON.parse(data.trim())
|
const event = JSON.parse(data.trim())
|
||||||
handleNiriEvent(event)
|
handleNiriEvent(event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("NiriWorkspaceService: Failed to parse event:", data, e)
|
console.warn("NiriService: Failed to parse event:", data, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
onExited: (exitCode) => {
|
||||||
if (exitCode !== 0 && root.niriAvailable) {
|
if (exitCode !== 0 && root.niriAvailable) {
|
||||||
console.warn("NiriWorkspaceService: Event stream exited with code", exitCode, "restarting immediately")
|
console.warn("NiriService: Event stream exited with code", exitCode, "restarting immediately")
|
||||||
eventStreamProcess.running = true
|
eventStreamProcess.running = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@ Singleton {
|
|||||||
|
|
||||||
var targetOutput = output || currentOutput
|
var targetOutput = output || currentOutput
|
||||||
if (!targetOutput) {
|
if (!targetOutput) {
|
||||||
console.warn("NiriWorkspaceService: No output specified for workspace switching")
|
console.warn("NiriService: No output specified for workspace switching")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ Singleton {
|
|||||||
return switchToWorkspace(workspace.id)
|
return switchToWorkspace(workspace.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("NiriWorkspaceService: No workspace", number, "found on output", targetOutput)
|
console.warn("NiriService: No workspace", number, "found on output", targetOutput)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ Rectangle {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -40,5 +41,7 @@ Rectangle {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ Rectangle {
|
|||||||
height: 60
|
height: 60
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||||
|
// Global keyboard handler for escape key
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
if (dropdownMenu.visible)
|
||||||
|
dropdownMenu.visible = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -43,11 +49,12 @@ Rectangle {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: dropdown
|
id: dropdown
|
||||||
|
|
||||||
width: 180
|
width: 180
|
||||||
height: 36
|
height: 36
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@@ -80,10 +87,12 @@ Rectangle {
|
|||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: dropdownArea
|
id: dropdownArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -97,34 +106,35 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Integrated dropdown menu with full-screen overlay
|
// Integrated dropdown menu with full-screen overlay
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: dropdownMenu
|
id: dropdownMenu
|
||||||
|
|
||||||
property int targetX: 0
|
property int targetX: 0
|
||||||
property int targetY: 0
|
property int targetY: 0
|
||||||
|
|
||||||
|
function updatePosition() {
|
||||||
|
var globalPos = dropdown.mapToGlobal(0, 0);
|
||||||
|
targetX = globalPos.x;
|
||||||
|
targetY = globalPos.y + dropdown.height + 4;
|
||||||
|
}
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
left: true
|
left: true
|
||||||
right: true
|
right: true
|
||||||
bottom: true
|
bottom: true
|
||||||
}
|
}
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
function updatePosition() {
|
|
||||||
var globalPos = dropdown.mapToGlobal(0, 0);
|
|
||||||
targetX = globalPos.x;
|
|
||||||
targetY = globalPos.y + dropdown.height + 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background click interceptor (invisible)
|
// Background click interceptor (invisible)
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -133,7 +143,7 @@ Rectangle {
|
|||||||
dropdownMenu.visible = false;
|
dropdownMenu.visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dropdown menu content
|
// Dropdown menu content
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: dropdownMenu.targetX
|
x: dropdownMenu.targetX
|
||||||
@@ -141,25 +151,25 @@ Rectangle {
|
|||||||
width: 180
|
width: 180
|
||||||
height: Math.min(200, root.options.length * 36 + 16)
|
height: Math.min(200, root.options.length * 36 + 16)
|
||||||
radius: Theme.cornerRadiusSmall
|
radius: Theme.cornerRadiusSmall
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0)
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
model: root.options
|
model: root.options
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: ListView.view.width
|
width: ListView.view.width
|
||||||
height: 32
|
height: 32
|
||||||
radius: Theme.cornerRadiusSmall
|
radius: Theme.cornerRadiusSmall
|
||||||
color: optionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
color: optionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: Theme.spacingS
|
anchors.leftMargin: Theme.spacingS
|
||||||
@@ -169,9 +179,10 @@ Rectangle {
|
|||||||
color: root.currentValue === modelData ? Theme.primary : Theme.surfaceText
|
color: root.currentValue === modelData ? Theme.primary : Theme.surfaceText
|
||||||
font.weight: root.currentValue === modelData ? Font.Medium : Font.Normal
|
font.weight: root.currentValue === modelData ? Font.Medium : Font.Normal
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: optionArea
|
id: optionArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -181,16 +192,15 @@ Rectangle {
|
|||||||
dropdownMenu.visible = false;
|
dropdownMenu.visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global keyboard handler for escape key
|
}
|
||||||
Keys.onEscapePressed: {
|
|
||||||
if (dropdownMenu.visible) {
|
|
||||||
dropdownMenu.visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ ScrollView {
|
|||||||
GridView {
|
GridView {
|
||||||
id: grid
|
id: grid
|
||||||
|
|
||||||
property int baseCellWidth: adaptiveColumns ?
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) :
|
|
||||||
(width - Theme.spacingS * 2) / columns
|
|
||||||
property int baseCellHeight: baseCellWidth + 20
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
@@ -68,14 +66,8 @@ ScrollView {
|
|||||||
width: grid.cellWidth - cellPadding
|
width: grid.cellWidth - cellPadding
|
||||||
height: grid.cellHeight - cellPadding
|
height: grid.cellHeight - cellPadding
|
||||||
radius: Theme.cornerRadiusLarge
|
radius: Theme.cornerRadiusLarge
|
||||||
color: currentIndex === index ?
|
color: currentIndex === index ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
|
border.color: currentIndex === index ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
mouseArea.containsMouse ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
|
||||||
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: currentIndex === index ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) :
|
|
||||||
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: currentIndex === index ? 2 : 1
|
border.width: currentIndex === index ? 2 : 1
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -91,6 +83,7 @@ ScrollView {
|
|||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: iconImg
|
id: iconImg
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: (model.icon) ? Quickshell.iconPath(model.icon, "") : ""
|
source: (model.icon) ? Quickshell.iconPath(model.icon, "") : ""
|
||||||
smooth: true
|
smooth: true
|
||||||
@@ -113,7 +106,9 @@ ScrollView {
|
|||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -128,24 +123,29 @@ ScrollView {
|
|||||||
maximumLineCount: 2
|
maximumLineCount: 2
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
z: 10
|
z: 10
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (hoverUpdatesSelection) {
|
if (hoverUpdatesSelection)
|
||||||
currentIndex = index;
|
currentIndex = index;
|
||||||
}
|
|
||||||
itemHovered(index);
|
itemHovered(index);
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
itemClicked(index, model);
|
itemClicked(index, model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import qs.Common
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: icon
|
id: icon
|
||||||
|
|
||||||
property alias name: icon.text
|
property alias name: icon.text
|
||||||
property alias size: icon.font.pixelSize
|
property alias size: icon.font.pixelSize
|
||||||
property alias color: icon.color
|
property alias color: icon.color
|
||||||
@@ -15,4 +15,4 @@ Text {
|
|||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,14 +55,8 @@ ScrollView {
|
|||||||
width: list.width
|
width: list.width
|
||||||
height: itemHeight
|
height: itemHeight
|
||||||
radius: Theme.cornerRadiusLarge
|
radius: Theme.cornerRadiusLarge
|
||||||
color: ListView.isCurrentItem ?
|
color: ListView.isCurrentItem ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
|
border.color: ListView.isCurrentItem ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
mouseArea.containsMouse ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
|
||||||
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: ListView.isCurrentItem ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) :
|
|
||||||
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: ListView.isCurrentItem ? 2 : 1
|
border.width: ListView.isCurrentItem ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -77,6 +71,7 @@ ScrollView {
|
|||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: iconImg
|
id: iconImg
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: (model.icon) ? Quickshell.iconPath(model.icon, "") : ""
|
source: (model.icon) ? Quickshell.iconPath(model.icon, "") : ""
|
||||||
smooth: true
|
smooth: true
|
||||||
@@ -99,7 +94,9 @@ ScrollView {
|
|||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -124,25 +121,31 @@ ScrollView {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
visible: showDescription && model.comment && model.comment.length > 0
|
visible: showDescription && model.comment && model.comment.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
z: 10
|
z: 10
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (hoverUpdatesSelection) {
|
if (hoverUpdatesSelection)
|
||||||
listView.currentIndex = index;
|
listView.currentIndex = index;
|
||||||
}
|
|
||||||
itemHovered(index);
|
itemHovered(index);
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
itemClicked(index, model);
|
itemClicked(index, model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,139 +6,139 @@ import qs.Widgets
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string currentLocation: ""
|
property string currentLocation: ""
|
||||||
property string placeholderText: "Search for a location..."
|
property string placeholderText: "Search for a location..."
|
||||||
|
|
||||||
signal locationSelected(string displayName, string coordinates)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
|
|
||||||
|
|
||||||
property bool _internalChange: false
|
property bool _internalChange: false
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
property string helperTextState: "default" // "default", "prompt", "searching", "found", "not_found"
|
property string helperTextState: "default" // "default", "prompt", "searching", "found", "not_found"
|
||||||
property string currentSearchText: ""
|
property string currentSearchText: ""
|
||||||
|
|
||||||
|
signal locationSelected(string displayName, string coordinates)
|
||||||
|
|
||||||
|
function resetSearchState() {
|
||||||
|
locationSearchTimer.stop();
|
||||||
|
dropdownHideTimer.stop();
|
||||||
|
if (locationSearcher.running)
|
||||||
|
locationSearcher.running = false;
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
searchResultsModel.clear();
|
||||||
|
helperTextState = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
id: searchResultsModel
|
id: searchResultsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearchState() {
|
|
||||||
locationSearchTimer.stop()
|
|
||||||
dropdownHideTimer.stop()
|
|
||||||
if (locationSearcher.running) {
|
|
||||||
locationSearcher.running = false;
|
|
||||||
}
|
|
||||||
isLoading = false
|
|
||||||
searchResultsModel.clear()
|
|
||||||
helperTextState = "default"
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: locationSearchTimer
|
id: locationSearchTimer
|
||||||
|
|
||||||
interval: 500
|
interval: 500
|
||||||
running: false
|
running: false
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (locationInput.text.length > 2) {
|
if (locationInput.text.length > 2) {
|
||||||
if (locationSearcher.running) {
|
if (locationSearcher.running)
|
||||||
locationSearcher.running = false
|
locationSearcher.running = false;
|
||||||
}
|
|
||||||
|
searchResultsModel.clear();
|
||||||
searchResultsModel.clear()
|
root.isLoading = true;
|
||||||
root.isLoading = true
|
root.helperTextState = "searching";
|
||||||
root.helperTextState = "searching"
|
const searchLocation = locationInput.text;
|
||||||
|
root.currentSearchText = searchLocation;
|
||||||
const searchLocation = locationInput.text
|
const encodedLocation = encodeURIComponent(searchLocation);
|
||||||
root.currentSearchText = searchLocation
|
const curlCommand = `curl -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'`;
|
||||||
const encodedLocation = encodeURIComponent(searchLocation)
|
locationSearcher.command = ["bash", "-c", curlCommand];
|
||||||
const curlCommand = `curl -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'`
|
locationSearcher.running = true;
|
||||||
|
|
||||||
locationSearcher.command = ["bash", "-c", curlCommand]
|
|
||||||
locationSearcher.running = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: dropdownHideTimer
|
id: dropdownHideTimer
|
||||||
|
|
||||||
interval: 200
|
interval: 200
|
||||||
running: false
|
running: false
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!locationInput.getActiveFocus() && !searchDropdown.hovered) {
|
if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
|
||||||
root.resetSearchState()
|
root.resetSearchState();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: locationSearcher
|
id: locationSearcher
|
||||||
|
|
||||||
command: ["bash", "-c", "echo"]
|
command: ["bash", "-c", "echo"]
|
||||||
running: false
|
running: false
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
root.isLoading = false;
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
searchResultsModel.clear();
|
||||||
|
root.helperTextState = "not_found";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (root.currentSearchText !== locationInput.text) {
|
if (root.currentSearchText !== locationInput.text)
|
||||||
return
|
return ;
|
||||||
}
|
|
||||||
|
|
||||||
const raw = text.trim()
|
|
||||||
root.isLoading = false
|
|
||||||
searchResultsModel.clear()
|
|
||||||
|
|
||||||
|
const raw = text.trim();
|
||||||
|
root.isLoading = false;
|
||||||
|
searchResultsModel.clear();
|
||||||
if (!raw || raw[0] !== "[") {
|
if (!raw || raw[0] !== "[") {
|
||||||
root.helperTextState = "not_found"
|
root.helperTextState = "not_found";
|
||||||
return
|
return ;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(raw)
|
const data = JSON.parse(raw);
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
root.helperTextState = "not_found"
|
root.helperTextState = "not_found";
|
||||||
return
|
return ;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(data.length, 5); i++) {
|
for (let i = 0; i < Math.min(data.length, 5); i++) {
|
||||||
const location = data[i]
|
const location = data[i];
|
||||||
if (location.display_name && location.lat && location.lon) {
|
if (location.display_name && location.lat && location.lon) {
|
||||||
const parts = location.display_name.split(', ')
|
const parts = location.display_name.split(', ');
|
||||||
let cleanName = parts[0]
|
let cleanName = parts[0];
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
const state = parts[parts.length - 2]
|
const state = parts[parts.length - 2];
|
||||||
if (state && state !== cleanName) {
|
if (state && state !== cleanName)
|
||||||
cleanName += `, ${state}`
|
cleanName += `, ${state}`;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const query = `${location.lat},${location.lon}`
|
const query = `${location.lat},${location.lon}`;
|
||||||
searchResultsModel.append({ "name": cleanName, "query": query })
|
searchResultsModel.append({
|
||||||
|
"name": cleanName,
|
||||||
|
"query": query
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root.helperTextState = "found"
|
root.helperTextState = "found";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
root.helperTextState = "not_found"
|
root.helperTextState = "not_found";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
root.isLoading = false
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
searchResultsModel.clear()
|
|
||||||
root.helperTextState = "not_found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search input field
|
// Search input field
|
||||||
Item {
|
Item {
|
||||||
id: searchInputField
|
id: searchInputField
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 48
|
height: 48
|
||||||
|
|
||||||
DankTextField {
|
DankTextField {
|
||||||
id: locationInput
|
id: locationInput
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
leftIconName: "search"
|
leftIconName: "search"
|
||||||
@@ -147,123 +147,138 @@ Item {
|
|||||||
backgroundColor: Theme.surfaceVariant
|
backgroundColor: Theme.surfaceVariant
|
||||||
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
focusedBorderColor: Theme.primary
|
focusedBorderColor: Theme.primary
|
||||||
|
|
||||||
onTextEdited: {
|
onTextEdited: {
|
||||||
if (root._internalChange) return
|
if (root._internalChange)
|
||||||
|
return ;
|
||||||
|
|
||||||
if (getActiveFocus()) {
|
if (getActiveFocus()) {
|
||||||
if (text.length > 2) {
|
if (text.length > 2) {
|
||||||
root.isLoading = true
|
root.isLoading = true;
|
||||||
root.helperTextState = "searching"
|
root.helperTextState = "searching";
|
||||||
locationSearchTimer.restart()
|
locationSearchTimer.restart();
|
||||||
} else {
|
} else {
|
||||||
root.resetSearchState()
|
root.resetSearchState();
|
||||||
root.helperTextState = "prompt"
|
root.helperTextState = "prompt";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocusStateChanged: (hasFocus) => {
|
onFocusStateChanged: (hasFocus) => {
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
dropdownHideTimer.stop()
|
dropdownHideTimer.stop();
|
||||||
if (text.length <= 2) {
|
if (text.length <= 2)
|
||||||
root.helperTextState = "prompt"
|
root.helperTextState = "prompt";
|
||||||
}
|
|
||||||
}
|
} else {
|
||||||
else {
|
dropdownHideTimer.start();
|
||||||
dropdownHideTimer.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status icon overlay
|
// Status icon overlay
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: {
|
name: {
|
||||||
if (root.isLoading) return "hourglass_empty"
|
if (root.isLoading)
|
||||||
if (searchResultsModel.count > 0) return "check_circle"
|
return "hourglass_empty";
|
||||||
if (locationInput.getActiveFocus() && locationInput.text.length > 2 && !root.isLoading) return "error"
|
|
||||||
return ""
|
if (searchResultsModel.count > 0)
|
||||||
|
return "check_circle";
|
||||||
|
|
||||||
|
if (locationInput.getActiveFocus() && locationInput.text.length > 2 && !root.isLoading)
|
||||||
|
return "error";
|
||||||
|
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
size: Theme.iconSize - 4
|
size: Theme.iconSize - 4
|
||||||
color: {
|
color: {
|
||||||
if (root.isLoading) return Theme.surfaceVariantText
|
if (root.isLoading)
|
||||||
if (searchResultsModel.count > 0) return Theme.success || Theme.primary
|
return Theme.surfaceVariantText;
|
||||||
if (locationInput.getActiveFocus() && locationInput.text.length > 2) return Theme.error
|
|
||||||
return "transparent"
|
if (searchResultsModel.count > 0)
|
||||||
|
return Theme.success || Theme.primary;
|
||||||
|
|
||||||
|
if (locationInput.getActiveFocus() && locationInput.text.length > 2)
|
||||||
|
return Theme.error;
|
||||||
|
|
||||||
|
return "transparent";
|
||||||
}
|
}
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1.0 : 0.0
|
opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search results dropdown
|
// Search results dropdown
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: searchDropdown
|
id: searchDropdown
|
||||||
|
|
||||||
|
property bool hovered: false
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
|
height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
|
||||||
|
|
||||||
y: searchInputField.height
|
y: searchInputField.height
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.popupBackground()
|
color: Theme.popupBackground()
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading)
|
visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading)
|
||||||
|
|
||||||
property bool hovered: false
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: {
|
onEntered: {
|
||||||
parent.hovered = true
|
parent.hovered = true;
|
||||||
dropdownHideTimer.stop()
|
dropdownHideTimer.stop();
|
||||||
}
|
}
|
||||||
onExited: {
|
onExited: {
|
||||||
parent.hovered = false
|
parent.hovered = false;
|
||||||
if (!locationInput.getActiveFocus()) {
|
if (!locationInput.getActiveFocus())
|
||||||
dropdownHideTimer.start()
|
dropdownHideTimer.start();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: searchResultsList
|
id: searchResultsList
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
model: searchResultsModel
|
model: searchResultsModel
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: searchResultsList.width
|
width: searchResultsList.width
|
||||||
height: 36
|
height: 36
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: resultMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
color: resultMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "place"
|
name: "place"
|
||||||
size: Theme.iconSize - 6
|
size: Theme.iconSize - 6
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: model.name || "Unknown"
|
text: model.name || "Unknown"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
@@ -272,30 +287,31 @@ Item {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width - 30
|
width: parent.width - 30
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: resultMouseArea
|
id: resultMouseArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root._internalChange = true
|
root._internalChange = true;
|
||||||
const selectedName = model.name
|
const selectedName = model.name;
|
||||||
const selectedQuery = model.query
|
const selectedQuery = model.query;
|
||||||
|
locationInput.text = selectedName;
|
||||||
locationInput.text = selectedName
|
root.locationSelected(selectedName, selectedQuery);
|
||||||
root.locationSelected(selectedName, selectedQuery)
|
root.resetSearchState();
|
||||||
|
locationInput.setFocus(false);
|
||||||
root.resetSearchState()
|
root._internalChange = false;
|
||||||
locationInput.setFocus(false)
|
|
||||||
root._internalChange = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show message when no results
|
// Show message when no results
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -304,6 +320,9 @@ Item {
|
|||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
visible: searchResultsList.count === 0 && locationInput.text.length > 2
|
visible: searchResultsList.count === 0 && locationInput.text.length > 2
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,33 +10,29 @@ PanelWindow {
|
|||||||
|
|
||||||
// Core properties
|
// Core properties
|
||||||
property alias content: contentLoader.sourceComponent
|
property alias content: contentLoader.sourceComponent
|
||||||
|
|
||||||
// Sizing - can use fixed or relative to screen
|
// Sizing - can use fixed or relative to screen
|
||||||
property real width: 400
|
property real width: 400
|
||||||
property real height: 300
|
property real height: 300
|
||||||
|
|
||||||
// Screen-relative sizing helpers
|
// Screen-relative sizing helpers
|
||||||
readonly property real screenWidth: screen ? screen.width : 1920
|
readonly property real screenWidth: screen ? screen.width : 1920
|
||||||
readonly property real screenHeight: screen ? screen.height : 1080
|
readonly property real screenHeight: screen ? screen.height : 1080
|
||||||
|
|
||||||
// Background behavior
|
// Background behavior
|
||||||
property bool showBackground: true
|
property bool showBackground: true
|
||||||
property real backgroundOpacity: 0.5
|
property real backgroundOpacity: 0.5
|
||||||
|
|
||||||
// Positioning
|
// Positioning
|
||||||
property string positioning: "center" // "center", "top-right", "custom"
|
property string positioning: "center"
|
||||||
|
// "center", "top-right", "custom"
|
||||||
property point customPosition: Qt.point(0, 0)
|
property point customPosition: Qt.point(0, 0)
|
||||||
|
|
||||||
// Focus management
|
// Focus management
|
||||||
property string keyboardFocus: "ondemand" // "ondemand", "exclusive", "none"
|
property string keyboardFocus: "ondemand"
|
||||||
|
// "ondemand", "exclusive", "none"
|
||||||
property bool closeOnEscapeKey: true
|
property bool closeOnEscapeKey: true
|
||||||
property bool closeOnBackgroundClick: true
|
property bool closeOnBackgroundClick: true
|
||||||
|
|
||||||
// Animation
|
// Animation
|
||||||
property string animationType: "scale" // "scale", "slide", "fade"
|
property string animationType: "scale"
|
||||||
|
// "scale", "slide", "fade"
|
||||||
property int animationDuration: Theme.mediumDuration
|
property int animationDuration: Theme.mediumDuration
|
||||||
property var animationEasing: Theme.emphasizedEasing
|
property var animationEasing: Theme.emphasizedEasing
|
||||||
|
|
||||||
// Styling
|
// Styling
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
property color backgroundColor: Theme.surfaceContainer
|
||||||
property color borderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
property color borderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
@@ -49,11 +45,47 @@ PanelWindow {
|
|||||||
signal dialogClosed()
|
signal dialogClosed()
|
||||||
signal backgroundClicked()
|
signal backgroundClicked()
|
||||||
|
|
||||||
|
// Convenience functions
|
||||||
|
function open() {
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
visible = !visible;
|
||||||
|
}
|
||||||
|
|
||||||
// PanelWindow configuration
|
// PanelWindow configuration
|
||||||
// visible property is inherited from PanelWindow
|
// visible property is inherited from PanelWindow
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: {
|
||||||
|
switch (root.keyboardFocus) {
|
||||||
|
case "exclusive":
|
||||||
|
return WlrKeyboardFocus.Exclusive;
|
||||||
|
case "none":
|
||||||
|
return WlrKeyboardFocus.None;
|
||||||
|
default:
|
||||||
|
return WlrKeyboardFocus.OnDemand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (root.visible) {
|
||||||
|
opened();
|
||||||
|
} else {
|
||||||
|
// Properly cleanup text input surfaces
|
||||||
|
if (Qt.inputMethod) {
|
||||||
|
Qt.inputMethod.hide();
|
||||||
|
Qt.inputMethod.reset();
|
||||||
|
}
|
||||||
|
dialogClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
left: true
|
left: true
|
||||||
@@ -61,103 +93,77 @@ PanelWindow {
|
|||||||
bottom: true
|
bottom: true
|
||||||
}
|
}
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: {
|
|
||||||
switch (root.keyboardFocus) {
|
|
||||||
case "exclusive": return WlrKeyboardFocus.Exclusive
|
|
||||||
case "none": return WlrKeyboardFocus.None
|
|
||||||
default: return WlrKeyboardFocus.OnDemand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (root.visible) {
|
|
||||||
opened()
|
|
||||||
} else {
|
|
||||||
// Properly cleanup text input surfaces
|
|
||||||
if (Qt.inputMethod) {
|
|
||||||
Qt.inputMethod.hide()
|
|
||||||
Qt.inputMethod.reset()
|
|
||||||
}
|
|
||||||
dialogClosed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background overlay
|
// Background overlay
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: background
|
id: background
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "black"
|
color: "black"
|
||||||
opacity: root.showBackground ? (root.visible ? root.backgroundOpacity : 0) : 0
|
opacity: root.showBackground ? (root.visible ? root.backgroundOpacity : 0) : 0
|
||||||
visible: root.showBackground
|
visible: root.showBackground
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.closeOnBackgroundClick
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
var localPos = mapToItem(contentContainer, mouse.x, mouse.y);
|
||||||
|
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height)
|
||||||
|
root.backgroundClicked();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: root.animationDuration
|
duration: root.animationDuration
|
||||||
easing.type: root.animationEasing
|
easing.type: root.animationEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.closeOnBackgroundClick
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
var localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
|
||||||
if (localPos.x < 0 || localPos.x > contentContainer.width ||
|
|
||||||
localPos.y < 0 || localPos.y > contentContainer.height) {
|
|
||||||
root.backgroundClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main content container
|
// Main content container
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: contentContainer
|
id: contentContainer
|
||||||
|
|
||||||
width: root.width
|
width: root.width
|
||||||
height: root.height
|
height: root.height
|
||||||
|
|
||||||
// Positioning
|
// Positioning
|
||||||
anchors.centerIn: positioning === "center" ? parent : undefined
|
anchors.centerIn: positioning === "center" ? parent : undefined
|
||||||
x: {
|
x: {
|
||||||
if (positioning === "top-right") {
|
if (positioning === "top-right")
|
||||||
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL)
|
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL);
|
||||||
} else if (positioning === "custom") {
|
else if (positioning === "custom")
|
||||||
return root.customPosition.x
|
return root.customPosition.x;
|
||||||
}
|
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
|
||||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
|
||||||
}
|
}
|
||||||
y: {
|
y: {
|
||||||
if (positioning === "top-right") {
|
if (positioning === "top-right")
|
||||||
return Theme.barHeight + Theme.spacingXS
|
return Theme.barHeight + Theme.spacingXS;
|
||||||
} else if (positioning === "custom") {
|
else if (positioning === "custom")
|
||||||
return root.customPosition.y
|
return root.customPosition.y;
|
||||||
}
|
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
|
||||||
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
color: root.backgroundColor
|
color: root.backgroundColor
|
||||||
radius: root.cornerRadius
|
radius: root.cornerRadius
|
||||||
border.color: root.borderColor
|
border.color: root.borderColor
|
||||||
border.width: root.borderWidth
|
border.width: root.borderWidth
|
||||||
layer.enabled: root.enableShadow
|
layer.enabled: root.enableShadow
|
||||||
|
|
||||||
// Animation properties
|
// Animation properties
|
||||||
opacity: root.visible ? 1 : 0
|
opacity: root.visible ? 1 : 0
|
||||||
scale: {
|
scale: {
|
||||||
if (root.animationType === "scale") {
|
if (root.animationType === "scale")
|
||||||
return root.visible ? 1 : 0.9
|
return root.visible ? 1 : 0.9;
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
// Transform for slide animation
|
// Transform for slide animation
|
||||||
transform: root.animationType === "slide" ? slideTransform : null
|
transform: root.animationType === "slide" ? slideTransform : null
|
||||||
|
|
||||||
Translate {
|
Translate {
|
||||||
id: slideTransform
|
id: slideTransform
|
||||||
|
|
||||||
x: root.visible ? 0 : 15
|
x: root.visible ? 0 : 15
|
||||||
y: root.visible ? 0 : -30
|
y: root.visible ? 0 : -30
|
||||||
}
|
}
|
||||||
@@ -165,6 +171,7 @@ PanelWindow {
|
|||||||
// Content area
|
// Content area
|
||||||
Loader {
|
Loader {
|
||||||
id: contentLoader
|
id: contentLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: true
|
active: true
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
@@ -176,14 +183,17 @@ PanelWindow {
|
|||||||
duration: root.animationDuration
|
duration: root.animationDuration
|
||||||
easing.type: root.animationEasing
|
easing.type: root.animationEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
enabled: root.animationType === "scale"
|
enabled: root.animationType === "scale"
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: root.animationDuration
|
duration: root.animationDuration
|
||||||
easing.type: root.animationEasing
|
easing.type: root.animationEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shadow effect
|
// Shadow effect
|
||||||
@@ -195,14 +205,15 @@ PanelWindow {
|
|||||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||||
shadowOpacity: 0.3
|
shadowOpacity: 0.3
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard handling
|
// Keyboard handling
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: focusScope
|
id: focusScope
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: root.visible // Only active when the modal is visible
|
visible: root.visible // Only active when the modal is visible
|
||||||
|
|
||||||
Keys.onEscapePressed: (event) => {
|
Keys.onEscapePressed: (event) => {
|
||||||
if (root.closeOnEscapeKey) {
|
if (root.closeOnEscapeKey) {
|
||||||
root.visible = false;
|
root.visible = false;
|
||||||
@@ -211,16 +222,4 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience functions
|
}
|
||||||
function open() {
|
|
||||||
visible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
visible = !visible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ Item {
|
|||||||
property bool enabled: true
|
property bool enabled: true
|
||||||
property string unit: "%"
|
property string unit: "%"
|
||||||
property bool showValue: true
|
property bool showValue: true
|
||||||
|
property bool isDragging: false
|
||||||
|
|
||||||
signal sliderValueChanged(int newValue)
|
signal sliderValueChanged(int newValue)
|
||||||
signal sliderDragFinished(int finalValue)
|
signal sliderDragFinished(int finalValue)
|
||||||
|
|
||||||
height: 80
|
height: 80
|
||||||
|
|
||||||
property bool isDragging: false
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Item {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: tabRow
|
id: tabRow
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: tabBar.spacing
|
spacing: tabBar.spacing
|
||||||
|
|
||||||
@@ -30,19 +31,14 @@ Item {
|
|||||||
property bool hasIcon: tabBar.showIcons && modelData.icon && modelData.icon.length > 0
|
property bool hasIcon: tabBar.showIcons && modelData.icon && modelData.icon.length > 0
|
||||||
property bool hasText: modelData.text && modelData.text.length > 0
|
property bool hasText: modelData.text && modelData.text.length > 0
|
||||||
|
|
||||||
width: tabBar.equalWidthTabs ?
|
width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * (tabCount - 1)) / tabCount : contentRow.implicitWidth + Theme.spacingM * 2
|
||||||
(tabBar.width - tabBar.spacing * (tabCount - 1)) / tabCount :
|
|
||||||
contentRow.implicitWidth + Theme.spacingM * 2
|
|
||||||
height: tabBar.tabHeight
|
height: tabBar.tabHeight
|
||||||
radius: Theme.cornerRadiusSmall
|
radius: Theme.cornerRadiusSmall
|
||||||
color: isActive ?
|
color: isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : tabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
|
|
||||||
tabArea.containsMouse ?
|
|
||||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
|
||||||
"transparent"
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: contentRow
|
id: contentRow
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
@@ -62,16 +58,18 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: parent.parent.hasText
|
visible: parent.parent.hasText
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: tabArea
|
id: tabArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
tabBar.currentIndex = index
|
tabBar.currentIndex = index;
|
||||||
tabBar.tabClicked(index)
|
tabBar.tabClicked(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +78,13 @@ Item {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: background
|
id: background
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: toggle.text ? Theme.cornerRadius : 0
|
radius: toggle.text ? Theme.cornerRadius : 0
|
||||||
color: toggle.text ? (toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) : "transparent"
|
color: toggle.text ? (toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) : "transparent"
|
||||||
@@ -26,6 +27,7 @@ Item {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: textRow
|
id: textRow
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: toggleTrack.left
|
anchors.right: toggleTrack.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -53,11 +55,14 @@ Item {
|
|||||||
width: Math.min(implicitWidth, toggle.width - 120)
|
width: Math.min(implicitWidth, toggle.width - 120)
|
||||||
visible: toggle.description.length > 0
|
visible: toggle.description.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: toggleTrack
|
id: toggleTrack
|
||||||
|
|
||||||
width: toggle.text ? 48 : parent.width
|
width: toggle.text ? 48 : parent.width
|
||||||
height: toggle.text ? 24 : parent.height
|
height: toggle.text ? 24 : parent.height
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@@ -69,6 +74,7 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: toggleHandle
|
id: toggleHandle
|
||||||
|
|
||||||
width: 20
|
width: 20
|
||||||
height: 20
|
height: 20
|
||||||
radius: 10
|
radius: 10
|
||||||
@@ -76,13 +82,6 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
x: toggle.checked ? parent.width - width - 2 : 2
|
x: toggle.checked ? parent.width - width - 2 : 2
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width + 2
|
width: parent.width + 2
|
||||||
@@ -93,12 +92,22 @@ Item {
|
|||||||
border.width: 1
|
border.width: 1
|
||||||
z: -1
|
z: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: toggleArea
|
id: toggleArea
|
||||||
|
|
||||||
anchors.fill: toggle.text ? toggle : toggleTrack
|
anchors.fill: toggle.text ? toggle : toggleTrack
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: toggle.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: toggle.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
@@ -110,4 +119,4 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,24 +10,29 @@ IconImage {
|
|||||||
|
|
||||||
property string colorOverride: ""
|
property string colorOverride: ""
|
||||||
property real brightnessOverride: 0.5
|
property real brightnessOverride: 0.5
|
||||||
property real contrastOverride: 1.0
|
property real contrastOverride: 1
|
||||||
|
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
layer.enabled: colorOverride !== ""
|
layer.enabled: colorOverride !== ""
|
||||||
|
|
||||||
|
Process {
|
||||||
|
running: true
|
||||||
|
command: ["sh", "-c", ". /etc/os-release && echo $LOGO"]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: () => {
|
||||||
|
root.source = Quickshell.iconPath(this.text.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
colorization: 1
|
colorization: 1
|
||||||
colorizationColor: colorOverride
|
colorizationColor: colorOverride
|
||||||
brightness: brightnessOverride
|
brightness: brightnessOverride
|
||||||
contrast: contrastOverride
|
contrast: contrastOverride
|
||||||
}
|
}
|
||||||
Process {
|
|
||||||
running: true
|
}
|
||||||
command: ["sh", "-c", ". /etc/os-release && echo $LOGO"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: () => {
|
|
||||||
root.source = Quickshell.iconPath(this.text.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import qs.Modules.ControlCenter
|
|||||||
import qs.Modules.Settings
|
import qs.Modules.Settings
|
||||||
import qs.Modules.TopBar
|
import qs.Modules.TopBar
|
||||||
import qs.Modules.ProcessList
|
import qs.Modules.ProcessList
|
||||||
|
import qs.Modules.ControlCenter.Network
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
@@ -90,8 +91,8 @@ ShellRoot {
|
|||||||
id: processListModal
|
id: processListModal
|
||||||
}
|
}
|
||||||
|
|
||||||
ClipboardHistory {
|
ClipboardHistoryModal {
|
||||||
id: clipboardHistoryPopup
|
id: clipboardHistoryModalPopup
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast {
|
Toast {
|
||||||
|
|||||||
Reference in New Issue
Block a user