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:
@@ -2,11 +2,8 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import Flora
|
||||
|
||||
// Мастер первого запуска / добавления хранилища:
|
||||
// 1) выбор папки (новая или с файлами);
|
||||
// 2) автосоздание git-репозитория + подключение провайдеров (опционально);
|
||||
// 3) готово.
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
@@ -20,27 +17,48 @@ Dialog {
|
||||
|
||||
signal finished()
|
||||
|
||||
title: firstRun ? qsTr("Первое знакомство: создайте хранилище")
|
||||
: qsTr("Новое хранилище")
|
||||
title: firstRun ? qsTr("Первое знакомство: создайте хранилище") : qsTr("Новое хранилище")
|
||||
modal: true
|
||||
width: 560
|
||||
height: 480
|
||||
anchors.centerIn: parent
|
||||
closePolicy: Dialog.NoAutoClose
|
||||
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
|
||||
|
||||
// индикатор шагов
|
||||
header: Rectangle {
|
||||
height: 48
|
||||
color: Theme.bgAlt
|
||||
radius: 10
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
Repeater {
|
||||
model: 2
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 4
|
||||
radius: 2
|
||||
color: index <= root.step ? Theme.accent : Theme.border
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; height: 1; color: Theme.divider }
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: stack
|
||||
anchors.fill: parent
|
||||
currentIndex: root.step
|
||||
|
||||
// ---------- Шаг 1: выбор папки ----------
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
Label {
|
||||
text: qsTr("Выберите локальную папку — это будет ваше хранилище.\n"
|
||||
+ "Можно указать новую папку (будет создана) или уже существующую с файлами.")
|
||||
text: qsTr("Выберите локальную папку — это будет ваше хранилище.\nМожно указать новую папку (будет создана) или уже существующую с файлами.")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
color: Theme.textSecondary
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
@@ -48,67 +66,93 @@ Dialog {
|
||||
id: pathField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Путь к папке…")
|
||||
placeholderTextColor: Theme.textFaint
|
||||
text: root.vaultPath
|
||||
color: Theme.text
|
||||
background: Rectangle { color: Theme.bgAlt; border.color: pathField.activeFocus ? Theme.accent : Theme.border; radius: 6 }
|
||||
onTextChanged: root.vaultPath = text
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Обзор…")
|
||||
onClicked: folderDialog.open()
|
||||
}
|
||||
Button { text: qsTr("Обзор…"); onClicked: folderDialog.open() }
|
||||
}
|
||||
FolderDialog {
|
||||
id: folderDialog
|
||||
title: qsTr("Выберите папку хранилища")
|
||||
onAccepted: {
|
||||
// selectedFolder — URL вида "file:///path/to/folder"
|
||||
var u = selectedFolder.toString()
|
||||
if (u.startsWith("file://"))
|
||||
root.vaultPath = u.substring(7)
|
||||
else
|
||||
root.vaultPath = u
|
||||
// Используем toLocalFile где возможно для корректной обработки unicode и пробелов
|
||||
let p = ""
|
||||
try {
|
||||
const url = selectedFolder
|
||||
if (url && url.toString) {
|
||||
// QUrl -> local file
|
||||
// В QML FolderDialog selectedFolder уже QUrl, пробуем toString и декодируем
|
||||
const s = url.toString()
|
||||
if (s.startsWith("file://")) {
|
||||
// Пытаемся через decodeURIComponent и обрезку file://
|
||||
p = decodeURIComponent(s)
|
||||
if (p.startsWith("file://")) p = p.substring(7)
|
||||
// Windows: /C:/path -> C:/path
|
||||
if (p.length >= 3 && p[0] === "/" && p[2] === ":" && /[A-Za-z]/.test(p[1])) p = p.substring(1)
|
||||
// Убираем якорь/параметры если есть
|
||||
const hash = p.indexOf('#')
|
||||
if (hash >= 0) p = p.substring(0, hash)
|
||||
const q = p.indexOf('?')
|
||||
if (q >= 0) p = p.substring(0, q)
|
||||
} else {
|
||||
p = decodeURIComponent(s)
|
||||
}
|
||||
}
|
||||
} catch(e) { console.warn("FolderDialog parse error", e) }
|
||||
if (p.length === 0 && selectedFolder) {
|
||||
try { p = selectedFolder.toString() } catch(e) {}
|
||||
}
|
||||
root.vaultPath = p
|
||||
}
|
||||
}
|
||||
Label {
|
||||
id: step1Error
|
||||
color: "red"
|
||||
wrapMode: Text.Wrap
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
text: ""
|
||||
visible: step1Error.text.length > 0
|
||||
color: Theme.errorBg
|
||||
border.color: Theme.errorText
|
||||
radius: 6
|
||||
height: step1Error.implicitHeight + 12
|
||||
Label {
|
||||
id: step1Error
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
color: Theme.errorText
|
||||
wrapMode: Text.Wrap
|
||||
text: ""
|
||||
}
|
||||
}
|
||||
Item { Layout.fillHeight: true }
|
||||
Label { text: qsTr("Шаг 1 из 2 — выбор папки"); color: Theme.textFaint; font.pixelSize: 11 }
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: qsTr("Отмена")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
Button { text: qsTr("Отмена"); onClicked: root.reject() }
|
||||
Button {
|
||||
text: qsTr("Далее")
|
||||
highlighted: true
|
||||
enabled: root.vaultPath.length > 0 && !busyOverlay.running
|
||||
onClicked: {
|
||||
step1Error.text = ""
|
||||
busyOverlay.running = true
|
||||
const err = vaultManager.createVault(root.vaultPath)
|
||||
busyOverlay.running = false
|
||||
if (err.length > 0) {
|
||||
step1Error.text = err
|
||||
return
|
||||
}
|
||||
if (err.length > 0) { step1Error.text = err; return }
|
||||
root.step = stepSetup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Шаг 2: git + провайдеры ----------
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
Label {
|
||||
text: qsTr("Хранилище готово. Внутри автоматически создан git-репозиторий.\n"
|
||||
+ "При желании подключите синхронизацию:")
|
||||
text: qsTr("Хранилище готово. Внутри автоматически создан git-репозиторий.\nПри желании подключите синхронизацию:")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
color: Theme.textSecondary
|
||||
}
|
||||
GridLayout {
|
||||
columns: 2
|
||||
@@ -116,58 +160,36 @@ Dialog {
|
||||
Layout.fillHeight: true
|
||||
columnSpacing: 8
|
||||
rowSpacing: 8
|
||||
|
||||
Button { text: "GitHub"; Layout.fillWidth: true; onClicked: openProviderGuide(0, "GitHub") }
|
||||
Button { text: "GitLab"; Layout.fillWidth: true; onClicked: openProviderGuide(1, "GitLab") }
|
||||
Button { text: "Gitea"; Layout.fillWidth: true; onClicked: openProviderGuide(2, "Gitea") }
|
||||
Button { text: "Nextcloud (WebDAV)"; Layout.fillWidth: true; onClicked: openProviderGuide(3, "Nextcloud (WebDAV)") }
|
||||
Button { text: "Syncthing"; Layout.fillWidth: true; onClicked: openProviderGuide(4, "Syncthing") }
|
||||
|
||||
Button { text: "Syncthing"; Layout.fillWidth: true; Layout.columnSpan: 2; onClicked: openProviderGuide(4, "Syncthing") }
|
||||
function openProviderGuide(idx, name) {
|
||||
console.log("openProviderGuide:", idx, name)
|
||||
providerGuideForButtons.providerIndex = idx
|
||||
providerGuideForButtons.providerName = name
|
||||
providerGuideForButtons.connected = false
|
||||
providerGuideForButtons.open()
|
||||
console.log("guide opened:", providerGuideForButtons.visible, providerGuideForButtons.width, providerGuideForButtons.height)
|
||||
}
|
||||
Label {
|
||||
color: "#888"
|
||||
Layout.columnSpan: 2
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Button { text: qsTr("Назад"); onClicked: root.step = stepFolder }
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: qsTr("Готово")
|
||||
onClicked: {
|
||||
vaultManager.selectVault(root.vaultPath)
|
||||
root.accept()
|
||||
root.finished()
|
||||
}
|
||||
highlighted: true
|
||||
onClicked: { vaultManager.selectVault(root.vaultPath); root.accept(); root.finished() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
id: busyOverlay
|
||||
anchors.centerIn: parent
|
||||
running: false
|
||||
visible: running
|
||||
}
|
||||
BusyIndicator { id: busyOverlay; anchors.centerIn: parent; running: false; visible: running }
|
||||
|
||||
ProviderSetupGuide {
|
||||
id: providerGuideForButtons
|
||||
}
|
||||
ProviderSetupGuide { id: providerGuideForButtons }
|
||||
|
||||
onRejected: {
|
||||
if (firstRun) {
|
||||
// Пусть пользователь продолжит позже; хранилище не выбрано.
|
||||
Qt.callLater(() => { console.log("Мастер отменён") })
|
||||
visible = false
|
||||
}
|
||||
if (firstRun) visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user