SQLite-база хранилища: свойства, ~sql-блоки, настройки хранилища
- Frontmatter: шапка --- ключ: значение (источник правды свойств) - VaultSettings: .flora/settings.json (журнал, sqlEnabled, sqlMaxRows) - Database: files/tags/file_tags/links/properties, только SELECT/WITH на read-only соединении (mode=ro + query_only + зонд), лимит строк - MdParser: ~sql-блоки таблицей (parseBlocks + Renderer), шапка скрыта в просмотре - Окно: per-window Database/VaultSettings, sync при сохранении, дебаунс reindex - QML: VaultSettingsDialog, FilePropertiesDialog, меню, автообновление ~sql - FileStore.journalDir, DocumentManager.reloadDocument - Тест flora_db_test (23/23 зелёных)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Flora
|
||||
|
||||
// Свойства заметки (Notion-поля): источник правды — frontmatter в самом .md,
|
||||
// база обновляется следом. Грязную вкладку правим только после сохранения.
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
property string filePath: ""
|
||||
|
||||
title: qsTr("Свойства заметки")
|
||||
modal: true
|
||||
width: 520
|
||||
height: 560
|
||||
anchors.centerIn: parent
|
||||
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
|
||||
standardButtons: Dialog.NoButton
|
||||
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
|
||||
|
||||
footer: RowLayout {
|
||||
spacing: 8
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: qsTr("Отмена")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Сохранить")
|
||||
highlighted: true
|
||||
onClicked: root.saveProps()
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
statusLabel.text = ""
|
||||
newKeyField.text = ""
|
||||
newValueField.text = ""
|
||||
propsModel.clear()
|
||||
const props = vaultDatabase.fileProperties(root.filePath)
|
||||
const keys = Object.keys(props).sort()
|
||||
for (let i = 0; i < keys.length; ++i)
|
||||
propsModel.append({ key: keys[i], value: props[keys[i]] })
|
||||
}
|
||||
|
||||
function saveProps() {
|
||||
const map = {}
|
||||
for (let i = 0; i < propsModel.count; ++i) {
|
||||
const row = propsModel.get(i)
|
||||
map[row.key] = row.value
|
||||
}
|
||||
const err = vaultDatabase.saveFileProperties(root.filePath, map)
|
||||
if (err.length > 0) {
|
||||
statusLabel.text = err
|
||||
return
|
||||
}
|
||||
// Обновить открытые вкладки с этим файлом (только чистые)
|
||||
let dirty = false
|
||||
for (let t = 0; t < documentManager.openTabs.length; ++t) {
|
||||
if (documentManager.pathAt(t) === root.filePath) {
|
||||
if (documentManager.isDirty(t)) {
|
||||
dirty = true
|
||||
} else {
|
||||
documentManager.reloadDocument(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
vaultIndex.updateFile(root.filePath)
|
||||
vaultDatabase.syncFile(root.filePath)
|
||||
if (dirty) {
|
||||
statusLabel.text = qsTr("Свойства записаны, но вкладка изменена — сохраните её (Ctrl+S) и переоткройте.")
|
||||
return
|
||||
}
|
||||
root.accept()
|
||||
}
|
||||
|
||||
function addProp() {
|
||||
const k = newKeyField.text.trim()
|
||||
const v = newValueField.text
|
||||
if (k.length === 0) {
|
||||
statusLabel.text = qsTr("Укажите имя свойства.")
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < propsModel.count; ++i) {
|
||||
if (propsModel.get(i).key === k) {
|
||||
statusLabel.text = qsTr("Такое свойство уже есть — правьте его в списке.")
|
||||
return
|
||||
}
|
||||
}
|
||||
propsModel.append({ key: k, value: v })
|
||||
newKeyField.text = ""
|
||||
newValueField.text = ""
|
||||
statusLabel.text = ""
|
||||
}
|
||||
|
||||
ListModel { id: propsModel }
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
text: root.filePath
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: propsView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: propsModel
|
||||
clip: true
|
||||
spacing: 6
|
||||
delegate: RowLayout {
|
||||
width: propsView.width
|
||||
spacing: 8
|
||||
Label {
|
||||
text: model.key
|
||||
color: Theme.text
|
||||
font.bold: true
|
||||
Layout.preferredWidth: 140
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
text: model.value
|
||||
placeholderText: qsTr("значение")
|
||||
onTextChanged: propsModel.setProperty(index, "value", text)
|
||||
}
|
||||
Button {
|
||||
text: "✕"
|
||||
onClicked: propsModel.remove(index)
|
||||
}
|
||||
}
|
||||
Label {
|
||||
visible: propsModel.count === 0
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Свойств нет — добавьте первое ниже")
|
||||
color: Theme.textFaint
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
TextField {
|
||||
id: newKeyField
|
||||
Layout.preferredWidth: 150
|
||||
placeholderText: qsTr("имя")
|
||||
onAccepted: root.addProp()
|
||||
}
|
||||
TextField {
|
||||
id: newValueField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("значение")
|
||||
onAccepted: root.addProp()
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Добавить")
|
||||
onClicked: root.addProp()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Имя: буквы, цифры, _ и - (до 64 символов).")
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 11
|
||||
}
|
||||
Label {
|
||||
id: statusLabel
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
color: "#e5484d"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user