Merge pull request #9 from cutefishos/thumbnailer

Thumbnailer
This commit is contained in:
Reion Wong 2021-07-25 03:51:27 +08:00 committed by GitHub
commit b18a84a493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 330 additions and 62 deletions

View file

@ -39,6 +39,7 @@ add_executable(cutefish-filemanager
helper/pathhistory.cpp helper/pathhistory.cpp
helper/fm.cpp helper/fm.cpp
helper/shortcut.cpp helper/shortcut.cpp
helper/thumbnailerjob.cpp
desktopiconprovider.cpp desktopiconprovider.cpp

View file

@ -21,6 +21,8 @@
#include <KIO/PreviewJob> #include <KIO/PreviewJob>
#include "thumbnailerjob.h"
#include <QDebug> #include <QDebug>
#include <QImage> #include <QImage>
@ -34,20 +36,35 @@ AsyncImageResponse::AsyncImageResponse(const QString &id, const QSize &requested
: m_id(id) : m_id(id)
, m_requestedSize(requestedSize) , m_requestedSize(requestedSize)
{ {
QStringList plugins = KIO::PreviewJob::defaultPlugins(); auto job_ = new ThumbnailerJob(QUrl::fromUserInput(id).toLocalFile(), requestedSize);
auto job = new KIO::PreviewJob(KFileItemList() << KFileItem(QUrl::fromUserInput(id)), requestedSize, &plugins); connect(job_, &ThumbnailerJob::gotPreview, [this] (QPixmap pixmap) {
connect(job, &KIO::PreviewJob::gotPreview, [this](KFileItem, QPixmap pixmap) {
m_image = pixmap.toImage(); m_image = pixmap.toImage();
emit this->finished(); emit this->finished();
}); });
connect(job, &KIO::PreviewJob::failed, [this](KFileItem) { connect(job_, &ThumbnailerJob::failed, [this] {
emit this->cancel(); emit this->cancel();
emit this->finished(); emit this->finished();
}); });
job->start(); job_->start();
// QStringList plugins = KIO::PreviewJob::defaultPlugins();
// auto job = new KIO::PreviewJob(KFileItemList() << KFileItem(QUrl::fromUserInput(id)), requestedSize, &plugins);
// connect(job, &KIO::PreviewJob::gotPreview, [this](KFileItem, QPixmap pixmap) {
// m_image = pixmap.toImage();
// emit this->finished();
// });
// connect(job, &KIO::PreviewJob::failed, [this](KFileItem) {
// emit this->cancel();
// emit this->finished();
// });
// job->start();
} }
QQuickTextureFactory *AsyncImageResponse::textureFactory() const QQuickTextureFactory *AsyncImageResponse::textureFactory() const

164
helper/thumbnailerjob.cpp Normal file
View file

@ -0,0 +1,164 @@
/*
* Copyright (C) 2021 CutefishOS Team.
*
* Author: Reion Wong <reionwong@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "thumbnailerjob.h"
#include <QStandardPaths>
#include <QCryptographicHash>
#include <QFile>
#include <QDir>
#include <QImage>
#include <QPixmap>
#include <QSaveFile>
#include <QDebug>
#include <QImageReader>
#include <sys/ipc.h>
#include <sys/shm.h>
ThumbnailerJob::ThumbnailerJob(const QString &fileName, const QSize &size, QObject *parent)
: QThread(parent)
, m_url(QUrl::fromUserInput(fileName))
, m_size(size)
, shmaddr(nullptr)
, shmid(-1)
{
// http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY
m_thumbnailsDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/");
}
ThumbnailerJob::~ThumbnailerJob()
{
if (shmaddr) {
shmdt((char *)shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
}
}
void ThumbnailerJob::run()
{
if (!QFile::exists(m_url.toLocalFile())) {
emit failed();
return;
}
int width = m_size.width();
int height = m_size.height();
int cacheSize = 0;
bool needCache = false;
// 首先需要找到目录
if (width <= 128 && height <= 128)
cacheSize = 128;
else if (width <= 256 && height <= 256)
cacheSize = 256;
else
cacheSize = 512;
struct CachePool {
QString path;
int minSize;
};
const static auto pools = {
CachePool{QStringLiteral("/normal/"), 128},
CachePool{QStringLiteral("/large/"), 256},
CachePool{QStringLiteral("/x-large/"), 512},
CachePool{QStringLiteral("/xx-large/"), 1024},
};
QString thumbDir;
int wants = /*devicePixelRatio **/ cacheSize;
for (const auto &p : pools) {
if (p.minSize < wants) {
continue;
} else {
thumbDir = p.path;
break;
}
}
m_thumbnailsPath = m_thumbnailsDir + thumbDir;
// 不存在需要创建路径
if (!QDir(m_thumbnailsPath).exists()) {
if (QDir().mkpath(m_thumbnailsPath)) {
QFile f(m_thumbnailsPath);
f.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); // 0700
}
}
// 求出文件的 md5
QByteArray origName;
const QFileInfo info(m_url.toLocalFile());
const QString canonicalPath = info.canonicalFilePath();
origName = QUrl::fromLocalFile(canonicalPath).toEncoded(QUrl::RemovePassword | QUrl::FullyEncoded);
if (origName.isEmpty()) {
emit failed();
return;
}
QCryptographicHash md5(QCryptographicHash::Md5);
md5.addData(origName);
m_thumbnailsName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png");
// 是否需要生成缓存
needCache = !QFile::exists(m_thumbnailsPath + m_thumbnailsName);
if (needCache) {
QFile f(m_url.toLocalFile());
if (f.open(QIODevice::ReadOnly)) {
QByteArray data = f.readAll();
QImage thumb;
thumb.loadFromData(data);
thumb = thumb.scaled(m_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
// 保存 cache 文件
QSaveFile saveFile(m_thumbnailsPath + m_thumbnailsName);
if (saveFile.open(QIODevice::WriteOnly)) {
if (thumb.save(&saveFile, "PNG")) {
saveFile.commit();
}
}
emitPreview(thumb);
}
} else {
QFile f(m_thumbnailsPath + m_thumbnailsName);
if (f.open(QIODevice::ReadOnly)) {
QByteArray data = f.readAll();
QImage thumb;
thumb.loadFromData(data);
emitPreview(thumb);
}
}
}
void ThumbnailerJob::emitPreview(const QImage &image)
{
QPixmap pixmap;
if (image.width() > m_size.width() || image.height() > m_size.height()) {
pixmap = QPixmap::fromImage(image.scaled(m_size, Qt::KeepAspectRatio, Qt::SmoothTransformation));
} else {
pixmap = QPixmap::fromImage(image);
}
emit gotPreview(pixmap);
}

59
helper/thumbnailerjob.h Normal file
View file

@ -0,0 +1,59 @@
/*
* Copyright (C) 2021 CutefishOS Team.
*
* Author: Reion Wong <reionwong@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef THUMBNAILERJOB_H
#define THUMBNAILERJOB_H
#include <QObject>
#include <QThread>
#include <QSize>
#include <QUrl>
class ThumbnailerJob : public QThread
{
Q_OBJECT
public:
explicit ThumbnailerJob(const QString &fileName, const QSize &size, QObject *parent = nullptr);
~ThumbnailerJob();
void run() override;
signals:
void gotPreview(const QPixmap &pixmap);
void failed();
private:
void emitPreview(const QImage &image);
private:
QUrl m_url;
QSize m_size;
// thumbnail cache folder
QString m_thumbnailsDir;
QString m_thumbnailsPath;
QString m_thumbnailsName;
uchar *shmaddr;
size_t shmsize;
int shmid;
};
#endif // THUMBNAILERJOB_H

View file

@ -41,7 +41,7 @@ Item {
property bool selected: model.selected property bool selected: model.selected
property bool blank: model.blank property bool blank: model.blank
property color hoveredColor: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.backgroundColor, 1.1) property color hoveredColor: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.backgroundColor, 2.3)
: Qt.darker(FishUI.Theme.backgroundColor, 1.05) : Qt.darker(FishUI.Theme.backgroundColor, 1.05)
property color selectedColor: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.backgroundColor, 1.2) property color selectedColor: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.backgroundColor, 1.2)
: Qt.darker(FishUI.Theme.backgroundColor, 1.15) : Qt.darker(FishUI.Theme.backgroundColor, 1.15)

View file

@ -20,6 +20,7 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.0 import Qt.labs.platform 1.0
import Cutefish.FileManager 1.0 as FM import Cutefish.FileManager 1.0 as FM
@ -89,10 +90,26 @@ Item {
Rectangle { Rectangle {
id: _background id: _background
anchors.fill: parent anchors.fill: parent
anchors.rightMargin: FishUI.Units.smallSpacing * 1.5
anchors.bottomMargin: FishUI.Units.smallSpacing * 1.5
radius: FishUI.Theme.smallRadius radius: FishUI.Theme.smallRadius
color: FishUI.Theme.secondBackgroundColor color: FishUI.Theme.secondBackgroundColor
Rectangle {
id: _topRightRect
anchors.right: parent.right
anchors.top: parent.top
height: FishUI.Theme.smallRadius
width: FishUI.Theme.smallRadius
color: FishUI.Theme.secondBackgroundColor
}
Rectangle {
id: _bottomLeftRect
anchors.left: parent.left
anchors.bottom: parent.bottom
height: FishUI.Theme.smallRadius
width: FishUI.Theme.smallRadius
color: FishUI.Theme.secondBackgroundColor
}
} }
Label { Label {
@ -140,7 +157,7 @@ Item {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: FishUI.Theme.smallRadius anchors.bottomMargin: 2
spacing: 0 spacing: 0
Loader { Loader {
@ -161,32 +178,35 @@ Item {
} }
} }
Loader { Item {
Layout.fillWidth: true visible: settings.viewMethod === 0
sourceComponent: _statusBar height: statusBarHeight
} }
} }
function rename() {
_viewLoader.item.rename()
}
Component.onCompleted: {
dirModel.requestRename.connect(rename)
}
Component {
id: _statusBar
Item { Item {
id: _statusBar
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: statusBarHeight height: statusBarHeight
z: 999 z: 999
Rectangle {
anchors.fill: parent
color: FishUI.Theme.backgroundColor
opacity: 0.7
}
MouseArea {
anchors.fill: parent
}
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: FishUI.Units.smallSpacing anchors.leftMargin: FishUI.Units.smallSpacing
anchors.rightMargin: FishUI.Units.largeSpacing anchors.rightMargin: FishUI.Units.smallSpacing
anchors.bottomMargin: 1 // anchors.bottomMargin: 1
spacing: FishUI.Units.largeSpacing spacing: FishUI.Units.largeSpacing
Label { Label {
@ -213,11 +233,18 @@ Item {
text: qsTr("Empty Trash") text: qsTr("Empty Trash")
font.pointSize: 10 font.pointSize: 10
onClicked: dirModel.emptyTrash() onClicked: dirModel.emptyTrash()
visible: dirModel.url === "trash:/" visible: dirModel.url === "trash:///"
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
} }
} }
} }
function rename() {
_viewLoader.item.rename()
}
Component.onCompleted: {
dirModel.requestRename.connect(rename)
} }
Component { Component {
@ -253,7 +280,8 @@ Item {
topMargin: FishUI.Units.smallSpacing topMargin: FishUI.Units.smallSpacing
leftMargin: FishUI.Units.largeSpacing leftMargin: FishUI.Units.largeSpacing
rightMargin: FishUI.Units.largeSpacing + FishUI.Theme.smallRadius rightMargin: FishUI.Units.largeSpacing
bottomMargin: FishUI.Units.largeSpacing
spacing: FishUI.Units.largeSpacing spacing: FishUI.Units.largeSpacing
onCountChanged: { onCountChanged: {

View file

@ -111,7 +111,6 @@ ListView {
id: _label id: _label
text: model.name text: model.name
color: checked ? FishUI.Theme.highlightedTextColor : FishUI.Theme.textColor color: checked ? FishUI.Theme.highlightedTextColor : FishUI.Theme.textColor
elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }