mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-29 07:52:50 -05:00
settings: complete re-organize and breakout
This commit is contained in:
309
Widgets/DankLocationSearch.qml
Normal file
309
Widgets/DankLocationSearch.qml
Normal file
@@ -0,0 +1,309 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string currentLocation: ""
|
||||
property string placeholderText: "Search for a location..."
|
||||
|
||||
signal locationSelected(string displayName, string coordinates)
|
||||
|
||||
width: parent.width
|
||||
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
|
||||
|
||||
property bool _internalChange: false
|
||||
property bool isLoading: false
|
||||
property string helperTextState: "default" // "default", "prompt", "searching", "found", "not_found"
|
||||
property string currentSearchText: ""
|
||||
|
||||
ListModel {
|
||||
id: searchResultsModel
|
||||
}
|
||||
|
||||
function resetSearchState() {
|
||||
locationSearchTimer.stop()
|
||||
dropdownHideTimer.stop()
|
||||
if (locationSearcher.running) {
|
||||
locationSearcher.running = false;
|
||||
}
|
||||
isLoading = false
|
||||
searchResultsModel.clear()
|
||||
helperTextState = "default"
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: locationSearchTimer
|
||||
interval: 500
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (locationInput.text.length > 2) {
|
||||
if (locationSearcher.running) {
|
||||
locationSearcher.running = false
|
||||
}
|
||||
|
||||
searchResultsModel.clear()
|
||||
root.isLoading = true
|
||||
root.helperTextState = "searching"
|
||||
|
||||
const searchLocation = locationInput.text
|
||||
root.currentSearchText = searchLocation
|
||||
const encodedLocation = encodeURIComponent(searchLocation)
|
||||
const curlCommand = `curl -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'`
|
||||
|
||||
locationSearcher.command = ["bash", "-c", curlCommand]
|
||||
locationSearcher.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dropdownHideTimer
|
||||
interval: 200
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!locationInput.getActiveFocus() && !searchDropdown.hovered) {
|
||||
root.resetSearchState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: locationSearcher
|
||||
command: ["bash", "-c", "echo"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (root.currentSearchText !== locationInput.text) {
|
||||
return
|
||||
}
|
||||
|
||||
const raw = text.trim()
|
||||
root.isLoading = false
|
||||
searchResultsModel.clear()
|
||||
|
||||
if (!raw || raw[0] !== "[") {
|
||||
root.helperTextState = "not_found"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
if (data.length === 0) {
|
||||
root.helperTextState = "not_found"
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < Math.min(data.length, 5); i++) {
|
||||
const location = data[i]
|
||||
if (location.display_name && location.lat && location.lon) {
|
||||
const parts = location.display_name.split(', ')
|
||||
let cleanName = parts[0]
|
||||
if (parts.length > 1) {
|
||||
const state = parts[parts.length - 2]
|
||||
if (state && state !== cleanName) {
|
||||
cleanName += `, ${state}`
|
||||
}
|
||||
}
|
||||
const query = `${location.lat},${location.lon}`
|
||||
searchResultsModel.append({ "name": cleanName, "query": query })
|
||||
}
|
||||
}
|
||||
root.helperTextState = "found"
|
||||
} catch (e) {
|
||||
root.helperTextState = "not_found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
root.isLoading = false
|
||||
if (exitCode !== 0) {
|
||||
searchResultsModel.clear()
|
||||
root.helperTextState = "not_found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search input field
|
||||
Item {
|
||||
id: searchInputField
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
DankTextField {
|
||||
id: locationInput
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
leftIconName: "search"
|
||||
placeholderText: root.placeholderText
|
||||
text: root.currentLocation
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
onTextEdited: {
|
||||
if (root._internalChange) return
|
||||
if (getActiveFocus()) {
|
||||
if (text.length > 2) {
|
||||
root.isLoading = true
|
||||
root.helperTextState = "searching"
|
||||
locationSearchTimer.restart()
|
||||
} else {
|
||||
root.resetSearchState()
|
||||
root.helperTextState = "prompt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onActiveFocusChanged: (hasFocus) => {
|
||||
if (hasFocus) {
|
||||
dropdownHideTimer.stop()
|
||||
if (text.length <= 2) {
|
||||
root.helperTextState = "prompt"
|
||||
}
|
||||
}
|
||||
else {
|
||||
dropdownHideTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status icon overlay
|
||||
DankIcon {
|
||||
name: {
|
||||
if (root.isLoading) return "hourglass_empty"
|
||||
if (searchResultsModel.count > 0) return "check_circle"
|
||||
if (locationInput.getActiveFocus() && locationInput.text.length > 2 && !root.isLoading) return "error"
|
||||
return ""
|
||||
}
|
||||
size: Theme.iconSize - 4
|
||||
color: {
|
||||
if (root.isLoading) return Theme.surfaceVariantText
|
||||
if (searchResultsModel.count > 0) return Theme.success || Theme.primary
|
||||
if (locationInput.getActiveFocus() && locationInput.text.length > 2) return Theme.error
|
||||
return "transparent"
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search results dropdown
|
||||
Rectangle {
|
||||
id: searchDropdown
|
||||
width: parent.width
|
||||
height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
|
||||
|
||||
y: searchInputField.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: 1
|
||||
visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading)
|
||||
|
||||
property bool hovered: false
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
parent.hovered = true
|
||||
dropdownHideTimer.stop()
|
||||
}
|
||||
onExited: {
|
||||
parent.hovered = false
|
||||
if (!locationInput.getActiveFocus()) {
|
||||
dropdownHideTimer.start()
|
||||
}
|
||||
}
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
ListView {
|
||||
id: searchResultsList
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
model: searchResultsModel
|
||||
spacing: 2
|
||||
|
||||
delegate: Rectangle {
|
||||
width: searchResultsList.width
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: resultMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "place"
|
||||
size: Theme.iconSize - 6
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.name || "Unknown"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 30
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resultMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
root._internalChange = true
|
||||
const selectedName = model.name
|
||||
const selectedQuery = model.query
|
||||
|
||||
locationInput.text = selectedName
|
||||
root.locationSelected(selectedName, selectedQuery)
|
||||
|
||||
root.resetSearchState()
|
||||
locationInput.setFocus(false)
|
||||
root._internalChange = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show message when no results
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.isLoading ? "Searching..." : "No locations found"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: searchResultsList.count === 0 && locationInput.text.length > 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user