import QtQuick import QtQuick.Controls import Flora Item { id: root property string markdown: "" property string vaultRoot: "" property string fileDir: "" property string emptyText: qsTr("Пустой файл — начните писать в режиме редактирования") // Если задан — markdown считается старой версией и подсвечиваются отличия от него // (блоки del — только в markdown, add — только в compareMarkdown). // Сравнение считается в рабочем потоке, чтобы UI не висел на больших файлах. property string compareMarkdown: "" property int _diffReq: -1 implicitHeight: column.implicitHeight + 24 signal linkActivated(string href) signal diffApplied() // Для обновления. Async-ветка идёт через дебаунс: updateDiff() в диалоге меняет // markdown и compareMarkdown по очереди, без дебаунса уходили бы два запроса // (первый заведомо протухает и отбрасывается по _diffReq). Timer { id: asyncDebounce; interval: 80; onTriggered: root._requestDiff() } function _requestDiff() { if (compareMarkdown.length === 0) return // пока таймер ждал, сравнение уже сняли _diffReq = mdParser.parseBlocksWithDiffAsync(markdown, compareMarkdown, vaultRoot, fileDir) } function update() { if (compareMarkdown.length > 0) { asyncDebounce.restart() return } _diffReq = -1 // гасим возможный async в полёте: его ответ станет чужим // Триггерим пересборку блоков blocksModel.clear() const blocks = mdParser.parseBlocks(markdown, vaultRoot, fileDir) for (let i = 0; i < blocks.length; i++) { blocksModel.append(blocks[i]) } // Позиция скролла в начало // contentItem.contentY = 0 } // База изменилась (переиндексация, правка свойств, вкл/выкл SQL) — // ~sql-блоки надо пересчитать. Дебаунс против шторма при reindex. Timer { id: dbDebounce; interval: 400; onTriggered: root.update() } Connections { target: vaultDatabase function onChanged() { dbDebounce.restart() } function onQueryEnabledChanged() { dbDebounce.restart() } function onMaxRowsChanged() { dbDebounce.restart() } } Connections { target: mdParser function onBlocksReady(reqId, blocks) { if (reqId !== root._diffReq) return // устарел — уже запрошен более свежий blocksModel.clear() for (let i = 0; i < blocks.length; i++) { blocksModel.append(blocks[i]) } root.diffApplied() } } onMarkdownChanged: update() onCompareMarkdownChanged: update() onVaultRootChanged: update() onFileDirChanged: update() Component.onCompleted: update() ListModel { id: blocksModel } Column { id: column anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 12 spacing: 8 Repeater { model: blocksModel delegate: Item { required property var model required property int index width: parent.width height: childrenRect.height property string blockType: model.type || "" property string blockHtml: model.html || "" property string blockCode: model.codeText || "" property string blockLang: model.language || "text" property string blockCodeId: model.codeId || "" property string blockHighlighted: model.highlightedText || "" // HTML block Text { visible: blockType === "html" && blockHtml width: parent.width text: blockHtml textFormat: Text.RichText wrapMode: Text.Wrap color: Theme.text font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace" font.pixelSize: 14 onLinkActivated: (href) => root.linkActivated(href) } // Code block - separate component CodeBlock { visible: blockType === "code" width: parent.width code: blockCode language: blockLang codeId: blockCodeId highlightedText: blockHighlighted onCopyRequested: (id) => { if (mdParser.copyCodeBlock(id)) root.linkActivated("flora://copycode/" + id) } } } } Label { width: parent.width text: markdown.trim().length === 0 ? emptyText : "" color: Theme.textFaint wrapMode: Text.Wrap font.italic: true visible: markdown.trim().length === 0 && blocksModel.count === 0 } } }