mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
spotlight: danksearch integration (indexed file search)
This commit is contained in:
237
Modals/Spotlight/FileSearchController.qml
Normal file
237
Modals/Spotlight/FileSearchController.qml
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: controller
|
||||||
|
|
||||||
|
property string searchQuery: ""
|
||||||
|
property alias model: fileModel
|
||||||
|
property int selectedIndex: 0
|
||||||
|
property bool keyboardNavigationActive: false
|
||||||
|
property bool isSearching: false
|
||||||
|
property int totalResults: 0
|
||||||
|
property string searchField: "filename"
|
||||||
|
|
||||||
|
signal searchCompleted
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: fileModel
|
||||||
|
}
|
||||||
|
|
||||||
|
function performSearch() {
|
||||||
|
if (!DSearchService.isConnected) {
|
||||||
|
model.clear()
|
||||||
|
totalResults = 0
|
||||||
|
isSearching = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.length === 0) {
|
||||||
|
model.clear()
|
||||||
|
totalResults = 0
|
||||||
|
isSearching = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching = true
|
||||||
|
const params = {
|
||||||
|
"limit": 50,
|
||||||
|
"fuzzy": true,
|
||||||
|
"sort_by": "score",
|
||||||
|
"sort_desc": true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchField && searchField !== "all") {
|
||||||
|
params.field = searchField
|
||||||
|
}
|
||||||
|
|
||||||
|
DSearchService.search(searchQuery, params, response => {
|
||||||
|
if (response.error) {
|
||||||
|
model.clear()
|
||||||
|
totalResults = 0
|
||||||
|
isSearching = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.result) {
|
||||||
|
updateModel(response.result)
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching = false
|
||||||
|
searchCompleted()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModel(result) {
|
||||||
|
model.clear()
|
||||||
|
totalResults = result.total_hits || 0
|
||||||
|
selectedIndex = 0
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
|
||||||
|
if (!result.hits || result.hits.length === 0) {
|
||||||
|
selectedIndex = -1
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < result.hits.length; i++) {
|
||||||
|
const hit = result.hits[i]
|
||||||
|
const filePath = hit.id || ""
|
||||||
|
const fileName = getFileName(filePath)
|
||||||
|
const fileExt = getFileExtension(fileName)
|
||||||
|
const fileType = determineFileType(fileName, filePath)
|
||||||
|
const dirPath = getDirPath(filePath)
|
||||||
|
|
||||||
|
model.append({
|
||||||
|
"filePath": filePath,
|
||||||
|
"fileName": fileName,
|
||||||
|
"fileExtension": fileExt,
|
||||||
|
"fileType": fileType,
|
||||||
|
"dirPath": dirPath,
|
||||||
|
"score": hit.score || 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName(path) {
|
||||||
|
const parts = path.split('/')
|
||||||
|
return parts[parts.length - 1] || path
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtension(fileName) {
|
||||||
|
const parts = fileName.split('.')
|
||||||
|
if (parts.length > 1) {
|
||||||
|
return parts[parts.length - 1].toLowerCase()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirPath(path) {
|
||||||
|
const lastSlash = path.lastIndexOf('/')
|
||||||
|
if (lastSlash > 0) {
|
||||||
|
return path.substring(0, lastSlash)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineFileType(fileName, filePath) {
|
||||||
|
const ext = getFileExtension(fileName)
|
||||||
|
|
||||||
|
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
|
||||||
|
if (imageExts.includes(ext)) {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
|
||||||
|
if (videoExts.includes(ext)) {
|
||||||
|
return "video"
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
|
||||||
|
if (audioExts.includes(ext)) {
|
||||||
|
return "audio"
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
|
||||||
|
if (codeExts.includes(ext)) {
|
||||||
|
return "code"
|
||||||
|
}
|
||||||
|
|
||||||
|
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
|
||||||
|
if (docExts.includes(ext)) {
|
||||||
|
return "document"
|
||||||
|
}
|
||||||
|
|
||||||
|
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
|
||||||
|
if (archiveExts.includes(ext)) {
|
||||||
|
return "archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ext || fileName.indexOf('.') === -1) {
|
||||||
|
return "binary"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
if (model.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, model.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
if (model.count === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyboardNavigationActive = true
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
signal fileOpened
|
||||||
|
|
||||||
|
function openFile(filePath) {
|
||||||
|
if (!filePath || filePath.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = filePath
|
||||||
|
if (!url.startsWith("file://")) {
|
||||||
|
url = "file://" + filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt.openUrlExternally(url)
|
||||||
|
fileOpened()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFolder(filePath) {
|
||||||
|
if (!filePath || filePath.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSlash = filePath.lastIndexOf('/')
|
||||||
|
if (lastSlash <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirPath = filePath.substring(0, lastSlash)
|
||||||
|
let url = dirPath
|
||||||
|
if (!url.startsWith("file://")) {
|
||||||
|
url = "file://" + dirPath
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt.openUrlExternally(url)
|
||||||
|
fileOpened()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSelected() {
|
||||||
|
if (model.count === 0 || selectedIndex < 0 || selectedIndex >= model.count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = model.get(selectedIndex)
|
||||||
|
if (item && item.filePath) {
|
||||||
|
openFile(item.filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
searchQuery = ""
|
||||||
|
model.clear()
|
||||||
|
selectedIndex = -1
|
||||||
|
keyboardNavigationActive = false
|
||||||
|
isSearching = false
|
||||||
|
totalResults = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchQueryChanged: {
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchFieldChanged: {
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
155
Modals/Spotlight/FileSearchEntry.qml
Normal file
155
Modals/Spotlight/FileSearchEntry.qml
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: entry
|
||||||
|
|
||||||
|
required property string filePath
|
||||||
|
required property string fileName
|
||||||
|
required property string fileExtension
|
||||||
|
required property string fileType
|
||||||
|
required property string dirPath
|
||||||
|
required property bool isSelected
|
||||||
|
required property int itemIndex
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
readonly property int iconSize: 40
|
||||||
|
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: isSelected ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: imagePreview
|
||||||
|
anchors.fill: parent
|
||||||
|
source: fileType === "image" ? `file://${filePath}` : ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
cache: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: fileType === "image" && status === Image.Ready
|
||||||
|
sourceSize.width: 128
|
||||||
|
sourceSize.height: 128
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: imagePreview
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: imageMask
|
||||||
|
visible: fileType === "image" && imagePreview.status === Image.Ready
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: imageMask
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: "black"
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: getFileTypeColor()
|
||||||
|
visible: fileType !== "image" || imagePreview.status !== Image.Ready
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: getFileIconText()
|
||||||
|
font.pixelSize: fileExtension.length > 0 ? (fileExtension.length > 3 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall) : Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: fileName
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: dirPath
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: entry.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileTypeColor() {
|
||||||
|
switch (fileType) {
|
||||||
|
case "code":
|
||||||
|
return Theme.codeFileColor || Theme.primarySelected
|
||||||
|
case "document":
|
||||||
|
return Theme.docFileColor || Theme.secondarySelected
|
||||||
|
case "video":
|
||||||
|
return Theme.videoFileColor || Theme.tertiarySelected
|
||||||
|
case "audio":
|
||||||
|
return Theme.audioFileColor || Theme.errorSelected
|
||||||
|
case "archive":
|
||||||
|
return Theme.archiveFileColor || Theme.warningSelected
|
||||||
|
case "binary":
|
||||||
|
return Theme.binaryFileColor || Theme.surfaceDim
|
||||||
|
default:
|
||||||
|
return Theme.surfaceLight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileIconText() {
|
||||||
|
if (fileType === "binary") {
|
||||||
|
return "bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileExtension.length > 0) {
|
||||||
|
return fileExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
214
Modals/Spotlight/FileSearchResults.qml
Normal file
214
Modals/Spotlight/FileSearchResults.qml
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: resultsContainer
|
||||||
|
|
||||||
|
property var fileSearchController: null
|
||||||
|
|
||||||
|
function resetScroll() {
|
||||||
|
filesList.contentY = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: filesList
|
||||||
|
|
||||||
|
property int itemHeight: 60
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool keyboardNavigationActive: fileSearchController ? fileSearchController.keyboardNavigationActive : false
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return
|
||||||
|
|
||||||
|
const itemY = index * (itemHeight + itemSpacing)
|
||||||
|
const itemBottom = itemY + itemHeight
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
model: fileSearchController ? fileSearchController.model : null
|
||||||
|
currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1
|
||||||
|
clip: true
|
||||||
|
spacing: itemSpacing
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property int index
|
||||||
|
required property string filePath
|
||||||
|
required property string fileName
|
||||||
|
required property string fileExtension
|
||||||
|
required property string fileType
|
||||||
|
required property string dirPath
|
||||||
|
|
||||||
|
width: ListView.view.width
|
||||||
|
height: filesList.itemHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : fileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconBackground
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: width / 2
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
visible: fileType !== "image"
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
active: fileName.toLowerCase().startsWith("dockerfile")
|
||||||
|
sourceComponent: DankNFIcon {
|
||||||
|
name: "docker"
|
||||||
|
size: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: fileExtension ? (fileExtension.length > 4 ? fileExtension.substring(0, 4) : fileExtension) : "?"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Bold
|
||||||
|
visible: !fileName.toLowerCase().startsWith("dockerfile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
active: fileType === "image"
|
||||||
|
sourceComponent: Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: "file://" + filePath
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: false
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
maskEnabled: true
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1.0
|
||||||
|
maskSource: ShaderEffectSource {
|
||||||
|
sourceItem: Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - 40 - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: fileName || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: dirPath || ""
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: fileMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
z: 10
|
||||||
|
onEntered: () => {
|
||||||
|
if (fileSearchController && filesList.keyboardNavigationActive) {
|
||||||
|
fileSearchController.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
|
filesList.currentIndex = index
|
||||||
|
}
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
if (fileSearchController)
|
||||||
|
fileSearchController.openFile(filePath)
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
if (fileSearchController)
|
||||||
|
fileSearchController.openFolder(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !fileSearchController || !fileSearchController.model || fileSearchController.model.count === 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
property string displayText: {
|
||||||
|
if (!fileSearchController) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (!DSearchService.isConnected) {
|
||||||
|
return I18n.tr("DankSearch not available")
|
||||||
|
}
|
||||||
|
if (fileSearchController.isSearching) {
|
||||||
|
return I18n.tr("Searching...")
|
||||||
|
}
|
||||||
|
if (fileSearchController.searchQuery.length === 0) {
|
||||||
|
return I18n.tr("Enter a search query")
|
||||||
|
}
|
||||||
|
if (!fileSearchController.model || fileSearchController.model.count === 0) {
|
||||||
|
return I18n.tr("No files found")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
text: displayText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: displayText.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,40 @@ Item {
|
|||||||
|
|
||||||
property alias appLauncher: appLauncher
|
property alias appLauncher: appLauncher
|
||||||
property alias searchField: searchField
|
property alias searchField: searchField
|
||||||
|
property alias fileSearchController: fileSearchController
|
||||||
property var parentModal: null
|
property var parentModal: null
|
||||||
|
property string searchMode: "apps"
|
||||||
|
|
||||||
function resetScroll() {
|
function resetScroll() {
|
||||||
resultsView.resetScroll()
|
if (searchMode === "apps") {
|
||||||
|
resultsView.resetScroll()
|
||||||
|
} else {
|
||||||
|
fileSearchResults.resetScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSearchMode() {
|
||||||
|
if (searchField.text.startsWith("/")) {
|
||||||
|
if (searchMode !== "files") {
|
||||||
|
searchMode = "files"
|
||||||
|
}
|
||||||
|
const query = searchField.text.substring(1)
|
||||||
|
fileSearchController.searchQuery = query
|
||||||
|
} else {
|
||||||
|
if (searchMode !== "apps") {
|
||||||
|
searchMode = "apps"
|
||||||
|
fileSearchController.reset()
|
||||||
|
appLauncher.searchQuery = searchField.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchModeChanged: {
|
||||||
|
if (searchMode === "files") {
|
||||||
|
appLauncher.keyboardNavigationActive = false
|
||||||
|
} else {
|
||||||
|
fileSearchController.keyboardNavigationActive = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -27,59 +57,95 @@ Item {
|
|||||||
|
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Down) {
|
} else if (event.key === Qt.Key_Down) {
|
||||||
appLauncher.selectNext()
|
if (searchMode === "apps") {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
} else {
|
||||||
|
fileSearchController.selectNext()
|
||||||
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Up) {
|
} else if (event.key === Qt.Key_Up) {
|
||||||
appLauncher.selectPrevious()
|
if (searchMode === "apps") {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
} else {
|
||||||
|
fileSearchController.selectPrevious()
|
||||||
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
|
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||||
appLauncher.selectNextInRow()
|
appLauncher.selectNextInRow()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
|
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||||
appLauncher.selectPreviousInRow()
|
appLauncher.selectPreviousInRow()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||||
appLauncher.selectNext()
|
if (searchMode === "apps") {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
} else {
|
||||||
|
fileSearchController.selectNext()
|
||||||
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||||
appLauncher.selectPrevious()
|
if (searchMode === "apps") {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
} else {
|
||||||
|
fileSearchController.selectPrevious()
|
||||||
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
|
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||||
appLauncher.selectNextInRow()
|
appLauncher.selectNextInRow()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
|
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||||
appLauncher.selectPreviousInRow()
|
appLauncher.selectPreviousInRow()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Tab) {
|
} else if (event.key === Qt.Key_Tab) {
|
||||||
if (appLauncher.viewMode === "grid") {
|
if (searchMode === "apps") {
|
||||||
appLauncher.selectNextInRow()
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
appLauncher.selectNext()
|
fileSearchController.selectNext()
|
||||||
}
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Backtab) {
|
} else if (event.key === Qt.Key_Backtab) {
|
||||||
if (appLauncher.viewMode === "grid") {
|
if (searchMode === "apps") {
|
||||||
appLauncher.selectPreviousInRow()
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
appLauncher.selectPrevious()
|
fileSearchController.selectPrevious()
|
||||||
}
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||||
if (appLauncher.viewMode === "grid") {
|
if (searchMode === "apps") {
|
||||||
appLauncher.selectNextInRow()
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectNextInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectNext()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
appLauncher.selectNext()
|
fileSearchController.selectNext()
|
||||||
}
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||||
if (appLauncher.viewMode === "grid") {
|
if (searchMode === "apps") {
|
||||||
appLauncher.selectPreviousInRow()
|
if (appLauncher.viewMode === "grid") {
|
||||||
|
appLauncher.selectPreviousInRow()
|
||||||
|
} else {
|
||||||
|
appLauncher.selectPrevious()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
appLauncher.selectPrevious()
|
fileSearchController.selectPrevious()
|
||||||
}
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
appLauncher.launchSelected()
|
if (searchMode === "apps") {
|
||||||
|
appLauncher.launchSelected()
|
||||||
|
} else if (searchMode === "files") {
|
||||||
|
fileSearchController.openSelected()
|
||||||
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,6 +164,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileSearchController {
|
||||||
|
id: fileSearchController
|
||||||
|
|
||||||
|
onFileOpened: () => {
|
||||||
|
if (parentModal)
|
||||||
|
parentModal.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
@@ -118,7 +193,7 @@ Item {
|
|||||||
backgroundColor: Theme.surfaceContainerHigh
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
normalBorderColor: Theme.outlineMedium
|
normalBorderColor: Theme.outlineMedium
|
||||||
focusedBorderColor: Theme.primary
|
focusedBorderColor: Theme.primary
|
||||||
leftIconName: "search"
|
leftIconName: searchMode === "files" ? "folder" : "search"
|
||||||
leftIconSize: Theme.iconSize
|
leftIconSize: Theme.iconSize
|
||||||
leftIconColor: Theme.surfaceVariantText
|
leftIconColor: Theme.surfaceVariantText
|
||||||
leftIconFocusedColor: Theme.primary
|
leftIconFocusedColor: Theme.primary
|
||||||
@@ -126,14 +201,18 @@ Item {
|
|||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
enabled: parentModal ? parentModal.spotlightOpen : true
|
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||||
placeholderText: ""
|
placeholderText: searchMode === "files" ? "Search files..." : "Search apps..."
|
||||||
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
|
ignoreLeftRightKeys: searchMode === "files" || appLauncher.viewMode !== "list"
|
||||||
ignoreTabKeys: true
|
ignoreTabKeys: true
|
||||||
keyForwardTargets: [spotlightKeyHandler]
|
keyForwardTargets: [spotlightKeyHandler]
|
||||||
text: appLauncher.searchQuery
|
onTextChanged: {
|
||||||
onTextEdited: () => {
|
if (searchMode === "apps") {
|
||||||
appLauncher.searchQuery = text
|
appLauncher.searchQuery = text
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
onTextEdited: {
|
||||||
|
updateSearchMode()
|
||||||
|
}
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
if (parentModal)
|
if (parentModal)
|
||||||
@@ -141,12 +220,18 @@ Item {
|
|||||||
|
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
if (searchMode === "apps") {
|
||||||
appLauncher.launchSelected()
|
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
||||||
else if (appLauncher.model.count > 0)
|
appLauncher.launchSelected()
|
||||||
appLauncher.launchApp(appLauncher.model.get(0))
|
else if (appLauncher.model.count > 0)
|
||||||
|
appLauncher.launchApp(appLauncher.model.get(0))
|
||||||
|
} else if (searchMode === "files") {
|
||||||
|
if (fileSearchController.model.count > 0)
|
||||||
|
fileSearchController.openSelected()
|
||||||
|
}
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key
|
||||||
|
=== Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||||
event.accepted = false
|
event.accepted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +239,7 @@ Item {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
visible: appLauncher.model.count > 0
|
visible: searchMode === "apps" && appLauncher.model.count > 0
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -207,12 +292,116 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: searchMode === "files"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: filenameFilterButton
|
||||||
|
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "title"
|
||||||
|
size: 18
|
||||||
|
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: filenameFilterArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
fileSearchController.searchField = "filename"
|
||||||
|
}
|
||||||
|
onEntered: {
|
||||||
|
filenameTooltipLoader.active = true
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (filenameTooltipLoader.item) {
|
||||||
|
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||||
|
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
if (filenameTooltipLoader.item)
|
||||||
|
filenameTooltipLoader.item.hide()
|
||||||
|
|
||||||
|
filenameTooltipLoader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: contentFilterButton
|
||||||
|
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "description"
|
||||||
|
size: 18
|
||||||
|
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: contentFilterArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: () => {
|
||||||
|
fileSearchController.searchField = "body"
|
||||||
|
}
|
||||||
|
onEntered: {
|
||||||
|
contentTooltipLoader.active = true
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (contentTooltipLoader.item) {
|
||||||
|
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||||
|
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
if (contentTooltipLoader.item)
|
||||||
|
contentTooltipLoader.item.hide()
|
||||||
|
|
||||||
|
contentTooltipLoader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpotlightResults {
|
Item {
|
||||||
id: resultsView
|
width: parent.width
|
||||||
appLauncher: spotlightKeyHandler.appLauncher
|
height: parent.height - y
|
||||||
contextMenu: contextMenu
|
|
||||||
|
SpotlightResults {
|
||||||
|
id: resultsView
|
||||||
|
anchors.fill: parent
|
||||||
|
appLauncher: spotlightKeyHandler.appLauncher
|
||||||
|
contextMenu: contextMenu
|
||||||
|
visible: searchMode === "apps"
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSearchResults {
|
||||||
|
id: fileSearchResults
|
||||||
|
anchors.fill: parent
|
||||||
|
fileSearchController: spotlightKeyHandler.fileSearchController
|
||||||
|
visible: searchMode === "files"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +422,6 @@ Item {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
||||||
// Prevent closing when clicking on the menu itself
|
|
||||||
x: contextMenu.x
|
x: contextMenu.x
|
||||||
y: contextMenu.y
|
y: contextMenu.y
|
||||||
width: contextMenu.width
|
width: contextMenu.width
|
||||||
@@ -241,4 +429,18 @@ Item {
|
|||||||
onClicked: () => {}
|
onClicked: () => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: filenameTooltipLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
sourceComponent: DankTooltip {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentTooltipLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
sourceComponent: DankTooltip {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ DankModal {
|
|||||||
open()
|
open()
|
||||||
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
if (spotlightContent && spotlightContent.searchField) {
|
if (spotlightContent && spotlightContent.searchField) {
|
||||||
spotlightContent.searchField.forceActiveFocus()
|
spotlightContent.searchField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWithQuery(query) {
|
function showWithQuery(query) {
|
||||||
@@ -40,10 +40,10 @@ DankModal {
|
|||||||
open()
|
open()
|
||||||
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
if (spotlightContent && spotlightContent.searchField) {
|
if (spotlightContent && spotlightContent.searchField) {
|
||||||
spotlightContent.searchField.forceActiveFocus()
|
spotlightContent.searchField.forceActiveFocus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -58,6 +58,9 @@ DankModal {
|
|||||||
spotlightContent.appLauncher.selectedIndex = 0
|
spotlightContent.appLauncher.selectedIndex = 0
|
||||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
|
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
|
||||||
}
|
}
|
||||||
|
if (spotlightContent.fileSearchController) {
|
||||||
|
spotlightContent.fileSearchController.reset()
|
||||||
|
}
|
||||||
if (spotlightContent.resetScroll) {
|
if (spotlightContent.resetScroll) {
|
||||||
spotlightContent.resetScroll()
|
spotlightContent.resetScroll()
|
||||||
}
|
}
|
||||||
@@ -111,17 +114,17 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(): string {
|
function open(): string {
|
||||||
spotlightModal.show()
|
spotlightModal.show()
|
||||||
return "SPOTLIGHT_OPEN_SUCCESS"
|
return "SPOTLIGHT_OPEN_SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
spotlightModal.hide()
|
spotlightModal.hide()
|
||||||
return "SPOTLIGHT_CLOSE_SUCCESS"
|
return "SPOTLIGHT_CLOSE_SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
spotlightModal.toggle()
|
spotlightModal.toggle()
|
||||||
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
return "SPOTLIGHT_TOGGLE_SUCCESS"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ import qs.Widgets
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: resultsContainer
|
id: resultsContainer
|
||||||
|
|
||||||
// DEVELOPER NOTE: This component renders the Spotlight launcher (accessed via Mod+Space).
|
|
||||||
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
|
|
||||||
// likely require corresponding updates in Modules/AppDrawer/AppLauncher.qml and vice versa.
|
|
||||||
|
|
||||||
property var appLauncher: null
|
property var appLauncher: null
|
||||||
property var contextMenu: null
|
property var contextMenu: null
|
||||||
|
|
||||||
@@ -19,8 +15,6 @@ Rectangle {
|
|||||||
resultsGrid.contentY = 0
|
resultsGrid.contentY = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - y
|
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
clip: true
|
clip: true
|
||||||
|
|||||||
311
Services/DSearchService.qml
Normal file
311
Services/DSearchService.qml
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
pragma ComponentBehavior
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool dsearchAvailable: false
|
||||||
|
property bool isConnected: false
|
||||||
|
property bool isConnecting: false
|
||||||
|
property int apiVersion: 0
|
||||||
|
readonly property int expectedApiVersion: 1
|
||||||
|
property bool handshakeReceived: false
|
||||||
|
|
||||||
|
property var pendingRequests: ({})
|
||||||
|
property int requestIdCounter: 0
|
||||||
|
|
||||||
|
signal connectionStateChanged
|
||||||
|
signal searchResultsReceived(var results)
|
||||||
|
signal statsReceived(var stats)
|
||||||
|
signal errorOccurred(string error)
|
||||||
|
signal apiVersionReceived(int version)
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
discoverAndConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
function discoverAndConnect() {
|
||||||
|
if (requestSocket.connected) {
|
||||||
|
requestSocket.connected = false
|
||||||
|
Qt.callLater(() => {
|
||||||
|
performDiscovery()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
performDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
function performDiscovery() {
|
||||||
|
dsearchAvailable = false
|
||||||
|
isConnecting = false
|
||||||
|
isConnected = false
|
||||||
|
|
||||||
|
requestSocket.connected = false
|
||||||
|
requestSocket.path = ""
|
||||||
|
|
||||||
|
const xdgRuntimeDir = Quickshell.env("XDG_RUNTIME_DIR")
|
||||||
|
if (xdgRuntimeDir && xdgRuntimeDir.length > 0) {
|
||||||
|
findSocketProcess.searchPath = xdgRuntimeDir
|
||||||
|
findSocketProcess.running = true
|
||||||
|
} else {
|
||||||
|
findSocketProcess.searchPath = "/tmp"
|
||||||
|
findSocketProcess.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: findSocketProcess
|
||||||
|
property string searchPath: ""
|
||||||
|
|
||||||
|
command: ["find", searchPath, "-maxdepth", "1", "-type", "s", "-name", "danksearch-*.sock"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const lines = text.trim().split('\n').filter(line => line.length > 0)
|
||||||
|
|
||||||
|
if (lines.length > 0) {
|
||||||
|
const socketPath = lines[0]
|
||||||
|
testSocketProcess.socketPath = socketPath
|
||||||
|
testSocketProcess.running = true
|
||||||
|
} else {
|
||||||
|
if (findSocketProcess.searchPath !== "/tmp") {
|
||||||
|
findSocketProcess.searchPath = "/tmp"
|
||||||
|
findSocketProcess.running = true
|
||||||
|
} else {
|
||||||
|
root.dsearchAvailable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.dsearchAvailable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: testSocketProcess
|
||||||
|
property string socketPath: ""
|
||||||
|
|
||||||
|
command: ["test", "-S", socketPath]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.dsearchAvailable = true
|
||||||
|
requestSocket.path = socketPath
|
||||||
|
connectSocket()
|
||||||
|
} else {
|
||||||
|
root.dsearchAvailable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectSocket() {
|
||||||
|
if (!dsearchAvailable || isConnected || isConnecting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestSocket.path || requestSocket.path.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnecting = true
|
||||||
|
handshakeReceived = false
|
||||||
|
requestSocket.connected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSocket {
|
||||||
|
id: requestSocket
|
||||||
|
path: ""
|
||||||
|
connected: false
|
||||||
|
|
||||||
|
onConnectionStateChanged: {
|
||||||
|
if (!connected) {
|
||||||
|
root.isConnected = false
|
||||||
|
root.isConnecting = false
|
||||||
|
root.apiVersion = 0
|
||||||
|
root.handshakeReceived = false
|
||||||
|
root.dsearchAvailable = false
|
||||||
|
root.pendingRequests = {}
|
||||||
|
|
||||||
|
requestSocket.connected = false
|
||||||
|
requestSocket.path = ""
|
||||||
|
|
||||||
|
root.connectionStateChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser: SplitParser {
|
||||||
|
onRead: line => {
|
||||||
|
if (!line || line.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(line)
|
||||||
|
|
||||||
|
if (!root.handshakeReceived && message.apiVersion !== undefined) {
|
||||||
|
handleHandshake(message)
|
||||||
|
} else {
|
||||||
|
handleResponse(message)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("DSearchService: Failed to parse message:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHandshake(message) {
|
||||||
|
handshakeReceived = true
|
||||||
|
apiVersion = message.apiVersion || 0
|
||||||
|
|
||||||
|
isConnected = true
|
||||||
|
isConnecting = false
|
||||||
|
connectionStateChanged()
|
||||||
|
apiVersionReceived(apiVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRequest(method, params, callback) {
|
||||||
|
if (!isConnected) {
|
||||||
|
if (callback) {
|
||||||
|
callback({
|
||||||
|
"error": "not connected to dsearch socket"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestIdCounter++
|
||||||
|
const id = Date.now() + requestIdCounter
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
"id": id,
|
||||||
|
"method": method
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params && Object.keys(params).length > 0) {
|
||||||
|
request.params = params
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
pendingRequests[id] = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
requestSocket.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResponse(response) {
|
||||||
|
const callback = pendingRequests[response.id]
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
delete pendingRequests[response.id]
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
errorOccurred(response.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ping(callback) {
|
||||||
|
sendRequest("ping", null, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(query, params, callback) {
|
||||||
|
if (!query || query.length === 0) {
|
||||||
|
if (callback) {
|
||||||
|
callback({
|
||||||
|
"error": "query is required"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
discoverAndConnect()
|
||||||
|
if (callback) {
|
||||||
|
callback({
|
||||||
|
"error": "not connected - attempting reconnection"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
"query": query
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
for (const key in params) {
|
||||||
|
searchParams[key] = params[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest("search", searchParams, response => {
|
||||||
|
if (response.result) {
|
||||||
|
searchResultsReceived(response.result)
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStats(callback) {
|
||||||
|
sendRequest("stats", null, response => {
|
||||||
|
if (response.result) {
|
||||||
|
statsReceived(response.result)
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sync(callback) {
|
||||||
|
sendRequest("sync", null, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function reindex(callback) {
|
||||||
|
sendRequest("reindex", null, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchStart(callback) {
|
||||||
|
sendRequest("watch.start", null, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchStop(callback) {
|
||||||
|
sendRequest("watch.stop", null, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchStatus(callback) {
|
||||||
|
sendRequest("watch.status", null, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rediscover() {
|
||||||
|
if (isConnected) {
|
||||||
|
requestSocket.connected = false
|
||||||
|
}
|
||||||
|
discoverAndConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (isConnected || isConnecting) {
|
||||||
|
requestSocket.connected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Widgets/DankNFIcon.qml
Normal file
37
Widgets/DankNFIcon.qml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string name: ""
|
||||||
|
property alias size: icon.font.pixelSize
|
||||||
|
property alias color: icon.color
|
||||||
|
|
||||||
|
implicitWidth: icon.implicitWidth
|
||||||
|
implicitHeight: icon.implicitHeight
|
||||||
|
|
||||||
|
readonly property var iconMap: ({
|
||||||
|
"docker": "\uf21f"
|
||||||
|
})
|
||||||
|
|
||||||
|
FontLoader {
|
||||||
|
id: firaCodeFont
|
||||||
|
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: icon
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
font.family: firaCodeFont.name
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
text: root.iconMap[root.name] || ""
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
antialiasing: true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user