Инкрементальный индекс, пресеты автосейва, диалог Настройки
- VaultIndex: updateFile/removeFile/syncIncremental (mtime+size), полный рескан только на старте/смене/внешних изменениях; relink висячих [[ссылок]] при создании файла; FileStore.externalChanged для внешних правок - QML: сейвы и правки идут точечно, дерево — через syncIncremental - Настройки: пресеты safe/balanced/eco/manual (диск 2/5/10с/выкл, коммит после паузы 5/15/60с/вручную), диалог с +/-, шкалами и подсказкой; коммит только в простое, в manual — по Ctrl+S - Удалены highlightedCode, словный merge, fileDiffText/Html, 2-arg fileHistory; renderInner без побочек - Тесты: инкремент индекса, пресеты, autosaveEnabled, ~~~-блоки
This commit is contained in:
@@ -49,6 +49,15 @@ ApplicationWindow {
|
||||
// статус можно показать в UI, пока просто игнорируем
|
||||
}
|
||||
|
||||
function applyAutosavePreset() {
|
||||
if (settingsStore.autosaveEnabled) {
|
||||
documentManager.setAutosaveIntervalMs(settingsStore.autosaveDiskMs)
|
||||
documentManager.setAutosaveEnabled(true)
|
||||
} else {
|
||||
documentManager.setAutosaveEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (windowVault.length > 0) {
|
||||
fileStore.openVault(windowVault)
|
||||
@@ -58,6 +67,11 @@ ApplicationWindow {
|
||||
if (vaultManager.vaults.length === 0) {
|
||||
welcomeWizard.open()
|
||||
}
|
||||
applyAutosavePreset()
|
||||
}
|
||||
Connections {
|
||||
target: settingsStore
|
||||
function onAutosavePresetChanged() { applyAutosavePreset() }
|
||||
}
|
||||
|
||||
menuBar: MenuBar {
|
||||
@@ -72,6 +86,7 @@ ApplicationWindow {
|
||||
Action { text: qsTr("В PDF…"); enabled: false }
|
||||
}
|
||||
MenuSeparator { }
|
||||
Action { text: qsTr("Настройки…"); onTriggered: settingsDialog.open() }
|
||||
Action { text: qsTr("Выход"); onTriggered: window.close() }
|
||||
}
|
||||
Menu {
|
||||
@@ -164,6 +179,10 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDialog {
|
||||
id: settingsDialog
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: vaultSwitcher
|
||||
x: 12
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Flora
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
// Наведённый вариант (для живой подсказки), иначе — выбранный
|
||||
property string hoveredPreset: ""
|
||||
function presetById(id) {
|
||||
for (let i = 0; i < presets.length; ++i)
|
||||
if (presets[i].id === id) return presets[i]
|
||||
return presets[1]
|
||||
}
|
||||
property var shownPreset: presetById(hoveredPreset.length > 0 ? hoveredPreset : settingsStore.autosavePreset)
|
||||
|
||||
property var presets: [
|
||||
{
|
||||
id: "safe", name: qsTr("Максимум сохранности"),
|
||||
desc: qsTr("На диск — каждые 2 с, версия в историю — после 5 с паузы"),
|
||||
pros: [qsTr("Текст почти невозможно потерять"), qsTr("Подробная история — легко откатить любой момент")],
|
||||
cons: [qsTr("Больше коммитов при долгой печати"), qsTr("Чаще пишет на диск")],
|
||||
meters: [["disk", 5], ["commits", 4], ["load", 4]]
|
||||
},
|
||||
{
|
||||
id: "balanced", name: qsTr("Сбалансированный"),
|
||||
desc: qsTr("На диск — каждые 5 с, версия в историю — после 15 с паузы"),
|
||||
pros: [qsTr("Разумный баланс потерь и чистоты истории"), qsTr("Версии только после паузы — без мусора")],
|
||||
cons: [qsTr("До 5 секунд текста живёт только в памяти")],
|
||||
meters: [["disk", 3], ["commits", 3], ["load", 3]]
|
||||
},
|
||||
{
|
||||
id: "eco", name: qsTr("Экономный"),
|
||||
desc: qsTr("На диск — каждые 10 с, версия в историю — после минуты паузы"),
|
||||
pros: [qsTr("Минимум нагрузки — для слабых дисков и огромных хранилищ"), qsTr("История чистая, только вехи")],
|
||||
cons: [qsTr("До 10 секунд текста живёт только в памяти"), qsTr("Промежуточные мысли в историю не попадут")],
|
||||
meters: [["disk", 2], ["commits", 2], ["load", 2]]
|
||||
},
|
||||
{
|
||||
id: "manual", name: qsTr("Вручную"),
|
||||
desc: qsTr("Без таймеров: Ctrl+S сохраняет на диск и делает версию"),
|
||||
pros: [qsTr("Полный контроль, ноль фоновой работы")],
|
||||
cons: [qsTr("Забыли Ctrl+S — потеряли"), qsTr("История растёт только от ваших рук")],
|
||||
meters: [["disk", 1], ["commits", 1], ["load", 1]]
|
||||
}
|
||||
]
|
||||
|
||||
function meterName(key) {
|
||||
if (key === "disk") return qsTr("Запись на диск")
|
||||
if (key === "commits") return qsTr("Коммиты в историю")
|
||||
return qsTr("Фоновая нагрузка")
|
||||
}
|
||||
|
||||
title: qsTr("Настройки")
|
||||
modal: true
|
||||
width: 640
|
||||
height: 620
|
||||
anchors.centerIn: parent
|
||||
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
|
||||
standardButtons: Dialog.Close
|
||||
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Подсказка: как читать шкалу
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: Theme.bgAlt
|
||||
border.color: Theme.border
|
||||
radius: 8
|
||||
Layout.preferredHeight: hintLabel.implicitHeight + 16
|
||||
Label {
|
||||
id: hintLabel
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
text: qsTr("Как выбирать: чем меньше галочек — тем проще и легче решение. " +
|
||||
"Берите самое простое, которого вам хватает: обычно это «Сбалансированный», " +
|
||||
"для слабого железа или огромных хранилищ — «Экономный».")
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Автосохранение")
|
||||
font.bold: true
|
||||
color: Theme.text
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ButtonGroup { id: presetGroup }
|
||||
|
||||
Repeater {
|
||||
model: root.presets
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
radius: 8
|
||||
color: modelData.id === settingsStore.autosavePreset ? Theme.surfaceSelected
|
||||
: (rowHover.hovered ? Theme.surfaceHover : Theme.bgAlt)
|
||||
border.color: modelData.id === settingsStore.autosavePreset ? Theme.accent : Theme.border
|
||||
border.width: modelData.id === settingsStore.autosavePreset ? 1 : 0
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
RadioButton {
|
||||
checked: modelData.id === settingsStore.autosavePreset
|
||||
ButtonGroup.group: presetGroup
|
||||
onClicked: settingsStore.autosavePreset = modelData.id
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
Label {
|
||||
text: modelData.name
|
||||
font.bold: true
|
||||
color: Theme.text
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Label {
|
||||
text: modelData.desc
|
||||
color: Theme.textMuted
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
HoverHandler {
|
||||
id: rowHover
|
||||
onHoveredChanged: {
|
||||
if (hovered) root.hoveredPreset = modelData.id
|
||||
else if (root.hoveredPreset === modelData.id) root.hoveredPreset = ""
|
||||
}
|
||||
}
|
||||
TapHandler {
|
||||
onTapped: settingsStore.autosavePreset = modelData.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Детали: плюсы/минусы + шкалы выбранного (или наведённого) варианта
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.bgAlt
|
||||
border.color: Theme.border
|
||||
radius: 8
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 6
|
||||
Label {
|
||||
text: root.shownPreset.name
|
||||
font.bold: true
|
||||
color: Theme.text
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Repeater {
|
||||
model: root.shownPreset.pros
|
||||
delegate: Label {
|
||||
required property string modelData
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.successText
|
||||
font.pixelSize: 11
|
||||
text: "+ " + modelData
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: root.shownPreset.cons
|
||||
delegate: Label {
|
||||
required property string modelData
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.errorText
|
||||
font.pixelSize: 11
|
||||
text: "− " + modelData
|
||||
}
|
||||
}
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
|
||||
Repeater {
|
||||
model: root.shownPreset.meters
|
||||
delegate: RowLayout {
|
||||
id: meterRow
|
||||
required property var modelData
|
||||
property string meterKey: modelData[0]
|
||||
property int meterLevel: modelData[1]
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Label {
|
||||
text: root.meterName(meterRow.meterKey)
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
Layout.preferredWidth: 140
|
||||
}
|
||||
Row {
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: 5
|
||||
Label {
|
||||
required property int index
|
||||
text: "✓"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: index < meterRow.meterLevel ? Theme.accent : Theme.textFaint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-13
@@ -782,23 +782,26 @@ Item {
|
||||
}
|
||||
refreshGitStatus()
|
||||
}
|
||||
function armAutoCommit(path) {
|
||||
// Коммит только после паузы в печати (idle), а не на каждое сохранение
|
||||
if (!settingsStore.autocommitEnabled || !fileStore.ready || !path || path.length === 0)
|
||||
return
|
||||
autoCommitDebounce.filePath = path
|
||||
autoCommitDebounce.restart()
|
||||
}
|
||||
Connections {
|
||||
target: documentManager
|
||||
function onActiveIndexChanged() { root.loadContent() }
|
||||
function onContentChanged(idx) { if (idx === documentManager.activeIndex && !textArea.activeFocus) root.loadContent(); refreshGitStatus() }
|
||||
function onContentChanged(idx) {
|
||||
if (idx === documentManager.activeIndex && !textArea.activeFocus) root.loadContent()
|
||||
refreshGitStatus()
|
||||
if (idx >= 0) root.armAutoCommit(documentManager.pathAt(idx))
|
||||
}
|
||||
function onOpenTabsChanged() { root.loadContent() }
|
||||
function onDirtyFlagChanged() { refreshGitStatus() }
|
||||
function onDocumentSaved(idx) {
|
||||
refreshGitStatus()
|
||||
// Авто-коммит для простого пользователя — каждый сохранённый файл попадает в историю
|
||||
if (fileStore.ready && idx >= 0) {
|
||||
const p = documentManager.pathAt(idx)
|
||||
if (p && p.length>0) {
|
||||
// Небольшая задержка чтобы не спамить на каждый автосейв
|
||||
autoCommitDebounce.filePath = p
|
||||
autoCommitDebounce.restart()
|
||||
}
|
||||
}
|
||||
if (idx >= 0) root.armAutoCommit(documentManager.pathAt(idx))
|
||||
}
|
||||
}
|
||||
Connections { target: fileStore; function onVaultChanged() { refreshGitStatus(); loadContent() } }
|
||||
@@ -806,9 +809,9 @@ Item {
|
||||
Timer {
|
||||
id: autoCommitDebounce
|
||||
property string filePath: ""
|
||||
interval: 2000
|
||||
interval: settingsStore.autocommitIdleMs
|
||||
onTriggered: {
|
||||
if (fileStore.ready && filePath.length>0) {
|
||||
if (settingsStore.autocommitEnabled && fileStore.ready && filePath.length>0) {
|
||||
gitManager.autoCommit(fileStore.vault, filePath)
|
||||
refreshGitStatus()
|
||||
}
|
||||
@@ -996,7 +999,16 @@ Item {
|
||||
|
||||
TableEditor { id: tableEditor; onMarkdownReady: (md) => { textArea.insert(textArea.cursorPosition, md); documentManager.updateContent(documentManager.activeIndex, textArea.text) } }
|
||||
|
||||
Shortcut { sequence: "Ctrl+S"; onActivated: { documentManager.saveAll(); refreshGitStatus() } }
|
||||
Shortcut { sequence: "Ctrl+S"; onActivated: {
|
||||
const dirty = []
|
||||
for (let i = 0; i < documentManager.openTabs.length; ++i)
|
||||
if (documentManager.isDirty(i)) dirty.push(documentManager.pathAt(i))
|
||||
documentManager.saveAll()
|
||||
// В ручном режиме Ctrl+S и сохраняет, и коммитит изменённые файлы
|
||||
if (!settingsStore.autosaveEnabled && fileStore.ready)
|
||||
for (const p of dirty) gitManager.autoCommit(fileStore.vault, p)
|
||||
refreshGitStatus()
|
||||
} }
|
||||
Shortcut { sequence: "Ctrl+W"; onActivated: documentManager.closeTab(documentManager.activeIndex) }
|
||||
Shortcut { sequence: "Ctrl+H"; onActivated: {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
|
||||
@@ -151,6 +151,7 @@ Item {
|
||||
if (drop.source && drop.source.dragPath && fileStore.ready) {
|
||||
const err = fileStore.moveItem(drop.source.dragPath, fileStore.vault)
|
||||
if (err.length > 0) showStatus(err, false)
|
||||
else vaultIndex.syncIncremental()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,6 +283,7 @@ Item {
|
||||
if (!rowRect.isDir || !drop.source || !drop.source.dragPath) return
|
||||
const err = fileStore.moveItem(drop.source.dragPath, rowRect.path)
|
||||
if (err.length > 0) root.showStatus(err, false)
|
||||
else vaultIndex.syncIncremental()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,9 +441,14 @@ Item {
|
||||
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)
|
||||
if (err.length>0) {
|
||||
showStatus(err,false)
|
||||
} else {
|
||||
if (pendingCreate.kind!=="rename") showStatus(qsTr("Создано: %1").arg(name), true)
|
||||
vaultIndex.syncIncremental()
|
||||
}
|
||||
pendingCreate.parent=""
|
||||
}
|
||||
}
|
||||
ConfirmDialog { id: deleteDialog; onConfirmed: { const err=fileStore.removeItem(deleteTarget.path); if(err.length>0) showStatus(err,false); else showStatus(qsTr("Удалено"), true) } }
|
||||
ConfirmDialog { id: deleteDialog; onConfirmed: { const err=fileStore.removeItem(deleteTarget.path); if(err.length>0) showStatus(err,false); else { showStatus(qsTr("Удалено"), true); vaultIndex.syncIncremental() } } }
|
||||
}
|
||||
|
||||
@@ -26,16 +26,31 @@ Item {
|
||||
target: fileStore
|
||||
function onVaultChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
|
||||
function onReadyChanged() { if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
|
||||
// Внешнее изменение (синхронизация/другая программа): досинхронизировать индекс
|
||||
// по mtime — дёшево, без полного рескана
|
||||
function onExternalChanged() { if (fileStore.ready) vaultIndex.syncIncremental() }
|
||||
}
|
||||
Connections {
|
||||
target: documentManager
|
||||
function onActiveIndexChanged() { root.refreshLinksTags() }
|
||||
function onOpenTabsChanged() { root.refreshLinksTags() }
|
||||
function onContentChanged(i) { if (i === documentManager.activeIndex) { root.refreshLinksTags(); rebuildDebounce.restart() } }
|
||||
function onDocumentSaved() { rebuildDebounce.restart() }
|
||||
function onContentChanged(i) { if (i === documentManager.activeIndex) { root.refreshLinksTags(); indexDebounce.restart() } }
|
||||
function onDocumentSaved(idx, path) {
|
||||
if (fileStore.ready && path && path.length > 0) vaultIndex.updateFile(path)
|
||||
}
|
||||
}
|
||||
Connections { target: vaultIndex; function onIndexChanged() { root.refreshLinksTags() } }
|
||||
Timer { id: rebuildDebounce; interval: 800; onTriggered: if (fileStore.ready) vaultIndex.rebuild(fileStore.vault) }
|
||||
// Правка активного файла: обновить только его (один парс), а не всё хранилище
|
||||
Timer {
|
||||
id: indexDebounce
|
||||
interval: 800
|
||||
onTriggered: {
|
||||
if (fileStore.ready && documentManager.activeIndex >= 0) {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (p && p.length > 0) vaultIndex.updateFile(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Делаем панели ресайзабельными внутри правой колонки — увеличенная зона по всей ширине
|
||||
SplitView {
|
||||
|
||||
Reference in New Issue
Block a user