Фаза 7: правая панель — VaultIndex (теги/[[вики]]/md-ссылки, граф), MonthCalendar (заметки дня), RightPanel (ссылки+теги, чипы, попап файлов), GraphView (Canvas, drag, клик-открытие); dailyNotePath; тест vaultindex (16/16); self-contained Flora.app
This commit is contained in:
@@ -261,6 +261,27 @@ QString FileStore::createFile(const QString &parentPath, const QString &name)
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString FileStore::dailyNotePath(const QString &dateStr)
|
||||
{
|
||||
if (m_vault.isEmpty())
|
||||
return QString();
|
||||
if (!QDir().mkpath(m_vault + "/Журнал"))
|
||||
return QString();
|
||||
const QString path = m_vault + "/Журнал/" + dateStr + ".md";
|
||||
if (!QFileInfo::exists(path)) {
|
||||
QFile f(path);
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
f.write(QStringLiteral("# %1\n\n").arg(dateStr).toUtf8());
|
||||
f.close();
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
m_expanded.insert(m_vault + "/Журнал");
|
||||
scheduleRebuild();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
QString FileStore::createFolder(const QString &parentPath, const QString &name)
|
||||
{
|
||||
const QString dir = parentPath.isEmpty() ? m_vault : parentPath;
|
||||
|
||||
@@ -47,6 +47,9 @@ public:
|
||||
|
||||
Q_INVOKABLE QString createFile(const QString &parentPath, const QString &name);
|
||||
Q_INVOKABLE QString createFolder(const QString &parentPath, const QString &name);
|
||||
|
||||
// Заметка дня: Журнал/ГГГГ-ММ-ДД.md (создаётся при отсутствии).
|
||||
Q_INVOKABLE QString dailyNotePath(const QString &dateStr);
|
||||
Q_INVOKABLE QString renameItem(const QString &path, const QString &newName);
|
||||
Q_INVOKABLE QString removeItem(const QString &path);
|
||||
Q_INVOKABLE QString moveItem(const QString &srcPath, const QString &destDir);
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
#include "core/VaultIndex.h"
|
||||
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace {
|
||||
|
||||
QStringList kIgnoredNames = {".git", ".flora", ".stfolder"};
|
||||
|
||||
// Извлекает теги #tag из текста (пропуская блоки кода).
|
||||
QStringList extractTags(const QString &text)
|
||||
{
|
||||
QStringList tags;
|
||||
static const QRegularExpression tagRe(
|
||||
QStringLiteral("(?:^|[^\\w#])#([\\p{L}\\w][\\p{L}\\w-]*)"));
|
||||
bool inCode = false;
|
||||
const QStringList lines = text.split(QLatin1Char('\n'));
|
||||
for (const QString &line : lines) {
|
||||
const QString t = line.trimmed();
|
||||
if (t.startsWith(QLatin1String("```"))) {
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
if (inCode)
|
||||
continue;
|
||||
// Заголовок "# Заголовок" — не тег (после # идёт пробел)
|
||||
if (t.startsWith(QLatin1String("# ")) || t.startsWith(QLatin1String("## ")))
|
||||
continue;
|
||||
auto it = tagRe.globalMatch(line);
|
||||
while (it.hasNext()) {
|
||||
const QString tag = it.next().captured(1);
|
||||
if (!tags.contains(tag))
|
||||
tags.append(tag);
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
QString cleanDir(const QString &p)
|
||||
{
|
||||
QString s = p;
|
||||
while (s.endsWith(QLatin1Char('/')))
|
||||
s.chop(1);
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
VaultIndex::VaultIndex(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString VaultIndex::displayName(const QString &nodeKey) const
|
||||
{
|
||||
if (nodeKey.startsWith(QLatin1String("tag:")))
|
||||
return nodeKey.mid(4);
|
||||
const int idx = nodeKey.lastIndexOf(QLatin1Char('/'));
|
||||
return idx >= 0 ? nodeKey.mid(idx + 1) : nodeKey;
|
||||
}
|
||||
|
||||
QString VaultIndex::nodeKeyFor(const QString &path, bool isFile) const
|
||||
{
|
||||
return isFile ? QDir::cleanPath(path) : QStringLiteral("tag:%1").arg(path);
|
||||
}
|
||||
|
||||
int VaultIndex::ensureNode(const QString &key, const QString &name, const QString &type)
|
||||
{
|
||||
auto it = m_nodeById.constFind(key);
|
||||
if (it != m_nodeById.constEnd())
|
||||
return it.value();
|
||||
QVariantMap node;
|
||||
node["key"] = key;
|
||||
node["name"] = name;
|
||||
node["type"] = type;
|
||||
m_nodes.append(node);
|
||||
const int id = m_nodes.size() - 1;
|
||||
m_nodeById.insert(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
void VaultIndex::addEdge(const QString &from, const QString &to)
|
||||
{
|
||||
if (from == to)
|
||||
return;
|
||||
QVariantMap edge;
|
||||
edge["from"] = from;
|
||||
edge["to"] = to;
|
||||
m_edges.append(edge);
|
||||
}
|
||||
|
||||
QString VaultIndex::resolveLink(const QString &fileDir, const QString &target) const
|
||||
{
|
||||
QString t = target.trimmed();
|
||||
if (t.isEmpty())
|
||||
return QString();
|
||||
if (t.contains(QLatin1String("://")))
|
||||
return QString(); // внешняя
|
||||
QStringList cands;
|
||||
cands << cleanDir(fileDir) + '/' + t
|
||||
<< cleanDir(fileDir) + '/' + t + ".md";
|
||||
for (const QString &c : cands)
|
||||
if (QFileInfo::exists(c))
|
||||
return QDir::cleanPath(c);
|
||||
// поиск по имени файла по всему хранилищу (без расширения)
|
||||
auto it = m_fileByBase.constFind(t);
|
||||
if (it != m_fileByBase.constEnd())
|
||||
return it.value();
|
||||
it = m_fileByBase.constFind(t + ".md");
|
||||
if (it != m_fileByBase.constEnd())
|
||||
return it.value();
|
||||
return QString();
|
||||
}
|
||||
|
||||
void VaultIndex::rebuild(const QString &vault)
|
||||
{
|
||||
m_busy = true;
|
||||
emit busyChanged();
|
||||
|
||||
m_nodes.clear();
|
||||
m_edges.clear();
|
||||
m_nodeById.clear();
|
||||
m_tagsOfFile.clear();
|
||||
m_linksOfFile.clear();
|
||||
m_filesWithTag.clear();
|
||||
m_fileByBase.clear();
|
||||
m_fileCount = 0;
|
||||
|
||||
// Проход 1: собрать все md-файлы и карту имён
|
||||
QStringList files;
|
||||
collectFiles(vault, 0, files);
|
||||
m_fileCount = files.size();
|
||||
for (const QString &f : files) {
|
||||
const QString base = QFileInfo(f).completeBaseName();
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(base))
|
||||
m_fileByBase.insert(base, f);
|
||||
}
|
||||
// Проход 2: узлы/теги/ссылки
|
||||
for (const QString &f : files)
|
||||
parseFile(f);
|
||||
|
||||
m_busy = false;
|
||||
emit busyChanged();
|
||||
emit indexChanged();
|
||||
}
|
||||
|
||||
void VaultIndex::collectFiles(const QString &dir, int depth, QStringList &out) const
|
||||
{
|
||||
if (depth > 24)
|
||||
return;
|
||||
QDir d(dir);
|
||||
const QFileInfoList entries = d.entryInfoList(
|
||||
QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::NoSymLinks);
|
||||
for (const QFileInfo &fi : entries) {
|
||||
if (kIgnoredNames.contains(fi.fileName()))
|
||||
continue;
|
||||
if (fi.isDir())
|
||||
collectFiles(fi.absoluteFilePath(), depth + 1, out);
|
||||
else if (fi.suffix().compare(QLatin1String("md"), Qt::CaseInsensitive) == 0)
|
||||
out.append(fi.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
void VaultIndex::parseFile(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
const QString text = QString::fromUtf8(f.readAll());
|
||||
f.close();
|
||||
|
||||
// m_fileCount уже установлен в rebuild (files.size())
|
||||
const QString fileDir = QFileInfo(path).absolutePath();
|
||||
const QString key = nodeKeyFor(path, true);
|
||||
ensureNode(key, QFileInfo(path).fileName(), QStringLiteral("file"));
|
||||
{
|
||||
const QString base = QFileInfo(path).completeBaseName();
|
||||
if (!base.isEmpty() && !m_fileByBase.contains(base))
|
||||
m_fileByBase.insert(base, path);
|
||||
}
|
||||
|
||||
// Теги
|
||||
const QStringList tags = extractTags(text);
|
||||
m_tagsOfFile.insert(path, tags);
|
||||
for (const QString &tag : tags) {
|
||||
const QString tagKey = nodeKeyFor(tag, false);
|
||||
ensureNode(tagKey, QStringLiteral("#%1").arg(tag), QStringLiteral("tag"));
|
||||
addEdge(key, tagKey);
|
||||
m_filesWithTag[tag].append(path);
|
||||
}
|
||||
|
||||
// Ссылки [[X]] и [text](rel)
|
||||
QVariantList links;
|
||||
static const QRegularExpression wikiRe(QStringLiteral("\\[\\[([^\\]]+)\\]\\]"));
|
||||
auto wi = wikiRe.globalMatch(text);
|
||||
while (wi.hasNext()) {
|
||||
const QString target = wi.next().captured(1);
|
||||
const QString resolved = resolveLink(fileDir, target);
|
||||
if (!resolved.isEmpty()) {
|
||||
links.append(QVariantMap{{"text", target}, {"path", resolved}});
|
||||
addEdge(key, nodeKeyFor(resolved, true));
|
||||
ensureNode(nodeKeyFor(resolved, true),
|
||||
QFileInfo(resolved).fileName(), QStringLiteral("file"));
|
||||
}
|
||||
}
|
||||
static const QRegularExpression mdLinkRe(
|
||||
QStringLiteral("\\[([^\\]]+)\\]\\(([^\\)]+)\\)"));
|
||||
auto mi = mdLinkRe.globalMatch(text);
|
||||
while (mi.hasNext()) {
|
||||
auto m = mi.next();
|
||||
const QString target = m.captured(2);
|
||||
if (target.startsWith(QLatin1String("#")))
|
||||
continue;
|
||||
const QString resolved = resolveLink(fileDir, target);
|
||||
if (!resolved.isEmpty()) {
|
||||
links.append(QVariantMap{{"text", m.captured(1)}, {"path", resolved}});
|
||||
addEdge(key, nodeKeyFor(resolved, true));
|
||||
ensureNode(nodeKeyFor(resolved, true),
|
||||
QFileInfo(resolved).fileName(), QStringLiteral("file"));
|
||||
}
|
||||
}
|
||||
m_linksOfFile.insert(path, links);
|
||||
}
|
||||
|
||||
QStringList VaultIndex::tagsOfFile(const QString &path) const
|
||||
{
|
||||
return m_tagsOfFile.value(path);
|
||||
}
|
||||
|
||||
QVariantList VaultIndex::linksOfFile(const QString &path) const
|
||||
{
|
||||
return m_linksOfFile.value(path);
|
||||
}
|
||||
|
||||
QStringList VaultIndex::filesWithTag(const QString &tag) const
|
||||
{
|
||||
return m_filesWithTag.value(tag);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
#include <QHash>
|
||||
#include <QStringList>
|
||||
|
||||
// Индекс связей хранилища: теги, [[перекрёстные ссылки]], md-ссылки между
|
||||
// файлами, а также узлы и рёбра графа. Сканируется синхронно.
|
||||
class VaultIndex : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QVariantList nodes READ nodes NOTIFY indexChanged)
|
||||
Q_PROPERTY(QVariantList edges READ edges NOTIFY indexChanged)
|
||||
Q_PROPERTY(int fileCount READ fileCount NOTIFY indexChanged)
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
|
||||
public:
|
||||
explicit VaultIndex(QObject *parent = nullptr);
|
||||
|
||||
QVariantList nodes() const { return m_nodes; }
|
||||
QVariantList edges() const { return m_edges; }
|
||||
int fileCount() const { return m_fileCount; }
|
||||
bool busy() const { return m_busy; }
|
||||
|
||||
// Полное пересканирование хранилища.
|
||||
Q_INVOKABLE void rebuild(const QString &vault);
|
||||
|
||||
// Теги файла / ссылки файла (список {text,path} для открытия).
|
||||
Q_INVOKABLE QStringList tagsOfFile(const QString &path) const;
|
||||
Q_INVOKABLE QVariantList linksOfFile(const QString &path) const;
|
||||
|
||||
// Все файлы с тегом tag.
|
||||
Q_INVOKABLE QStringList filesWithTag(const QString &tag) const;
|
||||
|
||||
Q_INVOKABLE QString displayName(const QString &nodeKey) const;
|
||||
|
||||
signals:
|
||||
void indexChanged();
|
||||
void busyChanged();
|
||||
|
||||
private:
|
||||
QString nodeKeyFor(const QString &path, bool isFile) const;
|
||||
void collectFiles(const QString &dir, int depth, QStringList &out) const;
|
||||
void parseFile(const QString &path);
|
||||
QString resolveLink(const QString &fileDir, const QString &target) const;
|
||||
int ensureNode(const QString &key, const QString &name, const QString &type);
|
||||
void addEdge(const QString &from, const QString &to);
|
||||
|
||||
QVariantList m_nodes;
|
||||
QVariantList m_edges;
|
||||
QHash<QString, int> m_nodeById;
|
||||
QHash<QString, QStringList> m_tagsOfFile;
|
||||
QHash<QString, QVariantList> m_linksOfFile;
|
||||
QHash<QString, QStringList> m_filesWithTag;
|
||||
QHash<QString, QString> m_fileByBase; // имя без расширения → путь
|
||||
int m_fileCount = 0;
|
||||
bool m_busy = false;
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "app/WindowController.h"
|
||||
#include "core/DocumentManager.h"
|
||||
#include "core/SettingsStore.h"
|
||||
#include "core/VaultIndex.h"
|
||||
#include "git/GitManager.h"
|
||||
#include "markdown/MdParser.h"
|
||||
#include "sync/SyncManager.h"
|
||||
@@ -41,6 +42,7 @@ int main(int argc, char *argv[])
|
||||
VaultManager vaultManager(&settingsStore, &gitManager);
|
||||
DocumentManager documentManager;
|
||||
MdParser mdParser;
|
||||
VaultIndex vaultIndex;
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.rootContext()->setContextProperty("settingsStore", &settingsStore);
|
||||
@@ -49,6 +51,7 @@ int main(int argc, char *argv[])
|
||||
engine.rootContext()->setContextProperty("vaultManager", &vaultManager);
|
||||
engine.rootContext()->setContextProperty("documentManager", &documentManager);
|
||||
engine.rootContext()->setContextProperty("mdParser", &mdParser);
|
||||
engine.rootContext()->setContextProperty("vaultIndex", &vaultIndex);
|
||||
|
||||
WindowController windowController(
|
||||
&engine, QStringLiteral("qrc:/qt/qml/Flora/qml/MainWindow.qml"));
|
||||
|
||||
Reference in New Issue
Block a user