d6cc460b52
- SplitView handle: implicit 6, видимая линия 1 + хит-зона 16 (cocoa 16, vertical 16), cursor SplitH/VCursor, hover ToolTip, для файлового дерева/живого просмотра/графа-календаря - MonthCalendar: RowLayout spacing 4, кнопки 28x28 radius 14, background Theme.surface/hover/selected + border, Label месяца/года разделены (70+42, font 11, elide, минимум 90+44 → всегда влезает 'сентябрь 2026'), убрана прозрачность - RightPanel: удалён дублирующий ToolButton 'Сегодня' (☷) из calBox header, оставлен только в MonthCalendar (⌂)
322 lines
14 KiB
QML
322 lines
14 KiB
QML
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: 6
|
||
color: Theme.divider
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
height: 1; width: parent.width
|
||
color: Theme.divider
|
||
}
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
height: 16; width: parent.width
|
||
color: "transparent"
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
cursorShape: Qt.SplitVCursor
|
||
hoverEnabled: true
|
||
onPressed: mouse.accepted = false
|
||
}
|
||
}
|
||
}
|
||
|
||
// Календарь
|
||
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 }
|
||
}
|
||
}
|
||
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 }
|
||
}
|