Блоки кода: отдельный компонент CodeBlock + MarkdownView (Telegram-пузырь)

- CodeBlock.qml: отдельный Rectangle #2e4d6a radius 12, header с языком и кнопкой 📋 Копировать (hover rgba 0.22/active 0.30, border, radius 11), код в rgba(0,0,0,0.22) radius 8, JetBrains Mono 13/tab 4, syntax highlight тёмной темой, selectByMouse
- MarkdownView.qml: ScrollView+Column+Repeater, parseBlocks() разбивает markdown на html/code блоки, html через Text RichText, code через CodeBlock, copyRequested → mdParser.copyToClipboard
- MdParser: parseBlocks() regex ```lang\n.*?\n``` + render для html частей, codeId/highlighted, highlightedCode(), highlight(...,true) forceDark, QTime удалён, QRegularExpression добавлен
- EditorPanel: viewArea/livePreview теперь MarkdownView (markdown property), liveDebounce/markdown, onLinkActivated → handleLink, viewMode переключает markdown
- CMake: добавлены CodeBlock.qml/MarkdownView.qml в qml модуль
- Исправлены qmllint: rgba → Qt.rgba, lineHeight убран, model.type → required property
This commit is contained in:
2026-09-01 21:07:55 +03:00
parent 79b76c8c42
commit ed7dc4e033
6 changed files with 338 additions and 47 deletions
+24 -47
View File
@@ -22,11 +22,9 @@ Item {
_loadingViewMode = false
// обновить превью если нужно
if (viewMode === "preview") {
const rendered = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
viewArea.text = rendered.trim().length === 0 ? "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Пустой файл — начните писать в режиме редактирования") + "</i></p>" : rendered
viewArea.markdown = textArea.text
} else if (viewMode === "live") {
const rendered = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
livePreview.text = rendered.trim().length === 0 ? "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Предпросмотр пуст") + "</i></p>" : rendered
livePreview.markdown = textArea.text
}
}
@@ -42,16 +40,9 @@ Item {
if (_loadingViewMode) return
saveViewModeForCurrentTab()
if (viewMode === "preview") {
const rendered = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
if (rendered.trim().length === 0) {
viewArea.text = "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Пустой файл — начните писать в режиме редактирования") + "</i></p>"
} else {
viewArea.text = rendered
}
viewArea.markdown = textArea.text
} else if (viewMode === "live") {
const r = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
livePreview.text = r.trim().length === 0 ? "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Предпросмотр пуст") + "</i></p>" : r
if (livePreview.text.trim().length === 0) livePreview.text = "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Предпросмотр пуст") + "</i></p>"
livePreview.markdown = textArea.text
}
}
@@ -532,7 +523,7 @@ Item {
}
}
// Превью — виден в preview и live
// Превью — виден в preview и live (отдельный компонент для блоков кода)
ScrollView {
id: viewScroll
SplitView.fillWidth: true
@@ -540,33 +531,23 @@ Item {
visible: viewMode !== "edit" && documentManager.activeIndex >= 0
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
background: Rectangle { color: Theme.editorBg }
TextArea {
MarkdownView {
id: viewArea
visible: viewMode === "preview"
readOnly: true
textFormat: Text.RichText
color: Theme.text
selectionColor: Theme.selectionBg
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
font.pixelSize: 14
wrapMode: TextArea.Wrap
background: Rectangle { color: Theme.editorBg }
width: viewScroll.width
vaultRoot: fileStore.vault
fileDir: activeFileDir()
onLinkActivated: (href) => handleLink(href)
}
// Live preview отдельный TextArea чтобы не конфликтовать с обычным preview
TextArea {
MarkdownView {
id: livePreview
visible: viewMode === "live"
readOnly: true
textFormat: Text.RichText
color: Theme.text
selectionColor: Theme.selectionBg
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
font.pixelSize: 14
wrapMode: TextArea.Wrap
background: Rectangle { color: Theme.editorBg }
width: viewScroll.width
vaultRoot: fileStore.vault
fileDir: activeFileDir()
onLinkActivated: (href) => handleLink(href)
}
}
@@ -594,8 +575,8 @@ Item {
else if (/\[x\]/.test(ln)) lines[line - 1] = ln.replace("[x]", "[ ]")
textArea.text = lines.join("\n")
documentManager.updateContent(documentManager.activeIndex, textArea.text)
if (viewMode === "preview") viewArea.text = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
else if (viewMode === "live") livePreview.text = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
if (viewMode === "preview") viewArea.markdown = textArea.text
else if (viewMode === "live") livePreview.markdown = textArea.text
}
}
} else if (href.startsWith("flora://open/")) {
@@ -653,7 +634,7 @@ Item {
}
Timer { id: editDebounce; interval: 250; onTriggered: documentManager.updateContent(documentManager.activeIndex, textArea.text) }
Timer { id: liveDebounce; interval: 150; onTriggered: { if (viewMode === "live") livePreview.text = mdParser.render(textArea.text, fileStore.vault, activeFileDir()) } }
Timer { id: liveDebounce; interval: 150; onTriggered: { if (viewMode === "live") livePreview.markdown = textArea.text } }
property string gitStatusText: ""
property string lastModifiedStr: ""
@@ -686,16 +667,14 @@ Item {
textArea.cursorPosition = Math.min(pos, c.length)
}
if (viewMode === "preview") {
const rendered = mdParser.render(c, fileStore.vault, activeFileDir())
viewArea.text = rendered.trim().length === 0 ? "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Пустой файл — начните писать в режиме редактирования") + "</i></p>" : rendered
viewArea.markdown = c
} else if (viewMode === "live") {
const rendered = mdParser.render(c, fileStore.vault, activeFileDir())
livePreview.text = rendered.trim().length === 0 ? "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Предпросмотр пуст") + "</i></p>" : rendered
livePreview.markdown = c
}
} else {
textArea.text = ""
viewArea.text = ""
livePreview.text = ""
viewArea.markdown = ""
livePreview.markdown = ""
}
refreshGitStatus()
}
@@ -887,7 +866,7 @@ Item {
ToolTip.visible: hovered
onClicked: {
viewMode = "live"
livePreview.text = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
livePreview.markdown = textArea.text
}
}
ToolButton {
@@ -898,8 +877,7 @@ Item {
ToolTip.text: qsTr("Просмотр (Ctrl+E)")
ToolTip.visible: hovered
onClicked: {
const rendered = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
viewArea.text = rendered.trim().length === 0 ? "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Пустой файл") + "</i></p>" : rendered
viewArea.markdown = textArea.text
viewMode = "preview"
}
}
@@ -922,8 +900,7 @@ Item {
} }
Shortcut { sequence: "Ctrl+E"; onActivated: {
if (viewMode === "edit") {
const rendered = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
viewArea.text = rendered.trim().length === 0 ? "<p style='color:" + Theme.textFaint + "'><i>" + qsTr("Пустой файл") + "</i></p>" : rendered
viewArea.markdown = textArea.text
viewMode = "preview"
} else if (viewMode === "preview") {
viewMode = "edit"