diff --git a/src/deepin-image-viewer.qrc b/src/deepin-image-viewer.qrc index bf63ea6e..0f1353e4 100644 --- a/src/deepin-image-viewer.qrc +++ b/src/deepin-image-viewer.qrc @@ -34,5 +34,6 @@ qml/Utils/FloatingNotice.qml qml/Utils/ImageInputHandler.qml qml/Utils/RightMenuItem.qml + qml/Dialog/RemoveDialog.qml diff --git a/src/main.cpp b/src/main.cpp index bc72a4d6..fb6439d7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "src/imagedata/imageinfo.h" #include "src/imagedata/imagesourcemodel.h" #include "src/imagedata/imageprovider.h" +#include "src/utils/filetrashhelper.h" #include "config.h" #include @@ -57,6 +58,8 @@ int main(int argc, char *argv[]) qmlRegisterType(uri.toUtf8().data(), 1, 0, "ImageSourceModel"); qmlRegisterType(uri.toUtf8().data(), 1, 0, "MouseTrackItem"); qmlRegisterUncreatableType(uri.toUtf8().data(), 1, 0, "Types", "Types only use for define"); + // 文件回收站处理 + qmlRegisterType(uri.toUtf8().data(), 1, 0, "FileTrashHelper"); // QML全局单例 GlobalControl control; diff --git a/src/qml/Dialog/RemoveDialog.qml b/src/qml/Dialog/RemoveDialog.qml new file mode 100644 index 00000000..d93e5df3 --- /dev/null +++ b/src/qml/Dialog/RemoveDialog.qml @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.11 +import QtQuick.Layouts 1.11 +import org.deepin.dtk 1.0 + +DialogWindow { + id: dialog + + property alias fileName: textMetics.text + property int nameMaxWidth: 200 + + // 操作结束 true: 允许删除 false: 取消 + signal finished(bool ret) + + flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowStaysOnTopHint + height: 180 + maximumHeight: 180 + maximumWidth: 400 + minimumHeight: 180 + minimumWidth: 400 + modality: Qt.WindowModal + visible: false + width: 400 + + header: DialogTitleBar { + enableInWindowBlendBlur: true + + // 仅保留默认状态,否则 hover 上会有变化效果 + icon { + mode: DTK.NormalState + name: "user-trash-full-opened" + } + } + + Component.onCompleted: { + setX(window.x + window.width / 2 - width / 2); + setY(window.y + window.height / 2 - height / 2); + show(); + } + onClosing: { + finished(false); + } + + ColumnLayout { + height: parent.height + width: parent.width + + ColumnLayout { + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + Layout.maximumHeight: 80 + Layout.minimumHeight: 80 + Layout.preferredHeight: 80 + + Label { + id: notifyText + + property Palette textColor: Palette { + normal: ("black") + normalDark: ("white") + } + + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + color: ColorSelector.textColor + font: DTK.fontManager.t5 + horizontalAlignment: Text.AlignHCenter + text: qsTr("Cannot move \"%1\" to the trash. Do you want to permanently delete it?").arg(textMetics.elidedText) + wrapMode: Text.Wrap + + TextMetrics { + id: textMetics + + elide: Text.ElideMiddle + elideWidth: nameMaxWidth + font: notifyText.font + } + } + + Label { + id: messageText + + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + font: DTK.fontManager.t6 + text: qsTr("This action cannot be undone") + width: dialog.width + } + } + + Row { + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.fillHeight: true + spacing: 10 + + Button { + height: 36 + text: qsTr("Cancel") + width: 185 + + onClicked: { + finished(false); + } + } + + RecommandButton { + height: 36 + text: qsTr("Confirm") + width: 185 + + onClicked: { + finished(true); + } + } + } + } +} diff --git a/src/qml/ImageDelegate/DamagedImageDelegate.qml b/src/qml/ImageDelegate/DamagedImageDelegate.qml index 376a33e1..bd2b5c83 100644 --- a/src/qml/ImageDelegate/DamagedImageDelegate.qml +++ b/src/qml/ImageDelegate/DamagedImageDelegate.qml @@ -14,7 +14,6 @@ BaseImageDelegate { anchors.centerIn: parent height: 151 name: "photo_breach" - palette: DTK.makeIconPalette(parent.palette) theme: DTK.DTK.themeType width: 151 } diff --git a/src/qml/ReName.qml b/src/qml/ReName.qml index ba30de15..bdf4d721 100644 --- a/src/qml/ReName.qml +++ b/src/qml/ReName.qml @@ -94,7 +94,7 @@ DialogWindow { regExp: /^[^ \\.\\\\/\':\\*\\?\"<>|%&][^\\\\/\':\\*\\?\"<>|%&]*/ } - Keys.onPressed: event => { + Keys.onPressed: { switch (event.key) { case Qt.Key_Return: case Qt.Key_Enter: @@ -117,7 +117,6 @@ DialogWindow { Button { id: cancelbtn - font.pixelSize: 16 height: 36 text: qsTr("Cancel") width: 185 diff --git a/src/qml/ThumbnailListView.qml b/src/qml/ThumbnailListView.qml index 6d3a74f2..ca62e48b 100644 --- a/src/qml/ThumbnailListView.qml +++ b/src/qml/ThumbnailListView.qml @@ -8,6 +8,7 @@ import QtQuick.Layouts 1.11 import QtGraphicalEffects 1.0 import org.deepin.dtk 1.0 import org.deepin.image.viewer 1.0 as IV +import "./Dialog" Item { id: thumbnailView @@ -21,11 +22,31 @@ Item { property Image targetImage function deleteCurrentImage() { - if (!IV.FileControl.deleteImagePath(IV.GControl.currentSource)) { - // 取消删除文件 - return; + var trashFile = IV.GControl.currentSource; + if (!fileTrashHelper.fileCanTrash(trashFile)) { + // 无法移动到回收站,显示删除提示对话框 + removeDialogLoader.active = true; + } else { + deleteCurrentImageImpl(false); } - IV.GControl.removeImage(IV.GControl.currentSource); + } + + // 实际移除文件操作, directDelete:是否直接删除文件而不是移动到回收站(部分文件系统不支持) + function deleteCurrentImageImpl(directDelete) { + var trashFile = IV.GControl.currentSource; + if (directDelete) { + if (!fileTrashHelper.removeFile(trashFile)) { + return; + } + } else { + // 移动文件到回收站 + if (!fileTrashHelper.moveFileToTrash(trashFile)) { + return; + } + } + IV.GControl.removeImage(trashFile); + + // 删除最后图片,恢复到初始界面 if (0 === IV.GControl.imageCount) { stackView.switchOpenImage(); } @@ -45,6 +66,32 @@ Item { IV.GStatus.viewInteractive = true; } + // 用于文件移动至回收站/删除的辅助类 + IV.FileTrashHelper { + id: fileTrashHelper + + } + + // 删除确认对话框加载器 + Loader { + id: removeDialogLoader + + active: false + asynchronous: true + + sourceComponent: RemoveDialog { + fileName: IV.FileControl.slotGetFileNameSuffix(IV.GControl.currentSource) + + onFinished: { + if (ret) { + thumbnailView.deleteCurrentImageImpl(true); + } + // 使用后释放对话框 + removeDialogLoader.active = false; + } + } + } + Binding { delayed: true property: "thumbnailVaildWidth" diff --git a/src/src/filecontrol.cpp b/src/src/filecontrol.cpp index d8d9089b..f62d006c 100644 --- a/src/src/filecontrol.cpp +++ b/src/src/filecontrol.cpp @@ -88,7 +88,7 @@ FileControl::FileControl(QObject *parent) m_ocrInterface = new OcrInterface("com.deepin.Ocr", "/com/deepin/Ocr", QDBusConnection::sessionBus(), this); m_shortcutViewProcess = new QProcess(this); m_config = LibConfigSetter::instance(); - imageFileWatcher = new ImageFileWatcher(this); + imageFileWatcher = ImageFileWatcher::instance(); QObject::connect(imageFileWatcher, &ImageFileWatcher::imageFileChanged, this, &FileControl::imageFileChanged); diff --git a/src/src/imagedata/imagefilewatcher.cpp b/src/src/imagedata/imagefilewatcher.cpp index 24c09bce..6cb6f794 100644 --- a/src/src/imagedata/imagefilewatcher.cpp +++ b/src/src/imagedata/imagefilewatcher.cpp @@ -10,14 +10,19 @@ #include ImageFileWatcher::ImageFileWatcher(QObject *parent) - : QObject(parent) - , fileWatcher(new QFileSystemWatcher(this)) + : QObject(parent), fileWatcher(new QFileSystemWatcher(this)) { connect(fileWatcher, &QFileSystemWatcher::fileChanged, this, &ImageFileWatcher::onImageFileChanged); connect(fileWatcher, &QFileSystemWatcher::directoryChanged, this, &ImageFileWatcher::onImageDirChanged); } -ImageFileWatcher::~ImageFileWatcher() {} +ImageFileWatcher::~ImageFileWatcher() { } + +ImageFileWatcher *ImageFileWatcher::instance() +{ + static ImageFileWatcher ins; + return &ins; +} /** @brief 重置监控文件列表为 \a filePaths , 若监控的文件路重复,则不执行重置 diff --git a/src/src/imagedata/imagefilewatcher.h b/src/src/imagedata/imagefilewatcher.h index 85653f3c..1ae6b1aa 100644 --- a/src/src/imagedata/imagefilewatcher.h +++ b/src/src/imagedata/imagefilewatcher.h @@ -13,8 +13,7 @@ class ImageFileWatcher : public QObject { Q_OBJECT public: - explicit ImageFileWatcher(QObject *parent = nullptr); - ~ImageFileWatcher() override; + static ImageFileWatcher *instance(); void resetImageFiles(const QStringList &filePaths); void fileRename(const QString &oldPath, const QString &newPath); @@ -30,12 +29,17 @@ class ImageFileWatcher : public QObject // 当处理的图片文件夹变更(新增图片等) Q_SLOT void onImageDirChanged(const QString &dir); + explicit ImageFileWatcher(QObject *parent = nullptr); + ~ImageFileWatcher() override; + private: - QHash cacheFileInfo; ///< 缓存的图片信息,用于判断图片信息是否变更 QHash<完整路径, url信息> - QHash removedFile; ///< 缓存被移除的文件信息(FileWatcher在文件删除/移动后将不会继续观察) - QFileSystemWatcher *fileWatcher = nullptr; ///< 文件观察类,用于提示文件变更 + QHash cacheFileInfo; ///< 缓存的图片信息,用于判断图片信息是否变更 QHash<完整路径, url信息> + QHash removedFile; ///< 缓存被移除的文件信息(FileWatcher在文件删除/移动后将不会继续观察) + QFileSystemWatcher *fileWatcher = nullptr; ///< 文件观察类,用于提示文件变更 QString rotateImagePath; + + Q_DISABLE_COPY(ImageFileWatcher) }; -#endif // IMAGEFILEWATCHER_H +#endif // IMAGEFILEWATCHER_H diff --git a/src/src/utils/filetrashhelper.cpp b/src/src/utils/filetrashhelper.cpp new file mode 100644 index 00000000..e92e36a3 --- /dev/null +++ b/src/src/utils/filetrashhelper.cpp @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "filetrashhelper.h" +#include "imagedata/imagefilewatcher.h" +#include "unionimage/baseutils.h" + +#include +#include +#include +#include +#include +#include + +/*! + \brief 拷贝自 dde-file-manager 代码,看图中仅使用 kRemovable 访问可能被卸载的设备 + */ +enum DeviceQueryOption { + kNoCondition = 0, + kMountable = 1, + kMounted = 1 << 1, + kRemovable = 1 << 2, + kNotIgnored = 1 << 3, + kNotMounted = 1 << 4, + kOptical = 1 << 5, + kSystem = 1 << 6, + kLoop = 1 << 7, +}; + +/*! + \class FileTrashHelper::FileTrashHelper + \brief 文件回收站辅助类 + \details 用于判断文件是否可被移动到回收站中,以及移动文件到回收站 + \note 强关联文管DBus接口,需注意对应接口更新 + */ +FileTrashHelper::FileTrashHelper(QObject *parent) + : QObject(parent) +{ + // DFM 设备管理接口,访问文件挂载信息 + dfmDeviceManager.reset(new QDBusInterface(QStringLiteral("org.deepin.filemanager.server"), + QStringLiteral("/org/deepin/filemanager/server/DeviceManager"), + QStringLiteral("org.deepin.filemanager.server.DeviceManager"))); +} + +/*! + \return 返回文件 \a url 是否可被移动到回收站中 + */ +bool FileTrashHelper::fileCanTrash(const QUrl &url) +{ + // 检测当前文件和上次是否相同 + QDir currentDir(url.path()); + if (lastDir != currentDir) { + lastDir = currentDir; + resetMountInfo(); + } + + queryMountInfo(); + + if (isGvfsFile(url)) { + return false; + } + + if (isExternalDevice(url.path())) { + return false; + } + + return true; +} + +/*! + \return 移动文件 \a url 到回收站 + */ +bool FileTrashHelper::moveFileToTrash(const QUrl &url) +{ + // 优先采用文管后端 DBus 服务 + bool ret = moveFileToTrashWithDBus(url); + + if (!ret) { + qInfo() << qPrintable("Move file to trash DBus interface failed! Rollback to v20 version"); + // 备用 V20 接口 + ret = Libutils::base::trashFile(url.path()); + } + + if (!ret) { + qWarning() << qPrintable("Move file to trash failed:"); + } + + return ret; +} + +/*! + \return 返回删除文件 \a url 的结果 + */ +bool FileTrashHelper::removeFile(const QUrl &url) +{ + QFile file(url.path()); + bool ret = file.remove(); + if (!ret) { + qWarning() << qPrintable("Remove file failed:") << file.errorString(); + } + + return ret; +} + +/*! + \brief 重置文件挂载信息,仅在需要获取时重新取得挂载数据 + 对于看图应用的特殊处理,在打开新的图片后,仅需在首次触发查询挂载设备信息时查询, + 对于中途取消挂载的设备,文件监控会提示文件不存在,因此无需处理; + 同时,看图只会访问同一目录内容,挂载信息仅需查询单次,无需重复查询或监控设备 + */ +void FileTrashHelper::resetMountInfo() +{ + initData = false; + mountDevices.clear(); +} + +/*! + \brief 查询当前挂载设备信息 + \note 注意依赖文官接口的变动 + */ +void FileTrashHelper::queryMountInfo() +{ + if (initData) { + return; + } + + initData = true; + + // 调用 DBus 接口查询可被卸载设备信息 + // GetBlockDevicesIdList(int opts) + QDBusReply deviceListReply = dfmDeviceManager->call("GetBlockDevicesIdList", kRemovable); + if (!deviceListReply.isValid()) { + qWarning() << qPrintable("DBus call GetBlockDevicesIdList failed") << deviceListReply.error().message(); + return; + } + + for (const QString &id : deviceListReply.value()) { + // QueryBlockDeviceInfo(const QString &id, bool reload) + QDBusReply deviceReply = dfmDeviceManager->call("QueryBlockDeviceInfo", id, false); + if (!deviceReply.isValid()) { + qWarning() << qPrintable("DBus call QueryBlockDeviceInfo failed") << deviceReply.error().message(); + continue; + } + + const QVariantMap deviceInfo = deviceReply.value(); + if (QString("usb") == deviceInfo.value("ConnectionBus").toString()) { + const QStringList mountPaths = deviceInfo.value("MountPoints").toStringList(); + if (mountPaths.isEmpty()) { + const QString mountPath = deviceInfo.value("MountPoint").toString(); + + if (!mountPath.isEmpty()) { + mountDevices.insert(id, mountPath); + } + } else { + for (const QString &mount : mountPaths) { + mountDevices.insert(id, mount); + } + } + } + } +} + +/*! + \return 返回文件路径 \a path 是否指向外部设备 + */ +bool FileTrashHelper::isExternalDevice(const QString &path) +{ + for (auto itr = mountDevices.begin(); itr != mountDevices.end(); ++itr) { + if (path.startsWith(itr.value())) { + return true; + } + } + + return false; +} + +/*! + \return 判断 \a url 是否为远程挂载路径,此路径下同样无法恢复文件 + */ +bool FileTrashHelper::isGvfsFile(const QUrl &url) const +{ + if (!url.isValid()) + return false; + + const QString &path = url.toLocalFile(); + static const QString gvfsMatch { "(^/run/user/\\d+/gvfs/|^/root/.gvfs/|^/media/[\\s\\S]*/smbmounts)" }; + QRegularExpression re { gvfsMatch }; + QRegularExpressionMatch match { re.match(path) }; + return match.hasMatch(); +} + +/*! + \brief 通过后端文管接口将文件 \a url 移动到回收站 + 注意文管接口是异步接口且没有返回值,此处通过文件变更信号判断文件是否被移动。 + \return 移动文件到回收站是否成功 + */ +bool FileTrashHelper::moveFileToTrashWithDBus(const QUrl &url) +{ + if (!url.isValid()) { + return false; + } + + QStringList list; + list << url.toString(); + // 优先采用文管后端的DBus服务 + QDBusInterface interface(QStringLiteral("org.freedesktop.FileManager1"), + QStringLiteral("/org/freedesktop/FileManager1"), + QStringLiteral("org.freedesktop.FileManager1")); + // 默认超时时间大约25s, 修改为最大限制 + interface.setTimeout(INT_MAX); + auto pendingCall = interface.asyncCall("Trash", list); + + QEventLoop loop; + bool waitRet = false; + // 等待文件变更 + QString filePath = url.path(); + auto conn = QObject::connect(ImageFileWatcher::instance(), + &ImageFileWatcher::imageFileChanged, + this, + [&filePath, &loop, &waitRet](const QString &imagePath) { + // 删除信息未通过 DBus 返回,直接判断文件是否已被删除 + if ((imagePath == filePath) && !QFile::exists(filePath)) { + waitRet = true; + loop.quit(); + }; + }); + + // 等待最多200ms超时 + static const int overTime = 200; + QTimer::singleShot(overTime, &loop, &QEventLoop::quit); + loop.exec(); + + if (pendingCall.isError()) { + auto error = pendingCall.error(); + qWarning() << "Delete image by dbus error:" << error.name() << error.message(); + } + + QObject::disconnect(conn); + return waitRet; +} diff --git a/src/src/utils/filetrashhelper.h b/src/src/utils/filetrashhelper.h new file mode 100644 index 00000000..60532544 --- /dev/null +++ b/src/src/utils/filetrashhelper.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef FILETRASHHELPER_H +#define FILETRASHHELPER_H + +#include +#include +#include +#include + +// 文件移动至回收站 辅助类 +class FileTrashHelper : public QObject +{ + Q_OBJECT + +public: + explicit FileTrashHelper(QObject *parent = nullptr); + + Q_INVOKABLE bool fileCanTrash(const QUrl &url); + Q_INVOKABLE bool moveFileToTrash(const QUrl &url); + Q_INVOKABLE bool removeFile(const QUrl &url); + + Q_INVOKABLE void resetMountInfo(); + +private: + void queryMountInfo(); + bool isExternalDevice(const QString &path); + bool isGvfsFile(const QUrl &url) const; + + bool moveFileToTrashWithDBus(const QUrl &url); + +private: + QScopedPointer dfmDeviceManager; + + bool initData { false }; // 挂载数据是否被初始化 + QDir lastDir; // 上一次访问的文件目录 + QMultiHash mountDevices; // 当前挂载设备信息 +}; + +#endif // FILETRASHHELPER_H diff --git a/src/translations/deepin-image-viewer.ts b/src/translations/deepin-image-viewer.ts index 6e14d0b4..2e792924 100644 --- a/src/translations/deepin-image-viewer.ts +++ b/src/translations/deepin-image-viewer.ts @@ -264,6 +264,25 @@ Confirm + + RemoveDialog + + Cannot move "%1" to the trash. Do you want to permanently delete it? + Cannot move "%1" to the trash. Do you want to permanently delete it? + + + This action cannot be undone + This action cannot be undone + + + Cancel + Cancel + + + Confirm + Confirm + + SliderShow @@ -393,11 +412,11 @@ Version - Version + Version %1 is released under %2 - %1 is released under %2 + %1 is released under %2 Open image diff --git a/src/translations/deepin-image-viewer_bo.ts b/src/translations/deepin-image-viewer_bo.ts index 6b6983a8..35920089 100644 --- a/src/translations/deepin-image-viewer_bo.ts +++ b/src/translations/deepin-image-viewer_bo.ts @@ -262,6 +262,25 @@ གཏན་འཁེལ། + + RemoveDialog + + Cannot move "%1" to the trash. Do you want to permanently delete it? + “%1”ནང་དོན་སྙིགས་སྣོད་སུ་འཇོག་ཐབས་མེད་པས། ཁྱེད་ཀྱིས་རྦད་དེ་བསུབ་བམ། + + + This action cannot be undone + བཀོལ་སྤྱོད་འདི་བྱས་ཚེ་སོར་ཆུད་མི་ཐུབ། + + + Cancel + འདོར་བ། + + + Confirm + གཏན་འཁེལ། + + SliderShow @@ -391,11 +410,11 @@ Version - པར་གཞི། + པར་གཞི། %1 is released under %2 - %1བརྩི་སྲུང་། %2ཆིངས་ཡིག་ཁྱབ་བསྒྲགས། + %1བརྩི་སྲུང་། %2ཆིངས་ཡིག་ཁྱབ་བསྒྲགས། Open image diff --git a/src/translations/deepin-image-viewer_ug.ts b/src/translations/deepin-image-viewer_ug.ts index 5df73d39..c178a8ab 100644 --- a/src/translations/deepin-image-viewer_ug.ts +++ b/src/translations/deepin-image-viewer_ug.ts @@ -262,6 +262,25 @@ جەزملەش + + RemoveDialog + + Cannot move "%1" to the trash. Do you want to permanently delete it? + «%1» نى ئەخلەت ساندۇقىغا قويغىلى بولمىدى، تەلتۆكۈس ئۆچۈرەمسىز؟ + + + This action cannot be undone + بۇ مەشغۇلاتنى ئەسلىگە قايتۇرغىلى بولمايدۇ + + + Cancel + بىكار قىلىش + + + Confirm + جەزملەش + + SliderShow @@ -391,11 +410,11 @@ Version - نەشرى + نەشرى %1 is released under %2 - %1 كېلىشىم %2 گە ئەمەل قىلغان ئاساستا ئېلان قىلىندى + %1 كېلىشىم %2 گە ئەمەل قىلغان ئاساستا ئېلان قىلىندى Open image diff --git a/src/translations/deepin-image-viewer_zh_CN.ts b/src/translations/deepin-image-viewer_zh_CN.ts index acd79b55..587715fb 100644 --- a/src/translations/deepin-image-viewer_zh_CN.ts +++ b/src/translations/deepin-image-viewer_zh_CN.ts @@ -262,6 +262,25 @@ 确定 + + RemoveDialog + + Cannot move "%1" to the trash. Do you want to permanently delete it? + 无法将“%1”放到回收站,您要彻底删除吗? + + + This action cannot be undone + 此操作不可以恢复 + + + Cancel + 取消 + + + Confirm + 确定 + + SliderShow @@ -391,11 +410,11 @@ Version - 版本 + 版本 %1 is released under %2 - %1遵循%2协议发布 + %1遵循%2协议发布 Open image diff --git a/src/translations/deepin-image-viewer_zh_HK.ts b/src/translations/deepin-image-viewer_zh_HK.ts index 9cab323a..bb268471 100644 --- a/src/translations/deepin-image-viewer_zh_HK.ts +++ b/src/translations/deepin-image-viewer_zh_HK.ts @@ -262,6 +262,25 @@ 確定 + + RemoveDialog + + Cannot move "%1" to the trash. Do you want to permanently delete it? + 無法將“%1”放到回收站,您要徹底刪除嗎? + + + This action cannot be undone + 此操作不可以恢復 + + + Cancel + 取消 + + + Confirm + 確定 + + SliderShow @@ -391,11 +410,11 @@ Version - 版本 + 版本 %1 is released under %2 - %1遵循%2協議發佈 + %1遵循%2協議發佈 Open image diff --git a/src/translations/deepin-image-viewer_zh_TW.ts b/src/translations/deepin-image-viewer_zh_TW.ts index 8b25df97..2f7c9420 100644 --- a/src/translations/deepin-image-viewer_zh_TW.ts +++ b/src/translations/deepin-image-viewer_zh_TW.ts @@ -262,6 +262,25 @@ 確定 + + RemoveDialog + + Cannot move "%1" to the trash. Do you want to permanently delete it? + 無法將“%1”放到回收站,您要徹底刪除嗎? + + + This action cannot be undone + 此操作不可以復原 + + + Cancel + 取消 + + + Confirm + 確定 + + SliderShow @@ -391,11 +410,11 @@ Version - 版本 + 版本 %1 is released under %2 - %1遵循%2協議發布 + %1遵循%2協議發布 Open image