440 lines
19 KiB
QML
440 lines
19 KiB
QML
|
|
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 }
|
||
|
|
}
|
||
|
|
}
|