From 047dcd2b2f097c2066b2920daa0a5382b2683740 Mon Sep 17 00:00:00 2001 From: liyepan Date: Thu, 22 Oct 2026 18:22:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.9.0=20=E2=80=94=E2=80=94=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E6=98=AF=20git=20=E4=BB=93=E5=BA=93=E6=97=B6?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=B8=B8=E7=94=A8=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 编辑器 - 语言下拉挪到代码框右下角,复制仍在右上角 Git - 打开文件夹时检测 git 仓库(rev-parse --show-toplevel),是的话侧栏底部 出现工具条,显示当前分支,按钮:拉取(pull --ff-only)、获取(fetch --all --prune)、 推送(push)、暂存(add -A)、提交(commit -m) - 侧栏文件右键:打开 / 添加到提交(git add -- file) / 忽略(并从远端删除) - 忽略流程:写入 .gitignore → git rm --cached → 提交 → 推送,执行前弹确认框 逐条列出命令;本地文件保留,远端删除 - 新增 GitRunner:QProcess 异步执行,MergedChannels 收集输出,90 秒超时 - 多步操作走队列,按顺序执行,失败即中止并弹出 git 原始输出 其它 - 新增 29 条中英文翻译 - README 增加 Git 一节,代码结构更新 - 版本 0.9.0 --- README.md | 40 +++- VoiletMark.pro | 4 +- src/GitRunner.cpp | 70 ++++++ src/GitRunner.h | 35 +++ src/MainWindow.cpp | 292 +++++++++++++++++++++++++ src/MainWindow.h | 36 ++++ src/MarkdownEditor.cpp | 2 +- translations/voiletmark_en.qm | Bin 6425 -> 9579 bytes translations/voiletmark_en.ts | 391 +++++++++++++++++++++++++--------- 9 files changed, 754 insertions(+), 116 deletions(-) create mode 100644 src/GitRunner.cpp create mode 100644 src/GitRunner.h diff --git a/README.md b/README.md index 48c967c..b82c0b4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。 -当前版本 **0.8.1**。 +当前版本 **0.9.0**。 ## 界面 @@ -45,6 +45,27 @@ - 每 5 秒自动扫描一次:目录里新增 / 删除的 Markdown 会自动出现在列表里 - 上次打开的文件夹会被记住,下次启动自动恢复 +### Git + +打开的文件夹如果是个 git 仓库,侧栏底部会出现一行 Git 工具条(显示当前分支): + +| 按钮 | 实际执行 | +| --- | --- | +| 拉取 | `git pull --ff-only` | +| 获取 | `git fetch --all --prune` | +| 推送 | `git push` | +| 暂存 | `git add -A` | +| 提交 | 填说明后 `git commit -m` | + +右键侧栏里的文件还有两项: + +- **添加到提交** → `git add -- ` +- **忽略(并从远端删除)** → 写进 `.gitignore` + `git rm --cached` + 提交 + 推送。 + **远端会真的删掉这个文件**,所以点之前会弹确认框,把要执行的命令逐条列出来; + 本地文件保留。 + +所有命令都是异步跑的,不会卡界面;失败时弹窗显示 git 的原始输出。 + ### 编辑模式 / 源代码模式 默认是**编辑模式**,按“写字”的排版来: @@ -195,16 +216,19 @@ VoiletMark --screenshot a.png # 渲染窗口截图后退出(调试 / 视觉 ``` src/ -├── main.cpp 入口:命令行解析、Fusion 样式、主题初始化 +├── main.cpp 入口:命令行解析、Fusion 样式、主题 / 语言初始化 ├── ThemeManager.{h,cpp} 四种主题模式 + 系统检测 + 调色板 / QSS 生成 -├── MainWindow.{h,cpp} 菜单、文件读写、文件夹侧栏、状态栏、设置 -├── MarkdownEditor.{h,cpp} 编辑器控件:行号、缩进、标题 / 代码块命令、源码模式 -├── MarkdownHighlighter.{h,cpp} Markdown 着色与就地渲染(隐藏标记、标题缩放) -├── MarkdownRenderer.{h,cpp} 导出用的 Markdown → 富文本文档转换 -└── ExportDialog.{h,cpp} PDF 预览与保存对话框 +├── LanguageManager.{h,cpp} 中英文切换 +├── MainWindow.{h,cpp} 菜单、文件读写、侧栏、Git 工具栏、状态栏、设置 +├── MarkdownEditor.{h,cpp} 编辑器控件:行号、缩进、代码框装饰、源码模式 +├── MarkdownHighlighter.{h,cpp} Markdown 着色与就地渲染 +├── CodeHighlight.{h,cpp} 多语言语法高亮 tokenizer +├── MarkdownRenderer.{h,cpp} 导出用的 Markdown → 富文本文档转换 +└── GitRunner.{h,cpp} 异步执行 git 命令 logo.png 图标源文件 -resources.qrc 窗口图标资源 +resources.qrc 窗口图标与翻译资源 +translations/ 中英文翻译(.ts / .qm) packaging/ 各平台图标(ico / icns / png)与 .desktop ``` diff --git a/VoiletMark.pro b/VoiletMark.pro index 3709601..825279a 100644 --- a/VoiletMark.pro +++ b/VoiletMark.pro @@ -1,6 +1,6 @@ TEMPLATE = app TARGET = VoiletMark -VERSION = 0.8.2 +VERSION = 0.9.0 QT += core gui widgets printsupport @@ -24,6 +24,7 @@ SOURCES += \ src/MarkdownHighlighter.cpp \ src/MarkdownRenderer.cpp \ src/CodeHighlight.cpp \ + src/GitRunner.cpp \ src/LanguageManager.cpp \ src/ThemeManager.cpp @@ -33,6 +34,7 @@ HEADERS += \ src/MarkdownHighlighter.h \ src/MarkdownRenderer.h \ src/CodeHighlight.h \ + src/GitRunner.h \ src/LanguageManager.h \ src/ThemeManager.h diff --git a/src/GitRunner.cpp b/src/GitRunner.cpp new file mode 100644 index 0000000..51bb4cf --- /dev/null +++ b/src/GitRunner.cpp @@ -0,0 +1,70 @@ +#include "GitRunner.h" + +#include +#include + +GitRunner::GitRunner(QObject *parent) + : QObject(parent) + , m_process(new QProcess(this)) + , m_timeout(new QTimer(this)) +{ + m_process->setProcessChannelMode(QProcess::MergedChannels); + + connect(m_process, &QProcess::readyReadStandardOutput, this, [this] { + m_output += QString::fromLocal8Bit(m_process->readAllStandardOutput()); + }); + + connect(m_process, QOverload::of(&QProcess::finished), this, + [this](int code, QProcess::ExitStatus status) { + report(code == 0 && status == QProcess::NormalExit); + }); + + connect(m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { + if (error == QProcess::FailedToStart) + report(false); + }); + + m_timeout->setSingleShot(true); + m_timeout->setInterval(90000); + connect(m_timeout, &QTimer::timeout, this, [this] { + if (m_process->state() == QProcess::NotRunning) + return; + m_output += tr("\n(超时,已中止)"); + m_process->kill(); + }); +} + +void GitRunner::setWorkingDirectory(const QString &directory) +{ + m_directory = directory; +} + +bool GitRunner::isBusy() const +{ + return m_process->state() != QProcess::NotRunning; +} + +void GitRunner::run(const QStringList &arguments, const QString &label) +{ + if (isBusy()) + return; + + m_label = label; + m_output.clear(); + m_reported = false; + + m_process->setWorkingDirectory(m_directory); + m_process->start(QStringLiteral("git"), arguments); + m_timeout->start(); + + emit started(label); +} + +void GitRunner::report(bool ok) +{ + if (m_reported) + return; + m_reported = true; + m_timeout->stop(); + emit finished(ok, m_label, m_output.trimmed()); +} diff --git a/src/GitRunner.h b/src/GitRunner.h new file mode 100644 index 0000000..1aa44b9 --- /dev/null +++ b/src/GitRunner.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +class QProcess; +class QTimer; + +/// 异步执行 git 命令,避免 pull / push 这种网络操作把界面卡住。 +class GitRunner : public QObject +{ + Q_OBJECT + +public: + explicit GitRunner(QObject *parent = nullptr); + + void setWorkingDirectory(const QString &directory); + bool isBusy() const; + + void run(const QStringList &arguments, const QString &label); + +signals: + void started(const QString &label); + void finished(bool ok, const QString &label, const QString &output); + +private: + void report(bool ok); + + QString m_directory; + QProcess *m_process = nullptr; + QTimer *m_timeout = nullptr; + QString m_label; + QString m_output; + bool m_reported = false; +}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e6c7a8b..0242e0a 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,5 +1,6 @@ #include "MainWindow.h" +#include "GitRunner.h" #include "LanguageManager.h" #include "MarkdownEditor.h" #include "MarkdownRenderer.h" @@ -16,7 +17,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -25,13 +28,16 @@ #include #include #include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -141,6 +147,9 @@ MainWindow::MainWindow(QWidget *parent) m_editor = new MarkdownEditor(this); setCentralWidget(m_editor); + m_git = new GitRunner(this); + connect(m_git, &GitRunner::finished, this, &MainWindow::onGitFinished); + createActions(); createSidebar(); createMenus(); @@ -298,8 +307,50 @@ void MainWindow::createSidebar() m_fileList->setUniformItemSizes(true); m_fileList->setTextElideMode(Qt::ElideMiddle); m_fileList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_fileList->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_fileList, 1); + // ---- Git 工具条:只有打开的是仓库时才显示 ---- + m_gitBar = new QWidget(content); + QGridLayout *gitLayout = new QGridLayout(m_gitBar); + gitLayout->setContentsMargins(0, 2, 0, 0); + gitLayout->setSpacing(4); + + m_gitStatusLabel = new QLabel(tr("Git 仓库"), m_gitBar); + m_gitStatusLabel->setForegroundRole(QPalette::PlaceholderText); + gitLayout->addWidget(m_gitStatusLabel, 0, 0, 1, 3); + + auto makeGitButton = [this](const QString &text, const QString &tip) { + QPushButton *button = new QPushButton(text, m_gitBar); + button->setToolTip(tip); + button->setCursor(Qt::PointingHandCursor); + button->setStyleSheet(QStringLiteral("padding: 3px 6px; min-width: 0;")); + return button; + }; + + m_gitPullButton = makeGitButton(tr("拉取"), tr("git pull --ff-only")); + m_gitFetchButton = makeGitButton(tr("获取"), tr("git fetch --all --prune")); + m_gitPushButton = makeGitButton(tr("推送"), tr("git push")); + m_gitStageButton = makeGitButton(tr("暂存"), tr("git add -A")); + m_gitCommitButton = makeGitButton(tr("提交"), tr("把暂存区的内容提交")); + + gitLayout->addWidget(m_gitPullButton, 1, 0); + gitLayout->addWidget(m_gitFetchButton, 1, 1); + gitLayout->addWidget(m_gitPushButton, 1, 2); + gitLayout->addWidget(m_gitStageButton, 2, 0); + gitLayout->addWidget(m_gitCommitButton, 2, 1, 1, 2); + + layout->addWidget(m_gitBar); + m_gitBar->setVisible(false); + + connect(m_gitPullButton, &QPushButton::clicked, this, &MainWindow::gitPull); + connect(m_gitFetchButton, &QPushButton::clicked, this, &MainWindow::gitFetch); + connect(m_gitPushButton, &QPushButton::clicked, this, &MainWindow::gitPush); + connect(m_gitStageButton, &QPushButton::clicked, this, &MainWindow::gitStageAll); + connect(m_gitCommitButton, &QPushButton::clicked, this, &MainWindow::gitCommit); + connect(m_fileList, &QListWidget::customContextMenuRequested, + this, &MainWindow::showFileContextMenu); + m_sidebar->setWidget(content); addDockWidget(Qt::LeftDockWidgetArea, m_sidebar); resizeDocks({m_sidebar}, {250}, Qt::Horizontal); @@ -470,6 +521,8 @@ void MainWindow::loadSettings() const bool sourceMode = settings.value(QStringLiteral("editor/sourceMode"), false).toBool(); setSourceModeEnabled(sourceMode); + + updateGitState(); } void MainWindow::saveSettings() @@ -921,6 +974,7 @@ void MainWindow::openFolder(const QString &path) m_lastDirectory = m_folderPath; QSettings().setValue(QStringLiteral("files/folder"), m_folderPath); refreshFolder(); + updateGitState(); m_sidebar->setVisible(true); m_sidebarAction->setChecked(true); @@ -1062,3 +1116,241 @@ void MainWindow::setSourceModeEnabled(bool enabled) QSettings().setValue(QStringLiteral("editor/sourceMode"), enabled); } + +// ---------------------------------------------------------------- Git + +void MainWindow::updateGitState() +{ + m_gitRoot.clear(); + m_gitBranch.clear(); + + if (!m_folderPath.isEmpty() && QFileInfo(m_folderPath).isDir()) { + QProcess process; + process.setWorkingDirectory(m_folderPath); + process.start(QStringLiteral("git"), + {QStringLiteral("rev-parse"), QStringLiteral("--show-toplevel")}); + if (process.waitForFinished(3000) && process.exitCode() == 0) + m_gitRoot = QString::fromLocal8Bit(process.readAllStandardOutput()).trimmed(); + } + + if (!m_gitRoot.isEmpty()) { + QProcess branch; + branch.setWorkingDirectory(m_gitRoot); + branch.start(QStringLiteral("git"), {QStringLiteral("rev-parse"), + QStringLiteral("--abbrev-ref"), + QStringLiteral("HEAD")}); + if (branch.waitForFinished(3000) && branch.exitCode() == 0) + m_gitBranch = QString::fromLocal8Bit(branch.readAllStandardOutput()).trimmed(); + } + + if (m_git) + m_git->setWorkingDirectory(m_gitRoot.isEmpty() ? m_folderPath : m_gitRoot); + + if (m_gitBar) { + m_gitBar->setVisible(!m_gitRoot.isEmpty()); + m_gitStatusLabel->setText(m_gitBranch.isEmpty() ? tr("Git 仓库") : m_gitBranch); + m_gitStatusLabel->setToolTip(m_gitRoot); + } +} + +void MainWindow::setGitBusy(bool busy) +{ + m_gitBusy = busy; + const QList buttons = {m_gitPullButton, m_gitFetchButton, m_gitPushButton, + m_gitStageButton, m_gitCommitButton}; + for (QPushButton *button : buttons) { + if (button) + button->setEnabled(!busy); + } +} + +void MainWindow::enqueueGit(const QStringList &arguments, const QString &label) +{ + GitStep step; + step.arguments = arguments; + step.label = label; + m_gitQueue.enqueue(step); + if (!m_gitBusy) + runNextGit(); +} + +void MainWindow::runNextGit() +{ + if (m_gitQueue.isEmpty()) { + setGitBusy(false); + statusBar()->clearMessage(); + return; + } + + const GitStep step = m_gitQueue.dequeue(); + setGitBusy(true); + statusBar()->showMessage(tr("正在%1…").arg(step.label)); + m_git->run(step.arguments, step.label); +} + +void MainWindow::onGitFinished(bool ok, const QString &label, const QString &output) +{ + if (!ok) { + m_gitQueue.clear(); + setGitBusy(false); + statusBar()->clearMessage(); + QMessageBox::warning(this, tr("Git 操作失败"), + tr("%1失败:\n\n%2") + .arg(label, output.isEmpty() ? tr("(没有输出)") : output)); + return; + } + + if (m_gitQueue.isEmpty()) { + setGitBusy(false); + statusBar()->showMessage(tr("%1完成").arg(label), 3000); + refreshFolder(); + } else { + runNextGit(); + } +} + +void MainWindow::gitPull() +{ + if (m_gitRoot.isEmpty()) + return; + enqueueGit({QStringLiteral("pull"), QStringLiteral("--ff-only")}, tr("拉取")); +} + +void MainWindow::gitFetch() +{ + if (m_gitRoot.isEmpty()) + return; + enqueueGit({QStringLiteral("fetch"), QStringLiteral("--all"), QStringLiteral("--prune")}, + tr("获取")); +} + +void MainWindow::gitPush() +{ + if (m_gitRoot.isEmpty()) + return; + enqueueGit({QStringLiteral("push")}, tr("推送")); +} + +void MainWindow::gitStageAll() +{ + if (m_gitRoot.isEmpty()) + return; + enqueueGit({QStringLiteral("add"), QStringLiteral("-A")}, tr("暂存")); +} + +void MainWindow::gitCommit() +{ + if (m_gitRoot.isEmpty()) + return; + + QProcess staged; + staged.setWorkingDirectory(m_gitRoot); + staged.start(QStringLiteral("git"), {QStringLiteral("diff"), QStringLiteral("--cached"), + QStringLiteral("--quiet")}); + if (staged.waitForFinished(5000) && staged.exitCode() == 0) { + QMessageBox::information(this, tr("提交"), + tr("暂存区是空的,先点「暂存」把所有改动加进去。")); + return; + } + + bool ok = false; + const QString message = QInputDialog::getMultiLineText( + this, tr("提交"), tr("提交说明:"), QString(), &ok); + if (!ok || message.trimmed().isEmpty()) + return; + + enqueueGit({QStringLiteral("commit"), QStringLiteral("-m"), message.trimmed()}, tr("提交")); +} + +void MainWindow::gitAddFile(const QString &path) +{ + if (m_gitRoot.isEmpty()) + return; + const QString relative = QDir(m_gitRoot).relativeFilePath(path); + enqueueGit({QStringLiteral("add"), QStringLiteral("--"), relative}, tr("添加到提交")); +} + +void MainWindow::gitIgnoreFile(const QString &path) +{ + if (m_gitRoot.isEmpty()) + return; + + const QString relative = QDir(m_gitRoot).relativeFilePath(path); + + QMessageBox box(QMessageBox::Warning, tr("忽略文件"), + tr("忽略「%1」?\n\n" + "会依次执行:\n" + "1. 把路径写进 .gitignore\n" + "2. git rm --cached(从仓库移除,本地文件保留)\n" + "3. 提交并推送到远端——远端也会删掉这个文件\n\n" + "其他人 pull 之后,本地的那份也会被删掉。") + .arg(relative), + QMessageBox::NoButton, this); + QPushButton *confirm = box.addButton(tr("忽略并删除远端"), QMessageBox::AcceptRole); + box.addButton(tr("取消"), QMessageBox::RejectRole); + box.exec(); + if (box.clickedButton() != confirm) + return; + + const QString ignorePath = QDir(m_gitRoot).filePath(QStringLiteral(".gitignore")); + QStringList lines; + QFile ignoreFile(ignorePath); + if (ignoreFile.open(QIODevice::ReadOnly | QIODevice::Text)) + lines = QString::fromUtf8(ignoreFile.readAll()).split(QLatin1Char('\n')); + ignoreFile.close(); + + bool listed = false; + for (const QString &line : lines) { + if (line.trimmed() == relative) { + listed = true; + break; + } + } + if (!listed) { + if (ignoreFile.open(QIODevice::Append | QIODevice::Text)) { + QTextStream stream(&ignoreFile); + stream << relative << "\n"; + ignoreFile.close(); + } + } + + bool tracked = false; + QProcess check; + check.setWorkingDirectory(m_gitRoot); + check.start(QStringLiteral("git"), {QStringLiteral("ls-files"), QStringLiteral("--error-unmatch"), + QStringLiteral("--"), relative}); + if (check.waitForFinished(5000)) + tracked = (check.exitCode() == 0); + + if (tracked) + enqueueGit({QStringLiteral("rm"), QStringLiteral("--cached"), QStringLiteral("--"), relative}, + tr("从仓库移除")); + enqueueGit({QStringLiteral("add"), QStringLiteral("--"), QStringLiteral(".gitignore")}, + tr("更新忽略列表")); + enqueueGit({QStringLiteral("commit"), QStringLiteral("-m"), + tr("chore: 忽略 %1").arg(relative)}, + tr("提交")); + enqueueGit({QStringLiteral("push")}, tr("推送")); +} + +void MainWindow::showFileContextMenu(const QPoint &pos) +{ + QListWidgetItem *item = m_fileList->itemAt(pos); + if (!item) + return; + const QString path = item->data(Qt::UserRole).toString(); + if (path.isEmpty()) + return; + + QMenu menu(this); + menu.addAction(tr("打开"), this, [this, path] { openPath(path); }); + + if (!m_gitRoot.isEmpty()) { + menu.addSeparator(); + menu.addAction(tr("添加到提交"), this, [this, path] { gitAddFile(path); }); + menu.addAction(tr("忽略(并从远端删除)"), this, [this, path] { gitIgnoreFile(path); }); + } + + menu.exec(m_fileList->viewport()->mapToGlobal(pos)); +} + diff --git a/src/MainWindow.h b/src/MainWindow.h index 2dc98b3..19a69bb 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,12 +1,15 @@ #pragma once #include +#include #include class QAction; class QActionGroup; class QDockWidget; class QLabel; +class QPushButton; +class GitRunner; class QListWidget; class QListWidgetItem; class QMenu; @@ -58,6 +61,21 @@ private: void setSourceModeEnabled(bool enabled); void exportPdf(); + // ---- Git ---- + void updateGitState(); + void showFileContextMenu(const QPoint &pos); + void gitAddFile(const QString &path); + void gitIgnoreFile(const QString &path); + void gitCommit(); + void gitPull(); + void gitFetch(); + void gitPush(); + void gitStageAll(); + void enqueueGit(const QStringList &arguments, const QString &label); + void runNextGit(); + void onGitFinished(bool ok, const QString &label, const QString &output); + void setGitBusy(bool busy); + void changeZoom(int direction); void resetZoom(); void setWordWrapEnabled(bool enabled); @@ -81,6 +99,24 @@ private: QLabel *m_folderLabel = nullptr; QToolButton *m_sourceButton = nullptr; QToolButton *m_sidebarButton = nullptr; + + GitRunner *m_git = nullptr; + QWidget *m_gitBar = nullptr; + QLabel *m_gitStatusLabel = nullptr; + QPushButton *m_gitPullButton = nullptr; + QPushButton *m_gitFetchButton = nullptr; + QPushButton *m_gitPushButton = nullptr; + QPushButton *m_gitStageButton = nullptr; + QPushButton *m_gitCommitButton = nullptr; + QString m_gitRoot; + QString m_gitBranch; + + struct GitStep { + QStringList arguments; + QString label; + }; + QQueue m_gitQueue; + bool m_gitBusy = false; QTimer *m_folderRefreshTimer = nullptr; QString m_folderPath; QString m_displayedFolder; diff --git a/src/MarkdownEditor.cpp b/src/MarkdownEditor.cpp index 8d7aa7b..c2a162c 100644 --- a/src/MarkdownEditor.cpp +++ b/src/MarkdownEditor.cpp @@ -594,7 +594,7 @@ QRectF MarkdownEditor::languageRect(int index) const const QString label = languageLabel(index) + QStringLiteral(" \u25be"); const qreal width = metrics.horizontalAdvance(label) + 16; const qreal height = metrics.height() + 6; - return QRectF(rect.left() + 8, rect.top() + 6, width, height); + return QRectF(rect.right() - width - 8, rect.bottom() - height - 6, width, height); } QString MarkdownEditor::languageLabel(int index) const diff --git a/translations/voiletmark_en.qm b/translations/voiletmark_en.qm index 1e47e45576d4139169cad25fad3e53aded0e9713..2c66465b7bed19e7a2ad429fadf9478439222b61 100644 GIT binary patch delta 3360 zcmaJ@3s6+&6+XKQ`?$O8jmTTUs}V&;s9>ud%``zX(l`^}NygZ*S$7v$0}H~Elt!~F z5#<#yUA2NZDrmGsM1i6_6dGHnnYK1HQ)b%Cw3)c~vNRL%g=r>Z>G$8edqF$X;c*}T zf4=jb?>zq7l>OD(gUMc98W}o#M4AsM|K~p=GQLRHZ+uLYGC|)=3=<`8r*D_H5+yyN z$1m%NggIO8lWh}35li)klO_;w>Q`<8 zP|{ZY)^}dT^@e`;d%K7X>-1mOZv}y3!|YFAB${>H@L+u+B74s8u;{PA*=f`WLA)1@ z>3>HcmQ}`*+)5&=!}#^O$57;yDdIOSC|JPhTfBcHO8%VN<7|RNd0e3N5P&}69(@mh ziGMTC+XgA4P3G5o5UD2DY&)}x$h^?J=O>>)2{M1SaS;gDnkQy_2*STLfA??>(F})W z=DMR$Y_;XH^i&Y8vq&fM@c#FfZ~UDg)E<@Q=|Ku!h${2$dJ{Nzquv`pM3JpglO4~& zP-oSr?j(v$w?6yR1u*!^+Bp9oMA|Cr7rQ`+TV@S3Acy9Q*0GI<)cm>i!Q2->bf5L% zyZe!nA6Wl=7s|vmTEE@37uOxu$JxWcd&~Ox+A`qnQLaL>)jRnHn8l9p^yg7r+jz}A zIF4)LwVKUDx;wn?nF1*Dn2%_?0>(3V(;JPrj_@Wso|6{vreUmeCf;&sJ)RHqGjF~G zsh9H!chZsCZG6&p5K2klQ*xl(Tpur7ZO8j+{#gxsKJVohmpw!Z*78f%yRp&FFWZ6~ zCs**xeQ=cgA;01}1WFm^w{)O*5*E9N<|XLxDxEk=BM-T#h@9jgfr_ad&x>(qqaAoE z#S>duWW;vT<1djLAFK}cR!P6?9UrV0FLwpvuXIQqt>c3=kdsWgF3+ZNx7%5&@jJBt zS~iZR&~{(HKZ?sYmrn>CwqvC#u>rPx{m`epJn3!_3cxch%O#qV&f61w_48A826gi ztC8rXCsH#{uG4w+yj+`GkVIB;0=g1PAD}VxgZ7946By{ta!j&z#b*~ z>8a-^TejUKr$QiaSfEllRF*S4U|8)aeV8VPAC`p?31>PiPuP7}Cf2iW!kQI6+g|}P z0! z2kV*eZbprTz*ZG?ie+_k$m^E*p(?TPHlk+2g$s_cw}B}$E5)h}{~b>xLy4&@1j{sY z{ZFKJV3xp3rdkBwDXn(fSD)(5y6t;^eDDOv1qPb~*N#i)Pf0argQx1*{K?7`q}tlx z6jaOjwjiMn;=yV0;*cBa6JxU>juW4GB0oDR&xr^*q7QEE9awp^5s90&~b z28NE09&W;bs4XlTt%X zaHuJ8rCmYdII*fXFmNQ$->>% za{TBi_i0G9QhQGlcyc0y>uKshN>MP81@J*46fz1i|K9k-cx9}+RT6rs#xD-~{@g$fH+o1Acwxh#0#{FH zcobr%9g|0(c~jW9S7uEq*s%jy8I9pnEqp$)dB>8@*q5w&-?xB)lN7lBPSu7`OWIdsV>O&uFxE!jS?)q4Zc1avQ)Yyka_!Er{ cOV6~=vS`%5)O89{1h4tU1O455Px# delta 663 zcmX9)T}V@57=F*U^Ru(Fov*n$Cge0+MI*?JG%7;9C{j|2Frp$&SN5-1S6NFdDvXqd zm6Z^QWg{o;Tog!QN#2xNiv*=IBGMEE|0x*uSm);DIq&zp&(CO1Lt0&8fMrr=*J~n2 zl;&nSi1hFDJL3V-q8y5ClZoPs1gCSJ$et=Bc1MX6zuWT^ERpRG zb4oXfv^3`C3Xz=2ibKDMoHtli&lbYR>MQ$*ti!Bl9DHe$sjNCc0dgHP)%7jMK1;0k z!H6~?h9uNjKmXW-vAt%pP9$< zVjwJ4#aD-s{FFt$_l_uT$a2=-fQE7{v#W4`@6(E!v(ZGi_Hp|PB9pAm*dIdhvDJ}x z8_o4w=YnmxpJb~jKMJF@whQeG20XU;){WRV=qdXH%kZF3ul0T<5*qbyys$nGQVpf5xZk^MwcTznxR>5atywOdNsmIW9Im2j9-s!dlEqt`-CH z5LY`&f!nzCX+DA`a7WKpH1>qMbH{zCY#sMxA&JYylV5~@{X9ijz}b9d#Wa!^@YMVh zsJx7)?<@zt#52xAATGk!cOsah5jDBO!B*3V;gOP=F`%4g!MEBQW5k-pjJ + + GitRunner + + + +(超时,已中止) + +(timed out, aborted) + + MainWindow - - + + 新建 New - - + + 打开… Open… - - + + 保存 Save - - + + 另存为… Save As… - - + + 导出 PDF… Export PDF… - - + + 退出 Quit - - + + 撤销 Undo - - + + 重做 Redo - - + + 剪切 Cut - - + + 复制 Copy - - + + 粘贴 Paste - - + + 全选 Select All - - + + 自动换行 Word Wrap - - + + 放大 Zoom In - - + + 缩小 Zoom Out - - + + 实际大小 Actual Size - - + + 打开文件夹… Open Folder… - - - - + + + + 侧栏 Sidebar - - + + 源代码模式 Source Mode - - + + 代码块 Code Block - - + + 标题 %1 Heading %1 - + 文件夹 Folder - - + + 未打开文件夹 No folder open - - + + + Git 仓库 + Git repository + + + + + 拉取 + Pull + + + + git pull --ff-only + git pull --ff-only + + + + + 获取 + Fetch + + + + git fetch --all --prune + git fetch --all --prune + + + + + + 推送 + Push + + + + git push + git push + + + + + 暂存 + Stage + + + + git add -A + git add -A + + + + + + + + 提交 + Commit + + + + 把暂存区的内容提交 + Commit the staged changes + + + + 文件 File - - + + 最近打开 Recent Files - - + + 编辑 Edit - - + + 视图 View - - + + 主题 Theme - - + + 6:00–18:00 使用浅色,其余时间使用深色 Light from 6:00 to 18:00, dark the rest of the day - - + + 语言 Language - - + + 格式 Format - - + + 标题 Heading - - + + 帮助 Help - - - + + + 关于 VoiletMark About VoiletMark - - + + 显示 / 隐藏文件夹侧栏(Ctrl+B) Show / hide the folder sidebar (Ctrl+B) - - + + 源代码模式:关闭语法高亮与行号(Ctrl+Shift+M) Source mode: turn off highlighting and line numbers (Ctrl+Shift+M) - - + + 未命名 Untitled - + 打开 Markdown 文件 Open Markdown File - + Markdown 文件 (*.md *.markdown *.mdown *.mkd);;文本文件 (*.txt);;所有文件 (*) Markdown files (*.md *.markdown *.mdown *.mkd);;Text files (*.txt);;All files (*) - + 无法打开文件 Cannot Open File - + 读取「%1」失败:%2 Could not read "%1": %2 - + 另存为 Save As - + Markdown 文件 (*.md);;文本文件 (*.txt);;所有文件 (*) Markdown files (*.md);;Text files (*.txt);;All files (*) - + 导出 PDF Export PDF - + PDF 文件 (*.pdf) PDF files (*.pdf) - + 导出失败 Export Failed - + 没能写出「%1」,换个位置再试一次。 Could not write "%1". Try another location. - + 已导出 %1 Exported to %1 - - - + + + 无法保存文件 Cannot Save File - + 写入「%1」失败:%2 Could not write "%1": %2 - + 写入「%1」时发生错误。 An error occurred while writing "%1". - + 提交「%1」失败:%2 Could not commit "%1": %2 - + 已保存 %1 Saved to %1 - + VoiletMark VoiletMark - + 「%1」有未保存的修改,是否保存? "%1" has unsaved changes. Save them? - + %1[*] — VoiletMark %1[*] — VoiletMark - + 行 %1,列 %2 Line %1, Col %2 - + 已选 %1 字 %1 selected - + %1 字 %1 chars - + <h3>VoiletMark %1</h3><p>一个轻量的跨平台 Markdown 编辑器。</p><p>基于 Qt %2 与 C++17 构建。</p> <h3>VoiletMark %1</h3><p>A lightweight cross-platform Markdown editor.</p><p>Built with Qt %2 and C++17.</p> - + (暂无记录) (none) - + 清空列表 Clear List - + 打开文件夹 Open Folder - + 文件 → 打开文件夹… File → Open Folder… - + (没有 Markdown 文件) (no Markdown files) + + + 正在%1… + %1… + + + + Git 操作失败 + Git Operation Failed + + + + %1失败: + +%2 + %1 failed: + +%2 + + + + (没有输出) + (no output) + + + + %1完成 + %1 finished + + + + 暂存区是空的,先点「暂存」把所有改动加进去。 + Nothing is staged yet. Click "Stage" first to add all changes. + + + + 提交说明: + Commit message: + + + + + 添加到提交 + Add to commit + + + + 忽略文件 + Ignore File + + + + 忽略「%1」? + +会依次执行: +1. 把路径写进 .gitignore +2. git rm --cached(从仓库移除,本地文件保留) +3. 提交并推送到远端——远端也会删掉这个文件 + +其他人 pull 之后,本地的那份也会被删掉。 + Ignore "%1"? + +This will: +1. Add the path to .gitignore +2. Run git rm --cached (removes it from the repo, keeps your local file) +3. Commit and push - the file is deleted on the remote too + +Anyone who pulls afterwards will lose their local copy. + + + + 忽略并删除远端 + Ignore and delete remote + + + + 取消 + Cancel + + + + 从仓库移除 + Remove from repository + + + + 更新忽略列表 + Update ignore list + + + + chore: 忽略 %1 + chore: ignore %1 + + + + 打开 + Open + + + + 忽略(并从远端删除) + Ignore (and delete from remote) + MarkdownEditor