From 8e8cf3ce70f302fbcbfe806f483bcae83b8d8387 Mon Sep 17 00:00:00 2001 From: grendervill Date: Mon, 7 Sep 2026 16:39:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D1=80=D0=B0=D1=84=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9=20=D0=B1=D1=80=D0=B0=D1=83=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20(DatabaseBrowser)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Диалог: готовые выборки (файлы/теги/свойства/связи), свой SQL, результат таблицей, пути кликабельны, ошибки красным - Ctrl+Enter для выполнения, автопересчёт при изменении базы - Меню Файл -> База данных (Ctrl+D) --- CMakeLists.txt | 1 + qml/MainWindow.qml | 10 ++ qml/dialogs/DatabaseBrowser.qml | 187 ++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 qml/dialogs/DatabaseBrowser.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 35ea625..ff4c244 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,7 @@ qt_add_qml_module(flora qml/dialogs/SettingsDialog.qml qml/dialogs/VaultSettingsDialog.qml qml/dialogs/FilePropertiesDialog.qml + qml/dialogs/DatabaseBrowser.qml ) # Theme как синглтон diff --git a/qml/MainWindow.qml b/qml/MainWindow.qml index dea8e95..a87ea6e 100644 --- a/qml/MainWindow.qml +++ b/qml/MainWindow.qml @@ -92,6 +92,12 @@ ApplicationWindow { enabled: fileStore.ready onTriggered: vaultSettingsDialog.open() } + Action { + text: qsTr("База данных…") + shortcut: "Ctrl+D" + enabled: fileStore.ready + onTriggered: dbBrowser.open() + } Action { text: qsTr("Выход"); onTriggered: window.close() } } Menu { @@ -208,6 +214,10 @@ ApplicationWindow { id: filePropsDialog } + DatabaseBrowser { + id: dbBrowser + } + // Fallback для пути визарда: хранилище открыли прямо в этом окне // (не через WindowController) — открываем настройки и базу здесь. // Обычный путь уже подключён в C++, повторный open() безвреден (guard ниже). diff --git a/qml/dialogs/DatabaseBrowser.qml b/qml/dialogs/DatabaseBrowser.qml new file mode 100644 index 0000000..759641b --- /dev/null +++ b/qml/dialogs/DatabaseBrowser.qml @@ -0,0 +1,187 @@ +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) + } + } + } +}