import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Flora
// Графическая работа с базой хранилища: готовые выборки, свой SQL,
// результат таблицей (пути кликабельны — открывают заметку).
Dialog {
id: root
title: qsTr("База данных хранилища")
modal: false
width: 760
height: 620
anchors.centerIn: parent
closePolicy: Dialog.CloseOnEscape
standardButtons: Dialog.Close
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
property string lastError: ""
function esc(s) {
return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """)
}
function openHref(href) {
if (href.startsWith("flora://open/"))
documentManager.openDocument(decodeURIComponent(href.substring(13)))
}
function runQuery() {
const res = vaultDatabase.queryTable(sqlEdit.text)
if (res.error.length > 0) {
root.lastError = res.error
errorLabel.visible = true
resultText.text = ""
countLabel.text = ""
return
}
root.lastError = ""
errorLabel.visible = false
const cols = res.columns
const rows = res.rows
let h = "
"
h += ""
for (let c = 0; c < cols.length; ++c)
h += "| " + esc(cols[c]) + " | "
h += "
"
for (let r = 0; r < rows.length; ++r) {
h += ""
for (let c = 0; c < cols.length; ++c) {
const cell = c < rows[r].length ? String(rows[r][c]) : ""
// значение похоже на путь внутри хранилища — делаем ссылкой
if ((cols[c] === "path" || cell.endsWith(".md"))
&& fileStore.fileExists(cell)) {
h += "| "
+ esc(cell) + " | "
} else {
h += "" + esc(cell) + " | "
}
}
h += "
"
}
h += "
"
resultText.text = h
countLabel.text = rows.length === 1 ? qsTr("1 строка")
: qsTr("%1 строк").arg(rows.length)
+ (res.truncated ? qsTr(" (показаны первые)") : "")
}
onOpened: {
errorLabel.visible = false
if (sqlEdit.text.length === 0)
presetFiles()
else
root.runQuery()
}
function presetFiles() {
sqlEdit.text = "SELECT title, path, size FROM files ORDER BY title;"
root.runQuery()
}
function presetTags() {
sqlEdit.text = "SELECT t.name AS тег, COUNT(*) AS файлов FROM tags t\n"
+ " JOIN file_tags ft ON ft.tag_id = t.id\n"
+ " GROUP BY t.name ORDER BY файлов DESC;"
root.runQuery()
}
function presetProps() {
sqlEdit.text = "SELECT f.title, p.key, p.value FROM properties p\n"
+ " JOIN files f ON f.id = p.file_id\n"
+ " ORDER BY f.title, p.key;"
root.runQuery()
}
function presetLinks() {
sqlEdit.text = "SELECT s.title AS от, d.title AS к FROM links l\n"
+ " JOIN files s ON s.id = l.src_id\n"
+ " JOIN files d ON d.id = l.dst_id\n"
+ " ORDER BY от, к;"
root.runQuery()
}
// База изменилась (сохранение, свойства) — пересчитать открытое
Timer { id: refreshDebounce; interval: 800; onTriggered: root.runQuery() }
Connections {
target: vaultDatabase
function onChanged() { if (root.visible) refreshDebounce.restart() }
}
ColumnLayout {
anchors.fill: parent
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 6
Label { text: qsTr("Выборки:"); color: Theme.textSecondary }
Button { text: qsTr("Файлы"); onClicked: root.presetFiles() }
Button { text: qsTr("Теги"); onClicked: root.presetTags() }
Button { text: qsTr("Свойства"); onClicked: root.presetProps() }
Button { text: qsTr("Связи"); onClicked: root.presetLinks() }
Item { Layout.fillWidth: true }
Label {
id: countLabel
color: Theme.textFaint
font.pixelSize: 11
}
}
ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 130
TextArea {
id: sqlEdit
font.family: "Menlo"
wrapMode: TextEdit.NoWrap
placeholderText: qsTr("SELECT … (только чтение)")
Keys.onPressed: (e) => {
if ((e.modifiers & Qt.ControlModifier) && e.key === Qt.Key_Return) {
root.runQuery()
e.accepted = true
}
}
}
}
RowLayout {
Layout.fillWidth: true
Button {
text: qsTr("Выполнить (Ctrl+Enter)")
highlighted: true
onClicked: root.runQuery()
}
Label {
Layout.fillWidth: true
wrapMode: Text.Wrap
color: Theme.textFaint
font.pixelSize: 11
text: qsTr("Таблицы: files, tags, file_tags, links, properties. Только SELECT / WITH.")
}
}
Label {
id: errorLabel
visible: false
Layout.fillWidth: true
wrapMode: Text.Wrap
color: "#e5484d"
text: root.lastError
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Text {
id: resultText
width: parent.width
wrapMode: Text.Wrap
textFormat: Text.RichText
color: Theme.text
onLinkActivated: (href) => root.openHref(href)
}
}
}
}