Git история: откат файлов в любой момент (фаза 9) — UI для простого пользователя
- GitManager: fileHistory (список версий с hash/time/message/author), fileContentAt, fileDiffHtml/Text (git_patch_from_buffers, цветной HTML), restoreFile (запись + автокоммит), lastModifiedTime/String, autoCommit, ensureGitignore (.flora/.stfolder)
- HistoryDialog.qml: список версий (время «только что/5 мин назад» + дата dd.MM.yyyy, сообщение, автор, shortHash), diff с легендой (зелёный +/красный -/синий заголовок), превью содержимого, кнопка «Восстановить эту версию» с подтверждением (объясняет что создастся новый коммит и можно вернуться вперёд), «Обновить/Закрыть»
- MainWindow: меню Правка → История файла (Ctrl+H) и Вернуться к предыдущей версии, FileTree ПКМ → История, передача vaultPath/filePath
- EditorPanel: индикатор «🕘» + lastModifiedStr (gitManager.lastModifiedTimeString), debounce autoCommit (2с) при documentSaved, shortcut Ctrl+H
- FileTreePanel: сигнал historyRequested
- CMake: добавлен HistoryDialog.qml в qml модуль
- Тесты: git_history_test — создание v1/v2, история 1→2, diff, восстановление назад к v1 (история 3), вперёд к v2 (4), v3, diff v1→v3, bad hash, other.md не влияет на историю note.md, autoCommit, lastModified, .gitignore; 21/21 зелёных
- Проверено вперёд/назад: v1→v2→восстановление v1→восстановление v2→v3→autoCommit, каждый шаг с проверкой содержимого и истории
This commit is contained in:
+41
-1
@@ -80,7 +80,29 @@ ApplicationWindow {
|
||||
Action { text: qsTr("Повторить"); shortcut: "Ctrl+Shift+Z"; enabled: false }
|
||||
MenuSeparator { }
|
||||
Action {
|
||||
text: qsTr("Вернуться к предыдущей версии…"); enabled: false
|
||||
text: qsTr("История файла…")
|
||||
shortcut: "Ctrl+H"
|
||||
enabled: documentManager.activeIndex >= 0 && fileStore.ready
|
||||
onTriggered: {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (p.length>0) {
|
||||
historyDialog.vaultPath = fileStore.vault
|
||||
historyDialog.filePath = p
|
||||
historyDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
Action {
|
||||
text: qsTr("Вернуться к предыдущей версии…")
|
||||
enabled: documentManager.activeIndex >= 0 && fileStore.ready
|
||||
onTriggered: {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (p.length>0) {
|
||||
historyDialog.vaultPath = fileStore.vault
|
||||
historyDialog.filePath = p
|
||||
historyDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
@@ -134,6 +156,14 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
HistoryDialog {
|
||||
id: historyDialog
|
||||
onRestored: {
|
||||
// после восстановления обновляем граф и статус
|
||||
if (fileStore.ready) vaultIndex.rebuild(fileStore.vault)
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: vaultSwitcher
|
||||
x: 12
|
||||
@@ -238,6 +268,11 @@ ApplicationWindow {
|
||||
leftPanel.showStatus(qsTr("Предпросмотр HTML/PDF/изображений появится в Фазе 6-10"), false)
|
||||
}
|
||||
}
|
||||
onHistoryRequested: (path) => {
|
||||
historyDialog.vaultPath = fileStore.vault
|
||||
historyDialog.filePath = path
|
||||
historyDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
EditorPanel {
|
||||
@@ -245,6 +280,11 @@ ApplicationWindow {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumWidth: 300
|
||||
height: window.height - (menuBar ? menuBar.height : 0)
|
||||
onHistoryRequested: (path) => {
|
||||
historyDialog.vaultPath = fileStore.vault
|
||||
historyDialog.filePath = path
|
||||
historyDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
RightPanel {
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Flora
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
property string vaultPath: ""
|
||||
property string filePath: ""
|
||||
property string fileName: {
|
||||
const p = filePath
|
||||
const i = p.lastIndexOf("/")
|
||||
return i >= 0 ? p.substring(i+1) : p
|
||||
}
|
||||
|
||||
signal restored()
|
||||
|
||||
title: qsTr("История: %1").arg(fileName.length>0 ? fileName : qsTr("файл"))
|
||||
modal: true
|
||||
width: 900
|
||||
height: 640
|
||||
anchors.centerIn: parent
|
||||
closePolicy: Dialog.NoAutoClose
|
||||
standardButtons: Dialog.NoButton
|
||||
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
|
||||
|
||||
property var historyModel: []
|
||||
property int selectedIndex: -1
|
||||
property string selectedHash: ""
|
||||
property string diffHtml: ""
|
||||
property bool busy: false
|
||||
|
||||
function reload() {
|
||||
if (vaultPath.length===0 || filePath.length===0) {
|
||||
historyModel = []
|
||||
diffHtml = "<p style='color:" + Theme.textFaint + "'>" + qsTr("Файл не выбран") + "</p>"
|
||||
return
|
||||
}
|
||||
busy = true
|
||||
// берём историю
|
||||
const hist = gitManager.fileHistory(vaultPath, filePath)
|
||||
historyModel = hist
|
||||
listView.model = hist
|
||||
if (hist.length>0) {
|
||||
selectedIndex = 0
|
||||
selectedHash = hist[0].hash
|
||||
updateDiff()
|
||||
} else {
|
||||
selectedIndex = -1
|
||||
selectedHash = ""
|
||||
const hasFile = filePath.length>0
|
||||
if (hasFile) {
|
||||
const st = gitManager.fileGitStatus(vaultPath, filePath)
|
||||
if (st === "неотслеживается" || st === "новый") {
|
||||
diffHtml = "<p style='color:" + Theme.warningText + "'>" + qsTr("История пуста — файл ещё не сохранён в git. Сохраните файл, и здесь появится первая версия.") + "</p>"
|
||||
} else {
|
||||
diffHtml = "<p style='color:" + Theme.textFaint + "'>" + qsTr("Нет версий для этого файла") + "</p>"
|
||||
}
|
||||
} else {
|
||||
diffHtml = ""
|
||||
}
|
||||
}
|
||||
busy = false
|
||||
}
|
||||
|
||||
function updateDiff() {
|
||||
if (selectedHash.length===0) {
|
||||
diffHtml = ""
|
||||
return
|
||||
}
|
||||
const html = gitManager.fileDiffHtml(vaultPath, filePath, selectedHash)
|
||||
if (html.length===0) diffHtml = "<p style='color:" + Theme.textFaint + "'>" + qsTr("Нет изменений") + "</p>"
|
||||
else diffHtml = html
|
||||
// также обновляем превью содержимого версии
|
||||
const content = gitManager.fileContentAt(vaultPath, filePath, selectedHash)
|
||||
previewArea.text = content
|
||||
}
|
||||
|
||||
onOpened: reload()
|
||||
onVaultPathChanged: if (visible) reload()
|
||||
onFilePathChanged: if (visible) reload()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Заголовок с информацией о файле
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: Theme.bgAlt
|
||||
radius: 8
|
||||
border.color: Theme.border
|
||||
height: infoColumn.implicitHeight + 16
|
||||
ColumnLayout {
|
||||
id: infoColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 4
|
||||
Label {
|
||||
text: root.filePath.length>0 ? root.filePath : qsTr("—")
|
||||
color: Theme.textMuted
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: 11
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: qsTr("Последнее изменение:")
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
text: {
|
||||
if (root.filePath.length===0) return "—"
|
||||
const s = gitManager.lastModifiedTimeString(root.vaultPath, root.filePath)
|
||||
return s.length>0 ? s : "—"
|
||||
}
|
||||
color: Theme.text
|
||||
font.pixelSize: 11
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: historyModel.length + " " + (historyModel.length===1 ? qsTr("версия") : (historyModel.length>=2 && historyModel.length<=4 ? qsTr("версии") : qsTr("версий")))
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 11
|
||||
}
|
||||
BusyIndicator { visible: root.busy; running: root.busy; implicitWidth: 16; implicitHeight: 16 }
|
||||
}
|
||||
Label {
|
||||
visible: historyModel.length===0
|
||||
text: qsTr("Подсказка: каждый раз когда вы сохраняете файл, Flora автоматически сохраняет версию. Здесь можно вернуться к любой из них.")
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 11
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
orientation: Qt.Horizontal
|
||||
handle: Rectangle { implicitWidth: 1; color: Theme.divider }
|
||||
|
||||
// Левая панель — список версий
|
||||
Rectangle {
|
||||
SplitView.preferredWidth: 280
|
||||
SplitView.minimumWidth: 200
|
||||
SplitView.maximumWidth: 380
|
||||
color: Theme.bg
|
||||
border.color: Theme.divider
|
||||
radius: 8
|
||||
clip: true
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
spacing: 6
|
||||
Label {
|
||||
text: qsTr("Версии")
|
||||
font.bold: true
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.divider }
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: ListView.view.width
|
||||
height: 72
|
||||
radius: 6
|
||||
color: index === root.selectedIndex ? Theme.surfaceSelected : (hover.hovered ? Theme.surfaceHover : "transparent")
|
||||
border.color: index === root.selectedIndex ? Theme.accent : "transparent"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 2
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: modelData.timeAgo || ""
|
||||
font.bold: true
|
||||
color: index === root.selectedIndex ? Theme.text : Theme.text
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: modelData.shortHash || ""
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 10
|
||||
font.family: "Menlo, monospace"
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: modelData.timeStr || ""
|
||||
color: Theme.textMuted
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Label {
|
||||
text: modelData.message || ""
|
||||
color: Theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Label {
|
||||
text: modelData.author || ""
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
HoverHandler { id: hover }
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
root.selectedIndex = index
|
||||
root.selectedHash = modelData.hash
|
||||
root.updateDiff()
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
visible: listView.count===0
|
||||
text: qsTr("Нет истории\nСохраните файл чтобы создать первую версию")
|
||||
color: Theme.textMuted
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Button {
|
||||
text: qsTr("Обновить")
|
||||
onClicked: root.reload()
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
visible: root.selectedHash.length>0
|
||||
text: qsTr("Выбрана: %1").arg(root.selectedHash.substring(0,7))
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 10
|
||||
font.family: "Menlo, monospace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Правая панель — diff и превью
|
||||
ColumnLayout {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumWidth: 300
|
||||
spacing: 0
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
TabButton { text: qsTr("Изменения") }
|
||||
TabButton { text: qsTr("Содержимое версии") }
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: tabBar.currentIndex
|
||||
|
||||
// Diff вкладка
|
||||
Rectangle {
|
||||
color: Theme.editorBg
|
||||
border.color: Theme.divider
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
TextArea {
|
||||
id: diffArea
|
||||
readOnly: true
|
||||
textFormat: Text.RichText
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.text
|
||||
selectionColor: Theme.selectionBg
|
||||
text: root.diffHtml
|
||||
background: Rectangle { color: Theme.editorBg }
|
||||
font.family: "Menlo, monospace"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preview вкладка
|
||||
Rectangle {
|
||||
color: Theme.editorBg
|
||||
border.color: Theme.divider
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
TextArea {
|
||||
id: previewArea
|
||||
readOnly: true
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.text
|
||||
selectionColor: Theme.selectionBg
|
||||
background: Rectangle { color: Theme.editorBg }
|
||||
font.family: "Menlo, monospace"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Легенда
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 6
|
||||
spacing: 12
|
||||
Row { spacing: 4; Rectangle { width:12; height:12; color:"#e6ffec"; border.color:"#1a7f37"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("добавлено"); color: Theme.textMuted; font.pixelSize:10 } }
|
||||
Row { spacing: 4; Rectangle { width:12; height:12; color:"#ffebe9"; border.color:"#cf222e"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("удалено"); color: Theme.textMuted; font.pixelSize:10 } }
|
||||
Row { spacing: 4; Rectangle { width:12; height:12; color:"#ddf4ff"; border.color:"#0969da"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("заголовок"); color: Theme.textMuted; font.pixelSize:10 } }
|
||||
Label {
|
||||
text: qsTr("Сравнение выбранной версии с текущим файлом")
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 10
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Нижняя панель кнопок
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Button {
|
||||
text: qsTr("Закрыть")
|
||||
onClicked: root.close()
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
visible: root.selectedHash.length>0
|
||||
text: qsTr("Выбрав версию, нажмите «Восстановить» чтобы вернуть файл к этому состоянию. Будет создан новый коммит, и вы сможете снова откатиться вперёд.")
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: 10
|
||||
wrapMode: Text.Wrap
|
||||
Layout.preferredWidth: 360
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Восстановить эту версию")
|
||||
enabled: root.selectedHash.length>0
|
||||
highlighted: true
|
||||
onClicked: confirmRestore.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Подтверждение восстановления
|
||||
Dialog {
|
||||
id: confirmRestore
|
||||
title: qsTr("Восстановить версию?")
|
||||
modal: true
|
||||
anchors.centerIn: parent
|
||||
width: 420
|
||||
standardButtons: Dialog.NoButton
|
||||
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
Label {
|
||||
text: qsTr("Файл будет заменён на выбранную версию и сохранён как новая версия. Вы сможете снова вернуться к текущей версии через историю.")
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
color: Theme.text
|
||||
}
|
||||
Label {
|
||||
text: qsTr("Версия: %1 (%2)").arg(root.selectedHash.substring(0,7)).arg(root.selectedIndex>=0 && historyModel.length>root.selectedIndex ? historyModel[root.selectedIndex].timeStr : "")
|
||||
color: Theme.textMuted
|
||||
font.pixelSize: 11
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Item { Layout.fillWidth: true }
|
||||
Button { text: qsTr("Отмена"); onClicked: confirmRestore.close() }
|
||||
Button {
|
||||
text: qsTr("Восстановить")
|
||||
highlighted: true
|
||||
onClicked: {
|
||||
confirmRestore.close()
|
||||
const err = gitManager.restoreFile(root.vaultPath, root.filePath, root.selectedHash)
|
||||
if (err.length===0) {
|
||||
// Перезагружаем документ в редакторе
|
||||
// Находим индекс вкладки и перезагружаем содержимое
|
||||
for (let i=0;i<documentManager.openTabs.length;++i) {
|
||||
if (documentManager.pathAt(i)===root.filePath) {
|
||||
// Принудительно перечитать с диска
|
||||
const content = gitManager.fileContentAt(root.vaultPath, root.filePath, root.selectedHash)
|
||||
// Но restore уже записал файл и закоммитил, поэтому просто переоткроем
|
||||
documentManager.closeTab(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
documentManager.openDocument(root.filePath)
|
||||
root.restored()
|
||||
root.close()
|
||||
} else {
|
||||
errorLabel.text = err
|
||||
errorPopup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: errorPopup
|
||||
title: qsTr("Ошибка")
|
||||
modal: true
|
||||
anchors.centerIn: parent
|
||||
standardButtons: Dialog.Ok
|
||||
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 10 }
|
||||
Label { id: errorLabel; color: Theme.errorText; wrapMode: Text.Wrap; width: 360 }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import Flora
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal historyRequested(string path)
|
||||
|
||||
property bool sideBySide: false
|
||||
property string viewMode: "edit" // edit, preview, live — per-tab, persisted
|
||||
property bool _loadingViewMode: false
|
||||
@@ -411,11 +413,13 @@ Item {
|
||||
Timer { id: liveDebounce; interval: 150; onTriggered: { if (viewMode === "live") livePreview.text = mdParser.render(textArea.text, fileStore.vault, activeFileDir()) } }
|
||||
|
||||
property string gitStatusText: ""
|
||||
property string lastModifiedStr: ""
|
||||
function refreshGitStatus() {
|
||||
if (documentManager.activeIndex < 0 || !fileStore.ready) { gitStatusText = ""; return }
|
||||
if (documentManager.activeIndex < 0 || !fileStore.ready) { gitStatusText = ""; lastModifiedStr = ""; return }
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (!p) { gitStatusText = ""; return }
|
||||
if (!p) { gitStatusText = ""; lastModifiedStr = ""; return }
|
||||
try { gitStatusText = gitManager.fileGitStatus(fileStore.vault, p) } catch(e) { gitStatusText = "" }
|
||||
try { lastModifiedStr = gitManager.lastModifiedTimeString(fileStore.vault, p) } catch(e) { lastModifiedStr = "" }
|
||||
}
|
||||
function loadContent() {
|
||||
const idx = documentManager.activeIndex
|
||||
@@ -458,10 +462,32 @@ Item {
|
||||
function onContentChanged(idx) { if (idx === documentManager.activeIndex && !textArea.activeFocus) root.loadContent(); refreshGitStatus() }
|
||||
function onOpenTabsChanged() { root.loadContent() }
|
||||
function onDirtyFlagChanged() { refreshGitStatus() }
|
||||
function onDocumentSaved() { 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections { target: fileStore; function onVaultChanged() { refreshGitStatus(); loadContent() } }
|
||||
Timer { id: gitPoll; interval: 2000; running: documentManager.activeIndex >= 0; repeat: true; onTriggered: refreshGitStatus() }
|
||||
Timer { id: gitPoll; interval: 3000; running: documentManager.activeIndex >= 0; repeat: true; onTriggered: refreshGitStatus() }
|
||||
Timer {
|
||||
id: autoCommitDebounce
|
||||
property string filePath: ""
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
if (fileStore.ready && filePath.length>0) {
|
||||
gitManager.autoCommit(fileStore.vault, filePath)
|
||||
refreshGitStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: loadContent()
|
||||
|
||||
function applyInline(pre, post) {
|
||||
@@ -578,7 +604,27 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
Item { width: 8 }
|
||||
ToolButton {
|
||||
text: "🕘"
|
||||
enabled: documentManager.activeIndex >= 0
|
||||
ToolTip.text: lastModifiedStr.length>0 ? qsTr("История — последнее изменение: %1").arg(lastModifiedStr) : qsTr("История файла (Ctrl+H)")
|
||||
ToolTip.visible: hovered
|
||||
onClicked: {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (p) root.historyRequested(p)
|
||||
}
|
||||
}
|
||||
Label {
|
||||
visible: lastModifiedStr.length>0 && documentManager.activeIndex>=0
|
||||
text: lastModifiedStr
|
||||
color: Theme.textMuted
|
||||
font.pixelSize: 10
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
ToolTip.text: qsTr("Последнее изменение")
|
||||
HoverHandler { id: lastModHover }
|
||||
ToolTip.visible: lastModHover.hovered
|
||||
}
|
||||
Item { width: 4 }
|
||||
ButtonGroup { id: viewGroup }
|
||||
ToolButton {
|
||||
text: "✎"
|
||||
@@ -627,6 +673,10 @@ Item {
|
||||
|
||||
Shortcut { sequence: "Ctrl+S"; onActivated: { documentManager.saveAll(); refreshGitStatus() } }
|
||||
Shortcut { sequence: "Ctrl+W"; onActivated: documentManager.closeTab(documentManager.activeIndex) }
|
||||
Shortcut { sequence: "Ctrl+H"; onActivated: {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (p) root.historyRequested(p)
|
||||
} }
|
||||
Shortcut { sequence: "Ctrl+E"; onActivated: {
|
||||
if (viewMode === "edit") {
|
||||
const rendered = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
|
||||
|
||||
@@ -8,6 +8,7 @@ Item {
|
||||
|
||||
signal openVaultRequested()
|
||||
signal openFileRequested(string path)
|
||||
signal historyRequested(string path)
|
||||
|
||||
function baseName(p) {
|
||||
const idx = p.lastIndexOf("/")
|
||||
@@ -415,6 +416,12 @@ Item {
|
||||
deleteDialog.open()
|
||||
}
|
||||
}
|
||||
MenuSeparator { visible: !rowMenu.targetIsDir }
|
||||
MenuItem {
|
||||
text: qsTr("История…")
|
||||
visible: !rowMenu.targetIsDir
|
||||
onClicked: root.historyRequested(rowMenu.targetPath)
|
||||
}
|
||||
}
|
||||
|
||||
function rowIndexOf(p) { const n = fileStore.rowCount(); for (let i=0;i<n;++i) if (fileStore.itemPath(i)===p) return i; return -1 }
|
||||
|
||||
Reference in New Issue
Block a user