Графический браузер базы данных (DatabaseBrowser)
- Диалог: готовые выборки (файлы/теги/свойства/связи), свой SQL, результат таблицей, пути кликабельны, ошибки красным - Ctrl+Enter для выполнения, автопересчёт при изменении базы - Меню Файл -> База данных (Ctrl+D)
This commit is contained in:
@@ -140,6 +140,7 @@ qt_add_qml_module(flora
|
|||||||
qml/dialogs/SettingsDialog.qml
|
qml/dialogs/SettingsDialog.qml
|
||||||
qml/dialogs/VaultSettingsDialog.qml
|
qml/dialogs/VaultSettingsDialog.qml
|
||||||
qml/dialogs/FilePropertiesDialog.qml
|
qml/dialogs/FilePropertiesDialog.qml
|
||||||
|
qml/dialogs/DatabaseBrowser.qml
|
||||||
)
|
)
|
||||||
|
|
||||||
# Theme как синглтон
|
# Theme как синглтон
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ ApplicationWindow {
|
|||||||
enabled: fileStore.ready
|
enabled: fileStore.ready
|
||||||
onTriggered: vaultSettingsDialog.open()
|
onTriggered: vaultSettingsDialog.open()
|
||||||
}
|
}
|
||||||
|
Action {
|
||||||
|
text: qsTr("База данных…")
|
||||||
|
shortcut: "Ctrl+D"
|
||||||
|
enabled: fileStore.ready
|
||||||
|
onTriggered: dbBrowser.open()
|
||||||
|
}
|
||||||
Action { text: qsTr("Выход"); onTriggered: window.close() }
|
Action { text: qsTr("Выход"); onTriggered: window.close() }
|
||||||
}
|
}
|
||||||
Menu {
|
Menu {
|
||||||
@@ -208,6 +214,10 @@ ApplicationWindow {
|
|||||||
id: filePropsDialog
|
id: filePropsDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DatabaseBrowser {
|
||||||
|
id: dbBrowser
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback для пути визарда: хранилище открыли прямо в этом окне
|
// Fallback для пути визарда: хранилище открыли прямо в этом окне
|
||||||
// (не через WindowController) — открываем настройки и базу здесь.
|
// (не через WindowController) — открываем настройки и базу здесь.
|
||||||
// Обычный путь уже подключён в C++, повторный open() безвреден (guard ниже).
|
// Обычный путь уже подключён в 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