2026-08-17 00:05:20 +03:00
import QtQuick
import QtQuick . Controls
import QtQuick . Layouts
2026-09-01 19:25:07 +03:00
import Flora
2026-08-17 00:05:20 +03:00
2026-08-17 18:22:56 +03:00
Item {
2026-08-17 00:05:20 +03:00
id: root
2026-09-01 19:40:55 +03:00
signal historyRequested ( string path )
2026-08-18 00:27:27 +03:00
property bool sideBySide: false
2026-09-01 19:25:07 +03:00
property string viewMode: "edit" // edit, preview, live — per-tab, persisted
property bool _loadingViewMode : false
function loadViewModeForCurrentTab ( ) {
if ( documentManager . activeIndex < 0 || ! fileStore . ready ) return
const path = documentManager . pathAt ( documentManager . activeIndex )
if ( ! path ) return
const saved = settingsStore . viewModeForFile ( fileStore . vault , path )
_loadingViewMode = true
viewMode = saved
_loadingViewMode = false
// обновить превью если нужно
if ( viewMode === "preview" ) {
const rendered = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
viewArea . text = rendered . trim ( ) . length === 0 ? "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Пустой файл — начните писать в режиме редактирования" ) + "</i></p>" : rendered
} else if ( viewMode === "live" ) {
const rendered = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
livePreview . text = rendered . trim ( ) . length === 0 ? "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Предпросмотр пуст" ) + "</i></p>" : rendered
}
}
function saveViewModeForCurrentTab ( ) {
if ( _loadingViewMode ) return
if ( documentManager . activeIndex < 0 || ! fileStore . ready ) return
const path = documentManager . pathAt ( documentManager . activeIndex )
if ( ! path ) return
settingsStore . setViewModeForFile ( fileStore . vault , path , viewMode )
}
onViewModeChanged: {
if ( _loadingViewMode ) return
saveViewModeForCurrentTab ( )
if ( viewMode === "preview" ) {
const rendered = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
if ( rendered . trim ( ) . length === 0 ) {
viewArea . text = "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Пустой файл — начните писать в режиме редактирования" ) + "</i></p>"
} else {
viewArea . text = rendered
}
} else if ( viewMode === "live" ) {
const r = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
livePreview . text = r . trim ( ) . length === 0 ? "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Предпросмотр пуст" ) + "</i></p>" : r
if ( livePreview . text . trim ( ) . length === 0 ) livePreview . text = "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Предпросмотр пуст" ) + "</i></p>"
}
}
2026-08-17 00:05:20 +03:00
2026-08-18 00:27:27 +03:00
function baseName ( p ) {
const i = p . lastIndexOf ( "/" )
return i >= 0 ? p . substring ( i + 1 ) : p
2026-08-17 12:34:03 +03:00
}
2026-09-01 19:25:07 +03:00
function dirName ( p ) {
const i = p . lastIndexOf ( "/" )
return i >= 0 ? p . substring ( 0 , i ) : ""
}
2026-08-17 18:15:22 +03:00
2026-09-01 19:25:07 +03:00
// --- Вкладки с перетаскиванием ---
2026-08-17 18:15:22 +03:00
Rectangle {
2026-08-18 00:27:27 +03:00
id: tabRow
2026-08-17 18:22:56 +03:00
anchors.top: parent . top
anchors.left: parent . left
anchors.right: parent . right
2026-09-01 19:25:07 +03:00
height: 38
color: Theme . bgAlt
border.color: Theme . divider
Rectangle { anchors.bottom: parent . bottom ; anchors.left: parent . left ; anchors.right: parent . right ; height: 1 ; color: Theme . divider }
2026-08-17 18:15:22 +03:00
2026-09-01 19:25:07 +03:00
ScrollView {
id: tabScroll
anchors.fill: parent
anchors.leftMargin: 6
anchors.rightMargin: 6
clip: true
ScrollBar.horizontal.policy: ScrollBar . AsNeeded
ScrollBar.vertical.policy: ScrollBar . AlwaysOff
Row {
id: tabRowLayout
spacing: 4
height: tabScroll . height
Repeater {
model: documentManager . openTabs
delegate: Rectangle {
id: tab
required property string modelData
required property int index
readonly property bool active: index === documentManager . activeIndex
y: 5
width: Math . max ( 120 , tabLabel . implicitWidth + 40 )
height: 28 ; radius: 6
color: active ? Theme.tabActiveBg : Theme . tabInactiveBg
border.color: active ? Theme.accent : ( dropArea . containsDrag ? Theme.accent : "transparent" )
border.width: active || dropArea . containsDrag ? 1 : 0
opacity: Drag . active ? 0.6 : 1.0
// Drag для переупорядочивания
Drag.active: dragHandler . active
Drag.source: tab
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: [ "tab" ]
Rectangle {
visible: active
anchors.bottom: parent . bottom
anchors.left: parent . left
anchors.right: parent . right
height: 2
color: Theme . accent
radius: 1
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 4
spacing: 4
Label {
id: tabLabel
Layout.fillWidth: true
text: root . baseName ( modelData ) + ( documentManager . isDirty ( index ) ? " •" : "" )
font.pixelSize: 12
color: tab . active ? Theme.text : Theme . textSecondary
elide: Text . ElideMiddle
}
ToolButton {
text: "\u2715"
implicitWidth: 20
implicitHeight: 20
font.pixelSize: 10
opacity: tab . active || hovered ? 0.9 : 0
visible: opacity > 0
onClicked: documentManager . closeTab ( index )
ToolTip.text: qsTr ( "Закрыть" )
ToolTip.visible: hovered
}
}
HoverHandler { id: hover }
// Клик — смена активной вкладки, средняя кнопка — закрыть
TapHandler {
id: tapHandler
acceptedButtons: Qt . LeftButton | Qt . MiddleButton
onTapped: ( eventPoint , button ) = > {
if ( button === Qt . MiddleButton ) documentManager . closeTab ( index )
else documentManager . activeIndex = index
}
}
// Перетаскивание вкладок
DragHandler {
id: dragHandler
target: null
acceptedButtons: Qt . LeftButton
}
DropArea {
id: dropArea
anchors.fill: parent
keys: [ "tab" ]
onEntered: ( drag ) = > {
if ( drag . source && drag . source !== tab ) drag . accepted = true
}
onDropped: ( drop ) = > {
if ( drop . source && drop . source . index !== undefined ) {
const from = drop . source . index
const to = index
if ( from !== to ) documentManager . moveTab ( from , to )
}
}
}
2026-08-17 18:15:22 +03:00
}
}
}
2026-09-01 19:25:07 +03:00
}
Label {
visible: documentManager . openTabs . length === 0
anchors.centerIn: parent
text: qsTr ( "Нет открытых файлов" )
color: Theme . textMuted
font.pixelSize: 12
font.italic: true
}
ToolButton {
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
anchors.rightMargin: 6
visible: false // старый sideBySide убран в пользу live
}
}
// Хлебные крошки
Rectangle {
id: crumbs
anchors.top: tabRow . bottom
anchors.left: parent . left
anchors.right: parent . right
height: visible ? 24 : 0
visible: documentManager . activeIndex >= 0
color: Theme . bg
Label {
anchors.fill: parent
anchors.leftMargin: 10
verticalAlignment: Text . AlignVCenter
text: {
const p = documentManager . pathAt ( documentManager . activeIndex )
if ( ! p ) return ""
const vault = fileStore . vault
let rel = p
if ( vault && p . startsWith ( vault ) ) rel = p . substring ( vault . length + 1 )
return rel
2026-08-17 18:15:22 +03:00
}
2026-09-01 19:25:07 +03:00
color: Theme . textMuted
font.pixelSize: 11
elide: Text . ElideMiddle
2026-08-17 12:34:03 +03:00
}
2026-09-01 19:25:07 +03:00
Rectangle { anchors.bottom: parent . bottom ; anchors.left: parent . left ; anchors.right: parent . right ; height: 1 ; color: Theme . divider ; visible: viewMode !== "live" }
2026-08-17 12:34:03 +03:00
}
2026-08-17 18:15:22 +03:00
FormatToolbar {
2026-08-18 00:27:27 +03:00
id: fmtBar
2026-09-01 19:25:07 +03:00
anchors.top: crumbs . bottom
anchors.left: parent . left
anchors.right: parent . right
height: 36
visible: documentManager . activeIndex >= 0 && viewMode !== "preview"
targetPane: root
2026-08-17 18:22:56 +03:00
onTableRequested: { tableEditor . resetDialog ( ) ; tableEditor . open ( ) }
2026-08-17 18:15:22 +03:00
}
2026-09-01 19:25:07 +03:00
// --- Контент ---
SplitView {
id: contentSplit
anchors.top: fmtBar . visible ? fmtBar.bottom : crumbs . bottom
2026-08-17 18:22:56 +03:00
anchors.left: parent . left
anchors.right: parent . right
2026-08-18 00:27:27 +03:00
anchors.bottom: toolbar . top
2026-09-01 19:25:07 +03:00
orientation: Qt . Horizontal
handle: Rectangle {
implicitWidth: viewMode === "live" ? 1 : 0
implicitHeight: viewMode === "live" ? 1 : 0
color: Theme . divider
visible: viewMode === "live"
}
// Редактор — виден в edit и live
ScrollView {
id: editorScroll
SplitView.fillWidth: true
SplitView.minimumWidth: 200
visible: viewMode !== "preview" && documentManager . activeIndex >= 0
clip: true
ScrollBar.vertical.policy: ScrollBar . AsNeeded
ScrollBar.horizontal.policy: ScrollBar . AsNeeded
TextArea {
id: textArea
placeholderText: qsTr ( "Начните писать…" )
placeholderTextColor: Theme . textFaint
color: Theme . editorText
selectionColor: Theme . selectionBg
selectedTextColor: Theme . text
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
font.pixelSize: 14
wrapMode: TextArea . NoWrap
selectByMouse: true
background: Rectangle { color: Theme . editorBg }
onTextChanged: {
if ( activeFocus && text !== documentManager . contentAt ( documentManager . activeIndex ) ) {
editDebounce . restart ( )
}
if ( viewMode === "live" ) liveDebounce . restart ( )
}
function insertText ( t ) { insert ( cursorPosition , t ) }
}
}
// Превью — виден в preview и live
ScrollView {
id: viewScroll
SplitView.fillWidth: true
SplitView.minimumWidth: 200
visible: viewMode !== "edit" && documentManager . activeIndex >= 0
clip: true
ScrollBar.vertical.policy: ScrollBar . AsNeeded
TextArea {
id: viewArea
visible: viewMode === "preview"
readOnly: true
textFormat: Text . RichText
color: Theme . text
selectionColor: Theme . selectionBg
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
font.pixelSize: 14
wrapMode: TextArea . Wrap
background: Rectangle { color: Theme . editorBg }
onLinkActivated: ( href ) = > handleLink ( href )
}
// Live preview отдельный TextArea чтобы не конфликтовать с обычным preview
TextArea {
id: livePreview
visible: viewMode === "live"
readOnly: true
textFormat: Text . RichText
color: Theme . text
selectionColor: Theme . selectionBg
font.family: "Menlo, Consolas, 'DejaVu Sans Mono', monospace"
font.pixelSize: 14
wrapMode: TextArea . Wrap
background: Rectangle { color: Theme . editorBg }
onLinkActivated: ( href ) = > handleLink ( href )
}
}
}
function handleLink ( href ) {
if ( href . startsWith ( "flora://copycode/" ) ) {
const id = href . substring ( 17 )
const code = mdParser . getCodeBlock ( id )
if ( code . length > 0 ) {
mdParser . copyToClipboard ( code )
copyFeedback . text = qsTr ( "Скопировано!" )
copyFeedback . visible = true
copyTimer . restart ( )
}
return
}
if ( href . startsWith ( "flora://task/" ) ) {
const line = Number ( href . substring ( 13 ) )
if ( line > 0 ) {
const lines = textArea . text . split ( "\n" )
const ln = lines [ line - 1 ]
if ( ln !== undefined ) {
if ( /\[ \]/ . test ( ln ) ) lines [ line - 1 ] = ln . replace ( "[ ]" , "[x]" )
else if ( /\[x\]/ . test ( ln ) ) lines [ line - 1 ] = ln . replace ( "[x]" , "[ ]" )
textArea . text = lines . join ( "\n" )
documentManager . updateContent ( documentManager . activeIndex , textArea . text )
if ( viewMode === "preview" ) viewArea . text = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
else if ( viewMode === "live" ) livePreview . text = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
}
}
} else if ( href . startsWith ( "flora://open/" ) ) {
documentManager . openDocument ( decodeURIComponent ( href . substring ( 13 ) ) )
} else if ( href . startsWith ( "flora://wiki/" ) ) {
const name = decodeURIComponent ( href . substring ( 12 ) )
const p = fileStore . vault + "/" + name + ".md"
documentManager . openDocument ( p )
} else {
Qt . openUrlExternally ( href )
}
}
// Тост "Скопировано"
Rectangle {
id: copyToast
anchors.top: contentSplit . top
anchors.horizontalCenter: parent . horizontalCenter
anchors.topMargin: 8
visible: copyFeedback . visible
color: "#333"
radius: 6
width: copyFeedback . implicitWidth + 16
height: copyFeedback . implicitHeight + 10
z: 10
Label {
id: copyFeedback
anchors.centerIn: parent
visible: false
text: qsTr ( "Скопировано!" )
color: "white"
font.pixelSize: 11
}
}
Timer { id: copyTimer ; interval: 1500 ; onTriggered: copyFeedback . visible = false }
// Пустое состояние
Column {
anchors.centerIn: contentSplit
visible: documentManager . activeIndex < 0
spacing: 12
Label { anchors.horizontalCenter: parent . horizontalCenter ; text: "\uD83D\uDCC4" ; font.pixelSize: 36 ; opacity: 0.6 }
Label { text: qsTr ( "Откройте файл из дерева слева" ) ; color: Theme . textMuted ; font.pixelSize: 13 }
Button {
anchors.horizontalCenter: parent . horizontalCenter
text: qsTr ( "Создать заметку" )
enabled: fileStore . ready
onClicked: {
const err = fileStore . createFile ( fileStore . vault , "Новая заметка" )
if ( err . length === 0 ) {
for ( let i = 0 ; i < fileStore . rowCount ( ) ; ++ i ) if ( fileStore . itemPath ( i ) . endsWith ( "Новая заметка.md" ) ) { documentManager . openDocument ( fileStore . itemPath ( i ) ) ; break }
}
}
}
}
Timer { id: editDebounce ; interval: 250 ; onTriggered: documentManager . updateContent ( documentManager . activeIndex , textArea . text ) }
Timer { id: liveDebounce ; interval: 150 ; onTriggered: { if ( viewMode === "live" ) livePreview . text = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) ) } }
property string gitStatusText: ""
2026-09-01 19:40:55 +03:00
property string lastModifiedStr: ""
2026-09-01 19:25:07 +03:00
function refreshGitStatus ( ) {
2026-09-01 19:40:55 +03:00
if ( documentManager . activeIndex < 0 || ! fileStore . ready ) { gitStatusText = "" ; lastModifiedStr = "" ; return }
2026-09-01 19:25:07 +03:00
const p = documentManager . pathAt ( documentManager . activeIndex )
2026-09-01 19:40:55 +03:00
if ( ! p ) { gitStatusText = "" ; lastModifiedStr = "" ; return }
2026-09-01 19:25:07 +03:00
try { gitStatusText = gitManager . fileGitStatus ( fileStore . vault , p ) } catch ( e ) { gitStatusText = "" }
2026-09-01 19:40:55 +03:00
try { lastModifiedStr = gitManager . lastModifiedTimeString ( fileStore . vault , p ) } catch ( e ) { lastModifiedStr = "" }
2026-09-01 19:25:07 +03:00
}
function loadContent ( ) {
const idx = documentManager . activeIndex
// загружаем режим для текущей вкладки перед обновлением содержимого
if ( idx >= 0 && fileStore . ready ) {
const path = documentManager . pathAt ( idx )
if ( path ) {
const saved = settingsStore . viewModeForFile ( fileStore . vault , path )
if ( saved !== viewMode ) {
_loadingViewMode = true
viewMode = saved
_loadingViewMode = false
}
}
}
if ( idx >= 0 ) {
const c = documentManager . contentAt ( idx )
if ( textArea . text !== c ) {
const pos = textArea . cursorPosition
textArea . text = c
textArea . cursorPosition = Math . min ( pos , c . length )
}
if ( viewMode === "preview" ) {
const rendered = mdParser . render ( c , fileStore . vault , activeFileDir ( ) )
viewArea . text = rendered . trim ( ) . length === 0 ? "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Пустой файл — начните писать в режиме редактирования" ) + "</i></p>" : rendered
} else if ( viewMode === "live" ) {
const rendered = mdParser . render ( c , fileStore . vault , activeFileDir ( ) )
livePreview . text = rendered . trim ( ) . length === 0 ? "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Предпросмотр пуст" ) + "</i></p>" : rendered
}
} else {
textArea . text = ""
viewArea . text = ""
livePreview . text = ""
}
refreshGitStatus ( )
}
Connections {
target: documentManager
function onActiveIndexChanged ( ) { root . loadContent ( ) }
function onContentChanged ( idx ) { if ( idx === documentManager . activeIndex && ! textArea . activeFocus ) root . loadContent ( ) ; refreshGitStatus ( ) }
function onOpenTabsChanged ( ) { root . loadContent ( ) }
function onDirtyFlagChanged ( ) { refreshGitStatus ( ) }
2026-09-01 19:40:55 +03:00
function onDocumentSaved ( idx ) {
refreshGitStatus ( )
// Авто-коммит для простого пользователя — каждый сохранённый файл попадает в историю
if ( fileStore . ready && idx >= 0 ) {
const p = documentManager . pathAt ( idx )
if ( p && p . length > 0 ) {
// Небольшая задержка чтобы не спамить на каждый автосейв
autoCommitDebounce . filePath = p
autoCommitDebounce . restart ( )
}
}
}
2026-09-01 19:25:07 +03:00
}
Connections { target: fileStore ; function onVaultChanged ( ) { refreshGitStatus ( ) ; loadContent ( ) } }
2026-09-01 19:40:55 +03:00
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 ( )
}
}
}
2026-09-01 19:25:07 +03:00
Component.onCompleted: loadContent ( )
function applyInline ( pre , post ) {
if ( viewMode === "preview" ) return
textArea . forceActiveFocus ( )
const s = textArea . selectionStart , e = textArea . selectionEnd
if ( s !== e ) {
const sel = textArea . selectedText
textArea . remove ( s , e )
textArea . insert ( s , pre + sel + post )
textArea . select ( s , s + pre . length + sel . length + post . length )
} else {
const pos = textArea . cursorPosition
textArea . insert ( pos , pre + post )
textArea . cursorPosition = pos + pre . length
}
documentManager . updateContent ( documentManager . activeIndex , textArea . text )
if ( viewMode === "live" ) liveDebounce . restart ( )
}
function applyHeading ( level ) {
if ( viewMode === "preview" ) return
textArea . forceActiveFocus ( )
const txt = textArea . text
const pos = textArea . cursorPosition
let start = txt . lastIndexOf ( "\n" , pos - 1 ) + 1
let end = txt . indexOf ( "\n" , pos )
if ( end < 0 ) end = txt . length
let line = txt . substring ( start , end )
line = line . replace ( /^#{1,6}\s*/ , "" )
const prefix = "#" . repeat ( level ) + " "
const newLine = prefix + line
textArea . remove ( start , end )
textArea . insert ( start , newLine )
textArea . cursorPosition = start + newLine . length
documentManager . updateContent ( documentManager . activeIndex , textArea . text )
}
function applyLinePrefix ( prefix ) {
if ( viewMode === "preview" ) return
textArea . forceActiveFocus ( )
const txt = textArea . text
const s = textArea . selectionStart , e = textArea . selectionEnd
let start = txt . lastIndexOf ( "\n" , s - 1 ) + 1
let finish = txt . indexOf ( "\n" , e )
if ( finish < 0 ) finish = txt . length
const block = txt . substring ( start , finish )
const lines = block . split ( "\n" )
const newBlock = lines . map ( l = > prefix + l ) . join ( "\n" )
textArea . remove ( start , finish )
textArea . insert ( start , newBlock )
textArea . select ( start , start + newBlock . length )
documentManager . updateContent ( documentManager . activeIndex , textArea . text )
}
function applyTaskToggle ( ) { applyLinePrefix ( "- [ ] " ) }
function applyCallout ( kind ) { applyLinePrefix ( "> [!" + kind + "] " ) }
function applyFloatWrap ( ) {
if ( viewMode === "preview" ) return
textArea . forceActiveFocus ( )
textArea . insert ( textArea . cursorPosition , "![[image.png|float:left|width:40%]]" )
documentManager . updateContent ( documentManager . activeIndex , textArea . text )
}
function applyColor ( color , highlightMode ) {
if ( viewMode === "preview" ) return
if ( highlightMode ) applyInline ( '<span style="background-color:' + color + '">' , '</span>' )
else applyInline ( '<span style="color:' + color + '">' , '</span>' )
}
function applySubSup ( kind ) {
if ( kind === "sup" ) applyInline ( "<sup>" , "</sup>" )
else applyInline ( "<sub>" , "</sub>" )
}
// --- Нижняя панель ---
2026-08-18 00:27:27 +03:00
Rectangle {
id: toolbar
anchors.bottom: parent . bottom ; anchors.left: parent . left ; anchors.right: parent . right
2026-09-01 19:25:07 +03:00
height: 36
color: Theme . toolbarBg
border.color: Theme . divider
2026-08-18 00:27:27 +03:00
RowLayout {
2026-09-01 19:25:07 +03:00
anchors.fill: parent ; anchors.leftMargin: 8 ; anchors.rightMargin: 8 ; spacing: 8
Label {
visible: documentManager . activeIndex >= 0
text: {
const t = textArea . text
const words = t . trim ( ) . length === 0 ? 0 : t . trim ( ) . split ( /\s+/ ) . length
const chars = t . length
return qsTr ( "%1 слов, %2 симв." ) . arg ( words ) . arg ( chars )
}
color: Theme . textMuted
font.pixelSize: 11
elide: Text . ElideRight
}
2026-08-18 00:27:27 +03:00
Item { Layout.fillWidth: true }
2026-09-01 19:25:07 +03:00
Row {
visible: documentManager . activeIndex >= 0
spacing: 8
Label {
text: documentManager . isDirty ( documentManager . activeIndex ) ? qsTr ( "● Редактируется" ) : qsTr ( "✓ Сохранено" )
color: documentManager . isDirty ( documentManager . activeIndex ) ? Theme.warningText : Theme . successText
font.pixelSize: 11
font.bold: documentManager . isDirty ( documentManager . activeIndex )
anchors.verticalCenter: parent . verticalCenter
}
Rectangle { width: 1 ; height: 12 ; color: Theme . divider ; anchors.verticalCenter: parent . verticalCenter ; visible: gitStatusText . length > 0 }
Label {
visible: gitStatusText . length > 0
text: "Git: " + gitStatusText
color: {
if ( gitStatusText === "чисто" ) return Theme . textMuted
if ( gitStatusText === "изменён" || gitStatusText === "неотслеживается" || gitStatusText === "новый" ) return Theme . warningText
if ( gitStatusText === "конфликт" ) return Theme . errorText
return Theme . textMuted
}
font.pixelSize: 11
anchors.verticalCenter: parent . verticalCenter
}
}
2026-09-01 19:40:55 +03:00
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 }
2026-09-01 19:25:07 +03:00
ButtonGroup { id: viewGroup }
ToolButton {
text: "✎"
checkable: true
checked: viewMode === "edit"
ButtonGroup.group: viewGroup
ToolTip.text: qsTr ( "Редактирование (Ctrl+E)" )
ToolTip.visible: hovered
onClicked: viewMode = "edit"
}
ToolButton {
text: "\u25A8"
checkable: true
checked: viewMode === "live"
ButtonGroup.group: viewGroup
ToolTip.text: qsTr ( "Живой просмотр (редактор | превью)" )
ToolTip.visible: hovered
onClicked: {
viewMode = "live"
livePreview . text = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
}
}
ToolButton {
text: "👁"
checkable: true
checked: viewMode === "preview"
ButtonGroup.group: viewGroup
ToolTip.text: qsTr ( "Просмотр (Ctrl+E)" )
ToolTip.visible: hovered
onClicked: {
const rendered = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
viewArea . text = rendered . trim ( ) . length === 0 ? "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Пустой файл" ) + "</i></p>" : rendered
viewMode = "preview"
}
}
2026-08-17 18:15:22 +03:00
}
2026-08-18 00:27:27 +03:00
}
2026-08-17 18:15:22 +03:00
2026-08-18 00:27:27 +03:00
function activeFileDir ( ) {
const p = documentManager . pathAt ( documentManager . activeIndex )
const i = p . lastIndexOf ( "/" )
return i >= 0 ? p . substring ( 0 , i ) : ""
2026-08-17 18:15:22 +03:00
}
2026-09-01 19:25:07 +03:00
TableEditor { id: tableEditor ; onMarkdownReady: ( md ) = > { textArea . insert ( textArea . cursorPosition , md ) ; documentManager . updateContent ( documentManager . activeIndex , textArea . text ) } }
2026-08-17 18:22:56 +03:00
2026-09-01 19:25:07 +03:00
Shortcut { sequence: "Ctrl+S" ; onActivated: { documentManager . saveAll ( ) ; refreshGitStatus ( ) } }
2026-08-17 18:22:56 +03:00
Shortcut { sequence: "Ctrl+W" ; onActivated: documentManager . closeTab ( documentManager . activeIndex ) }
2026-09-01 19:40:55 +03:00
Shortcut { sequence: "Ctrl+H" ; onActivated: {
const p = documentManager . pathAt ( documentManager . activeIndex )
if ( p ) root . historyRequested ( p )
} }
2026-09-01 19:25:07 +03:00
Shortcut { sequence: "Ctrl+E" ; onActivated: {
if ( viewMode === "edit" ) {
const rendered = mdParser . render ( textArea . text , fileStore . vault , activeFileDir ( ) )
viewArea . text = rendered . trim ( ) . length === 0 ? "<p style='color:" + Theme . textFaint + "'><i>" + qsTr ( "Пустой файл" ) + "</i></p>" : rendered
viewMode = "preview"
} else if ( viewMode === "preview" ) {
viewMode = "edit"
} else {
viewMode = "edit"
}
} }
Shortcut { sequence: "Ctrl+Tab" ; onActivated: { const n = documentManager . openTabs . length ; if ( n > 0 ) documentManager . activeIndex = ( documentManager . activeIndex + 1 ) % n } }
Shortcut { sequence: "Ctrl+Shift+Tab" ; onActivated: { const n = documentManager . openTabs . length ; if ( n > 0 ) documentManager . activeIndex = ( documentManager . activeIndex - 1 + n ) % n } }
}