2026-08-17 00:05:20 +03:00
|
|
|
|
import QtQuick
|
|
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
|
import QtQuick.Layouts
|
2026-09-01 19:25:07 +03:00
|
|
|
|
import Flora
|
2026-08-17 00:05:20 +03:00
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
|
id: root
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
function activePath() { return documentManager.pathAt(documentManager.activeIndex) }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
|
|
|
|
|
|
function refreshLinksTags() {
|
|
|
|
|
|
const p = root.activePath()
|
|
|
|
|
|
linksModel.clear()
|
|
|
|
|
|
if (p.length > 0) {
|
|
|
|
|
|
const links = vaultIndex.linksOfFile(p)
|
2026-09-01 19:25:07 +03:00
|
|
|
|
for (let i = 0; i < links.length; ++i) linksModel.append({ text: links[i].text, path: links[i].path })
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
tagsModel.clear()
|
|
|
|
|
|
const tags = p.length > 0 ? vaultIndex.tagsOfFile(p) : []
|
2026-09-01 19:25:07 +03:00
|
|
|
|
for (let i = 0; i < tags.length; ++i) tagsModel.append({ tag: tags[i] })
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Component.onCompleted: { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: fileStore
|
2026-09-01 19:25:07 +03:00
|
|
|
|
function onVaultChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
|
|
|
|
|
|
function onReadyChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: documentManager
|
|
|
|
|
|
function onActiveIndexChanged() { root.refreshLinksTags() }
|
|
|
|
|
|
function onOpenTabsChanged() { root.refreshLinksTags() }
|
2026-09-01 19:25:07 +03:00
|
|
|
|
function onContentChanged(i) { if (i === documentManager.activeIndex) { root.refreshLinksTags(); rebuildDebounce.restart() } }
|
|
|
|
|
|
function onDocumentSaved() { rebuildDebounce.restart() }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Connections { target: vaultIndex; function onIndexChanged() { root.refreshLinksTags() } }
|
|
|
|
|
|
Timer { id: rebuildDebounce; interval: 800; onTriggered: if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
|
2026-09-01 22:23:09 +03:00
|
|
|
|
// Делаем панели ресайзабельными внутри правой колонки — увеличенная зона
|
2026-09-01 19:25:07 +03:00
|
|
|
|
SplitView {
|
2026-08-17 00:05:20 +03:00
|
|
|
|
anchors.fill: parent
|
2026-09-01 19:25:07 +03:00
|
|
|
|
orientation: Qt.Vertical
|
2026-09-01 22:23:09 +03:00
|
|
|
|
handle: Rectangle {
|
|
|
|
|
|
implicitHeight: 6
|
|
|
|
|
|
color: Theme.divider
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
|
height: 1; width: parent.width
|
|
|
|
|
|
color: Theme.divider
|
|
|
|
|
|
}
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
|
height: 16; width: parent.width
|
|
|
|
|
|
color: "transparent"
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
cursorShape: Qt.SplitVCursor
|
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
|
onPressed: mouse.accepted = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-17 00:05:20 +03:00
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
// Календарь
|
2026-08-17 00:05:20 +03:00
|
|
|
|
Rectangle {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
id: calBox
|
|
|
|
|
|
SplitView.preferredHeight: 240
|
|
|
|
|
|
SplitView.minimumHeight: 180
|
|
|
|
|
|
SplitView.maximumHeight: 360
|
|
|
|
|
|
color: Theme.bgAlt
|
|
|
|
|
|
border.color: Theme.divider
|
2026-08-17 14:18:43 +03:00
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
spacing: 0
|
2026-09-01 19:25:07 +03:00
|
|
|
|
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 }
|
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
MonthCalendar {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
id: cal
|
2026-08-17 14:18:43 +03:00
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.fillHeight: true
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Layout.margins: 4
|
2026-08-17 14:18:43 +03:00
|
|
|
|
onDaySelected: (ds) => {
|
|
|
|
|
|
const path = fileStore.dailyNotePath(ds)
|
2026-09-01 19:25:07 +03:00
|
|
|
|
if (path.length > 0) documentManager.openDocument(path)
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
// Ссылки и теги
|
2026-08-17 14:18:43 +03:00
|
|
|
|
Rectangle {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
id: linksBox
|
|
|
|
|
|
SplitView.preferredHeight: 260
|
|
|
|
|
|
SplitView.minimumHeight: 120
|
|
|
|
|
|
color: Theme.bg
|
|
|
|
|
|
ScrollView {
|
2026-08-17 14:18:43 +03:00
|
|
|
|
anchors.fill: parent
|
2026-09-01 19:25:07 +03:00
|
|
|
|
clip: true
|
|
|
|
|
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
width: parent.width - 12
|
|
|
|
|
|
x: 6
|
|
|
|
|
|
y: 6
|
|
|
|
|
|
spacing: 8
|
2026-08-17 14:18:43 +03:00
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
// Ссылки
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
// Теги
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Item { Layout.fillHeight: true }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
2026-08-17 00:05:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
// Граф
|
2026-08-17 00:05:20 +03:00
|
|
|
|
Rectangle {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
id: graphBox
|
|
|
|
|
|
SplitView.fillHeight: true
|
|
|
|
|
|
SplitView.minimumHeight: 160
|
|
|
|
|
|
color: Theme.graphBg
|
2026-08-17 14:18:43 +03:00
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
spacing: 0
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Rectangle {
|
2026-08-17 14:18:43 +03:00
|
|
|
|
Layout.fillWidth: true
|
2026-09-01 19:25:07 +03:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
GraphView {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
id: graph
|
2026-08-17 14:18:43 +03:00
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
|
nodes: vaultIndex.nodes
|
|
|
|
|
|
edges: vaultIndex.edges
|
|
|
|
|
|
onFileClicked: (path) => documentManager.openDocument(path)
|
|
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
// легенда
|
|
|
|
|
|
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 }
|
|
|
|
|
|
}
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
function baseName(p) { const idx = p.lastIndexOf("/"); return idx >= 0 ? p.substring(idx+1) : p }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
|
|
|
|
|
|
Popup {
|
|
|
|
|
|
id: tagFilesPopup
|
|
|
|
|
|
modal: true
|
|
|
|
|
|
anchors.centerIn: parent
|
2026-09-01 19:25:07 +03:00
|
|
|
|
width: 360
|
|
|
|
|
|
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
ColumnLayout {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
width: parent.width - 16
|
|
|
|
|
|
x: 8; y: 8
|
|
|
|
|
|
Label { id: tagFilesTitle; font.bold: true; color: Theme.text }
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
|
Layout.preferredHeight: Math.min(260, contentHeight)
|
2026-08-17 14:18:43 +03:00
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
clip: true
|
2026-09-01 19:25:07 +03:00
|
|
|
|
ListView {
|
|
|
|
|
|
model: ListModel { id: tagFilesModel }
|
|
|
|
|
|
delegate: ItemDelegate {
|
|
|
|
|
|
width: ListView.view.width
|
|
|
|
|
|
text: model.name
|
|
|
|
|
|
onClicked: { documentManager.openDocument(model.path); tagFilesPopup.close() }
|
2026-08-17 14:18:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-17 00:05:20 +03:00
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Button { text: qsTr("Закрыть"); Layout.alignment: Qt.AlignRight; onClicked: tagFilesPopup.close() }
|
2026-08-17 00:05:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
|
|
|
|
|
|
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 }
|
|
|
|
|
|
}
|