mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-26 14:32:52 -05:00
mecha refactoring
This commit is contained in:
591
Modals/ClipboardHistoryModal.qml
Normal file
591
Modals/ClipboardHistoryModal.qml
Normal file
@@ -0,0 +1,591 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
// Don't hide the interface, just show toast
|
||||
|
||||
id: clipboardHistoryModal
|
||||
|
||||
property bool isVisible: false
|
||||
property int totalCount: 0
|
||||
property var activeTheme: Theme
|
||||
property bool showClearConfirmation: false
|
||||
property var clipboardEntries: []
|
||||
property string searchText: ""
|
||||
|
||||
function updateFilteredModel() {
|
||||
filteredClipboardModel.clear();
|
||||
for (let i = 0; i < clipboardModel.count; i++) {
|
||||
const entry = clipboardModel.get(i).entry;
|
||||
if (searchText.trim().length === 0) {
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
});
|
||||
} else {
|
||||
const content = getEntryPreview(entry).toLowerCase();
|
||||
if (content.includes(searchText.toLowerCase()))
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
}
|
||||
|
||||
function show() {
|
||||
clipboardHistoryModal.isVisible = true;
|
||||
refreshClipboard();
|
||||
console.log("ClipboardHistoryModal: Opening and refreshing");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clipboardHistoryModal.isVisible = false;
|
||||
clipboardHistoryModal.searchText = "";
|
||||
cleanupTempFiles();
|
||||
}
|
||||
|
||||
function cleanupTempFiles() {
|
||||
cleanupProcess.command = ["sh", "-c", "rm -f /tmp/clipboard_preview_*.png"];
|
||||
cleanupProcess.running = true;
|
||||
}
|
||||
|
||||
function refreshClipboard() {
|
||||
clipboardProcess.running = true;
|
||||
}
|
||||
|
||||
function copyEntry(entry) {
|
||||
const entryId = entry.split('\t')[0];
|
||||
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`];
|
||||
copyProcess.running = true;
|
||||
console.log("ClipboardHistoryModal: Entry copied, showing toast");
|
||||
ToastService.showInfo("Copied to clipboard");
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
console.log("Deleting entry:", entry);
|
||||
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`];
|
||||
deleteProcess.running = true;
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
clearProcess.running = true;
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
let content = entry.replace(/^\s*\d+\s+/, "");
|
||||
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||
const dimensionMatch = content.match(/(\d+)x(\d+)/);
|
||||
if (dimensionMatch)
|
||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`;
|
||||
|
||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i);
|
||||
if (typeMatch)
|
||||
return `Image (${typeMatch[1].toUpperCase()})`;
|
||||
|
||||
return "Image";
|
||||
}
|
||||
if (content.length > 100)
|
||||
return content.substring(0, 100) + "...";
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function getEntryType(entry) {
|
||||
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
|
||||
return "image";
|
||||
|
||||
if (entry.length > 200)
|
||||
return "long_text";
|
||||
|
||||
return "text";
|
||||
}
|
||||
|
||||
// DankModal configuration
|
||||
visible: isVisible
|
||||
width: 650
|
||||
height: 550
|
||||
keyboardFocus: "ondemand"
|
||||
onBackgroundClicked: {
|
||||
hide();
|
||||
}
|
||||
|
||||
// Clear confirmation dialog
|
||||
DankModal {
|
||||
id: clearConfirmDialog
|
||||
|
||||
visible: showClearConfirmation
|
||||
width: 350
|
||||
height: 150
|
||||
keyboardFocus: "ondemand"
|
||||
onBackgroundClicked: {
|
||||
showClearConfirmation = false;
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Clear All History?"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "This will permanently delete all clipboard history."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelClearButton.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Text {
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelClearButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: showClearConfirmation = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: confirmClearButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.9) : Theme.error
|
||||
|
||||
Text {
|
||||
text: "Clear All"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmClearButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
clearAll();
|
||||
showClearConfirmation = false;
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Data models
|
||||
ListModel {
|
||||
id: clipboardModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredClipboardModel
|
||||
}
|
||||
|
||||
// Processes
|
||||
Process {
|
||||
id: clipboardProcess
|
||||
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
clipboardModel.clear();
|
||||
const lines = text.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim().length > 0)
|
||||
clipboardModel.append({
|
||||
"entry": line
|
||||
});
|
||||
|
||||
}
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: copyProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.error("Copy failed with exit code:", exitCode);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deleteProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
refreshClipboard();
|
||||
else
|
||||
console.error("Delete failed with exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: clearProcess
|
||||
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
clipboardModel.clear();
|
||||
filteredClipboardModel.clear();
|
||||
totalCount = 0;
|
||||
} else {
|
||||
console.error("Clear failed with exit code:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cleanupProcess
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
console.log("ClipboardHistoryModal: IPC open() called");
|
||||
clipboardHistoryModal.show();
|
||||
return "CLIPBOARD_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close() {
|
||||
console.log("ClipboardHistoryModal: IPC close() called");
|
||||
clipboardHistoryModal.hide();
|
||||
return "CLIPBOARD_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
console.log("ClipboardHistoryModal: IPC toggle() called");
|
||||
clipboardHistoryModal.toggle();
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "clipboard"
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Header with search
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40
|
||||
color: "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "content_paste"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: `Clipboard History (${totalCount})`
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "delete_sweep"
|
||||
iconSize: Theme.iconSize
|
||||
iconColor: Theme.error
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: {
|
||||
showClearConfirmation = true;
|
||||
}
|
||||
}
|
||||
|
||||
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: hide()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Search field
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width
|
||||
placeholderText: "Search clipboard history..."
|
||||
leftIconName: "search"
|
||||
showClearButton: true
|
||||
onTextChanged: {
|
||||
clipboardHistoryModal.searchText = text;
|
||||
updateFilteredModel();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onOpened() {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
searchField.clearFocus();
|
||||
}
|
||||
|
||||
target: clipboardHistoryModal
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clipboard entries list
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - 110
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
|
||||
clip: true
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: clipboardListView
|
||||
|
||||
width: parent.availableWidth
|
||||
model: filteredClipboardModel
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "No clipboard entries found"
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: filteredClipboardModel.count === 0
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
property string entryType: getEntryType(model.entry)
|
||||
property string entryPreview: getEntryPreview(model.entry)
|
||||
property int entryIndex: index + 1
|
||||
|
||||
width: clipboardListView.width
|
||||
height: Math.max(60, contentText.contentHeight + Theme.spacingL)
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.04)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS // Reduced right margin
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Index number
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: entryIndex.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Content icon and text
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 68 // Account for index (24) + spacing (16) + delete button (32) - small margin
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (entryType === "image")
|
||||
return "image";
|
||||
|
||||
if (entryType === "long_text")
|
||||
return "subject";
|
||||
|
||||
return "content_copy";
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (entryType) {
|
||||
case "image":
|
||||
return "Image • " + entryPreview;
|
||||
case "long_text":
|
||||
return "Long Text";
|
||||
default:
|
||||
return "Text";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
id: contentText
|
||||
|
||||
text: entryPreview
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: entryType === "long_text" ? 3 : 1
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Delete button
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "dangerous"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.error
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: {
|
||||
console.log("Delete clicked for entry:", model.entry);
|
||||
deleteEntry(model.entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Main click area - explicitly excludes delete button area
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40 // Enough space to avoid delete button (32 + 8 margin)
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyEntry(model.entry)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
224
Modals/DankModal.qml
Normal file
224
Modals/DankModal.qml
Normal file
@@ -0,0 +1,224 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
// Core properties
|
||||
property alias content: contentLoader.sourceComponent
|
||||
// Sizing - can use fixed or relative to screen
|
||||
property real width: 400
|
||||
property real height: 300
|
||||
// Screen-relative sizing helpers
|
||||
readonly property real screenWidth: screen ? screen.width : 1920
|
||||
readonly property real screenHeight: screen ? screen.height : 1080
|
||||
// Background behavior
|
||||
property bool showBackground: true
|
||||
property real backgroundOpacity: 0.5
|
||||
// Positioning
|
||||
property string positioning: "center"
|
||||
// "center", "top-right", "custom"
|
||||
property point customPosition: Qt.point(0, 0)
|
||||
// Focus management
|
||||
property string keyboardFocus: "ondemand"
|
||||
// "ondemand", "exclusive", "none"
|
||||
property bool closeOnEscapeKey: true
|
||||
property bool closeOnBackgroundClick: true
|
||||
// Animation
|
||||
property string animationType: "scale"
|
||||
// "scale", "slide", "fade"
|
||||
property int animationDuration: Theme.mediumDuration
|
||||
property var animationEasing: Theme.emphasizedEasing
|
||||
// Styling
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
property real borderWidth: 1
|
||||
property real cornerRadius: Theme.cornerRadiusLarge
|
||||
property bool enableShadow: false
|
||||
|
||||
// Signals
|
||||
signal opened()
|
||||
signal dialogClosed()
|
||||
signal backgroundClicked()
|
||||
|
||||
// Convenience functions
|
||||
function open() {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
// PanelWindow configuration
|
||||
// visible property is inherited from PanelWindow
|
||||
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 {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Background overlay
|
||||
Rectangle {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: root.showBackground ? (root.visible ? root.backgroundOpacity : 0) : 0
|
||||
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 {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.animationEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Main content container
|
||||
Rectangle {
|
||||
id: contentContainer
|
||||
|
||||
width: root.width
|
||||
height: root.height
|
||||
// Positioning
|
||||
anchors.centerIn: positioning === "center" ? parent : undefined
|
||||
x: {
|
||||
if (positioning === "top-right")
|
||||
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL);
|
||||
else if (positioning === "custom")
|
||||
return root.customPosition.x;
|
||||
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
|
||||
}
|
||||
y: {
|
||||
if (positioning === "top-right")
|
||||
return Theme.barHeight + Theme.spacingXS;
|
||||
else if (positioning === "custom")
|
||||
return root.customPosition.y;
|
||||
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
|
||||
}
|
||||
color: root.backgroundColor
|
||||
radius: root.cornerRadius
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
layer.enabled: root.enableShadow
|
||||
// Animation properties
|
||||
opacity: root.visible ? 1 : 0
|
||||
scale: {
|
||||
if (root.animationType === "scale")
|
||||
return root.visible ? 1 : 0.9;
|
||||
|
||||
return 1;
|
||||
}
|
||||
// Transform for slide animation
|
||||
transform: root.animationType === "slide" ? slideTransform : null
|
||||
|
||||
Translate {
|
||||
id: slideTransform
|
||||
|
||||
x: root.visible ? 0 : 15
|
||||
y: root.visible ? 0 : -30
|
||||
}
|
||||
|
||||
// Content area
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: true
|
||||
asynchronous: false
|
||||
}
|
||||
|
||||
// Animations
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.animationEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: root.animationType === "scale"
|
||||
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.animationEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Shadow effect
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 8
|
||||
shadowBlur: 1
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadowOpacity: 0.3
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Keyboard handling
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
|
||||
anchors.fill: parent
|
||||
visible: root.visible // Only active when the modal is visible
|
||||
Keys.onEscapePressed: (event) => {
|
||||
if (root.closeOnEscapeKey) {
|
||||
root.visible = false;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
183
Modals/NetworkInfoModal.qml
Normal file
183
Modals/NetworkInfoModal.qml
Normal file
@@ -0,0 +1,183 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property bool networkInfoModalVisible: false
|
||||
property string networkSSID: ""
|
||||
property var networkData: null
|
||||
property string networkDetails: ""
|
||||
|
||||
function showNetworkInfo(ssid, data) {
|
||||
networkSSID = ssid;
|
||||
networkData = data;
|
||||
networkInfoModalVisible = true;
|
||||
WifiService.fetchNetworkInfo(ssid);
|
||||
}
|
||||
|
||||
function hideDialog() {
|
||||
networkInfoModalVisible = false;
|
||||
networkSSID = "";
|
||||
networkData = null;
|
||||
networkDetails = "";
|
||||
}
|
||||
|
||||
visible: networkInfoModalVisible
|
||||
width: 600
|
||||
height: 500
|
||||
enableShadow: true
|
||||
onBackgroundClicked: {
|
||||
hideDialog();
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
networkSSID = "";
|
||||
networkData = null;
|
||||
networkDetails = "";
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "Network Information"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Details for \"" + networkSSID + "\""
|
||||
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: {
|
||||
root.hideDialog();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Network Details
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 140
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Flickable {
|
||||
contentWidth: parent.width
|
||||
contentHeight: detailsRect.height
|
||||
|
||||
Rectangle {
|
||||
id: detailsRect
|
||||
|
||||
width: parent.width
|
||||
height: Math.max(parent.parent.height, detailsText.contentHeight + Theme.spacingM * 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: detailsText
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
text: WifiService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
lineHeight: 1.5
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Close Button
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
Text {
|
||||
id: closeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "Close"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hideDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
185
Modals/PowerConfirmModal.qml
Normal file
185
Modals/PowerConfirmModal.qml
Normal file
@@ -0,0 +1,185 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property bool powerConfirmVisible: false
|
||||
property string powerConfirmAction: ""
|
||||
property string powerConfirmTitle: ""
|
||||
property string powerConfirmMessage: ""
|
||||
|
||||
function executePowerAction(action) {
|
||||
console.log("Executing power action:", action);
|
||||
let command = [];
|
||||
switch (action) {
|
||||
case "logout":
|
||||
command = ["niri", "msg", "action", "quit", "-s"];
|
||||
break;
|
||||
case "suspend":
|
||||
command = ["systemctl", "suspend"];
|
||||
break;
|
||||
case "reboot":
|
||||
command = ["systemctl", "reboot"];
|
||||
break;
|
||||
case "poweroff":
|
||||
command = ["systemctl", "poweroff"];
|
||||
break;
|
||||
}
|
||||
if (command.length > 0) {
|
||||
powerActionProcess.command = command;
|
||||
powerActionProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
// DankModal configuration
|
||||
visible: powerConfirmVisible
|
||||
width: 350
|
||||
height: 160
|
||||
keyboardFocus: "ondemand"
|
||||
enableShadow: false
|
||||
onBackgroundClicked: {
|
||||
powerConfirmVisible = false;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: powerActionProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.error("Power action failed with exit code:", exitCode);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Title
|
||||
Text {
|
||||
text: powerConfirmTitle
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: {
|
||||
switch (powerConfirmAction) {
|
||||
case "poweroff":
|
||||
return Theme.error;
|
||||
case "reboot":
|
||||
return Theme.warning;
|
||||
default:
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Message
|
||||
Text {
|
||||
text: powerConfirmMessage
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
height: Theme.spacingS
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Cancel button
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButton.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: cancelButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerConfirmVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Confirm button
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let baseColor;
|
||||
switch (powerConfirmAction) {
|
||||
case "poweroff":
|
||||
baseColor = Theme.error;
|
||||
break;
|
||||
case "reboot":
|
||||
baseColor = Theme.warning;
|
||||
break;
|
||||
default:
|
||||
baseColor = Theme.primary;
|
||||
break;
|
||||
}
|
||||
return confirmButton.containsMouse ? Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) : baseColor;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Confirm"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerConfirmVisible = false;
|
||||
executePowerAction(powerConfirmAction);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
319
Modals/ProcessListModal.qml
Normal file
319
Modals/ProcessListModal.qml
Normal file
@@ -0,0 +1,319 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ProcessList
|
||||
|
||||
DankModal {
|
||||
id: processListModal
|
||||
|
||||
property int currentTab: 0
|
||||
property var tabNames: ["Processes", "Performance", "System"]
|
||||
|
||||
function show() {
|
||||
processListModal.visible = true;
|
||||
ProcessMonitorService.updateSystemInfo();
|
||||
ProcessMonitorService.updateProcessList();
|
||||
SystemMonitorService.enableDetailedMonitoring(true);
|
||||
SystemMonitorService.updateSystemInfo();
|
||||
UserInfoService.getUptime();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
processListModal.visible = false;
|
||||
SystemMonitorService.enableDetailedMonitoring(false);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (processListModal.visible)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
}
|
||||
|
||||
width: 900
|
||||
height: 680
|
||||
visible: false
|
||||
keyboardFocus: "exclusive"
|
||||
backgroundColor: Theme.popupBackground()
|
||||
cornerRadius: Theme.cornerRadiusXLarge
|
||||
enableShadow: true
|
||||
|
||||
onVisibleChanged: {
|
||||
ProcessMonitorService.enableMonitoring(visible);
|
||||
}
|
||||
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
processListModal.hide();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_1) {
|
||||
currentTab = 0;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_2) {
|
||||
currentTab = 1;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_3) {
|
||||
currentTab = 2;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
height: 40
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "System Monitor"
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 280
|
||||
height: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: ProcessMonitorService.processes.length + " processes"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
width: Math.min(implicitWidth, 120)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: tabNames
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||
height: 44
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: currentTab === index ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : (tabMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "list_alt";
|
||||
case 1:
|
||||
return "analytics";
|
||||
case 2:
|
||||
return "settings";
|
||||
default:
|
||||
return "tab";
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: currentTab === index ? Font.Bold : Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Loader {
|
||||
id: processesTab
|
||||
|
||||
anchors.fill: parent
|
||||
visible: currentTab === 0
|
||||
opacity: currentTab === 0 ? 1 : 0
|
||||
sourceComponent: processesTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTab
|
||||
|
||||
anchors.fill: parent
|
||||
visible: currentTab === 1
|
||||
opacity: currentTab === 1 ? 1 : 0
|
||||
sourceComponent: performanceTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTab
|
||||
|
||||
anchors.fill: parent
|
||||
visible: currentTab === 2
|
||||
opacity: currentTab === 2 ? 1 : 0
|
||||
sourceComponent: systemTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processesTabComponent
|
||||
ProcessesTab {
|
||||
contextMenu: processContextMenuWindow
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
PerformanceTab {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
SystemTab {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
ProcessContextMenu {
|
||||
id: processContextMenuWindow
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
processListModal.show();
|
||||
return "PROCESSLIST_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close() {
|
||||
processListModal.hide();
|
||||
return "PROCESSLIST_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
processListModal.toggle();
|
||||
return "PROCESSLIST_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "processlist"
|
||||
}
|
||||
|
||||
}
|
||||
164
Modals/SettingsModal.qml
Normal file
164
Modals/SettingsModal.qml
Normal file
@@ -0,0 +1,164 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings
|
||||
|
||||
DankModal {
|
||||
id: settingsModal
|
||||
|
||||
property bool settingsVisible: false
|
||||
|
||||
signal closingModal()
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
closingModal();
|
||||
|
||||
}
|
||||
// DankModal configuration
|
||||
visible: settingsVisible
|
||||
width: 650
|
||||
height: 750
|
||||
keyboardFocus: "ondemand"
|
||||
enableShadow: true
|
||||
onBackgroundClicked: {
|
||||
settingsVisible = false;
|
||||
}
|
||||
|
||||
// Keyboard focus and shortcuts
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: settingsModal.settingsVisible
|
||||
Keys.onEscapePressed: settingsModal.settingsVisible = false
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Settings"
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 175 // Spacer to push close button to the right
|
||||
height: 1
|
||||
}
|
||||
|
||||
// Close button
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
onClicked: settingsModal.settingsVisible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Settings sections
|
||||
ScrollView {
|
||||
id: settingsScrollView
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - 50
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
|
||||
width: settingsScrollView.width - 20
|
||||
spacing: Theme.spacingL
|
||||
bottomPadding: Theme.spacingL
|
||||
|
||||
// Profile Settings
|
||||
SettingsSection {
|
||||
title: "Profile"
|
||||
iconName: "person"
|
||||
|
||||
content: ProfileTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clock Settings
|
||||
SettingsSection {
|
||||
title: "Clock & Time"
|
||||
iconName: "schedule"
|
||||
|
||||
content: ClockTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Weather Settings
|
||||
SettingsSection {
|
||||
title: "Weather"
|
||||
iconName: "wb_sunny"
|
||||
|
||||
content: WeatherTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Widget Visibility Settings
|
||||
SettingsSection {
|
||||
title: "Top Bar Widgets"
|
||||
iconName: "widgets"
|
||||
|
||||
content: WidgetsTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Workspace Settings
|
||||
SettingsSection {
|
||||
title: "Workspaces"
|
||||
iconName: "tab"
|
||||
|
||||
content: WorkspaceTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Display Settings
|
||||
SettingsSection {
|
||||
title: "Display & Appearance"
|
||||
iconName: "palette"
|
||||
|
||||
content: DisplayTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
579
Modals/SpotlightModal.qml
Normal file
579
Modals/SpotlightModal.qml
Normal file
@@ -0,0 +1,579 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: spotlightModal
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property var filteredApps: []
|
||||
property int selectedIndex: 0
|
||||
property int maxResults: 50
|
||||
property var categories: {
|
||||
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
|
||||
return cat !== "Education" && cat !== "Science";
|
||||
});
|
||||
// Insert "Recents" after "All"
|
||||
var result = ["All", "Recents"];
|
||||
return result.concat(allCategories.filter((cat) => {
|
||||
return cat !== "All";
|
||||
}));
|
||||
}
|
||||
property string selectedCategory: "All"
|
||||
property string viewMode: Prefs.spotlightModalViewMode // "list" or "grid"
|
||||
property int gridColumns: 4
|
||||
|
||||
function show() {
|
||||
console.log("SpotlightModal: show() called");
|
||||
spotlightOpen = true;
|
||||
console.log("SpotlightModal: spotlightOpen set to", spotlightOpen);
|
||||
searchDebounceTimer.stop(); // Stop any pending search
|
||||
updateFilteredApps(); // Immediate update when showing
|
||||
}
|
||||
|
||||
function hide() {
|
||||
spotlightOpen = false;
|
||||
searchDebounceTimer.stop(); // Stop any pending search
|
||||
searchQuery = "";
|
||||
selectedIndex = 0;
|
||||
selectedCategory = "All";
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
}
|
||||
|
||||
property string searchQuery: ""
|
||||
function updateFilteredApps() {
|
||||
filteredApps = [];
|
||||
selectedIndex = 0;
|
||||
var apps = [];
|
||||
if (searchQuery.length === 0) {
|
||||
// Show apps from category
|
||||
if (selectedCategory === "All") {
|
||||
// For "All" category, show all available apps
|
||||
apps = AppSearchService.applications || [];
|
||||
} else if (selectedCategory === "Recents") {
|
||||
// For "Recents" category, get recent apps from Prefs and filter out non-existent ones
|
||||
var recentApps = Prefs.getRecentApps();
|
||||
apps = recentApps.map((recentApp) => {
|
||||
return AppSearchService.getAppByExec(recentApp.exec);
|
||||
}).filter((app) => {
|
||||
return app !== null && !app.noDisplay;
|
||||
});
|
||||
} else {
|
||||
// For specific categories, limit results
|
||||
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
|
||||
apps = categoryApps.slice(0, maxResults);
|
||||
}
|
||||
} else {
|
||||
// Search with category filter
|
||||
if (selectedCategory === "All") {
|
||||
// For "All" category, search all apps without limit
|
||||
apps = AppSearchService.searchApplications(searchQuery);
|
||||
} else if (selectedCategory === "Recents") {
|
||||
// For "Recents" category, search within recent apps
|
||||
var recentApps = Prefs.getRecentApps();
|
||||
var recentDesktopEntries = recentApps.map((recentApp) => {
|
||||
return AppSearchService.getAppByExec(recentApp.exec);
|
||||
}).filter((app) => {
|
||||
return app !== null && !app.noDisplay;
|
||||
});
|
||||
if (recentDesktopEntries.length > 0) {
|
||||
var allSearchResults = AppSearchService.searchApplications(searchQuery);
|
||||
var recentNames = new Set(recentDesktopEntries.map((app) => {
|
||||
return app.name;
|
||||
}));
|
||||
// Filter search results to only include recent apps
|
||||
apps = allSearchResults.filter((searchApp) => {
|
||||
return recentNames.has(searchApp.name);
|
||||
});
|
||||
} else {
|
||||
apps = [];
|
||||
}
|
||||
} else {
|
||||
// For specific categories, filter search results by category
|
||||
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
|
||||
if (categoryApps.length > 0) {
|
||||
var allSearchResults = AppSearchService.searchApplications(searchQuery);
|
||||
var categoryNames = new Set(categoryApps.map((app) => {
|
||||
return app.name;
|
||||
}));
|
||||
// Filter search results to only include apps from the selected category
|
||||
apps = allSearchResults.filter((searchApp) => {
|
||||
return categoryNames.has(searchApp.name);
|
||||
}).slice(0, maxResults);
|
||||
} else {
|
||||
apps = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert to our format - batch operations for better performance
|
||||
filteredApps = apps.map((app) => {
|
||||
return ({
|
||||
"name": app.name,
|
||||
"exec": app.execString || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"comment": app.comment || "",
|
||||
"categories": app.categories || [],
|
||||
"desktopEntry": app
|
||||
});
|
||||
});
|
||||
// Clear and repopulate model efficiently
|
||||
filteredModel.clear();
|
||||
filteredApps.forEach((app) => {
|
||||
return filteredModel.append(app);
|
||||
});
|
||||
}
|
||||
|
||||
function launchApp(app) {
|
||||
Prefs.addRecentApp(app);
|
||||
if (app.desktopEntry) {
|
||||
app.desktopEntry.execute();
|
||||
} else {
|
||||
var cleanExec = app.exec.replace(/%[fFuU]/g, "").trim();
|
||||
console.log("Spotlight: Launching app directly:", cleanExec);
|
||||
Quickshell.execDetached(["sh", "-c", cleanExec]);
|
||||
}
|
||||
hide();
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredModel.count > 0) {
|
||||
if (viewMode === "grid") {
|
||||
// Grid navigation: move DOWN by one row (gridColumns positions)
|
||||
var columnsCount = gridColumns;
|
||||
var newIndex = Math.min(selectedIndex + columnsCount, filteredModel.count - 1);
|
||||
selectedIndex = newIndex;
|
||||
} else {
|
||||
// List navigation: next item
|
||||
selectedIndex = (selectedIndex + 1) % filteredModel.count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredModel.count > 0) {
|
||||
if (viewMode === "grid") {
|
||||
// Grid navigation: move UP by one row (gridColumns positions)
|
||||
var columnsCount = gridColumns;
|
||||
var newIndex = Math.max(selectedIndex - columnsCount, 0);
|
||||
selectedIndex = newIndex;
|
||||
} else {
|
||||
// List navigation: previous item
|
||||
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredModel.count - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectNextInRow() {
|
||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
||||
// Grid navigation: move RIGHT by one position
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function selectPreviousInRow() {
|
||||
if (filteredModel.count > 0 && viewMode === "grid") {
|
||||
// Grid navigation: move LEFT by one position
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function launchSelected() {
|
||||
if (filteredModel.count > 0 && selectedIndex >= 0 && selectedIndex < filteredModel.count) {
|
||||
var selectedApp = filteredModel.get(selectedIndex);
|
||||
launchApp(selectedApp);
|
||||
}
|
||||
}
|
||||
|
||||
// DankModal configuration
|
||||
visible: spotlightOpen
|
||||
width: 550
|
||||
height: 600
|
||||
keyboardFocus: "ondemand"
|
||||
backgroundColor: Theme.popupBackground()
|
||||
cornerRadius: Theme.cornerRadiusXLarge
|
||||
borderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
|
||||
onVisibleChanged: {
|
||||
console.log("SpotlightModal visibility changed to:", visible);
|
||||
if (visible && !spotlightOpen) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundClicked: {
|
||||
spotlightOpen = false;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("SpotlightModal: Component.onCompleted called - component loaded successfully!");
|
||||
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
|
||||
return cat !== "Education" && cat !== "Science";
|
||||
});
|
||||
// Insert "Recents" after "All"
|
||||
var result = ["All", "Recents"];
|
||||
categories = result.concat(allCategories.filter((cat) => {
|
||||
return cat !== "All";
|
||||
}));
|
||||
}
|
||||
|
||||
// Search debouncing
|
||||
Timer {
|
||||
id: searchDebounceTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: updateFilteredApps()
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredModel
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
hide();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
selectNext();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
selectPrevious();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Right && viewMode === "grid") {
|
||||
selectNextInRow();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Left && viewMode === "grid") {
|
||||
selectPreviousInRow();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
launchSelected();
|
||||
event.accepted = true;
|
||||
} else if (event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
|
||||
searchField.text = event.text;
|
||||
searchField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
|
||||
// Combined row for categories and view mode toggle
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: categories.length > 1 || filteredModel.count > 0
|
||||
|
||||
// Categories organized in 2 rows: 4 + 5
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Top row: All, Development, Graphics, Internet (4 items)
|
||||
Row {
|
||||
property var topRowCategories: ["All", "Recents", "Development", "Graphics"]
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: parent.topRowCategories.filter((cat) => {
|
||||
return categories.includes(cat);
|
||||
})
|
||||
|
||||
Rectangle {
|
||||
height: 36
|
||||
width: (parent.width - (parent.topRowCategories.length - 1) * Theme.spacingS) / parent.topRowCategories.length
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedCategory = modelData;
|
||||
updateFilteredApps();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom row: Media, Office, Settings, System, Utilities (5 items)
|
||||
Row {
|
||||
property var bottomRowCategories: ["Internet", "Media", "Office", "Settings", "System"]
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: parent.bottomRowCategories.filter((cat) => {
|
||||
return categories.includes(cat);
|
||||
})
|
||||
|
||||
Rectangle {
|
||||
height: 36
|
||||
width: (parent.width - (parent.bottomRowCategories.length - 1) * Theme.spacingS) / parent.bottomRowCategories.length
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedCategory = modelData;
|
||||
updateFilteredApps();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search field with view toggle buttons
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width - 80 - Theme.spacingM // Leave space for view toggle buttons
|
||||
height: 56
|
||||
cornerRadius: Theme.cornerRadiusLarge
|
||||
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
|
||||
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: spotlightOpen
|
||||
placeholderText: "Search applications..."
|
||||
text: searchQuery
|
||||
onTextEdited: {
|
||||
searchQuery = text;
|
||||
searchDebounceTimer.restart();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: spotlightModal
|
||||
function onOpened() {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
function onDialogClosed() {
|
||||
searchField.clearFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
hide();
|
||||
event.accepted = true;
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && searchQuery.length > 0) {
|
||||
// Launch first app when typing in search field
|
||||
if (filteredApps.length > 0) {
|
||||
launchApp(filteredApps[0]);
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
|
||||
(event.key === Qt.Key_Left && viewMode === "grid") ||
|
||||
(event.key === Qt.Key_Right && viewMode === "grid") ||
|
||||
((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && searchQuery.length === 0)) {
|
||||
// Pass navigation keys and enter (when not searching) to main handler
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View mode toggle buttons next to search bar
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: filteredModel.count > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// List view button
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : listViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
|
||||
border.color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
||||
border.width: 1
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "view_list"
|
||||
size: 18
|
||||
color: viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
viewMode = "list";
|
||||
Prefs.setSpotlightModalViewMode("list");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grid view button
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : gridViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
|
||||
border.color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
||||
border.width: 1
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "grid_view"
|
||||
size: 18
|
||||
color: viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
viewMode = "grid";
|
||||
Prefs.setSpotlightModalViewMode("grid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Results container
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - y // Use remaining space
|
||||
color: "transparent"
|
||||
|
||||
// List view
|
||||
DankListView {
|
||||
id: resultsList
|
||||
|
||||
anchors.fill: parent
|
||||
visible: viewMode === "list"
|
||||
model: filteredModel
|
||||
currentIndex: selectedIndex
|
||||
itemHeight: 60
|
||||
iconSize: 40
|
||||
showDescription: true
|
||||
hoverUpdatesSelection: false
|
||||
onItemClicked: function(index, modelData) {
|
||||
launchApp(modelData);
|
||||
}
|
||||
onItemHovered: function(index) {
|
||||
selectedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
// Grid view
|
||||
DankGridView {
|
||||
id: resultsGrid
|
||||
|
||||
anchors.fill: parent
|
||||
visible: viewMode === "grid"
|
||||
model: filteredModel
|
||||
columns: 4
|
||||
adaptiveColumns: false
|
||||
minCellWidth: 120
|
||||
maxCellWidth: 160
|
||||
iconSizeRatio: 0.55
|
||||
maxIconSize: 48
|
||||
currentIndex: selectedIndex
|
||||
hoverUpdatesSelection: false
|
||||
onItemClicked: function(index, modelData) {
|
||||
launchApp(modelData);
|
||||
}
|
||||
onItemHovered: function(index) {
|
||||
selectedIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
console.log("SpotlightModal: IPC open() called");
|
||||
spotlightModal.show();
|
||||
return "SPOTLIGHT_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close() {
|
||||
console.log("SpotlightModal: IPC close() called");
|
||||
spotlightModal.hide();
|
||||
return "SPOTLIGHT_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
console.log("SpotlightModal: IPC toggle() called");
|
||||
spotlightModal.toggle();
|
||||
return "SPOTLIGHT_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
}
|
||||
275
Modals/WifiPasswordModal.qml
Normal file
275
Modals/WifiPasswordModal.qml
Normal file
@@ -0,0 +1,275 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property bool wifiPasswordModalVisible: false
|
||||
property string wifiPasswordSSID: ""
|
||||
property string wifiPasswordInput: ""
|
||||
|
||||
visible: wifiPasswordModalVisible
|
||||
width: 420
|
||||
height: 230
|
||||
keyboardFocus: "ondemand"
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
wifiPasswordInput = "";
|
||||
|
||||
}
|
||||
onBackgroundClicked: {
|
||||
wifiPasswordModalVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
|
||||
// Auto-reopen dialog on invalid password
|
||||
Connections {
|
||||
function onPasswordDialogShouldReopenChanged() {
|
||||
if (WifiService.passwordDialogShouldReopen && WifiService.connectingSSID !== "") {
|
||||
wifiPasswordSSID = WifiService.connectingSSID;
|
||||
wifiPasswordInput = "";
|
||||
wifiPasswordModalVisible = true;
|
||||
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: {
|
||||
wifiPasswordModalVisible = 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);
|
||||
wifiPasswordModalVisible = 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: {
|
||||
wifiPasswordModalVisible = 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);
|
||||
wifiPasswordModalVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
passwordInput.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user