mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-08 22:45:38 -05:00
329 lines
11 KiB
QML
329 lines
11 KiB
QML
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..."
|
|
property bool _internalChange: false
|
|
property bool isLoading: false
|
|
property string helperTextState: "default" // "default", "prompt", "searching", "found", "not_found"
|
|
property string currentSearchText: ""
|
|
|
|
signal locationSelected(string displayName, string coordinates)
|
|
|
|
function resetSearchState() {
|
|
locationSearchTimer.stop();
|
|
dropdownHideTimer.stop();
|
|
if (locationSearcher.running)
|
|
locationSearcher.running = false;
|
|
|
|
isLoading = false;
|
|
searchResultsModel.clear();
|
|
helperTextState = "default";
|
|
}
|
|
|
|
width: parent.width
|
|
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
|
|
|
|
ListModel {
|
|
id: searchResultsModel
|
|
}
|
|
|
|
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
|
|
onExited: (exitCode) => {
|
|
root.isLoading = false;
|
|
if (exitCode !== 0) {
|
|
searchResultsModel.clear();
|
|
root.helperTextState = "not_found";
|
|
}
|
|
}
|
|
|
|
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";
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// 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";
|
|
}
|
|
}
|
|
}
|
|
onFocusStateChanged: (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
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Search results dropdown
|
|
Rectangle {
|
|
id: searchDropdown
|
|
|
|
property bool hovered: false
|
|
|
|
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)
|
|
|
|
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
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|