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 += "" 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 += "" } else { h += "" } } h += "" } h += "
" + esc(cols[c]) + "
" + esc(cell) + "" + esc(cell) + "
" 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) } } } }