Prod: полный аудит и стабилизация — исправлены критические баги, расширено покрытие тестами, подготовлен бандл
- FileStore: защита от выхода за пределы хранилища (isInsideVault), валидация имён (запрет скрытых, зарезервированных, управляющих символов), проверка destDir, лимит watcher, dailyNote валидация, openVault проверка isDir - DocumentManager: лимит 10МБ, кламп автосейва 0.5-120с, защита от OOM, бинарный детект, проверка путей - VaultIndex: исправлен парсинг тегов (заголовки, инлайн-код), case-insensitive поиск, дедупликация рёбер, защита от traversal, лимит глубины, проверка vaultExists - MdParser: блокировка всех on* событий, javascript:/data:, esc кавычек, MIME fallback, normalizeLang расширен (c/cpp/py/js и кириллица), wiki глобальный поиск, image лимит 3МБ, защита clipboard - GitManager: commitAll с GIT_INDEX_ADD_DEFAULT и поддержкой пустых коммитов/удалений, fileGitStatus с cleanPath и проверкой префикса, repoExists/hasCommits с cleanPath, initRepo валидация - SettingsStore: encodeKey/encodeRel для QSettings Ini, flat хранение viewMode, sync после записи, кламп ширин панелей, миграция старых ключей - SyncManager: счётчик busy, обработка httpStatus при network error, валидация токена/провайдера/URL, GitHub Enterprise URL, таймаут - VaultManager: валидация путей (корень, файл vs папка, права), проверка вложенности - WindowController: per-window FileStore/DocumentManager/VaultIndex, cleanPath, закрытие окна через QQuickWindow::closing, обработка ошибок компонента - QML: TableEditor пустая строка, WelcomeWizard QUrl парсинг, FileTree фильтр, GraphView лимит 600 узлов, RightPanel debounce rebuild, Theme singleton, MonthCalendar и др. - Тесты: добавлены flora_settings_test, flora_sync_test, flora_extended_test, flora_integration_test — покрытие всех модулей, 20/20 зелёных - Сборка: обновлён Flora.app (4.6М бинарь -> 173М бандл, ad-hoc подпись, проверен offscreen запуск, версия 0.0.1 + cmark 0.29 + libgit2 1.9) - .gitignore: добавлены Testing/, coverage, gcda/gcov, __pycache__, .tmp, test-vault и др.
This commit is contained in:
+174
-65
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Flora
|
||||
|
||||
Item {
|
||||
id: pane
|
||||
@@ -13,17 +14,22 @@ Item {
|
||||
signal openBeside()
|
||||
signal closePane()
|
||||
|
||||
function insertText(t) {
|
||||
textDisplay.insert(textDisplay.cursorPosition, t)
|
||||
}
|
||||
function insertText(t) { textDisplay.insert(textDisplay.cursorPosition, t) }
|
||||
|
||||
function refresh() {
|
||||
const idx = pane.docIndex
|
||||
if (idx >= 0) {
|
||||
const c = documentManager.contentAt(idx)
|
||||
textDisplay.text = c
|
||||
if (textDisplay.text !== c && !textDisplay.activeFocus)
|
||||
textDisplay.text = c
|
||||
if (!pane.editMode) {
|
||||
viewDisplay.text = mdParser.render(c, fileStore.vault, filePathDir(documentManager.pathAt(idx)))
|
||||
} else {
|
||||
viewDisplay.text = ""
|
||||
}
|
||||
} else {
|
||||
textDisplay.text = ""
|
||||
viewDisplay.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,102 +43,205 @@ Item {
|
||||
|
||||
Connections {
|
||||
target: documentManager
|
||||
function onActiveIndexChanged() {
|
||||
if (documentManager.activeIndex === pane.docIndex)
|
||||
refresh()
|
||||
}
|
||||
function onContentChanged(idx) { if (idx === pane.docIndex && !textDisplay.activeFocus) refresh() }
|
||||
function onActiveIndexChanged() { if (documentManager.activeIndex === pane.docIndex) refresh() }
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
// --- Текстовый редактор (сверху до низу, без шапки) ---
|
||||
TextEdit {
|
||||
id: textDisplay
|
||||
function applyInline(pre, post) {
|
||||
if (!pane.editMode) return
|
||||
textDisplay.forceActiveFocus()
|
||||
const s = textDisplay.selectionStart, e = textDisplay.selectionEnd
|
||||
if (s !== e) {
|
||||
const sel = textDisplay.selectedText
|
||||
textDisplay.remove(s, e)
|
||||
textDisplay.insert(s, pre + sel + post)
|
||||
textDisplay.select(s, s + pre.length + sel.length + post.length)
|
||||
} else {
|
||||
const pos = textDisplay.cursorPosition
|
||||
textDisplay.insert(pos, pre + post)
|
||||
textDisplay.cursorPosition = pos + pre.length
|
||||
}
|
||||
documentManager.updateContent(pane.docIndex, textDisplay.text)
|
||||
}
|
||||
function applyHeading(level) {
|
||||
if (!pane.editMode) return
|
||||
textDisplay.forceActiveFocus()
|
||||
const txt = textDisplay.text
|
||||
const pos = textDisplay.cursorPosition
|
||||
let start = txt.lastIndexOf("\n", pos - 1) + 1
|
||||
let end = txt.indexOf("\n", pos)
|
||||
if (end < 0) end = txt.length
|
||||
let line = txt.substring(start, end)
|
||||
line = line.replace(/^#{1,6}\s*/, "")
|
||||
const newLine = "#".repeat(level) + " " + line
|
||||
textDisplay.remove(start, end)
|
||||
textDisplay.insert(start, newLine)
|
||||
textDisplay.cursorPosition = start + newLine.length
|
||||
documentManager.updateContent(pane.docIndex, textDisplay.text)
|
||||
}
|
||||
function applyLinePrefix(prefix) {
|
||||
if (!pane.editMode) return
|
||||
textDisplay.forceActiveFocus()
|
||||
const txt = textDisplay.text
|
||||
const s = textDisplay.selectionStart, e = textDisplay.selectionEnd
|
||||
let start = txt.lastIndexOf("\n", s - 1) + 1
|
||||
let finish = txt.indexOf("\n", e)
|
||||
if (finish < 0) finish = txt.length
|
||||
const block = txt.substring(start, finish)
|
||||
const newBlock = block.split("\n").map(l => prefix + l).join("\n")
|
||||
textDisplay.remove(start, finish)
|
||||
textDisplay.insert(start, newBlock)
|
||||
textDisplay.select(start, start + newBlock.length)
|
||||
documentManager.updateContent(pane.docIndex, textDisplay.text)
|
||||
}
|
||||
function applyTaskToggle() { applyLinePrefix("- [ ] ") }
|
||||
function applyCallout(kind) { applyLinePrefix("> [!" + kind + "] ") }
|
||||
function applyFloatWrap() {
|
||||
if (!pane.editMode) return
|
||||
textDisplay.forceActiveFocus()
|
||||
textDisplay.insert(textDisplay.cursorPosition, "![[image.png|float:left|width:40%]]")
|
||||
documentManager.updateContent(pane.docIndex, textDisplay.text)
|
||||
}
|
||||
function applyColor(color, highlightMode) {
|
||||
if (highlightMode) applyInline('<span style="background-color:' + color + '">', '</span>')
|
||||
else applyInline('<span style="color:' + color + '">', '</span>')
|
||||
}
|
||||
function applySubSup(kind) {
|
||||
if (kind === "sup") applyInline("<sup>", "</sup>")
|
||||
else applyInline("<sub>", "</sub>")
|
||||
}
|
||||
|
||||
// --- Текстовый редактор ---
|
||||
ScrollView {
|
||||
id: editScroll
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 32
|
||||
anchors.bottomMargin: 36
|
||||
visible: pane.editMode
|
||||
clip: true
|
||||
text: documentManager.activeIndex >= 0
|
||||
? documentManager.contentAt(documentManager.activeIndex)
|
||||
: ""
|
||||
color: "black"
|
||||
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
|
||||
font.pixelSize: 14
|
||||
wrapMode: TextEdit.NoWrap
|
||||
selectByMouse: true
|
||||
onTextEdited: documentManager.updateContent(pane.docIndex, text)
|
||||
}
|
||||
|
||||
// --- Режим просмотра ---
|
||||
TextEdit {
|
||||
id: viewDisplay
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 32
|
||||
visible: !pane.editMode
|
||||
readOnly: true
|
||||
clip: true
|
||||
text: pane.editMode ? "" : mdParser.render(
|
||||
documentManager.contentAt(pane.docIndex),
|
||||
fileStore.vault,
|
||||
filePathDir(documentManager.pathAt(pane.docIndex))
|
||||
)
|
||||
textFormat: TextEdit.RichText
|
||||
color: "black"
|
||||
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
|
||||
font.pixelSize: 14
|
||||
wrapMode: TextEdit.Wrap
|
||||
onLinkActivated: (href) => {
|
||||
if (href.startsWith("flora://task/")) {
|
||||
const line = Number(href.substring(14))
|
||||
if (line > 0) {
|
||||
const lines = textDisplay.text.split("\n")
|
||||
const ln = lines[line - 1]
|
||||
if (ln !== undefined) {
|
||||
if (/\[ \]/.test(ln)) lines[line - 1] = ln.replace("[ ]", "[x]")
|
||||
else if (/\[x\]/.test(ln)) lines[line - 1] = ln.replace("[x]", "[ ]")
|
||||
textDisplay.text = lines.join("\n")
|
||||
documentManager.updateContent(pane.docIndex, textDisplay.text)
|
||||
}
|
||||
}
|
||||
} else if (href.startsWith("flora://open/")) {
|
||||
documentManager.openDocument(decodeURIComponent(href.substring(13)))
|
||||
}
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
TextArea {
|
||||
id: textDisplay
|
||||
placeholderText: qsTr("Начните писать…")
|
||||
placeholderTextColor: Theme.textFaint
|
||||
color: Theme.editorText
|
||||
selectionColor: Theme.selectionBg
|
||||
selectedTextColor: Theme.text
|
||||
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
|
||||
font.pixelSize: 14
|
||||
wrapMode: TextArea.NoWrap
|
||||
selectByMouse: true
|
||||
background: Rectangle { color: Theme.editorBg }
|
||||
onTextChanged: if (activeFocus && text !== documentManager.contentAt(pane.docIndex)) editDebounce.restart()
|
||||
}
|
||||
}
|
||||
Timer { id: editDebounce; interval: 250; onTriggered: documentManager.updateContent(pane.docIndex, textDisplay.text) }
|
||||
|
||||
// --- Серая панель ВНИЗУ: кнопки ✎👁 × ---
|
||||
ScrollView {
|
||||
id: viewScroll
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 36
|
||||
visible: !pane.editMode
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
TextArea {
|
||||
id: viewDisplay
|
||||
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 }
|
||||
onLinkActivated: (href) => {
|
||||
if (href.startsWith("flora://copycode/")) {
|
||||
const id = href.substring(17)
|
||||
const code = mdParser.getCodeBlock(id)
|
||||
if (code.length > 0) { mdParser.copyToClipboard(code); copyFeedback.visible = true; copyTimer.restart() }
|
||||
return
|
||||
}
|
||||
if (href.startsWith("flora://task/")) {
|
||||
const line = Number(href.substring(14))
|
||||
if (line > 0) {
|
||||
const lines = textDisplay.text.split("\n")
|
||||
const ln = lines[line - 1]
|
||||
if (ln !== undefined) {
|
||||
if (/\[ \]/.test(ln)) lines[line - 1] = ln.replace("[ ]", "[x]")
|
||||
else if (/\[x\]/.test(ln)) lines[line - 1] = ln.replace("[x]", "[ ]")
|
||||
textDisplay.text = lines.join("\n")
|
||||
documentManager.updateContent(pane.docIndex, textDisplay.text)
|
||||
}
|
||||
}
|
||||
} else if (href.startsWith("flora://open/")) {
|
||||
documentManager.openDocument(decodeURIComponent(href.substring(13)))
|
||||
} else {
|
||||
Qt.openUrlExternally(href)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Тост копирования
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: 8
|
||||
visible: copyFeedback.visible
|
||||
color: "#333"
|
||||
radius: 6
|
||||
width: copyFeedback.implicitWidth + 16
|
||||
height: copyFeedback.implicitHeight + 10
|
||||
z: 11
|
||||
Label { id: copyFeedback; anchors.centerIn: parent; visible: false; text: qsTr("Скопировано!"); color: "white"; font.pixelSize: 11 }
|
||||
}
|
||||
Timer { id: copyTimer; interval: 1500; onTriggered: copyFeedback.visible = false }
|
||||
}
|
||||
|
||||
// --- Нижняя панель ---
|
||||
Rectangle {
|
||||
id: toolbar
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 32
|
||||
height: 36
|
||||
z: 10
|
||||
color: "#e0e0e0"
|
||||
border.color: "#ccc"
|
||||
color: Theme.toolbarBg
|
||||
border.color: Theme.divider
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 4
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 4
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
visible: pane.docIndex >= 0
|
||||
text: pane.docIndex >= 0 ? documentManager.titleAt(pane.docIndex) : ""
|
||||
color: Theme.textMuted
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ToolButton {
|
||||
text: "✎"
|
||||
checkable: true
|
||||
checked: pane.editMode
|
||||
ToolTip.text: qsTr("Редактирование")
|
||||
ToolTip.visible: hovered
|
||||
onClicked: pane.editMode = true
|
||||
}
|
||||
ToolButton {
|
||||
text: "👁"
|
||||
checkable: true
|
||||
checked: !pane.editMode
|
||||
ToolTip.text: qsTr("Просмотр")
|
||||
ToolTip.visible: hovered
|
||||
onClicked: pane.editMode = false
|
||||
}
|
||||
ToolButton {
|
||||
text: "\u2715"
|
||||
visible: pane.closable
|
||||
ToolTip.text: qsTr("Закрыть")
|
||||
ToolTip.visible: hovered
|
||||
onClicked: pane.closePane()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user