2026-08-17 00:05:20 +03:00
|
|
|
|
import QtQuick
|
|
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
|
import QtQuick.Layouts
|
|
|
|
|
|
|
2026-08-17 14:18:43 +03:00
|
|
|
|
// Правая панель: календарь (заметки дня), «Ссылки и теги» активного файла,
|
|
|
|
|
|
// граф связей (файлы + теги, рёбра по ссылкам).
|
2026-08-17 00:05:20 +03:00
|
|
|
|
Item {
|
|
|
|
|
|
id: root
|
|
|
|
|
|
|
2026-08-17 14:18:43 +03:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: documentManager
|
|
|
|
|
|
function onActiveIndexChanged() { root.refreshLinksTags() }
|
|
|
|
|
|
function onOpenTabsChanged() { root.refreshLinksTags() }
|
|
|
|
|
|
function onContentChanged(i) {
|
|
|
|
|
|
if (i === documentManager.activeIndex) root.refreshLinksTags()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: vaultIndex
|
|
|
|
|
|
function onIndexChanged() { root.refreshLinksTags() }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 00:05:20 +03:00
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
spacing: 0
|
|
|
|
|
|
|
2026-08-17 14:18:43 +03:00
|
|
|
|
// ---------- Календарь ----------
|
2026-08-17 00:05:20 +03:00
|
|
|
|
Rectangle {
|
|
|
|
|
|
Layout.fillWidth: true
|
2026-08-17 14:18:43 +03:00
|
|
|
|
implicitHeight: 218
|
2026-08-17 00:05:20 +03:00
|
|
|
|
color: "#fafafa"
|
2026-08-17 14:18:43 +03:00
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
spacing: 0
|
|
|
|
|
|
Label {
|
|
|
|
|
|
text: qsTr("Календарь")
|
|
|
|
|
|
font.bold: true
|
|
|
|
|
|
color: "#555"
|
|
|
|
|
|
padding: 6
|
|
|
|
|
|
}
|
|
|
|
|
|
MonthCalendar {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
|
onDaySelected: (ds) => {
|
|
|
|
|
|
const path = fileStore.dailyNotePath(ds)
|
|
|
|
|
|
if (path.length > 0)
|
|
|
|
|
|
documentManager.openDocument(path)
|
|
|
|
|
|
}
|
|
|
|
|
|
} }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Ссылки и теги активного файла ----------
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
|
color: "#f4f4f4"
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
anchors.margins: 6
|
|
|
|
|
|
spacing: 4
|
|
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
|
text: qsTr("Ссылки и теги")
|
|
|
|
|
|
font.bold: true
|
|
|
|
|
|
color: "#555"
|
|
|
|
|
|
}
|
|
|
|
|
|
Label {
|
|
|
|
|
|
text: root.activePath().length > 0
|
|
|
|
|
|
? qsTr("Файл: %1").arg(root.baseName(activePath()))
|
|
|
|
|
|
: qsTr("Нет активного файла")
|
|
|
|
|
|
elide: Text.ElideMiddle
|
|
|
|
|
|
color: "#888"
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Label { text: qsTr("Ссылки"); color: "#666"; font.pixelSize: 11 }
|
|
|
|
|
|
ListView {
|
|
|
|
|
|
id: linksList
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.preferredHeight: Math.min(120, linksModel.count * 22)
|
|
|
|
|
|
model: ListModel { id: linksModel }
|
|
|
|
|
|
clip: true
|
|
|
|
|
|
delegate: ItemDelegate {
|
|
|
|
|
|
width: ListView.view.width
|
|
|
|
|
|
height: 22
|
|
|
|
|
|
text: model.text
|
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
|
onClicked: documentManager.openDocument(model.path)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Label {
|
|
|
|
|
|
visible: linksModel.count === 0
|
|
|
|
|
|
text: qsTr("— нет ссылок —")
|
|
|
|
|
|
color: "#bbb"
|
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Label { text: qsTr("Теги"); color: "#666"; font.pixelSize: 11 }
|
|
|
|
|
|
Flow {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
readonly property var m: ListModel { id: tagsModel }
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Item { Layout.fillHeight: true }
|
2026-08-17 00:05:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 14:18:43 +03:00
|
|
|
|
// ---------- Граф связей ----------
|
2026-08-17 00:05:20 +03:00
|
|
|
|
Rectangle {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.fillHeight: true
|
2026-08-17 14:18:43 +03:00
|
|
|
|
color: "#eef1f5"
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
spacing: 0
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Label {
|
|
|
|
|
|
text: qsTr("Граф связей")
|
|
|
|
|
|
font.bold: true
|
|
|
|
|
|
color: "#555"
|
|
|
|
|
|
padding: 6
|
|
|
|
|
|
}
|
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
|
Button {
|
|
|
|
|
|
text: qsTr("Обновить")
|
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
|
implicitHeight: 22
|
|
|
|
|
|
onClicked: vaultIndex.rebuild(fileStore.vault)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
GraphView {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
|
nodes: vaultIndex.nodes
|
|
|
|
|
|
edges: vaultIndex.edges
|
|
|
|
|
|
onFileClicked: (path) => documentManager.openDocument(path)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function baseName(p) {
|
|
|
|
|
|
const idx = p.lastIndexOf("/")
|
|
|
|
|
|
return idx >= 0 ? p.substring(idx + 1) : p
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Popup {
|
|
|
|
|
|
id: tagFilesPopup
|
|
|
|
|
|
modal: true
|
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
|
width: 300
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
width: parent.width
|
|
|
|
|
|
Label { id: tagFilesTitle; font.bold: true }
|
|
|
|
|
|
ListView {
|
|
|
|
|
|
Layout.preferredHeight: Math.min(220, contentHeight)
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
model: ListModel { id: tagFilesModel }
|
|
|
|
|
|
clip: true
|
|
|
|
|
|
delegate: ItemDelegate {
|
|
|
|
|
|
width: ListView.view.width
|
|
|
|
|
|
text: model.name
|
|
|
|
|
|
onClicked: {
|
|
|
|
|
|
documentManager.openDocument(model.path)
|
|
|
|
|
|
tagFilesPopup.close()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-17 00:05:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|