Files
Flora/qml/panels/RightPanel.qml
T
grendervill fdf59f5b39 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 зелёных)
2026-09-07 16:30:06 +03:00

335 lines
15 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Flora
Item {
id: root
function activePath() { return documentManager.pathAt(documentManager.activeIndex) }
function refreshLinksTags() {
const p = root.activePath()
linksModel.clear()
if (p.length > 0) {
const links = vaultIndex.linksOfFile(p)
for (let i = 0; i < links.length; ++i) linksModel.append({ text: links[i].text, path: links[i].path })
}
tagsModel.clear()
const tags = p.length > 0 ? vaultIndex.tagsOfFile(p) : []
for (let i = 0; i < tags.length; ++i) tagsModel.append({ tag: tags[i] })
}
Component.onCompleted: { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
Connections {
target: fileStore
function onVaultChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
function onReadyChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
// Внешнее изменение (синхронизация/другая программа): досинхронизировать индекс
// по mtime — дёшево, без полного рескана
function onExternalChanged() { if (fileStore.ready) vaultIndex.syncIncremental() }
}
Connections {
target: documentManager
function onActiveIndexChanged() { root.refreshLinksTags() }
function onOpenTabsChanged() { root.refreshLinksTags() }
function onContentChanged(i) { if (i === documentManager.activeIndex) { root.refreshLinksTags(); indexDebounce.restart() } }
// Сохранение обрабатывает C++ (WindowController): сначала индекс, потом база.
// Здесь не дублируем, иначе двойной парс файла.
}
Connections { target: vaultIndex; function onIndexChanged() { root.refreshLinksTags() } }
// Правка активного файла: обновить только его (один парс), а не всё хранилище
Timer {
id: indexDebounce
interval: 800
onTriggered: {
if (fileStore.ready && documentManager.activeIndex >= 0) {
const p = documentManager.pathAt(documentManager.activeIndex)
if (p && p.length > 0) vaultIndex.updateFile(p)
}
}
}
// Делаем панели ресайзабельными внутри правой колонки — увеличенная зона по всей ширине
SplitView {
anchors.fill: parent
orientation: Qt.Vertical
handle: Rectangle {
implicitHeight: 12
color: "transparent"
Rectangle {
anchors.centerIn: parent
height: 1; width: parent.width
color: Theme.divider
}
Rectangle {
anchors.fill: parent
color: "transparent"
MouseArea {
anchors.fill: parent
cursorShape: Qt.SplitVCursor
hoverEnabled: true
onPressed: mouse.accepted = false
}
}
}
// Календарь
Rectangle {
id: calBox
SplitView.preferredHeight: 240
SplitView.minimumHeight: 180
SplitView.maximumHeight: 360
color: Theme.bgAlt
border.color: Theme.divider
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
height: 28
color: "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8; anchors.rightMargin: 4
Label { text: qsTr("Календарь"); font.bold: true; color: Theme.textSecondary; font.pixelSize: 12 }
Item { Layout.fillWidth: true }
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
MonthCalendar {
id: cal
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 4
onDaySelected: (ds) => {
const path = fileStore.dailyNotePath(ds)
if (path.length > 0) documentManager.openDocument(path)
}
}
}
}
// Ссылки и теги
Rectangle {
id: linksBox
SplitView.preferredHeight: 260
SplitView.minimumHeight: 120
color: Theme.bg
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: parent.width - 12
x: 6
y: 6
spacing: 8
RowLayout {
Layout.fillWidth: true
Label { text: qsTr("Ссылки и теги"); font.bold: true; color: Theme.textSecondary; font.pixelSize: 12 }
Item { Layout.fillWidth: true }
Label {
text: vaultIndex.fileCount + " " + qsTr("файлов")
color: Theme.textFaint
font.pixelSize: 10
}
}
Label {
text: root.activePath().length > 0 ? qsTr("Файл: %1").arg(root.baseName(activePath())) : qsTr("Нет активного файла")
elide: Text.ElideMiddle
color: Theme.textMuted
Layout.fillWidth: true
font.pixelSize: 11
}
// Ссылки
Rectangle {
Layout.fillWidth: true
color: Theme.bgAlt
radius: 8
border.color: Theme.border
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
Label { text: qsTr("Ссылки"); color: Theme.textSecondary; font.pixelSize: 11; font.bold: true }
Repeater {
model: ListModel { id: linksModel }
// placeholder, используем ListView внутри
}
ListView {
id: linksList
Layout.fillWidth: true
Layout.preferredHeight: Math.min(140, contentHeight)
model: linksModel
clip: true
delegate: ItemDelegate {
width: ListView.view.width
height: 28
text: model.text
font.pixelSize: 12
palette.text: Theme.text
background: Rectangle { color: hovered ? Theme.surfaceHover : "transparent"; radius: 6 }
onClicked: documentManager.openDocument(model.path)
}
ScrollBar.vertical: ScrollBar {}
}
Label {
visible: linksModel.count === 0
text: qsTr("— нет ссылок —")
color: Theme.textFaint
font.pixelSize: 11
font.italic: true
}
}
}
// Теги
Rectangle {
Layout.fillWidth: true
color: Theme.bgAlt
radius: 8
border.color: Theme.border
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 6
Label { text: qsTr("Теги"); color: Theme.textSecondary; font.pixelSize: 11; font.bold: true }
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: tagsModel
delegate: TagChip {
text: "#" + model.tag
onClicked: {
tagFilesModel.clear()
const files = vaultIndex.filesWithTag(model.tag)
for (let i=0;i<files.length;++i) tagFilesModel.append({ path: files[i], name: baseName(files[i]) })
tagFilesTitle.text = qsTr("Файлы с тегом #%1 (%2)").arg(model.tag).arg(files.length)
tagFilesPopup.open()
}
}
}
Label {
visible: tagsModel.count === 0
text: qsTr("— нет тегов —")
color: Theme.textFaint
font.pixelSize: 11
font.italic: true
}
}
}
}
Item { Layout.fillHeight: true }
}
}
}
// Граф
Rectangle {
id: graphBox
SplitView.fillHeight: true
SplitView.minimumHeight: 160
color: Theme.graphBg
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
height: 32
color: "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8; anchors.rightMargin: 4
Label { text: qsTr("Граф связей"); font.bold: true; color: Theme.textSecondary; font.pixelSize: 12 }
Item { Layout.fillWidth: true }
Label { text: vaultIndex.nodes.length + "·" + vaultIndex.edges.length; color: Theme.textFaint; font.pixelSize: 10 }
ToolButton {
text: "\u21BB"
font.pixelSize: 12
ToolTip.text: qsTr("Обновить")
ToolTip.visible: hovered
onClicked: vaultIndex.rebuild(fileStore.vault)
}
ToolButton {
text: "\u2630"
ToolTip.text: qsTr("Справка")
ToolTip.visible: hovered
onClicked: graphHelp.open()
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
GraphView {
id: graph
Layout.fillWidth: true
Layout.fillHeight: true
nodes: vaultIndex.nodes
edges: vaultIndex.edges
onFileClicked: (path) => documentManager.openDocument(path)
}
// легенда
Row {
Layout.fillWidth: true
Layout.margins: 6
spacing: 12
Row { spacing: 4; Rectangle { width: 10; height: 10; radius: 5; color: Theme.graphNodeFile; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("Файл"); color: Theme.textMuted; font.pixelSize: 10 } }
Row { spacing: 4; Rectangle { width: 10; height: 10; radius: 5; color: Theme.graphNodeTag; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("Тег"); color: Theme.textMuted; font.pixelSize: 10 } }
Label { text: qsTr("Колесо — зум, перетаскивание — перемещение"); color: Theme.textFaint; font.pixelSize: 9; elide: Text.ElideRight; Layout.fillWidth: true; horizontalAlignment: Text.AlignRight }
}
}
}
}
function baseName(p) { const idx = p.lastIndexOf("/"); return idx >= 0 ? p.substring(idx+1) : p }
Popup {
id: tagFilesPopup
modal: true
anchors.centerIn: parent
width: 360
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
ColumnLayout {
width: parent.width - 16
x: 8; y: 8
Label { id: tagFilesTitle; font.bold: true; color: Theme.text }
ScrollView {
Layout.preferredHeight: Math.min(260, contentHeight)
Layout.fillWidth: true
clip: true
ListView {
model: ListModel { id: tagFilesModel }
delegate: ItemDelegate {
width: ListView.view.width
text: model.name
onClicked: { documentManager.openDocument(model.path); tagFilesPopup.close() }
}
}
}
Button { text: qsTr("Закрыть"); Layout.alignment: Qt.AlignRight; onClicked: tagFilesPopup.close() }
}
}
Popup {
id: graphHelp
anchors.centerIn: parent
width: 320
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
ColumnLayout {
width: parent.width - 16
x: 8; y: 8
spacing: 8
Label { text: qsTr("Граф связей"); font.bold: true; color: Theme.text }
Label { text: qsTr("• Синие узлы — файлы, зелёные — теги\n• Клик по файлу — открыть\n• Перетаскивание — переместить узел\n• Колесо — зум, перетаскивание фона — панорама\n• Двойной клик — сброс"); color: Theme.textSecondary; wrapMode: Text.Wrap; Layout.fillWidth: true; font.pixelSize: 11 }
Button { text: qsTr("Понятно"); Layout.alignment: Qt.AlignRight; onClicked: graphHelp.close() }
}
}
// скрытая модель тегов (для Flow)
ListModel { id: tagsModel }
}