Фаза 7: правая панель — VaultIndex (теги/[[вики]]/md-ссылки, граф), MonthCalendar (заметки дня), RightPanel (ссылки+теги, чипы, попап файлов), GraphView (Canvas, drag, клик-открытие); dailyNotePath; тест vaultindex (16/16); self-contained Flora.app
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
|
||||
// Небольшое полотно графа связей: узлы-файлы (синие) и теги (зелёные),
|
||||
// рёбра — связи; узлы можно перетаскивать, клик по файлу открывает его.
|
||||
Canvas {
|
||||
id: g
|
||||
|
||||
property var nodes: [] // [{key,name,type}]
|
||||
property var edges: [] // [{from,to}]
|
||||
signal fileClicked(string path)
|
||||
|
||||
property var pos: ({}) // key -> {x,y}
|
||||
property string dragKey: ""
|
||||
|
||||
function nodeForKey(key) {
|
||||
for (let i = 0; i < nodes.length; ++i)
|
||||
if (nodes[i].key === key) return nodes[i]
|
||||
return null
|
||||
}
|
||||
|
||||
function relayout() {
|
||||
pos = {}
|
||||
const n = nodes.length
|
||||
if (n === 0) return
|
||||
const cx = width / 2, cy = height / 2
|
||||
const rad = Math.min(width, height) / 2 - 30
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const a = (i / n) * 2 * Math.PI - Math.PI / 2
|
||||
pos[nodes[i].key] = { x: cx + rad * Math.cos(a), y: cy + rad * Math.sin(a) }
|
||||
}
|
||||
requestPaint()
|
||||
}
|
||||
|
||||
onNodesChanged: relayout()
|
||||
onWidthChanged: relayout()
|
||||
onHeightChanged: relayout()
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
// рёбра
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = "#b9c6d6"
|
||||
for (let i = 0; i < edges.length; ++i) {
|
||||
const f = pos[edges[i].from]
|
||||
const t = pos[edges[i].to]
|
||||
if (!f || !t) continue
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(f.x, f.y)
|
||||
ctx.lineTo(t.x, t.y)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// узлы
|
||||
ctx.textAlign = "center"
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
const nd = nodes[i]
|
||||
const p = pos[nd.key]
|
||||
if (!p) continue
|
||||
const isFile = nd.type === "file"
|
||||
const r = isFile ? 6 : 5
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, r, 0, 2 * Math.PI)
|
||||
ctx.fillStyle = isFile ? (nd.key === g.dragKey ? "#1a6ce0" : "#3f7fd0")
|
||||
: "#3aa06a"
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = "#ffffff"
|
||||
ctx.lineWidth = 1
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = "#5a6a7d"
|
||||
ctx.font = "9px sans-serif"
|
||||
const label = isFile ? nd.name : nd.name
|
||||
const txt = label.length > 14 ? label.substring(0, 13) + "…" : label
|
||||
ctx.fillText(txt, p.x, p.y + r + 10)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: (m) => {
|
||||
dragKey = ""
|
||||
for (const key in g.pos) {
|
||||
const p = g.pos[key]
|
||||
const dx = m.x - p.x, dy = m.y - p.y
|
||||
if (dx * dx + dy * dy <= 144) { // радиус захвата 12px
|
||||
dragKey = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
onPositionChanged: (m) => {
|
||||
if (dragKey.length > 0 && g.pos[dragKey]) {
|
||||
g.pos[dragKey] = { x: m.x, y: m.y }
|
||||
g.requestPaint()
|
||||
}
|
||||
}
|
||||
onReleased: (m) => {
|
||||
if (dragKey.length === 0) {
|
||||
for (const key in g.pos) {
|
||||
const p = g.pos[key]
|
||||
const dx = m.x - p.x, dy = m.y - p.y
|
||||
if (dx * dx + dy * dy <= 100) {
|
||||
const nd = g.nodeForKey(key)
|
||||
if (nd && nd.type === "file")
|
||||
g.fileClicked(nd.key)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
dragKey = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user