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 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 } }