Графический браузер базы данных (DatabaseBrowser)
- Диалог: готовые выборки (файлы/теги/свойства/связи), свой SQL, результат таблицей, пути кликабельны, ошибки красным - Ctrl+Enter для выполнения, автопересчёт при изменении базы - Меню Файл -> База данных (Ctrl+D)
This commit is contained in:
@@ -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 ниже).
|
||||
|
||||
@@ -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, ">").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 = "<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">"
|
||||
h += "<tr>"
|
||||
for (let c = 0; c < cols.length; ++c)
|
||||
h += "<th align=\"left\">" + esc(cols[c]) + "</th>"
|
||||
h += "</tr>"
|
||||
for (let r = 0; r < rows.length; ++r) {
|
||||
h += "<tr>"
|
||||
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 += "<td><a href=\"flora://open/" + encodeURIComponent(cell) + "\">"
|
||||
+ esc(cell) + "</a></td>"
|
||||
} else {
|
||||
h += "<td>" + esc(cell) + "</td>"
|
||||
}
|
||||
}
|
||||
h += "</tr>"
|
||||
}
|
||||
h += "</table>"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user