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:
+200
-161
@@ -1,10 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Flora
|
||||
|
||||
// Левая панель: файловое дерево хранилища (Фаза 3).
|
||||
// Создание/переименование/удаление/перемещение, иконки, стрелки раскрытия,
|
||||
// отступы подпапок, ПКМ, drag&drop. Кнопка хранилища внизу слева.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@@ -16,26 +14,33 @@ Item {
|
||||
return idx >= 0 ? p.substring(idx + 1) : p
|
||||
}
|
||||
|
||||
// ----- Заголовок: «+» + название -----
|
||||
property string filterText: ""
|
||||
|
||||
// ----- Заголовок -----
|
||||
Rectangle {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 36
|
||||
color: "whitesmoke"
|
||||
height: 44
|
||||
color: Theme.headerBg
|
||||
border.color: Theme.divider
|
||||
border.width: 0; // нижняя линия отдельно
|
||||
Rectangle { anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; height: 1; color: Theme.divider }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4
|
||||
anchors.rightMargin: 4
|
||||
spacing: 4
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
anchors.topMargin: 6
|
||||
anchors.bottomMargin: 6
|
||||
spacing: 6
|
||||
|
||||
Button {
|
||||
id: addButton
|
||||
text: "+"
|
||||
implicitWidth: 28
|
||||
implicitHeight: 26
|
||||
implicitWidth: 32
|
||||
implicitHeight: 28
|
||||
enabled: fileStore.ready
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Создать файл или папку")
|
||||
@@ -46,38 +51,99 @@ Item {
|
||||
text: fileStore.ready ? qsTr("Файлы") : qsTr("Хранилище не выбрано")
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
color: "#555"
|
||||
color: Theme.textSecondary
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: searchToggle
|
||||
checkable: true
|
||||
text: "\u2315"
|
||||
font.pixelSize: 14
|
||||
ToolTip.text: qsTr("Поиск")
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
ToolButton {
|
||||
text: "\u2630"
|
||||
font.pixelSize: 12
|
||||
ToolTip.text: qsTr("Развернуть / свернуть")
|
||||
ToolTip.visible: hovered
|
||||
onClicked: expandMenu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Поле поиска -----
|
||||
Rectangle {
|
||||
id: searchBar
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: vaultButton.top
|
||||
color: "#fafafa"
|
||||
height: searchToggle.checked ? 38 : 0
|
||||
visible: height > 0
|
||||
clip: true
|
||||
color: Theme.bgAlt
|
||||
border.color: Theme.divider
|
||||
Behavior on height { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
// ПКМ по пустому месту дерева — то же меню, что и «+»
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
spacing: 6
|
||||
TextField {
|
||||
id: searchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Поиск файлов…")
|
||||
font.pixelSize: 12
|
||||
onTextChanged: root.filterText = text.trim()
|
||||
Keys.onEscapePressed: { clear(); searchToggle.checked = false }
|
||||
function clear() { text = ""; root.filterText = "" }
|
||||
}
|
||||
ToolButton {
|
||||
text: "\u2715"
|
||||
visible: searchField.text.length > 0
|
||||
onClicked: searchField.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Меню развернуть/свернуть
|
||||
Menu {
|
||||
id: expandMenu
|
||||
MenuItem { text: qsTr("Развернуть всё"); onClicked: fileStore.expandAll() }
|
||||
MenuItem { text: qsTr("Свернуть всё"); onClicked: fileStore.collapseAll() }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: treeContainer
|
||||
anchors.top: searchBar.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: vaultButton.top
|
||||
color: Theme.bg
|
||||
|
||||
// ПКМ по пустому месту
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton)
|
||||
addMenu.x = mouse.x; addMenu.y = mouse.y; addMenu.popup()
|
||||
addMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: treeView
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
clip: true
|
||||
model: fileStore
|
||||
currentIndex: -1
|
||||
focus: true
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
spacing: 1
|
||||
|
||||
// Перемещение на пустое место = в корень хранилища
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onDropped: (drop) => {
|
||||
@@ -99,31 +165,46 @@ Item {
|
||||
required property string icon
|
||||
required property int index
|
||||
|
||||
// Фильтрация: показываем только совпадающие; папки с детьми не показываем автоматически чтобы не захламлять
|
||||
readonly property bool matchesFilter: root.filterText.length === 0 || display.toLowerCase().indexOf(root.filterText.toLowerCase()) !== -1
|
||||
visible: matchesFilter
|
||||
height: matchesFilter ? 32 : 0
|
||||
width: treeView.width
|
||||
height: 30
|
||||
color: rowRect.dragging ? "#d6e4ff"
|
||||
: (treeView.currentIndex === index ? "#e6f0ff"
|
||||
: (index % 2 === 0 ? "#fafafa" : "#f4f4f4"))
|
||||
color: {
|
||||
if (dragging) return Theme.accentMuted
|
||||
if (treeView.currentIndex === index) return Theme.surfaceSelected
|
||||
if (hover.hovered) return Theme.surfaceHover
|
||||
return "transparent"
|
||||
}
|
||||
border.color: treeView.currentIndex === index ? Theme.accent : "transparent"
|
||||
border.width: treeView.currentIndex === index ? 1 : 0
|
||||
radius: 4
|
||||
|
||||
property string dragPath: ""
|
||||
property bool dragging: false
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4 + depth * 16
|
||||
anchors.rightMargin: 6
|
||||
spacing: 4
|
||||
anchors.leftMargin: 6 + depth * 14
|
||||
anchors.rightMargin: 8
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: isDir ? (expanded ? "\u25BE" : "\u25B8") : ""
|
||||
font.pixelSize: 12
|
||||
Layout.preferredWidth: isDir ? 16 : 8
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#666"
|
||||
opacity: hasChildren ? 1 : 0.35
|
||||
// Стрелка
|
||||
Item {
|
||||
Layout.preferredWidth: isDir ? 16 : 0
|
||||
Layout.preferredHeight: 16
|
||||
visible: isDir
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: expanded ? "\u25BE" : "\u25B8"
|
||||
font.pixelSize: 12
|
||||
color: Theme.textMuted
|
||||
opacity: hasChildren ? 1 : 0.25
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: hasChildren
|
||||
enabled: hasChildren
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: fileStore.toggleExpand(index)
|
||||
}
|
||||
}
|
||||
@@ -137,30 +218,45 @@ Item {
|
||||
text: display
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
color: isDir ? "#1a1a1a" : "#333"
|
||||
color: isDir ? Theme.text : Theme.textSecondary
|
||||
font.bold: isDir
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
// быстрый бейдж для папок с детьми
|
||||
Rectangle {
|
||||
visible: isDir && hasChildren
|
||||
implicitWidth: countLabel.implicitWidth + 8
|
||||
implicitHeight: 16
|
||||
radius: 8
|
||||
color: Theme.tagChipBg
|
||||
Label {
|
||||
id: countLabel
|
||||
anchors.centerIn: parent
|
||||
text: "" // можно показать число детей, но пока стрелка
|
||||
font.pixelSize: 9
|
||||
color: Theme.textMuted
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: hover }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
onPressed: {
|
||||
treeView.currentIndex = index
|
||||
}
|
||||
onPressed: treeView.currentIndex = index
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
rowMenu.x = mouse.x; rowMenu.y = mouse.y
|
||||
rowMenu.openFor(path, isDir)
|
||||
}
|
||||
}
|
||||
onDoubleClicked: (mouse) => {
|
||||
if (mouse.button !== Qt.LeftButton) return
|
||||
if (isDir)
|
||||
fileStore.toggleExpand(index)
|
||||
else
|
||||
root.openFileRequested(path)
|
||||
if (isDir) fileStore.toggleExpand(index)
|
||||
else root.openFileRequested(path)
|
||||
}
|
||||
onPressAndHold: (mouse) => {
|
||||
if (mouse.button !== Qt.LeftButton || rowRect.dragPath.length > 0) return
|
||||
@@ -169,20 +265,15 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Перетаскивание элемента
|
||||
Drag.active: rowRect.dragging
|
||||
Drag.source: rowRect
|
||||
Drag.hotSpot: Qt.point(width / 2, height / 2)
|
||||
Drag.keys: [ "application/x-flora" ]
|
||||
Drag.keys: ["application/x-flora"]
|
||||
Drag.onDragFinished: rowRect.finishDrag()
|
||||
Drag.onActiveChanged: if (!Drag.active) rowRect.finishDrag()
|
||||
|
||||
function finishDrag() {
|
||||
dragging = false
|
||||
dragPath = ""
|
||||
}
|
||||
function finishDrag() { dragging = false; dragPath = "" }
|
||||
|
||||
// Приём перетаскивания в папку
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onEntered: (drag) => { drag.accepted = rowRect.isDir }
|
||||
@@ -209,88 +300,92 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
visible: !fileStore.ready
|
||||
text: qsTr("Откройте хранилище,\nчтобы увидеть файлы")
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#bbb"
|
||||
spacing: 8
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "\uD83D\uDCC1"
|
||||
font.pixelSize: 32
|
||||
opacity: 0.5
|
||||
}
|
||||
Label {
|
||||
text: qsTr("Откройте хранилище,\nчтобы увидеть файлы")
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Theme.textMuted
|
||||
lineHeight: 1.2
|
||||
}
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Выбрать хранилище…")
|
||||
onClicked: root.openVaultRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
visible: fileStore.ready && treeView.count === 0
|
||||
text: root.filterText.length > 0 ? qsTr("Ничего не найдено") : qsTr("Папка пуста")
|
||||
color: Theme.textMuted
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопка хранилища внизу слева
|
||||
Button {
|
||||
id: vaultButton
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 6
|
||||
height: 36
|
||||
text: qsTr("▨ Хранилище…")
|
||||
onClicked: root.openVaultRequested()
|
||||
}
|
||||
|
||||
// Статус-строка
|
||||
property string statusText: ""
|
||||
Timer { id: statusTimer; interval: 4000; onTriggered: root.statusText = "" }
|
||||
function showStatus(text, ok) {
|
||||
root.statusText = (ok ? "✓ " : "✕ ") + text
|
||||
statusTimer.restart()
|
||||
}
|
||||
function showStatus(text, ok) { root.statusText = (ok ? "✓ " : "✕ ") + text; statusTimer.restart() }
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: vaultButton.top
|
||||
height: 24
|
||||
anchors.bottom: vaultButton.top
|
||||
anchors.bottomMargin: 6
|
||||
height: 28
|
||||
visible: statusText.length > 0
|
||||
color: statusText.startsWith("✓") ? "#e5f6e5" : "#fdeaea"
|
||||
color: statusText.startsWith("✓") ? Theme.successBg : Theme.errorBg
|
||||
border.color: statusText.startsWith("✓") ? Theme.successText : Theme.errorText
|
||||
radius: 6
|
||||
Label {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: root.statusText
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 11
|
||||
color: statusText.startsWith("✓") ? "#256029" : "#9c1c1c"
|
||||
color: statusText.startsWith("✓") ? Theme.successText : Theme.errorText
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Меню «+» (файл/папка) -----
|
||||
Menu {
|
||||
id: addMenu
|
||||
width: 150
|
||||
width: 180
|
||||
MenuItem {
|
||||
text: qsTr("Файл")
|
||||
onClicked: {
|
||||
nameDialog.dialogTitle = qsTr("Новый файл")
|
||||
nameDialog.actionLabel = qsTr("Создать")
|
||||
nameDialog.initialText = ""
|
||||
nameDialog.open()
|
||||
pendingCreate.kind = "file"
|
||||
}
|
||||
onClicked: { nameDialog.dialogTitle = qsTr("Новый файл"); nameDialog.actionLabel = qsTr("Создать"); nameDialog.initialText = ""; nameDialog.open(); pendingCreate.kind = "file" }
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Папка")
|
||||
onClicked: {
|
||||
nameDialog.dialogTitle = qsTr("Новая папка")
|
||||
nameDialog.actionLabel = qsTr("Создать")
|
||||
nameDialog.initialText = ""
|
||||
nameDialog.open()
|
||||
pendingCreate.kind = "folder"
|
||||
}
|
||||
onClicked: { nameDialog.dialogTitle = qsTr("Новая папка"); nameDialog.actionLabel = qsTr("Создать"); nameDialog.initialText = ""; nameDialog.open(); pendingCreate.kind = "folder" }
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Контекстное меню строки -----
|
||||
Menu {
|
||||
id: rowMenu
|
||||
property string targetPath: ""
|
||||
property bool targetIsDir: false
|
||||
|
||||
function openFor(path, isDir) {
|
||||
targetPath = path
|
||||
targetIsDir = isDir
|
||||
popup()
|
||||
}
|
||||
|
||||
function openFor(path, isDir) { targetPath = path; targetIsDir = isDir; popup() }
|
||||
MenuItem {
|
||||
text: qsTr("Открыть")
|
||||
onClicked: {
|
||||
@@ -302,71 +397,28 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Создать файл внутри")
|
||||
visible: rowMenu.targetIsDir
|
||||
onClicked: {
|
||||
nameDialog.dialogTitle = qsTr("Новый файл")
|
||||
nameDialog.actionLabel = qsTr("Создать")
|
||||
nameDialog.initialText = ""
|
||||
nameDialog.open()
|
||||
pendingCreate.kind = "file"
|
||||
pendingCreate.parent = rowMenu.targetPath
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Создать папку внутри")
|
||||
visible: rowMenu.targetIsDir
|
||||
onClicked: {
|
||||
nameDialog.dialogTitle = qsTr("Новая папка")
|
||||
nameDialog.actionLabel = qsTr("Создать")
|
||||
nameDialog.initialText = ""
|
||||
nameDialog.open()
|
||||
pendingCreate.kind = "folder"
|
||||
pendingCreate.parent = rowMenu.targetPath
|
||||
}
|
||||
}
|
||||
MenuItem { text: qsTr("Создать файл внутри"); visible: rowMenu.targetIsDir; onClicked: { nameDialog.dialogTitle = qsTr("Новый файл"); nameDialog.actionLabel = qsTr("Создать"); nameDialog.initialText = ""; nameDialog.open(); pendingCreate.kind = "file"; pendingCreate.parent = rowMenu.targetPath } }
|
||||
MenuItem { text: qsTr("Создать папку внутри"); visible: rowMenu.targetIsDir; onClicked: { nameDialog.dialogTitle = qsTr("Новая папка"); nameDialog.actionLabel = qsTr("Создать"); nameDialog.initialText = ""; nameDialog.open(); pendingCreate.kind = "folder"; pendingCreate.parent = rowMenu.targetPath } }
|
||||
MenuSeparator { }
|
||||
MenuItem {
|
||||
text: qsTr("Переименовать")
|
||||
onClicked: {
|
||||
renameTarget.path = rowMenu.targetPath
|
||||
nameDialog.dialogTitle = qsTr("Переименовать")
|
||||
nameDialog.actionLabel = qsTr("Переименовать")
|
||||
nameDialog.initialText = root.baseName(rowMenu.targetPath)
|
||||
nameDialog.open()
|
||||
pendingCreate.kind = "rename"
|
||||
}
|
||||
onClicked: { renameTarget.path = rowMenu.targetPath; nameDialog.dialogTitle = qsTr("Переименовать"); nameDialog.actionLabel = qsTr("Переименовать"); nameDialog.initialText = root.baseName(rowMenu.targetPath); nameDialog.open(); pendingCreate.kind = "rename" }
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Удалить")
|
||||
onClicked: {
|
||||
const p = rowMenu.targetPath
|
||||
const isDir = rowMenu.targetIsDir
|
||||
const p = rowMenu.targetPath; const isDir = rowMenu.targetIsDir
|
||||
deleteTarget.path = p
|
||||
deleteDialog.doubleConfirm = isDir && fileStore.itemHasChildren(rowIndexOf(p))
|
||||
if (isDir)
|
||||
deleteDialog.message = fileStore.itemHasChildren(rowIndexOf(p))
|
||||
? qsTr("Удалить папку «%1» со всем её содержимым?").arg(root.baseName(p))
|
||||
: qsTr("Удалить пустую папку «%1»?").arg(root.baseName(p))
|
||||
else
|
||||
deleteDialog.message = qsTr("Удалить файл «%1»?").arg(root.baseName(p))
|
||||
if (isDir) deleteDialog.message = fileStore.itemHasChildren(rowIndexOf(p)) ? qsTr("Удалить папку «%1» со всем её содержимым?").arg(root.baseName(p)) : qsTr("Удалить пустую папку «%1»?").arg(root.baseName(p))
|
||||
else deleteDialog.message = qsTr("Удалить файл «%1»?").arg(root.baseName(p))
|
||||
deleteDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rowIndexOf(p) {
|
||||
const n = fileStore.rowCount()
|
||||
for (let i = 0; i < n; ++i)
|
||||
if (fileStore.itemPath(i) === p) return i
|
||||
return -1
|
||||
}
|
||||
|
||||
function parentForCreate() {
|
||||
if (treeView.currentIndex >= 0 && fileStore.itemIsDir(treeView.currentIndex))
|
||||
return fileStore.itemPath(treeView.currentIndex)
|
||||
return fileStore.vault
|
||||
}
|
||||
function rowIndexOf(p) { const n = fileStore.rowCount(); for (let i=0;i<n;++i) if (fileStore.itemPath(i)===p) return i; return -1 }
|
||||
function parentForCreate() { if (treeView.currentIndex>=0 && fileStore.itemIsDir(treeView.currentIndex)) return fileStore.itemPath(treeView.currentIndex); return fileStore.vault }
|
||||
|
||||
property var renameTarget: ({ path: "" })
|
||||
property var deleteTarget: ({ path: "" })
|
||||
@@ -376,26 +428,13 @@ Item {
|
||||
id: nameDialog
|
||||
onConfirmed: (name) => {
|
||||
let err = ""
|
||||
const createIn = pendingCreate.parent.length > 0
|
||||
? pendingCreate.parent
|
||||
: parentForCreate()
|
||||
if (pendingCreate.kind === "file") {
|
||||
err = fileStore.createFile(createIn, name)
|
||||
} else if (pendingCreate.kind === "folder") {
|
||||
err = fileStore.createFolder(createIn, name)
|
||||
} else if (pendingCreate.kind === "rename") {
|
||||
err = fileStore.renameItem(renameTarget.path, name)
|
||||
}
|
||||
if (err.length > 0) showStatus(err, false)
|
||||
pendingCreate.parent = ""
|
||||
const createIn = pendingCreate.parent.length>0 ? pendingCreate.parent : parentForCreate()
|
||||
if (pendingCreate.kind==="file") err=fileStore.createFile(createIn,name)
|
||||
else if (pendingCreate.kind==="folder") err=fileStore.createFolder(createIn,name)
|
||||
else if (pendingCreate.kind==="rename") err=fileStore.renameItem(renameTarget.path,name)
|
||||
if (err.length>0) showStatus(err,false); else if (pendingCreate.kind!=="rename") showStatus(qsTr("Создано: %1").arg(name), true)
|
||||
pendingCreate.parent=""
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDialog {
|
||||
id: deleteDialog
|
||||
onConfirmed: {
|
||||
const err = fileStore.removeItem(deleteTarget.path)
|
||||
if (err.length > 0) showStatus(err, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfirmDialog { id: deleteDialog; onConfirmed: { const err=fileStore.removeItem(deleteTarget.path); if(err.length>0) showStatus(err,false); else showStatus(qsTr("Удалено"), true) } }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user