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:
+187
-5
@@ -1,12 +1,31 @@
|
||||
#include "core/SettingsStore.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
constexpr auto kGroupVaults = "vaults";
|
||||
constexpr auto kKeyVaults = "vaults/vaultList";
|
||||
constexpr auto kKeyCurrent = "vaults/current";
|
||||
constexpr auto kKeyProviders = "sync/providers";
|
||||
constexpr auto kKeyTheme = "ui/theme";
|
||||
constexpr auto kKeyLeftW = "ui/leftPanelWidth";
|
||||
constexpr auto kKeyRightW = "ui/rightPanelWidth";
|
||||
constexpr auto kKeyViewModes = "ui/viewModes";
|
||||
|
||||
QString encodeKey(const QString &s) {
|
||||
// Процент-кодирование чтобы избежать интерпретации "/" как разделителя групп в QSettings Ini
|
||||
return QString::fromLatin1(QUrl::toPercentEncoding(s, QByteArray(), QByteArray("/")));
|
||||
}
|
||||
QString encodeRel(const QString &rel) {
|
||||
// Для относительных путей тоже кодируем "/" как %2F чтобы ключ остался плоским
|
||||
return QString::fromLatin1(QUrl::toPercentEncoding(rel));
|
||||
}
|
||||
QString decodeKey(const QString &s) {
|
||||
return QUrl::fromPercentEncoding(s.toLatin1());
|
||||
}
|
||||
}
|
||||
|
||||
SettingsStore::SettingsStore(QObject *parent)
|
||||
@@ -31,7 +50,8 @@ QString SettingsStore::currentVault() const
|
||||
void SettingsStore::setCurrentVault(const QString &path)
|
||||
{
|
||||
QSettings s;
|
||||
s.setValue(kKeyCurrent, path);
|
||||
s.setValue(kKeyCurrent, QDir::cleanPath(path));
|
||||
s.sync();
|
||||
emit currentVaultChanged();
|
||||
}
|
||||
|
||||
@@ -42,18 +62,26 @@ QVariantList SettingsStore::providers() const
|
||||
|
||||
void SettingsStore::addVault(const QString &path)
|
||||
{
|
||||
const QString clean = QDir::cleanPath(path);
|
||||
if (clean.isEmpty())
|
||||
return;
|
||||
QStringList list = vaults();
|
||||
if (!list.contains(path))
|
||||
list.prepend(path);
|
||||
// нормализуем существующие записи
|
||||
for (QString &v : list) v = QDir::cleanPath(v);
|
||||
list.removeAll(clean);
|
||||
list.prepend(clean);
|
||||
QSettings s;
|
||||
s.setValue(kKeyVaults, list);
|
||||
s.setValue(kKeyCurrent, path);
|
||||
s.setValue(kKeyCurrent, clean);
|
||||
s.sync();
|
||||
emit vaultsChanged();
|
||||
emit currentVaultChanged();
|
||||
}
|
||||
|
||||
void SettingsStore::addProvider(const QString &provider, const QVariantMap &config)
|
||||
{
|
||||
if (provider.isEmpty())
|
||||
return;
|
||||
QVariantList list = m_providers;
|
||||
for (auto &item : list) {
|
||||
auto m = item.toMap();
|
||||
@@ -74,12 +102,18 @@ void SettingsStore::addProvider(const QString &provider, const QVariantMap &conf
|
||||
save:
|
||||
QSettings s;
|
||||
s.setValue(kKeyProviders, m_providers);
|
||||
s.sync();
|
||||
emit providersChanged();
|
||||
}
|
||||
|
||||
bool SettingsStore::hasVault(const QString &path) const
|
||||
{
|
||||
return vaults().contains(path);
|
||||
const QString clean = QDir::cleanPath(path);
|
||||
const QStringList list = vaults();
|
||||
for (const QString &v : list)
|
||||
if (QDir::cleanPath(v) == clean)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void SettingsStore::clearProvider(const QString &provider)
|
||||
@@ -93,5 +127,153 @@ void SettingsStore::clearProvider(const QString &provider)
|
||||
m_providers = list;
|
||||
QSettings s;
|
||||
s.setValue(kKeyProviders, m_providers);
|
||||
s.sync();
|
||||
emit providersChanged();
|
||||
}
|
||||
|
||||
QString SettingsStore::theme() const
|
||||
{
|
||||
QSettings s;
|
||||
const QString t = s.value(kKeyTheme, QStringLiteral("light")).toString();
|
||||
return (t == "dark" || t == "light") ? t : QStringLiteral("light");
|
||||
}
|
||||
|
||||
void SettingsStore::setTheme(const QString &t)
|
||||
{
|
||||
const QString clean = (t == "dark") ? QStringLiteral("dark") : QStringLiteral("light");
|
||||
QSettings s;
|
||||
if (s.value(kKeyTheme).toString() == clean)
|
||||
return;
|
||||
s.setValue(kKeyTheme, clean);
|
||||
s.sync();
|
||||
emit themeChanged();
|
||||
}
|
||||
|
||||
int SettingsStore::leftPanelWidth() const
|
||||
{
|
||||
QSettings s;
|
||||
return s.value(kKeyLeftW, 220).toInt();
|
||||
}
|
||||
|
||||
void SettingsStore::setLeftPanelWidth(int w)
|
||||
{
|
||||
if (w < 100) w = 100;
|
||||
if (w > 800) w = 800;
|
||||
QSettings s;
|
||||
if (s.value(kKeyLeftW).toInt() == w)
|
||||
return;
|
||||
s.setValue(kKeyLeftW, w);
|
||||
s.sync();
|
||||
emit leftPanelWidthChanged();
|
||||
}
|
||||
|
||||
int SettingsStore::rightPanelWidth() const
|
||||
{
|
||||
QSettings s;
|
||||
return s.value(kKeyRightW, 220).toInt();
|
||||
}
|
||||
|
||||
void SettingsStore::setRightPanelWidth(int w)
|
||||
{
|
||||
if (w < 100) w = 100;
|
||||
if (w > 800) w = 800;
|
||||
QSettings s;
|
||||
if (s.value(kKeyRightW).toInt() == w)
|
||||
return;
|
||||
s.setValue(kKeyRightW, w);
|
||||
s.sync();
|
||||
emit rightPanelWidthChanged();
|
||||
}
|
||||
|
||||
QString SettingsStore::viewModeForFile(const QString &vault, const QString &filePath) const
|
||||
{
|
||||
if (vault.isEmpty() || filePath.isEmpty())
|
||||
return QStringLiteral("edit");
|
||||
QSettings s;
|
||||
const QVariantMap root = s.value(kKeyViewModes).toMap();
|
||||
const QString cleanVault = QDir::cleanPath(vault);
|
||||
const QString cleanPath = QDir::cleanPath(filePath);
|
||||
QString rel = cleanPath;
|
||||
if (cleanPath.startsWith(cleanVault)) {
|
||||
rel = cleanPath.mid(cleanVault.length());
|
||||
while (rel.startsWith("/") || rel.startsWith("\\")) rel.remove(0,1);
|
||||
} else {
|
||||
rel = cleanPath;
|
||||
}
|
||||
const QString oldKey = cleanVault + "|" + rel;
|
||||
if (root.contains(oldKey)) {
|
||||
const QString m = root.value(oldKey).toString();
|
||||
if (m == "preview" || m == "live" || m == "edit") return m;
|
||||
}
|
||||
const QString encVault = encodeKey(cleanVault);
|
||||
const QString encRel = encodeRel(rel);
|
||||
const QString encKey = encVault + "|" + encRel;
|
||||
if (root.contains(encKey)) {
|
||||
const QString m = root.value(encKey).toString();
|
||||
if (m == "preview" || m == "live" || m == "edit") return m;
|
||||
}
|
||||
// Обратная совместимость: старый вложенный формат
|
||||
QVariantMap vaultMap = root.value(encVault).toMap();
|
||||
if (vaultMap.isEmpty())
|
||||
vaultMap = root.value(cleanVault).toMap();
|
||||
QString mode = vaultMap.value(encRel).toString();
|
||||
if (mode.isEmpty())
|
||||
mode = vaultMap.value(rel).toString();
|
||||
if (mode == "preview" || mode == "live" || mode == "edit")
|
||||
return mode;
|
||||
return QStringLiteral("edit");
|
||||
}
|
||||
|
||||
void SettingsStore::setViewModeForFile(const QString &vault, const QString &filePath, const QString &mode)
|
||||
{
|
||||
if (vault.isEmpty() || filePath.isEmpty())
|
||||
return;
|
||||
QString cleanMode = mode;
|
||||
if (cleanMode != "preview" && cleanMode != "live")
|
||||
cleanMode = "edit";
|
||||
QSettings s;
|
||||
QVariantMap root = s.value(kKeyViewModes).toMap();
|
||||
const QString cleanVault = QDir::cleanPath(vault);
|
||||
const QString cleanPath = QDir::cleanPath(filePath);
|
||||
QString rel = cleanPath;
|
||||
if (cleanPath.startsWith(cleanVault)) {
|
||||
rel = cleanPath.mid(cleanVault.length());
|
||||
while (rel.startsWith("/") || rel.startsWith("\\")) rel.remove(0,1);
|
||||
} else {
|
||||
rel = cleanPath;
|
||||
}
|
||||
const QString encVault = encodeKey(cleanVault);
|
||||
const QString encRel = encodeRel(rel);
|
||||
const QString encKey = encVault + "|" + encRel;
|
||||
// мигрируем старые ключи
|
||||
const QString oldKey = cleanVault + "|" + rel;
|
||||
if (root.contains(oldKey))
|
||||
root.remove(oldKey);
|
||||
// мигрируем старый вложенный формат если есть
|
||||
if (root.contains(encVault) && root.value(encVault).typeId() == QMetaType::QVariantMap) {
|
||||
QVariantMap vm = root.value(encVault).toMap();
|
||||
if (!vm.isEmpty()) {
|
||||
// переносим все вложенные записи в плоский формат
|
||||
for (auto it = vm.constBegin(); it != vm.constEnd(); ++it) {
|
||||
const QString innerKey = encVault + "|" + it.key();
|
||||
if (!root.contains(innerKey))
|
||||
root.insert(innerKey, it.value());
|
||||
}
|
||||
root.remove(encVault);
|
||||
}
|
||||
}
|
||||
if (root.contains(cleanVault) && root.value(cleanVault).typeId() == QMetaType::QVariantMap) {
|
||||
QVariantMap vm = root.value(cleanVault).toMap();
|
||||
root.remove(cleanVault);
|
||||
for (auto it = vm.constBegin(); it != vm.constEnd(); ++it) {
|
||||
const QString k = encodeKey(cleanVault) + "|" + encodeRel(it.key());
|
||||
if (!root.contains(k))
|
||||
root.insert(k, it.value());
|
||||
}
|
||||
}
|
||||
if (root.value(encKey).toString() == cleanMode)
|
||||
return;
|
||||
root.insert(encKey, cleanMode);
|
||||
s.setValue(kKeyViewModes, root);
|
||||
s.sync();
|
||||
}
|
||||
Reference in New Issue
Block a user