2026-09-01 21:07:55 +03:00
|
|
|
import QtQuick
|
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
import Flora
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
id: root
|
|
|
|
|
|
|
|
|
|
property string markdown: ""
|
|
|
|
|
property string vaultRoot: ""
|
|
|
|
|
property string fileDir: ""
|
2026-09-06 18:57:45 +03:00
|
|
|
property string emptyText: qsTr("Пустой файл — начните писать в режиме редактирования")
|
2026-09-06 19:26:53 +03:00
|
|
|
// Если задан — markdown считается старой версией и подсвечиваются отличия от него
|
2026-09-06 21:17:25 +03:00
|
|
|
// (блоки del — только в markdown, add — только в compareMarkdown).
|
|
|
|
|
// Сравнение считается в рабочем потоке, чтобы UI не висел на больших файлах.
|
2026-09-06 19:26:53 +03:00
|
|
|
property string compareMarkdown: ""
|
2026-09-06 21:17:25 +03:00
|
|
|
property int _diffReq: -1
|
2026-09-01 21:07:55 +03:00
|
|
|
implicitHeight: column.implicitHeight + 24
|
|
|
|
|
|
|
|
|
|
signal linkActivated(string href)
|
2026-09-06 21:17:25 +03:00
|
|
|
signal diffApplied()
|
2026-09-01 21:07:55 +03:00
|
|
|
|
2026-09-06 22:17:13 +03:00
|
|
|
// Для обновления. 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)
|
|
|
|
|
}
|
2026-09-01 21:07:55 +03:00
|
|
|
function update() {
|
2026-09-06 21:17:25 +03:00
|
|
|
if (compareMarkdown.length > 0) {
|
2026-09-06 22:17:13 +03:00
|
|
|
asyncDebounce.restart()
|
2026-09-06 21:17:25 +03:00
|
|
|
return
|
|
|
|
|
}
|
2026-09-06 22:17:13 +03:00
|
|
|
_diffReq = -1 // гасим возможный async в полёте: его ответ станет чужим
|
2026-09-01 21:07:55 +03:00
|
|
|
// Триггерим пересборку блоков
|
|
|
|
|
blocksModel.clear()
|
2026-09-06 21:17:25 +03:00
|
|
|
const blocks = mdParser.parseBlocks(markdown, vaultRoot, fileDir)
|
2026-09-01 21:07:55 +03:00
|
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
|
|
|
blocksModel.append(blocks[i])
|
|
|
|
|
}
|
|
|
|
|
// Позиция скролла в начало
|
|
|
|
|
// contentItem.contentY = 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-06 21:17:25 +03:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-01 21:07:55 +03:00
|
|
|
onMarkdownChanged: update()
|
2026-09-06 19:26:53 +03:00
|
|
|
onCompareMarkdownChanged: update()
|
2026-09-01 21:07:55 +03:00
|
|
|
onVaultRootChanged: update()
|
|
|
|
|
onFileDirChanged: update()
|
|
|
|
|
|
|
|
|
|
Component.onCompleted: update()
|
|
|
|
|
|
|
|
|
|
ListModel { id: blocksModel }
|
|
|
|
|
|
|
|
|
|
Column {
|
|
|
|
|
id: column
|
2026-09-01 21:13:50 +03:00
|
|
|
anchors.top: parent.top
|
|
|
|
|
anchors.left: parent.left
|
|
|
|
|
anchors.right: parent.right
|
2026-09-01 21:07:55 +03:00
|
|
|
anchors.margins: 12
|
2026-09-01 21:24:37 +03:00
|
|
|
spacing: 8
|
2026-09-01 21:07:55 +03:00
|
|
|
|
|
|
|
|
Repeater {
|
|
|
|
|
model: blocksModel
|
|
|
|
|
delegate: Item {
|
2026-09-01 21:47:40 +03:00
|
|
|
required property var model
|
|
|
|
|
required property int index
|
2026-09-01 21:07:55 +03:00
|
|
|
width: parent.width
|
|
|
|
|
height: childrenRect.height
|
2026-09-01 21:24:37 +03:00
|
|
|
property string blockType: model.type || ""
|
|
|
|
|
property string blockHtml: model.html || ""
|
2026-09-01 21:47:40 +03:00
|
|
|
property string blockCode: model.codeText || ""
|
2026-09-01 21:24:37 +03:00
|
|
|
property string blockLang: model.language || "text"
|
|
|
|
|
property string blockCodeId: model.codeId || ""
|
2026-09-01 21:47:40 +03:00
|
|
|
property string blockHighlighted: model.highlightedText || ""
|
2026-09-01 21:07:55 +03:00
|
|
|
// HTML block
|
|
|
|
|
Text {
|
2026-09-01 21:24:37 +03:00
|
|
|
visible: blockType === "html" && blockHtml
|
2026-09-01 21:07:55 +03:00
|
|
|
width: parent.width
|
2026-09-01 21:24:37 +03:00
|
|
|
text: blockHtml
|
2026-09-01 21:07:55 +03:00
|
|
|
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 {
|
2026-09-01 21:24:37 +03:00
|
|
|
visible: blockType === "code"
|
2026-09-01 21:07:55 +03:00
|
|
|
width: parent.width
|
2026-09-01 21:24:37 +03:00
|
|
|
code: blockCode
|
|
|
|
|
language: blockLang
|
|
|
|
|
codeId: blockCodeId
|
|
|
|
|
highlightedText: blockHighlighted
|
2026-09-01 21:07:55 +03:00
|
|
|
onCopyRequested: (id) => {
|
2026-09-06 22:17:13 +03:00
|
|
|
if (mdParser.copyCodeBlock(id))
|
2026-09-01 21:07:55 +03:00
|
|
|
root.linkActivated("flora://copycode/" + id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
width: parent.width
|
2026-09-06 18:57:45 +03:00
|
|
|
text: markdown.trim().length === 0 ? emptyText : ""
|
2026-09-01 21:07:55 +03:00
|
|
|
color: Theme.textFaint
|
|
|
|
|
wrapMode: Text.Wrap
|
|
|
|
|
font.italic: true
|
|
|
|
|
visible: markdown.trim().length === 0 && blocksModel.count === 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|