417d0273b7
- 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, ~~~-блоки
225 lines
9.7 KiB
QML
225 lines
9.7 KiB
QML
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 }
|
||
}
|
||
}
|
||
}
|
||
}
|