2026-08-17 00:43:33 +03:00
|
|
|
|
import QtQuick
|
|
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
|
import QtQuick.Layouts
|
|
|
|
|
|
import QtQuick.Dialogs
|
2026-09-01 19:25:07 +03:00
|
|
|
|
import Flora
|
2026-08-17 00:43:33 +03:00
|
|
|
|
|
|
|
|
|
|
Dialog {
|
|
|
|
|
|
id: root
|
|
|
|
|
|
|
|
|
|
|
|
required property bool firstRun
|
|
|
|
|
|
|
|
|
|
|
|
readonly property int stepFolder: 0
|
|
|
|
|
|
readonly property int stepSetup: 1
|
|
|
|
|
|
property int step: stepFolder
|
|
|
|
|
|
property string vaultPath: ""
|
|
|
|
|
|
property string setupStatus: ""
|
|
|
|
|
|
|
|
|
|
|
|
signal finished()
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
title: firstRun ? qsTr("Первое знакомство: создайте хранилище") : qsTr("Новое хранилище")
|
2026-08-17 00:43:33 +03:00
|
|
|
|
modal: true
|
|
|
|
|
|
width: 560
|
|
|
|
|
|
height: 480
|
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
|
closePolicy: Dialog.NoAutoClose
|
2026-09-01 19:25:07 +03:00
|
|
|
|
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 }
|
|
|
|
|
|
}
|
2026-08-17 00:43:33 +03:00
|
|
|
|
|
|
|
|
|
|
StackLayout {
|
|
|
|
|
|
id: stack
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
currentIndex: root.step
|
|
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
spacing: 12
|
|
|
|
|
|
Label {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
text: qsTr("Выберите локальную папку — это будет ваше хранилище.\nМожно указать новую папку (будет создана) или уже существующую с файлами.")
|
2026-08-17 00:43:33 +03:00
|
|
|
|
wrapMode: Text.Wrap
|
|
|
|
|
|
Layout.fillWidth: true
|
2026-09-01 19:25:07 +03:00
|
|
|
|
color: Theme.textSecondary
|
2026-08-17 00:43:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
TextField {
|
|
|
|
|
|
id: pathField
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
placeholderText: qsTr("Путь к папке…")
|
2026-09-01 19:25:07 +03:00
|
|
|
|
placeholderTextColor: Theme.textFaint
|
2026-08-17 00:43:33 +03:00
|
|
|
|
text: root.vaultPath
|
2026-09-01 19:25:07 +03:00
|
|
|
|
color: Theme.text
|
|
|
|
|
|
background: Rectangle { color: Theme.bgAlt; border.color: pathField.activeFocus ? Theme.accent : Theme.border; radius: 6 }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
onTextChanged: root.vaultPath = text
|
|
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Button { text: qsTr("Обзор…"); onClicked: folderDialog.open() }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
FolderDialog {
|
|
|
|
|
|
id: folderDialog
|
|
|
|
|
|
title: qsTr("Выберите папку хранилища")
|
2026-08-17 14:58:52 +03:00
|
|
|
|
onAccepted: {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
// Используем 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
|
2026-08-17 14:58:52 +03:00
|
|
|
|
}
|
2026-08-17 00:43:33 +03:00
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Rectangle {
|
2026-08-17 00:43:33 +03:00
|
|
|
|
Layout.fillWidth: true
|
2026-09-01 19:25:07 +03:00
|
|
|
|
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: ""
|
|
|
|
|
|
}
|
2026-08-17 00:43:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
Item { Layout.fillHeight: true }
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Label { text: qsTr("Шаг 1 из 2 — выбор папки"); color: Theme.textFaint; font.pixelSize: 11 }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
RowLayout {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Item { Layout.fillWidth: true }
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Button { text: qsTr("Отмена"); onClicked: root.reject() }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
Button {
|
|
|
|
|
|
text: qsTr("Далее")
|
2026-09-01 19:25:07 +03:00
|
|
|
|
highlighted: true
|
2026-08-17 00:43:33 +03:00
|
|
|
|
enabled: root.vaultPath.length > 0 && !busyOverlay.running
|
|
|
|
|
|
onClicked: {
|
|
|
|
|
|
step1Error.text = ""
|
|
|
|
|
|
busyOverlay.running = true
|
|
|
|
|
|
const err = vaultManager.createVault(root.vaultPath)
|
|
|
|
|
|
busyOverlay.running = false
|
2026-09-01 19:25:07 +03:00
|
|
|
|
if (err.length > 0) { step1Error.text = err; return }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
root.step = stepSetup
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
spacing: 12
|
|
|
|
|
|
Label {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
text: qsTr("Хранилище готово. Внутри автоматически создан git-репозиторий.\nПри желании подключите синхронизацию:")
|
2026-08-17 00:43:33 +03:00
|
|
|
|
wrapMode: Text.Wrap
|
|
|
|
|
|
Layout.fillWidth: true
|
2026-09-01 19:25:07 +03:00
|
|
|
|
color: Theme.textSecondary
|
2026-08-17 00:43:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
GridLayout {
|
|
|
|
|
|
columns: 2
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
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)") }
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Button { text: "Syncthing"; Layout.fillWidth: true; Layout.columnSpan: 2; onClicked: openProviderGuide(4, "Syncthing") }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
function openProviderGuide(idx, name) {
|
|
|
|
|
|
providerGuideForButtons.providerIndex = idx
|
|
|
|
|
|
providerGuideForButtons.providerName = name
|
|
|
|
|
|
providerGuideForButtons.connected = false
|
|
|
|
|
|
providerGuideForButtons.open()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
|
Layout.fillWidth: true
|
2026-09-01 19:25:07 +03:00
|
|
|
|
Button { text: qsTr("Назад"); onClicked: root.step = stepFolder }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
|
Button {
|
|
|
|
|
|
text: qsTr("Готово")
|
2026-09-01 19:25:07 +03:00
|
|
|
|
highlighted: true
|
|
|
|
|
|
onClicked: { vaultManager.selectVault(root.vaultPath); root.accept(); root.finished() }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
BusyIndicator { id: busyOverlay; anchors.centerIn: parent; running: false; visible: running }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
|
2026-09-01 19:25:07 +03:00
|
|
|
|
ProviderSetupGuide { id: providerGuideForButtons }
|
2026-08-17 00:43:33 +03:00
|
|
|
|
|
|
|
|
|
|
onRejected: {
|
2026-09-01 19:25:07 +03:00
|
|
|
|
if (firstRun) visible = false
|
2026-08-17 00:43:33 +03:00
|
|
|
|
}
|
2026-09-01 19:25:07 +03:00
|
|
|
|
}
|