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 string versionContent: "" property string currentContent: "" property bool busy: false function fileDirOf(p) { const i = p.lastIndexOf("/") return i >= 0 ? p.substring(0, i) : "" } // Ссылки внутри просмотра исторической версии: только чтение, без правок function handleHistoryLink(href) { if (href.startsWith("flora://copycode/")) { copyFeedback.text = qsTr("Скопировано!") copyFeedback.visible = true copyTimer.restart() return } if (href.startsWith("flora://task/")) { return // историческая версия — только чтение, галочки не переключаем } else if (href.startsWith("flora://open/")) { documentManager.openDocument(decodeURIComponent(href.substring(13))) } else if (href.startsWith("flora://wiki/")) { return // несуществующие заметки из истории не создаём } else { Qt.openUrlExternally(href) } } function reload() { if (vaultPath.length===0 || filePath.length===0) { historyModel = [] versionContent = "" currentContent = "" diffHtml = "

" + qsTr("Файл не выбран") + "

" 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 = "" versionContent = "" currentContent = "" const hasFile = filePath.length>0 if (hasFile) { const st = gitManager.fileGitStatus(vaultPath, filePath) if (st === "неотслеживается" || st === "новый") { diffHtml = "

" + qsTr("История пуста — файл ещё не сохранён в git. Сохраните файл, и здесь появится первая версия.") + "

" } else { diffHtml = "

" + qsTr("Нет версий для этого файла") + "

" } } else { diffHtml = "" } } busy = false } function updateDiff() { if (selectedHash.length===0) { diffHtml = "" versionContent = "" currentContent = "" return } // свежее содержимое с диска — diff всегда актуален, даже если файл правили при открытом диалоге currentContent = gitManager.workdirContent(filePath) const html = gitManager.fileDiffHtml(vaultPath, filePath, selectedHash) if (html.length===0) diffHtml = "

" + qsTr("Нет изменений") + "

" else diffHtml = html // содержимое версии — сырой markdown, отрисуется через MarkdownView как в «Просмотре», // с подсветкой отличий от текущего файла прямо там versionContent = gitManager.fileContentAt(vaultPath, filePath, selectedHash) } 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 } } } // Просмотр версии — как кнопка «Просмотр» в редакторе Rectangle { color: Theme.editorBg border.color: Theme.divider ScrollView { id: versionScroll anchors.fill: parent anchors.margins: 6 clip: true ScrollBar.vertical: ScrollBar {} Column { width: versionScroll.availableWidth MarkdownView { id: versionPreview width: parent.width markdown: root.versionContent compareMarkdown: root.currentContent vaultRoot: root.vaultPath fileDir: root.fileDirOf(root.filePath) emptyText: qsTr("Нет версии для просмотра") onLinkActivated: (href) => root.handleHistoryLink(href) } } } } } // Легенда — цвета отличий от текущего файла (обе вкладки) Row { Layout.fillWidth: true Layout.margins: 6 spacing: 12 Row { spacing: 4; Rectangle { width:12; height:12; color:"#b7f5c6"; 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:"#ffd6dd"; border.color:"#e5484d"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("удалено"); color: Theme.textMuted; font.pixelSize:10 } } Row { visible: tabBar.currentIndex === 0; 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: tabBar.currentIndex === 0 ? qsTr("Сравнение выбранной версии с текущим файлом") : 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