Repository: linuxdeepin/dde-file-manager-external
Pull request #16: feat(wallpapersetting): implement thumbnail caching for wallpapers an…
Author: login: pppanghu77
Created: 2025-04-23T05:52:05Z | Merged: 2025-04-24T01:31:08Z

…d screensavers

- Add thumbnail cache utils to calculate file hashes and get cache paths
- Integrate cache loading and saving in wallpaper and screensaver thumbnail generation
- Improve error handling and logging for thumbnail operations
- Optimize thumbnail generation process by checking cache first

Log: feat(wallpapersetting): implement thumbnail caching for wallpapers and screensavers
Bug: https://pms.uniontech.com/bug-view-293799.html

## Summary by Sourcery

Implement thumbnail caching mechanism for wallpapers and screensavers to improve performance and reduce redundant image processing

New Features:
- Add a new thumbnail caching utility to generate and manage cached thumbnails for wallpapers and screensavers

Enhancements:
- Optimize thumbnail generation by implementing cache lookup before image processing
- Improve error handling and logging for thumbnail operations

Chores:
- Create a new utility class ThumbnailCacheUtils to handle thumbnail caching operations


Diff:
diff --git a/src/dde-control-center-plugins/wallpapersetting/common/thumbnailcacheutils.cpp b/src/dde-control-center-plugins/wallpapersetting/common/thumbnailcacheutils.cpp
new file mode 100644
index 0000000..5b04f75
--- /dev/null
+++ b/src/dde-control-center-plugins/wallpapersetting/common/thumbnailcacheutils.cpp
@@ -0,0 +1,66 @@
+// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "thumbnailcacheutils.h"
+
+#include <QFile>
+#include <QIODevice>
+#include <QCryptographicHash>
+#include <QStandardPaths>
+#include <QDir>
+#include <QDebug> 
+
+namespace ThumbnailCacheUtils {
+
+QString calculateFileHash(const QString &filePath) {
+    QFile file(filePath);
+    if (!file.open(QIODevice::ReadOnly)) {
+        qWarning() << "Failed to open file for hashing:" << filePath;
+        return QString(); // 返回空 QString 表示失败
+    }
+    QCryptographicHash hash(QCryptographicHash::Md5);
+    if (!file.atEnd()) { // 优化：如果文件为空，不需要读取
+         constexpr qint64 bufferSize = 8192;
+         while (!file.atEnd()) {
+             hash.addData(file.read(bufferSize));
+         }
+    }
+    file.close();
+    return QString::fromLatin1(hash.result().toHex()); // 明确使用 fromLatin1
+}
+
+QString getThumbnailCachePath(const QString &originalPath, const QSize &size) {
+    // 优先使用 CacheLocation，如果不可写，尝试回退到 GenericCacheLocation
+    QString cacheBaseDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
+    if (cacheBaseDir.isEmpty()) {
+         cacheBaseDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
+         if(cacheBaseDir.isEmpty()){
+             qWarning() << "Cannot find writable cache location!";
+             // 在极端情况下，可能返回一个无效路径，调用者需要处理
+             return QString();
+         }
+    }
+
+    QString cacheDir = cacheBaseDir + QDir::separator() + THUMBNAIL_CACHE_DIR_NAME;
+    QDir dir; // 创建 QDir 对象来处理路径创建
+    if (!dir.exists(cacheDir)) {
+        if (!dir.mkpath(cacheDir)) {
+             qWarning() << "Failed to create thumbnail cache directory:" << cacheDir;
+             // 即使创建失败也继续，后续保存会失败，但尝试加载可能仍然有用（如果目录已存在但权限有问题）
+        }
+    }
+
+    QString hash = calculateFileHash(originalPath);
+    if (hash.isEmpty()) {
+        // 如果计算哈希失败，使用路径本身的哈希作为备选
+        qWarning() << "Failed to calculate file hash for:" << originalPath << "Using path hash as fallback.";
+        hash = QString::fromLatin1(QCryptographicHash::hash(originalPath.toUtf8(), QCryptographicHash::Md5).toHex());
+    }
+
+    // 使用 PNG 格式，文件名包含哈希和尺寸
+    QString fileName = QStringLiteral("%1_%2x%3.png").arg(hash).arg(size.width()).arg(size.height());
+    return QDir(cacheDir).absoluteFilePath(fileName); // 使用 QDir 确保路径正确
+}
+
+} // namespace ThumbnailCacheUtils
diff --git a/src/dde-control-center-plugins/wallpapersetting/common/thumbnailcacheutils.h b/src/dde-control-center-plugins/wallpapersetting/common/thumbnailcacheutils.h
new file mode 100644
index 0000000..5e56d82
--- /dev/null
+++ b/src/dde-control-center-plugins/wallpapersetting/common/thumbnailcacheutils.h
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef THUMBNAILCACHEUTILS_H
+#define THUMBNAILCACHEUTILS_H
+
+#include <QString>
+#include <QSize>
+
+namespace ThumbnailCacheUtils {
+
+// 缓存目录的名称
+const QString THUMBNAIL_CACHE_DIR_NAME = QStringLiteral("thumbnails");
+
+/**
+ * @brief 计算文件内容的 MD5 哈希值
+ * @param filePath 文件路径
+ * @return 文件的 MD5 哈希值 (十六进制字符串)，如果失败则返回空字符串
+ */
+QString calculateFileHash(const QString &filePath);
+
+/**
+ * @brief 获取缩略图的缓存文件路径
+ * @param originalPath 原始文件的路径
+ * @param size 缩略图的目标尺寸
+ * @return 完整的缓存文件路径
+ */
+QString getThumbnailCachePath(const QString &originalPath, const QSize &size);
+
+} // namespace ThumbnailCacheUtils
+
+#endif // THUMBNAILCACHEUTILS_H
diff --git a/src/dde-control-center-plugins/wallpapersetting/screensaver/screensaverprovider.cpp b/src/dde-control-center-plugins/wallpapersetting/screensaver/screensaverprovider.cpp
index 36e26f8..ad07dc4 100644
--- a/src/dde-control-center-plugins/wallpapersetting/screensaver/screensaverprovider.cpp
+++ b/src/dde-control-center-plugins/wallpapersetting/screensaver/screensaverprovider.cpp
@@ -10,6 +10,7 @@
 #include <QScreen>
 #include <QImageReader>
 #include <QPainter>
+#include "common/thumbnailcacheutils.h"            
 
 using namespace dfm_wallpapersetting;
 
@@ -81,21 +82,64 @@ void ScreensaverWorker::generateThumbnail(const QMap<QString, QString> &paths)
     const qreal ratio = qApp->primaryScreen()->devicePixelRatio();
     const int itemWidth = static_cast<int>(LISTVIEW_ICON_WIDTH * ratio);
     const int itemHeight = static_cast<int>(LISTVIEW_ICON_HEIGHT * ratio);
-    for (auto it = paths.begin(); it != paths.end(); ++it){
-        const QString realPath = it.value();
+    const QSize size(itemWidth, itemHeight); // 目标缩略图尺寸
+
+    for (auto it = paths.begin(); it != paths.end(); ++it) {
+        if (!running) // 检查是否需要提前终止
+            return;
+
+        const QString screensaverName = it.key();
+        const QString realPath = it.value(); // 原始封面图片路径
+
+        if (realPath.isEmpty()) {
+            qWarning() << "Skipping thumbnail generation for" << screensaverName << "due to empty path.";
+            continue;
+        }
+
+        QString cachePath = ThumbnailCacheUtils::getThumbnailCachePath(realPath, size);
+        QFileInfo cacheFile(cachePath);
+        QPixmap pix;
+
+        if (cacheFile.exists()) {
+            // 尝试从缓存加载
+            if (pix.load(cachePath)) {
+                pix.setDevicePixelRatio(ratio); // 设置DPI
+                emit pushThumbnail(screensaverName, pix);
+                continue; // 缓存命中，处理下一个
+            } else {
+                qWarning() << "Failed to load screensaver thumbnail from cache:" << cachePath;
+                // 缓存文件可能已损坏，尝试删除
+                QFile::remove(cachePath);
+            }
+        }
+
+        // --- 缓存未命中或加载失败，生成新的缩略图 ---
+
         QImage image(realPath);
-        QPixmap pix = QPixmap::fromImage(image.scaled(QSize(itemWidth, itemHeight),
-                                                      Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
-        const QRect r(0, 0, itemWidth, itemHeight);
-        const QSize size(itemWidth, itemHeight);
+        if (image.isNull()) {
+            qWarning() << "Failed to load screensaver cover image:" << realPath;
+            continue; // 加载失败，跳过此项
+        }
+
+        // 生成缩略图 (保持原有逻辑)
+        pix = QPixmap::fromImage(image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
+        const QRect r(0, 0, itemWidth, itemHeight); // 使用 size 变量
 
-        if (pix.width() > itemWidth || pix.height() > itemHeight)
+        if (pix.width() > itemWidth || pix.height() > itemHeight) {
             pix = pix.copy(QRect(pix.rect().center() - r.center(), size));
+        }
 
         if (!running)
             return;
 
-        emit pushThumbnail(it.key(), pix);
+        // 保存到缓存
+        if (!pix.save(cachePath, "PNG")) {
+            qWarning() << "Failed to save screensaver thumbnail to cache:" << cachePath;
+        }
+
+        // 设置DPI并发送信号
+        pix.setDevicePixelRatio(ratio);
+        emit pushThumbnail(screensaverName, pix);
     }
 }
 
diff --git a/src/dde-control-center-plugins/wallpapersetting/wallpaper/wallpaperprovider.cpp b/src/dde-control-center-plugins/wallpapersetting/wallpaper/wallpaperprovider.cpp
index 1862798..f29bd79 100644
--- a/src/dde-control-center-plugins/wallpapersetting/wallpaper/wallpaperprovider.cpp
+++ b/src/dde-control-center-plugins/wallpapersetting/wallpaper/wallpaperprovider.cpp
@@ -9,6 +9,7 @@
 #include <QScreen>
 #include <QImageReader>
 #include <QPainter>
+#include "common/thumbnailcacheutils.h"      
 
 using namespace dfm_wallpapersetting;
 
@@ -437,20 +438,42 @@ void InterfaceWorker::generateThumbnails(const QStringList &paths)
     const int itemWidth = static_cast<int>(LISTVIEW_ICON_WIDTH * ratio);
     const int itemHeight = static_cast<int>(LISTVIEW_ICON_HEIGHT * ratio);
     const QSize size(itemWidth, itemHeight);
-    for (const QString &path : paths){
+    for (const QString &path : paths) {
+        if (!running)
+            return;
+
+        QString cachePath = ThumbnailCacheUtils::getThumbnailCachePath(path, size);
+        QFileInfo cacheFile(cachePath);
+        QPixmap pix;
+        if (cacheFile.exists()) {
+            // 如果缓存存在，直接加载
+            if (pix.load(cachePath)) {
+                pix.setDevicePixelRatio(ratio);
+                emit pushThumbnail(path, true, QVariant::fromValue(pix));
+                continue; // 继续下一个
+            } else {
+                qWarning() << "Failed to load thumbnail from cache:" << cachePath;
+                // 缓存文件损坏,尝试删除
+                QFile::remove(cachePath);
+            }
+        }
+        // 如果缓存不存在，生成缩略图
         bool pic;
         QVariant val;
         if (generateThumbnail(path, size, pic, val)) {
             if (pic) {
-                auto pix = val.value<QPixmap>();
+                pix = val.value<QPixmap>();
                 pix.setDevicePixelRatio(ratio);
+                if (!pix.save(cachePath, "PNG")) {
+                   qWarning() << "Failed to save thumbnail to cache:" << cachePath;
+                }
                 emit pushThumbnail(path, true, QVariant::fromValue(pix));
+
             } else {
                 emit pushThumbnail(path, false, val);
             }
+        } else {
+            qWarning() << "Failed to generate thumbnail for path:" << path;
         }
-
-        if (!running)
-            return;
     }
 }


Reviews:
user: login: sourcery-ai[bot]
body: Hey @pppanghu77 - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a mechanism to invalidate the cache when the original wallpaper file is modified.
- The thumbnail cache directory name should be a constant defined in a header file.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T05:53:08Z
user: login: liyigang1
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T06:21:02Z
user: login: max-lvs
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T06:40:36Z
user: login: liyigang1
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T08:36:27Z

Review comments:
diff_hunk: @@ -81,21 +82,64 @@ void ScreensaverWorker::generateThumbnail(const QMap<QString, QString> &paths)
     const qreal ratio = qApp->primaryScreen()->devicePixelRatio();
     const int itemWidth = static_cast<int>(LISTVIEW_ICON_WIDTH * ratio);
     const int itemHeight = static_cast<int>(LISTVIEW_ICON_HEIGHT * ratio);
path: src/dde-control-center-plugins/wallpapersetting/screensaver/screensaverprovider.cpp
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the thumbnail caching and creation logic into a helper function to simplify the code and improve readability by reducing nested conditionals in the loop.

Extracting the caching and thumbnail creation into a helper function can reduce the nested conditionals and improve readability. For example, you could add a helper function like this:

```cpp
QPixmap loadOrCreateThumbnail(const QString &screensaverName,
                              const QString &realPath,
                              const QSize &size,
                              int itemWidth,
                              int itemHeight,
                              qreal ratio)
{
    QString cachePath = ThumbnailCacheUtils::getThumbnailCachePath(realPath, size);
    QFileInfo cacheFile(cachePath);
    QPixmap pix;

    if (cacheFile.exists()) {
        if (pix.load(cachePath)) {
            pix.setDevicePixelRatio(ratio);
            return pix;
        } else {
            qWarning() << "Failed to load screensaver thumbnail from cache:" << cachePath;
            QFile::remove(cachePath);
        }
    }

    QImage image(realPath);
    if (image.isNull()) {
        qWarning() << "Failed to load screensaver cover image:" << realPath;
        return QPixmap(); // Return null pixmap on failure.
    }

    pix = QPixmap::fromImage(image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
    const QRect r(0, 0, itemWidth, itemHeight);
    if (pix.width() > itemWidth || pix.height() > itemHeight)
        pix = pix.copy(QRect(pix.rect().center() - r.center(), size));

    if (!pix.save(cachePath, "PNG"))
        qWarning() << "Failed to save screensaver thumbnail to cache:" << cachePath;

    pix.setDevicePixelRatio(ratio);
    return pix;
}
```

Then update your loop to use this helper:

```cpp
for (auto it = paths.begin(); it != paths.end(); ++it) {
    if (!running) // Early break.
        return;

    const QString screensaverName = it.key();
    const QString realPath = it.value();
    if (realPath.isEmpty()) {
        qWarning() << "Skipping thumbnail generation for" << screensaverName << "due to empty path.";
        continue;
    }

    QPixmap pix = loadOrCreateThumbnail(screensaverName, realPath, size, itemWidth, itemHeight, ratio);
    if (!pix.isNull())
        emit pushThumbnail(screensaverName, pix);
}
```

This refactoring isolates the caching logic into its own function, reducing complexity in the loop while keeping functionality unchanged.
created_at: 2025-04-23T05:53:08Z
author_association: NONE
diff_hunk: @@ -437,20 +438,42 @@ void InterfaceWorker::generateThumbnails(const QStringList &paths)
     const int itemWidth = static_cast<int>(LISTVIEW_ICON_WIDTH * ratio);
     const int itemHeight = static_cast<int>(LISTVIEW_ICON_HEIGHT * ratio);
     const QSize size(itemWidth, itemHeight);
path: src/dde-control-center-plugins/wallpapersetting/wallpaper/wallpaperprovider.cpp
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the thumbnail caching logic into helper functions to improve code structure and reduce nesting within the main loop, enhancing readability and maintainability of the code..

Consider abstracting the caching branch logic into helper functions. For example, create one helper to attempt loading a thumbnail from the cache and another to save the generated thumbnail. This will reduce nesting in the main loop and keep the caching concerns isolated. For instance:

```cpp
QPixmap loadThumbnailFromCache(const QString &path, const QSize &size, qreal ratio) {
    const QString cachePath = ThumbnailCacheUtils::getThumbnailCachePath(path, size);
    QFileInfo cacheFile(cachePath);
    QPixmap pix;
    if (cacheFile.exists() && pix.load(cachePath)) {
        pix.setDevicePixelRatio(ratio);
        return pix;
    }
    // Remove invalid cache file.
    QFile::remove(cachePath);
    return QPixmap();
}

bool saveThumbnailToCache(const QPixmap &pix, const QString &path, const QSize &size) {
    const QString cachePath = ThumbnailCacheUtils::getThumbnailCachePath(path, size);
    return pix.save(cachePath, "PNG");
}
```

Then simplify your main loop as follows:

```cpp
for (const QString &path : paths) {
    if (!running)
        return;

    QPixmap pix = loadThumbnailFromCache(path, size, ratio);
    if (!pix.isNull()) {
        emit pushThumbnail(path, true, QVariant::fromValue(pix));
        continue;
    }

    bool pic;
    QVariant val;
    if (generateThumbnail(path, size, pic, val)) {
        if (pic) {
            pix = val.value<QPixmap>();
            pix.setDevicePixelRatio(ratio);
            if (!saveThumbnailToCache(pix, path, size)) {
                qWarning() << "Failed to save thumbnail to cache for" << path;
            }
            emit pushThumbnail(path, true, QVariant::fromValue(pix));
        } else {
            emit pushThumbnail(path, false, val);
        }
    } else {
        qWarning() << "Failed to generate thumbnail for path:" << path;
    }
}
```

This refactoring keeps functionality intact while reducing complexity inside `generateThumbnails`.
created_at: 2025-04-23T05:53:08Z
author_association: NONE

PR comments:
user: login: deepin-ci-robot
created_at: 2025-04-23T05:52:26Z
body: ### deepin pr auto review
代码审查意见：

1. **文件哈希计算**:
   - `calculateFileHash` 函数中，如果文件为空，直接返回空字符串是合理的，但建议添加注释说明为什么在这种情况下返回空字符串。
   - 使用 `QCryptographicHash::Md5` 作为哈希算法是合理的，但应确保所有使用此函数的地方都一致。

2. **缓存路径获取**:
   - `getThumbnailCachePath` 函数中，如果 `cacheBaseDir` 为空，返回空字符串是合理的，但应确保调用者能够正确处理这种情况。
   - 在创建缓存目录时，即使创建失败也继续执行后续操作，这可能会导致后续保存操作失败，但尝试加载可能仍然有用。这种设计是合理的，但应确保调用者了解这一点。

3. **缩略图生成**:
   - 在 `ScreensaverWorker` 和 `InterfaceWorker` 类的 `generateThumbnail` 方法中，添加了对 `running` 变量的检查，以支持提前终止操作。这是一个好的实践，但应确保 `running` 变量在所有相关方法中都被正确更新。

4. **缓存加载和保存**:
   - 在加载缓存文件时，如果文件损坏，尝试删除并继续生成新的缩略图。这是一个合理的错误处理策略，但应确保删除操作不会影响其他正常操作。

5. **代码重复**:
   - `ScreensaverWorker` 和 `InterfaceWorker` 类中的 `generateThumbnail` 方法有很多相似的操作，可以考虑将公共逻辑提取到一个单独的函数中，以减少代码重复。

6. **错误处理**:
   - 在加载和保存缩略图时，如果操作失败，应该有更详细的错误处理，例如记录错误日志或通知用户。

7. **性能优化**:
   - 在 `calculateFileHash` 函数中，使用 `constexpr` 定义缓冲区大小可以提高代码的可读性和可维护性。

8. **代码注释**:
   - 代码中缺少一些必要的注释，例如函数的参数和返回值的说明，以及一些关键逻辑的注释。这有助于其他开发者理解代码的意图和实现细节。

9. **命名规范**:
   - 变量和函数的命名应遵循一致的命名规范，例如使用驼峰命名法或下划线命名法。例如，`THUMBNAIL_CACHE_DIR_NAME` 应该使用全大写字母和下划线分隔。

10. **代码风格**:
    - 代码缩进和格式应保持一致，以提高代码的可读性。例如，`if` 语句和循环的缩进应保持一致。

总体来说，代码逻辑清晰，功能实现合理，但存在一些可以改进的地方，例如错误处理、代码重复和代码风格等方面。
author_association: NONE
user: login: sourcery-ai[bot]
created_at: 2025-04-23T05:52:50Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request introduces thumbnail caching for wallpapers and screensavers. It adds a `ThumbnailCacheUtils` class to manage cache paths and file hashing. The wallpaper and screensaver thumbnail generation processes were modified to check for cached thumbnails before generating new ones, improving performance. Error handling and logging were also enhanced.

_No diagrams generated as the changes look simple and do not need a visual representation._

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implemented thumbnail caching for wallpapers and screensavers to improve performance by reusing previously generated thumbnails. | <ul><li>Added `ThumbnailCacheUtils` class with methods for calculating file hashes and determining cache paths.</li><li>Integrated cache loading and saving into wallpaper thumbnail generation.</li><li>Integrated cache loading and saving into screensaver thumbnail generation.</li><li>Improved error handling and logging for thumbnail operations, including handling cache loading failures and file access issues.</li><li>Optimized thumbnail generation by checking the cache for existing thumbnails before generating new ones.</li></ul> | `src/dde-control-center-plugins/wallpapersetting/screensaver/screensaverprovider.cpp`<br/>`src/dde-control-center-plugins/wallpapersetting/wallpaper/wallpaperprovider.cpp`<br/>`src/dde-control-center-plugins/common/thumbnailcacheutils.cpp`<br/>`src/dde-control-center-plugins/common/thumbnailcacheutils.h` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: deepin-ci-robot
created_at: 2025-04-23T08:36:32Z
body: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/linuxdeepin/dde-file-manager-external/pull/16#pullrequestreview-2786432329" title="Approved">liyigang1</a>*, *<a href="https://github.com/linuxdeepin/dde-file-manager-external/pull/16#pullrequestreview-2786107852" title="Approved">max-lvs</a>*, *<a href="https://github.com/linuxdeepin/dde-file-manager-external/pull/16#" title="Author self-approved">pppanghu77</a>*


The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=linuxdeepin%2Fdde-file-manager-external).

<details open>
Needs approval from an approver in each of these files:

- **[debian/deepin/OWNERS](https://github.com/linuxdeepin/dde-file-manager-external/blob/master/debian/deepin/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE