Files
Flora/qml/panels/RightPanel.qml
T
grendervill 95c03040c7 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 и др.
2026-09-01 19:25:07 +03:00

310 lines
14 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) }
}
Connections {
target: documentManager
function onActiveIndexChanged() { root.refreshLinksTags() }
function onOpenTabsChanged() { root.refreshLinksTags() }
function onContentChanged(i) { if (i === documentManager.activeIndex) { root.refreshLinksTags(); rebuildDebounce.restart() } }
function onDocumentSaved() { rebuildDebounce.restart() }
}
Connections { target: vaultIndex; function onIndexChanged() { root.refreshLinksTags() } }
Timer { id: rebuildDebounce; interval: 800; onTriggered: if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
// Делаем панели ресайзабельными внутри правой колонки
SplitView {
anchors.fill: parent
orientation: Qt.Vertical
handle: Rectangle { implicitHeight: 1; color: Theme.divider }
// Календарь
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 }
ToolButton {
text: "\u2637"
font.pixelSize: 12
ToolTip.text: qsTr("Сегодня")
ToolTip.visible: hovered
onClicked: cal.currentMonth = new Date()
}
}
}
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<files.length;++i) tagFilesModel.append({ path: files[i], name: baseName(files[i]) })
tagFilesTitle.text = qsTr("Файлы с тегом #%1 (%2)").arg(model.tag).arg(files.length)
tagFilesPopup.open()
}
}
}
Label {
visible: tagsModel.count === 0
text: qsTr("— нет тегов —")
color: Theme.textFaint
font.pixelSize: 11
font.italic: true
}
}
}
}
Item { Layout.fillHeight: true }
}
}
}
// Граф
Rectangle {
id: graphBox
SplitView.fillHeight: true
SplitView.minimumHeight: 160
color: Theme.graphBg
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
height: 32
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 }
Label { text: vaultIndex.nodes.length + "·" + vaultIndex.edges.length; color: Theme.textFaint; font.pixelSize: 10 }
ToolButton {
text: "\u21BB"
font.pixelSize: 12
ToolTip.text: qsTr("Обновить")
ToolTip.visible: hovered
onClicked: vaultIndex.rebuild(fileStore.vault)
}
ToolButton {
text: "\u2630"
ToolTip.text: qsTr("Справка")
ToolTip.visible: hovered
onClicked: graphHelp.open()
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
GraphView {
id: graph
Layout.fillWidth: true
Layout.fillHeight: true
nodes: vaultIndex.nodes
edges: vaultIndex.edges
onFileClicked: (path) => 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 }
}